diff --git a/.eslintrc.js b/.eslintrc.js index 187894b..8d1915f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,4 +1,20 @@ module.exports = { root: true, extends: '@react-native', + rules: { + 'react-native/no-inline-styles': 'off', + 'react/no-unstable-nested-components': ['warn', { allowAsProps: true }], + '@typescript-eslint/no-unused-vars': [ + 'warn', + { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, + ], + }, + overrides: [ + { + files: ['__tests__/**/*', 'jest.setup.js'], + env: { + jest: true, + }, + }, + ], }; diff --git a/jest.config.js b/jest.config.js index 8eb675e..84adba9 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,3 +1,7 @@ module.exports = { preset: 'react-native', + transformIgnorePatterns: [ + 'node_modules/(?!(jest-)?react-native|@react-native|@react-native-community|@react-navigation|lucide-react-native)/', + ], + setupFiles: ['/jest.setup.js'], }; diff --git a/jest.setup.js b/jest.setup.js new file mode 100644 index 0000000..90b13f7 --- /dev/null +++ b/jest.setup.js @@ -0,0 +1,13 @@ +jest.mock('@react-native-async-storage/async-storage', () => + require('@react-native-async-storage/async-storage/jest/async-storage-mock'), +); + +jest.mock('@notifee/react-native', () => + require('@notifee/react-native/jest-mock'), +); + +jest.mock('react-native-share-menu', () => ({ + getInitialShare: jest.fn(() => Promise.resolve(null)), + addNewShareListener: jest.fn(), + clearSharedText: jest.fn(), +})); diff --git a/src/components/AddTaskBottomSheet.tsx b/src/components/AddTaskBottomSheet.tsx index 8dc7a27..38fb629 100644 --- a/src/components/AddTaskBottomSheet.tsx +++ b/src/components/AddTaskBottomSheet.tsx @@ -63,6 +63,7 @@ export default function AddTaskBottomSheet({ visible, onClose, onSave, initialTa } else if (isMounted) { closeSheet(); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [visible, initialTaskData]); const closeSheet = () => { diff --git a/src/components/BottomSheet.tsx b/src/components/BottomSheet.tsx index 36dbfd1..4a97237 100644 --- a/src/components/BottomSheet.tsx +++ b/src/components/BottomSheet.tsx @@ -12,7 +12,6 @@ import { PanResponder, StyleSheet, TouchableWithoutFeedback, - Dimensions, StyleProp, ViewStyle, KeyboardAvoidingView, @@ -20,7 +19,7 @@ import { Easing, } from 'react-native'; -const { height: SCREEN_HEIGHT } = Dimensions.get('window'); + export interface BottomSheetProps { /** Height of the bottom sheet */ diff --git a/src/components/ChooseDestinationBottomSheet.tsx b/src/components/ChooseDestinationBottomSheet.tsx index eb26118..560c7bd 100644 --- a/src/components/ChooseDestinationBottomSheet.tsx +++ b/src/components/ChooseDestinationBottomSheet.tsx @@ -40,6 +40,7 @@ export default function ChooseDestinationBottomSheet({ visible, onClose, onSelec } else if (isMounted) { closeSheet(); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [visible]); const closeSheet = () => { diff --git a/src/components/GlobalCelebration.tsx b/src/components/GlobalCelebration.tsx index 9b039d6..6e5c741 100644 --- a/src/components/GlobalCelebration.tsx +++ b/src/components/GlobalCelebration.tsx @@ -27,7 +27,7 @@ export default function GlobalCelebration() { } else { setShowConfetti(false); } - }, [completedWhileAway, activeTaskId, tasks]); + }, [completedWhileAway, activeTaskId, tasks, setTaskStatus]); if (!completedWhileAway) return null; diff --git a/src/components/PrivacyStatus.tsx b/src/components/PrivacyStatus.tsx index f024e1d..bf2c30f 100644 --- a/src/components/PrivacyStatus.tsx +++ b/src/components/PrivacyStatus.tsx @@ -1,10 +1,9 @@ import React from 'react'; import { View, Text, StyleSheet, Animated } from 'react-native'; -import { Shield, ShieldCheck, Lock } from 'lucide-react-native'; +import { ShieldCheck } from 'lucide-react-native'; import theme from '../data/color-theme'; export const PrivacyStatus = () => { - const [isSecure, setIsSecure] = React.useState(true); const fadeAnim = React.useRef(new Animated.Value(0)).current; React.useEffect(() => { diff --git a/src/components/TaskCard.tsx b/src/components/TaskCard.tsx index 82ab278..17069b9 100644 --- a/src/components/TaskCard.tsx +++ b/src/components/TaskCard.tsx @@ -6,7 +6,6 @@ import { Animated, PanResponder, Dimensions, - Image, Linking, } from "react-native"; import theme from "../data/color-theme"; diff --git a/src/components/TaskDetailsInfo.tsx b/src/components/TaskDetailsInfo.tsx index 9c39f51..bf94850 100644 --- a/src/components/TaskDetailsInfo.tsx +++ b/src/components/TaskDetailsInfo.tsx @@ -19,9 +19,7 @@ import { extractYouTubeId, hideYouTubeUrl } from "../utils/youtube"; import YouTubePreview from "./YouTubePreview"; // ─── Status cycling helpers ──────────────────────────────────────────────────── -const STATUS_ORDER = ["to-do", "in-progress", "completed"] as const; -type TaskStatus = (typeof STATUS_ORDER)[number]; - +// Not using STATUS_ORDER and TaskStatus here as it is only used locally as array iteration keys type AdvanceCfg = { label: string; color: string; Icon: React.ReactNode }; const getAdvanceCfg = (status: string): AdvanceCfg => { diff --git a/src/components/YouTubePreview.tsx b/src/components/YouTubePreview.tsx index 56c705e..18d643e 100644 --- a/src/components/YouTubePreview.tsx +++ b/src/components/YouTubePreview.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from "react"; import { View, Text, Image, Pressable, Linking } from "react-native"; -import { Play, Youtube } from "lucide-react-native"; +import { Play } from "lucide-react-native"; import theme from "../data/color-theme"; type Props = { diff --git a/src/context/TimerContext.tsx b/src/context/TimerContext.tsx index 0c5bb42..fbd367f 100644 --- a/src/context/TimerContext.tsx +++ b/src/context/TimerContext.tsx @@ -38,7 +38,7 @@ export function TimerProvider({ children }: { children: ReactNode }) { const dataToSave = customState || stateRef.current; const encrypted = encryptObject(dataToSave); await AsyncStorage.setItem(TIMER_STORAGE_KEY, encrypted); - } catch (error) { } + } catch { } }; // Initial load @@ -51,7 +51,7 @@ export function TimerProvider({ children }: { children: ReactNode }) { if (!data) { try { data = JSON.parse(stored); - } catch (e) { + } catch { data = null; } } @@ -88,7 +88,7 @@ export function TimerProvider({ children }: { children: ReactNode }) { } } } - } catch (error) { } + } catch { } }; loadTimer(); }, []); diff --git a/src/hooks/useStreak.ts b/src/hooks/useStreak.ts index b3434c0..dbdea00 100644 --- a/src/hooks/useStreak.ts +++ b/src/hooks/useStreak.ts @@ -70,7 +70,7 @@ export function useStreak(tasks: any[]) { if (!parsed) { try { parsed = JSON.parse(raw) as StreakLog; - } catch (e) { + } catch { parsed = null; } } diff --git a/src/layouts/TasksScreen/TaskListContent.tsx b/src/layouts/TasksScreen/TaskListContent.tsx index a8d76fb..d91b5d5 100644 --- a/src/layouts/TasksScreen/TaskListContent.tsx +++ b/src/layouts/TasksScreen/TaskListContent.tsx @@ -53,7 +53,7 @@ const groupTasksByDate = (tasks: any[]): TaskGroup[] => { // Build groups and sort: Today always first, then ascending by date return Array.from(groupMap.entries()) - .map(([label, tasks]) => ({ label, tasks })) + .map(([label, groupTasks]) => ({ label, tasks: groupTasks })) .sort((a, b) => { const aKey = getLabelSortKey(a.label); const bKey = getLabelSortKey(b.label); diff --git a/src/layouts/homeScreen/TodayRecentTasks.tsx b/src/layouts/homeScreen/TodayRecentTasks.tsx index 635e4af..d18857b 100644 --- a/src/layouts/homeScreen/TodayRecentTasks.tsx +++ b/src/layouts/homeScreen/TodayRecentTasks.tsx @@ -1,5 +1,5 @@ -import React, { useState, useCallback } from "react"; -import { useNavigation, useFocusEffect } from "@react-navigation/native"; +import React, { useState } from "react"; +import { useNavigation } from "@react-navigation/native"; import { Text, View, @@ -24,7 +24,7 @@ import { useTaskManager } from "../../hooks/useTaskManager"; function TodayRecentTasks() { const navigation = useNavigation(); - const { tasks, saveNewTask, toggleTaskComplete, deleteTask, advanceTaskStatus } = useTaskManager(); + const { tasks, saveNewTask, deleteTask, advanceTaskStatus } = useTaskManager(); const [selectedTask, setSelectedTask] = useState(null); const [sheetVisible, setSheetVisible] = useState(false); @@ -56,16 +56,7 @@ function TodayRecentTasks() { } }; - const openTaskSheet = (task: any) => { - setSelectedTask(task); - setSheetVisible(true); - Animated.spring(slideAnim, { - toValue: 1, - useNativeDriver: true, - bounciness: 0, - speed: 14, - }).start(); - }; + // openTaskSheet removed since it's unused const closeTaskSheet = () => { Animated.timing(slideAnim, { diff --git a/src/navigation/TabNavigator.tsx b/src/navigation/TabNavigator.tsx index 9b87895..626eaf4 100644 --- a/src/navigation/TabNavigator.tsx +++ b/src/navigation/TabNavigator.tsx @@ -3,7 +3,7 @@ import HomeScreen from "../screens/HomeScreen"; import TaskScreen from "../screens/TaskScreen"; import BrainDumpScreen from "../screens/BrainDumpScreen"; import theme from "../data/color-theme"; -import { LayoutDashboard, ListTodo, Lightbulb, Settings, Brain } from "lucide-react-native"; +import { LayoutDashboard, ListTodo, Settings, Brain } from "lucide-react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import SettingScreen from "../screens/SettingScreen"; diff --git a/src/screens/AnalyticsScreen.tsx b/src/screens/AnalyticsScreen.tsx index f7d34d2..228d0ab 100644 --- a/src/screens/AnalyticsScreen.tsx +++ b/src/screens/AnalyticsScreen.tsx @@ -8,7 +8,6 @@ import { Target, TrendingUp, ListTodo, - BarChart2, } from "lucide-react-native"; import theme from "../data/color-theme"; import { useTaskManager } from "../hooks/useTaskManager"; diff --git a/src/screens/BrainDumpScreen.tsx b/src/screens/BrainDumpScreen.tsx index 241e471..083fa29 100644 --- a/src/screens/BrainDumpScreen.tsx +++ b/src/screens/BrainDumpScreen.tsx @@ -1,8 +1,8 @@ -import { View, Text, TextInput, ScrollView, Pressable, KeyboardAvoidingView, Platform, Linking, Image, Animated, PanResponder, Dimensions, TouchableOpacity } from "react-native"; +import { View, Text, TextInput, ScrollView, Pressable, KeyboardAvoidingView, Platform, Linking, Animated, PanResponder, Dimensions } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; -import { useState, useCallback, useEffect, useRef } from "react"; +import { useState, useCallback, useRef } from "react"; import AsyncStorage from "@react-native-async-storage/async-storage"; -import { Trash2, Plus, Brain, Sparkles, Play } from "lucide-react-native"; +import { Trash2, Plus, Brain, Sparkles } from "lucide-react-native"; import { useFocusEffect } from "@react-navigation/native"; import { encryptObject, decryptObject } from "../utils/security"; import theme from "../data/color-theme"; @@ -40,7 +40,6 @@ const formatDate = (iso: string) => { export default function BrainDumpScreen() { const [entries, setEntries] = useState([]); const [input, setInput] = useState(""); - const [pressingId, setPressingId] = useState(null); const [activeTab, setActiveTab] = useState<"texts" | "links">("texts"); const isLinkEntry = (text: string) => /(https?:\/\/[^\s]+)/g.test(text); diff --git a/src/screens/CalendarScreen.tsx b/src/screens/CalendarScreen.tsx index e3f07cf..e0d1854 100644 --- a/src/screens/CalendarScreen.tsx +++ b/src/screens/CalendarScreen.tsx @@ -531,8 +531,8 @@ export default function CalendarScreen() { color: theme.text, }} > - {MONTH_NAMES[parseInt(selectedDateKey.split("-")[1]) - 1]}{" "} - {parseInt(selectedDateKey.split("-")[2])},{" "} + {MONTH_NAMES[parseInt(selectedDateKey.split("-")[1], 10) - 1]}{" "} + {parseInt(selectedDateKey.split("-")[2], 10)},{" "} {selectedDateKey.split("-")[0]} { @@ -102,6 +104,7 @@ export default function FocusScreen() { duration: 1000, useNativeDriver: false, }).start(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [timeLeft]); const handleCompletion = async () => { diff --git a/src/screens/FocusSetupScreen.tsx b/src/screens/FocusSetupScreen.tsx index ef46474..9bddddd 100644 --- a/src/screens/FocusSetupScreen.tsx +++ b/src/screens/FocusSetupScreen.tsx @@ -1,5 +1,5 @@ import React, { useState, useRef, useEffect } from "react"; -import { View, Text, TouchableOpacity, ScrollView, TextInput, Animated, KeyboardAvoidingView, Platform, StyleSheet } from "react-native"; +import { View, Text, TouchableOpacity, TextInput, Animated, Platform, StyleSheet } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import { ArrowLeft, Play, Timer } from "lucide-react-native"; import { useNavigation, useRoute } from "@react-navigation/native"; @@ -51,10 +51,11 @@ export default function FocusSetupScreen() { duration: 800, useNativeDriver: true, }).start(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const startFocus = async () => { - const mins = parseInt(duration) || 25; + const mins = parseInt(duration, 10) || 25; let taskColor = null; let taskTitle = null; diff --git a/src/screens/HelpSupportScreen.tsx b/src/screens/HelpSupportScreen.tsx index 880aeff..1db5e63 100644 --- a/src/screens/HelpSupportScreen.tsx +++ b/src/screens/HelpSupportScreen.tsx @@ -1,4 +1,4 @@ -import { View, Text, ScrollView, Pressable, Linking, Image } from "react-native"; +import { View, Text, ScrollView, Pressable, Linking } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import { useNavigation } from "@react-navigation/native"; import { @@ -7,7 +7,6 @@ import { Github, Globe, ExternalLink, - HelpCircle, } from "lucide-react-native"; import theme from "../data/color-theme"; diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index d83696b..fa4f838 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -1,38 +1,17 @@ -import { ScrollView, View, Text } from "react-native"; +import { ScrollView } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import theme from "../data/color-theme"; import HeaderHeroScreen from "../layouts/homeScreen/Header"; import TodayRecentTasks from "../layouts/homeScreen/TodayRecentTasks"; import { useStreak } from "../hooks/useStreak"; import { useTaskManager } from "../hooks/useTaskManager"; -import { useState, useCallback } from "react"; -import { useFocusEffect } from "@react-navigation/native"; -import { WidgetPreview } from "react-native-android-widget"; -import { TaskWidgetAndroid } from "../widget/TaskWidget"; import StartTimerList from "../layouts/homeScreen/StartTimerList"; -import { PrivacyStatus } from "../components/PrivacyStatus"; import WeeklyFocusWidget from "../layouts/homeScreen/WeeklyFocusWidget"; export default function HomeScreen() { const { tasks } = useTaskManager(); const { currentStreak } = useStreak(tasks); - // Get today's recent task or any active task - const activeTasks = tasks.filter(t => !t.isCompleted); - const today = new Date(); - const todayTasks = activeTasks.filter(task => { - if (!task.dueDate) return false; - const taskDate = new Date(task.dueDate); - return ( - taskDate.getDate() === today.getDate() && - taskDate.getMonth() === today.getMonth() && - taskDate.getFullYear() === today.getFullYear() - ); - }); - - // Pick the most relevant task - const recentTask = todayTasks.length > 0 ? todayTasks[0] : (activeTasks.length > 0 ? activeTasks[0] : null); - return ( { if (!text) return null; const urlRegex = - /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|shorts\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/; + /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|shorts\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/; const match = text.match(urlRegex); return match ? match[1] : null; }; export const hideYouTubeUrl = (text: string): string => { const urlRegex = - /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|shorts\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})(?:\S+)?/g; + /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|shorts\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})(?:\S+)?/g; return text.replace(urlRegex, '').trim(); };