From 430b06b89ba83227202af8fdcf788ba0dbcd9411 Mon Sep 17 00:00:00 2001 From: Jakob Lee Date: Mon, 5 Oct 2026 11:34:00 -0700 Subject: [PATCH 1/3] Add QuickCop homepage UI --- apps/mobile/features/home/HomeScreen.tsx | 240 +++++++++++++++++++++-- 1 file changed, 228 insertions(+), 12 deletions(-) diff --git a/apps/mobile/features/home/HomeScreen.tsx b/apps/mobile/features/home/HomeScreen.tsx index c2fda01..d95cbd9 100644 --- a/apps/mobile/features/home/HomeScreen.tsx +++ b/apps/mobile/features/home/HomeScreen.tsx @@ -1,6 +1,11 @@ -import { Pressable, SafeAreaView, Text, View } from "react-native"; - -import { authStyles } from "../auth/styles"; +import { + Pressable, + SafeAreaView, + ScrollView, + StyleSheet, + Text, + View, +} from "react-native"; type HomeScreenProps = { onLogout: () => void; @@ -8,16 +13,227 @@ type HomeScreenProps = { export function HomeScreen({ onLogout }: HomeScreenProps) { return ( - - - - Log out + + + + {/* Header */} + + + ☰ + + + QuickCop + + + ⌕ + + + + Home + + {/* Watched Items */} + + Watched Items + + + + You are not watching any items yet. + + + + + {/* Recommended */} + + Recommended + + + + + + + + + {/* Popular */} + + Popular + + + + + + + + + {/* Logout */} + + Log out - + + ); } + +type ProductCardProps = { + name: string; + store: string; + price: string; +}; + +function ProductCard({ name, store, price }: ProductCardProps) { + return ( + + + IMG + + + {name} + {store} + {price} + + ); +} + +const styles = StyleSheet.create({ + screen: { + flex: 1, + backgroundColor: "#000", + }, + + content: { + padding: 20, + }, + + header: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + marginBottom: 30, + }, + + logo: { + color: "#fff", + fontSize: 24, + fontWeight: "700", + }, + + iconButton: { + width: 44, + height: 44, + alignItems: "center", + justifyContent: "center", + }, + + iconText: { + color: "#fff", + fontSize: 26, + }, + + title: { + color: "#fff", + fontSize: 30, + fontWeight: "700", + marginBottom: 28, + }, + + section: { + marginBottom: 32, + }, + + sectionTitle: { + color: "#fff", + fontSize: 21, + fontWeight: "600", + marginBottom: 14, + }, + + emptyCard: { + backgroundColor: "#161616", + borderWidth: 1, + borderColor: "#2b2b2b", + borderRadius: 12, + padding: 22, + }, + + emptyText: { + color: "#aaa", + fontSize: 15, + }, + + productRow: { + flexDirection: "row", + gap: 14, + }, + + productCard: { + flex: 1, + backgroundColor: "#161616", + borderWidth: 1, + borderColor: "#2b2b2b", + borderRadius: 12, + padding: 12, + }, + + productImage: { + height: 110, + backgroundColor: "#252525", + borderRadius: 8, + alignItems: "center", + justifyContent: "center", + marginBottom: 12, + }, + + imageText: { + color: "#777", + fontSize: 14, + }, + + productName: { + color: "#fff", + fontSize: 16, + fontWeight: "600", + marginBottom: 4, + }, + + productStore: { + color: "#888", + fontSize: 13, + marginBottom: 6, + }, + + productPrice: { + color: "#fff", + fontSize: 17, + fontWeight: "700", + }, + + logoutButton: { + borderWidth: 1, + borderColor: "#444", + borderRadius: 8, + paddingVertical: 12, + alignItems: "center", + marginBottom: 30, + }, + + logoutText: { + color: "#fff", + fontSize: 15, + }, +}); \ No newline at end of file From 6721fd94360110b5a9bc63449c4eaa7fc6eb1075 Mon Sep 17 00:00:00 2001 From: Jakob Lee Date: Mon, 5 Oct 2026 12:08:46 -0700 Subject: [PATCH 2/3] Add price alert notifications UI --- .../__tests__/notifications-screen.test.tsx | 52 +++++ apps/mobile/app/home.tsx | 7 +- apps/mobile/app/notifications.tsx | 5 + apps/mobile/features/home/HomeScreen.tsx | 149 ++++++++++-- .../notifications/NotificationsScreen.tsx | 213 ++++++++++++++++++ 5 files changed, 400 insertions(+), 26 deletions(-) create mode 100644 apps/mobile/__tests__/notifications-screen.test.tsx create mode 100644 apps/mobile/app/notifications.tsx create mode 100644 apps/mobile/features/notifications/NotificationsScreen.tsx diff --git a/apps/mobile/__tests__/notifications-screen.test.tsx b/apps/mobile/__tests__/notifications-screen.test.tsx new file mode 100644 index 0000000..9266fe1 --- /dev/null +++ b/apps/mobile/__tests__/notifications-screen.test.tsx @@ -0,0 +1,52 @@ +import { fireEvent, render } from "@testing-library/react-native"; + +import { + NotificationsScreen, + type PriceAlert, +} from "../features/notifications/NotificationsScreen"; + +describe("NotificationsScreen", () => { + it("shows example price-drop alerts and lets the user toggle an alert locally", async () => { + const screen = await render(); + + expect(screen.getByText("Nintendo Switch 2")).toBeTruthy(); + expect(screen.getByText("Price dropped from $499.99 to $449.99")).toBeTruthy(); + + const alertToggle = screen.getByRole("switch", { + name: "Nintendo Switch 2 price alert", + }); + expect(alertToggle.props.value).toBe(true); + + await fireEvent(alertToggle, "valueChange", false); + + expect( + screen.getByRole("switch", { name: "Nintendo Switch 2 price alert" }).props + .value, + ).toBe(false); + }); + + it("shows an empty state when no price alerts are available", async () => { + const alerts: PriceAlert[] = []; + const screen = await render(); + + expect(screen.getByText("No price alerts yet")).toBeTruthy(); + expect( + screen.getByText("Watch an item and set a target price to see alerts here."), + ).toBeTruthy(); + }); + + it("lets the user toggle notification settings locally", async () => { + const screen = await render(); + const priceDropToggle = screen.getByRole("switch", { + name: "Price drop notifications", + }); + + expect(priceDropToggle.props.value).toBe(true); + await fireEvent(priceDropToggle, "valueChange", false); + + expect( + screen.getByRole("switch", { name: "Price drop notifications" }).props + .value, + ).toBe(false); + }); +}); diff --git a/apps/mobile/app/home.tsx b/apps/mobile/app/home.tsx index 338d82b..0f4a7d8 100644 --- a/apps/mobile/app/home.tsx +++ b/apps/mobile/app/home.tsx @@ -5,5 +5,10 @@ import { HomeScreen } from "../features/home/HomeScreen"; export default function HomeRoute() { const router = useRouter(); - return router.replace("/")} />; + return ( + router.replace("/")} + onOpenNotifications={() => router.push("/notifications")} + /> + ); } diff --git a/apps/mobile/app/notifications.tsx b/apps/mobile/app/notifications.tsx new file mode 100644 index 0000000..6ae35ba --- /dev/null +++ b/apps/mobile/app/notifications.tsx @@ -0,0 +1,5 @@ +import { NotificationsScreen } from "../features/notifications/NotificationsScreen"; + +export default function NotificationsRoute() { + return ; +} diff --git a/apps/mobile/features/home/HomeScreen.tsx b/apps/mobile/features/home/HomeScreen.tsx index d95cbd9..e73cbfb 100644 --- a/apps/mobile/features/home/HomeScreen.tsx +++ b/apps/mobile/features/home/HomeScreen.tsx @@ -1,3 +1,5 @@ +import { useState } from "react"; + import { Pressable, SafeAreaView, @@ -9,16 +11,28 @@ import { type HomeScreenProps = { onLogout: () => void; + onOpenNotifications: () => void; }; -export function HomeScreen({ onLogout }: HomeScreenProps) { +export function HomeScreen({ + onLogout, + onOpenNotifications, +}: HomeScreenProps) { + const [menuOpen, setMenuOpen] = useState(false); + const [categoriesOpen, setCategoriesOpen] = useState(false); + const [storesOpen, setStoresOpen] = useState(false); + return ( - {/* Header */} - + setMenuOpen(!menuOpen)} + > ☰ @@ -29,6 +43,78 @@ export function HomeScreen({ onLogout }: HomeScreenProps) { + {/* Hamburger Menu */} + {menuOpen && ( + + + Home + + + + Watched Items + + + + Price alerts + + + + Recommended + + + + Popular + + + {/* Categories */} + setCategoriesOpen(!categoriesOpen)} + > + + Categories {categoriesOpen ? "▲" : "▼"} + + + + {categoriesOpen && ( + + Tickets + Cards + Lego + See All + + )} + + {/* Stores */} + setStoresOpen(!storesOpen)}> + + Stores {storesOpen ? "▲" : "▼"} + + + + {storesOpen && ( + + Amazon + Target + Walmart + See All + + )} + + {/* Settings */} + + Settings + + + + Log out + + + )} + + {/* Home */} Home {/* Watched Items */} @@ -79,12 +165,6 @@ export function HomeScreen({ onLogout }: HomeScreenProps) { /> - - {/* Logout */} - - Log out - - ); @@ -124,7 +204,7 @@ const styles = StyleSheet.create({ flexDirection: "row", alignItems: "center", justifyContent: "space-between", - marginBottom: 30, + marginBottom: 20, }, logo: { @@ -145,6 +225,39 @@ const styles = StyleSheet.create({ fontSize: 26, }, + menu: { + backgroundColor: "#161616", + borderWidth: 1, + borderColor: "#333", + borderRadius: 12, + padding: 18, + marginBottom: 25, + }, + + menuItem: { + color: "#fff", + fontSize: 18, + paddingVertical: 10, + }, + + subMenu: { + marginLeft: 20, + marginBottom: 6, + }, + + subMenuItem: { + color: "#aaa", + fontSize: 16, + paddingVertical: 7, + }, + + logoutMenuItem: { + color: "#ff7777", + fontSize: 18, + paddingVertical: 10, + marginTop: 8, + }, + title: { color: "#fff", fontSize: 30, @@ -222,18 +335,4 @@ const styles = StyleSheet.create({ fontSize: 17, fontWeight: "700", }, - - logoutButton: { - borderWidth: 1, - borderColor: "#444", - borderRadius: 8, - paddingVertical: 12, - alignItems: "center", - marginBottom: 30, - }, - - logoutText: { - color: "#fff", - fontSize: 15, - }, -}); \ No newline at end of file +}); diff --git a/apps/mobile/features/notifications/NotificationsScreen.tsx b/apps/mobile/features/notifications/NotificationsScreen.tsx new file mode 100644 index 0000000..1d8b0ce --- /dev/null +++ b/apps/mobile/features/notifications/NotificationsScreen.tsx @@ -0,0 +1,213 @@ +import { useState } from "react"; + +import { + ScrollView, + StyleSheet, + Switch, + Text, + View, +} from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; + +export type PriceAlert = { + id: string; + name: string; + retailer: string; + previousPrice: string; + currentPrice: string; + targetPrice: string; + enabled: boolean; +}; + +const exampleAlerts: PriceAlert[] = [ + { + id: "switch-2", + name: "Nintendo Switch 2", + retailer: "Target", + previousPrice: "$499.99", + currentPrice: "$449.99", + targetPrice: "$450.00", + enabled: true, + }, + { + id: "headphones", + name: "Sony WH-1000XM6", + retailer: "Amazon", + previousPrice: "$449.99", + currentPrice: "$398.00", + targetPrice: "$400.00", + enabled: true, + }, +]; + +type NotificationsScreenProps = { + alerts?: PriceAlert[]; +}; + +export function NotificationsScreen({ + alerts = exampleAlerts, +}: NotificationsScreenProps) { + const [priceAlerts, setPriceAlerts] = useState(alerts); + const [priceDropNotificationsEnabled, setPriceDropNotificationsEnabled] = + useState(true); + const [backInStockNotificationsEnabled, setBackInStockNotificationsEnabled] = + useState(true); + + function setAlertEnabled(id: string, enabled: boolean) { + setPriceAlerts((currentAlerts) => + currentAlerts.map((alert) => + alert.id === id ? { ...alert, enabled } : alert, + ), + ); + } + + return ( + + + + Price alerts + + + Track price drops for the items you are watching. + + + + + Recent price drops + + + {priceAlerts.length === 0 ? ( + + No price alerts yet + + Watch an item and set a target price to see alerts here. + + + ) : ( + priceAlerts.map((alert) => ( + + + {alert.name} + {alert.retailer} + + Price dropped from {alert.previousPrice} to {alert.currentPrice} + + + Your target: {alert.targetPrice} + + + setAlertEnabled(alert.id, enabled)} + thumbColor={alert.enabled ? "#FFFFFF" : "#B8B8B8"} + trackColor={{ false: "#4A4A4A", true: "#2E8B57" }} + value={alert.enabled} + /> + + )) + )} + + + + + Notification settings + + + + + + + + These settings are shown for preview only and are not saved yet. + + + + + ); +} + +type SettingRowProps = { + description: string; + label: string; + onValueChange: (value: boolean) => void; + value: boolean; +}; + +function SettingRow({ + description, + label, + onValueChange, + value, +}: SettingRowProps) { + return ( + + + {label} + {description} + + + + ); +} + +const styles = StyleSheet.create({ + screen: { flex: 1, backgroundColor: "#000" }, + content: { padding: 20, paddingBottom: 40 }, + title: { color: "#fff", fontSize: 30, fontWeight: "700", marginBottom: 8 }, + subtitle: { color: "#aaa", fontSize: 16, lineHeight: 22, marginBottom: 32 }, + section: { marginBottom: 32 }, + sectionTitle: { color: "#fff", fontSize: 21, fontWeight: "600", marginBottom: 14 }, + alertCard: { + alignItems: "center", + backgroundColor: "#161616", + borderColor: "#2b2b2b", + borderRadius: 12, + borderWidth: 1, + flexDirection: "row", + marginBottom: 12, + padding: 16, + }, + alertContent: { flex: 1, paddingRight: 12 }, + productName: { color: "#fff", fontSize: 17, fontWeight: "600", marginBottom: 4 }, + retailer: { color: "#aaa", fontSize: 14, marginBottom: 10 }, + priceChange: { color: "#fff", fontSize: 15, lineHeight: 21, marginBottom: 4 }, + targetPrice: { color: "#79DCA0", fontSize: 14, fontWeight: "600" }, + emptyCard: { + backgroundColor: "#161616", + borderColor: "#2b2b2b", + borderRadius: 12, + borderWidth: 1, + padding: 22, + }, + emptyTitle: { color: "#fff", fontSize: 17, fontWeight: "600", marginBottom: 6 }, + emptyText: { color: "#aaa", fontSize: 15, lineHeight: 21 }, + settingsCard: { + backgroundColor: "#161616", + borderColor: "#2b2b2b", + borderRadius: 12, + borderWidth: 1, + paddingHorizontal: 16, + }, + settingRow: { alignItems: "center", flexDirection: "row", paddingVertical: 16 }, + settingContent: { flex: 1, paddingRight: 12 }, + settingLabel: { color: "#fff", fontSize: 16, fontWeight: "600", marginBottom: 4 }, + settingDescription: { color: "#aaa", fontSize: 14, lineHeight: 20 }, + divider: { backgroundColor: "#2b2b2b", height: 1 }, + helperText: { color: "#888", fontSize: 13, lineHeight: 19, marginTop: 10 }, +}); From 7bd8becbb5e48ed093cced345bbd9fac7e2517b4 Mon Sep 17 00:00:00 2001 From: Jakob Lee Date: Mon, 5 Oct 2026 13:25:17 -0700 Subject: [PATCH 3/3] Fix formatting for quality checks --- .../__tests__/notifications-screen.test.tsx | 12 ++-- apps/mobile/features/home/HomeScreen.tsx | 58 +++++++++---------- .../notifications/NotificationsScreen.tsx | 49 +++++++++++----- apps/mobile/tsconfig.json | 9 +-- 4 files changed, 71 insertions(+), 57 deletions(-) diff --git a/apps/mobile/__tests__/notifications-screen.test.tsx b/apps/mobile/__tests__/notifications-screen.test.tsx index 9266fe1..9493865 100644 --- a/apps/mobile/__tests__/notifications-screen.test.tsx +++ b/apps/mobile/__tests__/notifications-screen.test.tsx @@ -10,7 +10,9 @@ describe("NotificationsScreen", () => { const screen = await render(); expect(screen.getByText("Nintendo Switch 2")).toBeTruthy(); - expect(screen.getByText("Price dropped from $499.99 to $449.99")).toBeTruthy(); + expect( + screen.getByText("Price dropped from $499.99 to $449.99"), + ).toBeTruthy(); const alertToggle = screen.getByRole("switch", { name: "Nintendo Switch 2 price alert", @@ -20,8 +22,8 @@ describe("NotificationsScreen", () => { await fireEvent(alertToggle, "valueChange", false); expect( - screen.getByRole("switch", { name: "Nintendo Switch 2 price alert" }).props - .value, + screen.getByRole("switch", { name: "Nintendo Switch 2 price alert" }) + .props.value, ).toBe(false); }); @@ -31,7 +33,9 @@ describe("NotificationsScreen", () => { expect(screen.getByText("No price alerts yet")).toBeTruthy(); expect( - screen.getByText("Watch an item and set a target price to see alerts here."), + screen.getByText( + "Watch an item and set a target price to see alerts here.", + ), ).toBeTruthy(); }); diff --git a/apps/mobile/features/home/HomeScreen.tsx b/apps/mobile/features/home/HomeScreen.tsx index e73cbfb..dca7774 100644 --- a/apps/mobile/features/home/HomeScreen.tsx +++ b/apps/mobile/features/home/HomeScreen.tsx @@ -12,11 +12,13 @@ import { type HomeScreenProps = { onLogout: () => void; onOpenNotifications: () => void; + onOpenWatchedItems?: () => void; }; export function HomeScreen({ onLogout, onOpenNotifications, + onOpenWatchedItems = () => undefined, }: HomeScreenProps) { const [menuOpen, setMenuOpen] = useState(false); const [categoriesOpen, setCategoriesOpen] = useState(false); @@ -50,14 +52,11 @@ export function HomeScreen({ Home - + Watched Items - + Price alerts @@ -70,9 +69,7 @@ export function HomeScreen({ {/* Categories */} - setCategoriesOpen(!categoriesOpen)} - > + setCategoriesOpen(!categoriesOpen)}> Categories {categoriesOpen ? "▲" : "▼"} @@ -121,11 +118,17 @@ export function HomeScreen({ Watched Items - + You are not watching any items yet. - + Add or manage watched items + {/* Recommended */} @@ -133,17 +136,9 @@ export function HomeScreen({ Recommended - - - + + + @@ -152,17 +147,9 @@ export function HomeScreen({ Popular - - - + + + @@ -289,6 +276,13 @@ const styles = StyleSheet.create({ fontSize: 15, }, + emptyAction: { + color: "#79DCA0", + fontSize: 14, + fontWeight: "600", + marginTop: 10, + }, + productRow: { flexDirection: "row", gap: 14, diff --git a/apps/mobile/features/notifications/NotificationsScreen.tsx b/apps/mobile/features/notifications/NotificationsScreen.tsx index 1d8b0ce..68fdc00 100644 --- a/apps/mobile/features/notifications/NotificationsScreen.tsx +++ b/apps/mobile/features/notifications/NotificationsScreen.tsx @@ -1,12 +1,6 @@ import { useState } from "react"; -import { - ScrollView, - StyleSheet, - Switch, - Text, - View, -} from "react-native"; +import { ScrollView, StyleSheet, Switch, Text, View } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; export type PriceAlert = { @@ -90,7 +84,8 @@ export function NotificationsScreen({ {alert.name} {alert.retailer} - Price dropped from {alert.previousPrice} to {alert.currentPrice} + Price dropped from {alert.previousPrice} to{" "} + {alert.currentPrice} Your target: {alert.targetPrice} @@ -98,7 +93,9 @@ export function NotificationsScreen({ setAlertEnabled(alert.id, enabled)} + onValueChange={(enabled) => + setAlertEnabled(alert.id, enabled) + } thumbColor={alert.enabled ? "#FFFFFF" : "#B8B8B8"} trackColor={{ false: "#4A4A4A", true: "#2E8B57" }} value={alert.enabled} @@ -172,7 +169,12 @@ const styles = StyleSheet.create({ title: { color: "#fff", fontSize: 30, fontWeight: "700", marginBottom: 8 }, subtitle: { color: "#aaa", fontSize: 16, lineHeight: 22, marginBottom: 32 }, section: { marginBottom: 32 }, - sectionTitle: { color: "#fff", fontSize: 21, fontWeight: "600", marginBottom: 14 }, + sectionTitle: { + color: "#fff", + fontSize: 21, + fontWeight: "600", + marginBottom: 14, + }, alertCard: { alignItems: "center", backgroundColor: "#161616", @@ -184,7 +186,12 @@ const styles = StyleSheet.create({ padding: 16, }, alertContent: { flex: 1, paddingRight: 12 }, - productName: { color: "#fff", fontSize: 17, fontWeight: "600", marginBottom: 4 }, + productName: { + color: "#fff", + fontSize: 17, + fontWeight: "600", + marginBottom: 4, + }, retailer: { color: "#aaa", fontSize: 14, marginBottom: 10 }, priceChange: { color: "#fff", fontSize: 15, lineHeight: 21, marginBottom: 4 }, targetPrice: { color: "#79DCA0", fontSize: 14, fontWeight: "600" }, @@ -195,7 +202,12 @@ const styles = StyleSheet.create({ borderWidth: 1, padding: 22, }, - emptyTitle: { color: "#fff", fontSize: 17, fontWeight: "600", marginBottom: 6 }, + emptyTitle: { + color: "#fff", + fontSize: 17, + fontWeight: "600", + marginBottom: 6, + }, emptyText: { color: "#aaa", fontSize: 15, lineHeight: 21 }, settingsCard: { backgroundColor: "#161616", @@ -204,9 +216,18 @@ const styles = StyleSheet.create({ borderWidth: 1, paddingHorizontal: 16, }, - settingRow: { alignItems: "center", flexDirection: "row", paddingVertical: 16 }, + settingRow: { + alignItems: "center", + flexDirection: "row", + paddingVertical: 16, + }, settingContent: { flex: 1, paddingRight: 12 }, - settingLabel: { color: "#fff", fontSize: 16, fontWeight: "600", marginBottom: 4 }, + settingLabel: { + color: "#fff", + fontSize: 16, + fontWeight: "600", + marginBottom: 4, + }, settingDescription: { color: "#aaa", fontSize: 14, lineHeight: 20 }, divider: { backgroundColor: "#2b2b2b", height: 1 }, helperText: { color: "#888", fontSize: 13, lineHeight: 19, marginTop: 10 }, diff --git a/apps/mobile/tsconfig.json b/apps/mobile/tsconfig.json index fc3e146..186ee5f 100644 --- a/apps/mobile/tsconfig.json +++ b/apps/mobile/tsconfig.json @@ -3,13 +3,8 @@ "compilerOptions": { "strict": true, "paths": { - "@/*": [ - "./*" - ] + "@/*": ["./*"] } }, - "include": [ - "**/*.ts", - "**/*.tsx" - ] + "include": ["**/*.ts", "**/*.tsx"] }