diff --git a/app.json b/app.json index bdf0055..4555060 100644 --- a/app.json +++ b/app.json @@ -1,6 +1,6 @@ { "expo": { - "name": "simpl-liste", + "name": "Simpl-Liste", "slug": "simpl-liste", "version": "1.0.0", "orientation": "portrait", @@ -11,26 +11,26 @@ "splash": { "image": "./assets/images/splash-icon.png", "resizeMode": "contain", - "backgroundColor": "#ffffff" + "backgroundColor": "#FFF8F0" }, "ios": { - "supportsTablet": true + "supportsTablet": true, + "bundleIdentifier": "com.lacompagniemaximus.simpliste" }, "android": { + "package": "com.lacompagniemaximus.simpliste", "adaptiveIcon": { "foregroundImage": "./assets/images/adaptive-icon.png", - "backgroundColor": "#ffffff" + "backgroundColor": "#FFF8F0" }, - "edgeToEdgeEnabled": true, - "predictiveBackGestureEnabled": false - }, - "web": { - "bundler": "metro", - "output": "static", - "favicon": "./assets/images/favicon.png" + "edgeToEdgeEnabled": true }, "plugins": [ - "expo-router" + "expo-router", + "expo-sqlite", + "expo-font", + "expo-localization", + "@react-native-community/datetimepicker" ], "experiments": { "typedRoutes": true diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 30914fb..bad222c 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -1,57 +1,54 @@ -import React from 'react'; -import FontAwesome from '@expo/vector-icons/FontAwesome'; -import { Link, Tabs } from 'expo-router'; -import { Pressable } from 'react-native'; +import { Tabs } from 'expo-router'; +import { useColorScheme } from 'react-native'; +import { Inbox, List, Settings } from 'lucide-react-native'; +import { useTranslation } from 'react-i18next'; -import Colors from '@/constants/Colors'; -import { useColorScheme } from '@/components/useColorScheme'; -import { useClientOnlyValue } from '@/components/useClientOnlyValue'; - -// You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/ -function TabBarIcon(props: { - name: React.ComponentProps['name']; - color: string; -}) { - return ; -} +import { colors } from '@/src/theme/colors'; +import { useSettingsStore } from '@/src/stores/useSettingsStore'; export default function TabLayout() { - const colorScheme = useColorScheme(); + const { t } = useTranslation(); + const systemScheme = useColorScheme(); + const theme = useSettingsStore((s) => s.theme); + const isDark = (theme === 'system' ? systemScheme : theme) === 'dark'; return ( + tabBarActiveTintColor: colors.bleu.DEFAULT, + tabBarInactiveTintColor: isDark ? colors.dark.textSecondary : colors.light.textSecondary, + tabBarStyle: { + backgroundColor: isDark ? colors.dark.surface : colors.light.surface, + borderTopColor: isDark ? colors.dark.border : colors.light.border, + }, + headerStyle: { + backgroundColor: isDark ? colors.dark.surface : colors.light.surface, + }, + headerTintColor: isDark ? colors.dark.text : colors.light.text, + headerTitleStyle: { + fontFamily: 'Inter_600SemiBold', + }, + }} + > , - headerRight: () => ( - - - {({ pressed }) => ( - - )} - - - ), + title: t('nav.inbox'), + tabBarIcon: ({ color, size }) => , }} /> , + title: t('nav.lists'), + tabBarIcon: ({ color, size }) => , + }} + /> + , }} /> diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 6cbee6d..1ee075e 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -1,31 +1,105 @@ -import { StyleSheet } from 'react-native'; +import { useEffect, useState, useCallback } from 'react'; +import { View, Text, FlatList, Pressable, useColorScheme, Alert } from 'react-native'; +import { useRouter } from 'expo-router'; +import { Plus } from 'lucide-react-native'; +import { useTranslation } from 'react-i18next'; +import * as Haptics from 'expo-haptics'; -import EditScreenInfo from '@/components/EditScreenInfo'; -import { Text, View } from '@/components/Themed'; +import { colors } from '@/src/theme/colors'; +import { useSettingsStore } from '@/src/stores/useSettingsStore'; +import { getTasksByList, toggleComplete, deleteTask } from '@/src/db/repository/tasks'; +import { getInboxId } 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 InboxScreen() { + const { t } = useTranslation(); + const router = useRouter(); + const [tasks, setTasks] = useState([]); + const systemScheme = useColorScheme(); + const theme = useSettingsStore((s) => s.theme); + const isDark = (theme === 'system' ? systemScheme : theme) === 'dark'; + + const loadTasks = useCallback(async () => { + const result = await getTasksByList(getInboxId()); + setTasks(result as Task[]); + }, []); + + useEffect(() => { + loadTasks(); + }, [loadTasks]); + + useEffect(() => { + const interval = setInterval(loadTasks, 500); + return () => clearInterval(interval); + }, [loadTasks]); + + const handleToggle = async (id: string) => { + await toggleComplete(id); + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + loadTasks(); + }; + + const handleDelete = async (id: string) => { + Alert.alert(t('task.deleteConfirm'), '', [ + { text: t('common.cancel'), style: 'cancel' }, + { + text: t('common.delete'), + style: 'destructive', + onPress: async () => { + await deleteTask(id); + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + loadTasks(); + }, + }, + ]); + }; -export default function TabOneScreen() { return ( - - Tab One - - + + {tasks.length === 0 ? ( + + + {t('empty.inbox')} + + + ) : ( + item.id} + contentContainerStyle={{ paddingBottom: 100 }} + renderItem={({ item }) => ( + handleToggle(item.id)} + onPress={() => router.push(`/task/${item.id}` as any)} + onDelete={() => handleDelete(item.id)} + /> + )} + /> + )} + + router.push('/task/new' 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 }} + > + + ); } - -const styles = StyleSheet.create({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - }, - title: { - fontSize: 20, - fontWeight: 'bold', - }, - separator: { - marginVertical: 30, - height: 1, - width: '80%', - }, -}); diff --git a/app/(tabs)/lists.tsx b/app/(tabs)/lists.tsx new file mode 100644 index 0000000..6a00577 --- /dev/null +++ b/app/(tabs)/lists.tsx @@ -0,0 +1,143 @@ +import { useEffect, useState, useCallback } from 'react'; +import { View, Text, FlatList, Pressable, useColorScheme, TextInput, Alert } from 'react-native'; +import { useRouter } from 'expo-router'; +import { Plus, ChevronRight, Trash2 } from 'lucide-react-native'; +import { useTranslation } from 'react-i18next'; + +import { colors } from '@/src/theme/colors'; +import { useSettingsStore } from '@/src/stores/useSettingsStore'; +import { getAllLists, createList, deleteList } from '@/src/db/repository/lists'; +import { getTasksByList } from '@/src/db/repository/tasks'; + +type ListWithCount = { + id: string; + name: string; + color: string | null; + isInbox: boolean; + taskCount: number; +}; + +export default function ListsScreen() { + const { t } = useTranslation(); + const router = useRouter(); + const [lists, setLists] = useState([]); + const [showNewInput, setShowNewInput] = useState(false); + const [newName, setNewName] = useState(''); + const systemScheme = useColorScheme(); + const theme = useSettingsStore((s) => s.theme); + const isDark = (theme === 'system' ? systemScheme : theme) === 'dark'; + + const loadLists = useCallback(async () => { + const allLists = await getAllLists(); + const withCounts = await Promise.all( + allLists.map(async (list) => { + const tasks = await getTasksByList(list.id); + return { ...list, taskCount: tasks.length }; + }) + ); + setLists(withCounts); + }, []); + + useEffect(() => { + loadLists(); + }, [loadLists]); + + useEffect(() => { + const interval = setInterval(loadLists, 500); + return () => clearInterval(interval); + }, [loadLists]); + + const handleCreateList = async () => { + if (!newName.trim()) return; + await createList(newName.trim()); + setNewName(''); + setShowNewInput(false); + loadLists(); + }; + + const handleDeleteList = (id: string, name: string) => { + Alert.alert(t('list.deleteConfirm'), '', [ + { text: t('common.cancel'), style: 'cancel' }, + { + text: t('common.delete'), + style: 'destructive', + onPress: async () => { + await deleteList(id); + loadLists(); + }, + }, + ]); + }; + + return ( + + item.id} + contentContainerStyle={{ paddingBottom: 100 }} + renderItem={({ item }) => ( + router.push(`/task/new?listId=${item.id}`)} + className={`flex-row items-center justify-between border-b px-4 py-4 ${ + isDark ? 'border-[#3A3A3A]' : 'border-[#E5E7EB]' + }`} + > + + + + {item.isInbox ? t('list.inbox') : item.name} + + + + + {item.taskCount} + + {!item.isInbox && ( + handleDeleteList(item.id, item.name)} className="mr-2 p-1"> + + + )} + + + + )} + ListFooterComponent={ + showNewInput ? ( + + { setShowNewInput(false); setNewName(''); }} + placeholder={t('list.namePlaceholder')} + placeholderTextColor={isDark ? '#A0A0A0' : '#6B6B6B'} + className={`flex-1 rounded-lg border px-3 py-2 text-base ${ + isDark + ? 'border-[#3A3A3A] bg-[#2A2A2A] text-[#F5F5F5]' + : 'border-[#E5E7EB] bg-white text-[#1A1A1A]' + }`} + style={{ fontFamily: 'Inter_400Regular' }} + /> + + ) : null + } + /> + + {/* FAB */} + setShowNewInput(true)} + 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/(tabs)/settings.tsx b/app/(tabs)/settings.tsx new file mode 100644 index 0000000..a470ed3 --- /dev/null +++ b/app/(tabs)/settings.tsx @@ -0,0 +1,147 @@ +import { View, Text, Pressable, useColorScheme } from 'react-native'; +import { useTranslation } from 'react-i18next'; +import { Sun, Moon, Smartphone } from 'lucide-react-native'; +import Constants from 'expo-constants'; + +import { colors } from '@/src/theme/colors'; +import { useSettingsStore } from '@/src/stores/useSettingsStore'; +import i18n from '@/src/i18n'; + +type ThemeMode = 'light' | 'dark' | 'system'; + +export default function SettingsScreen() { + const { t } = useTranslation(); + const systemScheme = useColorScheme(); + const { theme, locale, setTheme, setLocale } = useSettingsStore(); + const isDark = (theme === 'system' ? systemScheme : theme) === 'dark'; + + const themeOptions: { value: ThemeMode; label: string; icon: typeof Sun }[] = [ + { value: 'light', label: t('settings.light'), icon: Sun }, + { value: 'dark', label: t('settings.dark'), icon: Moon }, + { value: 'system', label: t('settings.system'), icon: Smartphone }, + ]; + + const handleLocaleChange = (newLocale: 'fr' | 'en') => { + setLocale(newLocale); + i18n.changeLanguage(newLocale); + }; + + return ( + + {/* Theme Section */} + + + {t('settings.theme')} + + + {themeOptions.map((option) => { + const Icon = option.icon; + const isActive = theme === option.value; + return ( + setTheme(option.value)} + className={`flex-row items-center border-b px-4 py-3.5 ${ + isDark ? 'border-[#3A3A3A]' : 'border-[#E5E7EB]' + } ${isActive ? (isDark ? 'bg-[#3A3A3A]' : 'bg-creme-dark') : ''}`} + > + + + {option.label} + + + ); + })} + + + + {/* Language Section */} + + + {t('settings.language')} + + + {(['fr', 'en'] as const).map((lang) => { + const isActive = locale === lang; + return ( + handleLocaleChange(lang)} + className={`border-b px-4 py-3.5 ${ + isDark ? 'border-[#3A3A3A]' : 'border-[#E5E7EB]' + } ${isActive ? (isDark ? 'bg-[#3A3A3A]' : 'bg-creme-dark') : ''}`} + > + + {lang === 'fr' ? 'Français' : 'English'} + + + ); + })} + + + + {/* About Section */} + + + {t('settings.about')} + + + + Simpl-Liste {t('settings.version')} {Constants.expoConfig?.version ?? '1.0.0'} + + + La Compagnie Maximus + + + + + ); +} diff --git a/app/(tabs)/two.tsx b/app/(tabs)/two.tsx deleted file mode 100644 index f2ea47e..0000000 --- a/app/(tabs)/two.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { StyleSheet } from 'react-native'; - -import EditScreenInfo from '@/components/EditScreenInfo'; -import { Text, View } from '@/components/Themed'; - -export default function TabTwoScreen() { - return ( - - Tab Two - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - }, - title: { - fontSize: 20, - fontWeight: 'bold', - }, - separator: { - marginVertical: 30, - height: 1, - width: '80%', - }, -}); diff --git a/app/+html.tsx b/app/+html.tsx deleted file mode 100644 index cb31090..0000000 --- a/app/+html.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { ScrollViewStyleReset } from 'expo-router/html'; - -// This file is web-only and used to configure the root HTML for every -// web page during static rendering. -// The contents of this function only run in Node.js environments and -// do not have access to the DOM or browser APIs. -export default function Root({ children }: { children: React.ReactNode }) { - return ( - - - - - - - {/* - Disable body scrolling on web. This makes ScrollView components work closer to how they do on native. - However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line. - */} - - - {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */} -