From 1ceef4df030bb51c71b8ead71f3a30fc137e564c Mon Sep 17 00:00:00 2001 From: Kelvin Truong Date: Tue, 24 Feb 2026 23:18:34 -0800 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20=E2=9C=A8=20add=20edit=20preference?= =?UTF-8?q?s=20ui=20in=20sidebar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/edit-preferences-content.tsx | 253 ++++++++++++++++++ .../components/ui/sidebar/sidebar-content.tsx | 43 +-- apps/next/src/components/ui/toolbar.tsx | 65 +++-- 3 files changed, 330 insertions(+), 31 deletions(-) create mode 100644 apps/next/src/components/ui/edit-preferences-content.tsx diff --git a/apps/next/src/components/ui/edit-preferences-content.tsx b/apps/next/src/components/ui/edit-preferences-content.tsx new file mode 100644 index 00000000..cf359bc0 --- /dev/null +++ b/apps/next/src/components/ui/edit-preferences-content.tsx @@ -0,0 +1,253 @@ +"use client"; + +import { KeyboardArrowLeft, KeyboardArrowRight } from "@mui/icons-material"; +import { + Avatar, + Button, + MobileStepper, + ToggleButton, + ToggleButtonGroup, + Typography, +} from "@mui/material"; +import { useState } from "react"; +import { useSession } from "@/utils/auth-client"; +import { trpc } from "@/utils/trpc"; +import { + AllergenKeys, + PreferenceKeys, +} from "../../../../../packages/validators/src/adobe-ecommerce"; + +export default function EditPreferencesContent() { + const { data: session } = useSession(); + const firstName = session?.user?.name?.split(" ")[0] || "User"; + const [activeStep, setActiveStep] = useState(0); + const [isSubmitting, setIsSubmitting] = useState(false); + const [formData, setFormData] = useState({ + allergies: [] as string[], + preferences: [] as string[], + }); + + const addAllergies = trpc.allergy.addAllergies.useMutation(); + const addPreferences = trpc.preference.addDietaryPreferences.useMutation(); + + const handleToggle = (key: keyof typeof formData, newValues: string[]) => { + setFormData((prev) => ({ + ...prev, + [key]: newValues, + })); + }; + + const handleSubmit = async () => { + if (!session?.user?.id) { + console.error("Please login first."); + return; + } + + setIsSubmitting(true); + try { + await addAllergies.mutateAsync({ + userId: session.user.id, + allergies: formData.allergies, + }); + await addPreferences.mutateAsync({ + userId: session.user.id, + preferences: formData.preferences, + }); + } catch (error) { + console.error("Saving preferences failed:", error); + } finally { + setIsSubmitting(false); + } + }; + + const handleNext = () => { + setActiveStep((prev) => Math.min(prev + 1, 1)); + }; + + const handleBack = () => { + setActiveStep((prev) => Math.max(prev - 1, 0)); + }; + + return ( +
+ {activeStep === 0 && ( +
+
+ + + Edit Preferences + + + Update your dining profile + +
+ +
+ + Food Allergies + + + Help us keep you safe by selecting your food allergies (optional) + +
+ + handleToggle("allergies", newValues)} + aria-label="select allergies" + exclusive={false} + fullWidth + className="pt-2 px-10 grid grid-cols-2 sm:grid-cols-3 gap-2 [&_.MuiToggleButtonGroup-grouped]:!border-2 [&_.MuiToggleButtonGroup-grouped]:!rounded-[10px] [&_.MuiToggleButtonGroup-grouped]:!border-gray-400" + > + {AllergenKeys.map((option) => ( + + + {option} + + + ))} + +
+ )} + {activeStep === 1 && ( +
+
+ + + Edit Preferences + + + Update your dining profile + +
+ +
+ + Dietary Preferences + + + Select any dietary restrictions that apply to you (optional) + +
+ + handleToggle("preferences", newValues)} + aria-label="select preferences" + exclusive={false} + fullWidth + className="pt-2 px-10 grid grid-cols-2 sm:grid-cols-3 gap-2 [&_.MuiToggleButtonGroup-grouped]:!border-2 [&_.MuiToggleButtonGroup-grouped]:!rounded-[10px] [&_.MuiToggleButtonGroup-grouped]:!border-gray-400" + > + {PreferenceKeys.map((option) => ( + + + {option} + + + ))} + +
+ )} + + + {isSubmitting ? "Saving..." : "Save"} + + ) : ( + + ) + } + backButton={ + + } + /> +
+ ); +} diff --git a/apps/next/src/components/ui/sidebar/sidebar-content.tsx b/apps/next/src/components/ui/sidebar/sidebar-content.tsx index 329a407f..27bc40de 100644 --- a/apps/next/src/components/ui/sidebar/sidebar-content.tsx +++ b/apps/next/src/components/ui/sidebar/sidebar-content.tsx @@ -10,20 +10,24 @@ import { LightMode as LightModeIcon, Logout as LogoutIcon, } from "@mui/icons-material"; +import { Tooltip } from "@mui/material"; import Image from "next/image"; import Link from "next/link"; import { useTheme } from "next-themes"; +import type { ReactNode } from "react"; import { useEffect, useState } from "react"; import { GoogleSignInButton } from "@/components/auth/google-sign-in"; import { signOut, useSession } from "@/utils/auth-client"; interface ProfileMenuContentProps { onClose: () => void; + onEditPreferencesClick: () => void; } export default function SidebarContent({ onClose, -}: ProfileMenuContentProps): JSX.Element { + onEditPreferencesClick, +}: ProfileMenuContentProps) { const { data: session } = useSession(); const user = session?.user; const { theme, setTheme } = useTheme(); @@ -64,7 +68,7 @@ export default function SidebarContent({ onClick={onClose} className="rounded-full p-1.5 text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800" > - + @@ -108,19 +112,19 @@ export default function SidebarContent({ setTheme("light")} - icon={} + icon={} label="Light" /> setTheme("system")} - icon={} + icon={} label="Device" /> setTheme("dark")} - icon={} + icon={} label="Dark" /> @@ -128,13 +132,22 @@ export default function SidebarContent({ {/* Links */}
- } - > - Edit Preferences - + + + + + void; - icon: React.ReactNode; + icon: ReactNode; label: string; }) { return ( @@ -211,8 +224,8 @@ function MenuLink({ }: { href: string; onClick: () => void; - icon: React.ReactNode; - children: React.ReactNode; + icon: ReactNode; + children: ReactNode; }) { return ( (null); const open = Boolean(anchorEl); - const handleClick = (event: React.MouseEvent) => { + const handleClick = (event: MouseEvent) => { setAnchorEl(event.currentTarget); }; @@ -115,17 +120,25 @@ function ToolbarDropdown({ element }: { element: ToolbarElement }) { ); } -export default function Toolbar(): React.JSX.Element { +export default function Toolbar() { + const isDesktop = useMediaQuery("(min-width: 768px)"); const [profileAnchor, setProfileAnchor] = useState(null); const profileOpen = Boolean(profileAnchor); + const [editPreferencesOpen, setEditPreferencesOpen] = useState(false); - const handleProfileOpen = (event: React.MouseEvent) => { + const handleProfileOpen = (event: MouseEvent) => { setProfileAnchor(event.currentTarget); }; const handleProfileClose = () => { setProfileAnchor(null); }; + const handleEditPreferencesOpen = () => { + setEditPreferencesOpen(true); + }; + const handleEditPreferencesClose = () => { + setEditPreferencesOpen(false); + }; const { data: session, isPending } = useSession(); const user = session?.user; @@ -230,25 +243,45 @@ export default function Toolbar(): React.JSX.Element { horizontal: "right", }} PaperProps={{ - sx: { - backgroundColor: "transparent", - boxShadow: "none", - padding: 0, - width: 357, - maxHeight: 658, - // borderRadius: 3, - mt: 1, - }, + className: + "bg-transparent shadow-none p-0 w-[357px] max-h-[658px] mt-1", }} MenuListProps={{ - sx: { - padding: 0, - }, + className: "p-0", }} > - + + {isDesktop ? ( + + + + ) : ( + + + + )} + {/* setDrawerOpen(!drawerOpen)} From 8036fcfe5b0102f2b7a36eacc937127caa31f2b7 Mon Sep 17 00:00:00 2001 From: Mihai Zecheru Date: Wed, 25 Feb 2026 16:14:38 -0800 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20=E2=9C=A8=20add=20backend=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sync data up to backend, handle getting and updating data. --- apps/next/src/app/account/page.tsx | 128 +----------------- .../ui/edit-preferences-content.tsx | 75 ++++++++-- 2 files changed, 71 insertions(+), 132 deletions(-) diff --git a/apps/next/src/app/account/page.tsx b/apps/next/src/app/account/page.tsx index 241c338d..bac3b71d 100644 --- a/apps/next/src/app/account/page.tsx +++ b/apps/next/src/app/account/page.tsx @@ -1,127 +1,9 @@ "use client"; -import { useState } from "react"; +import EditPreferencesContent from "@/components/ui/edit-preferences-content"; -export default function MyAccountPage() { - // Dummy state variables - const [isLoggedIn, setIsLoggedIn] = useState(false); - const [username, setUsername] = useState("peter_plate_user"); - const [newUsername, setNewUsername] = useState(""); +const AccountPage = () => { + return ; +}; - const [currentPassword, setCurrentPassword] = useState(""); - const [newPassword, setNewPassword] = useState(""); - - const handleCreateAccount = () => { - console.log("Creating account with:", newUsername); - // Placeholder for creating new account - setIsLoggedIn(true); - setUsername(newUsername); - }; - - const handlePasswordReset = () => { - console.log("Resetting password..."); - // Placeholder for real password update logic - }; - - return ( -
-

My Account

-

- Manage your PeterPlate account details here. -

- -
- {/* Username Section */} -
-

Username

-

- This is the name shown in your ratings and favorites. -

- - {isLoggedIn ? ( - // Logged in - show username (read-only) - - ) : ( - // Logged out - enter username - setNewUsername(e.target.value)} - className="w-full rounded-md border border-neutral-300 dark:border-neutral-700 bg-transparent px-3 py-2 text-sm" - /> - )} -
- - {/* Password Section */} -
-

Password

-

- {isLoggedIn - ? "Update your password to keep your account secure." - : "Set a password to create your account."} -

- - {isLoggedIn ? ( - // Logged-in password reset -
- setCurrentPassword(e.target.value)} - className="w-full rounded-md border border-neutral-300 dark:border-neutral-700 bg-transparent px-3 py-2 text-sm" - /> - setNewPassword(e.target.value)} - className="w-full rounded-md border border-neutral-300 dark:border-neutral-700 bg-transparent px-3 py-2 text-sm" - /> - -
- ) : ( - // Logged-out account creation fields -
- setNewPassword(e.target.value)} - className="w-full rounded-md border border-neutral-300 dark:border-neutral-700 bg-transparent px-3 py-2 text-sm" - /> - -
- )} -
- - {/* Debug button just for testing change in UI */} - -
-
- ); -} +export default AccountPage; diff --git a/apps/next/src/components/ui/edit-preferences-content.tsx b/apps/next/src/components/ui/edit-preferences-content.tsx index cf359bc0..0fbd6e4f 100644 --- a/apps/next/src/components/ui/edit-preferences-content.tsx +++ b/apps/next/src/components/ui/edit-preferences-content.tsx @@ -4,12 +4,13 @@ import { KeyboardArrowLeft, KeyboardArrowRight } from "@mui/icons-material"; import { Avatar, Button, + CircularProgress, MobileStepper, ToggleButton, ToggleButtonGroup, Typography, } from "@mui/material"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useSession } from "@/utils/auth-client"; import { trpc } from "@/utils/trpc"; import { @@ -27,6 +28,27 @@ export default function EditPreferencesContent() { preferences: [] as string[], }); + const { data: existingAllergies, isLoading: loadingAllergies } = + trpc.allergy.getAllergies.useQuery( + { userId: session?.user?.id ?? "" }, + { enabled: !!session?.user?.id }, + ); + + const { data: existingPreferences, isLoading: loadingPrefs } = + trpc.preference.getDietaryPreferences.useQuery( + { userId: session?.user?.id ?? "" }, + { enabled: !!session?.user?.id }, + ); + + useEffect(() => { + if (existingAllergies || existingPreferences) { + setFormData({ + allergies: existingAllergies || [], + preferences: existingPreferences || [], + }); + } + }, [existingAllergies, existingPreferences]); + const addAllergies = trpc.allergy.addAllergies.useMutation(); const addPreferences = trpc.preference.addDietaryPreferences.useMutation(); @@ -38,23 +60,50 @@ export default function EditPreferencesContent() { }; const handleSubmit = async () => { - if (!session?.user?.id) { - console.error("Please login first."); - return; - } - + if (!session?.user?.id) return; setIsSubmitting(true); + try { + const allergiesToDelete = + existingAllergies?.filter( + (x: any) => !formData.allergies.includes(x), + ) || []; + + await Promise.all( + allergiesToDelete.map((allergy: any) => + trpc.allergy.deleteAllergy.mutateAsync({ + userId: session.user.id, + allergy, + }), + ), + ); + await addAllergies.mutateAsync({ userId: session.user.id, allergies: formData.allergies, }); + + const prefsToDelete = + existingPreferences?.filter( + (x: any) => !formData.preferences.includes(x), + ) || []; + await Promise.all( + prefsToDelete.map((preference: any) => + trpc.preference.deletePreference.mutateAsync({ + userId: session.user.id, + preference, + }), + ), + ); + await addPreferences.mutateAsync({ userId: session.user.id, preferences: formData.preferences, }); + + alert("Preferences updated successfully!"); } catch (error) { - console.error("Saving preferences failed:", error); + console.error("Save failed", error); } finally { setIsSubmitting(false); } @@ -68,6 +117,14 @@ export default function EditPreferencesContent() { setActiveStep((prev) => Math.max(prev - 1, 0)); }; + if (loadingAllergies || loadingPrefs) { + return ( +
+ +
+ ); + } + return (
{activeStep === 0 && ( @@ -76,7 +133,7 @@ export default function EditPreferencesContent() { Date: Wed, 25 Feb 2026 16:25:26 -0800 Subject: [PATCH 3/9] =?UTF-8?q?fix:=20=F0=9F=90=9B=20prevent=20interaction?= =?UTF-8?q?=20when=20logged=20out?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Buttons cannot be clicked while the user is logged out. Added tooltip telling them to log in. --- .../ui/edit-preferences-content.tsx | 169 ++++++++++-------- 1 file changed, 97 insertions(+), 72 deletions(-) diff --git a/apps/next/src/components/ui/edit-preferences-content.tsx b/apps/next/src/components/ui/edit-preferences-content.tsx index 0fbd6e4f..9b8e864b 100644 --- a/apps/next/src/components/ui/edit-preferences-content.tsx +++ b/apps/next/src/components/ui/edit-preferences-content.tsx @@ -8,6 +8,7 @@ import { MobileStepper, ToggleButton, ToggleButtonGroup, + Tooltip, Typography, } from "@mui/material"; import { useEffect, useState } from "react"; @@ -28,6 +29,8 @@ export default function EditPreferencesContent() { preferences: [] as string[], }); + const isGuest = !session?.user; // Check if logged out + const { data: existingAllergies, isLoading: loadingAllergies } = trpc.allergy.getAllergies.useQuery( { userId: session?.user?.id ?? "" }, @@ -144,7 +147,9 @@ export default function EditPreferencesContent() { Edit Preferences - Update your dining profile + {isGuest + ? "Log in to update your dining profile" + : "Update your dining profile"}
@@ -167,31 +172,38 @@ export default function EditPreferencesContent() {
- handleToggle("allergies", newValues)} - aria-label="select allergies" - exclusive={false} - fullWidth - className="pt-2 px-10 grid grid-cols-2 sm:grid-cols-3 gap-2 [&_.MuiToggleButtonGroup-grouped]:!border-2 [&_.MuiToggleButtonGroup-grouped]:!rounded-[10px] [&_.MuiToggleButtonGroup-grouped]:!border-gray-400" - > - {AllergenKeys.map((option) => ( - +
+ + handleToggle("allergies", newValues) + } + aria-label="select allergies" + exclusive={false} + fullWidth + disabled={isGuest} + className="pt-2 grid grid-cols-2 sm:grid-cols-3 gap-2 [&_.MuiToggleButtonGroup-grouped]:!border-2 [&_.MuiToggleButtonGroup-grouped]:!rounded-[10px] [&_.MuiToggleButtonGroup-grouped]:!border-gray-400" > - - {option} - - - ))} - + {AllergenKeys.map((option) => ( + + + {option} + + + ))} + +
+ )} {activeStep === 1 && ( @@ -211,7 +223,9 @@ export default function EditPreferencesContent() { Edit Preferences - Update your dining profile + {isGuest + ? "Log in to update your dining profile" + : "Update your dining profile"} @@ -234,31 +248,38 @@ export default function EditPreferencesContent() { - handleToggle("preferences", newValues)} - aria-label="select preferences" - exclusive={false} - fullWidth - className="pt-2 px-10 grid grid-cols-2 sm:grid-cols-3 gap-2 [&_.MuiToggleButtonGroup-grouped]:!border-2 [&_.MuiToggleButtonGroup-grouped]:!rounded-[10px] [&_.MuiToggleButtonGroup-grouped]:!border-gray-400" - > - {PreferenceKeys.map((option) => ( - +
+ + handleToggle("preferences", newValues) + } + aria-label="select preferences" + exclusive={false} + fullWidth + disabled={isGuest} + className="pt-2 grid grid-cols-2 sm:grid-cols-3 gap-2 [&_.MuiToggleButtonGroup-grouped]:!border-2 [&_.MuiToggleButtonGroup-grouped]:!rounded-[10px] [&_.MuiToggleButtonGroup-grouped]:!border-gray-400" > - - {option} - - - ))} - + {PreferenceKeys.map((option) => ( + + + {option} + + + ))} + +
+ )} @@ -269,28 +290,32 @@ export default function EditPreferencesContent() { activeStep={activeStep} className="px-10" nextButton={ - activeStep === 1 ? ( - - ) : ( - - ) + + + {activeStep === 1 ? ( + + ) : ( + + )} + + } backButton={ diff --git a/apps/next/src/components/ui/toolbar.tsx b/apps/next/src/components/ui/toolbar.tsx index e7a84f85..ee9fa48d 100644 --- a/apps/next/src/components/ui/toolbar.tsx +++ b/apps/next/src/components/ui/toolbar.tsx @@ -11,6 +11,7 @@ import { Menu, MenuItem, Toolbar as MuiToolbar, + Snackbar, } from "@mui/material"; import Image from "next/image"; import Link from "next/link"; @@ -125,6 +126,8 @@ export default function Toolbar() { const [profileAnchor, setProfileAnchor] = useState(null); const profileOpen = Boolean(profileAnchor); const [editPreferencesOpen, setEditPreferencesOpen] = useState(false); + const [editPreferencesSnackbarOpen, setEditPreferencesSnackbarOpen] = + useState(false); const handleProfileOpen = (event: MouseEvent) => { setProfileAnchor(event.currentTarget); @@ -139,6 +142,10 @@ export default function Toolbar() { const handleEditPreferencesClose = () => { setEditPreferencesOpen(false); }; + const handleEditPreferencesSaved = () => { + setEditPreferencesOpen(false); + setEditPreferencesSnackbarOpen(true); + }; const { data: session, isPending } = useSession(); const user = session?.user; @@ -266,7 +273,7 @@ export default function Toolbar() { "w-[460px] max-w-[90vw] max-h-[90vh] m-2 p-0 overflow-hidden flex flex-col rounded-2xl", }} > - + ) : ( - + )} + { + if (reason === "clickaway") return; + setEditPreferencesSnackbarOpen(false); + }} + message="Preferences updated successfully" + anchorOrigin={{ vertical: "bottom", horizontal: "center" }} + /> + {/* setDrawerOpen(!drawerOpen)}