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 <noreply@anthropic.com>
This commit is contained in:
le king fu 2026-02-20 20:15:49 -05:00
parent 0526a47900
commit 72f4a50e2b
16 changed files with 218 additions and 30 deletions

View file

@ -77,7 +77,7 @@ export default function ListsScreen() {
contentContainerStyle={{ paddingBottom: 100 }}
renderItem={({ item }) => (
<Pressable
onPress={() => 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]'
}`}

View file

@ -86,6 +86,10 @@ export default function RootLayout() {
name="task/[id]"
options={{ headerShown: false }}
/>
<Stack.Screen
name="list/[id]"
options={{ headerShown: false }}
/>
</Stack>
</ThemeProvider>
</GestureHandlerRootView>

130
app/list/[id].tsx Normal file
View file

@ -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<Task[]>([]);
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 (
<View className={`flex-1 ${isDark ? 'bg-[#1A1A1A]' : 'bg-creme'}`}>
{/* Header */}
<View
className={`flex-row items-center border-b px-4 pb-3 pt-14 ${
isDark ? 'border-[#3A3A3A]' : 'border-[#E5E7EB]'
}`}
>
<Pressable onPress={() => router.back()} className="mr-3 p-1">
<ArrowLeft size={24} color={isDark ? '#F5F5F5' : '#1A1A1A'} />
</Pressable>
<Text
className={`text-lg ${isDark ? 'text-[#F5F5F5]' : 'text-[#1A1A1A]'}`}
style={{ fontFamily: 'Inter_600SemiBold' }}
>
{listName}
</Text>
</View>
{tasks.length === 0 ? (
<View className="flex-1 items-center justify-center px-8">
<Text
className={`text-center text-base ${isDark ? 'text-[#A0A0A0]' : 'text-[#6B6B6B]'}`}
style={{ fontFamily: 'Inter_400Regular' }}
>
{t('empty.list')}
</Text>
</View>
) : (
<FlatList
data={tasks}
keyExtractor={(item) => item.id}
contentContainerStyle={{ paddingBottom: 100 }}
renderItem={({ item }) => (
<TaskItem
task={item}
isDark={isDark}
onToggle={() => handleToggle(item.id)}
onPress={() => router.push(`/task/${item.id}` as any)}
onDelete={() => handleDelete(item.id)}
/>
)}
/>
)}
{/* FAB */}
<Pressable
onPress={() => 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 }}
>
<Plus size={28} color="#FFFFFF" />
</Pressable>
</View>
);
}

View file

@ -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')}
</Text>
<View className="flex-row">
{priorityOptions.map((opt) => (
{getPriorityOptions(isDark).map((opt) => (
<Pressable
key={opt.value}
onPress={() => setPriority(opt.value)}

View file

@ -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')}
</Text>
<View className="flex-row">
{priorityOptions.map((opt) => (
{getPriorityOptions(isDark).map((opt) => (
<Pressable
key={opt.value}
onPress={() => setPriority(opt.value)}

View file

@ -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" });

13
package-lock.json generated
View file

@ -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",

View file

@ -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",

11
sql-transformer.js Normal file
View file

@ -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 });
};

View file

@ -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 && (
<View
className="ml-2 h-2.5 w-2.5 rounded-full"
style={{ backgroundColor: priorityColors[task.priority] }}
style={{ backgroundColor: getPriorityColor(task.priority, isDark) }}
/>
)}

View file

@ -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);

View file

@ -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)

29
src/lib/priority.ts Normal file
View file

@ -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) },
];
}

5
src/lib/uuid.ts Normal file
View file

@ -0,0 +1,5 @@
import * as Crypto from 'expo-crypto';
export function randomUUID(): string {
return Crypto.randomUUID();
}

View file

@ -18,6 +18,10 @@ export const colors = {
medium: '#4A90A4',
low: '#8BA889',
none: '#9CA3AF',
highLight: '#E8A090',
mediumLight: '#7CC0D6',
lowLight: '#B0D4A8',
noneLight: '#C0C7CF',
},
light: {
background: '#FFF8F0',

View file

@ -12,6 +12,7 @@
"**/*.ts",
"**/*.tsx",
".expo/types/**/*.ts",
"expo-env.d.ts"
"expo-env.d.ts",
"nativewind-env.d.ts"
]
}
}