diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..b01a8eb --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,6 @@ +# We put the id's of the commits that we want to ignore in this file, one per line. +# For example: this is a format commit that we don't want to see in the blame output: +2d222c86c982bef96b232e705dd536f4a7cd853b +# Because it doesn't change the code, just the formatting: +# Tell git to use this file for blame by running the following command in your terminal: +# git config blame.ignoreRevsFile .git-blame-ignore-revs \ No newline at end of file diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000..be6b7d8 --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,21 @@ +name: Checks + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + +jobs: + checks: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install dependencies (root) + run: npm install + + - name: Run checks (root script) + run: npm run checks diff --git a/App.tsx b/App.tsx index f93ef5a..33db49d 100644 --- a/App.tsx +++ b/App.tsx @@ -1,14 +1,14 @@ // FILE: App.tsx -import { ThemeProvider } from "@/core/theme/ThemeContext"; -import React, { useEffect } from "react"; -import { StatusBar } from "expo-status-bar"; -import useAppStore from "@/core/store/appStore"; -import { GestureHandlerRootView } from "react-native-gesture-handler"; -import i18n from "@/core/i18n"; -import { useForceUpdate } from "@/core/hooks/useForceUpdate"; -import { I18nManager } from "react-native"; -import { NavigationContainer } from "@react-navigation/native"; -import AppNavigator from "@/navigation/AppNavigator"; +import { ThemeProvider } from '@/core/theme/ThemeContext'; +import React, { useEffect } from 'react'; +import { StatusBar } from 'expo-status-bar'; +import useAppStore from '@/core/store/appStore'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; +import i18n from '@/core/i18n'; +import { useForceUpdate } from '@/core/hooks/useForceUpdate'; +import { I18nManager } from 'react-native'; +import { NavigationContainer } from '@react-navigation/native'; +import AppNavigator from '@/navigation/AppNavigator'; I18nManager.allowRTL(true); @@ -33,7 +33,7 @@ export default function App() { - + diff --git a/eslint.config.js b/eslint.config.js index a726b8a..dc385ba 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,14 +1,14 @@ -const { defineConfig } = require("eslint/config"); -const expoConfig = require("eslint-config-expo/flat"); -const eslintPluginPrettierRecommended = require("eslint-plugin-prettier/recommended"); +const { defineConfig } = require('eslint/config'); +const expoConfig = require('eslint-config-expo/flat'); +const eslintPluginPrettierRecommended = require('eslint-plugin-prettier/recommended'); module.exports = defineConfig([ expoConfig, eslintPluginPrettierRecommended, { rules: { - "prettier/prettier": "warn", + 'prettier/prettier': 'warn', }, - ignores: ["dist/*", "node_modules/*", "/.expo"], + ignores: ['dist/*', 'node_modules/*', '/.expo'], }, ]); diff --git a/index.ts b/index.ts index 828b356..1d6e981 100644 --- a/index.ts +++ b/index.ts @@ -1,6 +1,6 @@ -import { registerRootComponent } from "expo"; +import { registerRootComponent } from 'expo'; -import App from "./App"; +import App from './App'; // registerRootComponent calls AppRegistry.registerComponent('main', () => App); // It also ensures that whether you load the app in Expo Go or in a native build, diff --git a/package.json b/package.json index 2e08f3a..278a240 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,9 @@ "format": "prettier --write .", "checks:lint": "expo lint --fix", "checks:tsc": "tsc --noEmit", + "checks:export": "expo export", "checks:format": "prettier --check .", - "checks": "npm run checks:lint && npm run checks:tsc && npm run checks:format", + "checks": "npm run checks:lint && npm run checks:tsc && npm run checks:export && npm run checks:format", "lint": "expo lint" }, "dependencies": { diff --git a/src/components/FAB.tsx b/src/components/FAB.tsx index a68e7d3..ec0c680 100644 --- a/src/components/FAB.tsx +++ b/src/components/FAB.tsx @@ -1,14 +1,9 @@ // src/components/FAB.tsx -import { AppTheme } from "@/constants/theme"; -import { useTheme } from "@/core/theme/ThemeContext"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import React from "react"; -import { - I18nManager, - StyleSheet, - TouchableOpacity, - TouchableOpacityProps, -} from "react-native"; +import { AppTheme } from '@/constants/theme'; +import { useTheme } from '@/core/theme/ThemeContext'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import React from 'react'; +import { I18nManager, StyleSheet, TouchableOpacity, TouchableOpacityProps } from 'react-native'; export const FAB = (props: TouchableOpacityProps) => { const { theme } = useTheme(); @@ -16,11 +11,7 @@ export const FAB = (props: TouchableOpacityProps) => { return ( - + ); }; @@ -28,16 +19,16 @@ export const FAB = (props: TouchableOpacityProps) => { const getStyles = (theme: AppTheme) => StyleSheet.create({ container: { - position: "absolute", + position: 'absolute', bottom: theme.spacing.l, - [I18nManager.isRTL ? "left" : "right"]: theme.spacing.l, + [I18nManager.isRTL ? 'left' : 'right']: theme.spacing.l, width: 60, height: 60, borderRadius: 16, backgroundColor: theme.colors.primary, - justifyContent: "center", - alignItems: "center", - shadowColor: "#000", + justifyContent: 'center', + alignItems: 'center', + shadowColor: '#000', shadowOffset: { width: 0, height: 2, diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 9d36a9a..e2638d5 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,11 +1,11 @@ // src/components/Header.tsx -import { AppTheme } from "@/constants/theme"; -import { useTheme } from "@/core/theme/ThemeContext"; -import React from "react"; -import { StyleSheet } from "react-native"; -import { EdgeInsets } from "react-native-safe-area-context"; -import { Box } from "./base/Box"; -import { ThemedText } from "./base/ThemedText"; +import { AppTheme } from '@/constants/theme'; +import { useTheme } from '@/core/theme/ThemeContext'; +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { EdgeInsets } from 'react-native-safe-area-context'; +import { Box } from './base/Box'; +import { ThemedText } from './base/ThemedText'; export type HeaderProps = { title: string; @@ -27,9 +27,7 @@ export const Header = ({ return ( - - {renderLeftAction && renderLeftAction()} - + {renderLeftAction && renderLeftAction()} @@ -42,9 +40,7 @@ export const Header = ({ )} - - {renderRightAction && renderRightAction()} - + {renderRightAction && renderRightAction()} ); }; @@ -52,9 +48,9 @@ export const Header = ({ const getStyles = (theme: AppTheme, insets: EdgeInsets) => StyleSheet.create({ container: { - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', paddingHorizontal: theme.spacing.m, height: 60 + insets.top, paddingTop: insets.top, @@ -62,13 +58,13 @@ const getStyles = (theme: AppTheme, insets: EdgeInsets) => }, actionSlot: { width: 40, - justifyContent: "center", - alignItems: "center", + justifyContent: 'center', + alignItems: 'center', }, titleContainer: { flex: 1, - justifyContent: "center", - alignItems: "center", + justifyContent: 'center', + alignItems: 'center', }, title: { fontSize: theme.typography.fontSize.l, diff --git a/src/components/Screen.tsx b/src/components/Screen.tsx index a8eef32..9f78718 100644 --- a/src/components/Screen.tsx +++ b/src/components/Screen.tsx @@ -1,13 +1,13 @@ // src/components/Screen.tsx -import { AppTheme } from "@/constants/theme"; -import { useTheme } from "@/core/theme/ThemeContext"; -import React from "react"; -import { StyleSheet } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { Box } from "./base/Box"; -import { Header, HeaderProps } from "./Header"; +import { AppTheme } from '@/constants/theme'; +import { useTheme } from '@/core/theme/ThemeContext'; +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { Box } from './base/Box'; +import { Header, HeaderProps } from './Header'; -type ScreenProps = Partial> & { +type ScreenProps = Partial> & { children: React.ReactNode; }; diff --git a/src/components/base/Box.tsx b/src/components/base/Box.tsx index cf5caac..7d72e1f 100644 --- a/src/components/base/Box.tsx +++ b/src/components/base/Box.tsx @@ -1,6 +1,6 @@ // src/components/base/Box.tsx -import { View, ViewProps } from "react-native"; -import React from "react"; +import { View, ViewProps } from 'react-native'; +import React from 'react'; // For now, it's a simple pass-through. // It establishes a pattern for a component that will become more powerful. diff --git a/src/components/base/ThemedText.tsx b/src/components/base/ThemedText.tsx index eae1b96..49f7e18 100644 --- a/src/components/base/ThemedText.tsx +++ b/src/components/base/ThemedText.tsx @@ -1,7 +1,7 @@ // src/components/base/ThemedText.tsx -import { useTheme } from "@/core/theme/ThemeContext"; -import React from "react"; -import { Text, TextProps } from "react-native"; +import { useTheme } from '@/core/theme/ThemeContext'; +import React from 'react'; +import { Text, TextProps } from 'react-native'; export const ThemedText = (props: TextProps) => { const { theme } = useTheme(); diff --git a/src/components/base/ThemedTextInput.tsx b/src/components/base/ThemedTextInput.tsx index 66252ef..3c0b43d 100644 --- a/src/components/base/ThemedTextInput.tsx +++ b/src/components/base/ThemedTextInput.tsx @@ -1,8 +1,8 @@ // src/components/base/ThemedTextInput.tsx -import { AppTheme } from "@/constants/theme"; -import { useTheme } from "@/core/theme/ThemeContext"; -import React from "react"; -import { StyleSheet, TextInput, TextInputProps } from "react-native"; +import { AppTheme } from '@/constants/theme'; +import { useTheme } from '@/core/theme/ThemeContext'; +import React from 'react'; +import { StyleSheet, TextInput, TextInputProps } from 'react-native'; export const ThemedTextInput = (props: TextInputProps) => { const { theme } = useTheme(); diff --git a/src/constants/colors.ts b/src/constants/colors.ts index cf9b533..f48f72b 100644 --- a/src/constants/colors.ts +++ b/src/constants/colors.ts @@ -2,28 +2,28 @@ const palette = { // Grays - neutral100: "#FFFFFF", - neutral200: "#F4F2F1", - neutral300: "#D7CEC9", - neutral400: "#B6ACA6", - neutral500: "#978F8A", - neutral600: "#564F4A", - neutral700: "#3C3835", - neutral800: "#2B2724", - neutral900: "#1C1917", + neutral100: '#FFFFFF', + neutral200: '#F4F2F1', + neutral300: '#D7CEC9', + neutral400: '#B6ACA6', + neutral500: '#978F8A', + neutral600: '#564F4A', + neutral700: '#3C3835', + neutral800: '#2B2724', + neutral900: '#1C1917', // Primary (Example: A muted green) - primary100: "#E3F9E5", - primary200: "#C1EAC5", - primary300: "#A3D9A5", - primary400: "#85C78A", - primary500: "#67B56F", - primary600: "#4F8B55", + primary100: '#E3F9E5', + primary200: '#C1EAC5', + primary300: '#A3D9A5', + primary400: '#85C78A', + primary500: '#67B56F', + primary600: '#4F8B55', // Accents for statuses - accentRed: "#D9534F", - accentYellow: "#F0AD4E", - accentGreen: "#5CB85C", + accentRed: '#D9534F', + accentYellow: '#F0AD4E', + accentGreen: '#5CB85C', } as const; export const colors = { diff --git a/src/constants/icons.ts b/src/constants/icons.ts index 74f82cb..705b71a 100644 --- a/src/constants/icons.ts +++ b/src/constants/icons.ts @@ -1,38 +1,36 @@ // src/constants/icons.ts -import { MaterialCommunityIcons } from "@expo/vector-icons"; +import { MaterialCommunityIcons } from '@expo/vector-icons'; // A curated list of icons for custom deeds -export const customDeedIcons: React.ComponentProps< - typeof MaterialCommunityIcons ->["name"][] = [ - "book-open-page-variant", - "charity", - "run", - "meditation", - "brain", - "account-group", - "phone", - "message-text", - "heart", - "star", - "leaf", - "water", - "food-apple", - "lightbulb-on", - "school", - "briefcase", - "home", - "camera", - "walk", - "human-greeting-variant", - "tree", - "earth", - "pencil", - "draw", - "palette", - "music-note", - "moon-waning-crescent", - "white-balance-sunny", - "flag", - "trophy", +export const customDeedIcons: React.ComponentProps['name'][] = [ + 'book-open-page-variant', + 'charity', + 'run', + 'meditation', + 'brain', + 'account-group', + 'phone', + 'message-text', + 'heart', + 'star', + 'leaf', + 'water', + 'food-apple', + 'lightbulb-on', + 'school', + 'briefcase', + 'home', + 'camera', + 'walk', + 'human-greeting-variant', + 'tree', + 'earth', + 'pencil', + 'draw', + 'palette', + 'music-note', + 'moon-waning-crescent', + 'white-balance-sunny', + 'flag', + 'trophy', ]; diff --git a/src/constants/theme.ts b/src/constants/theme.ts index 41d9a93..7c12807 100644 --- a/src/constants/theme.ts +++ b/src/constants/theme.ts @@ -1,8 +1,8 @@ // src/constants/theme.ts -import { colors } from "./colors"; -import { spacing } from "./spacing"; -import { typography } from "./typography"; +import { colors } from './colors'; +import { spacing } from './spacing'; +import { typography } from './typography'; export const theme = { colors, diff --git a/src/constants/typography.ts b/src/constants/typography.ts index ce0f480..2877c80 100644 --- a/src/constants/typography.ts +++ b/src/constants/typography.ts @@ -10,10 +10,10 @@ export const typography = { xxl: 32, }, fontWeight: { - light: "300", - normal: "400", - medium: "500", - semibold: "600", - bold: "700", + light: '300', + normal: '400', + medium: '500', + semibold: '600', + bold: '700', }, } as const; diff --git a/src/core/data/demoData.ts b/src/core/data/demoData.ts index 446508b..e4c4cca 100644 --- a/src/core/data/demoData.ts +++ b/src/core/data/demoData.ts @@ -1,22 +1,22 @@ // src/core/data/demoData.ts -import { subDays, formatISO } from "date-fns"; -import { DeedLog } from "./models"; +import { subDays, formatISO } from 'date-fns'; +import { DeedLog } from './models'; const PRAYER_DEED_IDS = [ - "prayer-fajr", - "prayer-dhuhr", - "prayer-asr", - "prayer-maghrib", - "prayer-isha", + 'prayer-fajr', + 'prayer-dhuhr', + 'prayer-asr', + 'prayer-maghrib', + 'prayer-isha', ]; // Weighted random selection to make data look more realistic const getRandomStatusId = (): string => { const rand = Math.random(); - if (rand < 0.4) return "jamaah"; // 40% chance - if (rand < 0.75) return "on-time"; // 35% chance - if (rand < 0.95) return "late"; // 20% chance - return "missed"; // 5% chance + if (rand < 0.4) return 'jamaah'; // 40% chance + if (rand < 0.75) return 'on-time'; // 35% chance + if (rand < 0.95) return 'late'; // 20% chance + return 'missed'; // 5% chance }; export const generateDemoLogs = (): DeedLog[] => { @@ -26,7 +26,7 @@ export const generateDemoLogs = (): DeedLog[] => { for (let i = 0; i < daysToGenerate; i++) { const date = subDays(today, i); - const dateString = formatISO(date, { representation: "date" }); + const dateString = formatISO(date, { representation: 'date' }); for (const deedId of PRAYER_DEED_IDS) { // Add a chance to skip logging a deed for a day to create gaps diff --git a/src/core/data/mock.ts b/src/core/data/mock.ts index 59c3a94..3659e83 100644 --- a/src/core/data/mock.ts +++ b/src/core/data/mock.ts @@ -1,56 +1,56 @@ // src/core/data/mock.ts -import { Deed, DeedLog, DeedStatus } from "./models"; +import { Deed, DeedLog, DeedStatus } from './models'; const PRAYER_STATUSES: DeedStatus[] = [ - { id: "missed", icon: "close-circle", color: "missed" }, - { id: "late", icon: "clock-alert", color: "late" }, - { id: "on-time", icon: "clock-check", color: "onTime" }, - { id: "jamaah", icon: "check-all", color: "jamaah" }, + { id: 'missed', icon: 'close-circle', color: 'missed' }, + { id: 'late', icon: 'clock-alert', color: 'late' }, + { id: 'on-time', icon: 'clock-check', color: 'onTime' }, + { id: 'jamaah', icon: 'check-all', color: 'jamaah' }, ]; export const GENERIC_STATUSES: DeedStatus[] = [ - { id: "missed", icon: "close", color: "late" }, - { id: "completed", icon: "check", color: "primary" }, + { id: 'missed', icon: 'close', color: 'late' }, + { id: 'completed', icon: 'check', color: 'primary' }, ]; export const MOCK_DEEDS: Deed[] = [ { - id: "prayer-fajr", - name: "Fajr", - icon: "weather-sunset-up", - category: "PRAYERS", + id: 'prayer-fajr', + name: 'Fajr', + icon: 'weather-sunset-up', + category: 'PRAYERS', statuses: PRAYER_STATUSES, isCore: true, }, { - id: "prayer-dhuhr", - name: "Dhuhr", - icon: "weather-sunny", - category: "PRAYERS", + id: 'prayer-dhuhr', + name: 'Dhuhr', + icon: 'weather-sunny', + category: 'PRAYERS', statuses: PRAYER_STATUSES, isCore: true, }, { - id: "prayer-asr", - name: "Asr", - icon: "weather-partly-cloudy", - category: "PRAYERS", + id: 'prayer-asr', + name: 'Asr', + icon: 'weather-partly-cloudy', + category: 'PRAYERS', statuses: PRAYER_STATUSES, isCore: true, }, { - id: "prayer-maghrib", - name: "Maghrib", - icon: "weather-sunset-down", - category: "PRAYERS", + id: 'prayer-maghrib', + name: 'Maghrib', + icon: 'weather-sunset-down', + category: 'PRAYERS', statuses: PRAYER_STATUSES, isCore: true, }, { - id: "prayer-isha", - name: "Isha", - icon: "weather-night", - category: "PRAYERS", + id: 'prayer-isha', + name: 'Isha', + icon: 'weather-night', + category: 'PRAYERS', statuses: PRAYER_STATUSES, isCore: true, }, @@ -59,61 +59,61 @@ export const MOCK_DEEDS: Deed[] = [ export const SUGGESTED_DEEDS: Deed[] = [ // PRAYERS { - id: "prayer-witr", - name: "Witr", - icon: "moon-waning-crescent", - category: "PRAYERS", + id: 'prayer-witr', + name: 'Witr', + icon: 'moon-waning-crescent', + category: 'PRAYERS', statuses: GENERIC_STATUSES, isCore: false, }, { - id: "prayer-tahajjud", - name: "Tahajjud", - icon: "meditation", - category: "PRAYERS", + id: 'prayer-tahajjud', + name: 'Tahajjud', + icon: 'meditation', + category: 'PRAYERS', statuses: GENERIC_STATUSES, isCore: false, }, // QURAN { - id: "quran-reading", - name: "Read Quran", - icon: "book-open-page-variant", - category: "QURAN", + id: 'quran-reading', + name: 'Read Quran', + icon: 'book-open-page-variant', + category: 'QURAN', statuses: GENERIC_STATUSES, isCore: false, }, { - id: "quran-memorizing", - name: "Memorize Quran", - icon: "brain", - category: "QURAN", + id: 'quran-memorizing', + name: 'Memorize Quran', + icon: 'brain', + category: 'QURAN', statuses: GENERIC_STATUSES, isCore: false, }, // LEARNING { - id: "learning-book", - name: "Read a book", - icon: "book-education", - category: "LEARNING", + id: 'learning-book', + name: 'Read a book', + icon: 'book-education', + category: 'LEARNING', statuses: GENERIC_STATUSES, isCore: false, }, // SOCIAL { - id: "social-charity", - name: "Give Charity", - icon: "charity", - category: "SOCIAL", + id: 'social-charity', + name: 'Give Charity', + icon: 'charity', + category: 'SOCIAL', statuses: GENERIC_STATUSES, isCore: false, }, { - id: "social-family", - name: "Call family", - icon: "phone-in-talk", - category: "SOCIAL", + id: 'social-family', + name: 'Call family', + icon: 'phone-in-talk', + category: 'SOCIAL', statuses: GENERIC_STATUSES, isCore: false, }, @@ -122,34 +122,34 @@ export const SUGGESTED_DEEDS: Deed[] = [ export const MOCK_LOGS: DeedLog[] = [ // Today's logs { - id: "log-1", - deedId: "prayer-fajr", - date: "2025-08-29", - statusId: "jamaah", + id: 'log-1', + deedId: 'prayer-fajr', + date: '2025-08-29', + statusId: 'jamaah', }, { - id: "log-2", - deedId: "prayer-dhuhr", - date: "2025-08-29", - statusId: "on-time", + id: 'log-2', + deedId: 'prayer-dhuhr', + date: '2025-08-29', + statusId: 'on-time', }, // Yesterday's logs { - id: "log-3", - deedId: "prayer-fajr", - date: "2025-08-28", - statusId: "on-time", + id: 'log-3', + deedId: 'prayer-fajr', + date: '2025-08-28', + statusId: 'on-time', }, { - id: "log-4", - deedId: "prayer-dhuhr", - date: "2025-08-28", - statusId: "late", + id: 'log-4', + deedId: 'prayer-dhuhr', + date: '2025-08-28', + statusId: 'late', }, { - id: "log-5", - deedId: "prayer-asr", - date: "2025-08-28", - statusId: "jamaah", + id: 'log-5', + deedId: 'prayer-asr', + date: '2025-08-28', + statusId: 'jamaah', }, ]; diff --git a/src/core/data/models.ts b/src/core/data/models.ts index f2cf0df..826e2da 100644 --- a/src/core/data/models.ts +++ b/src/core/data/models.ts @@ -1,6 +1,6 @@ // src/core/data/models.ts -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import { ColorTheme } from "@/constants/theme"; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { ColorTheme } from '@/constants/theme'; /** * Represents a single possible status for a deed. @@ -8,12 +8,9 @@ import { ColorTheme } from "@/constants/theme"; */ export type DeedStatus = { id: string; // e.g., 'on-time', 'completed' - icon: React.ComponentProps["name"]; + icon: React.ComponentProps['name']; // Use keys of our theme colors for type safety - color: keyof Pick< - ColorTheme, - "jamaah" | "onTime" | "late" | "missed" | "primary" - >; + color: keyof Pick; }; /** @@ -24,7 +21,7 @@ export type DeedStatus = { * - 'yearly': The deed has a target count per year. */ export type DeedFrequency = { - type: "daily" | "weekly" | "monthly" | "yearly"; + type: 'daily' | 'weekly' | 'monthly' | 'yearly'; // For 'weekly', an array of numbers representing days (0=Sun, 1=Mon, ..., 6=Sat) days?: number[]; // For 'monthly' or 'yearly', the target number of times @@ -45,8 +42,8 @@ export type DeedGoal = { export type Deed = { id: string; // e.g., 'prayer-fajr' name: string; // e.g., "Fajr" - icon: React.ComponentProps["name"]; - category: "PRAYERS" | "QURAN" | "LEARNING" | "SOCIAL" | "CUSTOM"; + icon: React.ComponentProps['name']; + category: 'PRAYERS' | 'QURAN' | 'LEARNING' | 'SOCIAL' | 'CUSTOM'; statuses: DeedStatus[]; isCore?: boolean; // True for fundamental deeds that cannot be edited/deleted frequency?: DeedFrequency; diff --git a/src/core/hooks/useForceUpdate.ts b/src/core/hooks/useForceUpdate.ts index dd7dc57..50c8acc 100644 --- a/src/core/hooks/useForceUpdate.ts +++ b/src/core/hooks/useForceUpdate.ts @@ -1,4 +1,4 @@ -import { useState, useCallback } from "react"; +import { useState, useCallback } from 'react'; export const useForceUpdate = () => { const [, updateState] = useState({}); return useCallback(() => updateState({}), []); diff --git a/src/core/i18n/index.ts b/src/core/i18n/index.ts index 5fdc8c5..61425fa 100644 --- a/src/core/i18n/index.ts +++ b/src/core/i18n/index.ts @@ -1,9 +1,9 @@ // src/core/i18n/index.ts -import { I18n } from "i18n-js"; -import * as Localization from "expo-localization"; -import en from "./en.json"; -import ar from "./ar.json"; -import { enUS as enUSLocale, ar as arLocale } from "date-fns/locale"; +import { I18n } from 'i18n-js'; +import * as Localization from 'expo-localization'; +import en from './en.json'; +import ar from './ar.json'; +import { enUS as enUSLocale, ar as arLocale } from 'date-fns/locale'; // Set the key-value pairs for the different languages you want to support. const i18n = new I18n({ @@ -18,7 +18,7 @@ export const dateLocales = { // Set the locale once at the beginning of your app. const locales = Localization.getLocales(); -const languageCode = locales[0]?.languageCode ?? "en"; +const languageCode = locales[0]?.languageCode ?? 'en'; i18n.locale = languageCode; // When a value is missing from a language it'll fallback to another language with the key present. diff --git a/src/core/services/notificationService.ts b/src/core/services/notificationService.ts index e1e1d4a..48edd6b 100644 --- a/src/core/services/notificationService.ts +++ b/src/core/services/notificationService.ts @@ -1,9 +1,9 @@ // src/core/services/notificationService.ts -import * as Notifications from "expo-notifications"; -import { Platform } from "react-native"; -import i18n from "../i18n"; +import * as Notifications from 'expo-notifications'; +import { Platform } from 'react-native'; +import i18n from '../i18n'; -const REMINDER_NOTIFICATION_ID = "daily-reminder"; +const REMINDER_NOTIFICATION_ID = 'daily-reminder'; // This must be called once to configure notification behavior Notifications.setNotificationHandler({ @@ -23,22 +23,22 @@ const requestPermissions = async (): Promise => { const { status: existingStatus } = await Notifications.getPermissionsAsync(); let finalStatus = existingStatus; - if (existingStatus !== "granted") { + if (existingStatus !== 'granted') { const { status } = await Notifications.requestPermissionsAsync(); finalStatus = status; } - if (finalStatus !== "granted") { + if (finalStatus !== 'granted') { // Consider linking to settings here in a real app return false; } - if (Platform.OS === "android") { - await Notifications.setNotificationChannelAsync("default", { - name: "default", + if (Platform.OS === 'android') { + await Notifications.setNotificationChannelAsync('default', { + name: 'default', importance: Notifications.AndroidImportance.MAX, vibrationPattern: [0, 250, 250, 250], - lightColor: "#FF231F7C", + lightColor: '#FF231F7C', }); } @@ -52,7 +52,7 @@ const requestPermissions = async (): Promise => { export const scheduleDailyReminder = async (time: string) => { const hasPermission = await requestPermissions(); if (!hasPermission) { - console.log("Notification permission not granted. Cannot schedule."); + console.log('Notification permission not granted. Cannot schedule.'); // Silently fail for now, but we could alert the user here. return; } @@ -60,12 +60,12 @@ export const scheduleDailyReminder = async (time: string) => { // Cancel any existing reminder to ensure we only have one. await cancelAllReminders(); - const [hour, minute] = time.split(":").map(Number); + const [hour, minute] = time.split(':').map(Number); await Notifications.scheduleNotificationAsync({ content: { - title: i18n.t("notifications.reminderTitle"), - body: i18n.t("notifications.reminderBody"), + title: i18n.t('notifications.reminderTitle'), + body: i18n.t('notifications.reminderBody'), }, trigger: { type: Notifications.SchedulableTriggerInputTypes.DAILY, @@ -83,7 +83,7 @@ export const scheduleDailyReminder = async (time: string) => { */ export const cancelAllReminders = async () => { await Notifications.cancelAllScheduledNotificationsAsync(); - console.log("All scheduled reminders have been canceled."); + console.log('All scheduled reminders have been canceled.'); }; // --- Developer Functions --- @@ -92,11 +92,11 @@ export const cancelAllReminders = async () => { * Sends a test notification immediately. */ export const sendTestNotification = async () => { - console.log("Sending test notification now."); + console.log('Sending test notification now.'); await Notifications.scheduleNotificationAsync({ content: { - title: i18n.t("notifications.testTitle"), - body: i18n.t("notifications.testBody"), + title: i18n.t('notifications.testTitle'), + body: i18n.t('notifications.testBody'), }, trigger: null, // null trigger sends it immediately }); @@ -106,11 +106,11 @@ export const sendTestNotification = async () => { * Schedules a test notification to be sent in 5 seconds. */ export const scheduleTestNotificationIn5s = async () => { - console.log("Scheduling a test notification for 5 seconds from now."); + console.log('Scheduling a test notification for 5 seconds from now.'); await Notifications.scheduleNotificationAsync({ content: { - title: i18n.t("notifications.scheduledTestTitle"), - body: i18n.t("notifications.scheduledTestBody"), + title: i18n.t('notifications.scheduledTestTitle'), + body: i18n.t('notifications.scheduledTestBody'), }, trigger: { type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL, diff --git a/src/core/storage/storageService.ts b/src/core/storage/storageService.ts index 269a94a..8c6221e 100644 --- a/src/core/storage/storageService.ts +++ b/src/core/storage/storageService.ts @@ -1,6 +1,6 @@ // src/core/storage/storageService.ts -import * as FileSystem from "expo-file-system"; -import { AppData } from "../store/appStore"; +import * as FileSystem from 'expo-file-system'; +import { AppData } from '../store/appStore'; const dataFilePath = `${FileSystem.documentDirectory}muhsinData.json`; @@ -12,13 +12,13 @@ export const loadDataFromFile = async (): Promise => { try { const fileInfo = await FileSystem.getInfoAsync(dataFilePath); if (!fileInfo.exists) { - console.log("Data file does not exist. Starting fresh."); + console.log('Data file does not exist. Starting fresh.'); return null; } const content = await FileSystem.readAsStringAsync(dataFilePath); return JSON.parse(content) as AppData; } catch (error) { - console.error("Failed to load data from file:", error); + console.error('Failed to load data from file:', error); // In case of parsing error, etc., we treat it as no data found. return null; } @@ -33,6 +33,6 @@ export const saveDataToFile = async (data: AppData): Promise => { const content = JSON.stringify(data, null, 2); // Pretty print JSON await FileSystem.writeAsStringAsync(dataFilePath, content); } catch (error) { - console.error("Failed to save data to file:", error); + console.error('Failed to save data to file:', error); } }; diff --git a/src/core/store/appStore.ts b/src/core/store/appStore.ts index 46b19dd..e8523c1 100644 --- a/src/core/store/appStore.ts +++ b/src/core/store/appStore.ts @@ -1,28 +1,20 @@ // src/core/store/appStore.ts -import { create } from "zustand"; -import { Deed, DeedLog, DeedStatus } from "../data/models"; -import { - GENERIC_STATUSES, - MOCK_DEEDS, - MOCK_LOGS, - SUGGESTED_DEEDS, -} from "../data/mock"; -import { loadDataFromFile, saveDataToFile } from "../storage/storageService"; -import { formatISO } from "date-fns"; -import { Alert, Appearance, I18nManager } from "react-native"; -import { generateDemoLogs } from "../data/demoData"; -import i18n from "../i18n"; -import { - cancelAllReminders, - scheduleDailyReminder, -} from "../services/notificationService"; +import { create } from 'zustand'; +import { Deed, DeedLog, DeedStatus } from '../data/models'; +import { GENERIC_STATUSES, MOCK_DEEDS, MOCK_LOGS, SUGGESTED_DEEDS } from '../data/mock'; +import { loadDataFromFile, saveDataToFile } from '../storage/storageService'; +import { formatISO } from 'date-fns'; +import { Alert, Appearance, I18nManager } from 'react-native'; +import { generateDemoLogs } from '../data/demoData'; +import i18n from '../i18n'; +import { cancelAllReminders, scheduleDailyReminder } from '../services/notificationService'; export type AppSettings = { - theme: "system" | "light" | "dark"; + theme: 'system' | 'light' | 'dark'; isHapticsEnabled: boolean; isDevMode: boolean; isDemoMode: boolean; - language: "en" | "ar"; // Updated language options + language: 'en' | 'ar'; // Updated language options isReminderEnabled: boolean; reminderTime: string; }; @@ -44,12 +36,7 @@ type AppState = AppData & { type AppActions = { initialize: () => Promise; - addOrUpdateLog: ( - deed: Deed, - date: Date, - status: DeedStatus, - value?: number, - ) => void; + addOrUpdateLog: (deed: Deed, date: Date, status: DeedStatus, value?: number) => void; addDeed: (deed: Deed) => void; // DEED MANAGEMENT initializeDraftDeed: (deedId?: string) => void; @@ -59,8 +46,8 @@ type AppActions = { deleteDeed: (deedId: string) => void; setDeeds: (deeds: Deed[]) => void; // SETTINGS - setTheme: (theme: AppSettings["theme"]) => void; - setLanguage: (language: AppSettings["language"]) => void; + setTheme: (theme: AppSettings['theme']) => void; + setLanguage: (language: AppSettings['language']) => void; toggleHaptics: () => void; setDevMode: (isDev: boolean) => void; toggleDemoMode: () => Promise; @@ -70,13 +57,13 @@ type AppActions = { }; const defaultSettings: AppSettings = { - theme: "system", + theme: 'system', isHapticsEnabled: true, isDevMode: false, isDemoMode: false, - language: "en", + language: 'en', isReminderEnabled: false, - reminderTime: "20:00", + reminderTime: '20:00', }; const persistState = (state: AppState) => { @@ -130,11 +117,11 @@ const useAppStore = create((set, get) => ({ }); } - if (settings.theme !== "system") { + if (settings.theme !== 'system') { Appearance.setColorScheme(settings.theme); } i18n.locale = settings.language; - I18nManager.forceRTL(settings.language === "ar"); + I18nManager.forceRTL(settings.language === 'ar'); if (settings.isReminderEnabled) { scheduleDailyReminder(settings.reminderTime); @@ -148,7 +135,7 @@ const useAppStore = create((set, get) => ({ if (currentLanguage === language) return; i18n.locale = language; - const isRTL = language === "ar"; + const isRTL = language === 'ar'; const needsRestart = isRTL !== I18nManager.isRTL; set((state) => ({ settings: { ...state.settings, language } })); @@ -156,12 +143,12 @@ const useAppStore = create((set, get) => ({ if (needsRestart) { Alert.alert( - i18n.t("alerts.restartTitle"), - i18n.t("alerts.restartMessage"), + i18n.t('alerts.restartTitle'), + i18n.t('alerts.restartMessage'), [ { - text: i18n.t("alerts.cancel"), - style: "cancel", + text: i18n.t('alerts.cancel'), + style: 'cancel', onPress: () => { i18n.locale = currentLanguage; set((state) => ({ @@ -171,8 +158,8 @@ const useAppStore = create((set, get) => ({ }, }, { - text: i18n.t("alerts.restartNow"), - style: "default", + text: i18n.t('alerts.restartNow'), + style: 'default', onPress: async () => { I18nManager.forceRTL(isRTL); }, @@ -184,7 +171,7 @@ const useAppStore = create((set, get) => ({ }, addOrUpdateLog: (deed, date, status, value) => { - const dateString = formatISO(date, { representation: "date" }); + const dateString = formatISO(date, { representation: 'date' }); const currentLogs = get().logs; const existingLogIndex = currentLogs.findIndex( (log) => log.deedId === deed.id && log.date === dateString, @@ -223,10 +210,10 @@ const useAppStore = create((set, get) => ({ } else { set({ draftDeed: { - name: "", - icon: "star-outline", - frequency: { type: "daily" }, - category: "CUSTOM", + name: '', + icon: 'star-outline', + frequency: { type: 'daily' }, + category: 'CUSTOM', statuses: GENERIC_STATUSES, }, }); @@ -255,7 +242,7 @@ const useAppStore = create((set, get) => ({ id: `custom-${Date.now()}`, name: draft.name, icon: draft.icon, - category: "CUSTOM", + category: 'CUSTOM', statuses: GENERIC_STATUSES, frequency: draft.frequency, goal: draft.goal, @@ -281,7 +268,7 @@ const useAppStore = create((set, get) => ({ setTheme: (theme) => { set((state) => ({ settings: { ...state.settings, theme } })); - if (theme === "system") { + if (theme === 'system') { Appearance.setColorScheme(null); } else { Appearance.setColorScheme(theme); @@ -320,10 +307,7 @@ const useAppStore = create((set, get) => ({ deeds: MOCK_DEEDS, logs: generateDemoLogs(), }); - Alert.alert( - i18n.t("alerts.demoModeEnabledTitle"), - i18n.t("alerts.demoModeEnabledMessage"), - ); + Alert.alert(i18n.t('alerts.demoModeEnabledTitle'), i18n.t('alerts.demoModeEnabledMessage')); } else { // Disabling Demo Mode: Reload user's original data from disk const originalData = await loadDataFromFile(); @@ -331,10 +315,7 @@ const useAppStore = create((set, get) => ({ deeds: originalData?.deeds || MOCK_DEEDS, logs: originalData?.logs || [], }); - Alert.alert( - i18n.t("alerts.demoModeDisabledTitle"), - i18n.t("alerts.demoModeDisabledMessage"), - ); + Alert.alert(i18n.t('alerts.demoModeDisabledTitle'), i18n.t('alerts.demoModeDisabledMessage')); } // Persist the new state of `isDemoMode` in settings persistState(get()); @@ -348,7 +329,7 @@ const useAppStore = create((set, get) => ({ }; set(initialState); const theme = initialState.settings.theme; - if (theme === "system") { + if (theme === 'system') { Appearance.setColorScheme(null); } else { Appearance.setColorScheme(theme); diff --git a/src/core/theme/ThemeContext.tsx b/src/core/theme/ThemeContext.tsx index 4c6ebe9..d21ab90 100644 --- a/src/core/theme/ThemeContext.tsx +++ b/src/core/theme/ThemeContext.tsx @@ -1,8 +1,8 @@ // src/core/theme/ThemeContext.tsx -import React, { createContext, useContext } from "react"; -import { useColorScheme } from "react-native"; -import { AppTheme, ColorTheme, theme as defaultTheme } from "@/constants/theme"; -import useAppStore from "../store/appStore"; +import React, { createContext, useContext } from 'react'; +import { useColorScheme } from 'react-native'; +import { AppTheme, ColorTheme, theme as defaultTheme } from '@/constants/theme'; +import useAppStore from '../store/appStore'; interface ThemeContextType { theme: AppTheme; @@ -16,13 +16,11 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) { const userThemePreference = useAppStore((state) => state.settings.theme); const activeColorScheme = - userThemePreference === "system" ? systemColorScheme : userThemePreference; + userThemePreference === 'system' ? systemColorScheme : userThemePreference; - const isDark = activeColorScheme === "dark"; + const isDark = activeColorScheme === 'dark'; - const activeColors: ColorTheme = isDark - ? defaultTheme.colors.dark - : defaultTheme.colors.light; + const activeColors: ColorTheme = isDark ? defaultTheme.colors.dark : defaultTheme.colors.light; const activeTheme: AppTheme = { ...defaultTheme, @@ -30,16 +28,14 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) { }; return ( - - {children} - + {children} ); } export const useTheme = () => { const context = useContext(ThemeContext); if (!context) { - throw new Error("useTheme must be used within a ThemeProvider"); + throw new Error('useTheme must be used within a ThemeProvider'); } return context; }; diff --git a/src/core/utils/dateFormatter.ts b/src/core/utils/dateFormatter.ts index 0e4d413..24bf1ca 100644 --- a/src/core/utils/dateFormatter.ts +++ b/src/core/utils/dateFormatter.ts @@ -1,5 +1,5 @@ // src/core/utils/dateFormatter.ts -import i18n from "../i18n"; +import i18n from '../i18n'; /** * Formats a JavaScript Date object into a custom Hijri date string. @@ -7,27 +7,26 @@ import i18n from "../i18n"; * @returns A formatted string, e.g., "Rab Awal 6, 1447". */ export const formatHijriDate = (date: Date): string => { - const isArabic = i18n.locale.startsWith("ar"); + const isArabic = i18n.locale.startsWith('ar'); // This formatter's only job is to give us the numeric parts of the Hijri date. const hijriNumericParser = new Intl.DateTimeFormat( - `${isArabic ? "ar" : "en"}-u-ca-islamic-nu-latn`, + `${isArabic ? 'ar' : 'en'}-u-ca-islamic-nu-latn`, { - year: "numeric", - month: "numeric", - day: "numeric", + year: 'numeric', + month: 'numeric', + day: 'numeric', }, ); // Get the numeric parts of the date const parts = hijriNumericParser.formatToParts(date); - const day = parts.find((p) => p.type === "day")?.value; - const monthIndex = - parseInt(parts.find((p) => p.type === "month")?.value || "1", 10) - 1; - const year = parts.find((p) => p.type === "year")?.value; + const day = parts.find((p) => p.type === 'day')?.value; + const monthIndex = parseInt(parts.find((p) => p.type === 'month')?.value || '1', 10) - 1; + const year = parts.find((p) => p.type === 'year')?.value; // Look up our custom month name from the i18n files - const hijriMonthName = (i18n.t("hijri_months") as string[])[monthIndex]; + const hijriMonthName = (i18n.t('hijri_months') as string[])[monthIndex]; // Construct the final string exactly as desired if (isArabic) { diff --git a/src/core/utils/haptics.ts b/src/core/utils/haptics.ts index 126e4f1..5829ecc 100644 --- a/src/core/utils/haptics.ts +++ b/src/core/utils/haptics.ts @@ -1,6 +1,6 @@ // src/core/utils/haptics.ts -import * as Haptics from "expo-haptics"; -import useAppStore from "../store/appStore"; +import * as Haptics from 'expo-haptics'; +import useAppStore from '../store/appStore'; /** * Triggers haptic feedback if the user has it enabled in settings. diff --git a/src/features/deeds/ChildDeedLogItem.tsx b/src/features/deeds/ChildDeedLogItem.tsx index 55c4310..40aff88 100644 --- a/src/features/deeds/ChildDeedLogItem.tsx +++ b/src/features/deeds/ChildDeedLogItem.tsx @@ -1,15 +1,15 @@ // src/features/deeds/ChildDeedLogItem.tsx -import { Box } from "@/components/base/Box"; -import { ThemedText } from "@/components/base/ThemedText"; -import { AppTheme } from "@/constants/theme"; -import { Deed, DeedLog, DeedStatus } from "@/core/data/models"; -import i18n from "@/core/i18n"; -import useAppStore from "@/core/store/appStore"; -import { useTheme } from "@/core/theme/ThemeContext"; -import { triggerHaptic } from "@/core/utils/haptics"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import React from "react"; -import { StyleSheet, TouchableOpacity, View } from "react-native"; +import { Box } from '@/components/base/Box'; +import { ThemedText } from '@/components/base/ThemedText'; +import { AppTheme } from '@/constants/theme'; +import { Deed, DeedLog, DeedStatus } from '@/core/data/models'; +import i18n from '@/core/i18n'; +import useAppStore from '@/core/store/appStore'; +import { useTheme } from '@/core/theme/ThemeContext'; +import { triggerHaptic } from '@/core/utils/haptics'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import React from 'react'; +import { StyleSheet, TouchableOpacity, View } from 'react-native'; type ChildDeedLogItemProps = { deed: Deed; @@ -17,11 +17,7 @@ type ChildDeedLogItemProps = { date: Date; }; -export const ChildDeedLogItem = ({ - deed, - log, - date, -}: ChildDeedLogItemProps) => { +export const ChildDeedLogItem = ({ deed, log, date }: ChildDeedLogItemProps) => { const { theme } = useTheme(); const styles = getStyles(theme); const addOrUpdateLog = useAppStore((state) => state.addOrUpdateLog); @@ -33,11 +29,7 @@ export const ChildDeedLogItem = ({ return ( - + {i18n.t(`deeds_names.${deed.id}`, { defaultValue: deed.name })} @@ -52,16 +44,11 @@ export const ChildDeedLogItem = ({ { borderColor: theme.colors[status.color] }, isSelected && { backgroundColor: theme.colors[status.color] }, ]} - onPress={() => handleLogStatus(status)} - > + onPress={() => handleLogStatus(status)}> ); @@ -74,8 +61,8 @@ export const ChildDeedLogItem = ({ const getStyles = (theme: AppTheme) => StyleSheet.create({ container: { - flexDirection: "row", - alignItems: "center", + flexDirection: 'row', + alignItems: 'center', paddingVertical: theme.spacing.s, }, deedName: { @@ -84,7 +71,7 @@ const getStyles = (theme: AppTheme) => fontSize: theme.typography.fontSize.m, }, actionsContainer: { - flexDirection: "row", + flexDirection: 'row', }, statusButton: { padding: theme.spacing.s, diff --git a/src/features/deeds/DeedListItem.tsx b/src/features/deeds/DeedListItem.tsx index c42069f..d7a39fa 100644 --- a/src/features/deeds/DeedListItem.tsx +++ b/src/features/deeds/DeedListItem.tsx @@ -1,13 +1,13 @@ // src/features/deeds/DeedListItem.tsx -import { Box } from "@/components/base/Box"; -import { ThemedText } from "@/components/base/ThemedText"; -import { AppTheme } from "@/constants/theme"; -import { Deed, DeedLog } from "@/core/data/models"; -import i18n from "@/core/i18n"; -import { useTheme } from "@/core/theme/ThemeContext"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import React from "react"; -import { StyleSheet, TouchableOpacity } from "react-native"; +import { Box } from '@/components/base/Box'; +import { ThemedText } from '@/components/base/ThemedText'; +import { AppTheme } from '@/constants/theme'; +import { Deed, DeedLog } from '@/core/data/models'; +import i18n from '@/core/i18n'; +import { useTheme } from '@/core/theme/ThemeContext'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import React from 'react'; +import { StyleSheet, TouchableOpacity } from 'react-native'; type DeedListItemProps = { deed: Deed; @@ -24,21 +24,15 @@ const renderStatus = ( if (!log) return null; // Render goal progress if the deed has a goal and a value is logged - if (deed.goal && typeof log.value === "number") { + if (deed.goal && typeof log.value === 'number') { const isComplete = log.value >= deed.goal.value; return ( - + + ]}> {log.value} / {deed.goal.value} @@ -50,17 +44,8 @@ const renderStatus = ( if (!status) return null; return ( - - + + ); }; @@ -72,11 +57,7 @@ export const DeedListItem = ({ deed, log, onPress }: DeedListItemProps) => { return ( - + {i18n.t(`deeds_names.${deed.id}`, { defaultValue: deed.name })} @@ -90,8 +71,8 @@ export const DeedListItem = ({ deed, log, onPress }: DeedListItemProps) => { const getStyles = (theme: AppTheme) => StyleSheet.create({ container: { - flexDirection: "row", - alignItems: "center", + flexDirection: 'row', + alignItems: 'center', padding: theme.spacing.m, backgroundColor: theme.colors.foreground, borderRadius: 8, @@ -106,8 +87,8 @@ const getStyles = (theme: AppTheme) => paddingVertical: theme.spacing.xs, paddingHorizontal: theme.spacing.s, borderRadius: 16, - justifyContent: "center", - alignItems: "center", + justifyContent: 'center', + alignItems: 'center', minWidth: 32, }, goalInProgress: { diff --git a/src/features/deeds/DeedManagerListItem.tsx b/src/features/deeds/DeedManagerListItem.tsx index 31b3ee0..2038743 100644 --- a/src/features/deeds/DeedManagerListItem.tsx +++ b/src/features/deeds/DeedManagerListItem.tsx @@ -1,19 +1,19 @@ // FILE: src/features/deeds/DeedManagerListItem.tsx -import { Box } from "@/components/base/Box"; -import { ThemedText } from "@/components/base/ThemedText"; -import { AppTheme } from "@/constants/theme"; -import { Deed } from "@/core/data/models"; -import i18n from "@/core/i18n"; -import useAppStore from "@/core/store/appStore"; -import { useTheme } from "@/core/theme/ThemeContext"; -import { triggerHaptic } from "@/core/utils/haptics"; -import { HomeStackParamList } from "@/navigation/AppNavigator"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import { useNavigation } from "@react-navigation/native"; -import { NativeStackNavigationProp } from "@react-navigation/native-stack"; -import React from "react"; -import { Alert, StyleSheet, TouchableOpacity } from "react-native"; -import { ScaleDecorator } from "react-native-draggable-flatlist"; +import { Box } from '@/components/base/Box'; +import { ThemedText } from '@/components/base/ThemedText'; +import { AppTheme } from '@/constants/theme'; +import { Deed } from '@/core/data/models'; +import i18n from '@/core/i18n'; +import useAppStore from '@/core/store/appStore'; +import { useTheme } from '@/core/theme/ThemeContext'; +import { triggerHaptic } from '@/core/utils/haptics'; +import { HomeStackParamList } from '@/navigation/AppNavigator'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { useNavigation } from '@react-navigation/native'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import React from 'react'; +import { Alert, StyleSheet, TouchableOpacity } from 'react-native'; +import { ScaleDecorator } from 'react-native-draggable-flatlist'; type DeedManagerListItemProps = { deed: Deed; @@ -23,11 +23,7 @@ type DeedManagerListItemProps = { type DeedManagerNavigationProp = NativeStackNavigationProp; -export const DeedManagerListItem = ({ - deed, - drag, - isDragging, -}: DeedManagerListItemProps) => { +export const DeedManagerListItem = ({ deed, drag, isDragging }: DeedManagerListItemProps) => { const { theme } = useTheme(); const styles = getStyles(theme); const navigation = useNavigation(); @@ -37,7 +33,7 @@ export const DeedManagerListItem = ({ const handleEdit = () => { triggerHaptic(); - navigation.navigate("CreateDeed", { deedId: deed.id }); + navigation.navigate('CreateDeed', { deedId: deed.id }); }; const handleDelete = () => { @@ -46,13 +42,13 @@ export const DeedManagerListItem = ({ defaultValue: deed.name, }); Alert.alert( - i18n.t("alerts.deleteDeedTitle"), - i18n.t("alerts.deleteDeedMessage", { deedName }), + i18n.t('alerts.deleteDeedTitle'), + i18n.t('alerts.deleteDeedMessage', { deedName }), [ - { text: i18n.t("alerts.cancel"), style: "cancel" }, + { text: i18n.t('alerts.cancel'), style: 'cancel' }, { - text: i18n.t("alerts.delete"), - style: "destructive", + text: i18n.t('alerts.delete'), + style: 'destructive', onPress: () => deleteDeed(deed.id), }, ], @@ -65,8 +61,7 @@ export const DeedManagerListItem = ({ onLongPress={drag} delayLongPress={150} activeOpacity={0.8} - style={[styles.container, isDragging && styles.dragging]} - > + style={[styles.container, isDragging && styles.dragging]}> - + StyleSheet.create({ container: { - flexDirection: "row", - alignItems: "center", + flexDirection: 'row', + alignItems: 'center', paddingEnd: theme.spacing.s, backgroundColor: theme.colors.foreground, borderRadius: 12, marginBottom: theme.spacing.s, }, dragging: { - shadowColor: "#000", + shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.25, shadowRadius: 3.84, @@ -138,7 +130,7 @@ const getStyles = (theme: AppTheme) => flex: 1, }, actionsContainer: { - flexDirection: "row", + flexDirection: 'row', }, actionButton: { padding: theme.spacing.s, diff --git a/src/features/deeds/IconSelectorModal.tsx b/src/features/deeds/IconSelectorModal.tsx index 8a0d24f..5bce620 100644 --- a/src/features/deeds/IconSelectorModal.tsx +++ b/src/features/deeds/IconSelectorModal.tsx @@ -1,11 +1,11 @@ // src/features/deeds/IconSelectorModal.tsx -import { ThemedText } from "@/components/base/ThemedText"; -import { customDeedIcons } from "@/constants/icons"; -import { AppTheme } from "@/constants/theme"; -import i18n from "@/core/i18n"; -import { useTheme } from "@/core/theme/ThemeContext"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import React from "react"; +import { ThemedText } from '@/components/base/ThemedText'; +import { customDeedIcons } from '@/constants/icons'; +import { AppTheme } from '@/constants/theme'; +import i18n from '@/core/i18n'; +import { useTheme } from '@/core/theme/ThemeContext'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import React from 'react'; import { FlatList, Modal, @@ -14,24 +14,18 @@ import { TouchableWithoutFeedback, useWindowDimensions, View, -} from "react-native"; +} from 'react-native'; type IconSelectorModalProps = { isVisible: boolean; onClose: () => void; - onSelectIcon: ( - icon: React.ComponentProps["name"], - ) => void; + onSelectIcon: (icon: React.ComponentProps['name']) => void; }; const ICON_SIZE = 40; const PADDING = 16; -export const IconSelectorModal = ({ - isVisible, - onClose, - onSelectIcon, -}: IconSelectorModalProps) => { +export const IconSelectorModal = ({ isVisible, onClose, onSelectIcon }: IconSelectorModalProps) => { const { theme } = useTheme(); const styles = getStyles(theme); const { width } = useWindowDimensions(); @@ -39,29 +33,19 @@ export const IconSelectorModal = ({ const numColumns = Math.floor((width - PADDING * 2) / (ICON_SIZE + PADDING)); return ( - + {/* This View prevents the close event from firing when tapping the container */} - - {i18n.t("iconSelector.title")} - + {i18n.t('iconSelector.title')} item} numColumns={numColumns} renderItem={({ item }) => ( - onSelectIcon(item)} - > + onSelectIcon(item)}> StyleSheet.create({ overlay: { flex: 1, - justifyContent: "center", - alignItems: "center", - backgroundColor: "rgba(0, 0, 0, 0.6)", + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0, 0, 0, 0.6)', padding: PADDING, }, container: { - width: "100%", - height: "80%", // Use height instead of maxHeight + width: '100%', + height: '80%', // Use height instead of maxHeight backgroundColor: theme.colors.foreground, borderRadius: 16, padding: PADDING, @@ -98,11 +82,11 @@ const getStyles = (theme: AppTheme) => title: { fontSize: theme.typography.fontSize.l, fontWeight: theme.typography.fontWeight.semibold, - textAlign: "center", + textAlign: 'center', marginBottom: theme.spacing.m, }, grid: { - justifyContent: "center", + justifyContent: 'center', }, iconButton: { padding: theme.spacing.s, diff --git a/src/features/deeds/LogDeedModal.tsx b/src/features/deeds/LogDeedModal.tsx index e412756..48fc969 100644 --- a/src/features/deeds/LogDeedModal.tsx +++ b/src/features/deeds/LogDeedModal.tsx @@ -1,22 +1,16 @@ // src/features/deeds/LogDeedModal.tsx -import { Box } from "@/components/base/Box"; -import { ThemedText } from "@/components/base/ThemedText"; -import { AppTheme } from "@/constants/theme"; -import { Deed, DeedStatus } from "@/core/data/models"; -import i18n from "@/core/i18n"; -import useAppStore from "@/core/store/appStore"; -import { useTheme } from "@/core/theme/ThemeContext"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import { formatISO } from "date-fns"; -import React, { useMemo } from "react"; -import { - Modal, - StyleSheet, - TouchableOpacity, - View, - ScrollView, -} from "react-native"; -import { ChildDeedLogItem } from "./ChildDeedLogItem"; +import { Box } from '@/components/base/Box'; +import { ThemedText } from '@/components/base/ThemedText'; +import { AppTheme } from '@/constants/theme'; +import { Deed, DeedStatus } from '@/core/data/models'; +import i18n from '@/core/i18n'; +import useAppStore from '@/core/store/appStore'; +import { useTheme } from '@/core/theme/ThemeContext'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { formatISO } from 'date-fns'; +import React, { useMemo } from 'react'; +import { Modal, StyleSheet, TouchableOpacity, View, ScrollView } from 'react-native'; +import { ChildDeedLogItem } from './ChildDeedLogItem'; type LogDeedModalProps = { deed: Deed | null; @@ -45,7 +39,7 @@ export const LogDeedModal = ({ }, [deed, allDeeds]); const logsForDate = useMemo(() => { - const dateString = formatISO(date, { representation: "date" }); + const dateString = formatISO(date, { representation: 'date' }); return allLogs.filter((log) => log.date === dateString); }, [date, allLogs]); @@ -56,41 +50,27 @@ export const LogDeedModal = ({ }); return ( - - + + - + - {i18n.t("deeds.logDeedTitle", { deedName })} + {i18n.t('deeds.logDeedTitle', { deedName })} {deed.statuses.map((status) => { const statusLabelKey = - deed.isCore || status.id !== "missed" + deed.isCore || status.id !== 'missed' ? `deeds_statuses.${status.id}` : `deeds_statuses.generic-missed`; return ( onLogStatus(status)} - > + onPress={() => onLogStatus(status)}> {i18n.t(statusLabelKey, { - defaultValue: status.id.replace("-", " "), + defaultValue: status.id.replace('-', ' '), })} @@ -109,18 +89,11 @@ export const LogDeedModal = ({ {childDeeds.length > 0 && ( - {i18n.t("deeds.linkedDeeds")} + {i18n.t('deeds.linkedDeeds')} {childDeeds.map((child) => { const log = logsForDate.find((l) => l.deedId === child.id); - return ( - - ); + return ; })} )} @@ -135,31 +108,31 @@ const getStyles = (theme: AppTheme) => StyleSheet.create({ overlay: { flex: 1, - justifyContent: "center", - alignItems: "center", - backgroundColor: "rgba(0, 0, 0, 0.6)", + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0, 0, 0, 0.6)', padding: theme.spacing.l, }, container: { - width: "100%", - maxHeight: "80%", + width: '100%', + maxHeight: '80%', backgroundColor: theme.colors.foreground, borderRadius: 16, padding: theme.spacing.l, }, header: { - alignItems: "center", + alignItems: 'center', marginBottom: theme.spacing.m, }, title: { fontSize: theme.typography.fontSize.l, fontWeight: theme.typography.fontWeight.semibold, - textAlign: "center", + textAlign: 'center', marginTop: theme.spacing.m, }, optionRow: { - flexDirection: "row", - alignItems: "center", + flexDirection: 'row', + alignItems: 'center', paddingVertical: theme.spacing.m, }, optionLabel: { @@ -176,7 +149,7 @@ const getStyles = (theme: AppTheme) => fontSize: theme.typography.fontSize.s, fontWeight: theme.typography.fontWeight.bold, color: theme.colors.textSecondary, - textTransform: "uppercase", + textTransform: 'uppercase', marginBottom: theme.spacing.s, }, }); diff --git a/src/features/deeds/LogGoalModal.tsx b/src/features/deeds/LogGoalModal.tsx index 0842ca8..24dc8d4 100644 --- a/src/features/deeds/LogGoalModal.tsx +++ b/src/features/deeds/LogGoalModal.tsx @@ -1,15 +1,15 @@ // src/features/deeds/LogGoalModal.tsx -import { Box } from "@/components/base/Box"; -import { ThemedText } from "@/components/base/ThemedText"; -import { AppTheme } from "@/constants/theme"; -import { Deed, DeedLog } from "@/core/data/models"; -import i18n from "@/core/i18n"; -import useAppStore from "@/core/store/appStore"; -import { useTheme } from "@/core/theme/ThemeContext"; -import { triggerHaptic } from "@/core/utils/haptics"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import { formatISO } from "date-fns"; -import React, { useEffect, useMemo, useState } from "react"; +import { Box } from '@/components/base/Box'; +import { ThemedText } from '@/components/base/ThemedText'; +import { AppTheme } from '@/constants/theme'; +import { Deed, DeedLog } from '@/core/data/models'; +import i18n from '@/core/i18n'; +import useAppStore from '@/core/store/appStore'; +import { useTheme } from '@/core/theme/ThemeContext'; +import { triggerHaptic } from '@/core/utils/haptics'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { formatISO } from 'date-fns'; +import React, { useEffect, useMemo, useState } from 'react'; import { Modal, ScrollView, @@ -17,8 +17,8 @@ import { TouchableOpacity, TouchableWithoutFeedback, View, -} from "react-native"; -import { ChildDeedLogItem } from "./ChildDeedLogItem"; +} from 'react-native'; +import { ChildDeedLogItem } from './ChildDeedLogItem'; type LogGoalModalProps = { deed: Deed | null; @@ -50,7 +50,7 @@ export const LogGoalModal = ({ }, [deed, allDeeds]); const logsForDate = useMemo(() => { - const dateString = formatISO(date, { representation: "date" }); + const dateString = formatISO(date, { representation: 'date' }); return allLogs.filter((log) => log.date === dateString); }, [date, allLogs]); @@ -80,32 +80,20 @@ export const LogGoalModal = ({ }); return ( - + - + - {i18n.t("deeds.logDeedTitle", { deedName })} + {i18n.t('deeds.logDeedTitle', { deedName })} - + {value} - - {deed.goal.unit} - + {deed.goal.unit} - + - - {i18n.t("deeds.save")} - + {i18n.t('deeds.save')} {childDeeds.length > 0 && ( - {i18n.t("deeds.linkedDeeds")} + {i18n.t('deeds.linkedDeeds')} {childDeeds.map((child) => { - const childLog = logsForDate.find( - (l) => l.deedId === child.id, - ); + const childLog = logsForDate.find((l) => l.deedId === child.id); return ( - + ); })} @@ -170,40 +144,40 @@ const getStyles = (theme: AppTheme) => StyleSheet.create({ overlay: { flex: 1, - justifyContent: "center", - alignItems: "center", - backgroundColor: "rgba(0, 0, 0, 0.6)", + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0, 0, 0, 0.6)', padding: theme.spacing.l, }, container: { - width: "100%", - maxHeight: "80%", + width: '100%', + maxHeight: '80%', backgroundColor: theme.colors.foreground, borderRadius: 16, padding: theme.spacing.l, }, header: { - alignItems: "center", + alignItems: 'center', marginBottom: theme.spacing.m, }, title: { fontSize: theme.typography.fontSize.l, fontWeight: theme.typography.fontWeight.semibold, - textAlign: "center", + textAlign: 'center', marginTop: theme.spacing.m, }, counterContainer: { - flexDirection: "row", - alignItems: "center", - justifyContent: "space-around", + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-around', marginBottom: theme.spacing.m, }, counterButton: { padding: theme.spacing.s, }, valueDisplay: { - alignItems: "center", - justifyContent: "center", + alignItems: 'center', + justifyContent: 'center', minWidth: 100, }, counterValue: { @@ -219,7 +193,7 @@ const getStyles = (theme: AppTheme) => backgroundColor: theme.colors.primary, padding: theme.spacing.m, borderRadius: 8, - alignItems: "center", + alignItems: 'center', marginBottom: theme.spacing.l, }, saveButtonText: { @@ -236,7 +210,7 @@ const getStyles = (theme: AppTheme) => fontSize: theme.typography.fontSize.s, fontWeight: theme.typography.fontWeight.bold, color: theme.colors.textSecondary, - textTransform: "uppercase", + textTransform: 'uppercase', marginBottom: theme.spacing.s, }, }); diff --git a/src/features/deeds/SuggestedDeedListItem.tsx b/src/features/deeds/SuggestedDeedListItem.tsx index 8f7ef4a..52515f0 100644 --- a/src/features/deeds/SuggestedDeedListItem.tsx +++ b/src/features/deeds/SuggestedDeedListItem.tsx @@ -1,13 +1,13 @@ // src/features/deeds/SuggestedDeedListItem.tsx -import { Box } from "@/components/base/Box"; -import { ThemedText } from "@/components/base/ThemedText"; -import { AppTheme } from "@/constants/theme"; -import { Deed } from "@/core/data/models"; -import i18n from "@/core/i18n"; -import { useTheme } from "@/core/theme/ThemeContext"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import React from "react"; -import { StyleSheet, TouchableOpacity } from "react-native"; +import { Box } from '@/components/base/Box'; +import { ThemedText } from '@/components/base/ThemedText'; +import { AppTheme } from '@/constants/theme'; +import { Deed } from '@/core/data/models'; +import i18n from '@/core/i18n'; +import { useTheme } from '@/core/theme/ThemeContext'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import React from 'react'; +import { StyleSheet, TouchableOpacity } from 'react-native'; type SuggestedDeedListItemProps = { deed: Deed; @@ -15,31 +15,22 @@ type SuggestedDeedListItemProps = { onPress: () => void; }; -export const SuggestedDeedListItem = ({ - deed, - isAdded, - onPress, -}: SuggestedDeedListItemProps) => { +export const SuggestedDeedListItem = ({ deed, isAdded, onPress }: SuggestedDeedListItemProps) => { const { theme } = useTheme(); const styles = getStyles(theme); return ( - + {i18n.t(`deeds_names.${deed.id}`, { defaultValue: deed.name })} + style={[styles.addButton, isAdded && styles.addedButton]}> @@ -51,8 +42,8 @@ export const SuggestedDeedListItem = ({ const getStyles = (theme: AppTheme) => StyleSheet.create({ container: { - flexDirection: "row", - alignItems: "center", + flexDirection: 'row', + alignItems: 'center', padding: theme.spacing.m, backgroundColor: theme.colors.foreground, borderRadius: 8, diff --git a/src/features/home/DateItem.tsx b/src/features/home/DateItem.tsx index 3f25e62..243bca2 100644 --- a/src/features/home/DateItem.tsx +++ b/src/features/home/DateItem.tsx @@ -1,10 +1,10 @@ // src/features/home/DateItem.tsx -import { ThemedText } from "@/components/base/ThemedText"; -import { AppTheme } from "@/constants/theme"; -import { useTheme } from "@/core/theme/ThemeContext"; -import { format, Locale } from "date-fns"; -import React from "react"; -import { StyleSheet, TouchableOpacity } from "react-native"; +import { ThemedText } from '@/components/base/ThemedText'; +import { AppTheme } from '@/constants/theme'; +import { useTheme } from '@/core/theme/ThemeContext'; +import { format, Locale } from 'date-fns'; +import React from 'react'; +import { StyleSheet, TouchableOpacity } from 'react-native'; export const ITEM_MARGIN = 4; @@ -28,14 +28,11 @@ const DateItemComponent = ({ const { theme } = useTheme(); const styles = getStyles(theme, isSelected, isToday); - const dayName = format(date, "E", { locale }); - const dayNumber = format(date, "d", { locale }); + const dayName = format(date, 'E', { locale }); + const dayNumber = format(date, 'd', { locale }); return ( - + {dayName} @@ -49,25 +46,21 @@ export const DateItem = React.memo(DateItemComponent); const getStyles = (theme: AppTheme, isSelected: boolean, isToday: boolean) => StyleSheet.create({ container: { - height: "100%", - justifyContent: "center", - alignItems: "center", + height: '100%', + justifyContent: 'center', + alignItems: 'center', paddingVertical: theme.spacing.s, paddingHorizontal: theme.spacing.xs, borderRadius: 8, borderWidth: 2, - borderColor: isToday ? theme.colors.primary : "transparent", - backgroundColor: isSelected - ? theme.colors.primary - : theme.colors.foreground, + borderColor: isToday ? theme.colors.primary : 'transparent', + backgroundColor: isSelected ? theme.colors.primary : theme.colors.foreground, marginHorizontal: ITEM_MARGIN, }, dayName: { fontSize: theme.typography.fontSize.s, fontWeight: theme.typography.fontWeight.medium, - color: isSelected - ? theme.colors.primaryContrast - : theme.colors.textSecondary, + color: isSelected ? theme.colors.primaryContrast : theme.colors.textSecondary, }, dayNumber: { fontSize: theme.typography.fontSize.l, diff --git a/src/features/home/DateScroller.tsx b/src/features/home/DateScroller.tsx index 1107509..a878e32 100644 --- a/src/features/home/DateScroller.tsx +++ b/src/features/home/DateScroller.tsx @@ -7,10 +7,10 @@ import { startOfWeek, subYears, Locale, -} from "date-fns"; -import React, { useCallback, useMemo, useRef } from "react"; -import { FlatList, useWindowDimensions, I18nManager } from "react-native"; -import { DateItem, ITEM_MARGIN } from "./DateItem"; +} from 'date-fns'; +import React, { useCallback, useMemo, useRef } from 'react'; +import { FlatList, useWindowDimensions, I18nManager } from 'react-native'; +import { DateItem, ITEM_MARGIN } from './DateItem'; type DateScrollerProps = { selectedDate: Date; @@ -18,11 +18,7 @@ type DateScrollerProps = { locale: Locale; }; -const DateScrollerComponent = ({ - selectedDate, - onDateSelect, - locale, -}: DateScrollerProps) => { +const DateScrollerComponent = ({ selectedDate, onDateSelect, locale }: DateScrollerProps) => { const flatListRef = useRef(null); const { width: screenWidth } = useWindowDimensions(); @@ -30,8 +26,7 @@ const DateScrollerComponent = ({ const containerPadding = 32; const itemWidth = - (screenWidth - containerPadding - numberOfVisibleDays * ITEM_MARGIN * 2) / - numberOfVisibleDays; + (screenWidth - containerPadding - numberOfVisibleDays * ITEM_MARGIN * 2) / numberOfVisibleDays; const dateRange = useMemo(() => { const today = new Date(); diff --git a/src/features/settings/DevMenuModal.tsx b/src/features/settings/DevMenuModal.tsx index 01bae18..f070788 100644 --- a/src/features/settings/DevMenuModal.tsx +++ b/src/features/settings/DevMenuModal.tsx @@ -1,17 +1,11 @@ // src/features/settings/DevMenuModal.tsx -import { ThemedText } from "@/components/base/ThemedText"; -import { AppTheme } from "@/constants/theme"; -import i18n from "@/core/i18n"; -import useAppStore from "@/core/store/appStore"; -import { useTheme } from "@/core/theme/ThemeContext"; -import React from "react"; -import { - Modal, - StyleSheet, - Switch, - TouchableWithoutFeedback, - View, -} from "react-native"; +import { ThemedText } from '@/components/base/ThemedText'; +import { AppTheme } from '@/constants/theme'; +import i18n from '@/core/i18n'; +import useAppStore from '@/core/store/appStore'; +import { useTheme } from '@/core/theme/ThemeContext'; +import React from 'react'; +import { Modal, StyleSheet, Switch, TouchableWithoutFeedback, View } from 'react-native'; type DevMenuModalProps = { isVisible: boolean; @@ -25,23 +19,14 @@ export const DevMenuModal = ({ isVisible, onClose }: DevMenuModalProps) => { const isDemoMode = useAppStore((state) => state.settings.isDemoMode); return ( - + - - {i18n.t("devMenu.title")} - + {i18n.t('devMenu.title')} - - {i18n.t("devMenu.enableDemo")} - + {i18n.t('devMenu.enableDemo')} StyleSheet.create({ overlay: { flex: 1, - justifyContent: "center", - alignItems: "center", - backgroundColor: "rgba(0, 0, 0, 0.6)", + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0, 0, 0, 0.6)', padding: theme.spacing.l, }, container: { - width: "100%", + width: '100%', backgroundColor: theme.colors.foreground, borderRadius: 16, padding: theme.spacing.m, @@ -78,13 +63,13 @@ const getStyles = (theme: AppTheme) => title: { fontSize: theme.typography.fontSize.l, fontWeight: theme.typography.fontWeight.semibold, - textAlign: "center", + textAlign: 'center', marginBottom: theme.spacing.m, }, optionRow: { - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', paddingVertical: theme.spacing.m, paddingHorizontal: theme.spacing.s, }, diff --git a/src/features/settings/SettingsListItem.tsx b/src/features/settings/SettingsListItem.tsx index 19be26c..4b55dd5 100644 --- a/src/features/settings/SettingsListItem.tsx +++ b/src/features/settings/SettingsListItem.tsx @@ -1,19 +1,14 @@ // src/features/settings/SettingsListItem.tsx -import { ThemedText } from "@/components/base/ThemedText"; -import { AppTheme } from "@/constants/theme"; -import useAppStore from "@/core/store/appStore"; -import { useTheme } from "@/core/theme/ThemeContext"; -import { triggerHaptic } from "@/core/utils/haptics"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import React from "react"; -import { - StyleSheet, - Switch, - TouchableOpacity, - I18nManager, -} from "react-native"; -import { SettingsItem } from "./settingsData"; -import i18n from "@/core/i18n"; +import { ThemedText } from '@/components/base/ThemedText'; +import { AppTheme } from '@/constants/theme'; +import useAppStore from '@/core/store/appStore'; +import { useTheme } from '@/core/theme/ThemeContext'; +import { triggerHaptic } from '@/core/utils/haptics'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import React from 'react'; +import { StyleSheet, Switch, TouchableOpacity, I18nManager } from 'react-native'; +import { SettingsItem } from './settingsData'; +import i18n from '@/core/i18n'; type SettingsListItemProps = { item: SettingsItem; @@ -22,22 +17,17 @@ type SettingsListItemProps = { onPress: (item: SettingsItem) => void; }; -export const SettingsListItem = ({ - item, - isFirst, - isLast, - onPress, -}: SettingsListItemProps) => { +export const SettingsListItem = ({ item, isFirst, isLast, onPress }: SettingsListItemProps) => { const { theme } = useTheme(); const styles = getStyles(theme); const settings = useAppStore((state) => state.settings); const toggleHaptics = useAppStore((state) => state.toggleHaptics); - const value = item.type === "toggle" ? settings[item.stateKey] : false; + const value = item.type === 'toggle' ? settings[item.stateKey] : false; const handleToggle = () => { - if (item.type === "toggle" && item.stateKey === "isHapticsEnabled") { + if (item.type === 'toggle' && item.stateKey === 'isHapticsEnabled') { toggleHaptics(); triggerHaptic(); } @@ -48,39 +38,26 @@ export const SettingsListItem = ({ onPress(item); }; - const containerStyle = [ - styles.container, - isFirst && styles.isFirst, - isLast && styles.isLast, - ]; + const containerStyle = [styles.container, isFirst && styles.isFirst, isLast && styles.isLast]; return ( + disabled={item.type === 'toggle'} + activeOpacity={0.7}> + style={[styles.label, item.type === 'action' && item.color ? { color: item.color } : {}]}> {item.label} - {item.type === "toggle" && ( + {item.type === 'toggle' && ( )} - {(item.type === "navigation" || - item.type === "modal" || - (item.type === "action" && - item.label !== i18n.t("settings.privacy") && - !item.color)) && ( + {(item.type === 'navigation' || + item.type === 'modal' || + (item.type === 'action' && item.label !== i18n.t('settings.privacy') && !item.color)) && ( @@ -110,8 +85,8 @@ export const SettingsListItem = ({ const getStyles = (theme: AppTheme) => StyleSheet.create({ container: { - flexDirection: "row", - alignItems: "center", + flexDirection: 'row', + alignItems: 'center', paddingHorizontal: theme.spacing.m, paddingVertical: theme.spacing.s + 4, backgroundColor: theme.colors.foreground, diff --git a/src/features/settings/SettingsOptionModal.tsx b/src/features/settings/SettingsOptionModal.tsx index 10b52e1..116f211 100644 --- a/src/features/settings/SettingsOptionModal.tsx +++ b/src/features/settings/SettingsOptionModal.tsx @@ -1,29 +1,20 @@ // src/features/settings/SettingsOptionModal.tsx -import { ThemedText } from "@/components/base/ThemedText"; -import { AppTheme } from "@/constants/theme"; -import useAppStore, { AppSettings } from "@/core/store/appStore"; -import { useTheme } from "@/core/theme/ThemeContext"; -import { triggerHaptic } from "@/core/utils/haptics"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import React from "react"; -import { - Modal, - StyleSheet, - TouchableOpacity, - TouchableWithoutFeedback, - View, -} from "react-native"; -import { ModalSettingsItem } from "./settingsData"; +import { ThemedText } from '@/components/base/ThemedText'; +import { AppTheme } from '@/constants/theme'; +import useAppStore, { AppSettings } from '@/core/store/appStore'; +import { useTheme } from '@/core/theme/ThemeContext'; +import { triggerHaptic } from '@/core/utils/haptics'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import React from 'react'; +import { Modal, StyleSheet, TouchableOpacity, TouchableWithoutFeedback, View } from 'react-native'; +import { ModalSettingsItem } from './settingsData'; type SettingsOptionModalProps = { modalData: ModalSettingsItem | null; onClose: () => void; }; -export const SettingsOptionModal = ({ - modalData, - onClose, -}: SettingsOptionModalProps) => { +export const SettingsOptionModal = ({ modalData, onClose }: SettingsOptionModalProps) => { const { theme } = useTheme(); const styles = getStyles(theme); const { settings, setTheme, setLanguage } = useAppStore(); @@ -36,21 +27,16 @@ export const SettingsOptionModal = ({ triggerHaptic(); onClose(); setTimeout(() => { - if (modalData.stateKey === "theme") { + if (modalData.stateKey === 'theme') { setTheme(value); - } else if (modalData.stateKey === "language") { + } else if (modalData.stateKey === 'language') { setLanguage(value); } }, 250); }; return ( - + @@ -60,17 +46,10 @@ export const SettingsOptionModal = ({ handleSelect(option.value)} - > - - {option.label} - + onPress={() => handleSelect(option.value)}> + {option.label} {currentValue === option.value && ( - + )} ))} @@ -86,13 +65,13 @@ const getStyles = (theme: AppTheme) => StyleSheet.create({ overlay: { flex: 1, - justifyContent: "center", - alignItems: "center", - backgroundColor: "rgba(0, 0, 0, 0.6)", + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0, 0, 0, 0.6)', padding: theme.spacing.l, }, container: { - width: "100%", + width: '100%', backgroundColor: theme.colors.foreground, borderRadius: 16, padding: theme.spacing.m, @@ -100,12 +79,12 @@ const getStyles = (theme: AppTheme) => title: { fontSize: theme.typography.fontSize.l, fontWeight: theme.typography.fontWeight.semibold, - textAlign: "center", + textAlign: 'center', marginBottom: theme.spacing.m, }, optionRow: { - flexDirection: "row", - alignItems: "center", + flexDirection: 'row', + alignItems: 'center', paddingVertical: theme.spacing.m, paddingHorizontal: theme.spacing.s, }, diff --git a/src/features/settings/settingsData.ts b/src/features/settings/settingsData.ts index 2ba25ac..5b98eb8 100644 --- a/src/features/settings/settingsData.ts +++ b/src/features/settings/settingsData.ts @@ -1,35 +1,34 @@ // FILE: src/features/settings/settingsData.ts -import i18n from "@/core/i18n"; -import { AppSettings } from "@/core/store/appStore"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import { SettingsStackParamList } from "@/navigation/AppNavigator"; +import i18n from '@/core/i18n'; +import { AppSettings } from '@/core/store/appStore'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { SettingsStackParamList } from '@/navigation/AppNavigator'; // --- Type Definitions --- type BaseSettingsItem = { label: string; - icon: React.ComponentProps["name"]; + icon: React.ComponentProps['name']; }; export type NavigationSettingsItem = BaseSettingsItem & { - type: "navigation"; + type: 'navigation'; path: keyof SettingsStackParamList; }; export type ToggleSettingsItem = BaseSettingsItem & { - type: "toggle"; - stateKey: keyof Pick; + type: 'toggle'; + stateKey: keyof Pick; }; -export type ModalSettingsItem = - BaseSettingsItem & { - type: "modal"; - title: string; - stateKey: T; - options: { label: string; value: AppSettings[T] }[]; - }; +export type ModalSettingsItem = BaseSettingsItem & { + type: 'modal'; + title: string; + stateKey: T; + options: { label: string; value: AppSettings[T] }[]; +}; export type ActionSettingsItem = BaseSettingsItem & { - type: "action"; + type: 'action'; action: () => void; color?: string; }; @@ -37,8 +36,8 @@ export type ActionSettingsItem = BaseSettingsItem & { export type SettingsItem = | NavigationSettingsItem | ToggleSettingsItem - | ModalSettingsItem<"theme"> - | ModalSettingsItem<"language"> + | ModalSettingsItem<'theme'> + | ModalSettingsItem<'language'> | ActionSettingsItem; export type SettingsSection = { @@ -47,93 +46,91 @@ export type SettingsSection = { }; // --- Data Definition --- -export const getSettingsData = (actions: { - [key: string]: () => void; -}): SettingsSection[] => [ +export const getSettingsData = (actions: { [key: string]: () => void }): SettingsSection[] => [ { - title: i18n.t("settings.mainSettingsTitle"), + title: i18n.t('settings.mainSettingsTitle'), data: [ { - type: "navigation", - label: i18n.t("settings.deedManager"), - icon: "format-list-bulleted-square", - path: "DeedManager", + type: 'navigation', + label: i18n.t('settings.deedManager'), + icon: 'format-list-bulleted-square', + path: 'DeedManager', }, { - type: "navigation", - label: i18n.t("settings.notifications"), - icon: "bell-outline", - path: "Notifications", + type: 'navigation', + label: i18n.t('settings.notifications'), + icon: 'bell-outline', + path: 'Notifications', }, { - type: "modal", - label: i18n.t("settings.appearance"), - icon: "palette-outline", - title: i18n.t("settings.appearanceTitle"), - stateKey: "theme", + type: 'modal', + label: i18n.t('settings.appearance'), + icon: 'palette-outline', + title: i18n.t('settings.appearanceTitle'), + stateKey: 'theme', options: [ - { label: i18n.t("settings.system"), value: "system" }, - { label: i18n.t("settings.light"), value: "light" }, - { label: i18n.t("settings.dark"), value: "dark" }, + { label: i18n.t('settings.system'), value: 'system' }, + { label: i18n.t('settings.light'), value: 'light' }, + { label: i18n.t('settings.dark'), value: 'dark' }, ], }, { - type: "toggle", - label: i18n.t("settings.haptics"), - icon: "vibrate", - stateKey: "isHapticsEnabled", + type: 'toggle', + label: i18n.t('settings.haptics'), + icon: 'vibrate', + stateKey: 'isHapticsEnabled', }, { - type: "modal", - label: i18n.t("settings.appLanguage"), - icon: "translate", - title: i18n.t("settings.languageTitle"), - stateKey: "language", + type: 'modal', + label: i18n.t('settings.appLanguage'), + icon: 'translate', + title: i18n.t('settings.languageTitle'), + stateKey: 'language', options: [ - { label: i18n.t("settings.english"), value: "en" }, - { label: i18n.t("settings.arabic"), value: "ar" }, + { label: i18n.t('settings.english'), value: 'en' }, + { label: i18n.t('settings.arabic'), value: 'ar' }, ], }, ], }, { - title: i18n.t("settings.supportTitle"), + title: i18n.t('settings.supportTitle'), data: [ { - type: "action", - label: i18n.t("settings.invite"), - icon: "share-variant-outline", + type: 'action', + label: i18n.t('settings.invite'), + icon: 'share-variant-outline', action: actions.invite, }, { - type: "action", - label: i18n.t("settings.rate"), - icon: "star-outline", + type: 'action', + label: i18n.t('settings.rate'), + icon: 'star-outline', action: actions.rate, }, { - type: "action", - label: i18n.t("settings.feedback"), - icon: "email-outline", + type: 'action', + label: i18n.t('settings.feedback'), + icon: 'email-outline', action: actions.feedback, }, { - type: "action", - label: i18n.t("settings.privacy"), - icon: "shield-lock-outline", + type: 'action', + label: i18n.t('settings.privacy'), + icon: 'shield-lock-outline', action: actions.privacy, }, ], }, { - title: i18n.t("settings.dangerZoneTitle"), + title: i18n.t('settings.dangerZoneTitle'), data: [ { - type: "action", - label: i18n.t("settings.reset"), - icon: "alert-circle-outline", + type: 'action', + label: i18n.t('settings.reset'), + icon: 'alert-circle-outline', action: actions.reset, - color: "red", + color: 'red', }, ], }, diff --git a/src/features/stats/ActivityGraph.tsx b/src/features/stats/ActivityGraph.tsx index 4421dbb..f3f2cd2 100644 --- a/src/features/stats/ActivityGraph.tsx +++ b/src/features/stats/ActivityGraph.tsx @@ -1,18 +1,12 @@ // FILE: src/features/stats/ActivityGraph.tsx -import { AppTheme } from "@/constants/theme"; -import { Deed, DeedLog } from "@/core/data/models"; -import { useTheme } from "@/core/theme/ThemeContext"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import { formatISO } from "date-fns"; -import React, { - useRef, - useEffect, - useState, - useMemo, - useCallback, -} from "react"; -import { FlatList, LayoutChangeEvent, StyleSheet, View } from "react-native"; -import { GraphColumn, GraphColumnData } from "./GraphColumn"; +import { AppTheme } from '@/constants/theme'; +import { Deed, DeedLog } from '@/core/data/models'; +import { useTheme } from '@/core/theme/ThemeContext'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { formatISO } from 'date-fns'; +import React, { useRef, useEffect, useState, useMemo, useCallback } from 'react'; +import { FlatList, LayoutChangeEvent, StyleSheet, View } from 'react-native'; +import { GraphColumn, GraphColumnData } from './GraphColumn'; type ActivityGraphProps = { deeds: Deed[]; @@ -20,11 +14,7 @@ type ActivityGraphProps = { dateRange: Date[]; }; -export const ActivityGraph = ({ - deeds, - logs, - dateRange, -}: ActivityGraphProps) => { +export const ActivityGraph = ({ deeds, logs, dateRange }: ActivityGraphProps) => { const { theme } = useTheme(); const [containerWidth, setContainerWidth] = useState(0); const flatListRef = useRef(null); @@ -42,17 +32,13 @@ export const ActivityGraph = ({ } return dateRange.map((date) => { - const dateString = formatISO(date, { representation: "date" }); + const dateString = formatISO(date, { representation: 'date' }); const dailyLogs = logsByDate.get(dateString) || []; const cells = deeds.map((deed) => { const log = dailyLogs.find((l) => l.deedId === deed.id); - const status = log - ? deed.statuses.find((s) => s.id === log.statusId) - : null; - const cellColor = status - ? theme.colors[status.color] - : theme.colors.background; + const status = log ? deed.statuses.find((s) => s.id === log.statusId) : null; + const cellColor = status ? theme.colors[status.color] : theme.colors.background; return { id: deed.id, color: cellColor, @@ -72,21 +58,16 @@ export const ActivityGraph = ({ useEffect(() => { if (containerWidth > 0) { - setTimeout( - () => flatListRef.current?.scrollToEnd({ animated: false }), - 0, - ); + setTimeout(() => flatListRef.current?.scrollToEnd({ animated: false }), 0); } }, [containerWidth]); const yAxisWidth = 24; const daysToShow = 10; - const availableGridWidth = - containerWidth - yAxisWidth - theme.spacing.s - theme.spacing.m; + const availableGridWidth = containerWidth - yAxisWidth - theme.spacing.s - theme.spacing.m; - const totalWidthPerColumn = - containerWidth > 0 ? availableGridWidth / daysToShow : 0; + const totalWidthPerColumn = containerWidth > 0 ? availableGridWidth / daysToShow : 0; const cellSize = (totalWidthPerColumn - 4) * 0.8; @@ -94,13 +75,7 @@ export const ActivityGraph = ({ const renderItem = useCallback( ({ item }: { item: GraphColumnData }) => { - return ( - - ); + return ; }, [cellSize, totalWidthPerColumn], ); @@ -150,14 +125,14 @@ const getStyles = (theme: AppTheme, cellSize: number) => height: (cellSize + 4) * 5 + 16 + theme.spacing.m * 2, }, container: { - flexDirection: "row", + flexDirection: 'row', backgroundColor: theme.colors.foreground, borderRadius: 16, paddingVertical: theme.spacing.m, paddingRight: theme.spacing.m, }, yAxis: { - justifyContent: "flex-start", + justifyContent: 'flex-start', marginLeft: theme.spacing.s, }, yAxisHeaderSpacer: { @@ -166,7 +141,7 @@ const getStyles = (theme: AppTheme, cellSize: number) => }, yAxisItem: { height: cellSize + 4, - justifyContent: "center", - alignItems: "center", + justifyContent: 'center', + alignItems: 'center', }, }); diff --git a/src/features/stats/DeedStatsRow.tsx b/src/features/stats/DeedStatsRow.tsx index d99978e..4d309fe 100644 --- a/src/features/stats/DeedStatsRow.tsx +++ b/src/features/stats/DeedStatsRow.tsx @@ -1,15 +1,15 @@ // src/features/stats/DeedStatsRow.tsx -import { ThemedText } from "@/components/base/ThemedText"; -import { AppTheme, ColorTheme } from "@/constants/theme"; -import { Deed, DeedLog } from "@/core/data/models"; -import { useTheme } from "@/core/theme/ThemeContext"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import React, { useMemo } from "react"; -import { StyleSheet, View, I18nManager } from "react-native"; -import { colors } from "@/constants/colors"; -import i18n from "@/core/i18n"; +import { ThemedText } from '@/components/base/ThemedText'; +import { AppTheme, ColorTheme } from '@/constants/theme'; +import { Deed, DeedLog } from '@/core/data/models'; +import { useTheme } from '@/core/theme/ThemeContext'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import React, { useMemo } from 'react'; +import { StyleSheet, View, I18nManager } from 'react-native'; +import { colors } from '@/constants/colors'; +import i18n from '@/core/i18n'; -const PRAYER_STATUS_BREAKDOWN_ORDER = ["jamaah", "on-time", "late", "missed"]; +const PRAYER_STATUS_BREAKDOWN_ORDER = ['jamaah', 'on-time', 'late', 'missed']; const isColorLight = (hexColor: string) => { const r = parseInt(hexColor.slice(1, 3), 16); @@ -18,13 +18,7 @@ const isColorLight = (hexColor: string) => { return r * 0.299 + g * 0.587 + b * 0.114 > 186; }; -export const DeedStatsRow = ({ - deed, - logs, -}: { - deed: Deed; - logs: DeedLog[]; -}) => { +export const DeedStatsRow = ({ deed, logs }: { deed: Deed; logs: DeedLog[] }) => { const { theme } = useTheme(); const styles = getStyles(theme); @@ -48,9 +42,7 @@ export const DeedStatsRow = ({ id: statusId, percentage: (count / total) * 100, color: theme.colors[colorKey], - textColor: isColorLight(hexColor) - ? theme.colors.text - : theme.colors.primaryContrast, + textColor: isColorLight(hexColor) ? theme.colors.text : theme.colors.primaryContrast, }; }).filter((item) => item.percentage > 0); }, [logs, deed, theme.colors]); @@ -73,12 +65,9 @@ export const DeedStatsRow = ({ style={[ styles.barSegment, { width: `${stat.percentage}%`, backgroundColor: stat.color }, - ]} - > + ]}> {stat.percentage >= 15 && ( - + {stat.percentage.toFixed(0)}% )} @@ -92,30 +81,30 @@ export const DeedStatsRow = ({ const getStyles = (theme: AppTheme) => StyleSheet.create({ deedStatRow: { - flexDirection: I18nManager.isRTL ? "row-reverse" : "row", - alignItems: "center", + flexDirection: I18nManager.isRTL ? 'row-reverse' : 'row', + alignItems: 'center', marginBottom: theme.spacing.m, }, deedStatIcon: { marginHorizontal: theme.spacing.m, }, deedStatName: { - width: "25%", + width: '25%', fontWeight: theme.typography.fontWeight.semibold, - textAlign: I18nManager.isRTL ? "right" : "left", + textAlign: I18nManager.isRTL ? 'right' : 'left', }, deedStatBar: { flex: 1, height: 24, borderRadius: 12, backgroundColor: theme.colors.background, - flexDirection: "row", - overflow: "hidden", + flexDirection: 'row', + overflow: 'hidden', }, barSegment: { - height: "100%", - justifyContent: "center", - alignItems: "center", + height: '100%', + justifyContent: 'center', + alignItems: 'center', }, percentageText: { fontSize: theme.typography.fontSize.xs, diff --git a/src/features/stats/ExpandableSection.tsx b/src/features/stats/ExpandableSection.tsx index 0cd0ebf..5e84a20 100644 --- a/src/features/stats/ExpandableSection.tsx +++ b/src/features/stats/ExpandableSection.tsx @@ -1,30 +1,18 @@ // src/features/stats/ExpandableSection.tsx -import { ThemedText } from "@/components/base/ThemedText"; -import { AppTheme } from "@/constants/theme"; -import { useTheme } from "@/core/theme/ThemeContext"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import React, { useState } from "react"; -import { - LayoutChangeEvent, - StyleSheet, - TouchableOpacity, - View, -} from "react-native"; -import Animated, { - useAnimatedStyle, - useSharedValue, - withTiming, -} from "react-native-reanimated"; +import { ThemedText } from '@/components/base/ThemedText'; +import { AppTheme } from '@/constants/theme'; +import { useTheme } from '@/core/theme/ThemeContext'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import React, { useState } from 'react'; +import { LayoutChangeEvent, StyleSheet, TouchableOpacity, View } from 'react-native'; +import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; type ExpandableSectionProps = { title: string; children: React.ReactNode; }; -export const ExpandableSection = ({ - title, - children, -}: ExpandableSectionProps) => { +export const ExpandableSection = ({ title, children }: ExpandableSectionProps) => { const { theme } = useTheme(); const styles = getStyles(theme); const [isExpanded, setIsExpanded] = useState(true); @@ -67,11 +55,7 @@ export const ExpandableSection = ({ return ( - + {title} wrapper: { backgroundColor: theme.colors.foreground, borderRadius: 16, - overflow: "hidden", + overflow: 'hidden', }, header: { - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', padding: theme.spacing.m, }, title: { fontWeight: theme.typography.fontWeight.semibold, }, contentContainer: { - overflow: "hidden", + overflow: 'hidden', }, content: { - position: "absolute", - width: "100%", + position: 'absolute', + width: '100%', padding: theme.spacing.m, paddingTop: 0, }, diff --git a/src/features/stats/GoalStatsRow.tsx b/src/features/stats/GoalStatsRow.tsx index a3d4ad6..f873565 100644 --- a/src/features/stats/GoalStatsRow.tsx +++ b/src/features/stats/GoalStatsRow.tsx @@ -1,20 +1,14 @@ // src/features/stats/GoalStatsRow.tsx -import { ThemedText } from "@/components/base/ThemedText"; -import { AppTheme } from "@/constants/theme"; -import { Deed, DeedLog } from "@/core/data/models"; -import i18n from "@/core/i18n"; -import { useTheme } from "@/core/theme/ThemeContext"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import React, { useMemo } from "react"; -import { StyleSheet, View } from "react-native"; +import { ThemedText } from '@/components/base/ThemedText'; +import { AppTheme } from '@/constants/theme'; +import { Deed, DeedLog } from '@/core/data/models'; +import i18n from '@/core/i18n'; +import { useTheme } from '@/core/theme/ThemeContext'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import React, { useMemo } from 'react'; +import { StyleSheet, View } from 'react-native'; -export const GoalStatsRow = ({ - deed, - logs, -}: { - deed: Deed; - logs: DeedLog[]; -}) => { +export const GoalStatsRow = ({ deed, logs }: { deed: Deed; logs: DeedLog[] }) => { const { theme } = useTheme(); const styles = getStyles(theme); @@ -37,27 +31,16 @@ export const GoalStatsRow = ({ return ( - + {i18n.t(`deeds_names.${deed.id}`, { defaultValue: deed.name })} - {`${totalProgress} / ${deed.goal.value * logs.length} ${ - deed.goal.unit - }`} + {`${totalProgress} / ${deed.goal.value * logs.length} ${deed.goal.unit}`} - + ); @@ -69,8 +52,8 @@ const getStyles = (theme: AppTheme) => marginBottom: theme.spacing.m, }, header: { - flexDirection: "row", - alignItems: "center", + flexDirection: 'row', + alignItems: 'center', marginBottom: theme.spacing.s, }, deedName: { @@ -86,10 +69,10 @@ const getStyles = (theme: AppTheme) => height: 8, borderRadius: 4, backgroundColor: theme.colors.background, - overflow: "hidden", + overflow: 'hidden', }, progressBarFill: { - height: "100%", + height: '100%', backgroundColor: theme.colors.primary, }, }); diff --git a/src/features/stats/GraphColumn.tsx b/src/features/stats/GraphColumn.tsx index 70c8e3d..1ee7e6c 100644 --- a/src/features/stats/GraphColumn.tsx +++ b/src/features/stats/GraphColumn.tsx @@ -1,11 +1,11 @@ // FILE: src/features/stats/GraphColumn.tsx // FILE: src/features/stats/GraphColumn.tsx -import { AppTheme } from "@/constants/theme"; -import { useTheme } from "@/core/theme/ThemeContext"; -import { format } from "date-fns"; -import React from "react"; -import { StyleSheet, View } from "react-native"; -import { ThemedText } from "@/components/base/ThemedText"; +import { AppTheme } from '@/constants/theme'; +import { useTheme } from '@/core/theme/ThemeContext'; +import { format } from 'date-fns'; +import React from 'react'; +import { StyleSheet, View } from 'react-native'; +import { ThemedText } from '@/components/base/ThemedText'; // Define the shape of the pre-computed data export type GraphColumnData = { @@ -22,22 +22,15 @@ type GraphColumnProps = { columnWidth: number; }; -const GraphColumnComponent = ({ - data, - cellSize, - columnWidth, -}: GraphColumnProps) => { +const GraphColumnComponent = ({ data, cellSize, columnWidth }: GraphColumnProps) => { const { theme } = useTheme(); const styles = getStyles(theme, cellSize); return ( - {format(data.date, "d")} + {format(data.date, 'd')} {data.cells.map((cell) => ( - + ))} ); @@ -48,8 +41,8 @@ export const GraphColumn = React.memo(GraphColumnComponent); const getStyles = (theme: AppTheme, cellSize: number) => StyleSheet.create({ column: { - justifyContent: "flex-start", - alignItems: "center", + justifyContent: 'flex-start', + alignItems: 'center', paddingHorizontal: 2, }, dayText: { diff --git a/src/features/stats/IntervalSwitcher.tsx b/src/features/stats/IntervalSwitcher.tsx index c55b9d8..ce8391b 100644 --- a/src/features/stats/IntervalSwitcher.tsx +++ b/src/features/stats/IntervalSwitcher.tsx @@ -1,47 +1,36 @@ // src/features/stats/IntervalSwitcher.tsx -import { ThemedText } from "@/components/base/ThemedText"; -import { AppTheme } from "@/constants/theme"; -import i18n from "@/core/i18n"; -import { useTheme } from "@/core/theme/ThemeContext"; -import React from "react"; -import { StyleSheet, TouchableOpacity, View } from "react-native"; +import { ThemedText } from '@/components/base/ThemedText'; +import { AppTheme } from '@/constants/theme'; +import i18n from '@/core/i18n'; +import { useTheme } from '@/core/theme/ThemeContext'; +import React from 'react'; +import { StyleSheet, TouchableOpacity, View } from 'react-native'; -export type TimeInterval = "week" | "month" | "year" | "all"; +export type TimeInterval = 'week' | 'month' | 'year' | 'all'; type IntervalSwitcherProps = { selected: TimeInterval; onSelect: (interval: TimeInterval) => void; }; -export const IntervalSwitcher = ({ - selected, - onSelect, -}: IntervalSwitcherProps) => { +export const IntervalSwitcher = ({ selected, onSelect }: IntervalSwitcherProps) => { const { theme } = useTheme(); const styles = getStyles(theme); const options: { label: string; value: TimeInterval }[] = [ - { label: i18n.t("intervals.week"), value: "week" }, - { label: i18n.t("intervals.month"), value: "month" }, - { label: i18n.t("intervals.year"), value: "year" }, - { label: i18n.t("intervals.allTime"), value: "all" }, + { label: i18n.t('intervals.week'), value: 'week' }, + { label: i18n.t('intervals.month'), value: 'month' }, + { label: i18n.t('intervals.year'), value: 'year' }, + { label: i18n.t('intervals.allTime'), value: 'all' }, ]; return ( {options.map((opt) => ( onSelect(opt.value)} - > + style={[styles.switcherButton, selected === opt.value && styles.switcherSelected]} + onPress={() => onSelect(opt.value)}> + style={[styles.switcherText, selected === opt.value && styles.switcherSelectedText]}> {opt.label} @@ -53,17 +42,17 @@ export const IntervalSwitcher = ({ const getStyles = (theme: AppTheme) => StyleSheet.create({ switcherContainer: { - flexDirection: "row", + flexDirection: 'row', backgroundColor: theme.colors.foreground, borderRadius: 8, - overflow: "hidden", + overflow: 'hidden', marginBottom: theme.spacing.m, marginTop: theme.spacing.m, // Added top margin }, switcherButton: { flex: 1, padding: theme.spacing.m, - alignItems: "center", + alignItems: 'center', }, switcherSelected: { backgroundColor: theme.colors.primary, diff --git a/src/features/stats/SegmentedControl.tsx b/src/features/stats/SegmentedControl.tsx index ea28edf..9f2bce2 100644 --- a/src/features/stats/SegmentedControl.tsx +++ b/src/features/stats/SegmentedControl.tsx @@ -1,15 +1,12 @@ // src/features/stats/SegmentedControl.tsx -import React from "react"; -import { View, StyleSheet, useWindowDimensions } from "react-native"; -import Animated, { - useAnimatedStyle, - withSpring, -} from "react-native-reanimated"; -import { AppTheme } from "@/constants/theme"; -import { useTheme } from "@/core/theme/ThemeContext"; -import { ThemedText } from "@/components/base/ThemedText"; -import { TimeInterval } from "./types"; -import { TouchableOpacity } from "react-native-gesture-handler"; +import React from 'react'; +import { View, StyleSheet, useWindowDimensions } from 'react-native'; +import Animated, { useAnimatedStyle, withSpring } from 'react-native-reanimated'; +import { AppTheme } from '@/constants/theme'; +import { useTheme } from '@/core/theme/ThemeContext'; +import { ThemedText } from '@/components/base/ThemedText'; +import { TimeInterval } from './types'; +import { TouchableOpacity } from 'react-native-gesture-handler'; type Option = { label: string; @@ -22,11 +19,7 @@ type SegmentedControlProps = { onSelect: (interval: TimeInterval) => void; }; -export const SegmentedControl = ({ - options, - selected, - onSelect, -}: SegmentedControlProps) => { +export const SegmentedControl = ({ options, selected, onSelect }: SegmentedControlProps) => { const { theme } = useTheme(); const styles = getStyles(theme); const { width } = useWindowDimensions(); @@ -52,25 +45,13 @@ export const SegmentedControl = ({ return ( - + {options.map((opt) => ( onSelect(opt.value)} - > - + onPress={() => onSelect(opt.value)}> + {opt.label} @@ -82,7 +63,7 @@ export const SegmentedControl = ({ const getStyles = (theme: AppTheme) => StyleSheet.create({ container: { - flexDirection: "row", + flexDirection: 'row', backgroundColor: theme.colors.background, borderRadius: 12, padding: 4, @@ -90,13 +71,13 @@ const getStyles = (theme: AppTheme) => marginVertical: theme.spacing.m, }, indicator: { - position: "absolute", + position: 'absolute', top: 4, left: 4, - height: "100%", + height: '100%', backgroundColor: theme.colors.foreground, borderRadius: 8, - shadowColor: "#000", + shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 2, @@ -104,8 +85,8 @@ const getStyles = (theme: AppTheme) => }, segment: { flex: 1, - justifyContent: "center", - alignItems: "center", + justifyContent: 'center', + alignItems: 'center', }, segmentText: { fontWeight: theme.typography.fontWeight.semibold, diff --git a/src/features/stats/SummaryBox.tsx b/src/features/stats/SummaryBox.tsx index 86dfafc..c735f02 100644 --- a/src/features/stats/SummaryBox.tsx +++ b/src/features/stats/SummaryBox.tsx @@ -1,10 +1,10 @@ // src/features/stats/SummaryBox.tsx -import { ThemedText } from "@/components/base/ThemedText"; -import { AppTheme } from "@/constants/theme"; -import i18n from "@/core/i18n"; -import { useTheme } from "@/core/theme/ThemeContext"; -import React from "react"; -import { StyleSheet, View } from "react-native"; +import { ThemedText } from '@/components/base/ThemedText'; +import { AppTheme } from '@/constants/theme'; +import i18n from '@/core/i18n'; +import { useTheme } from '@/core/theme/ThemeContext'; +import React from 'react'; +import { StyleSheet, View } from 'react-native'; type SummaryBoxProps = { title: string; @@ -13,32 +13,20 @@ type SummaryBoxProps = { count: number; }; -export const SummaryBox = ({ - title, - color, - percentage, - count, -}: SummaryBoxProps) => { +export const SummaryBox = ({ title, color, percentage, count }: SummaryBoxProps) => { const { theme } = useTheme(); const styles = getStyles(theme); return ( - + {title} - - {percentage.toFixed(0)}% - - - {i18n.t("stats.times", { count })} - + {percentage.toFixed(0)}% + {i18n.t('stats.times', { count })} @@ -48,7 +36,7 @@ export const SummaryBox = ({ const getStyles = (theme: AppTheme) => StyleSheet.create({ summaryBox: { - width: "48%", + width: '48%', backgroundColor: theme.colors.foreground, borderRadius: 16, padding: theme.spacing.m, @@ -78,9 +66,9 @@ const getStyles = (theme: AppTheme) => borderRadius: 2, backgroundColor: theme.colors.background, marginTop: theme.spacing.m, - overflow: "hidden", + overflow: 'hidden', }, summaryBarFill: { - height: "100%", + height: '100%', }, }); diff --git a/src/features/stats/types.ts b/src/features/stats/types.ts index 0150f4e..6705b44 100644 --- a/src/features/stats/types.ts +++ b/src/features/stats/types.ts @@ -1,2 +1,2 @@ // src/features/stats/types.ts -export type TimeInterval = "week" | "month" | "year" | "all"; +export type TimeInterval = 'week' | 'month' | 'year' | 'all'; diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index 8a6a64e..f227b80 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -1,23 +1,23 @@ // FILE: src/navigation/AppNavigator.tsx -import React from "react"; -import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; -import { createNativeStackNavigator } from "@react-navigation/native-stack"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import { useTheme } from "@/core/theme/ThemeContext"; -import i18n from "@/core/i18n"; -import { NavigatorScreenParams } from "@react-navigation/native"; +import React from 'react'; +import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; +import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { useTheme } from '@/core/theme/ThemeContext'; +import i18n from '@/core/i18n'; +import { NavigatorScreenParams } from '@react-navigation/native'; // Import Screens -import StatsScreen from "@/screens/StatsScreen"; -import HomeScreen from "@/screens/HomeScreen"; -import AddDeedScreen from "@/screens/AddDeedScreen"; -import CreateDeedScreen from "@/screens/CreateDeedScreen"; -import ConfigureFrequencyScreen from "@/screens/ConfigureFrequencyScreen"; -import ConfigureGoalScreen from "@/screens/ConfigureGoalScreen"; -import ConfigureParentScreen from "@/screens/ConfigureParentScreen"; -import SettingsScreen from "@/screens/SettingsScreen"; -import DeedManagerScreen from "@/screens/DeedManagerScreen"; -import NotificationsScreen from "@/screens/NotificationsScreen"; +import StatsScreen from '@/screens/StatsScreen'; +import HomeScreen from '@/screens/HomeScreen'; +import AddDeedScreen from '@/screens/AddDeedScreen'; +import CreateDeedScreen from '@/screens/CreateDeedScreen'; +import ConfigureFrequencyScreen from '@/screens/ConfigureFrequencyScreen'; +import ConfigureGoalScreen from '@/screens/ConfigureGoalScreen'; +import ConfigureParentScreen from '@/screens/ConfigureParentScreen'; +import SettingsScreen from '@/screens/SettingsScreen'; +import DeedManagerScreen from '@/screens/DeedManagerScreen'; +import NotificationsScreen from '@/screens/NotificationsScreen'; // Define Param Lists for Type Safety export type HomeStackParamList = { @@ -47,12 +47,10 @@ const HomeStack = createNativeStackNavigator(); const SettingsStack = createNativeStackNavigator(); function TabBarIcon(props: { - name: React.ComponentProps["name"]; + name: React.ComponentProps['name']; color: string; }) { - return ( - - ); + return ; } // Main Tab Navigator @@ -71,47 +69,31 @@ export default function AppNavigator() { }, headerShown: false, }} - initialRouteName="Home" - > + initialRouteName="Home"> ( - - ), + title: i18n.t('tabs.stats'), + tabBarLabel: i18n.t('tabs.stats'), + tabBarIcon: ({ color }) => , }} /> , - }} - > + title: i18n.t('tabs.home'), + tabBarLabel: i18n.t('tabs.home'), + tabBarIcon: ({ color }) => , + }}> {() => ( - + - - - + + + )} @@ -119,28 +101,15 @@ export default function AppNavigator() { name="Settings" options={{ headerShown: false, - title: i18n.t("tabs.settings"), - tabBarLabel: i18n.t("tabs.settings"), - tabBarIcon: ({ color }) => , - }} - > + title: i18n.t('tabs.settings'), + tabBarLabel: i18n.t('tabs.settings'), + tabBarIcon: ({ color }) => , + }}> {() => ( - - - - + + + + )} diff --git a/src/screens/AddDeedScreen.tsx b/src/screens/AddDeedScreen.tsx index 911f432..dbf7214 100644 --- a/src/screens/AddDeedScreen.tsx +++ b/src/screens/AddDeedScreen.tsx @@ -1,23 +1,20 @@ // FILE: src/screens/AddDeedScreen.tsx -import { Screen } from "@/components/Screen"; -import { ThemedText } from "@/components/base/ThemedText"; -import { AppTheme } from "@/constants/theme"; -import { Deed } from "@/core/data/models"; -import i18n from "@/core/i18n"; -import useAppStore from "@/core/store/appStore"; -import { useTheme } from "@/core/theme/ThemeContext"; -import { SuggestedDeedListItem } from "@/features/deeds/SuggestedDeedListItem"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import { useNavigation } from "@react-navigation/native"; -import React, { useMemo } from "react"; -import { SectionList, StyleSheet, TouchableOpacity } from "react-native"; -import { HomeStackParamList } from "@/navigation/AppNavigator"; -import { NativeStackNavigationProp } from "@react-navigation/native-stack"; +import { Screen } from '@/components/Screen'; +import { ThemedText } from '@/components/base/ThemedText'; +import { AppTheme } from '@/constants/theme'; +import { Deed } from '@/core/data/models'; +import i18n from '@/core/i18n'; +import useAppStore from '@/core/store/appStore'; +import { useTheme } from '@/core/theme/ThemeContext'; +import { SuggestedDeedListItem } from '@/features/deeds/SuggestedDeedListItem'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { useNavigation } from '@react-navigation/native'; +import React, { useMemo } from 'react'; +import { SectionList, StyleSheet, TouchableOpacity } from 'react-native'; +import { HomeStackParamList } from '@/navigation/AppNavigator'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; -type AddDeedScreenNavigationProp = NativeStackNavigationProp< - HomeStackParamList, - "AddDeed" ->; +type AddDeedScreenNavigationProp = NativeStackNavigationProp; const AddDeedScreen = () => { const navigation = useNavigation(); @@ -29,10 +26,7 @@ const AddDeedScreen = () => { const suggestedDeeds = useAppStore((state) => state.suggestedDeeds); const addDeed = useAppStore((state) => state.addDeed); - const userDeedIds = useMemo( - () => new Set(userDeeds.map((d) => d.id)), - [userDeeds], - ); + const userDeedIds = useMemo(() => new Set(userDeeds.map((d) => d.id)), [userDeeds]); const suggestedDeedsBySection = useMemo(() => { const sections: { title: string; data: Deed[] }[] = []; @@ -55,17 +49,12 @@ const AddDeedScreen = () => { return ( ( navigation.goBack()}> - + - )} - > + )}> item.id} @@ -82,15 +71,10 @@ const AddDeedScreen = () => { ListHeaderComponent={ navigation.navigate("CreateDeed", {})} - > - + onPress={() => navigation.navigate('CreateDeed', {})}> + - {i18n.t("deeds.createDeedButton")} + {i18n.t('deeds.createDeedButton')} } @@ -108,7 +92,7 @@ const getStyles = (theme: AppTheme) => fontWeight: theme.typography.fontWeight.bold, fontSize: theme.typography.fontSize.s, color: theme.colors.textSecondary, - textTransform: "uppercase", + textTransform: 'uppercase', marginBottom: theme.spacing.s, marginTop: theme.spacing.l, }, @@ -117,8 +101,8 @@ const getStyles = (theme: AppTheme) => paddingTop: theme.spacing.s, }, createDeedButton: { - flexDirection: "row", - alignItems: "center", + flexDirection: 'row', + alignItems: 'center', padding: theme.spacing.m, backgroundColor: theme.colors.foreground, borderRadius: 8, diff --git a/src/screens/ConfigureFrequencyScreen.tsx b/src/screens/ConfigureFrequencyScreen.tsx index 3f96243..a0b8a27 100644 --- a/src/screens/ConfigureFrequencyScreen.tsx +++ b/src/screens/ConfigureFrequencyScreen.tsx @@ -1,42 +1,42 @@ // FILE: src/screens/ConfigureFrequencyScreen.tsx -import { Screen } from "@/components/Screen"; -import { ThemedText } from "@/components/base/ThemedText"; -import { AppTheme } from "@/constants/theme"; -import { DeedFrequency } from "@/core/data/models"; -import i18n from "@/core/i18n"; -import useAppStore from "@/core/store/appStore"; -import { useTheme } from "@/core/theme/ThemeContext"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import { useNavigation } from "@react-navigation/native"; -import React from "react"; -import { StyleSheet, TouchableOpacity, View, TextInput } from "react-native"; +import { Screen } from '@/components/Screen'; +import { ThemedText } from '@/components/base/ThemedText'; +import { AppTheme } from '@/constants/theme'; +import { DeedFrequency } from '@/core/data/models'; +import i18n from '@/core/i18n'; +import useAppStore from '@/core/store/appStore'; +import { useTheme } from '@/core/theme/ThemeContext'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { useNavigation } from '@react-navigation/native'; +import React from 'react'; +import { StyleSheet, TouchableOpacity, View, TextInput } from 'react-native'; const DAYS = [ - { label: i18n.t("frequency.days_abbr.sun"), value: 0 }, - { label: i18n.t("frequency.days_abbr.mon"), value: 1 }, - { label: i18n.t("frequency.days_abbr.tue"), value: 2 }, - { label: i18n.t("frequency.days_abbr.wed"), value: 3 }, - { label: i18n.t("frequency.days_abbr.thu"), value: 4 }, - { label: i18n.t("frequency.days_abbr.fri"), value: 5 }, - { label: i18n.t("frequency.days_abbr.sat"), value: 6 }, + { label: i18n.t('frequency.days_abbr.sun'), value: 0 }, + { label: i18n.t('frequency.days_abbr.mon'), value: 1 }, + { label: i18n.t('frequency.days_abbr.tue'), value: 2 }, + { label: i18n.t('frequency.days_abbr.wed'), value: 3 }, + { label: i18n.t('frequency.days_abbr.thu'), value: 4 }, + { label: i18n.t('frequency.days_abbr.fri'), value: 5 }, + { label: i18n.t('frequency.days_abbr.sat'), value: 6 }, ]; const ConfigureFrequencyScreen = () => { const navigation = useNavigation(); const { theme } = useTheme(); const styles = getStyles(theme); const { draftDeed, updateDraftDeed } = useAppStore(); - const frequency = draftDeed?.frequency || { type: "daily" }; - const setType = (type: DeedFrequency["type"]) => { + const frequency = draftDeed?.frequency || { type: 'daily' }; + const setType = (type: DeedFrequency['type']) => { const newFrequency: DeedFrequency = { type }; - if (type === "weekly") { + if (type === 'weekly') { newFrequency.days = frequency.days || []; - } else if (type === "monthly" || type === "yearly") { + } else if (type === 'monthly' || type === 'yearly') { newFrequency.count = frequency.count || 1; } updateDraftDeed({ frequency: newFrequency }); }; const toggleDay = (dayValue: number) => { - if (frequency.type !== "weekly") return; + if (frequency.type !== 'weekly') return; const currentDays = frequency.days || []; const newDays = currentDays.includes(dayValue) ? currentDays.filter((d) => d !== dayValue) @@ -51,48 +51,39 @@ const ConfigureFrequencyScreen = () => { }; return ( ( navigation.goBack()}> - + - )} - > + )}> - - {i18n.t("frequency.type")} - + {i18n.t('frequency.type')} setType("daily")} + label={i18n.t('frequency.daily')} + selected={frequency.type === 'daily'} + onPress={() => setType('daily')} /> setType("weekly")} + label={i18n.t('frequency.weekly')} + selected={frequency.type === 'weekly'} + onPress={() => setType('weekly')} /> setType("monthly")} + label={i18n.t('frequency.monthly')} + selected={frequency.type === 'monthly'} + onPress={() => setType('monthly')} /> setType("yearly")} + label={i18n.t('frequency.yearly')} + selected={frequency.type === 'yearly'} + onPress={() => setType('yearly')} /> - {frequency.type === "weekly" && ( + {frequency.type === 'weekly' && ( <> - - {i18n.t("frequency.selectDays")} - + {i18n.t('frequency.selectDays')} {DAYS.map((day) => ( { )} - {(frequency.type === "monthly" || frequency.type === "yearly") && ( + {(frequency.type === 'monthly' || frequency.type === 'yearly') && ( <> - {i18n.t("frequency.howManyTimes", { - interval: frequency.type.replace("ly", ""), + {i18n.t('frequency.howManyTimes', { + interval: frequency.type.replace('ly', ''), })} @@ -132,13 +123,8 @@ const OptionButton = ({ label, selected, onPress }) => { const { theme } = useTheme(); const styles = getStyles(theme); return ( - - - {label} - + + {label} ); }; @@ -146,13 +132,8 @@ const DayButton = ({ label, selected, onPress }) => { const { theme } = useTheme(); const styles = getStyles(theme); return ( - - - {label} - + + {label} ); }; @@ -164,16 +145,16 @@ const getStyles = (theme: AppTheme) => marginTop: theme.spacing.l, marginBottom: theme.spacing.s, color: theme.colors.textSecondary, - textTransform: "uppercase", + textTransform: 'uppercase', fontWeight: theme.typography.fontWeight.bold, }, optionGroup: { - flexDirection: "row", + flexDirection: 'row', backgroundColor: theme.colors.foreground, borderRadius: 8, - overflow: "hidden", + overflow: 'hidden', }, - optionButton: { flex: 1, padding: theme.spacing.m, alignItems: "center" }, + optionButton: { flex: 1, padding: theme.spacing.m, alignItems: 'center' }, selected: { backgroundColor: theme.colors.primary }, optionText: { fontWeight: theme.typography.fontWeight.semibold, @@ -182,8 +163,8 @@ const getStyles = (theme: AppTheme) => }, selectedText: { color: theme.colors.primaryContrast }, daySelector: { - flexDirection: "row", - justifyContent: "space-between", + flexDirection: 'row', + justifyContent: 'space-between', backgroundColor: theme.colors.foreground, borderRadius: 8, padding: theme.spacing.s, @@ -192,8 +173,8 @@ const getStyles = (theme: AppTheme) => flex: 1, height: 44, borderRadius: 8, - justifyContent: "center", - alignItems: "center", + justifyContent: 'center', + alignItems: 'center', marginHorizontal: theme.spacing.xs / 2, }, selectedDay: { backgroundColor: theme.colors.primary }, @@ -209,6 +190,6 @@ const getStyles = (theme: AppTheme) => countInput: { color: theme.colors.text, fontSize: theme.typography.fontSize.m, - textAlign: "center", + textAlign: 'center', }, }); diff --git a/src/screens/ConfigureGoalScreen.tsx b/src/screens/ConfigureGoalScreen.tsx index 9fd4e1e..562d591 100644 --- a/src/screens/ConfigureGoalScreen.tsx +++ b/src/screens/ConfigureGoalScreen.tsx @@ -1,15 +1,15 @@ // FILE: src/screens/ConfigureGoalScreen.tsx -import { Screen } from "@/components/Screen"; -import { ThemedText } from "@/components/base/ThemedText"; -import { ThemedTextInput } from "@/components/base/ThemedTextInput"; -import { AppTheme } from "@/constants/theme"; -import i18n from "@/core/i18n"; -import useAppStore from "@/core/store/appStore"; -import { useTheme } from "@/core/theme/ThemeContext"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import { useNavigation } from "@react-navigation/native"; -import React, { useState } from "react"; -import { StyleSheet, TouchableOpacity, View } from "react-native"; +import { Screen } from '@/components/Screen'; +import { ThemedText } from '@/components/base/ThemedText'; +import { ThemedTextInput } from '@/components/base/ThemedTextInput'; +import { AppTheme } from '@/constants/theme'; +import i18n from '@/core/i18n'; +import useAppStore from '@/core/store/appStore'; +import { useTheme } from '@/core/theme/ThemeContext'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { useNavigation } from '@react-navigation/native'; +import React, { useState } from 'react'; +import { StyleSheet, TouchableOpacity, View } from 'react-native'; const ConfigureGoalScreen = () => { const navigation = useNavigation(); @@ -17,8 +17,8 @@ const ConfigureGoalScreen = () => { const styles = getStyles(theme); const { draftDeed, updateDraftDeed } = useAppStore(); - const [value, setValue] = useState(draftDeed?.goal?.value?.toString() || ""); - const [unit, setUnit] = useState(draftDeed?.goal?.unit || ""); + const [value, setValue] = useState(draftDeed?.goal?.value?.toString() || ''); + const [unit, setUnit] = useState(draftDeed?.goal?.unit || ''); const handleValueChange = (text: string) => { const num = parseInt(text, 10); @@ -42,28 +42,23 @@ const ConfigureGoalScreen = () => { return ( ( navigation.goBack()}> - + - )} - > + )}> - {i18n.t("goal.target")} + {i18n.t('goal.target')} - {i18n.t("goal.unit")} + {i18n.t('goal.unit')} @@ -78,7 +73,7 @@ const getStyles = (theme: AppTheme) => container: { paddingTop: theme.spacing.m }, label: { color: theme.colors.textSecondary, - textTransform: "uppercase", + textTransform: 'uppercase', fontWeight: theme.typography.fontWeight.bold, marginBottom: theme.spacing.s, marginTop: theme.spacing.m, diff --git a/src/screens/ConfigureParentScreen.tsx b/src/screens/ConfigureParentScreen.tsx index 4d8f5fc..a9b0dc2 100644 --- a/src/screens/ConfigureParentScreen.tsx +++ b/src/screens/ConfigureParentScreen.tsx @@ -1,14 +1,14 @@ // FILE: src/screens/ConfigureParentScreen.tsx -import { Screen } from "@/components/Screen"; -import { ThemedText } from "@/components/base/ThemedText"; -import { AppTheme } from "@/constants/theme"; -import i18n from "@/core/i18n"; -import useAppStore from "@/core/store/appStore"; -import { useTheme } from "@/core/theme/ThemeContext"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import { useNavigation } from "@react-navigation/native"; -import React from "react"; -import { FlatList, StyleSheet, TouchableOpacity } from "react-native"; +import { Screen } from '@/components/Screen'; +import { ThemedText } from '@/components/base/ThemedText'; +import { AppTheme } from '@/constants/theme'; +import i18n from '@/core/i18n'; +import useAppStore from '@/core/store/appStore'; +import { useTheme } from '@/core/theme/ThemeContext'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { useNavigation } from '@react-navigation/native'; +import React from 'react'; +import { FlatList, StyleSheet, TouchableOpacity } from 'react-native'; const ConfigureParentScreen = () => { const navigation = useNavigation(); @@ -26,64 +26,36 @@ const ConfigureParentScreen = () => { return ( ( navigation.goBack()}> - + - )} - > + )}> item.id} renderItem={({ item }) => ( handleSelect(item.id)} - > - + style={[styles.row, draftDeed?.parentId === item.id && styles.selectedRow]} + onPress={() => handleSelect(item.id)}> + {i18n.t(`deeds_names.${item.id}`, { defaultValue: item.name })} {draftDeed?.parentId === item.id && ( - + )} )} ListHeaderComponent={ handleSelect(null)} - > - - - {i18n.t("deeds.none")} - + onPress={() => handleSelect(null)}> + + {i18n.t('deeds.none')} {!draftDeed?.parentId && ( - + )} } @@ -97,8 +69,8 @@ export default ConfigureParentScreen; const getStyles = (theme: AppTheme) => StyleSheet.create({ row: { - flexDirection: "row", - alignItems: "center", + flexDirection: 'row', + alignItems: 'center', padding: theme.spacing.m, backgroundColor: theme.colors.foreground, borderRadius: 8, diff --git a/src/screens/CreateDeedScreen.tsx b/src/screens/CreateDeedScreen.tsx index 71f8578..6b22940 100644 --- a/src/screens/CreateDeedScreen.tsx +++ b/src/screens/CreateDeedScreen.tsx @@ -1,31 +1,22 @@ // FILE: src/screens/CreateDeedScreen.tsx -import { Box } from "@/components/base/Box"; -import { ThemedText } from "@/components/base/ThemedText"; -import { ThemedTextInput } from "@/components/base/ThemedTextInput"; -import { Screen } from "@/components/Screen"; -import { AppTheme } from "@/constants/theme"; -import i18n from "@/core/i18n"; -import useAppStore from "@/core/store/appStore"; -import { useTheme } from "@/core/theme/ThemeContext"; -import { IconSelectorModal } from "@/features/deeds/IconSelectorModal"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import { useNavigation, useRoute, RouteProp } from "@react-navigation/native"; -import React, { useEffect, useState } from "react"; -import { - Alert, - StyleSheet, - TouchableOpacity, - View, - ActivityIndicator, -} from "react-native"; -import { HomeStackParamList } from "@/navigation/AppNavigator"; -import { NativeStackNavigationProp } from "@react-navigation/native-stack"; - -type CreateDeedScreenNavigationProp = NativeStackNavigationProp< - HomeStackParamList, - "CreateDeed" ->; -type CreateDeedScreenRouteProp = RouteProp; +import { Box } from '@/components/base/Box'; +import { ThemedText } from '@/components/base/ThemedText'; +import { ThemedTextInput } from '@/components/base/ThemedTextInput'; +import { Screen } from '@/components/Screen'; +import { AppTheme } from '@/constants/theme'; +import i18n from '@/core/i18n'; +import useAppStore from '@/core/store/appStore'; +import { useTheme } from '@/core/theme/ThemeContext'; +import { IconSelectorModal } from '@/features/deeds/IconSelectorModal'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { useNavigation, useRoute, RouteProp } from '@react-navigation/native'; +import React, { useEffect, useState } from 'react'; +import { Alert, StyleSheet, TouchableOpacity, View, ActivityIndicator } from 'react-native'; +import { HomeStackParamList } from '@/navigation/AppNavigator'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; + +type CreateDeedScreenNavigationProp = NativeStackNavigationProp; +type CreateDeedScreenRouteProp = RouteProp; const CreateDeedScreen = () => { const navigation = useNavigation(); @@ -36,13 +27,8 @@ const CreateDeedScreen = () => { const deedId = route.params?.deedId; const isEditMode = !!deedId; - const { - draftDeed, - initializeDraftDeed, - updateDraftDeed, - clearDraftDeed, - saveDraftDeed, - } = useAppStore(); + const { draftDeed, initializeDraftDeed, updateDraftDeed, clearDraftDeed, saveDraftDeed } = + useAppStore(); const allDeeds = useAppStore((state) => state.deeds); const [isIconModalVisible, setIconModalVisible] = useState(false); @@ -56,51 +42,44 @@ const CreateDeedScreen = () => { const handleSave = () => { if (!draftDeed?.name?.trim()) { - Alert.alert( - i18n.t("alerts.missingNameTitle"), - i18n.t("alerts.missingNameMessage"), - ); + Alert.alert(i18n.t('alerts.missingNameTitle'), i18n.t('alerts.missingNameMessage')); return; } saveDraftDeed(); navigation.goBack(); }; - const handleSelectIcon = ( - icon: React.ComponentProps["name"], - ) => { + const handleSelectIcon = (icon: React.ComponentProps['name']) => { updateDraftDeed({ icon }); setIconModalVisible(false); }; const getFrequencyLabel = () => { - if (!draftDeed?.frequency) return i18n.t("deeds.notSet"); - if (draftDeed.frequency.type === "daily") return i18n.t("frequency.daily"); - if (draftDeed.frequency.type === "weekly") { + if (!draftDeed?.frequency) return i18n.t('deeds.notSet'); + if (draftDeed.frequency.type === 'daily') return i18n.t('frequency.daily'); + if (draftDeed.frequency.type === 'weekly') { const dayCount = draftDeed.frequency.days?.length || 0; return dayCount === 1 - ? i18n.t("deeds.weeklyLabel_singular", { dayCount }) - : i18n.t("deeds.weeklyLabel", { dayCount }); + ? i18n.t('deeds.weeklyLabel_singular', { dayCount }) + : i18n.t('deeds.weeklyLabel', { dayCount }); } - return i18n.t("deeds.notSet"); + return i18n.t('deeds.notSet'); }; const getGoalLabel = () => { - if (!draftDeed?.goal) return i18n.t("deeds.notSet"); + if (!draftDeed?.goal) return i18n.t('deeds.notSet'); return `${draftDeed.goal.value} ${draftDeed.goal.unit}`; }; const getParentLabel = () => { - if (!draftDeed?.parentId) return i18n.t("deeds.none"); + if (!draftDeed?.parentId) return i18n.t('deeds.none'); const parent = allDeeds.find((d) => d.id === draftDeed.parentId); return parent ? i18n.t(`deeds_names.${parent.id}`, { defaultValue: parent.name }) - : i18n.t("deeds.none"); + : i18n.t('deeds.none'); }; - const screenTitle = isEditMode - ? i18n.t("screens.editDeed") - : i18n.t("screens.createDeed"); + const screenTitle = isEditMode ? i18n.t('screens.editDeed') : i18n.t('screens.createDeed'); if (!draftDeed) { return ( @@ -116,60 +95,44 @@ const CreateDeedScreen = () => { title={screenTitle} renderLeftAction={() => ( navigation.goBack()}> - + )} renderRightAction={() => ( - - {i18n.t("deeds.save")} - + {i18n.t('deeds.save')} - )} - > + )}> - setIconModalVisible(true)} - > - + setIconModalVisible(true)}> + updateDraftDeed({ name })} style={styles.textInput} /> - - {i18n.t("deeds.configuration")} - + {i18n.t('deeds.configuration')} navigation.navigate("ConfigureFrequency")} + onPress={() => navigation.navigate('ConfigureFrequency')} /> navigation.navigate("ConfigureGoal")} + onPress={() => navigation.navigate('ConfigureGoal')} /> navigation.navigate("ConfigureParent")} + onPress={() => navigation.navigate('ConfigureParent')} /> @@ -189,7 +152,7 @@ const ConfigRow = ({ value, onPress, }: { - icon: React.ComponentProps["name"]; + icon: React.ComponentProps['name']; label: string; value: string; onPress?: () => void; @@ -198,18 +161,10 @@ const ConfigRow = ({ const styles = getStyles(theme); return ( - + {label} {value} - + ); }; @@ -225,8 +180,8 @@ const getStyles = (theme: AppTheme) => paddingTop: theme.spacing.m, }, inputRow: { - flexDirection: "row", - alignItems: "center", + flexDirection: 'row', + alignItems: 'center', backgroundColor: theme.colors.foreground, borderRadius: 8, padding: theme.spacing.s, @@ -239,7 +194,7 @@ const getStyles = (theme: AppTheme) => }, textInput: { flex: 1, - backgroundColor: "transparent", + backgroundColor: 'transparent', borderWidth: 0, padding: 0, }, @@ -247,13 +202,13 @@ const getStyles = (theme: AppTheme) => marginTop: theme.spacing.xl, marginBottom: theme.spacing.s, color: theme.colors.textSecondary, - textTransform: "uppercase", + textTransform: 'uppercase', fontWeight: theme.typography.fontWeight.bold, fontSize: theme.typography.fontSize.s, }, configRow: { - flexDirection: "row", - alignItems: "center", + flexDirection: 'row', + alignItems: 'center', padding: theme.spacing.m, backgroundColor: theme.colors.foreground, borderRadius: 8, diff --git a/src/screens/DeedManagerScreen.tsx b/src/screens/DeedManagerScreen.tsx index e004898..6bb4fc4 100644 --- a/src/screens/DeedManagerScreen.tsx +++ b/src/screens/DeedManagerScreen.tsx @@ -1,18 +1,16 @@ // FILE: src/screens/DeedManagerScreen.tsx -import { Screen } from "@/components/Screen"; -import { AppTheme } from "@/constants/theme"; -import { Deed } from "@/core/data/models"; -import i18n from "@/core/i18n"; -import useAppStore from "@/core/store/appStore"; -import { useTheme } from "@/core/theme/ThemeContext"; -import { DeedManagerListItem } from "@/features/deeds/DeedManagerListItem"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import { useNavigation } from "@react-navigation/native"; -import React from "react"; -import { StyleSheet, TouchableOpacity } from "react-native"; -import DraggableFlatList, { - RenderItemParams, -} from "react-native-draggable-flatlist"; +import { Screen } from '@/components/Screen'; +import { AppTheme } from '@/constants/theme'; +import { Deed } from '@/core/data/models'; +import i18n from '@/core/i18n'; +import useAppStore from '@/core/store/appStore'; +import { useTheme } from '@/core/theme/ThemeContext'; +import { DeedManagerListItem } from '@/features/deeds/DeedManagerListItem'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { useNavigation } from '@react-navigation/native'; +import React from 'react'; +import { StyleSheet, TouchableOpacity } from 'react-native'; +import DraggableFlatList, { RenderItemParams } from 'react-native-draggable-flatlist'; const DeedManagerScreen = () => { const navigation = useNavigation(); @@ -22,24 +20,17 @@ const DeedManagerScreen = () => { const setDeeds = useAppStore((state) => state.setDeeds); const renderItem = ({ item, drag, isActive }: RenderItemParams) => { - return ( - - ); + return ; }; return ( ( navigation.goBack()}> - + - )} - > + )}> setDeeds(data)} diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index 12504d4..4fb361e 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -1,34 +1,26 @@ // src/screens/HomeScreen.tsx -import { Box } from "@/components/base/Box"; -import { Screen } from "@/components/Screen"; -import { ThemedText } from "@/components/base/ThemedText"; -import { FAB } from "@/components/FAB"; -import { AppTheme } from "@/constants/theme"; -import { Deed, DeedLog, DeedStatus } from "@/core/data/models"; -import i18n, { dateLocales } from "@/core/i18n"; -import useAppStore from "@/core/store/appStore"; -import { useTheme } from "@/core/theme/ThemeContext"; -import { formatHijriDate } from "@/core/utils/dateFormatter"; -import { DeedListItem } from "@/features/deeds/DeedListItem"; -import { LogDeedModal } from "@/features/deeds/LogDeedModal"; -import { LogGoalModal } from "@/features/deeds/LogGoalModal"; -import { DateScroller } from "@/features/home/DateScroller"; -import { HomeStackParamList } from "@/navigation/AppNavigator"; -import { useNavigation } from "@react-navigation/native"; -import { NativeStackNavigationProp } from "@react-navigation/native-stack"; -import { format, formatISO, getDay } from "date-fns"; -import React, { useMemo, useState } from "react"; -import { - ActivityIndicator, - I18nManager, - SectionList, - StyleSheet, -} from "react-native"; - -type HomeScreenNavigationProp = NativeStackNavigationProp< - HomeStackParamList, - "HomeMain" ->; +import { Box } from '@/components/base/Box'; +import { Screen } from '@/components/Screen'; +import { ThemedText } from '@/components/base/ThemedText'; +import { FAB } from '@/components/FAB'; +import { AppTheme } from '@/constants/theme'; +import { Deed, DeedLog, DeedStatus } from '@/core/data/models'; +import i18n, { dateLocales } from '@/core/i18n'; +import useAppStore from '@/core/store/appStore'; +import { useTheme } from '@/core/theme/ThemeContext'; +import { formatHijriDate } from '@/core/utils/dateFormatter'; +import { DeedListItem } from '@/features/deeds/DeedListItem'; +import { LogDeedModal } from '@/features/deeds/LogDeedModal'; +import { LogGoalModal } from '@/features/deeds/LogGoalModal'; +import { DateScroller } from '@/features/home/DateScroller'; +import { HomeStackParamList } from '@/navigation/AppNavigator'; +import { useNavigation } from '@react-navigation/native'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { format, formatISO, getDay } from 'date-fns'; +import React, { useMemo, useState } from 'react'; +import { ActivityIndicator, I18nManager, SectionList, StyleSheet } from 'react-native'; + +type HomeScreenNavigationProp = NativeStackNavigationProp; const HomeScreen = () => { const { theme } = useTheme(); @@ -36,8 +28,7 @@ const HomeScreen = () => { const navigation = useNavigation(); // --- Global State --- - const { isInitialized, deeds, logs, addOrUpdateLog, settings } = - useAppStore(); + const { isInitialized, deeds, logs, addOrUpdateLog, settings } = useAppStore(); const activeLocale = dateLocales[settings.language]; // --- Local State --- @@ -45,14 +36,12 @@ const HomeScreen = () => { const [isLogDeedModalVisible, setLogDeedModalVisible] = useState(false); const [isLogGoalModalVisible, setLogGoalModalVisible] = useState(false); const [selectedDeed, setSelectedDeed] = useState(null); - const [selectedLog, setSelectedLog] = useState( - undefined, - ); + const [selectedLog, setSelectedLog] = useState(undefined); // --- Memos --- const { gregorianDate, hijriDate } = useMemo(() => { return { - gregorianDate: format(selectedDate, "MMMM d", { + gregorianDate: format(selectedDate, 'MMMM d', { locale: activeLocale, }), hijriDate: formatHijriDate(selectedDate), @@ -60,7 +49,7 @@ const HomeScreen = () => { }, [selectedDate, activeLocale]); const logsForSelectedDate = useMemo(() => { - const dateString = formatISO(selectedDate, { representation: "date" }); + const dateString = formatISO(selectedDate, { representation: 'date' }); return logs.filter((log) => log.date === dateString); }, [selectedDate, logs]); @@ -75,13 +64,13 @@ const HomeScreen = () => { return true; } if ( - deed.frequency.type === "daily" || - deed.frequency.type === "monthly" || - deed.frequency.type === "yearly" + deed.frequency.type === 'daily' || + deed.frequency.type === 'monthly' || + deed.frequency.type === 'yearly' ) { return true; } - if (deed.frequency.type === "weekly") { + if (deed.frequency.type === 'weekly') { return deed.frequency.days?.includes(dayOfWeek) ?? false; } return false; @@ -128,8 +117,8 @@ const HomeScreen = () => { // For goals, we determine status based on value. const status = value > 0 - ? selectedDeed.statuses.find((s) => s.id === "completed")! - : selectedDeed.statuses.find((s) => s.id === "missed")!; + ? selectedDeed.statuses.find((s) => s.id === 'completed')! + : selectedDeed.statuses.find((s) => s.id === 'missed')!; addOrUpdateLog(selectedDeed, selectedDate, status, value); setLogGoalModalVisible(false); setSelectedDeed(null); @@ -137,7 +126,7 @@ const HomeScreen = () => { if (!isInitialized) { return ( - + @@ -161,17 +150,11 @@ const HomeScreen = () => { )} renderItem={({ item: deed }) => { const log = logsForSelectedDate.find((l) => l.deedId === deed.id); - return ( - handleOpenModal(deed)} - /> - ); + return handleOpenModal(deed)} />; }} contentContainerStyle={styles.listContent} /> - navigation.navigate("AddDeed")} /> + navigation.navigate('AddDeed')} /> fontWeight: theme.typography.fontWeight.bold, fontSize: theme.typography.fontSize.s, color: theme.colors.textSecondary, - textTransform: "uppercase", + textTransform: 'uppercase', marginBottom: theme.spacing.s, marginTop: theme.spacing.m, - textAlign: I18nManager.isRTL ? "right" : "left", + textAlign: I18nManager.isRTL ? 'right' : 'left', }, listContent: { paddingTop: theme.spacing.s, }, centered: { flex: 1, - justifyContent: "center", - alignItems: "center", + justifyContent: 'center', + alignItems: 'center', }, }); diff --git a/src/screens/NotificationsScreen.tsx b/src/screens/NotificationsScreen.tsx index f70c880..55bcd10 100644 --- a/src/screens/NotificationsScreen.tsx +++ b/src/screens/NotificationsScreen.tsx @@ -1,28 +1,20 @@ // src/screens/NotificationsScreen.tsx -import { Screen } from "@/components/Screen"; -import { ThemedText } from "@/components/base/ThemedText"; -import { AppTheme } from "@/constants/theme"; -import i18n from "@/core/i18n"; +import { Screen } from '@/components/Screen'; +import { ThemedText } from '@/components/base/ThemedText'; +import { AppTheme } from '@/constants/theme'; +import i18n from '@/core/i18n'; import { scheduleTestNotificationIn5s, sendTestNotification, -} from "@/core/services/notificationService"; -import useAppStore from "@/core/store/appStore"; -import { useTheme } from "@/core/theme/ThemeContext"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import DateTimePicker, { - DateTimePickerEvent, -} from "@react-native-community/datetimepicker"; -import { format } from "date-fns"; -import { useNavigation } from "@react-navigation/native"; -import React, { useState } from "react"; -import { - Platform, - StyleSheet, - Switch, - TouchableOpacity, - View, -} from "react-native"; +} from '@/core/services/notificationService'; +import useAppStore from '@/core/store/appStore'; +import { useTheme } from '@/core/theme/ThemeContext'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import DateTimePicker, { DateTimePickerEvent } from '@react-native-community/datetimepicker'; +import { format } from 'date-fns'; +import { useNavigation } from '@react-navigation/native'; +import React, { useState } from 'react'; +import { Platform, StyleSheet, Switch, TouchableOpacity, View } from 'react-native'; const NotificationsScreen = () => { const navigation = useNavigation(); @@ -35,13 +27,13 @@ const NotificationsScreen = () => { const [isPickerVisible, setPickerVisible] = useState(false); const reminderDate = new Date(); - const [hours, minutes] = (reminderTime || "20:00").split(":").map(Number); + const [hours, minutes] = (reminderTime || '20:00').split(':').map(Number); reminderDate.setHours(hours); reminderDate.setMinutes(minutes); const onChange = (event: DateTimePickerEvent, selectedDate?: Date) => { setPickerVisible(false); - if (event.type === "set" && selectedDate) { - setReminderTime(format(selectedDate, "HH:mm")); + if (event.type === 'set' && selectedDate) { + setReminderTime(format(selectedDate, 'HH:mm')); } }; const showPicker = () => { @@ -51,22 +43,15 @@ const NotificationsScreen = () => { return ( <> ( navigation.goBack()}> - + - )} - > + )}> - - {i18n.t("settings.dailyReminder")} - + {i18n.t('settings.dailyReminder')} { {isReminderEnabled && ( - - {i18n.t("settings.time")} - + {i18n.t('settings.time')} {reminderTime} { {isDevMode && ( - - {i18n.t("devMenu.title")} - - - - {i18n.t("devMenu.sendTest")} - + {i18n.t('devMenu.title')} + + {i18n.t('devMenu.sendTest')} - - - {i18n.t("devMenu.scheduleTest")} - + + {i18n.t('devMenu.scheduleTest')} )} @@ -124,7 +95,7 @@ const NotificationsScreen = () => { value={reminderDate} mode="time" is24Hour={true} - display={Platform.OS === "ios" ? "spinner" : "default"} + display={Platform.OS === 'ios' ? 'spinner' : 'default'} onChange={onChange} /> )} @@ -140,12 +111,12 @@ const getStyles = (theme: AppTheme) => marginTop: theme.spacing.m, backgroundColor: theme.colors.foreground, borderRadius: 16, - overflow: "hidden", + overflow: 'hidden', }, row: { - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', padding: theme.spacing.m, borderBottomWidth: 1, borderBottomColor: theme.colors.background, @@ -154,8 +125,8 @@ const getStyles = (theme: AppTheme) => fontSize: theme.typography.fontSize.m, }, valueContainer: { - flexDirection: "row", - alignItems: "center", + flexDirection: 'row', + alignItems: 'center', }, valueText: { color: theme.colors.textSecondary, @@ -171,15 +142,15 @@ const getStyles = (theme: AppTheme) => fontSize: theme.typography.fontSize.s, fontWeight: theme.typography.fontWeight.bold, color: theme.colors.textSecondary, - textTransform: "uppercase", - textAlign: "center", + textTransform: 'uppercase', + textAlign: 'center', marginBottom: theme.spacing.m, }, devButton: { backgroundColor: theme.colors.primary, padding: theme.spacing.m, borderRadius: 8, - alignItems: "center", + alignItems: 'center', marginBottom: theme.spacing.s, }, devButtonText: { diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index d659674..3f6cfa3 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -1,46 +1,32 @@ // FILE: src/screens/SettingsScreen.tsx -import { Screen } from "@/components/Screen"; -import { ThemedText } from "@/components/base/ThemedText"; -import { AppTheme } from "@/constants/theme"; -import i18n from "@/core/i18n"; -import useAppStore from "@/core/store/appStore"; -import { useTheme } from "@/core/theme/ThemeContext"; -import { DevMenuModal } from "@/features/settings/DevMenuModal"; -import { - getSettingsData, - ModalSettingsItem, - SettingsItem, -} from "@/features/settings/settingsData"; -import { SettingsListItem } from "@/features/settings/SettingsListItem"; -import { SettingsOptionModal } from "@/features/settings/SettingsOptionModal"; -import Constants from "expo-constants"; -import * as Linking from "expo-linking"; -import { useNavigation } from "@react-navigation/native"; -import React, { useMemo, useState } from "react"; -import { - Alert, - I18nManager, - Pressable, - SectionList, - Share, - StyleSheet, - View, -} from "react-native"; -import { SettingsStackParamList } from "@/navigation/AppNavigator"; -import { NativeStackNavigationProp } from "@react-navigation/native-stack"; +import { Screen } from '@/components/Screen'; +import { ThemedText } from '@/components/base/ThemedText'; +import { AppTheme } from '@/constants/theme'; +import i18n from '@/core/i18n'; +import useAppStore from '@/core/store/appStore'; +import { useTheme } from '@/core/theme/ThemeContext'; +import { DevMenuModal } from '@/features/settings/DevMenuModal'; +import { getSettingsData, ModalSettingsItem, SettingsItem } from '@/features/settings/settingsData'; +import { SettingsListItem } from '@/features/settings/SettingsListItem'; +import { SettingsOptionModal } from '@/features/settings/SettingsOptionModal'; +import Constants from 'expo-constants'; +import * as Linking from 'expo-linking'; +import { useNavigation } from '@react-navigation/native'; +import React, { useMemo, useState } from 'react'; +import { Alert, I18nManager, Pressable, SectionList, Share, StyleSheet, View } from 'react-native'; +import { SettingsStackParamList } from '@/navigation/AppNavigator'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; type SettingsScreenNavigationProp = NativeStackNavigationProp< SettingsStackParamList, - "SettingsMain" + 'SettingsMain' >; const SettingsScreen = () => { const { theme } = useTheme(); const styles = getStyles(theme); const navigation = useNavigation(); - const [modalData, setModalData] = useState | null>( - null, - ); + const [modalData, setModalData] = useState | null>(null); const { resetData, setDevMode } = useAppStore(); const isDevMode = useAppStore((state) => state.settings.isDevMode); const language = useAppStore((state) => state.settings.language); @@ -55,10 +41,7 @@ const SettingsScreen = () => { setVersionTapCount(newCount); if (newCount >= 7) { setDevMode(true); - Alert.alert( - i18n.t("alerts.devModeTitle"), - i18n.t("alerts.devModeMessage"), - ); + Alert.alert(i18n.t('alerts.devModeTitle'), i18n.t('alerts.devModeMessage')); } }; @@ -73,48 +56,35 @@ const SettingsScreen = () => { invite: async () => { try { await Share.share({ - message: i18n.t("settings.shareMessage"), + message: i18n.t('settings.shareMessage'), }); } catch (error) { - console.error("Failed to share:", error); + console.error('Failed to share:', error); } }, rate: () => { - const storeUrl = "market://details?id=com.uwayss.muhsin"; - Linking.openURL(storeUrl).catch((err) => - console.error("Couldn't load page", err), - ); + const storeUrl = 'market://details?id=com.uwayss.muhsin'; + Linking.openURL(storeUrl).catch((err) => console.error("Couldn't load page", err)); }, feedback: () => { - const subject = i18n.t("settings.feedbackSubject"); - const mailtoUrl = `mailto:support@uwayss.com?subject=${encodeURIComponent( - subject, - )}`; - Linking.openURL(mailtoUrl).catch((err) => - console.error("Couldn't open mail client", err), - ); + const subject = i18n.t('settings.feedbackSubject'); + const mailtoUrl = `mailto:support@uwayss.com?subject=${encodeURIComponent(subject)}`; + Linking.openURL(mailtoUrl).catch((err) => console.error("Couldn't open mail client", err)); }, reset: () => { - Alert.alert( - i18n.t("alerts.resetTitle"), - i18n.t("alerts.resetMessage"), - [ - { - text: i18n.t("alerts.cancel"), - style: "cancel", - }, - { - text: i18n.t("alerts.reset"), - style: "destructive", - onPress: () => resetData(), - }, - ], - ); + Alert.alert(i18n.t('alerts.resetTitle'), i18n.t('alerts.resetMessage'), [ + { + text: i18n.t('alerts.cancel'), + style: 'cancel', + }, + { + text: i18n.t('alerts.reset'), + style: 'destructive', + onPress: () => resetData(), + }, + ]); }, - privacy: () => - Linking.openURL( - "https://github.com/uwayss/muhsin/blob/main/PRIVACY.md", - ), + privacy: () => Linking.openURL('https://github.com/uwayss/muhsin/blob/main/PRIVACY.md'), }), [resetData], ); @@ -124,21 +94,21 @@ const SettingsScreen = () => { // eslint-disable-next-line react-hooks/exhaustive-deps [actions, language], ); - const appVersion = Constants.expoConfig?.version ?? "N/A"; + const appVersion = Constants.expoConfig?.version ?? 'N/A'; const handleItemPress = (item: SettingsItem) => { - if (item.type === "navigation") { + if (item.type === 'navigation') { navigation.navigate(item.path as any); - } else if (item.type === "modal") { + } else if (item.type === 'modal') { setModalData(item as ModalSettingsItem); - } else if (item.type === "action") { + } else if (item.type === 'action') { item.action(); } }; return ( <> - + item.label} @@ -151,24 +121,17 @@ const SettingsScreen = () => { /> )} renderSectionHeader={({ section: { title } }) => - title ? ( - {title} - ) : null + title ? {title} : null } ItemSeparatorComponent={() => } ListFooterComponent={ - + - {i18n.t("footer.appVersion", { version: appVersion })} - {isDevMode && i18n.t("footer.devMode")} - - - {i18n.t("footer.madeWith")} + {i18n.t('footer.appVersion', { version: appVersion })} + {isDevMode && i18n.t('footer.devMode')} + {i18n.t('footer.madeWith')} } @@ -177,14 +140,8 @@ const SettingsScreen = () => { /> - setModalData(null)} - /> - setDevMenuVisible(false)} - /> + setModalData(null)} /> + setDevMenuVisible(false)} /> ); }; @@ -197,11 +154,11 @@ const getStyles = (theme: AppTheme) => fontWeight: theme.typography.fontWeight.bold, fontSize: theme.typography.fontSize.s, color: theme.colors.textSecondary, - textTransform: "uppercase", + textTransform: 'uppercase', marginTop: theme.spacing.m + 4, marginBottom: theme.spacing.s, paddingHorizontal: theme.spacing.m, - textAlign: I18nManager.isRTL ? "right" : "left", + textAlign: I18nManager.isRTL ? 'right' : 'left', }, listContent: { paddingBottom: theme.spacing.l, @@ -212,7 +169,7 @@ const getStyles = (theme: AppTheme) => marginStart: theme.spacing.m, }, footer: { - alignItems: "center", + alignItems: 'center', marginTop: theme.spacing.l, }, footerText: { diff --git a/src/screens/StatsScreen.tsx b/src/screens/StatsScreen.tsx index 1b05ed4..83e5612 100644 --- a/src/screens/StatsScreen.tsx +++ b/src/screens/StatsScreen.tsx @@ -1,17 +1,17 @@ // FILE: src/screens/StatsScreen.tsx -import { Screen } from "@/components/Screen"; -import { ThemedText } from "@/components/base/ThemedText"; -import { AppTheme } from "@/constants/theme"; -import i18n from "@/core/i18n"; -import useAppStore from "@/core/store/appStore"; -import { useTheme } from "@/core/theme/ThemeContext"; -import { ActivityGraph } from "@/features/stats/ActivityGraph"; -import { DeedStatsRow } from "@/features/stats/DeedStatsRow"; -import { ExpandableSection } from "@/features/stats/ExpandableSection"; -import { GoalStatsRow } from "@/features/stats/GoalStatsRow"; -import { SegmentedControl } from "@/features/stats/SegmentedControl"; -import { TimeInterval } from "@/features/stats/types"; -import { SummaryBox } from "@/features/stats/SummaryBox"; +import { Screen } from '@/components/Screen'; +import { ThemedText } from '@/components/base/ThemedText'; +import { AppTheme } from '@/constants/theme'; +import i18n from '@/core/i18n'; +import useAppStore from '@/core/store/appStore'; +import { useTheme } from '@/core/theme/ThemeContext'; +import { ActivityGraph } from '@/features/stats/ActivityGraph'; +import { DeedStatsRow } from '@/features/stats/DeedStatsRow'; +import { ExpandableSection } from '@/features/stats/ExpandableSection'; +import { GoalStatsRow } from '@/features/stats/GoalStatsRow'; +import { SegmentedControl } from '@/features/stats/SegmentedControl'; +import { TimeInterval } from '@/features/stats/types'; +import { SummaryBox } from '@/features/stats/SummaryBox'; import { eachDayOfInterval, endOfDay, @@ -20,22 +20,16 @@ import { startOfWeek, startOfYear, subDays, -} from "date-fns"; -import React, { useEffect, useMemo, useState } from "react"; -import { - ActivityIndicator, - InteractionManager, - ScrollView, - StyleSheet, - View, -} from "react-native"; +} from 'date-fns'; +import React, { useEffect, useMemo, useState } from 'react'; +import { ActivityIndicator, InteractionManager, ScrollView, StyleSheet, View } from 'react-native'; const PRAYER_DEED_IDS = [ - "prayer-fajr", - "prayer-dhuhr", - "prayer-asr", - "prayer-maghrib", - "prayer-isha", + 'prayer-fajr', + 'prayer-dhuhr', + 'prayer-asr', + 'prayer-maghrib', + 'prayer-isha', ]; const StatsScreen = () => { @@ -43,7 +37,7 @@ const StatsScreen = () => { const styles = getStyles(theme); const { isInitialized, deeds, logs } = useAppStore(); - const [interval, setInterval] = useState("week"); + const [interval, setInterval] = useState('week'); const [isLoading, setIsLoading] = useState(true); const [displayData, setDisplayData] = useState<{ @@ -52,26 +46,18 @@ const StatsScreen = () => { } | null>(null); const prayerDeeds = useMemo( - () => - PRAYER_DEED_IDS.map((id) => deeds.find((d) => d.id === id)).filter( - Boolean, - ), + () => PRAYER_DEED_IDS.map((id) => deeds.find((d) => d.id === id)).filter(Boolean), [deeds], ); - const goalDeeds = useMemo( - () => deeds.filter((d) => d.goal && !d.isCore), - [deeds], - ); + const goalDeeds = useMemo(() => deeds.filter((d) => d.goal && !d.isCore), [deeds]); // --- Data specifically for the graph (last 60 days) --- const graphData = useMemo(() => { const today = new Date(); const sixtyDaysAgo = startOfDay(subDays(today, 59)); // Changed from 89 to 59 for ~2 months const dateRange = eachDayOfInterval({ start: sixtyDaysAgo, end: today }); - const relevantLogs = logs.filter( - (log) => new Date(log.date) >= sixtyDaysAgo, - ); + const relevantLogs = logs.filter((log) => new Date(log.date) >= sixtyDaysAgo); return { dateRange, logs: relevantLogs }; }, [logs]); @@ -83,33 +69,26 @@ const StatsScreen = () => { const today = new Date(); let dateRangeForFilter; - if (interval === "all") { + if (interval === 'all') { const firstLogDate = - logs.length > 0 - ? new Date(logs.reduce((a, b) => (a.date < b.date ? a : b)).date) - : today; + logs.length > 0 ? new Date(logs.reduce((a, b) => (a.date < b.date ? a : b)).date) : today; dateRangeForFilter = { start: startOfDay(firstLogDate), end: today }; } else { const range = { start: startOfWeek(today), end: endOfDay(today) }; - if (interval === "month") range.start = startOfMonth(today); - if (interval === "year") range.start = startOfYear(today); + if (interval === 'month') range.start = startOfMonth(today); + if (interval === 'year') range.start = startOfYear(today); dateRangeForFilter = range; } const filtered = - interval === "all" + interval === 'all' ? logs : logs.filter((log) => { const logDate = startOfDay(new Date(log.date)); - return ( - logDate >= dateRangeForFilter.start && - logDate <= dateRangeForFilter.end - ); + return logDate >= dateRangeForFilter.start && logDate <= dateRangeForFilter.end; }); - const prayerLogs = filtered.filter((log) => - PRAYER_DEED_IDS.includes(log.deedId), - ); + const prayerLogs = filtered.filter((log) => PRAYER_DEED_IDS.includes(log.deedId)); const total = prayerLogs.length; const counts = prayerLogs.reduce( (acc, log) => { @@ -123,9 +102,9 @@ const StatsScreen = () => { count: counts.jamaah || 0, percentage: total > 0 ? ((counts.jamaah || 0) / total) * 100 : 0, }, - "on-time": { - count: counts["on-time"] || 0, - percentage: total > 0 ? ((counts["on-time"] || 0) / total) * 100 : 0, + 'on-time': { + count: counts['on-time'] || 0, + percentage: total > 0 ? ((counts['on-time'] || 0) / total) * 100 : 0, }, late: { count: counts.late || 0, @@ -146,10 +125,10 @@ const StatsScreen = () => { }, [isInitialized, interval, logs]); const segmentedOptions = [ - { label: i18n.t("intervals.week"), value: "week" as TimeInterval }, - { label: i18n.t("intervals.month"), value: "month" as TimeInterval }, - { label: i18n.t("intervals.year"), value: "year" as TimeInterval }, - { label: i18n.t("intervals.allTime"), value: "all" as TimeInterval }, + { label: i18n.t('intervals.week'), value: 'week' as TimeInterval }, + { label: i18n.t('intervals.month'), value: 'month' as TimeInterval }, + { label: i18n.t('intervals.year'), value: 'year' as TimeInterval }, + { label: i18n.t('intervals.allTime'), value: 'all' as TimeInterval }, ]; const renderContent = () => { @@ -162,39 +141,32 @@ const StatsScreen = () => { } return ( <> - + - - {i18n.t("stats.prayerPerformance")} - + showsVerticalScrollIndicator={false}> + {i18n.t('stats.prayerPerformance')} { {goalDeeds.length > 0 && ( - - {i18n.t("stats.goalProgress")} - + {i18n.t('stats.goalProgress')} {goalDeeds.map((deed) => ( l.deedId === deed.id, - )} + logs={displayData.filteredLogs.filter((l) => l.deedId === deed.id)} /> ))} )} - - {i18n.t("stats.statsByDeed")} - - + {i18n.t('stats.statsByDeed')} + {prayerDeeds.map((deed) => ( - + ))} @@ -238,15 +200,9 @@ const StatsScreen = () => { }; return ( - - - {i18n.t("stats.activity")} - - + + {i18n.t('stats.activity')} + {renderContent()} ); @@ -258,8 +214,8 @@ const getStyles = (theme: AppTheme) => StyleSheet.create({ centered: { flex: 1, - justifyContent: "center", - alignItems: "center", + justifyContent: 'center', + alignItems: 'center', }, scrollContent: { paddingBottom: theme.spacing.xl, @@ -272,9 +228,9 @@ const getStyles = (theme: AppTheme) => marginBottom: theme.spacing.s, }, summaryGrid: { - flexDirection: "row", - flexWrap: "wrap", - justifyContent: "space-between", + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-between', marginHorizontal: -theme.spacing.xs, // Counteract summary box margin }, card: {