From 72f4a50e2b4a4a646cc0eecc4c74b06c4e47b29c Mon Sep 17 00:00:00 2001 From: le king fu Date: Fri, 20 Feb 2026 20:15:49 -0500 Subject: [PATCH] fix: list navigation, crypto polyfill, SQL transformer, dark mode priorities - Clicking a list now shows its tasks instead of opening new task form - Add list/[id] detail screen - Replace crypto.randomUUID() with expo-crypto (Hermes compatibility) - Add SQL transformer for Drizzle migration files - Improve priority color visibility in dark mode (lighter variants) Co-Authored-By: Claude Opus 4.6 --- app/(tabs)/lists.tsx | 2 +- app/_layout.tsx | 4 + app/list/[id].tsx | 130 +++++++++++++++++++++++++++++++ app/task/[id].tsx | 9 +-- app/task/new.tsx | 11 +-- metro.config.js | 6 ++ package-lock.json | 13 ++++ package.json | 1 + sql-transformer.js | 11 +++ src/components/task/TaskItem.tsx | 12 +-- src/db/repository/lists.ts | 3 +- src/db/repository/tasks.ts | 3 +- src/lib/priority.ts | 29 +++++++ src/lib/uuid.ts | 5 ++ src/theme/colors.ts | 4 + tsconfig.json | 5 +- 16 files changed, 218 insertions(+), 30 deletions(-) create mode 100644 app/list/[id].tsx create mode 100644 sql-transformer.js create mode 100644 src/lib/priority.ts create mode 100644 src/lib/uuid.ts diff --git a/app/(tabs)/lists.tsx b/app/(tabs)/lists.tsx index 6a00577..0ef6590 100644 --- a/app/(tabs)/lists.tsx +++ b/app/(tabs)/lists.tsx @@ -77,7 +77,7 @@ export default function ListsScreen() { contentContainerStyle={{ paddingBottom: 100 }} renderItem={({ item }) => ( router.push(`/task/new?listId=${item.id}`)} + onPress={() => router.push(`/list/${item.id}` as any)} className={`flex-row items-center justify-between border-b px-4 py-4 ${ isDark ? 'border-[#3A3A3A]' : 'border-[#E5E7EB]' }`} diff --git a/app/_layout.tsx b/app/_layout.tsx index 0ebbeed..dc3ff78 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -86,6 +86,10 @@ export default function RootLayout() { name="task/[id]" options={{ headerShown: false }} /> + diff --git a/app/list/[id].tsx b/app/list/[id].tsx new file mode 100644 index 0000000..2b06554 --- /dev/null +++ b/app/list/[id].tsx @@ -0,0 +1,130 @@ +import { useEffect, useState, useCallback } from 'react'; +import { View, Text, FlatList, Pressable, useColorScheme, Alert } from 'react-native'; +import { useRouter, useLocalSearchParams } from 'expo-router'; +import { ArrowLeft, Plus } from 'lucide-react-native'; +import { useTranslation } from 'react-i18next'; +import * as Haptics from 'expo-haptics'; + +import { colors } from '@/src/theme/colors'; +import { useSettingsStore } from '@/src/stores/useSettingsStore'; +import { getTasksByList, toggleComplete, deleteTask } from '@/src/db/repository/tasks'; +import { getAllLists } from '@/src/db/repository/lists'; +import TaskItem from '@/src/components/task/TaskItem'; + +type Task = { + id: string; + title: string; + completed: boolean; + priority: number; + dueDate: Date | null; + position: number; +}; + +export default function ListDetailScreen() { + const { t } = useTranslation(); + const router = useRouter(); + const { id } = useLocalSearchParams<{ id: string }>(); + const [tasks, setTasks] = useState([]); + const [listName, setListName] = useState(''); + const systemScheme = useColorScheme(); + const theme = useSettingsStore((s) => s.theme); + const isDark = (theme === 'system' ? systemScheme : theme) === 'dark'; + + const loadData = useCallback(async () => { + if (!id) return; + const result = await getTasksByList(id); + setTasks(result as Task[]); + + const lists = await getAllLists(); + const list = lists.find((l) => l.id === id); + if (list) { + setListName(list.isInbox ? t('list.inbox') : list.name); + } + }, [id, t]); + + useEffect(() => { + loadData(); + }, [loadData]); + + useEffect(() => { + const interval = setInterval(loadData, 500); + return () => clearInterval(interval); + }, [loadData]); + + const handleToggle = async (taskId: string) => { + await toggleComplete(taskId); + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + loadData(); + }; + + const handleDelete = async (taskId: string) => { + Alert.alert(t('task.deleteConfirm'), '', [ + { text: t('common.cancel'), style: 'cancel' }, + { + text: t('common.delete'), + style: 'destructive', + onPress: async () => { + await deleteTask(taskId); + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + loadData(); + }, + }, + ]); + }; + + return ( + + {/* Header */} + + router.back()} className="mr-3 p-1"> + + + + {listName} + + + + {tasks.length === 0 ? ( + + + {t('empty.list')} + + + ) : ( + item.id} + contentContainerStyle={{ paddingBottom: 100 }} + renderItem={({ item }) => ( + handleToggle(item.id)} + onPress={() => router.push(`/task/${item.id}` as any)} + onDelete={() => handleDelete(item.id)} + /> + )} + /> + )} + + {/* FAB */} + router.push(`/task/new?listId=${id}` as any)} + className="absolute bottom-6 right-6 h-14 w-14 items-center justify-center rounded-full bg-bleu" + style={{ elevation: 4, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.25, shadowRadius: 4 }} + > + + + + ); +} diff --git a/app/task/[id].tsx b/app/task/[id].tsx index e20f6be..bf82de6 100644 --- a/app/task/[id].tsx +++ b/app/task/[id].tsx @@ -17,6 +17,7 @@ import DateTimePicker, { DateTimePickerEvent } from '@react-native-community/dat import { colors } from '@/src/theme/colors'; import { useSettingsStore } from '@/src/stores/useSettingsStore'; +import { getPriorityOptions } from '@/src/lib/priority'; import { getTaskById, updateTask, @@ -26,12 +27,6 @@ import { toggleComplete, } from '@/src/db/repository/tasks'; -const priorityOptions = [ - { value: 0, labelKey: 'priority.none', color: colors.priority.none }, - { value: 1, labelKey: 'priority.low', color: colors.priority.low }, - { value: 2, labelKey: 'priority.medium', color: colors.priority.medium }, - { value: 3, labelKey: 'priority.high', color: colors.priority.high }, -]; type TaskData = { id: string; @@ -198,7 +193,7 @@ export default function TaskDetailScreen() { {t('task.priority')} - {priorityOptions.map((opt) => ( + {getPriorityOptions(isDark).map((opt) => ( setPriority(opt.value)} diff --git a/app/task/new.tsx b/app/task/new.tsx index 5fbce11..213634c 100644 --- a/app/task/new.tsx +++ b/app/task/new.tsx @@ -13,17 +13,10 @@ import { X, Calendar } from 'lucide-react-native'; import { useTranslation } from 'react-i18next'; import DateTimePicker, { DateTimePickerEvent } from '@react-native-community/datetimepicker'; -import { colors } from '@/src/theme/colors'; import { useSettingsStore } from '@/src/stores/useSettingsStore'; import { createTask } from '@/src/db/repository/tasks'; import { getInboxId, getAllLists } from '@/src/db/repository/lists'; - -const priorityOptions = [ - { value: 0, labelKey: 'priority.none', color: colors.priority.none }, - { value: 1, labelKey: 'priority.low', color: colors.priority.low }, - { value: 2, labelKey: 'priority.medium', color: colors.priority.medium }, - { value: 3, labelKey: 'priority.high', color: colors.priority.high }, -]; +import { getPriorityOptions } from '@/src/lib/priority'; export default function NewTaskScreen() { const { t } = useTranslation(); @@ -119,7 +112,7 @@ export default function NewTaskScreen() { {t('task.priority')} - {priorityOptions.map((opt) => ( + {getPriorityOptions(isDark).map((opt) => ( setPriority(opt.value)} diff --git a/metro.config.js b/metro.config.js index 60d28c8..1c88395 100644 --- a/metro.config.js +++ b/metro.config.js @@ -6,4 +6,10 @@ const config = getDefaultConfig(__dirname); // Add SQL extension for Drizzle migrations config.resolver.sourceExts.push("sql"); +// Transform .sql files into JS modules that export the SQL string +config.transformer = { + ...config.transformer, + babelTransformerPath: require.resolve("./sql-transformer.js"), +}; + module.exports = withNativeWind(config, { input: "./src/global.css" }); diff --git a/package-lock.json b/package-lock.json index bd9b03f..b1c5b26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "drizzle-orm": "^0.45.1", "expo": "~54.0.33", "expo-constants": "~18.0.13", + "expo-crypto": "~15.0.8", "expo-font": "~14.0.11", "expo-haptics": "~15.0.8", "expo-linking": "~8.0.11", @@ -5601,6 +5602,18 @@ "react-native": "*" } }, + "node_modules/expo-crypto": { + "version": "15.0.8", + "resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-15.0.8.tgz", + "integrity": "sha512-aF7A914TB66WIlTJvl5J6/itejfY78O7dq3ibvFltL9vnTALJ/7LYHvLT4fwmx9yUNS6ekLBtDGWivFWnj2Fcw==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-file-system": { "version": "19.0.21", "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.21.tgz", diff --git a/package.json b/package.json index 17595d2..c8345b1 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "drizzle-orm": "^0.45.1", "expo": "~54.0.33", "expo-constants": "~18.0.13", + "expo-crypto": "~15.0.8", "expo-font": "~14.0.11", "expo-haptics": "~15.0.8", "expo-linking": "~8.0.11", diff --git a/sql-transformer.js b/sql-transformer.js new file mode 100644 index 0000000..7d8cb32 --- /dev/null +++ b/sql-transformer.js @@ -0,0 +1,11 @@ +const upstreamTransformer = require("@expo/metro-config/babel-transformer"); +const fs = require("fs"); + +module.exports.transform = async ({ src, filename, options }) => { + if (filename.endsWith(".sql")) { + const sql = fs.readFileSync(filename, "utf8"); + const code = `export default ${JSON.stringify(sql)};`; + return upstreamTransformer.transform({ src: code, filename, options }); + } + return upstreamTransformer.transform({ src, filename, options }); +}; diff --git a/src/components/task/TaskItem.tsx b/src/components/task/TaskItem.tsx index fbfdce7..f52a91e 100644 --- a/src/components/task/TaskItem.tsx +++ b/src/components/task/TaskItem.tsx @@ -5,13 +5,7 @@ import { fr, enUS } from 'date-fns/locale'; import { useTranslation } from 'react-i18next'; import { colors } from '@/src/theme/colors'; - -const priorityColors = [ - colors.priority.none, - colors.priority.low, - colors.priority.medium, - colors.priority.high, -]; +import { getPriorityColor } from '@/src/lib/priority'; interface TaskItemProps { task: { @@ -43,7 +37,7 @@ export default function TaskItem({ task, isDark, onToggle, onPress, onDelete }: onPress={onToggle} className={`mr-3 h-6 w-6 items-center justify-center rounded-full border-2`} style={{ - borderColor: task.completed ? colors.bleu.DEFAULT : priorityColors[task.priority] ?? colors.priority.none, + borderColor: task.completed ? colors.bleu.DEFAULT : getPriorityColor(task.priority, isDark), backgroundColor: task.completed ? colors.bleu.DEFAULT : 'transparent', }} > @@ -79,7 +73,7 @@ export default function TaskItem({ task, isDark, onToggle, onPress, onDelete }: {task.priority > 0 && ( )} diff --git a/src/db/repository/lists.ts b/src/db/repository/lists.ts index 1a8952a..4eee9a0 100644 --- a/src/db/repository/lists.ts +++ b/src/db/repository/lists.ts @@ -1,6 +1,7 @@ import { eq } from 'drizzle-orm'; import { db } from '../client'; import { lists } from '../schema'; +import { randomUUID } from '@/src/lib/uuid'; const INBOX_ID = '00000000-0000-0000-0000-000000000001'; @@ -30,7 +31,7 @@ export async function getAllLists() { export async function createList(name: string, color?: string) { const now = new Date(); - const id = crypto.randomUUID(); + const id = randomUUID(); const allLists = await getAllLists(); const maxPosition = allLists.reduce((max, l) => Math.max(max, l.position), 0); diff --git a/src/db/repository/tasks.ts b/src/db/repository/tasks.ts index 5d97f40..a6e56d9 100644 --- a/src/db/repository/tasks.ts +++ b/src/db/repository/tasks.ts @@ -1,6 +1,7 @@ import { eq, and, isNull, desc, asc } from 'drizzle-orm'; import { db } from '../client'; import { tasks } from '../schema'; +import { randomUUID } from '@/src/lib/uuid'; export async function getTasksByList(listId: string) { return db @@ -32,7 +33,7 @@ export async function createTask(data: { parentId?: string; }) { const now = new Date(); - const id = crypto.randomUUID(); + const id = randomUUID(); const siblings = data.parentId ? await getSubtasks(data.parentId) diff --git a/src/lib/priority.ts b/src/lib/priority.ts new file mode 100644 index 0000000..eeda797 --- /dev/null +++ b/src/lib/priority.ts @@ -0,0 +1,29 @@ +import { colors } from '@/src/theme/colors'; + +const lightColors = [ + colors.priority.none, + colors.priority.low, + colors.priority.medium, + colors.priority.high, +]; + +const darkColors = [ + colors.priority.noneLight, + colors.priority.lowLight, + colors.priority.mediumLight, + colors.priority.highLight, +]; + +export function getPriorityColor(priority: number, isDark: boolean): string { + const palette = isDark ? darkColors : lightColors; + return palette[priority] ?? palette[0]; +} + +export function getPriorityOptions(isDark: boolean) { + return [ + { value: 0, labelKey: 'priority.none', color: getPriorityColor(0, isDark) }, + { value: 1, labelKey: 'priority.low', color: getPriorityColor(1, isDark) }, + { value: 2, labelKey: 'priority.medium', color: getPriorityColor(2, isDark) }, + { value: 3, labelKey: 'priority.high', color: getPriorityColor(3, isDark) }, + ]; +} diff --git a/src/lib/uuid.ts b/src/lib/uuid.ts new file mode 100644 index 0000000..2ce4eff --- /dev/null +++ b/src/lib/uuid.ts @@ -0,0 +1,5 @@ +import * as Crypto from 'expo-crypto'; + +export function randomUUID(): string { + return Crypto.randomUUID(); +} diff --git a/src/theme/colors.ts b/src/theme/colors.ts index ff212ef..08687ff 100644 --- a/src/theme/colors.ts +++ b/src/theme/colors.ts @@ -18,6 +18,10 @@ export const colors = { medium: '#4A90A4', low: '#8BA889', none: '#9CA3AF', + highLight: '#E8A090', + mediumLight: '#7CC0D6', + lowLight: '#B0D4A8', + noneLight: '#C0C7CF', }, light: { background: '#FFF8F0', diff --git a/tsconfig.json b/tsconfig.json index 909e901..2f35dd4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,7 @@ "**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", - "expo-env.d.ts" + "expo-env.d.ts", + "nativewind-env.d.ts" ] -} +} \ No newline at end of file