Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -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,
},
},
],
};
4 changes: 4 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -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: ['<rootDir>/jest.setup.js'],
};
13 changes: 13 additions & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
@@ -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(),
}));
1 change: 1 addition & 0 deletions src/components/AddTaskBottomSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down
3 changes: 1 addition & 2 deletions src/components/BottomSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,14 @@ import {
PanResponder,
StyleSheet,
TouchableWithoutFeedback,
Dimensions,
StyleProp,
ViewStyle,
KeyboardAvoidingView,
Platform,
Easing,
} from 'react-native';

const { height: SCREEN_HEIGHT } = Dimensions.get('window');

Comment thread
SM8UTI marked this conversation as resolved.

export interface BottomSheetProps {
/** Height of the bottom sheet */
Expand Down
1 change: 1 addition & 0 deletions src/components/ChooseDestinationBottomSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/GlobalCelebration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default function GlobalCelebration() {
} else {
setShowConfetti(false);
}
}, [completedWhileAway, activeTaskId, tasks]);
}, [completedWhileAway, activeTaskId, tasks, setTaskStatus]);

if (!completedWhileAway) return null;

Expand Down
3 changes: 1 addition & 2 deletions src/components/PrivacyStatus.tsx
Original file line number Diff line number Diff line change
@@ -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(() => {
Expand Down
1 change: 0 additions & 1 deletion src/components/TaskCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
Animated,
PanResponder,
Dimensions,
Image,
Linking,
} from "react-native";
import theme from "../data/color-theme";
Expand Down
4 changes: 1 addition & 3 deletions src/components/TaskDetailsInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
SM8UTI marked this conversation as resolved.
type AdvanceCfg = { label: string; color: string; Icon: React.ReactNode };

const getAdvanceCfg = (status: string): AdvanceCfg => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/YouTubePreview.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down
6 changes: 3 additions & 3 deletions src/context/TimerContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -51,7 +51,7 @@ export function TimerProvider({ children }: { children: ReactNode }) {
if (!data) {
try {
data = JSON.parse(stored);
} catch (e) {
} catch {
data = null;
}
}
Expand Down Expand Up @@ -88,7 +88,7 @@ export function TimerProvider({ children }: { children: ReactNode }) {
}
}
}
} catch (error) { }
} catch { }
};
loadTimer();
}, []);
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useStreak.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export function useStreak(tasks: any[]) {
if (!parsed) {
try {
parsed = JSON.parse(raw) as StreakLog;
} catch (e) {
} catch {
parsed = null;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/layouts/TasksScreen/TaskListContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
17 changes: 4 additions & 13 deletions src/layouts/homeScreen/TodayRecentTasks.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -24,7 +24,7 @@ import { useTaskManager } from "../../hooks/useTaskManager";

function TodayRecentTasks() {
const navigation = useNavigation<any>();
const { tasks, saveNewTask, toggleTaskComplete, deleteTask, advanceTaskStatus } = useTaskManager();
const { tasks, saveNewTask, deleteTask, advanceTaskStatus } = useTaskManager();

const [selectedTask, setSelectedTask] = useState<any | null>(null);
const [sheetVisible, setSheetVisible] = useState(false);
Expand Down Expand Up @@ -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, {
Expand Down
2 changes: 1 addition & 1 deletion src/navigation/TabNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
1 change: 0 additions & 1 deletion src/screens/AnalyticsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
Target,
TrendingUp,
ListTodo,
BarChart2,
} from "lucide-react-native";
import theme from "../data/color-theme";
import { useTaskManager } from "../hooks/useTaskManager";
Expand Down
7 changes: 3 additions & 4 deletions src/screens/BrainDumpScreen.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -40,7 +40,6 @@ const formatDate = (iso: string) => {
export default function BrainDumpScreen() {
const [entries, setEntries] = useState<DumpEntry[]>([]);
const [input, setInput] = useState("");
const [pressingId, setPressingId] = useState<number | null>(null);
const [activeTab, setActiveTab] = useState<"texts" | "links">("texts");

const isLinkEntry = (text: string) => /(https?:\/\/[^\s]+)/g.test(text);
Expand Down
4 changes: 2 additions & 2 deletions src/screens/CalendarScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]}
</Text>
<Text
Expand Down
5 changes: 4 additions & 1 deletion src/screens/FocusScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from "react";
import { View, Text, TouchableOpacity, Dimensions, Animated, StyleSheet, Pressable } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import Svg, { Circle } from "react-native-svg";
import { Info, Play, Pause, RotateCcw, SkipForward, ArrowLeft, Target, CheckCircle2, PartyPopper } from "lucide-react-native";
import { Play, Pause, RotateCcw, SkipForward, ArrowLeft, Target, CheckCircle2, PartyPopper } from "lucide-react-native";
import { useNavigation, useRoute, RouteProp } from "@react-navigation/native";
import ConfettiCannon from "react-native-confetti-cannon";
import theme from "../data/color-theme";
Expand Down Expand Up @@ -85,6 +85,7 @@ export default function FocusScreen() {
cancelScheduledFocusCompletion();
showActiveFocusNotification(taskTitle, true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isActive]);

// Detect completion when timeLeft hits 0 naturally from an active state
Expand All @@ -94,6 +95,7 @@ export default function FocusScreen() {
handleCompletion();
}
prevTimeLeft.current = timeLeft;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [timeLeft, isActive]);

useEffect(() => {
Expand All @@ -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 () => {
Expand Down
5 changes: 3 additions & 2 deletions src/screens/FocusSetupScreen.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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;

Expand Down
3 changes: 1 addition & 2 deletions src/screens/HelpSupportScreen.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -7,7 +7,6 @@ import {
Github,
Globe,
ExternalLink,
HelpCircle,
} from "lucide-react-native";
import theme from "../data/color-theme";

Expand Down
23 changes: 1 addition & 22 deletions src/screens/HomeScreen.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<SafeAreaView
style={{ flex: 1, backgroundColor: theme.background }}
Expand Down
2 changes: 1 addition & 1 deletion src/screens/SettingScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import theme from "../data/color-theme";

// ─── Sub-components ─────────────────────────────────────────────────────────

function SectionHeader({ title, icon: Icon, color }: { title: string; icon: any; color: string }) {
function SectionHeader({ title }: { title: string; icon: any; color: string }) {
return (
<View
style={{
Expand Down
4 changes: 0 additions & 4 deletions src/screens/TaskScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,8 @@ export default function TaskScreen() {
// Task data & CRUD
const {
tasks,
todoCount,
inProgressCount,
completedCount,
saveNewTask,
deleteTask,
toggleTaskComplete,
advanceTaskStatus,
setTaskStatus,
} = useTaskManager();
Expand Down
4 changes: 2 additions & 2 deletions src/utils/youtube.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
export const extractYouTubeId = (text?: string): string | null => {
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();
};