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:
parent
0526a47900
commit
72f4a50e2b
16 changed files with 218 additions and 30 deletions
|
|
@ -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]'
|
||||
}`}
|
||||
|
|
|
|||
|
|
@ -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
130
app/list/[id].tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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
13
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
11
sql-transformer.js
Normal 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 });
|
||||
};
|
||||
|
|
@ -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) }}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
29
src/lib/priority.ts
Normal 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
5
src/lib/uuid.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import * as Crypto from 'expo-crypto';
|
||||
|
||||
export function randomUUID(): string {
|
||||
return Crypto.randomUUID();
|
||||
}
|
||||
|
|
@ -18,6 +18,10 @@ export const colors = {
|
|||
medium: '#4A90A4',
|
||||
low: '#8BA889',
|
||||
none: '#9CA3AF',
|
||||
highLight: '#E8A090',
|
||||
mediumLight: '#7CC0D6',
|
||||
lowLight: '#B0D4A8',
|
||||
noneLight: '#C0C7CF',
|
||||
},
|
||||
light: {
|
||||
background: '#FFF8F0',
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".expo/types/**/*.ts",
|
||||
"expo-env.d.ts"
|
||||
"expo-env.d.ts",
|
||||
"nativewind-env.d.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue