### Initialize Supabase Client Source: https://context7.com/ravenous9099/demo_note_app_sup3/llms.txt Setup the Supabase client with environment variables and AsyncStorage for session persistence. ```typescript // src/config/supabase.ts import { createClient } from '@supabase/supabase-js'; import AsyncStorage from '@react-native-async-storage/async-storage'; const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL!; const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!; export const supabase = createClient(supabaseUrl, supabaseAnonKey, { auth: { storage: AsyncStorage, autoRefreshToken: true, persistSession: true, detectSessionInUrl: false, }, }); ``` -------------------------------- ### Feature Implementation Workflow Source: https://context7.com/ravenous9099/demo_note_app_sup3/llms.txt A structured markdown guide for implementing new features using an AI assistant, including planning, execution, and testing steps. ```markdown # Sử dụng prompt /implement-feature ## Bước 1: Đọc context Đọc các file theo thứ tự: 1. PRD.md — yêu cầu sản phẩm 2. SCREENS.md — flow người dùng 3. DB.md — schema database 4. AGENTS.md — trạng thái hiện tại 5. mistakes.md — lỗi cần tránh ## Bước 2: Tạo plan Tạo file plan-.md: # Plan: [tên feature] ## Mục tiêu [mô tả ngắn] ## Các file cần tạo/sửa - [ ] path/to/file.tsx — mô tả ## Các bước implement 1. [bước 1] 2. [bước 2] ## Test cases - [ ] [test case 1] ## Bước 3: Chờ xác nhận Không code cho đến khi user approve plan. ## Bước 4: Implement - Migration SQL (nếu cần) - Services → Hooks → Components → Screens ## Bước 5: Viết và chạy test npm test -- --watchAll=false ## Bước 6: Wrap up Chạy /wrap-up để cập nhật AGENTS.md ``` -------------------------------- ### Project Directory Structure Source: https://context7.com/ravenous9099/demo_note_app_sup3/llms.txt Overview of the project file organization. ```text src/ screens/ — Các màn hình (mapped bởi expo-router) components/ — UI components tái sử dụng hooks/ — Custom React hooks utils/ — Hàm tiện ích thuần (pure functions) services/ — Tầng truy cập Supabase (CRUD, auth, queries) types/ — TypeScript types và interfaces dùng chung config/ — Cấu hình Supabase client, constants ``` -------------------------------- ### Fetch All Categories Service Source: https://context7.com/ravenous9099/demo_note_app_sup3/llms.txt Retrieves all categories, ordered alphabetically by name. Handles potential Supabase errors. ```typescript // src/services/categories.ts import { supabase } from '../config/supabase'; import { Category } from '../types/category'; export async function getCategories(): Promise { try { const { data, error } = await supabase .from('categories') .select('*') .order('name', { ascending: true }); if (error) { console.error('Failed to fetch categories:', error); return []; } return data ?? []; } catch (err) { console.error('Unexpected error:', err); return []; } } ``` -------------------------------- ### Fetch All Notes Service Source: https://context7.com/ravenous9099/demo_note_app_sup3/llms.txt Retrieves all non-deleted notes, ordered by pin status and update time. Handles potential Supabase errors. ```typescript // src/services/notes.ts import { supabase } from '../config/supabase'; import { Note, NoteInput } from '../types/note'; export async function getNotes(): Promise { try { const { data, error } = await supabase .from('notes') .select('*') .is('deleted_at', null) .order('is_pinned', { ascending: false }) .order('updated_at', { ascending: false }); if (error) { console.error('Failed to fetch notes:', error); return []; } return data ?? []; } catch (err) { console.error('Unexpected error fetching notes:', err); return []; } } ``` -------------------------------- ### Implement Magic Link Authentication Source: https://context7.com/ravenous9099/demo_note_app_sup3/llms.txt Service functions for handling magic link sign-in, sign-out, and session retrieval. ```typescript // src/services/auth.ts import { supabase } from '../config/supabase'; export async function signInWithMagicLink(email: string): Promise<{ error: Error | null }> { try { const { error } = await supabase.auth.signInWithOtp({ email, options: { emailRedirectTo: 'mynotesapp://auth/callback', }, }); if (error) { console.error('Magic link error:', error.message); return { error }; } return { error: null }; } catch (err) { console.error('Unexpected auth error:', err); return { error: err as Error }; } } export async function signOut(): Promise { try { await supabase.auth.signOut(); } catch (err) { console.error('Sign out error:', err); } } export async function getCurrentSession() { const { data: { session } } = await supabase.auth.getSession(); return session; } ``` -------------------------------- ### Unit Test NoteCard with React Native Testing Library Source: https://context7.com/ravenous9099/demo_note_app_sup3/llms.txt Uses Jest and React Native Testing Library to verify component rendering and user interaction handlers. ```typescript // src/components/NoteCard.test.tsx import React from 'react'; import { render, fireEvent } from '@testing-library/react-native'; import { NoteCard } from './NoteCard'; const mockNote = { id: '1', user_id: 'user-1', title: 'Test Note', content: 'This is test content for the note card component.', category_id: 'cat-1', category_name: 'Work', is_pinned: false, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-15T10:30:00Z', deleted_at: null, }; describe('NoteCard', () => { it('renders note title and preview', () => { const { getByText } = render( ); expect(getByText('Test Note')).toBeTruthy(); expect(getByText(/This is test content/)).toBeTruthy(); }); it('shows pin icon when note is pinned', () => { const pinnedNote = { ...mockNote, is_pinned: true }; const { getByText } = render( ); expect(getByText('📌')).toBeTruthy(); }); it('displays category badge', () => { const { getByText } = render( ); expect(getByText('Work')).toBeTruthy(); }); it('calls onPress when tapped', () => { const onPress = jest.fn(); const { getByLabelText } = render( ); fireEvent.press(getByLabelText('Ghi chú: Test Note')); expect(onPress).toHaveBeenCalledTimes(1); }); it('calls onLongPress when long pressed', () => { const onLongPress = jest.fn(); const { getByLabelText } = render( ); fireEvent(getByLabelText('Ghi chú: Test Note'), 'longPress'); expect(onLongPress).toHaveBeenCalledTimes(1); }); }); ``` -------------------------------- ### Note Card Component with Stylesheet Source: https://context7.com/ravenous9099/demo_note_app_sup3/llms.txt Displays a single note with its title, preview, category, and date. Handles pinned status and uses `StyleSheet` for styling. Requires `React`, `View`, `Text`, `Pressable` from `react-native`. ```typescript import React from 'react'; import { View, Text, Pressable, StyleSheet } from 'react-native'; import { Note } from '../types/note'; interface NoteCardProps { note: Note; onPress: () => void; onLongPress: () => void; } export function NoteCard({ note, onPress, onLongPress }: NoteCardProps) { const displayTitle = note.title || note.content.split('\n')[0].slice(0, 50); const preview = note.content.slice(0, 100); return ( {note.is_pinned && 📌} {displayTitle} {preview} {note.category_name && ( {note.category_name} )} {new Date(note.updated_at).toLocaleDateString('vi-VN')} ); } const styles = StyleSheet.create({ container: { backgroundColor: '#ffffff', borderRadius: 12, padding: 16, marginHorizontal: 16, marginVertical: 8, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 3, }, header: { flexDirection: 'row', alignItems: 'center', marginBottom: 8, }, pinIcon: { marginRight: 8, }, title: { fontSize: 16, fontWeight: '600', color: '#1a1a1a', flex: 1, }, preview: { fontSize: 14, color: '#666666', lineHeight: 20, marginBottom: 12, }, footer: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, categoryBadge: { backgroundColor: '#e8f4f8', paddingHorizontal: 8, paddingVertical: 4, borderRadius: 4, }, categoryText: { fontSize: 12, color: '#0066cc', }, date: { fontSize: 12, color: '#999999', }, }); ``` -------------------------------- ### Custom Hook for Note Management (useNotes) Source: https://context7.com/ravenous9099/demo_note_app_sup3/llms.txt Manages state and logic for a list of notes, including fetching, saving, deleting, and pinning. Requires `useState`, `useEffect`, `useCallback` from React and note-related services. ```typescript import { useState, useEffect, useCallback } from 'react'; import { Note } from '../types/note'; import { getNotes, saveNote, softDeleteNote, togglePinNote } from '../services/notes'; export function useNotes() { const [notes, setNotes] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const fetchNotes = useCallback(async () => { setIsLoading(true); setError(null); const data = await getNotes(); setNotes(data); setIsLoading(false); }, []); useEffect(() => { fetchNotes(); }, [fetchNotes]); const handleSaveNote = useCallback(async (note: Partial) => { const savedNote = await saveNote(note); if (savedNote) { setNotes(prev => { const index = prev.findIndex(n => n.id === savedNote.id); if (index >= 0) { const updated = [...prev]; updated[index] = savedNote; return updated; } return [savedNote, ...prev]; }); } return savedNote; }, []); const handleDeleteNote = useCallback(async (id: string) => { const success = await softDeleteNote(id); if (success) { setNotes(prev => prev.filter(n => n.id !== id)); } return success; }, []); const handleTogglePin = useCallback(async (id: string, isPinned: boolean) => { const success = await togglePinNote(id, isPinned); if (success) { setNotes(prev => { const updated = prev.map(n => n.id === id ? { ...n, is_pinned: isPinned } : n ); return updated.sort((a, b) => { if (a.is_pinned !== b.is_pinned) return b.is_pinned ? 1 : -1; return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime(); }); }); } return success; }, []); return { notes, isLoading, error, refresh: fetchNotes, saveNote: handleSaveNote, deleteNote: handleDeleteNote, togglePin: handleTogglePin, }; } ``` -------------------------------- ### Git Branching Conventions Source: https://context7.com/ravenous9099/demo_note_app_sup3/llms.txt Naming conventions for creating new branches based on feature, fix, or chore types. ```bash # Format: type/short-kebab-case-description # Types: feat/, fix/, chore/, docs/ # Ví dụ tạo feature branch git checkout -b feat/add-note-categories # Ví dụ tạo fix branch git checkout -b fix/auth-session-expired # Ví dụ tạo chore branch git checkout -b chore/update-dependencies ``` -------------------------------- ### Git Commit Workflow Source: https://context7.com/ravenous9099/demo_note_app_sup3/llms.txt Standardized commands for verifying staged changes and committing using conventional commit message formats. ```bash # 1. Xem staged files git diff --cached --stat # 2. Xem nội dung thay đổi git diff --cached # 3. Commit với conventional format # Format: type(scope): description # Types: feat, fix, refactor, docs, chore, style, test git commit -m "feat(notes): add auto-save with debounce Implement automatic saving when user stops typing. Uses 1 second debounce to reduce API calls." ``` -------------------------------- ### Error Tracking in mistakes.md Source: https://context7.com/ravenous9099/demo_note_app_sup3/llms.txt Format for documenting recurring mistakes to prevent future occurrences. ```markdown # Sử dụng prompt /learn-from-mistake # Format entry trong mistakes.md: | Date | Mistake | Root Cause | Fix / Rule | |------------|----------------------------------|-------------------------------|----------------------------------------| | 2024-01-15 | Called Supabase directly in component | Skipped service layer pattern | Always use services/ for data access | | 2024-01-16 | Used inline styles | Rushed implementation | Use StyleSheet.create at file end | ``` -------------------------------- ### Fetch Note by ID Service Source: https://context7.com/ravenous9099/demo_note_app_sup3/llms.txt Retrieves a single note by its ID. Returns null if the note is not found or an error occurs. ```typescript // src/services/notes.ts import { supabase } from '../config/supabase'; import { Note, NoteInput } from '../types/note'; export async function getNoteById(id: string): Promise { try { const { data, error } = await supabase .from('notes') .select('*') .eq('id', id) .single(); if (error) { console.error('Failed to fetch note:', error); return null; } return data; } catch (err) { console.error('Unexpected error:', err); return null; } } ``` -------------------------------- ### Create Category Service Source: https://context7.com/ravenous9099/demo_note_app_sup3/llms.txt Creates a new category with the given name. Returns the created category or null if an error occurs. ```typescript // src/services/categories.ts import { supabase } from '../config/supabase'; import { Category } from '../types/category'; export async function createCategory(name: string): Promise { try { const { data, error } = await supabase .from('categories') .insert({ name }) .select() .single(); if (error) { console.error('Failed to create category:', error); return null; } return data; } catch (err) { console.error('Unexpected error:', err); return null; } } ``` -------------------------------- ### TypeScript Type Definitions Source: https://context7.com/ravenous9099/demo_note_app_sup3/llms.txt Core interfaces for Note and Category entities used throughout the application. ```typescript // src/types/note.ts export interface Note { id: string; user_id: string; title: string | null; content: string; category_id: string | null; category_name?: string; is_pinned: boolean; created_at: string; updated_at: string; deleted_at: string | null; } export type NoteInput = Omit & { id?: string; }; // src/types/category.ts export interface Category { id: string; user_id: string; name: string; created_at: string; } ``` -------------------------------- ### Soft Delete Note Service Source: https://context7.com/ravenous9099/demo_note_app_sup3/llms.txt Performs a soft delete by setting the 'deleted_at' timestamp for a given note ID. Returns true on success, false on failure. ```typescript // src/services/notes.ts import { supabase } from '../config/supabase'; import { Note, NoteInput } from '../types/note'; export async function softDeleteNote(id: string): Promise { try { const { error } = await supabase .from('notes') .update({ deleted_at: new Date().toISOString() }) .eq('id', id); if (error) { console.error('Failed to delete note:', error); return false; } return true; } catch (err) { console.error('Unexpected error deleting note:', err); return false; } } ``` -------------------------------- ### Search Notes Service Source: https://context7.com/ravenous9099/demo_note_app_sup3/llms.txt Searches for non-deleted notes based on a query string that matches titles or content (case-insensitive). Orders results by pin status and update time. ```typescript // src/services/notes.ts import { supabase } from '../config/supabase'; import { Note, NoteInput } from '../types/note'; export async function searchNotes(query: string): Promise { try { const { data, error } = await supabase .from('notes') .select('*') .is('deleted_at', null) .or(`title.ilike.%${query}%,content.ilike.%${query}%`) .order('is_pinned', { ascending: false }) .order('updated_at', { ascending: false }); if (error) { console.error('Search failed:', error); return []; } return data ?? []; } catch (err) { console.error('Unexpected search error:', err); return []; } } ``` -------------------------------- ### Toggle Pin Note Service Source: https://context7.com/ravenous9099/demo_note_app_sup3/llms.txt Toggles the pin status of a note by updating the 'is_pinned' field and 'updated_at' timestamp. Returns true on success, false on failure. ```typescript // src/services/notes.ts import { supabase } from '../config/supabase'; import { Note, NoteInput } from '../types/note'; export async function togglePinNote(id: string, isPinned: boolean): Promise { try { const { error } = await supabase .from('notes') .update({ is_pinned: isPinned, updated_at: new Date().toISOString() }) .eq('id', id); if (error) { console.error('Failed to toggle pin:', error); return false; } return true; } catch (err) { console.error('Unexpected error:', err); return false; } } ``` -------------------------------- ### Save Note Service Source: https://context7.com/ravenous9099/demo_note_app_sup3/llms.txt Saves or updates a note using Supabase's upsert functionality. Updates the 'updated_at' timestamp. ```typescript // src/services/notes.ts import { supabase } from '../config/supabase'; import { Note, NoteInput } from '../types/note'; export async function saveNote(note: NoteInput): Promise { try { const { data, error } = await supabase .from('notes') .upsert({ ...note, updated_at: new Date().toISOString(), }) .select() .single(); if (error) { console.error('Failed to save note:', error); return null; } return data; } catch (err) { console.error('Unexpected error saving note:', err); return null; } } ``` -------------------------------- ### Delete Category Service Source: https://context7.com/ravenous9099/demo_note_app_sup3/llms.txt Deletes a category by its ID. Before deletion, it reassigns notes belonging to this category to 'uncategorized' (null category_id). Returns true on success, false on failure. ```typescript // src/services/categories.ts import { supabase } from '../config/supabase'; import { Category } from '../types/category'; export async function deleteCategory(id: string): Promise { try { // Ghi chú thuộc danh mục này sẽ chuyển thành "chưa phân loại" await supabase .from('notes') .update({ category_id: null }) .eq('category_id', id); const { error } = await supabase .from('categories') .delete() .eq('id', id); if (error) { console.error('Failed to delete category:', error); return false; } return true; } catch (err) { console.error('Unexpected error:', err); return false; } } ``` -------------------------------- ### Implement Auto-save Hook in TypeScript Source: https://context7.com/ravenous9099/demo_note_app_sup3/llms.txt A React hook that triggers an asynchronous save operation using a debounce mechanism to minimize API calls during note editing. ```typescript // src/hooks/useAutoSave.ts import { useRef, useEffect, useCallback } from 'react'; import { saveNote } from '../services/notes'; import { Note } from '../types/note'; export function useAutoSave(note: Partial, debounceMs = 1000) { const timeoutRef = useRef(null); const lastSavedRef = useRef(null); const save = useCallback(async () => { if (!note.id && !note.content) return; const saved = await saveNote(note); if (saved) { lastSavedRef.current = new Date(); } return saved; }, [note]); useEffect(() => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } timeoutRef.current = setTimeout(() => { save(); }, debounceMs); return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, [note.title, note.content, note.category_id, save, debounceMs]); return { lastSaved: lastSavedRef.current, saveNow: save, }; } ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.