diff --git a/.nvmrc b/.nvmrc index bc78e9f2..b009dfb9 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.12.1 +lts/* diff --git a/apps/expo/jest.config.ts b/apps/expo/jest.config.ts new file mode 100644 index 00000000..c804c7ec --- /dev/null +++ b/apps/expo/jest.config.ts @@ -0,0 +1,9 @@ +import type { Config } from "jest"; + +export default { + verbose: true, + preset: "jest-expo", + transformIgnorePatterns: [ + "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)", + ], +} satisfies Config; diff --git a/apps/expo/package.json b/apps/expo/package.json index 06e5360e..f387aceb 100644 --- a/apps/expo/package.json +++ b/apps/expo/package.json @@ -13,6 +13,7 @@ "ios": "expo run:ios", "format": "prettier --check . --ignore-path ../../.gitignore", "lint": "eslint .", + "_test": "jest", "typecheck": "tsc --noEmit" }, "dependencies": { @@ -46,6 +47,7 @@ "expo-splash-screen": "~0.26.4", "expo-status-bar": "~1.11.1", "react": "18.2.0", + "react-datepicker": "^6.9.0", "react-dom": "18.2.0", "react-native": "~0.73.6", "react-native-css-interop": "~0.0.34", @@ -62,15 +64,22 @@ "devDependencies": { "@babel/core": "^7.24.0", "@babel/preset-env": "^7.24.0", + "@babel/preset-typescript": "^7.24.6", "@babel/runtime": "^7.24.0", + "@jest/globals": "^29.7.0", + "@testing-library/react-native": "^12.5.1", + "@types/react-datepicker": "^6.2.0", "@zotmeal/eslint-config": "workspace:^0.2.0", "@zotmeal/prettier-config": "workspace:^0.1.0", "@zotmeal/tailwind-config": "workspace:^0.1.0", "@zotmeal/tsconfig": "workspace:^0.1.0", - "@zotmeal/ui": "workspace:^", "eslint": "^8.57.0", + "jest": "^29.7.0", + "jest-expo": "^50.0.4", "prettier": "^3.2.5", + "react-test-renderer": "^18.2.0", "tailwindcss": "^3.4.3", + "ts-jest": "^29.1.4", "typescript": "^5.4.3" }, "eslintConfig": { diff --git a/apps/expo/src/__tests__/app.test.js b/apps/expo/src/__tests__/app.test.js new file mode 100644 index 00000000..c1f72a72 --- /dev/null +++ b/apps/expo/src/__tests__/app.test.js @@ -0,0 +1,10 @@ +import React from "react"; +import renderer from "react-test-renderer"; +import { Logo } from "~/components"; + +describe("", () => { + it("has 1 child", () => { + const tree = renderer.create().toJSON(); + expect(tree.children.length).toBe(1); + }); +}); diff --git a/apps/expo/src/app/_layout.tsx b/apps/expo/src/app/_layout.tsx index 741bfc3e..0c0c38b8 100644 --- a/apps/expo/src/app/_layout.tsx +++ b/apps/expo/src/app/_layout.tsx @@ -2,13 +2,10 @@ import { config } from "@tamagui/config/v3"; import "@tamagui/core/reset.css"; -import type { TokenCache } from "@clerk/clerk-expo/dist/cache"; import type { FontSource } from "expo-font"; -import { useColorScheme } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useFonts } from "expo-font"; import { Stack } from "expo-router"; -import * as SecureStore from "expo-secure-store"; import { StatusBar } from "expo-status-bar"; import { ClerkProvider } from "@clerk/clerk-expo"; import InterBold from "@tamagui/font-inter/otf/Inter-Bold.otf"; @@ -16,45 +13,25 @@ import Inter from "@tamagui/font-inter/otf/Inter-Medium.otf"; import { ToastProvider, ToastViewport } from "@tamagui/toast"; import { createTamagui, TamaguiProvider, Theme } from "tamagui"; -import Logo from "~/components/Logo"; +import { Logo } from "~/components"; import { HamburgerMenu } from "~/components/navigation/HamburgerMenu"; -import { TRPCProvider } from "~/utils"; +import { TRPCProvider, useZotmealColorScheme } from "~/utils"; +import { tokenCache } from "~/utils/tokenCache"; import { env } from "../utils/env"; -// Main layout of the app -// It wraps your pages with the providers they need - const tamaguiConfig = createTamagui(config); -const tokenCache: TokenCache = { - async getToken(key: string) { - try { - return SecureStore.getItemAsync(key); - } catch (err) { - return null; - } - }, - async saveToken(key: string, value: string) { - try { - await SecureStore.setItemAsync(key, value); - } catch (err) { - console.error(err); - } - }, -}; - export default function RootLayout() { const [loaded] = useFonts({ Inter: Inter as FontSource, InterBold: InterBold as FontSource, }); - const colorScheme = useColorScheme(); + const colorScheme = useZotmealColorScheme(); + const { bottom, left, right } = useSafeAreaInsets(); - if (!loaded) { - return null; - } + if (!loaded) return null; return ( diff --git a/apps/expo/src/app/events/event/[title].tsx b/apps/expo/src/app/events/event/[title].tsx index 2529e97f..6f7a59af 100644 --- a/apps/expo/src/app/events/event/[title].tsx +++ b/apps/expo/src/app/events/event/[title].tsx @@ -16,13 +16,14 @@ import { Separator, Square, Text, + View, XStack, YStack, } from "tamagui"; import type { Event } from "@zotmeal/db"; -import useZotmealStore from "~/utils/useZotmealStore"; +import { useZotmealStore } from "~/utils"; export default function Event() { const { title } = useGlobalSearchParams(); @@ -57,9 +58,6 @@ export default function Event() { justifyContent: "center", alignItems: "center", }} - contentInset={{ - bottom: 100, - }} > + ); diff --git a/apps/expo/src/app/events/index.tsx b/apps/expo/src/app/events/index.tsx index 7acd3c6b..143ad7ad 100644 --- a/apps/expo/src/app/events/index.tsx +++ b/apps/expo/src/app/events/index.tsx @@ -1,14 +1,15 @@ import { useEffect } from "react"; import { Link } from "expo-router"; +import { CalendarX2 } from "@tamagui/lucide-icons"; import { format } from "date-fns"; -import { H3, Image, Tabs, Text, YStack } from "tamagui"; +import { H3, Image, Spinner, Tabs, Text, View, YStack } from "tamagui"; import type { Event } from "@zotmeal/db"; import { getRestaurantNameById } from "@zotmeal/utils"; import { RestaurantTabs } from "~/components"; +import { useZotmealStore } from "~/utils"; import { api } from "~/utils/api"; -import useZotmealStore from "~/utils/useZotmealStore"; // Create a context for events, default value is a test event const _testData = { @@ -95,32 +96,37 @@ export default function Events() { setBrandywineEvents(brandywineEvents); }, [eventsQuery.data, setAnteateryEvents, setBrandywineEvents]); - if (eventsQuery?.isLoading) { - return Loading...; - } + // TODO: show a toast if there is an error + if (eventsQuery?.isError) console.error(eventsQuery.error); - if (eventsQuery?.isError) { - return Error: {eventsQuery.error.message}; - } - - if (!anteateryEvents || !brandywineEvents) { - return No events found; - } + const EventsContent = () => + eventsQuery.isLoading ? ( + + ) : brandywineEvents && anteateryEvents ? ( + <> + {[brandywineEvents, anteateryEvents].map((events, index) => ( + + + {events.map((event, index) => ( + + ))} + + + ))} + + ) : ( + + + Events not found + + ); return ( - {[brandywineEvents, anteateryEvents].map((events, index) => ( - - - {events.map((event, index) => ( - - ))} - - - ))} + ); } diff --git a/apps/expo/src/app/home/_components/date-picker.tsx b/apps/expo/src/app/home/_components/date-picker.tsx new file mode 100644 index 00000000..7f0bb311 --- /dev/null +++ b/apps/expo/src/app/home/_components/date-picker.tsx @@ -0,0 +1,52 @@ +import React, { useState } from "react"; +import { Platform } from "react-native"; +import DateTimePicker from "@react-native-community/datetimepicker"; +import { CalendarDays } from "@tamagui/lucide-icons"; +import { endOfWeek, startOfWeek } from "date-fns"; +import { Button } from "tamagui"; + +/** + * Native date picker for iOS and Android. + * + * Platform handling from an issue thread: + * + * @see https://github.com/react-native-datetimepicker/datetimepicker/issues/54 + */ +export const UniversalDatePicker = ({ + date, + setDate, +}: Readonly<{ date: Date; setDate: (date: Date) => void }>) => { + const [showDatePicker, setShowDatePicker] = useState(true); + + return ( + <> + {Platform.OS === "android" && ( + + )} + {showDatePicker && ( + { + // hide date picker on android + setShowDatePicker(Platform.OS === "ios"); + if (selectedDate) { + setDate(selectedDate); + } + }} + /> + )} + + ); +}; diff --git a/apps/expo/src/app/home/_components/date-picker.web.tsx b/apps/expo/src/app/home/_components/date-picker.web.tsx new file mode 100644 index 00000000..d8339749 --- /dev/null +++ b/apps/expo/src/app/home/_components/date-picker.web.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import { endOfWeek, startOfWeek } from "date-fns"; +import DatePicker from "react-datepicker"; + +import "react-datepicker/dist/react-datepicker.css"; + +import { CalendarDays } from "@tamagui/lucide-icons"; +import { Button, ButtonProps, TamaguiElement } from "tamagui"; + +interface UniversalDatePickerProps { + date: Date; + setDate: (date: Date) => void; +} + +interface CustomInputProps { + value: HTMLInputElement["value"]; + onClick: ButtonProps["onPress"]; +} + +/** + * Universal date picker for web. + */ +export const UniversalDatePicker = ({ + date, + setDate, +}: Readonly) => { + /** + * Courtesy of issue thread: + * @see https://github.com/Hacker0x01/react-datepicker/issues/2165#issuecomment-711032947 + */ + const CustomInput = ( + { value, onClick }: CustomInputProps, + ref: React.Ref, + ) => ( + + ); + + return ( + setDate(prev ?? new Date())} + /> + ); +}; diff --git a/apps/expo/src/app/home/_components/dish-card.tsx b/apps/expo/src/app/home/_components/dish-card.tsx index 5999ceac..eff3d95c 100644 --- a/apps/expo/src/app/home/_components/dish-card.tsx +++ b/apps/expo/src/app/home/_components/dish-card.tsx @@ -1,10 +1,11 @@ import { Link } from "expo-router"; import { StarFull } from "@tamagui/lucide-icons"; -import { Image, ListItem, Text, XStack, YGroup, YStack } from "tamagui"; - +import { Image, ListItem, Text, View, XStack, YGroup, YStack } from "tamagui"; import type { MenuWithRelations } from "@zotmeal/db"; -import { PinButton } from '~/components'; + +import { PinButton } from "~/components"; +import { testDishImages } from "../../../components/menu/testDishImages"; type Station = MenuWithRelations["stations"][0]; type Dish = MenuWithRelations["stations"][0]["dishes"][0]; @@ -16,52 +17,47 @@ export const DishCard = ({ dish: Dish; stationId: Station["id"]; }>) => ( - - + - - - - + /> + + + + + - + {dish.name} - - {dish.nutritionInfo.calories} cal + + {dish.nutritionInfo.calories + ? `${dish.nutritionInfo.calories} cal` + : ""} - + - - 5.0 - {" "} - (10,000 reviews) + 5.0 + (10,000 reviews) - - - - + + + + ); diff --git a/apps/expo/src/app/home/_components/event-toast.tsx b/apps/expo/src/app/home/_components/event-toast.tsx index d0be58e8..f4aa2dbe 100644 --- a/apps/expo/src/app/home/_components/event-toast.tsx +++ b/apps/expo/src/app/home/_components/event-toast.tsx @@ -4,8 +4,6 @@ import { Toast, useToastState } from "@tamagui/toast"; import { Button } from "tamagui"; import { LinearGradient } from "tamagui/linear-gradient"; - - export function EventToast() { const currentToast = useToastState(); if (!currentToast || currentToast.isHandledNatively) return null; @@ -37,7 +35,6 @@ export function EventToast() { - )} - {showDatePicker && ( - { - // hide date picker on android - setShowDatePicker(Platform.OS === "ios"); - if (selectedDate) { - setDate(selectedDate); - } - }} - /> - )} - - - - - - - - - - - {[brandywineMenu, anteateryMenu].map((menu) => ( - <> - {menu && ( - - - - )} - - ))} - - - ); -} diff --git a/apps/expo/src/app/home/item/[id].tsx b/apps/expo/src/app/home/item/[id].tsx deleted file mode 100644 index e448ffa2..00000000 --- a/apps/expo/src/app/home/item/[id].tsx +++ /dev/null @@ -1,193 +0,0 @@ -import { Redirect, Stack, useGlobalSearchParams } from "expo-router"; -import { ChevronRight } from "@tamagui/lucide-icons"; -import { - H3, - H4, - Image, - Paragraph, - ScrollView, - Separator, - Text, - useTheme, - View, - XStack, - YStack, -} from "tamagui"; - -import type { NutritionInfo } from "@zotmeal/db"; - -import { PinButton } from "~/components"; -import useZotmealStore from "~/utils/useZotmealStore"; -import RateItem from "./RateItem"; - -export default function MenuItem() { - const theme = useTheme(); - const { id, stationId } = useGlobalSearchParams(); - - if (!id || typeof id !== "string") throw new Error("id is not a string"); - if (!stationId || typeof stationId !== "string") - throw new Error("stationId is not a string"); - - const { selectedRestaurant, anteateryMenu, brandywineMenu } = - useZotmealStore(); - - const menu = - selectedRestaurant === "anteatery" ? anteateryMenu : brandywineMenu; - - // TODO: Log error if menu is not found - if (!menu) return ; - - const station = menu.stations.find((station) => station.id === stationId); - - // TODO: Log error if station is not found - if (!station) return ; - - const dish = station.dishes.find((dish) => dish.id === id); - - // TODO: Log error if dish is not found - if (!dish) return ; - - // Unused fields: - // caloriesFromFat - - const NutritionFacts = ({ - nutritionInfo, - }: { - nutritionInfo: NutritionInfo; - }) => ( - -

Nutrition Facts

- - - Serving Size - - {nutritionInfo.servingSize} - {nutritionInfo.servingUnit} - - - - Amount per serving - -

Calories

-

{nutritionInfo.calories}

-
- - {/* TODO: Add % Daily Value */} - {/* % Daily Value* */} - - Total Fat {nutritionInfo.totalFatG}g - - - Saturated Fat {nutritionInfo.saturatedFatG}g - - Trans Fat {nutritionInfo.transFatG}g - - - Cholesterol{" "} - {nutritionInfo.cholesterolMg} - - - - Sodium {nutritionInfo.sodiumMg}mg - - - - Total Carbohydrates{" "} - {nutritionInfo.totalCarbsG}g - - - Dietary Fiber {nutritionInfo.dietaryFiberG}g - - Sugars {nutritionInfo.sugarsMg}mg - - - Sodium {nutritionInfo.proteinG}g - - - - Vitamin A {nutritionInfo.vitaminAIU} IU - - - - - Vitamin C {nutritionInfo.vitaminCIU} IU - - - - - Iron {nutritionInfo.ironMg}mg - - - - - Calcium {nutritionInfo.calciumMg}mg - - - - - The % Daily Value tells you how much a nutrient in a serving of food - contributes to a daily diet. 2,000 calories a day is used for general - nutrition advice. - -
- ); - - return ( - <> - - - - - - - - {selectedRestaurant.charAt(0).toUpperCase() + - selectedRestaurant.slice(1)} - - - - {station.name} - - -

{dish.name ?? "No name found"}

- {dish.description ?? "No description found."} - - - - - -
-
- - ); -} diff --git a/apps/expo/src/app/index.tsx b/apps/expo/src/app/index.tsx index 72dae5e8..101784a6 100644 --- a/apps/expo/src/app/index.tsx +++ b/apps/expo/src/app/index.tsx @@ -1,5 +1,3 @@ -import { Home } from "./home"; +import { Home } from "../components"; -export default function Index() { - return ; -} +export default () => ; diff --git a/apps/expo/src/app/home/item/RateItem.tsx b/apps/expo/src/app/item/RateItem.tsx similarity index 61% rename from apps/expo/src/app/home/item/RateItem.tsx rename to apps/expo/src/app/item/RateItem.tsx index 9a395b6a..8e5ec707 100644 --- a/apps/expo/src/app/home/item/RateItem.tsx +++ b/apps/expo/src/app/item/RateItem.tsx @@ -1,24 +1,30 @@ import { useState } from "react"; -import { StarFull } from "@tamagui/lucide-icons"; -import { Adapt, Button, H4, Popover, XStack, YStack } from "tamagui"; +import { Star, StarFull } from "@tamagui/lucide-icons"; +import { Adapt, Button, H4, Popover, Text, XStack, YStack } from "tamagui"; import type { DishWithRelations } from "@zotmeal/db"; export default function RateItem({ item, }: Readonly<{ item: DishWithRelations }>) { - const [rating, setRating] = useState(5); + // const [rating, setRating] = useState(item.rating ?? 0); + const [rating, setRating] = useState(0); + const [isAuthenticated] = useState(true); // TODO: replace with actual auth check + const [userRated, setUserRated] = useState(false); // TODO: replace with actual user rating check return ( - - + + @@ -48,6 +54,7 @@ export default function RateItem({ }, ]} > +

Rate {item.name}

@@ -57,23 +64,34 @@ export default function RateItem({ key={i} onPress={() => setRating(i + 1)} icon={ - + i < rating ? ( + + ) : ( + + ) } /> ))}
diff --git a/apps/expo/src/app/item/[id].tsx b/apps/expo/src/app/item/[id].tsx new file mode 100644 index 00000000..8ebd46c9 --- /dev/null +++ b/apps/expo/src/app/item/[id].tsx @@ -0,0 +1,250 @@ +import { Platform } from "react-native"; +import { Link, Redirect, Stack, useGlobalSearchParams } from "expo-router"; +import { ChevronRight } from "@tamagui/lucide-icons"; +import { + H3, + H4, + Image, + Paragraph, + ScrollView, + Separator, + Text, + useTheme, + View, + XStack, + YStack, +} from "tamagui"; + +import type { NutritionInfo } from "@zotmeal/db"; + +import { PinButton } from "~/components"; +import { useZotmealStore } from "~/utils"; +import { testDishImages } from "../../components/menu/testDishImages"; +import RateItem from "./RateItem"; + +export default function MenuItem() { + const theme = useTheme(); + const { id, stationId } = useGlobalSearchParams(); + + if (!id || typeof id !== "string") throw new Error("id is not a string"); + if (!stationId || typeof stationId !== "string") + throw new Error("stationId is not a string"); + + const { selectedRestaurant, anteateryMenu, brandywineMenu } = + useZotmealStore(); + + const menu = + selectedRestaurant === "anteatery" ? anteateryMenu : brandywineMenu; + + // TODO: Log error if menu is not found + if (!menu) return ; + + const station = menu.stations.find((station) => station.id === stationId); + + // TODO: Log error if station is not found + if (!station) return ; + + const dish = station.dishes.find((dish) => dish.id === id); + + // TODO: Log error if dish is not found + if (!dish) return ; + + // Unused fields: + // caloriesFromFat + + const units = { + calories: "cal", + totalFatG: "g", + transFatG: "g", + saturatedFatG: "g", + cholesterolMg: "mg", + sodiumMg: "mg", + totalCarbsG: "g", + dietaryFiberG: "g", + sugarsMg: "mg", + proteinG: "g", + vitaminAIU: "IU", + vitaminCIU: "IU", + calciumMg: "mg", + ironMg: "mg", + } as const satisfies Partial>; + + const NutritionFacts = ({ + nutritionInfo, + }: { + nutritionInfo: NutritionInfo; + }) => ( + +

Nutrition Facts

+ + + Serving Size + + {nutritionInfo.servingSize ?? "?"} + {nutritionInfo.servingUnit} + + + + Amount per serving + +

Calories

+

{nutritionInfo.calories ?? "?"}

+
+ + {/* TODO: Add % Daily Value */} + {/* % Daily Value* */} + + Total Fat {nutritionInfo.totalFatG ?? "?"} + {units.totalFatG} + + + + Saturated Fat{" "} + {nutritionInfo.saturatedFatG ? nutritionInfo.saturatedFatG : "?"} + {units.saturatedFatG} + + + + Trans Fat {nutritionInfo.transFatG ?? "?"} + {units.transFatG} + + + + Cholesterol{" "} + {nutritionInfo.cholesterolMg ?? "?"} + {units.cholesterolMg} + + + + Sodium {nutritionInfo.sodiumMg ?? "?"} + {units.sodiumMg} + + + + Total Carbohydrates{" "} + {nutritionInfo.totalCarbsG ?? "?"} + {units.totalCarbsG} + + + + Dietary Fiber {nutritionInfo.dietaryFiberG ?? "?"} + {units.dietaryFiberG} + + + + Sugars {nutritionInfo.sugarsMg ?? "?"} + {units.sugarsMg} + + + + Sodium {nutritionInfo.proteinG ?? "?"} + {units.proteinG} + + + + + Vitamin A {nutritionInfo.vitaminAIU ?? "?"} {units.vitaminAIU} + + + + + + + Vitamin C {nutritionInfo.vitaminCIU ?? "?"} {units.vitaminCIU} + + + + + + + Iron {nutritionInfo.ironMg ?? "?"} + {units.ironMg} + + + + + + + Calcium {nutritionInfo.calciumMg ?? "?"} + {units.calciumMg} + + + + + + * Some nutrition facts may not be disclosed by the dining halls. Contact + the dining hall if you need more information. + {/* The % Daily Value tells you how much a nutrient in a serving of food + contributes to a daily diet. 2,000 calories a day is used for general + nutrition advice. */} + +
+ ); + + return ( + <> + + + + + + + + + {selectedRestaurant.charAt(0).toUpperCase() + + selectedRestaurant.slice(1)} + + + + + {station.name} + + +

{dish.name ?? "No name found"}

+ {dish.description ?? "No description found."} + + + + + +
+ +
+ + ); +} diff --git a/apps/expo/src/app/settings/index.tsx b/apps/expo/src/app/settings/index.tsx index 48e5e2f0..400bf7cf 100644 --- a/apps/expo/src/app/settings/index.tsx +++ b/apps/expo/src/app/settings/index.tsx @@ -1,5 +1,31 @@ -import { Text } from "tamagui"; +import { H3, RadioGroup, Separator, YStack } from "tamagui"; + +import { RadioGroupItemWithLabel, SwitchWithLabel } from "~/components"; +import { useSettingsStore } from "~/utils"; export default function Settings() { - return Settings; + const { colorSchemePreference, setColorSchemePreference } = + useSettingsStore(); + + return ( + +

Settings

+ + + setColorSchemePreference(value as "light" | "dark" | "system") + } + > + + + + + + + +
+ ); } diff --git a/apps/expo/src/components/Logo.tsx b/apps/expo/src/components/Logo.tsx deleted file mode 100644 index 11d1fefc..00000000 --- a/apps/expo/src/components/Logo.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { H3, Image, View } from "tamagui"; - -export default function Logo() { - return ( - - -

- ZotMeal -

-
- ); -} diff --git a/apps/expo/src/components/index.ts b/apps/expo/src/components/index.ts index 41f0eec4..a59088ad 100644 --- a/apps/expo/src/components/index.ts +++ b/apps/expo/src/components/index.ts @@ -1,4 +1,3 @@ -export * from "./navigation/HamburgerMenu"; -export * from "./Logo"; -export * from "./PinButton"; -export * from "./navigation/RestaurantTabs"; +export * from "./navigation"; +export * from "./ui"; +export * from "./menu"; diff --git a/apps/expo/src/components/menu/Home.tsx b/apps/expo/src/components/menu/Home.tsx new file mode 100644 index 00000000..2b81f473 --- /dev/null +++ b/apps/expo/src/components/menu/Home.tsx @@ -0,0 +1,228 @@ +import React, { useEffect, useState } from "react"; +import { Platform } from "react-native"; +import { AlertTriangle, RefreshCw } from "@tamagui/lucide-icons"; +import { useToastController } from "@tamagui/toast"; +import { + Button, + ScrollView, + Spinner, + Tabs, + Text, + useDebounce, + useTheme, + View, + XStack, +} from "tamagui"; + +import type { PeriodName } from "@zotmeal/utils"; +import { getCurrentPeriodName, getDayPeriodsByDate } from "@zotmeal/utils"; + +import { RestaurantTabs } from "~/components"; +import { useZotmealStore } from "~/utils"; +import { api } from "~/utils/api"; +import { UniversalDatePicker } from "../../app/home/_components/date-picker"; +import { EventToast } from "../../app/home/_components/event-toast"; +import { PeriodPicker } from "../../app/home/_components/period-picker"; +import { StationTabs } from "../../app/home/_components/station-tabs"; + +export function Home() { + const { anteateryMenu, brandywineMenu, setAnteateryMenu, setBrandywineMenu } = + useZotmealStore(); + + const toast = useToastController(); + + const [date, setDate] = useState(new Date()); + + const currentPeriod = getCurrentPeriodName(); + + const [period, setPeriod] = useState( + currentPeriod === "closed" ? "breakfast" : currentPeriod, + ); + + const theme = useTheme(); + + const queryOptions = { + retry: false, + refetchOnWindowFocus: false, + } satisfies Parameters[1]; + + const anteateryQuery = api.menu.get.useQuery( + { + date: date.toLocaleDateString("en-US"), + period, + restaurant: "anteatery", + }, + queryOptions, + ); + + const brandywineQuery = api.menu.get.useQuery( + { + date: date.toLocaleDateString("en-US"), + period, + restaurant: "brandywine", + }, + queryOptions, + ); + + // ! Not sure if this is actually working but we do want debouncing for the refresh button + const refetchMenusWithDebounce = useDebounce( + () => { + anteateryQuery.refetch(); + brandywineQuery.refetch(); + }, + 1000, + { leading: true }, + ); + + useEffect(() => { + if (anteateryQuery.isSuccess) setAnteateryMenu(anteateryQuery.data); + if (brandywineQuery.isSuccess) setBrandywineMenu(brandywineQuery.data); + + if (anteateryQuery.isSuccess && brandywineQuery.isSuccess) { + toast.show("There are 5 upcoming events.", { + // message: 'See upcoming events', + duration: 10_000_000, + burntOptions: { + shouldDismissByDrag: true, + from: "bottom", + }, + }); + } + }, [anteateryQuery.data, brandywineQuery.data, toast]); + + // TODO: show a toast if there is an error + if ( + (anteateryMenu && anteateryQuery.isError) || + (brandywineMenu && brandywineQuery.isError) + ) { + if (anteateryQuery.error) + console.error("anteatery query error", anteateryQuery.error); + if (brandywineQuery.error) + console.error("brandywine query error", brandywineQuery.error); + + setAnteateryMenu(null); + setBrandywineMenu(null); + } + + // TODO: make it not possible to click into the menu if it's loading + const MenuContent = () => ( + <> + + {brandywineQuery.isLoading ? ( + + ) : null} + {brandywineMenu ? ( + + ) : brandywineQuery.isPending ? null : ( + + + Menu not found + + )} + + + {anteateryQuery.isLoading ? ( + + ) : null} + {anteateryMenu ? ( + + ) : anteateryQuery.isPending ? null : ( + + + Menu not found + + )} + + + ); + + return ( + + + + + + { + setPeriod(period); + refetchMenusWithDebounce(); + }} + color={theme.color?.val as string} + /> + { + setDate(date); + refetchMenusWithDebounce(); + }} + /> +