diff --git a/apps/mobile/__tests__/notifications-screen.test.tsx b/apps/mobile/__tests__/notifications-screen.test.tsx
new file mode 100644
index 0000000..9493865
--- /dev/null
+++ b/apps/mobile/__tests__/notifications-screen.test.tsx
@@ -0,0 +1,56 @@
+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 c2fda01..dca7774 100644
--- a/apps/mobile/features/home/HomeScreen.tsx
+++ b/apps/mobile/features/home/HomeScreen.tsx
@@ -1,23 +1,332 @@
-import { Pressable, SafeAreaView, Text, View } from "react-native";
+import { useState } from "react";
-import { authStyles } from "../auth/styles";
+import {
+ Pressable,
+ SafeAreaView,
+ ScrollView,
+ StyleSheet,
+ Text,
+ View,
+} from "react-native";
type HomeScreenProps = {
onLogout: () => void;
+ onOpenNotifications: () => void;
+ onOpenWatchedItems?: () => void;
};
-export function HomeScreen({ onLogout }: HomeScreenProps) {
+export function HomeScreen({
+ onLogout,
+ onOpenNotifications,
+ onOpenWatchedItems = () => undefined,
+}: HomeScreenProps) {
+ const [menuOpen, setMenuOpen] = useState(false);
+ const [categoriesOpen, setCategoriesOpen] = useState(false);
+ const [storesOpen, setStoresOpen] = useState(false);
+
return (
-
-
-
- Log out
-
-
+
+
+ {/* Header */}
+
+ setMenuOpen(!menuOpen)}
+ >
+ ☰
+
+
+ QuickCop
+
+
+ ⌕
+
+
+
+ {/* 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 */}
+
+ Watched Items
+
+
+
+ You are not watching any items yet.
+
+ Add or manage watched items
+
+
+
+ {/* Recommended */}
+
+ Recommended
+
+
+
+
+
+
+
+
+ {/* Popular */}
+
+ Popular
+
+
+
+
+
+
+
+
);
}
+
+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: 20,
+ },
+
+ logo: {
+ color: "#fff",
+ fontSize: 24,
+ fontWeight: "700",
+ },
+
+ iconButton: {
+ width: 44,
+ height: 44,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+
+ iconText: {
+ color: "#fff",
+ 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,
+ 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,
+ },
+
+ emptyAction: {
+ color: "#79DCA0",
+ fontSize: 14,
+ fontWeight: "600",
+ marginTop: 10,
+ },
+
+ 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",
+ },
+});
diff --git a/apps/mobile/features/notifications/NotificationsScreen.tsx b/apps/mobile/features/notifications/NotificationsScreen.tsx
new file mode 100644
index 0000000..68fdc00
--- /dev/null
+++ b/apps/mobile/features/notifications/NotificationsScreen.tsx
@@ -0,0 +1,234 @@
+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 },
+});
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"]
}