diff --git a/app.json b/app.json index 97cff19..7ae5249 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "simple-notepad", "slug": "simple-notepad", - "version": "1.1.1", + "version": "1.2.0", "orientation": "portrait", "icon": "./assets/images/icon.png", "scheme": "simple-notepad", @@ -25,7 +25,7 @@ "backgroundColor": "#ffffff" }, "package": "com.pgarr.simplenotepad", - "versionCode": 13 + "versionCode": 14 }, "web": { "bundler": "metro", @@ -44,7 +44,7 @@ "projectId": "9e3820b7-558b-4bd2-a1b2-e49561e741e6" } }, - "runtimeVersion": "1.1.1", + "runtimeVersion": "1.2.0", "updates": { "url": "https://u.expo.dev/9e3820b7-558b-4bd2-a1b2-e49561e741e6" } diff --git a/app/add-list.tsx b/app/add-list.tsx index 4367e66..7911e40 100644 --- a/app/add-list.tsx +++ b/app/add-list.tsx @@ -1,45 +1,22 @@ import { HeaderBackButton } from '@/components/navigation/HeaderBackButton'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Text } from '@/components/ui/text'; import { useHardwareBackHandler } from '@/hooks/useHardwareBackHandler'; import { addList, type ListItem } from '@/lib/dataStorage'; import { useSQLiteContext } from 'expo-sqlite'; import { Stack, useRouter } from 'expo-router'; -import { useCallback, useState } from 'react'; -import { KeyboardAvoidingView, Platform, ScrollView } from 'react-native'; +import { useCallback } from 'react'; +import { ListForm } from '@/components/ListForm'; export default function AddListScreen() { const db = useSQLiteContext(); const router = useRouter(); - const [title, setTitle] = useState(''); - const [items, setItems] = useState([]); - const [saving, setSaving] = useState(false); - const handleAddRow = useCallback(() => { - setItems((current) => [...current, { checked: false, text: '' }]); - }, []); - - const handleUpdateRow = useCallback((index: number, text: string) => { - setItems((current) => - current.map((item, currentIndex) => (currentIndex === index ? { ...item, text } : item)) - ); - }, []); - - const handleSave = useCallback(async () => { - const trimmedTitle = title.trim(); - if (!trimmedTitle || saving) return; - const sanitizedItems = items - .map((item) => ({ ...item, text: item.text.trim() })) - .filter((item) => item.text.length > 0); - setSaving(true); - try { - await addList(db, { title: trimmedTitle, items: sanitizedItems }); + const handleSave = useCallback( + async (title: string, items: ListItem[]) => { + await addList(db, { title, items }); router.replace('/'); - } finally { - setSaving(false); - } - }, [db, items, router, saving, title]); + }, + [db, router] + ); useHardwareBackHandler(() => { router.replace('/'); @@ -59,35 +36,7 @@ export default function AddListScreen() { ), }} /> - - - - {items.map((item, index) => ( - handleUpdateRow(index, text)} - editable={!saving} - /> - ))} - - - - + ); } diff --git a/app/edit-list/[id].tsx b/app/edit-list/[id].tsx index 8f343fc..a8314c4 100644 --- a/app/edit-list/[id].tsx +++ b/app/edit-list/[id].tsx @@ -1,9 +1,6 @@ import { HeaderBackButton } from '@/components/navigation/HeaderBackButton'; import { ScreenLoadingState } from '@/components/state/ScreenLoadingState'; import { ScreenNotFoundState } from '@/components/state/ScreenNotFoundState'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Text } from '@/components/ui/text'; import { useHardwareBackHandler } from '@/hooks/useHardwareBackHandler'; import { useParsedNumericRouteParam } from '@/hooks/useParsedNumericRouteParam'; import { @@ -16,7 +13,7 @@ import { import { useSQLiteContext } from 'expo-sqlite'; import { Stack, useRouter } from 'expo-router'; import { useCallback, useEffect, useState } from 'react'; -import { KeyboardAvoidingView, Platform, ScrollView } from 'react-native'; +import { ListForm } from '@/components/ListForm'; type ListEditState = | 'loading' @@ -31,8 +28,7 @@ export default function EditListScreen() { const router = useRouter(); const { rawValue: id, value: listId, isValid: isValidId } = useParsedNumericRouteParam('id'); const [list, setList] = useState('loading'); - const [title, setTitle] = useState(''); - const [items, setItems] = useState([]); + const [saving, setSaving] = useState(false); const backTarget = isValidId ? `/list/${id}` : '/'; @@ -50,8 +46,6 @@ export default function EditListScreen() { } const loadedItems = (await getListItemsById(db, listId)) ?? []; - setTitle(content.title); - setItems(loadedItems); setList({ title: content.title, items: loadedItems }); }, [db, isValidId, listId]); @@ -63,33 +57,26 @@ export default function EditListScreen() { router.replace(backTarget as never); }, [backTarget, router]); - const handleAddRow = useCallback(() => { - setItems((current) => [...current, { checked: false, text: '' }]); - }, []); - - const handleUpdateRow = useCallback((index: number, text: string) => { - setItems((current) => - current.map((item, currentIndex) => (currentIndex === index ? { ...item, text } : item)) - ); - }, []); - - const handleSave = useCallback(async () => { - if (!isValidId || saving) return; - const trimmedTitle = title.trim(); - if (!trimmedTitle) return; - - const sanitizedItems = items - .map((item) => ({ ...item, text: item.text.trim() })) - .filter((item) => item.text.length > 0); - - setSaving(true); - try { - await updateList(db, listId, { title: trimmedTitle, items: sanitizedItems }); - router.replace(`/list/${id}`); - } finally { - setSaving(false); - } - }, [db, id, isValidId, items, listId, router, saving, title]); + const handleSave = useCallback( + async (title: string, items: ListItem[]) => { + if (!isValidId || saving) return; + const trimmedTitle = title.trim(); + if (!trimmedTitle) return; + + const sanitizedItems = items + .map((item) => ({ ...item, text: item.text.trim() })) + .filter((item) => item.text.length > 0); + + setSaving(true); + try { + await updateList(db, listId, { title: trimmedTitle, items: sanitizedItems }); + router.replace(`/list/${id}`); + } finally { + setSaving(false); + } + }, + [db, id, isValidId, listId, router, saving] + ); useHardwareBackHandler(handleBackToPreviousScreen); @@ -108,42 +95,11 @@ export default function EditListScreen() { title: 'Edit list', headerBackVisible: false, headerLeft: () => ( - + ), }} /> - - - - {items.map((item, index) => ( - handleUpdateRow(index, text)} - editable={!saving} - /> - ))} - - - - + ); } diff --git a/app/edit-note/[id].tsx b/app/edit-note/[id].tsx index 2fb092c..00fae00 100644 --- a/app/edit-note/[id].tsx +++ b/app/edit-note/[id].tsx @@ -4,7 +4,7 @@ import { ScreenLoadingState } from '@/components/state/ScreenLoadingState'; import { ScreenNotFoundState } from '@/components/state/ScreenNotFoundState'; import { useHardwareBackHandler } from '@/hooks/useHardwareBackHandler'; import { useParsedNumericRouteParam } from '@/hooks/useParsedNumericRouteParam'; -import { LIST_TYPE, getNoteById, updateNote, type Note } from '@/lib/dataStorage'; +import { NOTE_TYPE, getNoteById, updateNote, type Note } from '@/lib/dataStorage'; import { useSQLiteContext } from 'expo-sqlite'; import { Stack, useRouter } from 'expo-router'; import { useCallback, useEffect, useState } from 'react'; @@ -12,8 +12,7 @@ import { useCallback, useEffect, useState } from 'react'; export default function EditNoteScreen() { const db = useSQLiteContext(); const router = useRouter(); - const { rawValue: id, value: noteId, isValid: isValidId } = - useParsedNumericRouteParam('id'); + const { rawValue: id, value: noteId, isValid: isValidId } = useParsedNumericRouteParam('id'); const [note, setNote] = useState('loading'); const backTarget = isValidId ? `/note/${id}` : '/'; @@ -24,7 +23,7 @@ export default function EditNoteScreen() { return; } const found = await getNoteById(db, noteId); - if (found?.type === LIST_TYPE) { + if (found?.type !== NOTE_TYPE) { setNote(null); return; } @@ -65,18 +64,11 @@ export default function EditNoteScreen() { title: 'Edit note', headerBackVisible: false, headerLeft: () => ( - + ), }} /> - + ); } diff --git a/app/list/[id].tsx b/app/list/[id].tsx index 12b83d1..0255496 100644 --- a/app/list/[id].tsx +++ b/app/list/[id].tsx @@ -12,12 +12,13 @@ import { getNoteById, type ListItem, updateListItems, + deletePosition, } from '@/lib/dataStorage'; import { useSQLiteContext } from 'expo-sqlite'; import { Stack, useRouter } from 'expo-router'; -import { CheckSquare2, PencilIcon, Square } from 'lucide-react-native'; +import { CheckSquare2, PencilIcon, Square, Trash2Icon } from 'lucide-react-native'; import { useCallback, useEffect, useState } from 'react'; -import { Pressable, ScrollView, View } from 'react-native'; +import { Alert, Pressable, ScrollView, View } from 'react-native'; type ListViewState = | 'loading' @@ -70,6 +71,21 @@ export default function ListViewScreen() { [db, listView] ); + const handleDeletePress = useCallback(() => { + if (listView === null || listView === 'loading') return; + Alert.alert('Delete list', 'Are you sure you want to delete this list?', [ + { text: 'No', style: 'cancel' }, + { + text: 'Yes', + style: 'destructive', + onPress: async () => { + await deletePosition(db, listView.id); + router.replace('/'); + }, + }, + ]); + }, [db, listView, router]); + if (listView === 'loading') { return ; } @@ -96,10 +112,16 @@ export default function ListViewScreen() { variant="ghost" size="icon" onPress={() => router.push(`/edit-list/${listView.id}` as never)} - accessibilityLabel="Edit list" - > + accessibilityLabel="Edit list"> + ), }} @@ -111,8 +133,7 @@ export default function ListViewScreen() { className="flex-row items-center gap-3 rounded-md border border-border px-3 py-2" onPress={() => void handleToggleItem(index)} accessibilityRole="checkbox" - accessibilityState={{ checked: item.checked }} - > + accessibilityState={{ checked: item.checked }}> {item.text} diff --git a/app/note/[id].tsx b/app/note/[id].tsx index d0fa896..f6790ac 100644 --- a/app/note/[id].tsx +++ b/app/note/[id].tsx @@ -6,7 +6,7 @@ import { Icon } from '@/components/ui/icon'; import { Text } from '@/components/ui/text'; import { useHardwareBackHandler } from '@/hooks/useHardwareBackHandler'; import { useParsedNumericRouteParam } from '@/hooks/useParsedNumericRouteParam'; -import { LIST_TYPE, deleteNote, getNoteById, type Note } from '@/lib/dataStorage'; +import { LIST_TYPE, deletePosition, getNoteById, type Note } from '@/lib/dataStorage'; import { useSQLiteContext } from 'expo-sqlite'; import { Stack, useRouter } from 'expo-router'; import { PencilIcon, Trash2Icon } from 'lucide-react-native'; @@ -42,21 +42,17 @@ export default function NoteViewScreen() { const handleDeletePress = useCallback(() => { if (note === null || note === 'loading') return; - Alert.alert( - 'Delete note', - 'Are you sure you want to delete this note?', - [ - { text: 'No', style: 'cancel' }, - { - text: 'Yes', - style: 'destructive', - onPress: async () => { - await deleteNote(db, note.id); - router.replace('/'); - }, + Alert.alert('Delete note', 'Are you sure you want to delete this note?', [ + { text: 'No', style: 'cancel' }, + { + text: 'Yes', + style: 'destructive', + onPress: async () => { + await deletePosition(db, note.id); + router.replace('/'); }, - ] - ); + }, + ]); }, [db, note, router]); if (note === 'loading') { @@ -85,16 +81,14 @@ export default function NoteViewScreen() { variant="ghost" size="icon" onPress={() => router.push(`/edit-note/${note.id}`)} - accessibilityLabel="Edit note" - > + accessibilityLabel="Edit note"> diff --git a/components/AddContentDropdown.tsx b/components/AddContentDropdown.tsx index fe7e650..3c5d271 100644 --- a/components/AddContentDropdown.tsx +++ b/components/AddContentDropdown.tsx @@ -1,6 +1,8 @@ import { Button } from '@/components/ui/button'; import { Text } from '@/components/ui/text'; import { Pressable, View } from 'react-native'; +import { Icon } from '@/components/ui/icon'; +import { Pen, SquareCheck } from 'lucide-react-native'; type AddContentDropdownProps = { visible: boolean; @@ -22,9 +24,11 @@ export function AddContentDropdown({ diff --git a/components/ListForm.tsx b/components/ListForm.tsx new file mode 100644 index 0000000..052d8db --- /dev/null +++ b/components/ListForm.tsx @@ -0,0 +1,99 @@ +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Text } from '@/components/ui/text'; +import { useKeyboardOffset } from '@/hooks/useKeyboardOffset'; +import { ListItem } from '@/lib/dataStorage'; +import { useCallback, useState } from 'react'; +import { KeyboardAvoidingView, Platform, ScrollView, View } from 'react-native'; + +type NoteFormProps = { + initialTitle?: string; + initialItems?: ListItem[]; + onSave: (title: string, items: ListItem[]) => Promise; + submitLabel?: string; +}; + +export function ListForm({ + initialTitle = '', + initialItems = [], + onSave, + submitLabel = 'Save', +}: NoteFormProps) { + const [title, setTitle] = useState(initialTitle); + const [items, setItems] = useState(initialItems); + const [saving, setSaving] = useState(false); + + const { paddingBottom } = useKeyboardOffset(); + + const handleAddRow = useCallback(() => { + setItems((current) => [...current, { checked: false, text: '' }]); + }, []); + + const handleUpdateRow = useCallback((index: number, text: string) => { + setItems((current) => + current.map((item, currentIndex) => (currentIndex === index ? { ...item, text } : item)) + ); + }, []); + + const handleSave = useCallback(async () => { + const trimmedTitle = title.trim(); + if (!trimmedTitle) return; + setSaving(true); + try { + await onSave( + trimmedTitle, + items.map((item) => ({ ...item, text: item.text.trim() })) + ); + } finally { + setSaving(false); + } + }, [title, items, onSave]); + + return ( + + + + + + + + {items.map((item, index) => ( + handleUpdateRow(index, text)} + editable={!saving} + /> + ))} + + + + + + + + + ); +} diff --git a/components/NoteForm.tsx b/components/NoteForm.tsx index 004861d..04fdb6d 100644 --- a/components/NoteForm.tsx +++ b/components/NoteForm.tsx @@ -2,18 +2,9 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Text } from '@/components/ui/text'; import { Textarea } from '@/components/ui/textarea'; +import { useKeyboardOffset } from '@/hooks/useKeyboardOffset'; import { useCallback, useEffect, useState } from 'react'; -import { - Keyboard, - KeyboardAvoidingView, - Platform, - ScrollView, - View, -} from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -/** Extra space above Android keyboard for the input accessory bar (emoji, mic, etc.). */ -const ANDROID_KEYBOARD_ACCESSORY_OFFSET = 52; +import { KeyboardAvoidingView, Platform, ScrollView, View } from 'react-native'; type NoteFormProps = { initialTitle?: string; @@ -28,30 +19,16 @@ export function NoteForm({ onSave, submitLabel = 'Save', }: NoteFormProps) { - const insets = useSafeAreaInsets(); const [title, setTitle] = useState(initialTitle); const [noteContent, setNoteContent] = useState(initialContent); const [saving, setSaving] = useState(false); - const [keyboardAccessoryPadding, setKeyboardAccessoryPadding] = useState(0); useEffect(() => { setTitle(initialTitle); setNoteContent(initialContent); }, [initialTitle, initialContent]); - useEffect(() => { - if (Platform.OS !== 'android') return; - const showSub = Keyboard.addListener('keyboardDidShow', () => { - setKeyboardAccessoryPadding(ANDROID_KEYBOARD_ACCESSORY_OFFSET); - }); - const hideSub = Keyboard.addListener('keyboardDidHide', () => { - setKeyboardAccessoryPadding(0); - }); - return () => { - showSub.remove(); - hideSub.remove(); - }; - }, []); + const { paddingBottom } = useKeyboardOffset(); const handleSave = useCallback(async () => { const trimmedTitle = title.trim(); @@ -68,42 +45,38 @@ export function NoteForm({ - - - -