diff --git a/android/app/build.gradle b/android/app/build.gradle index fdb89a6..c2c5292 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -92,8 +92,8 @@ android { applicationId 'com.xaviscript.taskeep' minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 1 - versionName "1.0.0" + versionCode 4 + versionName "1.1.0" } signingConfigs { release { diff --git a/android/app/src/main/res/drawable-night-hdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-night-hdpi/splashscreen_logo.png new file mode 100644 index 0000000..c10e3c1 Binary files /dev/null and b/android/app/src/main/res/drawable-night-hdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/drawable-night-mdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-night-mdpi/splashscreen_logo.png new file mode 100644 index 0000000..713da30 Binary files /dev/null and b/android/app/src/main/res/drawable-night-mdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/drawable-night-xhdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-night-xhdpi/splashscreen_logo.png new file mode 100644 index 0000000..d93a227 Binary files /dev/null and b/android/app/src/main/res/drawable-night-xhdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/drawable-night-xxhdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-night-xxhdpi/splashscreen_logo.png new file mode 100644 index 0000000..a1ce0cf Binary files /dev/null and b/android/app/src/main/res/drawable-night-xxhdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/drawable-night-xxxhdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-night-xxxhdpi/splashscreen_logo.png new file mode 100644 index 0000000..66fd449 Binary files /dev/null and b/android/app/src/main/res/drawable-night-xxxhdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/values-night/colors.xml b/android/app/src/main/res/values-night/colors.xml index 3c05de5..f4d7c6a 100644 --- a/android/app/src/main/res/values-night/colors.xml +++ b/android/app/src/main/res/values-night/colors.xml @@ -1 +1,3 @@ - \ No newline at end of file + + #232323 + \ No newline at end of file diff --git a/android/gradle.properties b/android/gradle.properties index ea8e4ae..7784507 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -58,10 +58,10 @@ expo.useLegacyPackaging=false android.packagingOptions.pickFirsts=**/libc++_shared.so expo.sqlite.enableFTS=false expo.sqlite.useSQLCipher=false -# Whether the app is configured to use edge-to-edge via the app config or `react-native-edge-to-edge` plugin -expo.edgeToEdgeEnabled=true MYAPP_UPLOAD_STORE_FILE=taskeep.keystore MYAPP_UPLOAD_KEY_ALIAS=taskeep MYAPP_UPLOAD_STORE_PASSWORD=123?_Tk_?123 -MYAPP_UPLOAD_KEY_PASSWORD=123?_Tk_?123 \ No newline at end of file +MYAPP_UPLOAD_KEY_PASSWORD=123?_Tk_?123 +# Whether the app is configured to use edge-to-edge via the app config or `react-native-edge-to-edge` plugin +expo.edgeToEdgeEnabled=true \ No newline at end of file diff --git a/app.json b/app.json index 16dc3ac..b9bfa4d 100644 --- a/app.json +++ b/app.json @@ -3,7 +3,7 @@ "name": "Taskeep", "main": "index.tsx", "slug": "taskeep", - "version": "1.0.0", + "version": "1.1.0", "orientation": "portrait", "icon": "./assets/images/icon.png", "scheme": "taskeep", @@ -18,7 +18,8 @@ "backgroundColor": "#ffffff" }, "edgeToEdgeEnabled": true, - "package": "com.xaviscript.taskeep" + "package": "com.xaviscript.taskeep", + "versionCode": 4 }, "web": { "bundler": "metro", @@ -33,7 +34,11 @@ "image": "./assets/images/splash-icon.png", "imageWidth": 200, "resizeMode": "contain", - "backgroundColor": "#ffffff" + "backgroundColor": "#ffffff", + "dark": { + "image": "./assets/images/splash-icon.png", + "backgroundColor": "#232323" + } } ], [ diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index ce66021..362bfa6 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -4,6 +4,7 @@ import Ionicons from '@expo/vector-icons/Ionicons'; import { useFocusEffect } from 'expo-router'; import { navigate } from 'expo-router/build/global-state/routing'; import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { PermissionsAndroid, Platform } from 'react-native'; import { TaskItem } from '../../components/taskItem'; import { ThemedText } from '../../components/ThemedText'; @@ -33,6 +34,7 @@ export default function HomeScreen() { const [tasks, setTasks] = useState([]); const [categories, setCategories] = useState([]); const [ready, setReady] = useState(false); + const { t } = useTranslation(); useEffect(() => { // Initial data fetch (async () => { @@ -68,30 +70,13 @@ export default function HomeScreen() { } }, []); - async function schedulePushNotification() { - if (Platform.OS === 'web' || initiated.current) return; - initiated.current = true; - await Notifications.cancelAllScheduledNotificationsAsync(); - await Notifications.dismissAllNotificationsAsync(); - await Notifications.scheduleNotificationAsync({ - content: { - title: `${t("remember_notification_title")} 🌠`, - body: t("remember_notification_body"), - }, - trigger: { - hour: 22, - minute: 0, - repeats: true - }, - }); - } const RefreshTasks = async () => { try { const fetchedTasks = await taskRepository.findAll(); fetchedTasks.sort((a, b) => { return (a.daysToRedo! * 24 * 3600 * 1000 - (Date.now() - a.lastDone!)) - ((b.daysToRedo! * 24 * 3600 * 1000) - (Date.now() - b.lastDone!)); - }); + }) console.log("Tasks fetched:", fetchedTasks); diff --git a/app/categoryForm/index.tsx b/app/categoryForm/index.tsx index e9cfe98..be57729 100644 --- a/app/categoryForm/index.tsx +++ b/app/categoryForm/index.tsx @@ -44,11 +44,11 @@ export default function CategoryForm() { setNewCategory({ ...newCategory, icon: emoji })} > {newCategory.icon != "" ? {newCategory.icon} : } - {newCategory.icon != "" ? "Change" : "Select"} icon + {newCategory.icon != "" ? "Change" : "Select"} icon * (TaskQuery)); const categoryRepository = new CategoryRepository(new SQLiteDataService(CategoryQuery)); +const timeUnits = [ + { label: 'Minutes', value: 'minutes' }, + { label: 'Hours', value: 'hours' }, + { label: 'Days', value: 'days' }, + { label: 'Weeks', value: 'weeks' }, + { label: 'Months', value: 'months' } +]; + +const timeUnitToDays = (num: number, unit: string): number => { + let result = 0; + switch (unit) { // convert to days + case 'minutes': + result = num / 1440; // Convert minutes to days + break; + case 'hours': + result = num / 24; // Convert hours to days + break; + case 'days': + result = num; // Convert days to seconds + break; + case 'weeks': + result = num * 7; // Convert weeks to days + break; + case 'months': + result = num * 30; // Convert months to days (approximation) + break; + } + return result; +} export default function TasksForm() { const { task: taskJson } = useLocalSearchParams(); const task: Task = taskJson ? JSON.parse(taskJson as string) : null; const [categories, setCategories] = useState([]); + const [selectedTimeUnit, setSelectedTimeUnit] = useState("days"); const router = useRouter(); @@ -29,13 +59,35 @@ export default function TasksForm() { category: task?.category || -1, }); + const adaptDaysToGoToUnit = () => { + if (!task) return; + if (task.daysToRedo! <= 1000 * 3600) { + setSelectedTimeUnit("minutes"); + setNewTask({ ...newTask, daysToRedo: task.daysToRedo! * 60 * 24 }); + } + else if (task.daysToRedo! <= 1000 * 3600 * 24) { + setSelectedTimeUnit("hours"); + setNewTask({ ...newTask, daysToRedo: task.daysToRedo! * 24 }); + } else if (task.daysToRedo! <= 1000 * 3600 * 24 * 7) { + setSelectedTimeUnit("days"); + setNewTask({ ...newTask, daysToRedo: task.daysToRedo! }); + } else if (task.daysToRedo! <= 1000 * 3600 * 24 * 30) { + setSelectedTimeUnit("weeks"); + setNewTask({ ...newTask, daysToRedo: task.daysToRedo! / 7 }); + } else { + setSelectedTimeUnit("months"); + setNewTask({ ...newTask, daysToRedo: task.daysToRedo! / 30 }); + } + } + useFocusEffect(React.useCallback(() => { categoryRepository.findAll().then((categories) => { - categories.push(new Category("_create new_", "➕", 0)); + categories.push(new Category("Create new category", "➕", 0)); setCategories(categories); if (newTask.category === 0) { setNewTask({ ...newTask, category: -1 }); } + adaptDaysToGoToUnit(); }).catch((error) => { console.error("Error fetching categories:", error); }); @@ -65,14 +117,47 @@ export default function TasksForm() { value={newTask.title} onChange={(e) => setNewTask({ ...newTask, title: e.nativeEvent.text })} /> - setNewTask({ ...newTask, daysToRedo: e.nativeEvent.text.length > 0 ? parseInt(e.nativeEvent.text) : NaN })} - /> + + unit.value === selectedTimeUnit)?.label} after which task is due *`} + style={styles.inputText} + placeholderTextColor="#333" + keyboardType='numeric' + value={isNaN(newTask.daysToRedo ?? NaN) ? undefined : newTask.daysToRedo?.toString()} + onChange={(e) => setNewTask({ ...newTask, daysToRedo: e.nativeEvent.text.length > 0 ? parseInt(e.nativeEvent.text) : NaN })} + /> + setSelectedTimeUnit(value?.toString() ?? "")} + selectedValue={selectedTimeUnit} + maxSelectableItems={1} + checkboxControls={{ + checkboxLabelStyle: { fontSize: 20, paddingVertical: 10 }, + checkboxStyle: { + backgroundColor: '#e744a9ff', + borderColor: 'transparent', + } + }} + dropdownIconStyle={{ + alignSelf: 'flex-start', + margin: 0, + padding: 0, + marginTop: -30 + }} + dropdownContainerStyle={ + { + flex: 1, + marginTop: -2 + } + } + + dropdownStyle={{ + borderColor: '#ccc', + borderWidth: 2, + backgroundColor: '#fff', + }} + /> + { + + newTask.daysToRedo = timeUnitToDays(newTask.daysToRedo, selectedTimeUnit); if (newTask.title.length > 0 && newTask.daysToRedo > 0 && newTask.category > -1) { if (task && task.id) { // Update existing task @@ -167,8 +258,9 @@ const styles = StyleSheet.create({ position: 'absolute', }, inputText: { + flex: 1, backgroundColor: '#fff', - height: 50, + height: 60, color: '#333333', borderRadius: 8, padding: 10, diff --git a/app/widgets/hello.tsx b/app/widgets/hello.tsx index 09ba0bf..7cf05c7 100644 --- a/app/widgets/hello.tsx +++ b/app/widgets/hello.tsx @@ -5,16 +5,33 @@ import { Category } from '../../models/category'; import { Task } from '../../models/task'; import { GetTaskColor } from '../../utils/colors'; +const adaptDaysToGoToUnit = (t: Task) => { + const timeLeft = t.lastDone! + (t.daysToRedo! * 24 * 60 * 60 * 1000) - Date.now(); + if (timeLeft < 0) return 0; // Task is overdue + else if (timeLeft < 3 * 60 * 60 * 1000) return Math.ceil(timeLeft / (60 * 1000)) + " minutes"; + else if (timeLeft < 24 * 60 * 60 * 1000) return Math.ceil(timeLeft / (3600 * 1000)) + " hours"; + else if (timeLeft < 7 * 24 * 60 * 60 * 1000) return Math.ceil(timeLeft / (24 * 3600 * 1000)) + " days"; + else if (timeLeft < 30 * 24 * 60 * 60 * 1000) return Math.ceil(timeLeft / (7 * 24 * 3600 * 1000)) + " weeks"; + else return Math.ceil(timeLeft / (30 * 24 * 3600 * 1000)) + " months"; +}; + export function HelloWidget({ tasks, categories }: { tasks: Task[], categories?: Category[] }) { const demoTasks: Task[] = [ - new Task('Task 1', 3, 1, new Date().getTime()), - new Task('Task 2', 5, 1, new Date().getTime()), - new Task('Task 2', 5, 1, new Date().getTime()), - new Task('Task 2', 5, 1, new Date().getTime()), - new Task('Task 2', 5, 1, new Date().getTime()) + new Task('Task 1', 1, 1, new Date().getTime() - 1000 * 60 * 60 * 24 * 0, 5), // 2 days ago + new Task('Task 2', 2, 1, new Date().getTime() - 1000 * 60 * 60 * 24), // 1 day ago + new Task('Task 3', 3, 1, new Date().getTime() - 1000 * 60 * 60 * 24 * 2), // 2 days ago + new Task('Task 4', 4, 1, new Date().getTime() - 1000 * 60 * 60 * 24 * 3), // 3 days ago + new Task('Task 5', 5, 1, new Date().getTime() - 1000 * 60 * 60 * 24 * 4), // 4 days ago + new Task('Task 6', 6, 1, new Date().getTime() - 1000 * 60 * 60 * 24 * 10), // 4 days ago ]; + tasks = (tasks ?? demoTasks)?.sort((a, b) => { + const aExpiration = (Date.now() - a.lastDone!) / (a.daysToRedo! * 24 * 60 * 60 * 1000); + const bExpiration = (Date.now() - b.lastDone!) / (b.daysToRedo! * 24 * 60 * 60 * 1000); + return aExpiration - bExpiration; + }).reverse(); + return ( - {(tasks ?? demoTasks).map((task, index) => ( - + {tasks.map((task, index) => { + + const category = categories?.find(c => c.id === task.category); + const timeLeft = adaptDaysToGoToUnit(task) + const circleDegree = parseInt(timeLeft.toString()) > 0 ? ((task.daysToRedo! - (((Date.now() - task.lastDone!) / (24 * 3600 * 1000)))) / task.daysToRedo!) * 360 : 0; + + return ( + width: 'match_parent', + paddingVertical: 8 + }}> - + - c.id === task.category)?.title || 'Uncategorized'} - style={{ - color: '#ffffff', - fontSize: 16, - }} - /> + flexDirection: 'column' + }}> + + + 0 ? timeLeft + " left" : "Overdue"} + style={{ + color: '#ffffff', + fontSize: 16, + }} + /> + + - - - ))} + ) + })} {(tasks && tasks.length === 0) && notify_responseListener = Notifications.addNotificationResponseReceivedListener(response => { }); -const CheckTaskAndNotify = async (): Promise<{ tasks: Task[], categories: Category[] }> => { - tasks = await taskRepository.findAll(); - categories = await categoryRepository.findAll(); +const CheckTaskAndNotify = async (): Promise<{ newTasks: Task[], newCategories: Category[] }> => { + const tasks = await taskRepository.findAll(); + const categories = await categoryRepository.findAll(); const expiredTasks = tasks.filter(task => { const expirationDate = task.lastDone! + (task.daysToRedo! * 24 * 60 * 60 * 1000); // Convert days to milliseconds @@ -74,19 +75,30 @@ const CheckTaskAndNotify = async (): Promise<{ tasks: Task[], categories: Catego } } - return { tasks, categories }; + return { newTasks: tasks, newCategories: categories }; } +const throttledCheckTaskAndNotify = throttle(CheckTaskAndNotify, 1000); // Throttle to prevent too frequent calls + setInterval(async () => { - CheckTaskAndNotify(); + const { newTasks, newCategories } = await throttledCheckTaskAndNotify(); + console.log("Throttled task check and notify executed OUT"); + console.log("New tasks:", newTasks); + console.log("New categories:", newCategories); + tasks = newTasks; + categories = newCategories; }, 1000 * 60 * 30); // Check every 30 minutes export async function widgetTaskHandler(props: WidgetTaskHandlerProps) { async function updateAndRenderWidget() { - const { tasks, categories } = await CheckTaskAndNotify(); - + const { newTasks, newCategories } = await throttledCheckTaskAndNotify(); + tasks = newTasks; + categories = newCategories; + console.log("Throttled task check and notify executed IN"); + console.log("New tasks:", newTasks); + console.log("New categories:", newCategories); props.renderWidget(); } diff --git a/components/taskItem.tsx b/components/taskItem.tsx index 8ab3536..f618157 100644 --- a/components/taskItem.tsx +++ b/components/taskItem.tsx @@ -23,6 +23,16 @@ export type TaskItemProps = ViewProps & { const taskRepository = new TaskRepository(new SQLiteDataService(TaskQuery)); +const adaptDaysToGoToUnit = (t: Task) => { + const timeLeft = t.lastDone! + (t.daysToRedo! * 24 * 60 * 60 * 1000) - Date.now(); + if (timeLeft < 0) return 0; // Task is overdue + else if (timeLeft < 3 * 60 * 60 * 1000) return Math.ceil(timeLeft / (60 * 1000)) + " minutes"; + else if (timeLeft < 24 * 60 * 60 * 1000) return Math.ceil(timeLeft / (3600 * 1000)) + " hours"; + else if (timeLeft < 7 * 24 * 60 * 60 * 1000) return Math.ceil(timeLeft / (24 * 3600 * 1000)) + " days"; + else if (timeLeft < 30 * 24 * 60 * 60 * 1000) return Math.ceil(timeLeft / (7 * 24 * 3600 * 1000)) + " weeks"; + else return Math.ceil(timeLeft / (30 * 24 * 3600 * 1000)) + " months"; +}; + export function TaskItem({ task, categories, onUpdate, lightColor, darkColor, ...otherProps }: TaskItemProps) { const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background'); const router = useRouter(); @@ -79,7 +89,7 @@ export function TaskItem({ task, categories, onUpdate, lightColor, darkColor, .. {task?.title} - {category?.icon + " " + category?.title + " - " + Math.ceil(daysLeft.current / (3600 * 1000))} hours left + {category?.icon + " " + category?.title + " - " + (adaptDaysToGoToUnit(task) == 0 ? "Overdue" : adaptDaysToGoToUnit(task))} { @@ -92,7 +102,7 @@ export function TaskItem({ task, categories, onUpdate, lightColor, darkColor, .. console.error("Error updating task:", error); }); }}> - + 0 ? ((daysLeft.current / (24 * 3600 * 1000)) / task.daysToRedo!) * 360 : 0} width={40} height={40} color={GetTaskColor(task!)} /> ; } diff --git a/i18n.js b/i18n.js new file mode 100644 index 0000000..88f53b6 --- /dev/null +++ b/i18n.js @@ -0,0 +1,23 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import * as RNLocalize from 'react-native-localize'; +import en from './translations/en.json'; + +const resources = { + en: { translation: en }, +}; + +const fallback = { languageTag: 'en', isRTL: false }; + +const { languageTag } = RNLocalize.findBestLanguageTag(Object.keys(resources)) || fallback; + +i18n + .use(initReactI18next) + .init({ + resources, + lng: languageTag, + fallbackLng: 'en', + interpolation: { escapeValue: false }, + }); + +export default i18n; \ No newline at end of file diff --git a/index.tsx b/index.tsx index 5b770ee..f1539ef 100644 --- a/index.tsx +++ b/index.tsx @@ -4,4 +4,5 @@ import { widgetTaskHandler } from './app/widgets/widget-task-handler'; registerWidgetTaskHandler(widgetTaskHandler); import 'expo-router/entry'; +import './i18n.js'; diff --git a/package-lock.json b/package-lock.json index 3e571a6..6997087 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,13 +32,16 @@ "expo-symbols": "~0.4.5", "expo-system-ui": "~5.0.10", "expo-web-browser": "~14.2.0", + "i18next": "^25.3.2", "react": "19.0.0", "react-dom": "19.0.0", + "react-i18next": "^15.6.1", "react-native": "0.79.5", "react-native-android-widget": "^0.17.0", "react-native-emoji-popup": "^0.3.2", "react-native-gesture-handler": "~2.24.0", "react-native-input-select": "^2.1.7", + "react-native-localize": "^3.5.1", "react-native-reanimated": "~3.17.4", "react-native-safe-area-context": "5.4.0", "react-native-screens": "~4.11.1", @@ -8491,6 +8494,15 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -8545,6 +8557,37 @@ "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", "license": "BSD-3-Clause" }, + "node_modules/i18next": { + "version": "25.3.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.2.tgz", + "integrity": "sha512-JSnbZDxRVbphc5jiptxr3o2zocy5dEqpVm9qCGdJwRNO+9saUJS0/u4LnM/13C23fUEWxAylPqKU/NpMV/IjqA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -11870,6 +11913,32 @@ "react": ">=17.0.0" } }, + "node_modules/react-i18next": { + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.6.1.tgz", + "integrity": "sha512-uGrzSsOUUe2sDBG/+FJq2J1MM+Y4368/QW8OLEKSFvnDflHBbZhSd1u3UkW0Z06rMhZmnB/AQrhCpYfE5/5XNg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", @@ -12009,6 +12078,26 @@ "react-native": "*" } }, + "node_modules/react-native-localize": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/react-native-localize/-/react-native-localize-3.5.1.tgz", + "integrity": "sha512-AX2soRR1/BCK2GvzjIpH+0EQaR2acfojFtjAstnYBFPR7ThIQx5Xb+OlqpzYxcYTPDxIcU5i4/yMa23Agy7p8g==", + "license": "MIT", + "peerDependencies": { + "@expo/config-plugins": "^9.0.0 || ^10.0.0", + "react": "*", + "react-native": "*", + "react-native-macos": "*" + }, + "peerDependenciesMeta": { + "@expo/config-plugins": { + "optional": true + }, + "react-native-macos": { + "optional": true + } + } + }, "node_modules/react-native-reanimated": { "version": "3.17.5", "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.17.5.tgz", @@ -14150,7 +14239,7 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -14446,6 +14535,15 @@ "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==", "license": "MIT" }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", diff --git a/package.json b/package.json index 0451f2d..c964bec 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "expo-image": "~2.3.2", "expo-linking": "~7.1.7", "expo-navigation-bar": "~4.2.7", + "expo-notifications": "~0.31.4", "expo-router": "~5.1.3", "expo-splash-screen": "~0.30.10", "expo-sqlite": "~15.2.14", @@ -34,13 +35,16 @@ "expo-symbols": "~0.4.5", "expo-system-ui": "~5.0.10", "expo-web-browser": "~14.2.0", + "i18next": "^25.3.2", "react": "19.0.0", "react-dom": "19.0.0", + "react-i18next": "^15.6.1", "react-native": "0.79.5", "react-native-android-widget": "^0.17.0", "react-native-emoji-popup": "^0.3.2", "react-native-gesture-handler": "~2.24.0", "react-native-input-select": "^2.1.7", + "react-native-localize": "^3.5.1", "react-native-reanimated": "~3.17.4", "react-native-safe-area-context": "5.4.0", "react-native-screens": "~4.11.1", @@ -48,8 +52,7 @@ "react-native-vector-icons": "^10.2.0", "react-native-web": "~0.20.0", "react-native-webview": "13.13.5", - "sqlite": "^5.1.1", - "expo-notifications": "~0.31.4" + "sqlite": "^5.1.1" }, "devDependencies": { "@babel/core": "^7.25.2", diff --git a/translations/en.json b/translations/en.json new file mode 100644 index 0000000..f8b6f2c --- /dev/null +++ b/translations/en.json @@ -0,0 +1,4 @@ +{ + "welcome": "Welcome", + "hello_world": "Hello, World!" +} \ No newline at end of file diff --git a/utils/colors.ts b/utils/colors.ts index 74f8d2e..de7d9d1 100644 --- a/utils/colors.ts +++ b/utils/colors.ts @@ -1,4 +1,4 @@ -import { Task } from "@/models/task"; +import { Task } from "../models/task"; /* #85c75c diff --git a/utils/throttle.ts b/utils/throttle.ts new file mode 100644 index 0000000..9321d6f --- /dev/null +++ b/utils/throttle.ts @@ -0,0 +1,12 @@ +export function throttle any>(fn: T, delay: number): (...args: Parameters) => ReturnType { + let lastCall = 0; + let lastResult: ReturnType; + return function (...args: Parameters): ReturnType { + const now = Date.now(); + if (now - lastCall >= delay) { + lastCall = now; + lastResult = fn.apply(this, args); + } + return lastResult; + }; +}