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;
+ };
+}