diff --git a/.github/workflow/deploy.yml b/.github/workflow/deploy.yml new file mode 100644 index 0000000..e69de29 diff --git a/.github/workflows/testAndDeploy.yml b/.github/workflows/testAndDeploy.yml new file mode 100644 index 0000000..eeedb0f --- /dev/null +++ b/.github/workflows/testAndDeploy.yml @@ -0,0 +1,382 @@ +name: Test and Deploy Ignite React Native App + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + workflow_dispatch: + inputs: + deploy_environment: + description: 'Deployment environment' + required: true + default: 'staging' + type: choice + options: + - staging + - production + +env: + NODE_VERSION: '18' + JAVA_VERSION: '17' + XCODE_VERSION: '15.0' + +jobs: + # Install dependencies and cache them + setup: + runs-on: ubuntu-latest + outputs: + cache-key: ${{ steps.cache-key.outputs.key }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Generate cache key + id: cache-key + run: echo "key=node-modules-${{ hashFiles('**/package.json') }}" >> $GITHUB_OUTPUT + + - name: Cache node modules + uses: actions/cache@v3 + with: + path: node_modules + key: ${{ steps.cache-key.outputs.key }} + restore-keys: | + node-modules- + + - name: Install dependencies + run: | + npm install + npx react-native doctor || true + + # Lint and Type Check + lint-and-typecheck: + needs: setup + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Restore node modules cache + uses: actions/cache@v3 + with: + path: node_modules + key: ${{ needs.setup.outputs.cache-key }} + + - name: Install dependencies + run: npm install + + - name: Run ESLint + run: npm run lint + + - name: Run TypeScript check + run: npx tsc --noEmit + + - name: Check formatting + run: npm run format:check || npx prettier --check "**/*.{js,jsx,ts,tsx,json,md}" + + # Run tests + test: + needs: setup + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Restore node modules cache + uses: actions/cache@v3 + with: + path: node_modules + key: ${{ needs.setup.outputs.cache-key }} + + - name: Install dependencies + run: npm install + + - name: Run unit tests + run: npm test -- --coverage --watchAll=false + + - name: Upload coverage reports + uses: codecov/codecov-action@v3 + with: + file: ./coverage/lcov.info + fail_ci_if_error: false + + # Security audit + security-audit: + needs: setup + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Run security audit + run: npm audit --audit-level=high + + - name: Check for vulnerabilities + run: npx audit-ci --moderate + + # Build Android APK/AAB + build-android: + needs: [lint-and-typecheck, test] + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' || github.event_name == 'workflow_dispatch' + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Restore node modules cache + uses: actions/cache@v3 + with: + path: node_modules + key: ${{ needs.setup.outputs.cache-key }} + + - name: Install dependencies + run: npm install + + - name: Cache Gradle + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + android/.gradle + key: gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + + - name: Make gradlew executable + run: chmod +x android/gradlew + + - name: Clean Android build + run: | + cd android + ./gradlew clean + + - name: Build Android Debug APK + if: github.ref == 'refs/heads/develop' + run: | + cd android + ./gradlew assembleDebug + + - name: Build Android Release APK + if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' + env: + ANDROID_KEYSTORE: ${{ secrets.ANDROID_KEYSTORE }} + ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + run: | + cd android + ./gradlew assembleRelease + + - name: Upload Android APK + uses: actions/upload-artifact@v4 + with: + name: android-apk + path: | + android/app/build/outputs/apk/**/*.apk + retention-days: 30 + + # Build iOS IPA + build-ios: + needs: [lint-and-typecheck, test] + runs-on: macos-latest + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' || github.event_name == 'workflow_dispatch' + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Setup Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: ${{ env.XCODE_VERSION }} + + - name: Restore node modules cache + uses: actions/cache@v3 + with: + path: node_modules + key: ${{ needs.setup.outputs.cache-key }} + + - name: Install dependencies + run: npm install + + - name: Cache CocoaPods + uses: actions/cache@v3 + with: + path: ios/Pods + key: pods-${{ hashFiles('**/Podfile.lock') }} + restore-keys: | + pods- + + - name: Install CocoaPods dependencies + run: | + cd ios + bundle install + bundle exec pod install + + - name: Build iOS Debug + if: github.ref == 'refs/heads/develop' + run: | + cd ios + xcodebuild -workspace Dooit.xcworkspace \ + -scheme Dooit \ + -configuration Debug \ + -destination 'generic/platform=iOS Simulator' \ + build + + - name: Build iOS Release + if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' + env: + IOS_CERTIFICATE: ${{ secrets.IOS_CERTIFICATE }} + IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} + IOS_PROVISIONING_PROFILE: ${{ secrets.IOS_PROVISIONING_PROFILE }} + run: | + cd ios + # Setup certificates and provisioning profiles here + xcodebuild -workspace Dooit.xcworkspace \ + -scheme Dooit \ + -configuration Release \ + -archivePath Dooit.xcarchive \ + archive + + - name: Upload iOS Archive + if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' + uses: actions/upload-artifact@v4 + with: + name: ios-archive + path: ios/Dooit.xcarchive + retention-days: 30 + + # Deploy to staging/production + deploy: + needs: [build-android, build-ios] + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' + environment: + name: ${{ github.event.inputs.deploy_environment || 'staging' }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download Android APK + uses: actions/download-artifact@v4 + with: + name: android-apk + path: ./android-build + + - name: Download iOS Archive + uses: actions/download-artifact@v4 + with: + name: ios-archive + path: ./ios-build + + - name: Deploy to App Store Connect + if: contains(github.event.inputs.deploy_environment, 'production') || github.ref == 'refs/heads/main' + env: + APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }} + APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} + APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} + run: | + echo "Deploying to App Store Connect..." + # Add your App Store Connect deployment commands here + + - name: Deploy to Google Play Console + if: contains(github.event.inputs.deploy_environment, 'production') || github.ref == 'refs/heads/main' + env: + GOOGLE_PLAY_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT }} + run: | + echo "Deploying to Google Play Console..." + # Add your Google Play Console deployment commands here + + - name: Deploy to Firebase App Distribution + if: contains(github.event.inputs.deploy_environment, 'staging') + env: + FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} + FIREBASE_APP_ID_ANDROID: ${{ secrets.FIREBASE_APP_ID_ANDROID }} + FIREBASE_APP_ID_IOS: ${{ secrets.FIREBASE_APP_ID_IOS }} + run: | + npm install -g firebase-tools + echo "Deploying to Firebase App Distribution..." + # Add Firebase App Distribution commands here + + # Send notifications + notify: + needs: [deploy] + runs-on: ubuntu-latest + if: always() && (github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch') + steps: + - name: Send Slack notification + if: always() + uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + channel: '#mobile-deployments' + text: | + ${{ github.event_name == 'workflow_dispatch' && 'Manual' || 'Automatic' }} deployment ${{ job.status }} + Branch: ${{ github.ref_name }} + Environment: ${{ github.event.inputs.deploy_environment || 'staging' }} + Commit: ${{ github.sha }} + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + + - name: Send email notification + if: failure() + uses: dawidd6/action-send-mail@v3 + with: + server_address: smtp.gmail.com + server_port: 587 + username: ${{ secrets.EMAIL_USERNAME }} + password: ${{ secrets.EMAIL_PASSWORD }} + subject: "❌ Dooit App Deployment Failed" + body: | + The deployment of Dooit app has failed. + + Branch: ${{ github.ref_name }} + Commit: ${{ github.sha }} + Workflow: ${{ github.workflow }} + + Please check the GitHub Actions logs for more details. + to: ${{ secrets.NOTIFICATION_EMAIL }} + from: ${{ secrets.EMAIL_USERNAME }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index cb8b804..355a5de 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ # .DS_Store +.env + # Xcode # build/ @@ -85,6 +87,10 @@ web-build/ # Configurations !env.js +.env.*.js +.env.local +.env.*.local + /coverage @@ -95,3 +101,10 @@ web-build/ !.yarn/releases !.yarn/sdks !.yarn/versions + +google-services.json +firebase-service-account.json +dooit-6e563-firebase-adminsdk-fbsvc-a82e6fd47e.json +key.txt.pub +key.txt.pub +dooit-6e563-firebase-adminsdk-fbsvc-a82e6fd47e.json diff --git a/.txt b/.txt new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index 830563e..8d60509 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ A powerful and user-friendly to-do list application built with React Native and - Create, edit, and delete tasks - Mark tasks as complete/incomplete - Add task descriptions and due dates - - Real-time synchronization + - Real-time synchronizations - 🔐 User Authentication - Email/password sign-up and login diff --git a/app.json b/app.json index 01dab3c..5af76d4 100644 --- a/app.json +++ b/app.json @@ -27,7 +27,8 @@ "allowBackup": false, "splash": { "backgroundColor": "#fff" - } + }, + "googleServicesFile": "./google-services.json" }, "ios": { "icon": "./assets/images/app-icon-ios.png", @@ -35,6 +36,19 @@ "bundleIdentifier": "com.dooit", "splash": { "backgroundColor": "#fff" + }, + "privacyManifests": { + "NSPrivacyAccessedAPITypes": [ + { + "NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryUserDefaults", + "NSPrivacyAccessedAPITypeReasons": [ + "CA92.1" + ] + } + ] + }, + "infoPlist": { + "ITSAppUsesNonExemptEncryption": false } }, "web": { @@ -45,6 +59,13 @@ } }, "plugins": [ + [ + "expo-notifications", + { + "icon": "./assets/images/notification-icon.png", + "color": "#ffffff" + } + ], "expo-localization", "expo-font", [ diff --git a/app/app.tsx b/app/app.tsx index b1f990a..ec3bcfb 100644 --- a/app/app.tsx +++ b/app/app.tsx @@ -14,26 +14,33 @@ if (__DEV__) { // Load Reactotron in development only. // Note that you must be using metro's `inlineRequires` for this to work. // If you turn it off in metro.config.js, you'll have to manually import it. - require("./devtools/ReactotronConfig.ts") + require("./devtools/ReactotronConfig.ts"); } -import "./utils/gestureHandler" -import { initI18n } from "./i18n" -import { useFonts } from "expo-font" -import { useEffect, useState } from "react" -import { initialWindowMetrics, SafeAreaProvider } from "react-native-safe-area-context" -import * as Linking from "expo-linking" -import * as SplashScreen from "expo-splash-screen" -import { useInitialRootStore } from "./models" -import { AppNavigator, useNavigationPersistence } from "./navigators" -import * as storage from "./utils/storage" -import { customFontsToLoad } from "./theme" -import { KeyboardProvider } from "react-native-keyboard-controller" -import { loadDateFnsLocale } from "./utils/formatDate" +import "./utils/gestureHandler"; +import { initI18n } from "./i18n"; +import { useFonts } from "expo-font"; +import { useEffect, useState } from "react"; +import { + initialWindowMetrics, + SafeAreaProvider, +} from "react-native-safe-area-context"; +import * as Linking from "expo-linking"; +import * as SplashScreen from "expo-splash-screen"; +import { useInitialRootStore } from "./models"; +import { AppNavigator, useNavigationPersistence } from "./navigators"; +import * as storage from "./utils/storage"; +import { customFontsToLoad } from "./theme"; +import { KeyboardProvider } from "react-native-keyboard-controller"; +import { loadDateFnsLocale } from "./utils/formatDate"; +import "react-native-get-random-values"; +import { api } from "./services/api"; -export const NAVIGATION_PERSISTENCE_KEY = "NAVIGATION_STATE" + + +export const NAVIGATION_PERSISTENCE_KEY = "NAVIGATION_STATE"; // Web linking configuration -const prefix = Linking.createURL("/") +const prefix = Linking.createURL("/"); const config = { screens: { Login: { @@ -51,7 +58,7 @@ const config = { }, }, }, -} +}; /** * This is the root component of our app. @@ -63,24 +70,33 @@ export function App() { initialNavigationState, onNavigationStateChange, isRestored: isNavigationStateRestored, - } = useNavigationPersistence(storage, NAVIGATION_PERSISTENCE_KEY) + } = useNavigationPersistence(storage, NAVIGATION_PERSISTENCE_KEY); + + const [areFontsLoaded, fontLoadError] = useFonts(customFontsToLoad); + const [isI18nInitialized, setIsI18nInitialized] = useState(false); - const [areFontsLoaded, fontLoadError] = useFonts(customFontsToLoad) - const [isI18nInitialized, setIsI18nInitialized] = useState(false) + + const runTest = async () => { + const result = await api.getGlobalHealthcareNews(); + if (result.kind === "ok") { + console.log(result.articles); // Global health articles + } + }; useEffect(() => { + initI18n() .then(() => setIsI18nInitialized(true)) - .then(() => loadDateFnsLocale()) - }, []) + .then(() => loadDateFnsLocale()); + }, []); const { rehydrated } = useInitialRootStore(() => { // This runs after the root store has been initialized and rehydrated. // If your initialization scripts run very fast, it's good to show the splash screen for just a bit longer to prevent flicker. // Slightly delaying splash screen hiding for better UX; can be customized or removed as needed, - setTimeout(SplashScreen.hideAsync, 500) - }) + setTimeout(SplashScreen.hideAsync, 500); + }); // Before we show the app, we have to wait for our state to be ready. // In the meantime, don't render anything. This will be the background @@ -94,13 +110,13 @@ export function App() { !isI18nInitialized || (!areFontsLoaded && !fontLoadError) ) { - return null + return null; } const linking = { prefixes: [prefix], config, - } + }; // otherwise, we're ready to render the app return ( @@ -113,5 +129,5 @@ export function App() { /> - ) + ); } diff --git a/app/components/.txt b/app/components/.txt new file mode 100644 index 0000000..394720b --- /dev/null +++ b/app/components/.txt @@ -0,0 +1,91 @@ + + + // Simple time picker component + const SimpleTimePicker = ({ onTimeSelect }: { onTimeSelect: (time: Date) => void }) => { + const times = []; + + // Generate times in 15-minute intervals + for (let hour = 0; hour < 24; hour++) { + for (let minute = 0; minute < 60; minute += 15) { + const time = new Date(); + time.setHours(hour, minute, 0, 0); + times.push(time); + } + } + + return ( + + + + + Select Time + setShowTimePicker(false)}> + + + + + {times.map((time, index) => ( + { + onTimeSelect(time); + setShowTimePicker(false); + }} + > + {formatTime(time)} + + ))} + + + + + ); + }; + + const [showTimePicker, setShowTimePicker] = useState(false); + + const [selectedTime, setSelectedTime] = useState(new Date()); + + + +const formatTime = (time: Date) => { + return time.toLocaleTimeString("en-US", { + hour: "2-digit", + minute: "2-digit", + }); + }; + + const $pickerList: ThemedStyle = ({ spacing }) => ({ + maxHeight: 300, +}); + + + + + {/* Time Selection */} + + + Time + + setShowTimePicker(true)} + style={themed($dateTimeButton)} + > + + 🕐 {formatTime(selectedTime)} + + + + + +const $pickerItem: ThemedStyle = ({ colors, spacing }) => ({ + padding: spacing.md, + borderBottomWidth: 1, + borderBottomColor: colors.separator, +}); + +const $pickerItemText: ThemedStyle = ({ colors }) => ({ + color: colors.text, + fontSize: 16, +}); \ No newline at end of file diff --git a/app/components/AlertTongle.tsx b/app/components/AlertTongle.tsx new file mode 100644 index 0000000..f5a9c59 --- /dev/null +++ b/app/components/AlertTongle.tsx @@ -0,0 +1,104 @@ +import { Alert, StyleProp, TextStyle, View, ViewStyle } from "react-native" +import { observer } from "mobx-react-lite" +import { useAppTheme } from "@/utils/useAppTheme" +import type { ThemedStyle } from "@/theme" +import { Text } from "@/components/Text" +import React, { useEffect } from "react" + +export interface AlertTongleProps { + /** + * An optional style override useful for padding & margin. + */ + style?: StyleProp +} + +type AlertItem = { + title: string + message: string + buttons?: Array<{ + text: string + onPress?: () => void + style?: 'default' | 'cancel' | 'destructive' + }> +} + +// Static queue manager +class AlertQueueManager { + private static queue: AlertItem[] = [] + private static isShowing = false + private static currentAlert: AlertItem | null = null + + static enqueue(alert: AlertItem) { + if (this.isDuplicate(alert)) return + + this.queue.push(alert) + this.showNext() + } + + private static isDuplicate(alert: AlertItem): boolean { + if (this.isShowing && this.currentAlert?.title === alert.title && this.currentAlert?.message === alert.message) { + return true + } + if (this.queue.some(q => q.title === alert.title && q.message === alert.message)) { + return true + } + return false + } + + private static showNext() { + if (this.isShowing || this.queue.length === 0) return + + this.isShowing = true + const nextAlert = this.queue.shift()! + this.currentAlert = nextAlert + + const { title, message, buttons = [{ text: "OK" }] } = nextAlert + + Alert.alert(title, message, buttons.map(button => ({ + ...button, + onPress: () => { + button.onPress?.() + this.isShowing = false + this.currentAlert = null + this.showNext() + }, + }))) + } +} + +// Exportable function +export const showQueuedAlert = (alert: AlertItem) => { + AlertQueueManager.enqueue(alert) +} + + +// COMPONENT +export const AlertTongle = observer(function AlertTongle(props: AlertTongleProps) { + const { style } = props + const $styles = [$container, style] + const { themed } = useAppTheme() + + useEffect(() => { + // Example trigger + showQueuedAlert({ + title: "Initial Alert", + message: "This is triggered from AlertTongle component", + }) + }, []) + + return ( + + Hello + + ) +}) + +const $container: ViewStyle = { + justifyContent: "center", +} + +const $text: ThemedStyle = ({ colors, typography }) => ({ + fontFamily: typography.primary.normal, + fontSize: 14, + color: colors.palette.primary500, +}) diff --git a/app/components/Calendar.tsx b/app/components/Calendar.tsx new file mode 100644 index 0000000..df40a2e --- /dev/null +++ b/app/components/Calendar.tsx @@ -0,0 +1,1197 @@ +import { + StyleProp, + TextStyle, + View, + ViewStyle, + TouchableOpacity, + ScrollView, + SectionList, + RefreshControl, +} from "react-native"; +import { observer } from "mobx-react-lite"; +import { useAppTheme } from "@/utils/useAppTheme"; +import type { ThemedStyle } from "@/theme"; +import { Text } from "@/components/Text"; +import { useState, useCallback, useMemo } from "react"; +import { useStores } from "@/models"; + +export interface CalendarProps { + /** + * An optional style override useful for padding & margin. + */ + style?: StyleProp; + /** + * Callback when a date is selected + */ + onDateSelect?: (date: Date) => void; + /** + * Selected date + */ + selectedDate?: Date; + /** + * Show tasks on calendar + */ + showTasks?: boolean; + /** + * Minimum selectable date + */ + minDate?: Date; + /** + * Maximum selectable date + */ + maxDate?: Date; + /** + * Show agenda list view instead of task summary + */ + showAgenda?: boolean; + /** + * Callback when a task is pressed in the agenda + */ + onTaskPress?: (task: any) => void; + /** + * Number of days to show in agenda + */ + agendaDays?: number; +} + +interface AgendaSection { + title: string; + data: any[]; + date: Date; +} + +// ENHANCEMENT 1: New interfaces for task types and periods +interface TaskPeriod { + startDate: Date; + endDate: Date; + task: any; + isStart: boolean; + isEnd: boolean; + isMiddle: boolean; + isSingleDay: boolean; +} + +interface DayTaskInfo { + singleDayTasks: any[]; + multiDayPeriods: TaskPeriod[]; + allTasks: any[]; +} + +/** + * Custom Calendar component with task integration and agenda list + */ +export const Calendar = observer(function Calendar(props: CalendarProps) { + const { + style, + onDateSelect, + selectedDate, + showTasks = true, + minDate, + maxDate, + showAgenda = false, + onTaskPress, + agendaDays = 3, + } = props; + + const $styles = [$container, style]; + const { themed } = useAppTheme(); + const { taskStore } = useStores(); + + const [currentDate, setCurrentDate] = useState(new Date()); + const [viewDate, setViewDate] = useState(new Date()); + const [refreshing, setRefreshing] = useState(false); + + // Get current month and year + const currentMonth = viewDate.getMonth(); + const currentYear = viewDate.getFullYear(); + + // Month names + const monthNames = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ]; + + // Day names + const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + + // Get first day of the month and number of days + const firstDayOfMonth = new Date(currentYear, currentMonth, 1).getDay(); + const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate(); + const daysInPrevMonth = new Date(currentYear, currentMonth, 0).getDate(); + + // ENHANCEMENT 2: Helper function to check if a task spans multiple days + const isMultiDayTask = useCallback((task: any): boolean => { + if (!task.startDate || !task.dueDate) return false; + + const startDate = new Date(task.startDate); + const endDate = new Date(task.dueDate); + + // Reset time to compare only dates + startDate.setHours(0, 0, 0, 0); + endDate.setHours(0, 0, 0, 0); + + return startDate.getTime() !== endDate.getTime(); + }, []); + + // ENHANCEMENT 3: Helper function to get task period info for a specific date + const getTaskPeriodInfo = useCallback((task: any, date: Date): TaskPeriod | null => { + if (!task.startDate || !task.dueDate) return null; + + const startDate = new Date(task.startDate); + const endDate = new Date(task.dueDate); + const checkDate = new Date(date); + + // Reset times for date comparison + startDate.setHours(0, 0, 0, 0); + endDate.setHours(0, 0, 0, 0); + checkDate.setHours(0, 0, 0, 0); + + const startTime = startDate.getTime(); + const endTime = endDate.getTime(); + const checkTime = checkDate.getTime(); + + // Check if date falls within task period + if (checkTime < startTime || checkTime > endTime) return null; + + const isSingleDay = startTime === endTime; + const isStart = checkTime === startTime; + const isEnd = checkTime === endTime; + const isMiddle = !isStart && !isEnd && !isSingleDay; + + return { + startDate, + endDate, + task, + isStart, + isEnd, + isMiddle, + isSingleDay, + }; + }, []); + + // ENHANCEMENT 4: Enhanced task mapping for the current month + const monthTasksEnhanced = useMemo(() => { + if (!showTasks) return {}; + + const tasksMap: { [key: string]: DayTaskInfo } = {}; + const allTasks = taskStore.tasks.slice(); + + // Initialize all days of the month + for (let day = 1; day <= daysInMonth; day++) { + tasksMap[day.toString()] = { + singleDayTasks: [], + multiDayPeriods: [], + allTasks: [], + }; + } + + allTasks.forEach((task) => { + if (task.startDate && task.dueDate) { + const startDate = new Date(task.startDate); + const endDate = new Date(task.dueDate); + + // Check each day of the month to see if task overlaps + for (let day = 1; day <= daysInMonth; day++) { + const checkDate = new Date(currentYear, currentMonth, day); + const periodInfo = getTaskPeriodInfo(task, checkDate); + + if (periodInfo) { + const dayKey = day.toString(); + tasksMap[dayKey].allTasks.push(task); + + if (periodInfo.isSingleDay) { + tasksMap[dayKey].singleDayTasks.push(task); + } else { + tasksMap[dayKey].multiDayPeriods.push(periodInfo); + } + } + } + } else if (task.dueDate) { + // Fallback for tasks with only dueDate + const taskDate = new Date(task.dueDate); + if ( + taskDate.getMonth() === currentMonth && + taskDate.getFullYear() === currentYear + ) { + const dayKey = taskDate.getDate().toString(); + tasksMap[dayKey].singleDayTasks.push(task); + tasksMap[dayKey].allTasks.push(task); + } + } + }); + + return tasksMap; + }, [taskStore.tasks, currentMonth, currentYear, showTasks, daysInMonth, getTaskPeriodInfo]); + + // Format section title + const formatSectionTitle = (date: Date): string => { + const today = new Date(); + const tomorrow = new Date(today); + tomorrow.setDate(today.getDate() + 1); + + if ( + date.getDate() === today.getDate() && + date.getMonth() === today.getMonth() && + date.getFullYear() === today.getFullYear() + ) { + return "Today"; + } + + if ( + date.getDate() === tomorrow.getDate() && + date.getMonth() === tomorrow.getMonth() && + date.getFullYear() === tomorrow.getFullYear() + ) { + return "Tomorrow"; + } + + return date.toLocaleDateString("en-US", { + weekday: "long", + month: "short", + day: "numeric", + }); + }; + + // Force re-computation when tasks change by using tasks length as dependency + const tasksVersion = useMemo(() => taskStore.tasks.length, [ + taskStore.tasks.length, + ]); + + // CHANGE 1: Updated agendaSections to show tasks from startDate to dueDate + const agendaSections = useMemo((): AgendaSection[] => { + if (!showAgenda) return []; + + const sections: AgendaSection[] = []; + const startDate = selectedDate || new Date(); + + // Force fresh computation + const allTasks = taskStore.tasks.slice(); + + for (let i = 0; i < agendaDays; i++) { + const date = new Date(startDate); + date.setDate(startDate.getDate() + i); + + // CHANGE 2: Enhanced task filtering to include tasks that span across the date + const dayTasks = allTasks.filter((task) => { + // Check if task has both startDate and dueDate + if (task.startDate && task.dueDate) { + const taskStartDate = new Date(task.startDate); + const taskEndDate = new Date(task.dueDate); + const checkDate = new Date(date); + + // Reset times for date comparison + taskStartDate.setHours(0, 0, 0, 0); + taskEndDate.setHours(0, 0, 0, 0); + checkDate.setHours(0, 0, 0, 0); + + // Include task if the date falls within the task's date range + return checkDate.getTime() >= taskStartDate.getTime() && + checkDate.getTime() <= taskEndDate.getTime(); + } + + // Fallback: if only dueDate exists, use original logic + if (task.dueDate) { + const taskDate = new Date(task.dueDate); + return ( + taskDate.getDate() === date.getDate() && + taskDate.getMonth() === date.getMonth() && + taskDate.getFullYear() === date.getFullYear() + ); + } + + return false; + }); + + // Always add section, even if no tasks (to show empty state) + sections.push({ + title: formatSectionTitle(date), + data: dayTasks, + date: date, + }); + } + + return sections; + }, [taskStore.tasks, selectedDate, agendaDays, showAgenda, tasksVersion]); + + // Navigate to previous month + const goToPrevMonth = useCallback(() => { + setViewDate(new Date(currentYear, currentMonth - 1, 1)); + }, [currentYear, currentMonth]); + + // Navigate to next month + const goToNextMonth = useCallback(() => { + setViewDate(new Date(currentYear, currentMonth + 1, 1)); + }, [currentYear, currentMonth]); + + // Handle date selection + const handleDatePress = useCallback( + (day: number) => { + const selectedDate = new Date(currentYear, currentMonth, day); + + // Check if date is within allowed range + if (minDate && selectedDate < minDate) return; + if (maxDate && selectedDate > maxDate) return; + + setCurrentDate(selectedDate); + onDateSelect?.(selectedDate); + }, + [currentYear, currentMonth, minDate, maxDate, onDateSelect] + ); + + // Handle refresh + const onRefresh = useCallback(() => { + setRefreshing(true); + // Simulate refresh delay + setTimeout(() => { + setRefreshing(false); + }, 1000); + }, []); + + // Check if a date is today + const isToday = useCallback( + (day: number) => { + const today = new Date(); + return ( + today.getDate() === day && + today.getMonth() === currentMonth && + today.getFullYear() === currentYear + ); + }, + [currentMonth, currentYear] + ); + + // Check if a date is selected + const isSelected = useCallback( + (day: number) => { + if (!selectedDate) return false; + return ( + selectedDate.getDate() === day && + selectedDate.getMonth() === currentMonth && + selectedDate.getFullYear() === currentYear + ); + }, + [selectedDate, currentMonth, currentYear] + ); + + // Check if a date is disabled + const isDisabled = useCallback( + (day: number) => { + const date = new Date(currentYear, currentMonth, day); + if (minDate && date < minDate) return true; + if (maxDate && date > maxDate) return true; + return false; + }, + [currentYear, currentMonth, minDate, maxDate] + ); + + // ENHANCEMENT 5: Render multi-day task period indicator + const renderMultiDayIndicator = useCallback((periods: TaskPeriod[]) => { + if (periods.length === 0) return null; + + return ( + + {periods.slice(0, 3).map((period, index) => ( + + ))} + {periods.length > 3 && ( + +{periods.length - 3} + )} + + ); + }, [themed]); + + // ENHANCEMENT 6: Render single day task dots + const renderSingleDayDots = useCallback((tasks: any[]) => { + if (tasks.length === 0) return null; + + const maxDots = 4; + const visibleTasks = tasks.slice(0, maxDots); + const remainingCount = tasks.length - maxDots; + + return ( + + {visibleTasks.map((task, index) => ( + + ))} + {remainingCount > 0 && ( + +{remainingCount} + )} + + ); + }, [themed]); + + // ENHANCEMENT 7: Helper function to get consistent task colors + const getTaskColor = useCallback((task: any, index: number) => { + const colors = [ + '#FF6B6B', // Red + '#4ECDC4', // Teal + '#45B7D1', // Blue + '#96CEB4', // Green + '#FECA57', // Yellow + '#FF9FF3', // Pink + '#54A0FF', // Light Blue + '#5F27CD', // Purple + ]; + + // Use task priority for color if available + if (task.priority) { + switch (task.priority) { + case 'high': return '#FF6B6B'; + case 'medium': return '#FECA57'; + case 'low': return '#96CEB4'; + } + } + + // Fallback to index-based color + return colors[index % colors.length]; + }, []); + + // Generate calendar days + const generateCalendarDays = () => { + const days = []; + + // Previous month days + for (let i = firstDayOfMonth - 1; i >= 0; i--) { + const day = daysInPrevMonth - i; + days.push( + + {day} + + ); + } + + // Current month days + for (let day = 1; day <= daysInMonth; day++) { + const dayInfo = monthTasksEnhanced[day.toString()] || { + singleDayTasks: [], + multiDayPeriods: [], + allTasks: [], + }; + + const disabled = isDisabled(day); + + days.push( + handleDatePress(day)} + disabled={disabled} + > + + {day} + + + {/* ENHANCEMENT 8: Enhanced task indicators */} + {showTasks && ( + + {/* Multi-day task periods */} + {renderMultiDayIndicator(dayInfo.multiDayPeriods)} + + {/* Single day task dots */} + {renderSingleDayDots(dayInfo.singleDayTasks)} + + )} + + ); + } + + // Next month days to fill the grid + const remainingDays = 42 - days.length; // 6 rows × 7 days + for (let day = 1; day <= remainingDays; day++) { + days.push( + + {day} + + ); + } + + return days; + }; + + // CHANGE 3: Enhanced agenda item rendering with task period context + const renderAgendaItem = useCallback( + ({ item, section }: { item: any; section: AgendaSection }) => { + // CHANGE 4: Determine task period context for this specific date + const currentDate = section.date; + const taskPeriodInfo = getTaskPeriodInfo(item, currentDate); + + // CHANGE 5: Generate period indicator text + const getPeriodText = () => { + if (!taskPeriodInfo || taskPeriodInfo.isSingleDay) return null; + + if (taskPeriodInfo.isStart) return "Starts"; + if (taskPeriodInfo.isEnd) return "Ends"; + if (taskPeriodInfo.isMiddle) return "Continues"; + return null; + }; + + const periodText = getPeriodText(); + + return ( + onTaskPress?.(item)} + activeOpacity={0.7} + > + + + {/* CHANGE 7: Add period context text */} + {periodText && ( + + {periodText} + + )} + + {item.title} + + {item.description && ( + + {item.description} + + )} + {/* CHANGE 8: Show task duration for multi-day tasks */} + {taskPeriodInfo && !taskPeriodInfo.isSingleDay && ( + + {taskPeriodInfo.startDate.toLocaleDateString("en-US", { + month: "short", + day: "numeric" + })} - {taskPeriodInfo.endDate.toLocaleDateString("en-US", { + month: "short", + day: "numeric" + })} + + )} + {item.dueDate && ( + + {new Date(item.taskTime).toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + hour12: true, + })} + + )} + + + ); + }, + [onTaskPress, themed, getTaskPeriodInfo] + ); + + // Render empty agenda section + const renderEmptyAgendaSection = useCallback(() => { + return ( + + No tasks scheduled + + ); + }, [themed]); + + // Render section header + const renderSectionHeader = useCallback( + ({ section }: { section: AgendaSection }) => { + return ( + + {section.title} + + {section.date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + })} + + + ); + }, + [themed] + ); + + return ( + + {/* Header */} + + + + + + + {monthNames[currentMonth]} {currentYear} + + + + + + + + {/* Day names */} + + {dayNames.map((dayName) => ( + + {dayName} + + ))} + + + {/* Calendar grid */} + {generateCalendarDays()} + + {/* Agenda List */} + {showAgenda ? ( + + + item.id?.toString() || index.toString() + } + showsVerticalScrollIndicator={false} + refreshControl={ + + } + stickySectionHeadersEnabled={false} + ItemSeparatorComponent={() => ( + + )} + contentContainerStyle={themed($agendaContentContainer)} + /> + + ) : ( + /* Task summary for selected date */ + showTasks && + selectedDate && ( + + + Tasks for {selectedDate.toLocaleDateString()} + + {monthTasksEnhanced[selectedDate.getDate().toString()]?.allTasks.length > 0 ? ( + + {monthTasksEnhanced[selectedDate.getDate().toString()].allTasks.map( + (task, index) => ( + onTaskPress?.(task)} + > + + + + {task.title} + + {task.description && ( + + {task.description} + + )} + + + ) + )} + + ) : ( + No tasks for this date + )} + + ) + )} + + ); +}); + + +// #region Styles + +const $container: ViewStyle = { + backgroundColor: "transparent", + flex: 1, +}; + +const $header: ThemedStyle = ({ spacing }) => ({ + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingHorizontal: spacing.md, + paddingVertical: spacing.sm, +}); + +const $navButton: ThemedStyle = ({ colors, spacing }) => ({ + padding: spacing.xs, + borderRadius: 20, + backgroundColor: colors.palette.neutral100, + width: 40, + height: 40, + justifyContent: "center", + alignItems: "center", +}); + +const $navButtonText: ThemedStyle = ({ colors, typography }) => ({ + fontFamily: typography.primary.medium, + fontSize: 20, + color: colors.palette.primary500, +}); + +const $monthYearText: ThemedStyle = ({ colors, typography }) => ({ + fontFamily: typography.primary.medium, + fontSize: 18, + color: colors.text, +}); + +const $dayNamesContainer: ThemedStyle = ({ spacing }) => ({ + flexDirection: "row", + paddingHorizontal: spacing.md, + paddingVertical: spacing.sm, +}); + +const $dayNameContainer: ThemedStyle = () => ({ + flex: 1, + alignItems: "center", +}); + +const $dayNameText: ThemedStyle = ({ colors, typography }) => ({ + fontFamily: typography.primary.medium, + fontSize: 12, + color: colors.textDim, +}); + +const $calendarGrid: ThemedStyle = ({ spacing }) => ({ + flexDirection: "row", + flexWrap: "wrap", + paddingHorizontal: spacing.md, +}); + +const $dayContainer: ThemedStyle = ({ spacing }) => ({ + width: `${100 / 7}%`, + aspectRatio: 1, + justifyContent: "center", + alignItems: "center", + padding: spacing.xs, + position: "relative", +}); + +const $todayContainer: ThemedStyle = ({ colors }) => ({ + backgroundColor: colors.palette.primary100, + borderRadius: 20, +}); + +const $selectedContainer: ThemedStyle = ({ colors }) => ({ + backgroundColor: colors.palette.primary500, + borderRadius: 20, +}); + +const $disabledContainer: ThemedStyle = () => ({ + opacity: 0.3, +}); + +const $dayText: ThemedStyle = ({ colors, typography }) => ({ + fontFamily: typography.primary.normal, + fontSize: 16, + color: colors.text, +}); + +const $todayText: ThemedStyle = ({ colors }) => ({ + color: colors.palette.primary500, + fontWeight: "bold", +}); + +const $selectedText: ThemedStyle = ({ colors }) => ({ + color: colors.palette.neutral100, + fontWeight: "bold", +}); + +const $disabledText: ThemedStyle = ({ colors }) => ({ + color: colors.textDim, +}); + +const $dayTextDisabled: ThemedStyle = ({ colors, typography }) => ({ + fontFamily: typography.primary.normal, + fontSize: 16, + color: colors.palette.neutral400, +}); + +// ENHANCEMENT 9: New styles for enhanced task indicators +const $taskIndicatorsContainer: ThemedStyle = () => ({ + position: "absolute", + bottom: 2, + left: 0, + right: 0, + alignItems: "center", +}); + +// Multi-day task period styles +const $multiDayContainer: ThemedStyle = () => ({ + width: "100%", + alignItems: "center", + marginBottom: 2, +}); + +const $multiDayIndicator: ThemedStyle = () => ({ + height: 3, + width: "90%", + marginBottom: 1, +}); + +const $multiDayStart: ThemedStyle = () => ({ + borderTopLeftRadius: 2, + borderBottomLeftRadius: 2, +}); + +const $multiDayEnd: ThemedStyle = () => ({ + borderTopRightRadius: 2, + borderBottomRightRadius: 2, +}); + +const $multiDayMiddle: ThemedStyle = () => ({ + // No border radius for middle segments +}); + +const $multiDayOverflow: ThemedStyle = ({ colors, typography }) => ({ + fontFamily: typography.primary.normal, + fontSize: 8, + color: colors.textDim, + marginTop: 1, +}); + +// Single-day task dots styles +const $taskDotsContainer: ThemedStyle = () => ({ + flexDirection: "row", + justifyContent: "center", + alignItems: "center", + flexWrap: "wrap", + gap: 1, +}); + +const $taskDot: ThemedStyle = () => ({ + width: 4, + height: 4, + borderRadius: 2, +}); + +const $taskDotCompleted: ThemedStyle = () => ({ + opacity: 0.6, +}); + +const $taskDotsOverflow: ThemedStyle = ({ colors, typography }) => ({ + fontFamily: typography.primary.normal, + fontSize: 6, + color: colors.textDim, + marginLeft: 1, +}); + +// Legacy styles (kept for backward compatibility) +const $taskIndicatorContainer: ThemedStyle = () => ({ + position: "absolute", + bottom: 4, + flexDirection: "row", + gap: 2, +}); + +const $taskIndicatorPending: ThemedStyle = ({ colors }) => ({ + width: 4, + height: 4, + borderRadius: 2, + backgroundColor: colors.palette.angry500, +}); + +const $taskIndicatorCompleted: ThemedStyle = ({ colors }) => ({ + width: 4, + height: 4, + borderRadius: 2, + backgroundColor: colors.palette.primary500, +}); + +// Agenda List Styles +const $agendaContainer: ThemedStyle = ({ spacing }) => ({ + flex: 1, + marginTop: spacing.md, +}); + +const $agendaContentContainer: ThemedStyle = ({ spacing }) => ({ + paddingHorizontal: spacing.md, + paddingBottom: spacing.lg, +}); + +const $sectionHeader: ThemedStyle = ({ colors, spacing }) => ({ + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingVertical: spacing.sm, + paddingHorizontal: spacing.md, + backgroundColor: colors.palette.neutral100, + borderRadius: 8, + marginVertical: spacing.xs, +}); + +const $sectionHeaderText: ThemedStyle = ({ + colors, + typography, +}) => ({ + fontFamily: typography.primary.medium, + fontSize: 16, + color: colors.text, + textTransform: "capitalize", +}); + +const $sectionHeaderDate: ThemedStyle = ({ + colors, + typography, +}) => ({ + fontFamily: typography.primary.normal, + fontSize: 14, + color: colors.textDim, +}); + +const $agendaItem: ThemedStyle = ({ colors, spacing }) => ({ + flexDirection: "row", + alignItems: "flex-start", + backgroundColor: colors.palette.neutral100, + borderRadius: 8, + padding: spacing.md, + marginVertical: spacing.xs, + shadowColor: colors.palette.neutral900, + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.1, + shadowRadius: 2, + elevation: 2, +}); + +const $agendaItemIndicator: ThemedStyle = ({ colors }) => ({ + width: 4, + height: 40, + borderRadius: 2, + backgroundColor: colors.palette.angry500, + marginRight: 12, + marginTop: 2, +}); + +const $agendaItemIndicatorCompleted: ThemedStyle = ({ colors }) => ({ + backgroundColor: colors.palette.primary500, +}); + +// CHANGE 9: New styles for period-specific indicators +const $agendaItemIndicatorStart: ThemedStyle = ({ colors }) => ({ + backgroundColor: colors.palette.secondary500, + borderTopLeftRadius: 2, + borderTopRightRadius: 2, +}); + +const $agendaItemIndicatorEnd: ThemedStyle = ({ colors }) => ({ + backgroundColor: colors.palette.secondary500, + borderBottomLeftRadius: 2, + borderBottomRightRadius: 2, +}); + +const $agendaItemIndicatorMiddle: ThemedStyle = ({ colors }) => ({ + backgroundColor: colors.palette.secondary300, +}); + +// CHANGE 10: New styles for period text and duration +const $agendaItemPeriodText: ThemedStyle = ({ colors, typography }) => ({ + fontFamily: typography.primary.medium, + fontSize: 10, + color: colors.palette.secondary500, + textTransform: "uppercase", + marginBottom: 2, +}); + +const $agendaItemDuration: ThemedStyle = ({ colors, typography }) => ({ + fontFamily: typography.primary.normal, + fontSize: 11, + color: colors.palette.secondary400, + marginBottom: 2, + fontStyle: "italic", +}); + +const $agendaItemContent: ThemedStyle = () => ({ + flex: 1, +}); + +const $agendaItemTitle: ThemedStyle = ({ colors, typography }) => ({ + fontFamily: typography.primary.medium, + fontSize: 16, + color: colors.text, + marginBottom: 4, +}); + +const $agendaItemTitleCompleted: ThemedStyle = ({ colors }) => ({ + textDecorationLine: "line-through", + color: colors.textDim, +}); + +const $agendaItemDescription: ThemedStyle = ({ + colors, + typography, +}) => ({ + fontFamily: typography.primary.normal, + fontSize: 14, + color: colors.textDim, + marginBottom: 4, +}); + +const $agendaItemTime: ThemedStyle = ({ colors, typography }) => ({ + fontFamily: typography.primary.normal, + fontSize: 12, + color: colors.palette.primary500, + fontWeight: "600", +}); + +const $emptyAgendaSection: ThemedStyle = ({ spacing }) => ({ + padding: spacing.lg, + alignItems: "center", +}); + +const $emptyAgendaText: ThemedStyle = ({ colors, typography }) => ({ + fontFamily: typography.primary.normal, + fontSize: 14, + color: colors.textDim, + fontStyle: "italic", +}); + +const $itemSeparator: ThemedStyle = () => ({ + height: 1, + backgroundColor: "transparent", +}); + +// Original Task Summary Styles (kept for backward compatibility) +const $taskSummary: ThemedStyle = ({ colors, spacing }) => ({ + marginTop: spacing.md, + padding: spacing.md, + backgroundColor: colors.palette.neutral100, + borderRadius: 8, + maxHeight: 200, +}); + +const $taskSummaryTitle: ThemedStyle = ({ + colors, + typography, + spacing, +}) => ({ + fontFamily: typography.primary.medium, + fontSize: 16, + color: colors.text, + marginBottom: spacing.sm, +}); + +const $tasksList: ThemedStyle = () => ({ + flex: 1, +}); + +const $taskItem: ThemedStyle = ({ spacing }) => ({ + flexDirection: "row", + alignItems: "flex-start", + paddingVertical: spacing.xs, + gap: spacing.sm, +}); + +const $taskStatusIndicator: ThemedStyle = ({ colors }) => ({ + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: colors.palette.angry500, + marginTop: 4, +}); + +const $taskStatusCompleted: ThemedStyle = ({ colors }) => ({ + backgroundColor: colors.palette.primary500, +}); + +const $taskContent: ThemedStyle = () => ({ + flex: 1, +}); + +const $taskTitle: ThemedStyle = ({ colors, typography }) => ({ + fontFamily: typography.primary.medium, + fontSize: 14, + color: colors.text, +}); + +const $taskTitleCompleted: ThemedStyle = ({ colors }) => ({ + textDecorationLine: "line-through", + color: colors.textDim, +}); + +const $taskDescription: ThemedStyle = ({ colors, typography }) => ({ + fontFamily: typography.primary.normal, + fontSize: 12, + color: colors.textDim, + marginTop: 2, +}); + +const $noTasksText: ThemedStyle = ({ colors, typography }) => ({ + fontFamily: typography.primary.normal, + fontSize: 14, + color: colors.textDim, + textAlign: "center", + fontStyle: "italic", +}); \ No newline at end of file diff --git a/app/components/Icon.tsx b/app/components/Icon.tsx index e562ebd..13e5114 100644 --- a/app/components/Icon.tsx +++ b/app/components/Icon.tsx @@ -111,6 +111,7 @@ export function Icon(props: IconProps) { export const iconRegistry = { back: require("../../assets/icons/back.png"), bell: require("../../assets/icons/bell.png"), + calendar: require("../../assets/icons/schedule.png"), caretLeft: require("../../assets/icons/caretLeft.png"), caretRight: require("../../assets/icons/caretRight.png"), check: require("../../assets/icons/check.png"), @@ -131,6 +132,9 @@ export const iconRegistry = { slack: require("../../assets/icons/demo/slack.png"), view: require("../../assets/icons/view.png"), x: require("../../assets/icons/x.png"), + google: require("../../assets/icons/google.png"), + spot: require("../../assets/icons/black-circle.png"), + news: require("../../assets/icons/newspaper.png"), } const $imageStyleBase: ImageStyle = { diff --git a/app/components/TaskModal.tsx b/app/components/TaskModal.tsx new file mode 100644 index 0000000..643f9fb --- /dev/null +++ b/app/components/TaskModal.tsx @@ -0,0 +1,1238 @@ +// TaskModal.tsx - Enhanced version with start date, due date, and improved date navigation +import React, { FC, useState, useEffect } from "react"; +import { + StyleProp, + ViewStyle, + Modal, + View, + ScrollView, + TouchableOpacity, + Alert, + Dimensions, + TextStyle, + Platform, +} from "react-native"; +import { observer } from "mobx-react-lite"; + +import { useAppTheme } from "@/utils/useAppTheme"; +import type { ThemedStyle, Theme } from "@/theme"; +import { Text } from "@/components/Text"; +import { TextField } from "@/components/TextField"; +import { Button } from "@/components/Button"; +import DateTimePicker from "@react-native-community/datetimepicker"; + +export interface TaskModalProps { + style?: StyleProp; + visible: boolean; + onClose: () => void; + onSave: (task: { + title: string; + description: string; + startDate: Date; + dueDate: Date; + taskTime: Date; + priority: "high" | "medium" | "low"; + reminder: boolean; + }) => void; + initialDate?: Date; +} + +export const TaskModal: FC = observer(function TaskModal( + props +) { + const { style, visible, onClose, onSave, initialDate } = props; + const { themed, theme } = useAppTheme(); + + const [title, setTitle] = useState(""); + const [description, setDescription] = useState(""); + const [startDate, setStartDate] = useState(initialDate || new Date()); + const [dueDate, setDueDate] = useState(() => { + const date = initialDate || new Date(); + const tomorrow = new Date(date); + tomorrow.setDate(tomorrow.getDate() + 1); + return tomorrow; + }); + const [showTimePicker, setShowTimePicker] = useState(false); + const [selectedTime, setSelectedTime] = useState(new Date()); + const [showDatePicker, setShowDatePicker] = useState(false); + const [datePickerType, setDatePickerType] = useState<"start" | "due">( + "start" + ); + const [selectedPriority, setSelectedPriority] = useState< + "high" | "medium" | "low" + >("medium"); + const [reminderEnabled, setReminderEnabled] = useState(true); + + // Update dates when initialDate changes + useEffect(() => { + if (initialDate) { + setStartDate(new Date(initialDate)); + const tomorrow = new Date(initialDate); + tomorrow.setDate(tomorrow.getDate() + 1); + setDueDate(tomorrow); + } + }, [initialDate]); + + const priorities = [ + { key: "high", label: "🔴 High Priority", description: "Urgent tasks that need immediate attention" }, + { key: "medium", label: "🟡 Medium Priority", description: "Important tasks with moderate urgency" }, + { key: "low", label: "🟢 Low Priority", description: "Tasks that can be done when time allows" }, + ]; + + const handleSave = () => { + if (!title.trim()) { + Alert.alert("Error", "Please enter a task title"); + return; + } + + // Add date validation + if (isNaN(startDate.getTime()) || isNaN(dueDate.getTime())) { + Alert.alert("Error", "Please select valid dates"); + return; + } + + if (startDate > dueDate) { + Alert.alert("Error", "Start date cannot be after due date"); + return; + } + + onSave({ + title: title.trim(), + description: description.trim(), + taskTime: selectedTime, + startDate: startDate, + dueDate: dueDate, + priority: selectedPriority, + reminder: reminderEnabled, + }); + + // Reset form + setTitle(""); + setDescription(""); + setStartDate(new Date()); + setDueDate(new Date()); + setSelectedPriority("medium"); + setReminderEnabled(true); + onClose(); + }; + + const formatDate = (date: Date) => { + return date.toLocaleDateString("en-US", { + weekday: "short", + month: "short", + day: "numeric", + year: "numeric", + }); + }; + + const formatTime = (time: Date) => { + return time.toLocaleTimeString("en-US", { + hour: "2-digit", + minute: "2-digit", + }); + }; + + const openDatePicker = (type: "start" | "due") => { + setDatePickerType(type); + setShowDatePicker(true); + }; + + // Fixed Time Picker Component + const SimpleTimePicker = () => { + const [selectedHour, setSelectedHour] = useState(selectedTime.getHours()); + const [selectedMinute, setSelectedMinute] = useState( + selectedTime.getMinutes() + ); + + const hours = Array.from({ length: 24 }, (_, i) => i); + const minutes = Array.from({ length: 60 }, (_, i) => i); + + const handleConfirm = () => { + const newTime = new Date(selectedTime); + newTime.setHours(selectedHour, selectedMinute, 0, 0); + setSelectedTime(newTime); + setShowTimePicker(false); + }; + + return ( + + + + + + Select Time + + setShowTimePicker(false)}> + + + + + + {/* Hour Picker */} + + Hour + + {hours.map((hour) => ( + setSelectedHour(hour)} + > + + {hour.toString().padStart(2, "0")} + + + ))} + + + + {/* Separator */} + + : + + + {/* Minute Picker */} + + Minute + + {minutes + .filter((m) => m % 5 === 0) + .map((minute) => ( + setSelectedMinute(minute)} + > + + {minute.toString().padStart(2, "0")} + + + ))} + + + + + {/* Quick Time Buttons */} + + { + setSelectedHour(9); + setSelectedMinute(0); + }} + > + 9:00 AM + + { + setSelectedHour(13); + setSelectedMinute(0); + }} + > + 1:00 PM + + { + setSelectedHour(18); + setSelectedMinute(0); + }} + > + 6:00 PM + + + + {/* Action Buttons */} + + + ) +} + +// News article card component - simplified +const NewsArticleCard: FC<{ + article: NewsArticle + onPressFavorite: () => void + isFavorite: boolean +}> = ({ article, onPressFavorite, isFavorite }) => { + const { themed } = useAppTheme() + + const imageSource = useMemo(() => + article.image_url ? { uri: article.image_url } : undefined + , [article.image_url]) + + const accessibilityProps = useMemo( + (): AccessibilityProps => ({ + accessibilityLabel: article.title, + accessibilityActions: [{ name: "longpress", label: "Toggle favorite" }], + onAccessibilityAction: ({ nativeEvent }) => { + if (nativeEvent.actionName === "longpress") { + onPressFavorite() + } + }, + accessibilityRole: "button", + accessibilityHint: "Tap to view article, long press to toggle favorite", + }), + [article.title, onPressFavorite, isFavorite], + ) + + const handlePressCard = useCallback(() => { + openLinkInBrowser(article.link) + }, [article.link]) + + return ( + + + + {article.primaryCountry && ( + + )} + + } + content={article.title} + RightComponent={ + imageSource ? ( + + ) : undefined + } + FooterComponent={ + + {article.shortDescription && ( + + )} + + + } + /> + ) +} + +// Styles remain the same +const $listContentContainer: ThemedStyle = ({ spacing }) => ({ + paddingHorizontal: spacing.lg, + paddingTop: spacing.lg + spacing.xl, + paddingBottom: spacing.lg, +}) + +const $heading: ThemedStyle = ({ spacing }) => ({ + marginBottom: spacing.md, +}) + +const $subtitle: ThemedStyle = ({ colors, spacing }) => ({ + color: colors.textDim, + marginTop: spacing.xs, +}) + +const $resultsCount: ThemedStyle = ({ colors, spacing }) => ({ + color: colors.textDim, + marginTop: spacing.sm, +}) + +const $item: ThemedStyle = ({ colors, spacing }) => ({ + padding: spacing.md, + marginTop: spacing.md, + minHeight: 140, + backgroundColor: colors.palette.neutral100, +}) + +const $itemThumbnail: ThemedStyle = ({ spacing }) => ({ + marginTop: spacing.sm, + borderRadius: 8, + alignSelf: "flex-start", + width: 80, + height: 80, +}) + +const $description: ThemedStyle = ({ colors, spacing }) => ({ + color: colors.textDim, + marginTop: spacing.xs, marginBottom: spacing.sm, }) -const $tagline: ThemedStyle = ({ spacing }) => ({ - marginBottom: spacing.xxl, +const $toggle: ThemedStyle = ({ spacing }) => ({ + marginTop: spacing.md, }) -const $description: ThemedStyle = ({ spacing }) => ({ - marginBottom: spacing.lg, +const $labelStyle: TextStyle = { + textAlign: "left", +} + +const $iconContainer: ThemedStyle = ({ spacing }) => ({ + height: ICON_SIZE, + width: ICON_SIZE, + marginEnd: spacing.sm, }) -const $sectionTitle: ThemedStyle = ({ spacing }) => ({ - marginTop: spacing.xxl, +const $metadata: ThemedStyle = ({ spacing }) => ({ + marginTop: spacing.xs, }) -const $logoContainer: ThemedStyle = ({ spacing }) => ({ +const $metadataText: ThemedStyle = ({ colors, spacing }) => ({ + color: colors.textDim, marginEnd: spacing.md, - flexWrap: "wrap", - alignContent: "center", - alignSelf: "stretch", + marginBottom: spacing.xs, }) -const $logo: ImageStyle = { - height: 38, - width: 38, -} +const $favoriteButton: ThemedStyle = ({ colors, spacing }) => ({ + borderRadius: 17, + marginTop: spacing.md, + justifyContent: "flex-start", + backgroundColor: colors.palette.neutral300, + borderColor: colors.palette.neutral300, + paddingHorizontal: spacing.md, + paddingTop: spacing.xxxs, + paddingBottom: 0, + minHeight: 32, + alignSelf: "flex-start", +}) + +const $unFavoriteButton: ThemedStyle = ({ colors }) => ({ + borderColor: colors.palette.primary100, + backgroundColor: colors.palette.primary100, +}) + +const $loadingFooter: ThemedStyle = ({ spacing }) => ({ + paddingVertical: spacing.lg, + alignItems: "center", +}) + +const $loadingText: ThemedStyle = ({ colors, spacing }) => ({ + color: colors.textDim, + marginTop: spacing.sm, +}) + +const $emptyState: ThemedStyle = ({ spacing }) => ({ + marginTop: spacing.xxl, +}) + +const $emptyStateImage: ImageStyle = { + transform: [{ scaleX: isRTL ? -1 : 1 }], +} \ No newline at end of file diff --git a/app/screens/DemoDebugScreen.tsx b/app/screens/DemoDebugScreen.tsx index 50af476..f9e5ab7 100644 --- a/app/screens/DemoDebugScreen.tsx +++ b/app/screens/DemoDebugScreen.tsx @@ -1,5 +1,5 @@ -import { FC, useCallback, useMemo } from "react" -import * as Application from "expo-application" +import { FC, useCallback, useMemo, useState, useEffect } from "react"; +import * as Application from "expo-application"; import { LayoutAnimation, Linking, @@ -8,35 +8,74 @@ import { useColorScheme, View, ViewStyle, -} from "react-native" -import { Button, ListItem, Screen, Text } from "../components" -import { DemoTabScreenProps } from "../navigators/DemoNavigator" -import type { ThemedStyle } from "@/theme" -import { $styles } from "../theme" -import { isRTL } from "@/i18n" -import { useStores } from "../models" -import { useAppTheme } from "@/utils/useAppTheme" + Alert, +} from "react-native"; +import { Button, ListItem, Screen, Text } from "../components"; +import { DemoTabScreenProps } from "../navigators/DemoNavigator"; +import type { ThemedStyle } from "@/theme"; +import { $styles } from "../theme"; +import { isRTL } from "@/i18n"; +import { useStores } from "../models"; +import { useAppTheme } from "@/utils/useAppTheme"; +import { + registerForPushNotificationsAsync, + sendPushNotification, + setupNotificationListeners, + NotificationPermissionError +} from "../services/notifications/notification"; // Adjust path as needed /** * @param {string} url - The URL to open in the browser. * @returns {void} - No return value. */ function openLinkInBrowser(url: string) { - Linking.canOpenURL(url).then((canOpen) => canOpen && Linking.openURL(url)) + Linking.canOpenURL(url).then((canOpen) => canOpen && Linking.openURL(url)); } -const usingHermes = typeof HermesInternal === "object" && HermesInternal !== null +const usingHermes = + typeof HermesInternal === "object" && HermesInternal !== null; -export const DemoDebugScreen: FC> = function DemoDebugScreen( - _props, -) { - const { setThemeContextOverride, themeContext, themed } = useAppTheme() +export const DemoDebugScreen: FC> = function DemoDebugScreen(_props) { + const { setThemeContextOverride, themeContext, themed, theme } = useAppTheme(); const { authenticationStore: { logout }, - } = useStores() + } = useStores(); + + // Push notification state + const [expoPushToken, setExpoPushToken] = useState(''); + const [notificationError, setNotificationError] = useState(null); + const [isNotificationLoading, setIsNotificationLoading] = useState(false); // @ts-expect-error - const usingFabric = global.nativeFabricUIManager != null + const usingFabric = global.nativeFabricUIManager != null; + + // Setup push notifications + useEffect(() => { + const initializeNotifications = async () => { + setIsNotificationLoading(true); + try { + const token = await registerForPushNotificationsAsync( + theme.colors.palette.primary500 + ); + setExpoPushToken(token || ''); + setNotificationError(null); + } catch (error) { + setNotificationError(error as NotificationPermissionError); + console.error('Push notification setup failed:', error); + } finally { + setIsNotificationLoading(false); + } + }; + + initializeNotifications(); + + // Set up notification listeners + const cleanup = setupNotificationListeners(); + + return cleanup; + }, [theme.colors.palette.primary500]); const demoReactotron = useMemo( () => async () => { @@ -49,25 +88,75 @@ export const DemoDebugScreen: FC> = function Dem appVersion: Application.nativeApplicationVersion, appBuildVersion: Application.nativeBuildVersion, hermesEnabled: usingHermes, + pushToken: expoPushToken, }, important: true, - }) + }); } }, - [], - ) + [expoPushToken] + ); const toggleTheme = useCallback(() => { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) // Animate the transition - setThemeContextOverride(themeContext === "dark" ? "light" : "dark") - }, [themeContext, setThemeContextOverride]) + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); // Animate the transition + setThemeContextOverride(themeContext === "dark" ? "light" : "dark"); + }, [themeContext, setThemeContextOverride]); // Resets the theme to the system theme - const colorScheme = useColorScheme() + const colorScheme = useColorScheme(); const resetTheme = useCallback(() => { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) - setThemeContextOverride(undefined) - }, [setThemeContextOverride]) + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + setThemeContextOverride(undefined); + }, [setThemeContextOverride]); + + // Test push notification + const testPushNotification = useCallback(async () => { + if (!expoPushToken) { + Alert.alert('Error', 'No push token available. Make sure notifications are enabled.'); + return; + } + + try { + const success = await sendPushNotification( + expoPushToken, + 'Test Notification 🚀', + 'This is a test notification from the debug screen!', + { + screen: 'debug', + timestamp: new Date().toISOString(), + testData: 'Hello from Dooit!' + } + ); + + if (success) { + Alert.alert('Success', 'Test notification sent! Check your notification tray.'); + } else { + Alert.alert('Error', 'Failed to send test notification. Check console for details.'); + } + } catch (error) { + Alert.alert('Error', `Failed to send notification: ${error}`); + } + }, [expoPushToken]); + + const getNotificationStatus = () => { + if (isNotificationLoading) return 'Setting up...'; + if (notificationError) { + switch (notificationError.code) { + case 'PERMISSION_DENIED': + return 'Permission denied'; + case 'NO_DEVICE': + return 'Simulator (not supported)'; + case 'NO_PROJECT_ID': + return 'Config error'; + case 'TOKEN_ERROR': + return 'Token error'; + default: + return 'Setup failed'; + } + } + if (expoPushToken) return 'Ready'; + return 'Not available'; + }; return ( > = function Dem openLinkInBrowser("https://github.com/infinitered/ignite/issues")} + onPress={() => + openLinkInBrowser("https://github.com/neweracy/Dooit/issues") + } /> - + Current system theme: {colorScheme} Current app theme: {themeContext} + ) : undefined + } + RightComponent={ + + + } /> ) @@ -318,69 +437,136 @@ const $listContentContainer: ThemedStyle = ({ spacing }) => ({ const $heading: ThemedStyle = ({ spacing }) => ({ marginBottom: spacing.md, + alignItems: "center", +}) + +const $subtitle: ThemedStyle = ({ colors, spacing }) => ({ + color: colors.textDim, + marginTop: spacing.xs, +}) + +const $filterContainer: ThemedStyle = ({ spacing }) => ({ + marginBottom: spacing.md, +}) + +const $filterLabel: ThemedStyle = ({ colors, spacing }) => ({ + color: colors.textDim, + marginBottom: spacing.xs, + fontWeight: "600", +}) + +const $filterButtonRow: ThemedStyle = ({ spacing }) => ({ + + flexDirection: "row", + flexWrap: "wrap", + gap: spacing.xs, +}) + +const $filterButton: ThemedStyle = ({ colors, spacing }) => ({ + backgroundColor: colors.palette.neutral200, + borderColor: colors.palette.neutral300, + borderRadius: 16, + paddingHorizontal: spacing.sm, + paddingVertical: spacing.xs, + minHeight: 32, +}) + +const $priorityFilterButton: ThemedStyle = ({ colors, spacing }) => ({ + backgroundColor: colors.palette.neutral200, + borderColor: colors.palette.neutral300, + borderRadius: 16, + paddingHorizontal: spacing.sm, + paddingVertical: spacing.xs, + minHeight: 32, + flex: 1, + maxWidth: "27%", +}) + +const $activeFilterButton: ThemedStyle = ({ colors }) => ({ + backgroundColor: colors.palette.primary100, + borderColor: colors.palette.primary300, +}) + +const $filterButtonText: ThemedStyle = ({ colors }) => ({ + color: colors.textDim, + fontSize: 12, + fontWeight: "500", +}) + +const $activeFilterButtonText: ThemedStyle = ({ colors }) => ({ + color: colors.palette.primary600, + fontWeight: "600", }) const $item: ThemedStyle = ({ colors, spacing }) => ({ padding: spacing.md, marginTop: spacing.md, - minHeight: 120, + minHeight: 100, backgroundColor: colors.palette.neutral100, }) -const $itemThumbnail: ThemedStyle = ({ spacing }) => ({ - marginTop: spacing.sm, - borderRadius: 50, - alignSelf: "flex-start", +const $completedItem: ThemedStyle = ({ colors }) => ({ + backgroundColor: colors.palette.neutral100, + opacity: 0.8, }) -const $toggle: ThemedStyle = ({ spacing }) => ({ - marginTop: spacing.md, +const $completedText: ThemedStyle = ({ colors }) => ({ + textDecorationLine: "line-through", + color: colors.textDim, }) -const $labelStyle: TextStyle = { - textAlign: "left", -} +const $description: ThemedStyle = ({ colors, spacing }) => ({ + color: colors.textDim, + marginTop: spacing.xs, + fontStyle: "italic", +}) const $iconContainer: ThemedStyle = ({ spacing }) => ({ - height: ICON_SIZE, - width: ICON_SIZE, - marginEnd: spacing.sm, + // padding: spacing.xs, + alignItems: "center", + justifyContent: "center", }) -const $metadata: ThemedStyle = ({ colors, spacing }) => ({ - color: colors.textDim, - marginTop: spacing.xs, +const $metadata: ThemedStyle = ({ spacing }) => ({ + justifyContent: "space-between", + alignItems: "center", + marginBottom: spacing.xs, }) const $metadataText: ThemedStyle = ({ colors, spacing }) => ({ color: colors.textDim, - marginEnd: spacing.md, - marginBottom: spacing.xs, + marginStart: spacing.xs, }) -const $favoriteButton: ThemedStyle = ({ colors, spacing }) => ({ - borderRadius: 17, - marginTop: spacing.md, - justifyContent: "flex-start", - backgroundColor: colors.palette.neutral300, +const $actionButtons: ThemedStyle = ({ spacing }) => ({ + flexDirection: "row", + alignItems: "center", + gap: spacing.xs, +}) + +const $actionButton: ThemedStyle = ({ colors, spacing }) => ({ + backgroundColor: colors.palette.neutral200, borderColor: colors.palette.neutral300, - paddingHorizontal: spacing.md, - paddingTop: spacing.xxxs, - paddingBottom: 0, - minHeight: 32, - alignSelf: "flex-start", + // borderRadius: 20, + width: 36, + height: 20, + padding: 0, + alignItems: "center", + justifyContent: "center", }) -const $unFavoriteButton: ThemedStyle = ({ colors }) => ({ - borderColor: colors.palette.primary100, - backgroundColor: colors.palette.primary100, +const $deleteButton: ThemedStyle = ({ colors }) => ({ + backgroundColor: colors.palette.angry100, + borderColor: colors.palette.angry100, + borderRadius: 20, + width: 44, + height: 20, + paddingTop: 18, + alignItems: "center", + justifyContent: "center", }) const $emptyState: ThemedStyle = ({ spacing }) => ({ marginTop: spacing.xxl, }) - -const $emptyStateImage: ImageStyle = { - transform: [{ scaleX: isRTL ? -1 : 1 }], -} -// #endregion +// #endregion \ No newline at end of file diff --git a/app/screens/DemoShowroomScreen/DemoShowroomScreen.tsx b/app/screens/DemoShowroomScreen/DemoShowroomScreen.tsx index fc761b1..fcdc95b 100644 --- a/app/screens/DemoShowroomScreen/DemoShowroomScreen.tsx +++ b/app/screens/DemoShowroomScreen/DemoShowroomScreen.tsx @@ -1,290 +1,632 @@ -import { Link, RouteProp, useRoute } from "@react-navigation/native" -import { FC, ReactElement, useCallback, useEffect, useRef, useState } from "react" -import { Image, ImageStyle, Platform, SectionList, TextStyle, View, ViewStyle } from "react-native" -import { Drawer } from "react-native-drawer-layout" -import { type ContentStyle } from "@shopify/flash-list" -import { ListItem, ListView, ListViewRef, Screen, Text } from "../../components" -import { TxKeyPath, isRTL, translate } from "@/i18n" -import { DemoTabParamList, DemoTabScreenProps } from "../../navigators/DemoNavigator" -import type { Theme, ThemedStyle } from "@/theme" -import { $styles } from "@/theme" -import { useSafeAreaInsetsStyle } from "../../utils/useSafeAreaInsetsStyle" -import * as Demos from "./demos" -import { DrawerIconButton } from "./DrawerIconButton" -import SectionListWithKeyboardAwareScrollView from "./SectionListWithKeyboardAwareScrollView" -import { useAppTheme } from "@/utils/useAppTheme" - -const logo = require("../../../assets/images/logo.png") +// Import necessary React and React Native components and hooks +import { + Link, + RouteProp, + useFocusEffect, + useRoute, +} from "@react-navigation/native"; +import { + FC, + ReactElement, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { + Image, + ImageStyle, + Platform, + SectionList, + View, + ViewStyle, + Alert, + TextStyle, +} from "react-native"; +import { Drawer } from "react-native-drawer-layout"; +import type { ContentStyle } from "@shopify/flash-list"; +// Import custom components +import { + ListItem, + ListView, + ListViewRef, + Screen, + Text, + Button, + TaskModal, + showQueuedAlert, +} from "../../components"; +// Import i18n utilities for internationalization +import { TxKeyPath, isRTL, translate } from "@/i18n"; +// Import navigation types and theme utilities +import { + DemoTabParamList, + DemoTabScreenProps, +} from "../../navigators/DemoNavigator"; +import type { Theme, ThemedStyle } from "@/theme"; +import { $styles } from "@/theme"; +// Import custom hooks and utilities +import { useSafeAreaInsetsStyle } from "../../utils/useSafeAreaInsetsStyle"; +import * as Demos from "./demos"; +import { DrawerIconButton } from "./DrawerIconButton"; +import SectionListWithKeyboardAwareScrollView from "./SectionListWithKeyboardAwareScrollView"; +import { useAppTheme } from "@/utils/useAppTheme"; +// Import calendar components and state management +import { Calendar } from "@/components"; +import { useStores } from "@/models"; +import { observer } from "mobx-react-lite"; +import { useHandler } from "react-native-reanimated"; +import { useHeader } from "@/utils/useHeader"; +// App logo import +const logo = require("../../../assets/images/app-icon-android-adaptive-foreground.png"); + +/** + * Interface defining the structure of a demo component + * @property {string} name - Name of the demo + * @property {TxKeyPath} description - Internationalization key for the description + * @property {Function} data - Function that returns an array of React elements for the demo + */ export interface Demo { - name: string - description: TxKeyPath - data: ({ themed, theme }: { themed: any; theme: Theme }) => ReactElement[] + name: string; + description: TxKeyPath; + data: ({ themed, theme }: { themed: any; theme: Theme }) => ReactElement[]; } +/** + * Interface for demo list item props + * @property {Object} item - The demo item containing name and use cases + * @property {number} sectionIndex - Index of the current section + * @property {Function} [handleScroll] - Optional callback for handling scroll to section + */ interface DemoListItem { - item: { name: string; useCases: string[] } - sectionIndex: number - handleScroll?: (sectionIndex: number, itemIndex?: number) => void + item: { name: string; useCases: string[] }; + sectionIndex: number; + handleScroll?: (sectionIndex: number, itemIndex?: number) => void; } -const slugify = (str: string) => - str - .toLowerCase() - .trim() - .replace(/[^\w\s-]/g, "") - .replace(/[\s_-]+/g, "-") - .replace(/^-+|-+$/g, "") - -const WebListItem: FC = ({ item, sectionIndex }) => { - const sectionSlug = item.name.toLowerCase() - const { themed } = useAppTheme() - return ( - - - {item.name} - - {item.useCases.map((u) => { - const itemSlug = slugify(u) - - return ( - - {u} - - ) - })} - - ) -} +// Platform detection constant +const isAndroid = Platform.OS === "android"; -const NativeListItem: FC = ({ item, sectionIndex, handleScroll }) => { - const { themed } = useAppTheme() - return ( - - handleScroll?.(sectionIndex)} - preset="bold" - style={themed($menuContainer)} - > - {item.name} - - {item.useCases.map((u, index) => ( - handleScroll?.(sectionIndex, index)} - text={u} - rightIcon={isRTL ? "caretLeft" : "caretRight"} - /> - ))} - - ) -} +/** + * Main Demo Showroom Screen component + * Displays a collection of demo components with navigation + */ +export const DemoShowroomScreen: FC> = observer(function DemoShowroomScreen(_props) { + // State for controlling the drawer open/close + const [open, setOpen] = useState(false); + const listRef = useRef(null); -const ShowroomListItem = Platform.select({ web: WebListItem, default: NativeListItem }) -const isAndroid = Platform.OS === "android" - -export const DemoShowroomScreen: FC> = - function DemoShowroomScreen(_props) { - const [open, setOpen] = useState(false) - const timeout = useRef>() - const listRef = useRef(null) - const menuRef = useRef>(null) - const route = useRoute>() - const params = route.params - - const { themed, theme } = useAppTheme() - - const toggleDrawer = useCallback(() => { - if (!open) { - setOpen(true) - } else { - setOpen(false) - } - }, [open]) + const [taskModalVisible, setTaskModalVisible] = useState(false); + const [selectedDate, setSelectedDate] = useState(new Date()); - const handleScroll = useCallback((sectionIndex: number, itemIndex = 0) => { - try { - listRef.current?.scrollToLocation({ - animated: true, - itemIndex, - sectionIndex, - viewPosition: 0.25, - }) - } catch (e) { - console.error(e) - } + // Get theme and styles + const { themed, theme } = useAppTheme(); + + // Get stores and their methods + const { + taskStore: { + fetchTasks, // Fetches tasks from the server + tasks, // List of all tasks + completedTasks, // List of completed tasks + createTask, // Creates a new task + pendingTasks, // List of pending tasks + getTasksForDate, + }, + } = useStores(); + + // Fetch tasks when screen loads + useEffect(() => { + loadTasks(); + }, []); + + // Refresh tasks when screen comes into focus + useFocusEffect( + useCallback(() => { + loadTasks(); }, []) + ); - // handle Web links - useEffect(() => { - if (params !== undefined && Object.keys(params).length > 0) { - const demoValues = Object.values(Demos) - const findSectionIndex = demoValues.findIndex( - (x) => x.name.toLowerCase() === params.queryIndex, - ) - let findItemIndex = 0 - if (params.itemIndex) { - try { - findItemIndex = demoValues[findSectionIndex] - .data({ themed, theme }) - .findIndex((u) => slugify(translate(u.props.name)) === params.itemIndex) - } catch (err) { - console.error(err) - } + const loadTasks = async () => { + try { + await fetchTasks(); + } catch (error) { + console.error("Error loading tasks:", error); + + showQueuedAlert({ + title: "Error", + message: "Failed to load tasks. Please try again.", + }); + } + }; + + // Debounced task refresh to prevent race conditions + const debouncedRefresh = useCallback( + useMemo(() => { + let timeoutId: NodeJS.Timeout; + return () => { + clearTimeout(timeoutId); + timeoutId = setTimeout(loadTasks, 100); + }; + }, []), + [] + ); + + // Handle calendar day press + const handleDayPress = (day: { + dateString: string; + day: number; + month: number; + year: number; + timestamp: number; + }) => { + console.log("selected day", day); + setSelectedDate(new Date(day.dateString)); + // Open task modal when a day is pressed + setTaskModalVisible(true); + }; + + // Wrapper function for Calendar component + const handleDateSelect = (date: Date) => { + const dayObject = { + dateString: date.toISOString().split("T")[0], + day: date.getDate(), + month: date.getMonth() + 1, + year: date.getFullYear(), + timestamp: date.getTime(), + }; + handleDayPress(dayObject); + }; + + // Handle task creation + const handleSaveTask = useCallback( + async (taskData: { + title: string; + description: string; + startDate: Date; + dueDate: Date; + taskTime: Date; + priority: "high" | "medium" | "low"; + reminder: boolean; + }) => { + try { + // Use selected date if no specific date is provided + const startDate = taskData.startDate || selectedDate; + const dueDate = taskData.dueDate || selectedDate; + + const result = await createTask({ + title: taskData.title, + description: taskData.description, + startDate: startDate.toISOString(), + dueDate: dueDate.toISOString(), + taskTime: taskData.taskTime, + priority: taskData.priority, + reminderEnabled: taskData.reminder, + }); + + if (result) { + Alert.alert("Success", "Task created successfully!"); + setTaskModalVisible(false); + // Use debounced refresh to prevent race conditions + debouncedRefresh(); } - handleScroll(findSectionIndex, findItemIndex) + } catch (error) { + console.error("Task creation error:", error); + Alert.alert("Error", "Failed to create task"); } - }, [handleScroll, params, theme, themed]) + }, + [createTask, debouncedRefresh, selectedDate] + ); - const scrollToIndexFailed = (info: { - index: number - highestMeasuredFrameIndex: number - averageItemLength: number - }) => { - listRef.current?.getScrollResponder()?.scrollToEnd() - timeout.current = setTimeout( - () => - listRef.current?.scrollToLocation({ - animated: true, - itemIndex: info.index, - sectionIndex: 0, - }), - 50, - ) + const scrollToIndexFailed = (info: any) => { + const wait = new Promise((resolve) => setTimeout(resolve, 500)); + wait.then(() => { + listRef.current?.scrollToIndex({ index: info.index, animated: true }); + }); + }; + + // Get tasks for selected date - use useMemo to prevent unnecessary recalculations + const selectedDateTasks = useMemo(() => { + if (!selectedDate) return []; + try { + // Create plain objects to avoid MST tree issues + const tasksForDate = getTasksForDate(selectedDate); + return tasksForDate.map((task) => ({ + id: task.id, + title: task.title, + description: task.description, + priority: task.priority || "medium", + taskTime: task.taskTime, + isCompleted: task.isCompleted, + dueDate: task.dueDate, + })); + } catch (error) { + console.warn("Error getting tasks for date:", error); + return []; } + }, [selectedDate, tasks.length, getTasksForDate]); // Use tasks.length instead of tasks array directly - useEffect(() => { - return () => timeout.current && clearTimeout(timeout.current) - }, []) + // Get task counts safely + const completedTasksCount = useMemo(() => { + try { + return completedTasks.length; + } catch (error) { + console.warn("Error getting completed tasks count:", error); + return 0; + } + }, [completedTasks.length]); - const $drawerInsets = useSafeAreaInsetsStyle(["top"]) - - return ( - setOpen(true)} - onClose={() => setOpen(false)} - drawerType="back" - drawerPosition={isRTL ? "right" : "left"} - renderDrawerContent={() => ( - - - - - - ref={menuRef} - contentContainerStyle={themed($listContentContainer)} - estimatedItemSize={250} - data={Object.values(Demos).map((d) => ({ - name: d.name, - useCases: d.data({ theme, themed }).map((u) => translate(u.props.name)), - }))} - keyExtractor={(item) => item.name} - renderItem={({ item, index: sectionIndex }) => ( - - )} - /> - - )} - > - - - - ({ - name: d.name, - description: d.description, - data: [d.data({ theme, themed })], - }))} - renderItem={({ item, index: sectionIndex }) => ( - - {item.map((demo: ReactElement, demoIndex: number) => ( - {demo} - ))} + const pendingTasksCount = useMemo(() => { + try { + return pendingTasks.length; + } catch (error) { + console.warn("Error getting pending tasks count:", error); + return 0; + } + }, [pendingTasks.length]); + + // Define colors for different tasks + const taskColors = [ + "#5f9ea0", // cadet blue + "#ffa500", // orange + "#f0e68c", // khaki + "#dda0dd", // plum + "#98fb98", // pale green + "#f0b27a", // sandy brown + "#85c1e9", // sky blue + ]; + + // Demo sections - you can customize these based on your needs + const demoSections = useMemo( + () => [ + { + name: "Task Statistics", + description: "Overview of your tasks", + data: [ + { + content: ( + + + {completedTasksCount} + Completed + + + {pendingTasksCount} + Pending + - )} - renderSectionFooter={() => } - ListHeaderComponent={ - - + ), + }, + ], + }, + { + name: "Quick Actions", + description: "Common task actions", + data: [ + { + content: ( + +