From 0e356e62087885381c586da3c9d176e14c2487eb Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Sat, 28 Mar 2026 09:55:51 -0400 Subject: [PATCH 001/112] add layout checks to the middleware --- frontend/src/features/account/get-session.ts | 37 +++++++++++++++++ frontend/src/middleware.ts | 43 +++++++++++++++++--- 2 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 frontend/src/features/account/get-session.ts diff --git a/frontend/src/features/account/get-session.ts b/frontend/src/features/account/get-session.ts new file mode 100644 index 000000000..c9c38c874 --- /dev/null +++ b/frontend/src/features/account/get-session.ts @@ -0,0 +1,37 @@ +"use server"; + +import { redirect } from "next/navigation"; + +import { AccountDetails } from "@/features/account/type"; +import { getAuthCookieString } from "@/lib/utils/api/cookie-utils"; +import { ROUTES } from "@/lib/utils/api/endpoints"; +import { ApiErrorResponse } from "@/lib/utils/api/fetch-wrapper"; +import { serverGet } from "@/lib/utils/api/server-fetch"; + +export async function getSession(): Promise { + const cookieString = await getAuthCookieString(); + + // If no account session cookies is present, the user is not logged in, so we + // can redirect them to the login page immediately without making an API call. + if (!cookieString.includes("account_sess_token")) { + redirect("/login"); + } + + // Otherwise fetch user info. If the session cookie is invalid/expired, the API + // will return a 401/403 error, in which case we will return null. + try { + const data = await serverGet(ROUTES.auth.checkAccountAuth); + return { + email: data.email, + defaultName: data.default_display_name, + }; + } catch (e) { + const error = e as ApiErrorResponse; + + if (error.status === 401 || error.status === 403) { + return null; + } + + throw error; + } +} diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts index 1e5f83ab6..44889c1ff 100644 --- a/frontend/src/middleware.ts +++ b/frontend/src/middleware.ts @@ -1,18 +1,49 @@ -import { NextResponse } from "next/server"; -import type { NextRequest } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; + +const authRoutes = [ + "/login", + "/register", + "/forgot-password", + "/reset-password", + "/verify-email", +]; + +const protectedRoutes = ["/settings"]; export function middleware(request: NextRequest) { const response = NextResponse.next(); + const path = request.nextUrl.pathname; + + const hasAccountSessToken = request.cookies.has("account_sess_token"); + + const isAuthRoute = authRoutes.some((route) => path.startsWith(route)); + const isPretectedRoute = protectedRoutes.some((route) => + path.startsWith(route), + ); + + // If the user is logged in and tries to access an auth route, redirect them + // to the dashboard. + if (hasAccountSessToken && isAuthRoute) { + return NextResponse.redirect(new URL("/dashboard", request.nextUrl)); + } + + // If the user is not logged in and tries to access a protected route (like + // settings), redirect them to the login page. A callbackUrl is included so users + // can be redirected back after logging in. + if (!hasAccountSessToken && isPretectedRoute) { + const loginUrl = new URL("/login", request.nextUrl); + loginUrl.searchParams.set("callbackUrl", path); + return NextResponse.redirect(loginUrl); + } if (process.env.NEXT_PUBLIC_DEBUG !== "true") { const cookieNames = ["account_sess_token", "guest_sess_token"]; const hasLegacyCookies = cookieNames.some((name) => - request.cookies.has(name) + request.cookies.has(name), ); // If the user has auth cookies... if (hasLegacyCookies) { - // We defensively try to DELETE the Host-Only (legacy) cookies on every request. // By setting Max-Age=0 and omitting the Domain attribute, we target the Host-Only version. // The valid Domain cookie (.example.com) will remain untouched because the browser @@ -24,7 +55,7 @@ export function middleware(request: NextRequest) { cookieNames.forEach((name) => { response.headers.append( "Set-Cookie", - `${name}=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax` + `${name}=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax`, ); }); } @@ -36,4 +67,4 @@ export function middleware(request: NextRequest) { export const config = { // Run on all pages, but skip static files and API routes matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"], -}; \ No newline at end of file +}; From 7f00582ee356311f4592b4b115703c409d2504ba Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:08:47 -0400 Subject: [PATCH 002/112] remove auth provider --- frontend/src/app/(auth)/layout.tsx | 42 --------- frontend/src/app/(auth)/login/page.tsx | 16 ++-- frontend/src/app/dashboard/page-client.tsx | 8 +- frontend/src/app/dashboard/page.tsx | 4 +- frontend/src/app/layout.tsx | 26 +----- frontend/src/app/settings/layout.tsx | 81 +++++------------ frontend/src/app/settings/page.tsx | 6 +- .../src/components/header/account-button.tsx | 14 +-- frontend/src/components/header/header.tsx | 20 ++--- frontend/src/components/header/logo-area.tsx | 14 --- frontend/src/features/account/context.ts | 27 ------ frontend/src/features/account/get-session.ts | 20 +++-- frontend/src/features/account/provider.tsx | 53 ------------ .../src/features/account/settings/context.tsx | 31 +++++++ .../settings/dialogs/change-password.tsx | 5 +- .../features/account/settings/selector.tsx | 86 +++++++++++-------- .../features/account/settings/sidebar-nav.tsx | 38 ++++++++ frontend/src/lib/providers.tsx | 14 +-- 18 files changed, 186 insertions(+), 319 deletions(-) delete mode 100644 frontend/src/features/account/context.ts delete mode 100644 frontend/src/features/account/provider.tsx create mode 100644 frontend/src/features/account/settings/context.tsx create mode 100644 frontend/src/features/account/settings/sidebar-nav.tsx diff --git a/frontend/src/app/(auth)/layout.tsx b/frontend/src/app/(auth)/layout.tsx index a1373a2a8..ff87ae0a0 100644 --- a/frontend/src/app/(auth)/layout.tsx +++ b/frontend/src/app/(auth)/layout.tsx @@ -1,49 +1,7 @@ -"use client"; - -import { useEffect, useRef } from "react"; - -import { usePathname, useRouter } from "next/navigation"; - -import Loading from "@/app/loading"; -import { useAccount } from "@/features/account/context"; -import { useToast } from "@/features/system-feedback"; -import { MESSAGES } from "@/lib/messages"; - -const ALLOWED_AUTH_ROUTES = ["/reset-password"]; - export default function AuthLayout({ children, }: { children: React.ReactNode; }) { - const { loginState } = useAccount(); - const router = useRouter(); - const pathname = usePathname(); - const { addToast } = useToast(); - const hasBeenLoggedOutRef = useRef(false); - - const isAllowedRoute = ALLOWED_AUTH_ROUTES.includes(pathname); - - useEffect(() => { - if (loginState === "logged_out") { - hasBeenLoggedOutRef.current = true; - } - - if (loginState === "logged_in") { - if (isAllowedRoute) return; - - // Check if the user was logged out to avoid this triggering on login - if (!hasBeenLoggedOutRef.current) { - router.replace("/dashboard"); - addToast("info", MESSAGES.INFO_ALREADY_LOGGED_IN); - } - } - }, [loginState, router, addToast, isAllowedRoute]); - - if (loginState === "logged_in" && !isAllowedRoute) { - // Logged in status is included to avoid flickering on redirect - return ; - } - return <>{children}; } diff --git a/frontend/src/app/(auth)/login/page.tsx b/frontend/src/app/(auth)/login/page.tsx index b6946210b..ef7629964 100644 --- a/frontend/src/app/(auth)/login/page.tsx +++ b/frontend/src/app/(auth)/login/page.tsx @@ -3,13 +3,12 @@ import { useState } from "react"; import Link from "next/link"; -import { useRouter } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import Checkbox from "@/components/checkbox"; import AuthPageLayout from "@/components/layout/auth-page"; import LinkText from "@/components/link-text"; import TextInputField from "@/components/text-input-field"; -import { useAccount } from "@/features/account/context"; import ActionButton from "@/features/button/components/action"; import { useFormErrors } from "@/lib/hooks/use-form-errors"; import { MESSAGES } from "@/lib/messages"; @@ -21,9 +20,11 @@ export default function Page() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [rememberMe, setRememberMe] = useState(false); - const { login } = useAccount(); const router = useRouter(); + const searchParams = useSearchParams(); + const callbackUrl = searchParams.get("callbackUrl") || "/dashboard"; + // TOASTS AND ERROR STATES const { errors, handleError, clearAllErrors } = useFormErrors(); @@ -52,16 +53,13 @@ export default function Page() { } try { - const data = await clientPost(ROUTES.auth.login, { + await clientPost(ROUTES.auth.login, { email, password, remember_me: rememberMe, }); - login({ - email: data.email, - defaultName: data.default_display_name, - }); - router.push("/dashboard"); + router.push(callbackUrl); + router.refresh(); return true; } catch (e) { const error = e as ApiErrorResponse; diff --git a/frontend/src/app/dashboard/page-client.tsx b/frontend/src/app/dashboard/page-client.tsx index d8e265d6d..ac41c47d8 100644 --- a/frontend/src/app/dashboard/page-client.tsx +++ b/frontend/src/app/dashboard/page-client.tsx @@ -6,7 +6,6 @@ import Link from "next/link"; import HeaderSpacer from "@/components/header-spacer"; import SegmentedControl from "@/components/segmented-control"; -import { useAccount } from "@/features/account/context"; import { DashboardEventProps } from "@/features/dashboard/components/event"; import EventGrid from "@/features/dashboard/components/event-grid"; import { deleteEvent } from "@/features/dashboard/delete-event"; @@ -27,7 +26,8 @@ export type DashboardPageProps = { export default function ClientPage({ created_events, participated_events, -}: DashboardPageProps) { + logged_in, +}: DashboardPageProps & { logged_in: boolean }) { const [optimisticCreatedEvents, deleteOptimisticCreatedEvent] = useOptimistic( created_events, (state, eventToDelete: string) => { @@ -48,8 +48,6 @@ export default function ClientPage({ const eventToDelete = useRef(null); const { addToast } = useToast(); - const { loginState } = useAccount(); - const currentTabEvents = tab === "created" ? optimisticCreatedEvents : optimisticParticipatedEvents; @@ -82,7 +80,7 @@ export default function ClientPage({

Dashboard

- {loginState === "logged_out" && ( + {!logged_in && (
This data is only available from this browser.{" "} diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx index cb44a6a9a..3fd207e5a 100644 --- a/frontend/src/app/dashboard/page.tsx +++ b/frontend/src/app/dashboard/page.tsx @@ -1,6 +1,7 @@ import { Metadata } from "next"; import ClientPage from "@/app/dashboard/page-client"; +import { getSession } from "@/features/account/get-session"; import { ROUTES } from "@/lib/utils/api/endpoints"; import { ApiErrorResponse } from "@/lib/utils/api/fetch-wrapper"; import handleErrorResponse from "@/lib/utils/api/handle-api-error"; @@ -16,12 +17,13 @@ export function generateMetadata(): Metadata { } export default async function Page() { + const accountDetails = await getSession(); try { const eventData = await serverGet(ROUTES.dashboard.get, undefined, { cache: "no-store", }); const processedData = processDashboardData(eventData); - return ; + return ; } catch (e) { const error = e as ApiErrorResponse; handleErrorResponse(error.status, error.data); diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 3c2ed75d6..30ba63c77 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -2,11 +2,7 @@ import type { Metadata } from "next"; import { Modak, Nunito } from "next/font/google"; import Header from "@/components/header/header"; -import { AccountDetails } from "@/features/account/type"; import { Providers } from "@/lib/providers"; -import { ROUTES } from "@/lib/utils/api/endpoints"; -import { ApiErrorResponse } from "@/lib/utils/api/fetch-wrapper"; -import { serverGet } from "@/lib/utils/api/server-fetch"; import "@/styles/globals.css"; const modak = Modak({ @@ -65,31 +61,11 @@ export function generateMetadata(): Metadata { }; } -async function checkLoginStatus(): Promise { - try { - const data = await serverGet(ROUTES.auth.checkAccountAuth); - return { - email: data.email, - defaultName: data.default_display_name, - }; - } catch (e) { - const error = e as ApiErrorResponse; - - if (error.status === 401 || error.status === 403) { - return null; - } - - throw error; - } -} - export default async function RootLayout({ children, }: { children: React.ReactNode; }) { - const accountDetails = await checkLoginStatus(); - return (
- +
{children} diff --git a/frontend/src/app/settings/layout.tsx b/frontend/src/app/settings/layout.tsx index eb95137da..2313c62b6 100644 --- a/frontend/src/app/settings/layout.tsx +++ b/frontend/src/app/settings/layout.tsx @@ -1,48 +1,28 @@ -"use client"; +import { Metadata } from "next"; +import { redirect } from "next/navigation"; -import { useEffect, useRef } from "react"; - -import Link from "next/link"; -import { usePathname, useRouter } from "next/navigation"; - -import Loading from "@/app/loading"; import HeaderSpacer from "@/components/header-spacer"; -import { useAccount } from "@/features/account/context"; -import { useToast } from "@/features/system-feedback"; -import { MESSAGES } from "@/lib/messages"; -import { cn } from "@/lib/utils/classname"; - -const SETTINGS_TABS = [ - { href: "/settings", label: "General" }, - { href: "/settings/security", label: "Security" }, - { href: "/settings/remove", label: "Account Removal" }, -] as const; +import { getSession } from "@/features/account/get-session"; +import { SettingsProvider } from "@/features/account/settings/context"; +import SettingsNav from "@/features/account/settings/sidebar-nav"; +import { constructMetadata } from "@/lib/utils/construct-metadata"; + +export function generateMetadata(): Metadata { + return constructMetadata( + "Account Settings", + "Manage your account settings and preferences on Plancake.", + ); +} -export default function SettingsLayout({ +export default async function SettingsLayout({ children, }: { children: React.ReactNode; }) { - const pathname = usePathname(); - const router = useRouter(); - const { loginState } = useAccount(); - const { addToast } = useToast(); - const previousLoginState = useRef(loginState); - - useEffect(() => { - if (loginState === "logged_out") { - router.replace("/login"); + const accountDetails = await getSession(); - // Only show the toast if they didn't JUST log out - if (previousLoginState.current !== "logged_in") { - addToast("info", MESSAGES.INFO_NOT_LOGGED_IN); - } - } - previousLoginState.current = loginState; - }, [addToast, loginState, router]); - - if (loginState === "logged_out") { - return ; + if (!accountDetails) { + redirect("/login?redirect=/settings"); } return ( @@ -58,29 +38,14 @@ export default function SettingsLayout({
-
{children}
+
+ + {children} + +
); diff --git a/frontend/src/app/settings/page.tsx b/frontend/src/app/settings/page.tsx index 923718901..10e7ec5c2 100644 --- a/frontend/src/app/settings/page.tsx +++ b/frontend/src/app/settings/page.tsx @@ -5,7 +5,7 @@ import { useState } from "react"; import { CheckIcon } from "@radix-ui/react-icons"; import TextInputField from "@/components/text-input-field"; -import { useAccount } from "@/features/account/context"; +import { useSettingsAccount } from "@/features/account/settings/context"; import { MAX_DEFAULT_NAME_LENGTH } from "@/features/account/settings/lib/constants"; import ActionButton from "@/features/button/components/action"; import { useToast } from "@/features/system-feedback/toast/context"; @@ -16,7 +16,7 @@ import { ApiErrorResponse } from "@/lib/utils/api/fetch-wrapper"; import { cn } from "@/lib/utils/classname"; export default function Page() { - const { refreshAccount, accountDetails } = useAccount(); + const accountDetails = useSettingsAccount(); const { addToast } = useToast(); const [defaultName, setDefaultName] = useState( @@ -36,7 +36,6 @@ export default function Page() { await clientPost(ROUTES.account.setDefaultName, { display_name: defaultName, }); - await refreshAccount(); addToast("success", MESSAGES.SUCCESS_DEFAULT_NAME_SAVED); return true; } catch (e) { @@ -47,7 +46,6 @@ export default function Page() { } else { try { await clientPost(ROUTES.account.removeDefaultName); - await refreshAccount(); setDefaultName(""); addToast("success", MESSAGES.SUCCESS_DEFAULT_NAME_REMOVED); return true; diff --git a/frontend/src/components/header/account-button.tsx b/frontend/src/components/header/account-button.tsx index 3ed509baa..02ccc884c 100644 --- a/frontend/src/components/header/account-button.tsx +++ b/frontend/src/components/header/account-button.tsx @@ -4,21 +4,25 @@ import { useState } from "react"; import { PersonIcon } from "@radix-ui/react-icons"; -import { useAccount } from "@/features/account/context"; import AccountSettings from "@/features/account/settings/selector"; +import { AccountDetails } from "@/features/account/type"; import EmptyButton from "@/features/button/components/empty"; import LinkButton from "@/features/button/components/link"; -export default function AccountButton() { - const { loginState } = useAccount(); - +export default function AccountButton({ + accountDetails, +}: { + accountDetails: AccountDetails | null; +}) { + console.log("AccountButton received account details:", accountDetails); // Debug log to check received account details const [accountSettingsOpen, setAccountSettingsOpen] = useState(false); - if (loginState === "logged_in") { + if (accountDetails) { return ( { - setMounted(true); - }, []); - - if (!mounted) { - return null; - } +export default async function Header() { + const accountDetails = await getSession(); + console.log("Header account details:", accountDetails); // Debug log to check account details return (
@@ -28,7 +18,7 @@ export default function Header() { - +
diff --git a/frontend/src/components/header/logo-area.tsx b/frontend/src/components/header/logo-area.tsx index c6dca17cc..de10133ee 100644 --- a/frontend/src/components/header/logo-area.tsx +++ b/frontend/src/components/header/logo-area.tsx @@ -1,7 +1,3 @@ -"use client"; - -import { useEffect, useState } from "react"; - import Link from "next/link"; import LinkText from "@/components/link-text"; @@ -9,16 +5,6 @@ import Logo from "@/components/logo"; import { getCurrentVersion } from "@/features/version-history/data"; export default function LogoArea() { - const [mounted, setMounted] = useState(false); - - useEffect(() => { - setMounted(true); - }, []); - - if (!mounted) { - return null; - } - return (
{/* Text Container */} diff --git a/frontend/src/features/account/context.ts b/frontend/src/features/account/context.ts deleted file mode 100644 index fdd622ca7..000000000 --- a/frontend/src/features/account/context.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { createContext, useContext } from "react"; - -import { AccountDetails, LoginState } from "@/features/account/type"; - -export const AccountContext = createContext<{ - loginState: LoginState; - accountDetails: AccountDetails | null; - login: (accountDetails: AccountDetails) => void; - logout: () => void; - refreshAccount: () => Promise; -}>({ - loginState: "logged_out", - accountDetails: null, - login: () => {}, - logout: () => {}, - refreshAccount: async () => {}, -}); - -export function useAccount() { - const context = useContext(AccountContext); - if (!context) { - throw new Error("useAccount must be used within an AccountProvider"); - } - return context; -} - -export default AccountContext; diff --git a/frontend/src/features/account/get-session.ts b/frontend/src/features/account/get-session.ts index c9c38c874..2f8130a6c 100644 --- a/frontend/src/features/account/get-session.ts +++ b/frontend/src/features/account/get-session.ts @@ -1,6 +1,6 @@ "use server"; -import { redirect } from "next/navigation"; +import { cache } from "react"; import { AccountDetails } from "@/features/account/type"; import { getAuthCookieString } from "@/lib/utils/api/cookie-utils"; @@ -8,19 +8,21 @@ import { ROUTES } from "@/lib/utils/api/endpoints"; import { ApiErrorResponse } from "@/lib/utils/api/fetch-wrapper"; import { serverGet } from "@/lib/utils/api/server-fetch"; -export async function getSession(): Promise { +// 2. Wrap your entire async function in cache() +export const getSession = cache(async (): Promise => { const cookieString = await getAuthCookieString(); + console.log("Retrieved cookie string:", cookieString); - // If no account session cookies is present, the user is not logged in, so we - // can redirect them to the login page immediately without making an API call. if (!cookieString.includes("account_sess_token")) { - redirect("/login"); + return null; } - // Otherwise fetch user info. If the session cookie is invalid/expired, the API - // will return a 401/403 error, in which case we will return null. try { - const data = await serverGet(ROUTES.auth.checkAccountAuth); + const data = await serverGet(ROUTES.auth.checkAccountAuth, undefined, { + // Keep this! It stops Next.js from aggressively caching the + // result across DIFFERENT users/requests. + cache: "no-store", + }); return { email: data.email, defaultName: data.default_display_name, @@ -34,4 +36,4 @@ export async function getSession(): Promise { throw error; } -} +}); diff --git a/frontend/src/features/account/provider.tsx b/frontend/src/features/account/provider.tsx deleted file mode 100644 index e95f11dd9..000000000 --- a/frontend/src/features/account/provider.tsx +++ /dev/null @@ -1,53 +0,0 @@ -"use client"; - -import { useCallback, useState } from "react"; - -import AccountContext from "@/features/account/context"; -import { AccountDetails, LoginState } from "@/features/account/type"; -import { clientGet } from "@/lib/utils/api/client-fetch"; -import { ROUTES } from "@/lib/utils/api/endpoints"; - -export default function AccountProvider({ - children, - accountDetails: initialAccountDetails, -}: { - children: React.ReactNode; - accountDetails: AccountDetails | null; -}) { - const [accountDetails, setAccountDetails] = useState(initialAccountDetails); - const [loginState, setLoginState] = useState( - initialAccountDetails ? "logged_in" : "logged_out", - ); - - const login = useCallback((accountDetails: AccountDetails) => { - setAccountDetails(accountDetails); - setLoginState("logged_in"); - }, []); - - const logout = useCallback(() => { - setAccountDetails(null); - setLoginState("logged_out"); - }, []); - - const refreshAccount = useCallback(async () => { - try { - const data = await clientGet(ROUTES.auth.checkAccountAuth); - setAccountDetails({ - email: data.email, - defaultName: data.default_display_name, - }); - setLoginState("logged_in"); - } catch { - setAccountDetails(null); - setLoginState("logged_out"); - } - }, []); - - return ( - - {children} - - ); -} diff --git a/frontend/src/features/account/settings/context.tsx b/frontend/src/features/account/settings/context.tsx new file mode 100644 index 000000000..bafb6be75 --- /dev/null +++ b/frontend/src/features/account/settings/context.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { createContext, useContext } from "react"; + +import { AccountDetails } from "@/features/account/type"; + +const SettingsContext = createContext(null); + +export function SettingsProvider({ + children, + accountDetails, +}: { + children: React.ReactNode; + accountDetails: AccountDetails; +}) { + return ( + + {children} + + ); +} + +export function useSettingsAccount() { + const context = useContext(SettingsContext); + if (!context) { + throw new Error( + "useSettingsAccount must be used within a SettingsProvider", + ); + } + return context; +} diff --git a/frontend/src/features/account/settings/dialogs/change-password.tsx b/frontend/src/features/account/settings/dialogs/change-password.tsx index 86f04420a..fad162492 100644 --- a/frontend/src/features/account/settings/dialogs/change-password.tsx +++ b/frontend/src/features/account/settings/dialogs/change-password.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from "react"; import LinkText from "@/components/link-text"; import TextInputField from "@/components/text-input-field"; -import { useAccount } from "@/features/account/context"; +import { useSettingsAccount } from "@/features/account/settings/context"; import PasswordValidation from "@/features/auth/components/password-validation"; import EmptyButton from "@/features/button/components/empty"; import { ConfirmationDialog, useToast } from "@/features/system-feedback"; @@ -14,6 +14,7 @@ import { ROUTES } from "@/lib/utils/api/endpoints"; import { ApiErrorResponse } from "@/lib/utils/api/fetch-wrapper"; export default function ChangePasswordDialog() { + const accountDetails = useSettingsAccount(); const isMobile = useCheckMobile(); // TOASTS AND ERROR STATES @@ -50,8 +51,6 @@ export default function ChangePasswordDialog() { setConfirmPassword(value); }; - const { accountDetails } = useAccount(); - const handleForgotPassword = async () => { if (!accountDetails?.email) { addToast("error", "No email associated with this account."); diff --git a/frontend/src/features/account/settings/selector.tsx b/frontend/src/features/account/settings/selector.tsx index 9d4ea376f..5308e14d8 100644 --- a/frontend/src/features/account/settings/selector.tsx +++ b/frontend/src/features/account/settings/selector.tsx @@ -1,13 +1,13 @@ -import { useState } from "react"; +import { useState, startTransition, useOptimistic } from "react"; import { CheckIcon, ExitIcon } from "@radix-ui/react-icons"; import { useRouter } from "next/navigation"; import TextInputField from "@/components/text-input-field"; -import { useAccount } from "@/features/account/context"; import AccountSettingsDrawer from "@/features/account/settings/drawer"; import { MAX_DEFAULT_NAME_LENGTH } from "@/features/account/settings/lib/constants"; import AccountSettingsPopover from "@/features/account/settings/popover"; +import { AccountDetails } from "@/features/account/type"; import ActionButton from "@/features/button/components/action"; import { useToast } from "@/features/system-feedback"; import useCheckMobile from "@/lib/hooks/use-check-mobile"; @@ -21,17 +21,24 @@ export default function AccountSettings({ children, open, setOpenChange, + accountDetails, }: { children: React.ReactNode; open: boolean; setOpenChange: (open: boolean) => void; + accountDetails: AccountDetails; }) { const isMobile = useCheckMobile(); if (isMobile) { return ( } + content={ + + } open={open} setOpen={setOpenChange} > @@ -42,7 +49,12 @@ export default function AccountSettings({ return ( } + content={ + + } open={open} setOpen={setOpenChange} > @@ -53,10 +65,11 @@ export default function AccountSettings({ function SettingsContent({ setOpenChange, + accountDetails, }: { setOpenChange: (open: boolean) => void; + accountDetails: AccountDetails; }) { - const { login, logout, accountDetails } = useAccount(); const router = useRouter(); const [defaultName, setDefaultName] = useState( @@ -64,45 +77,44 @@ function SettingsContent({ ); const [defaultNameError, setDefaultNameError] = useState(""); + const [optimisticBaseName, setOptimisticBaseName] = useOptimistic( + accountDetails?.defaultName || "", + (newName: string) => newName, + ); + // editing states - const isEditingDefaultName = - defaultName !== (accountDetails?.defaultName || ""); + const isEditingDefaultName = defaultName !== optimisticBaseName; - const applyDefaultName = async () => { + const applyDefaultName = async (): Promise => { if (!isEditingDefaultName) return true; setDefaultNameError(""); - try { - if (defaultName) { - try { - await clientPost(ROUTES.account.setDefaultName, { - display_name: defaultName, - }); - login({ ...accountDetails!, defaultName: defaultName }); - addToast("success", MESSAGES.SUCCESS_DEFAULT_NAME_SAVED); - return true; - } catch (e) { - const error = e as ApiErrorResponse; - addToast("error", error.formattedMessage); - return false; - } - } else { + + return new Promise((resolve) => { + startTransition(async () => { + // UI update + setOptimisticBaseName(defaultName); + try { - await clientPost(ROUTES.account.removeDefaultName); - login({ ...accountDetails!, defaultName: "" }); - setDefaultName(""); - addToast("success", MESSAGES.SUCCESS_DEFAULT_NAME_REMOVED); - return true; + if (defaultName) { + await clientPost(ROUTES.account.setDefaultName, { + display_name: defaultName, + }); + addToast("success", MESSAGES.SUCCESS_DEFAULT_NAME_SAVED); + } else { + await clientPost(ROUTES.account.removeDefaultName); + addToast("success", MESSAGES.SUCCESS_DEFAULT_NAME_REMOVED); + } + + router.refresh(); + resolve(true); } catch (e) { + console.error("Fetch error:", e); const error = e as ApiErrorResponse; - addToast("error", error.formattedMessage); - return false; + addToast("error", error?.formattedMessage || MESSAGES.ERROR_GENERIC); + resolve(false); } - } - } catch (e) { - console.error("Fetch error:", e); - addToast("error", MESSAGES.ERROR_GENERIC); - return false; - } + }); + }); }; const handleDefaultNameChange = (value: string) => { @@ -120,8 +132,8 @@ function SettingsContent({ const signOut = async () => { try { await clientPost(ROUTES.auth.logout); - logout(); router.push("/login"); + router.refresh(); addToast("success", MESSAGES.SUCCESS_LOGOUT); setOpenChange(false); return true; diff --git a/frontend/src/features/account/settings/sidebar-nav.tsx b/frontend/src/features/account/settings/sidebar-nav.tsx new file mode 100644 index 000000000..2961dcc2f --- /dev/null +++ b/frontend/src/features/account/settings/sidebar-nav.tsx @@ -0,0 +1,38 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +import { cn } from "@/lib/utils/classname"; + +const SETTINGS_TABS = [ + { href: "/settings", label: "General" }, + { href: "/settings/security", label: "Security" }, + { href: "/settings/remove", label: "Account Removal" }, +] as const; + +export default function SettingsNav() { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/frontend/src/lib/providers.tsx b/frontend/src/lib/providers.tsx index 1223e3072..527268190 100644 --- a/frontend/src/lib/providers.tsx +++ b/frontend/src/lib/providers.tsx @@ -2,22 +2,12 @@ import { ThemeProvider } from "next-themes"; -import AccountProvider from "@/features/account/provider"; -import { AccountDetails } from "@/features/account/type"; import { ToastProvider } from "@/features/system-feedback"; -export function Providers({ - children, - accountDetails, -}: { - children: React.ReactNode; - accountDetails: AccountDetails | null; -}) { +export function Providers({ children }: { children: React.ReactNode }) { return ( - - {children} - + {children} ); } From eafabd4ff1751956a063191f2dff66f53f8d60d2 Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Mon, 30 Mar 2026 13:23:40 -0400 Subject: [PATCH 003/112] fix merge conflicts --- .../setting-dialogs/delete-account.tsx | 3 -- .../header/components/account-button.tsx | 29 +++++++++---------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/frontend/src/features/account/setting-dialogs/delete-account.tsx b/frontend/src/features/account/setting-dialogs/delete-account.tsx index 4e38ffb53..9d2405c33 100644 --- a/frontend/src/features/account/setting-dialogs/delete-account.tsx +++ b/frontend/src/features/account/setting-dialogs/delete-account.tsx @@ -5,7 +5,6 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; import TextInputField from "@/components/text-input-field"; -import { useAccount } from "@/features/account/context"; import EmptyButton from "@/features/button/components/empty"; import { ConfirmationDialog, useToast } from "@/features/system-feedback"; import useCheckMobile from "@/lib/hooks/use-check-mobile"; @@ -18,7 +17,6 @@ import { ApiErrorResponse } from "@/lib/utils/api/fetch-wrapper"; export default function DeleteAccountDialog() { const router = useRouter(); const isMobile = useCheckMobile(); - const { logout } = useAccount(); // TOASTS AND ERROR STATES const { addToast } = useToast(); @@ -49,7 +47,6 @@ export default function DeleteAccountDialog() { await clientPost(ROUTES.auth.deleteAccount, { password: currentPassword, }); - logout(); router.push("/login"); addToast("success", MESSAGES.SUCCESS_ACCOUNT_DELETE); return true; diff --git a/frontend/src/features/header/components/account-button.tsx b/frontend/src/features/header/components/account-button.tsx index 7c1280222..8af22c742 100644 --- a/frontend/src/features/header/components/account-button.tsx +++ b/frontend/src/features/header/components/account-button.tsx @@ -1,16 +1,14 @@ -"use client"; - import { ExitIcon, PersonIcon } from "@radix-ui/react-icons"; -import { useRouter } from "next/navigation"; +import { redirect } from "next/navigation"; import KebabMenu from "@/components/kebab-menu"; -import { useAccount } from "@/features/account/context"; +import { getSession } from "@/features/account/get-session"; import ActionButton from "@/features/button/components/action"; import EmptyButton from "@/features/button/components/empty"; import LinkButton from "@/features/button/components/link"; import ShrinkingHeaderButton from "@/features/header/components/shrinking-header-button"; -import { useToast } from "@/features/system-feedback"; -import { MESSAGES } from "@/lib/messages"; +// import { useToast } from "@/features/system-feedback"; +// import { MESSAGES } from "@/lib/messages"; import { clientPost } from "@/lib/utils/api/client-fetch"; import { ROUTES } from "@/lib/utils/api/endpoints"; import { ApiErrorResponse } from "@/lib/utils/api/fetch-wrapper"; @@ -19,23 +17,22 @@ interface AccountButtonProps { onMenuOpenChange?: (isOpen: boolean) => void; } -export default function AccountButton({ +export default async function AccountButton({ onMenuOpenChange, }: AccountButtonProps) { - const { loginState, logout, accountDetails } = useAccount(); - const router = useRouter(); - const { addToast } = useToast(); + const accountDetails = await getSession(); + + // const { addToast } = useToast(); const signOut = async () => { try { await clientPost(ROUTES.auth.logout); - logout(); - router.push("/login"); - addToast("success", MESSAGES.SUCCESS_LOGOUT); - return true; + redirect("/login"); + // cannot use toast after redirect bc toast does not work on server components } catch (e) { const error = e as ApiErrorResponse; - addToast("error", error.formattedMessage); + console.error("Logout error:", error); + // addToast("error", error.formattedMessage); return false; } }; @@ -58,7 +55,7 @@ export default function AccountButton({ /> ); - if (loginState === "logged_in") { + if (accountDetails) { return ( Date: Mon, 13 Apr 2026 17:29:20 -0400 Subject: [PATCH 004/112] initial dialog separation --- .../features/system-feedback/dialog/base.tsx | 109 ++++++++++++++++++ .../system-feedback/dialog/confirmation.tsx | 99 ++++++++++++++++ .../features/system-feedback/dialog/form.tsx | 67 +++++++++++ .../features/system-feedback/dialog/props.ts | 109 ++++++++++++++++++ .../src/features/system-feedback/index.ts | 2 +- frontend/src/features/system-feedback/type.ts | 2 +- 6 files changed, 386 insertions(+), 2 deletions(-) create mode 100644 frontend/src/features/system-feedback/dialog/base.tsx create mode 100644 frontend/src/features/system-feedback/dialog/confirmation.tsx create mode 100644 frontend/src/features/system-feedback/dialog/form.tsx create mode 100644 frontend/src/features/system-feedback/dialog/props.ts diff --git a/frontend/src/features/system-feedback/dialog/base.tsx b/frontend/src/features/system-feedback/dialog/base.tsx new file mode 100644 index 000000000..1983244e1 --- /dev/null +++ b/frontend/src/features/system-feedback/dialog/base.tsx @@ -0,0 +1,109 @@ +import { useState, useCallback } from "react"; + +import * as Dialog from "@radix-ui/react-dialog"; + +import { FloatingDrawer } from "@/features/drawer"; +import { BaseDialogProps } from "@/features/system-feedback/dialog/props"; +import { cn } from "@/lib/utils/classname"; + +export default function BaseDialog({ + type, + title, + description, + trigger, + children, + open: controlledOpen, + onOpenChange, + asNestedDrawer = false, + triggerDisabled = false, + icon, +}: BaseDialogProps) { + /* OPEN STATE MANAGEMENT */ + const [internalOpen, setInternalOpen] = useState(false); + const isControlled = controlledOpen !== undefined; + const open = isControlled ? controlledOpen : internalOpen; + + const handleOpenChange = useCallback( + (newOpen: boolean) => { + if (!isControlled) { + setInternalOpen(newOpen); + } + onOpenChange?.(newOpen); + }, + [isControlled, onOpenChange], + ); + + if (asNestedDrawer) { + return ( + } + > +
+ {icon} +

{title}

+ {children} +
+
+ ); + } + + return ( + + {trigger && ( + { + if (triggerDisabled) { + e.preventDefault(); + e.stopPropagation(); + } + }} + aria-disabled={triggerDisabled} + > + {trigger} + + )} + + + + +
+ +
+ {icon} +

{title}

+
+
+ + + {description} + + + {children} +
+
+
+
+ ); +} diff --git a/frontend/src/features/system-feedback/dialog/confirmation.tsx b/frontend/src/features/system-feedback/dialog/confirmation.tsx new file mode 100644 index 000000000..0f08f3fb0 --- /dev/null +++ b/frontend/src/features/system-feedback/dialog/confirmation.tsx @@ -0,0 +1,99 @@ +import { useCallback, useEffect } from "react"; + +import ActionButton from "@/features/button/components/action"; +import { DIALOG_CONFIG } from "@/features/system-feedback/confirmation/config"; +import BaseModal from "@/features/system-feedback/dialog/base"; +import { ConfirmationDialogProps } from "@/features/system-feedback/dialog/props"; +import { cn } from "@/lib/utils/classname"; + +export default function ConfirmationDialog({ + type, + title, + description, + onConfirm, + children, + trigger, + triggerDisabled = false, + showIcon = false, + autoClose = false, + asNestedDrawer = false, + open, + onOpenChange, +}: ConfirmationDialogProps) { + const handleConfirm = useCallback(async () => { + if (autoClose) { + onOpenChange?.(false); + onConfirm(); + return true; + } + const success = await onConfirm(); + if (success) { + onOpenChange?.(false); + } + return success; + }, [autoClose, onConfirm, onOpenChange]); + + /* KEY LISTENERS */ + useEffect(() => { + if (!open) return; + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Enter") handleConfirm(); + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [handleConfirm, open]); + + /* ICON RENDERING */ + const config = DIALOG_CONFIG[type] || DIALOG_CONFIG.info; + const Icon = config.icon; + + const renderIcon = () => { + if (!showIcon) return null; + if (config.isTextIcon) { + return ( +
+
!
+
+ ); + } + return ( +
+ {Icon && } +
+ ); + }; + + return ( + + {children} +
+ onOpenChange?.(false)} + /> + +
+
+ ); +} diff --git a/frontend/src/features/system-feedback/dialog/form.tsx b/frontend/src/features/system-feedback/dialog/form.tsx new file mode 100644 index 000000000..8f208729f --- /dev/null +++ b/frontend/src/features/system-feedback/dialog/form.tsx @@ -0,0 +1,67 @@ +import { useCallback } from "react"; + +import ActionButton from "@/features/button/components/action"; +import EmptyButton from "@/features/button/components/empty"; +import BaseModal from "@/features/system-feedback/dialog/base"; +import { FormDialogProps } from "@/features/system-feedback/dialog/props"; + +export default function FormDialog({ + type, + title, + description, + onSubmit, + submitLabel = "Save", + cancelLabel = "Cancel", + children, + trigger, + asNestedDrawer = false, + open, + onOpenChange, + icon, +}: FormDialogProps) { + const handleSubmit = useCallback( + async (e: React.FormEvent) => { + console.log("Form submitted"); + e.preventDefault(); + const success = await onSubmit(); + if (success) { + onOpenChange?.(false); + } + }, + [onSubmit, onOpenChange], + ); + + return ( + +
+ {children} + +
+ onOpenChange?.(false)} + /> + +
+
+
+ ); +} diff --git a/frontend/src/features/system-feedback/dialog/props.ts b/frontend/src/features/system-feedback/dialog/props.ts new file mode 100644 index 000000000..7387d373c --- /dev/null +++ b/frontend/src/features/system-feedback/dialog/props.ts @@ -0,0 +1,109 @@ +import { DialogType } from "@/features/system-feedback"; + +type CommonDialogProps = { + /** + * Type of the dialog, which determines its styling and default icon. + * + * @type {DialogType} + */ + type: DialogType; + /** + * Title of the dialog + * Required for accessibility (used in Title components and aria attributes) + * + * @type {string} + */ + title: string; + /** + * Description of the dialog + * Required for accessibility (used in Description components and aria attributes) + * + * @type {string} + */ + description: string; + /** + * Dialog open state (for controlled dialogs) + * If not provided, the dialog will manage its own open state internally. + * + * @type {boolean} + * @default undefined + */ + open?: boolean; + /** + * Callback when the open state changes (for controlled dialogs) + * If not provided, the dialog will manage its own open state internally. + * + * @type {(open: boolean) => void} + * @default undefined + */ + onOpenChange?: (open: boolean) => void; + /** + * The content of the dialog, which can be any valid React node. + * + * @type {React.ReactNode} + * @default undefined + */ + children?: React.ReactNode; + /** + * Trigger element to open the dialog. If not provided, the dialog can only be opened + * programmatically via the `open` prop. + * + * @type {React.ReactNode} + * @default undefined + */ + trigger?: React.ReactNode; + /** + * Whether to disable the trigger element (if provided). + * + * @type {boolean} + * @default false + */ + triggerDisabled?: boolean; + /** + * Indicates whether or not the drawer version of the dialog should be displayed + * as a nested drawer or not. This is to ensure the correct styling and behavior + * when the dialog is used inside another drawer. + * + * @type {boolean} + * @default false + */ + asNestedDrawer?: boolean; + /** + * Optional icon to display in the dialog header, next to the title. + * + * @type {React.ReactNode} + * @default undefined + */ + icon?: React.ReactNode; +}; + +export type BaseDialogProps = CommonDialogProps; + +export type ConfirmationDialogProps = Omit & { + /** + * Enforced strictly as a string for semantic standard confirmations. + */ + description: string; + + /** Callback function that is called when the user confirms the action */ + onConfirm: () => boolean | Promise; + + /** Whether to display the default icon associated with the dialog 'type' */ + showIcon?: boolean; + + /** Whether to automatically close the dialog after confirming */ + autoClose?: boolean; +}; + +export type FormDialogProps = CommonDialogProps & { + /** * Callback function called when the native
is submitted. + * Returning true (or a Promise resolving to true) typically closes the modal. + */ + onSubmit: () => boolean | Promise; + + /** Text for the submit button (Defaults to "Save" or "Submit") */ + submitLabel?: string; + + /** Text for the cancel button (Defaults to "Cancel") */ + cancelLabel?: string; +}; diff --git a/frontend/src/features/system-feedback/index.ts b/frontend/src/features/system-feedback/index.ts index 84475165a..f43c63a95 100644 --- a/frontend/src/features/system-feedback/index.ts +++ b/frontend/src/features/system-feedback/index.ts @@ -9,5 +9,5 @@ export { useToast } from "@/features/system-feedback/toast/context"; export type { BannerType, ToastType, - ConfirmationDialogType, + DialogType, } from "@/features/system-feedback/type"; diff --git a/frontend/src/features/system-feedback/type.ts b/frontend/src/features/system-feedback/type.ts index a0d3a6594..bb2c84943 100644 --- a/frontend/src/features/system-feedback/type.ts +++ b/frontend/src/features/system-feedback/type.ts @@ -10,4 +10,4 @@ export type BaseFeedbackType = "success" | "error" | "info"; /* EXTENDED FEEDBACK TYPES */ export type BannerType = BaseFeedbackType; export type ToastType = BaseFeedbackType | "copy"; -export type ConfirmationDialogType = BaseFeedbackType | "warning" | "delete"; +export type DialogType = BaseFeedbackType | "warning" | "delete"; From da0bf8b8ddebd1abff5f485338d9c6370d82a018 Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:31:30 -0400 Subject: [PATCH 005/112] adjust confirmation spacing --- frontend/src/features/system-feedback/dialog/confirmation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/features/system-feedback/dialog/confirmation.tsx b/frontend/src/features/system-feedback/dialog/confirmation.tsx index 0f08f3fb0..e24b59bba 100644 --- a/frontend/src/features/system-feedback/dialog/confirmation.tsx +++ b/frontend/src/features/system-feedback/dialog/confirmation.tsx @@ -81,7 +81,7 @@ export default function ConfirmationDialog({ icon={renderIcon()} > {children} -
+
Date: Fri, 17 Apr 2026 10:46:49 -0400 Subject: [PATCH 006/112] update all dialogs --- .../change-password/main-dialog.tsx | 34 ++- .../change-password/steps/change.tsx | 4 +- .../change-password/steps/otp.tsx | 4 +- .../change-password/steps/reset.tsx | 4 +- .../setting-dialogs/delete-account.tsx | 61 +++-- .../event/results/attendee-panel/panel.tsx | 25 +- .../src/features/event/results/drawer.tsx | 26 +- .../system-feedback/confirmation/base.tsx | 229 ------------------ .../system-feedback/confirmation/config.ts | 38 --- .../features/system-feedback/dialog/base.tsx | 7 +- .../features/system-feedback/dialog/config.ts | 39 +++ .../system-feedback/dialog/confirmation.tsx | 38 +-- .../features/system-feedback/dialog/form.tsx | 11 +- .../features/system-feedback/dialog/icon.tsx | 40 +++ .../features/system-feedback/dialog/props.ts | 15 +- .../src/features/system-feedback/index.ts | 3 +- 16 files changed, 198 insertions(+), 380 deletions(-) delete mode 100644 frontend/src/features/system-feedback/confirmation/base.tsx delete mode 100644 frontend/src/features/system-feedback/confirmation/config.ts create mode 100644 frontend/src/features/system-feedback/dialog/config.ts create mode 100644 frontend/src/features/system-feedback/dialog/icon.tsx diff --git a/frontend/src/features/account/setting-dialogs/change-password/main-dialog.tsx b/frontend/src/features/account/setting-dialogs/change-password/main-dialog.tsx index 5bed79e14..63826de10 100644 --- a/frontend/src/features/account/setting-dialogs/change-password/main-dialog.tsx +++ b/frontend/src/features/account/setting-dialogs/change-password/main-dialog.tsx @@ -7,7 +7,7 @@ import OtpStep from "@/features/account/setting-dialogs/change-password/steps/ot import ResetStep from "@/features/account/setting-dialogs/change-password/steps/reset"; import { useChangePasswordFlow } from "@/features/account/setting-dialogs/change-password/use-change-password"; import EmptyButton from "@/features/button/components/empty"; -import { ConfirmationDialog } from "@/features/system-feedback"; +import { FormDialog } from "@/features/system-feedback"; import useCheckMobile from "@/lib/hooks/use-check-mobile"; export default function ChangePasswordDialog() { @@ -27,35 +27,45 @@ export default function ChangePasswordDialog() { const displayStep = flow.open ? flow.step : renderedStep; let dialogTitle = ""; - let dialogDescription = null; + let dialogDescriptionText = ""; + let submitLabel = "Save"; let onConfirmHandler = async () => false; + let dialogContent = null; - // Use `displayStep` instead of `flow.step` for the if/else blocks + // Configure dynamic content based on the current step if (displayStep === "CHANGE") { dialogTitle = "Change Your Password"; + dialogDescriptionText = "Secure your account with a new password."; + submitLabel = "Change Password"; onConfirmHandler = flow.handleChangePassword; - dialogDescription = ; + dialogContent = ; } else if (displayStep === "OTP") { dialogTitle = "Enter Reset Code"; - onConfirmHandler = flow.handleVerifyOTP; - dialogDescription = ; + dialogDescriptionText = "Please enter the code sent to your email."; + submitLabel = "Verify Code"; + onConfirmHandler = () => flow.handleVerifyOTP(); + dialogContent = ; } else if (displayStep === "RESET") { dialogTitle = "Reset Password"; + dialogDescriptionText = "Create a new secure password."; + submitLabel = "Reset Password"; onConfirmHandler = flow.handleAuthedReset; - dialogDescription = ; + dialogContent = ; } return ( - } open={flow.open} onOpenChange={flow.handleOpenChange} - onConfirm={onConfirmHandler} + onSubmit={onConfirmHandler} > - - + {dialogContent} + ); } diff --git a/frontend/src/features/account/setting-dialogs/change-password/steps/change.tsx b/frontend/src/features/account/setting-dialogs/change-password/steps/change.tsx index 27748dde7..7227174a7 100644 --- a/frontend/src/features/account/setting-dialogs/change-password/steps/change.tsx +++ b/frontend/src/features/account/setting-dialogs/change-password/steps/change.tsx @@ -5,12 +5,12 @@ import { ChangePasswordStepProps } from "@/features/account/setting-dialogs/chan export default function ChangeStep({ flow }: ChangePasswordStepProps) { return ( -
+

Enter your current password and the new one you would like to replace it with.

-
+
-

We sent a password reset code to your email. Enter the code below!

+
+

Check your email for the password reset code!

+

Enter your new password!

-
+
-
-

- Are you absolutely sure you - want to delete your account? -

-

- This action cannot be undone. -

-
- - { - setCurrentPassword(value); - }} - outlined - error={errors.currentPassword || errors.api} - /> -
- } + description={"Delete Account Confirmation"} + trigger={} open={confirmationOpen} onOpenChange={handleOpenChange} - onConfirm={handleDeleteAccount} + onSubmit={handleDeleteAccount} + submitLabel="Delete Account" > - - +
+

+ Are you absolutely sure you want to + delete your account? +

+

+ This action cannot be undone. +

+
+ + { + setCurrentPassword(value); + }} + outlined + error={errors.currentPassword || errors.api} + /> + ); } diff --git a/frontend/src/features/event/results/attendee-panel/panel.tsx b/frontend/src/features/event/results/attendee-panel/panel.tsx index 592d46d20..7786cde64 100644 --- a/frontend/src/features/event/results/attendee-panel/panel.tsx +++ b/frontend/src/features/event/results/attendee-panel/panel.tsx @@ -63,16 +63,10 @@ export default function AttendeesPanel() { : "Remove Participant" } description={ - personToRemove == currentUser ? ( - "Are you sure you want to remove yourself from this event?" - ) : ( - - Are you sure you want to remove{" "} - {personToRemove}? - - ) + personToRemove === currentUser + ? "Are you sure you want to remove yourself from this event?" + : `Are you sure you want to remove ${personToRemove} from this event?` } - // Controlled Props open={isConfirmationOpen} onOpenChange={setIsConfirmationOpen} onConfirm={async () => { @@ -83,7 +77,18 @@ export default function AttendeesPanel() { } return success; }} - /> + > +
+ {personToRemove === currentUser ? ( + "Are you sure you want to remove yourself from this event?" + ) : ( + + Are you sure you want to remove{" "} + {personToRemove}? + + )} +
+
); } diff --git a/frontend/src/features/event/results/drawer.tsx b/frontend/src/features/event/results/drawer.tsx index 4adc81ce7..dee02bb15 100644 --- a/frontend/src/features/event/results/drawer.tsx +++ b/frontend/src/features/event/results/drawer.tsx @@ -9,7 +9,7 @@ import TimeZoneSelector from "@/features/event/components/selectors/timezone"; import PanelHeader from "@/features/event/results/attendee-panel/panel-header"; import ParticipantList from "@/features/event/results/attendee-panel/participant-list"; import { useResultsContext } from "@/features/event/results/context"; -import ConfirmationDialog from "@/features/system-feedback/confirmation/base"; +import { ConfirmationDialog } from "@/features/system-feedback"; import { tzEqual } from "@/lib/utils/date-time-format"; export default function ResultsDrawer({ @@ -136,14 +136,9 @@ export default function ResultsDrawer({ : "Remove Participant" } description={ - personToRemove == currentUser ? ( - "Are you sure you want to remove yourself from this event?" - ) : ( - - Are you sure you want to remove{" "} - {personToRemove}? - - ) + personToRemove === currentUser + ? "Are you sure you want to remove yourself from this event?" + : `Are you sure you want to remove ${personToRemove} from this event?` } open={isConfirmationOpen} onOpenChange={setIsConfirmationOpen} @@ -155,7 +150,18 @@ export default function ResultsDrawer({ } return success; }} - /> + > +
+ {personToRemove === currentUser ? ( + "Are you sure you want to remove yourself from this event?" + ) : ( + + Are you sure you want to remove{" "} + {personToRemove}? + + )} +
+ ); } diff --git a/frontend/src/features/system-feedback/confirmation/base.tsx b/frontend/src/features/system-feedback/confirmation/base.tsx deleted file mode 100644 index a8a801b41..000000000 --- a/frontend/src/features/system-feedback/confirmation/base.tsx +++ /dev/null @@ -1,229 +0,0 @@ -import React, { useState, useCallback, useEffect } from "react"; - -import * as Dialog from "@radix-ui/react-dialog"; - -import ActionButton from "@/features/button/components/action"; -import { FloatingDrawer } from "@/features/drawer"; -import { DIALOG_CONFIG } from "@/features/system-feedback/confirmation/config"; -import { ConfirmationDialogType } from "@/features/system-feedback/type"; -import { cn } from "@/lib/utils/classname"; - -type ConfirmationDialogProps = { - type: ConfirmationDialogType; - title: string; - description: React.ReactNode; - onConfirm: () => boolean | Promise; - children?: React.ReactNode; - disabled?: boolean; - showIcon?: boolean; - autoClose?: boolean; - asNestedDrawer?: boolean; - - // controlled props - // (for when the dialog needs to be controlled by the parent component) - open?: boolean; - onOpenChange?: (open: boolean) => void; -}; - -export default function ConfirmationDialog({ - type, - title, - description, - onConfirm, - children: triggerElement, - disabled = false, - showIcon = false, - autoClose = false, - asNestedDrawer = false, - open: controlledOpen, - onOpenChange, -}: ConfirmationDialogProps) { - const [internalOpen, setInternalOpen] = useState(false); - - // use controlled state if provided, otherwise local state - const isControlled = controlledOpen !== undefined; - const open = isControlled ? controlledOpen : internalOpen; - - const handleOpenChange = useCallback( - (newOpen: boolean) => { - if (!isControlled) { - setInternalOpen(newOpen); - } - onOpenChange?.(newOpen); - }, - [isControlled, onOpenChange], - ); - - const handleClose = useCallback(() => { - handleOpenChange(false); - }, [handleOpenChange]); - - const handleConfirm = useCallback(async () => { - // If autoClose is enabled, we optimistically close - // the dialog before calling onConfirm - if (autoClose) { - handleOpenChange(false); - onConfirm(); - return true; - } - - const success = await onConfirm(); - if (success) { - handleOpenChange(false); - } - return success; - }, [autoClose, onConfirm, handleOpenChange]); - - useEffect(() => { - // If the dialog is not open, then don't add the keydown listener - if (!open) return; - - // Add keydown listener for Enter and Escape keys to trigger confirm or cancel - // actions. - const handleKeyDown = (e: KeyboardEvent) => { - // Ignore key events coming from text inputs or editable elements - const target = e.target as HTMLElement | null; - const tagName = target?.tagName; - const isMultilineText = - tagName === "TEXTAREA" || target?.isContentEditable; - if (isMultilineText) { - return; - } - - if (e.key === "Escape") handleClose(); - else if (e.key === "Enter") handleConfirm(); - }; - - window.addEventListener("keydown", handleKeyDown); - - return () => window.removeEventListener("keydown", handleKeyDown); - }, [handleClose, handleConfirm, open]); - - const config = DIALOG_CONFIG[type] || DIALOG_CONFIG.info; - const Icon = config.icon; - - const renderIcon = () => { - if (config.isTextIcon) { - return ( -
-
!
-
- ); - } - return ( -
- {Icon && } -
- ); - }; - - if (asNestedDrawer) { - return ( - } - > -
-
- {showIcon && renderIcon()} -

{title}

-
-
- {description} -
-
- - -
-
-
- ); - } - - return ( - - {triggerElement && ( - { - if (disabled) { - e.preventDefault(); - e.stopPropagation(); - } - }} - aria-disabled={disabled} - > - {triggerElement} - - )} - - - - -
- -
- {showIcon && renderIcon()} -

{title}

-
-
- - -
{description}
-
- -
- - -
-
-
-
-
- ); -} diff --git a/frontend/src/features/system-feedback/confirmation/config.ts b/frontend/src/features/system-feedback/confirmation/config.ts deleted file mode 100644 index c45ed448b..000000000 --- a/frontend/src/features/system-feedback/confirmation/config.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { - CheckIcon, - InfoIcon, - TriangleAlertIcon, -} from "lucide-react"; - -export const DIALOG_CONFIG = { - warning: { - icon: null, // special case for the text "!" - bgClass: "bg-lion", - btnStyle: "primary", - isTextIcon: true, - }, - delete: { - icon: TriangleAlertIcon, - bgClass: "bg-error/40", - btnStyle: "danger", - isTextIcon: false, - }, - success: { - icon: CheckIcon, - bgClass: "bg-foreground/40", - btnStyle: "primary", - isTextIcon: false, - }, - error: { - icon: TriangleAlertIcon, - bgClass: "bg-error/40", - btnStyle: "danger", - isTextIcon: false, - }, - info: { - icon: InfoIcon, - bgClass: "bg-blue/40", - btnStyle: "primary", - isTextIcon: false, - }, -} as const; diff --git a/frontend/src/features/system-feedback/dialog/base.tsx b/frontend/src/features/system-feedback/dialog/base.tsx index 1983244e1..9a9d6c0e6 100644 --- a/frontend/src/features/system-feedback/dialog/base.tsx +++ b/frontend/src/features/system-feedback/dialog/base.tsx @@ -7,7 +7,6 @@ import { BaseDialogProps } from "@/features/system-feedback/dialog/props"; import { cn } from "@/lib/utils/classname"; export default function BaseDialog({ - type, title, description, trigger, @@ -17,6 +16,7 @@ export default function BaseDialog({ asNestedDrawer = false, triggerDisabled = false, icon, + overlayClassName, }: BaseDialogProps) { /* OPEN STATE MANAGEMENT */ const [internalOpen, setInternalOpen] = useState(false); @@ -77,15 +77,14 @@ export default function BaseDialog({
diff --git a/frontend/src/features/system-feedback/dialog/config.ts b/frontend/src/features/system-feedback/dialog/config.ts new file mode 100644 index 000000000..077fa0c92 --- /dev/null +++ b/frontend/src/features/system-feedback/dialog/config.ts @@ -0,0 +1,39 @@ +import type { ComponentType } from "react"; + +import { CheckIcon, InfoIcon, TriangleAlertIcon } from "lucide-react"; + +import { ButtonStyle } from "@/features/button/props"; + +export type DialogConfig = { + icon: ComponentType<{ className?: string }> | null; + iconStyle: string; + buttonStyle: ButtonStyle; +}; + +export const DIALOG_CONFIG: Record = { + warning: { + icon: null, // special case for the text "!" + iconStyle: "bg-lion", + buttonStyle: "primary", + }, + delete: { + icon: TriangleAlertIcon, + iconStyle: "bg-error/40", + buttonStyle: "danger", + }, + success: { + icon: CheckIcon, + iconStyle: "bg-foreground/40", + buttonStyle: "primary", + }, + error: { + icon: TriangleAlertIcon, + iconStyle: "bg-error/40", + buttonStyle: "danger", + }, + info: { + icon: InfoIcon, + iconStyle: "bg-blue/40", + buttonStyle: "primary", + }, +} as const; diff --git a/frontend/src/features/system-feedback/dialog/confirmation.tsx b/frontend/src/features/system-feedback/dialog/confirmation.tsx index e24b59bba..ec3587b7d 100644 --- a/frontend/src/features/system-feedback/dialog/confirmation.tsx +++ b/frontend/src/features/system-feedback/dialog/confirmation.tsx @@ -1,8 +1,8 @@ import { useCallback, useEffect } from "react"; import ActionButton from "@/features/button/components/action"; -import { DIALOG_CONFIG } from "@/features/system-feedback/confirmation/config"; import BaseModal from "@/features/system-feedback/dialog/base"; +import { DIALOG_CONFIG } from "@/features/system-feedback/dialog/config"; import { ConfirmationDialogProps } from "@/features/system-feedback/dialog/props"; import { cn } from "@/lib/utils/classname"; @@ -14,12 +14,13 @@ export default function ConfirmationDialog({ children, trigger, triggerDisabled = false, - showIcon = false, autoClose = false, asNestedDrawer = false, open, onOpenChange, }: ConfirmationDialogProps) { + const config = DIALOG_CONFIG[type] || DIALOG_CONFIG.info; + const handleConfirm = useCallback(async () => { if (autoClose) { onOpenChange?.(false); @@ -43,34 +44,8 @@ export default function ConfirmationDialog({ return () => window.removeEventListener("keydown", handleKeyDown); }, [handleConfirm, open]); - /* ICON RENDERING */ - const config = DIALOG_CONFIG[type] || DIALOG_CONFIG.info; - const Icon = config.icon; - - const renderIcon = () => { - if (!showIcon) return null; - if (config.isTextIcon) { - return ( -
-
!
-
- ); - } - return ( -
- {Icon && } -
- ); - }; - return ( {children}
@@ -88,7 +66,7 @@ export default function ConfirmationDialog({ onClick={() => onOpenChange?.(false)} /> { console.log("Form submitted"); @@ -33,7 +37,6 @@ export default function FormDialog({ return (
diff --git a/frontend/src/features/system-feedback/dialog/icon.tsx b/frontend/src/features/system-feedback/dialog/icon.tsx new file mode 100644 index 000000000..eb81ad0cc --- /dev/null +++ b/frontend/src/features/system-feedback/dialog/icon.tsx @@ -0,0 +1,40 @@ +import { DialogType } from "@/features/system-feedback/type"; +import { cn } from "@/lib/utils/classname"; + +type DialogIconProps = { + dialogConfig: { + icon: React.ComponentType<{ className?: string }>; + iconStyle: string; + }; + type?: DialogType; + showIcon?: boolean; +}; + +export default function DialogIcon({ + dialogConfig, + type = "info", + showIcon = true, +}: DialogIconProps) { + if (!showIcon) return null; + + const Icon = dialogConfig.icon; + + if (type === "warning") { + return ( +
+
!
+
+ ); + } + + return ( +
+ {Icon && } +
+ ); +} diff --git a/frontend/src/features/system-feedback/dialog/props.ts b/frontend/src/features/system-feedback/dialog/props.ts index 7387d373c..7b07b7133 100644 --- a/frontend/src/features/system-feedback/dialog/props.ts +++ b/frontend/src/features/system-feedback/dialog/props.ts @@ -1,12 +1,6 @@ import { DialogType } from "@/features/system-feedback"; type CommonDialogProps = { - /** - * Type of the dialog, which determines its styling and default icon. - * - * @type {DialogType} - */ - type: DialogType; /** * Title of the dialog * Required for accessibility (used in Title components and aria attributes) @@ -75,11 +69,19 @@ type CommonDialogProps = { * @default undefined */ icon?: React.ReactNode; + /** + * Additional className to apply to the dialog overlay, for further customization. + * + * @type {string} + * @default undefined + */ + overlayClassName?: string; }; export type BaseDialogProps = CommonDialogProps; export type ConfirmationDialogProps = Omit & { + type: DialogType; /** * Enforced strictly as a string for semantic standard confirmations. */ @@ -96,6 +98,7 @@ export type ConfirmationDialogProps = Omit & { }; export type FormDialogProps = CommonDialogProps & { + type: DialogType; /** * Callback function called when the native is submitted. * Returning true (or a Promise resolving to true) typically closes the modal. */ diff --git a/frontend/src/features/system-feedback/index.ts b/frontend/src/features/system-feedback/index.ts index f43c63a95..746a8df6d 100644 --- a/frontend/src/features/system-feedback/index.ts +++ b/frontend/src/features/system-feedback/index.ts @@ -1,7 +1,8 @@ // Export Components export { Banner } from "@/features/system-feedback/banner/base"; export { default as RateLimitBanner } from "@/features/system-feedback/banner/rate-limit"; -export { default as ConfirmationDialog } from "@/features/system-feedback/confirmation/base"; +export { default as ConfirmationDialog } from "@/features/system-feedback/dialog/confirmation"; +export { default as FormDialog } from "@/features/system-feedback/dialog/form"; export { default as ToastProvider } from "@/features/system-feedback/toast/provider"; export { useToast } from "@/features/system-feedback/toast/context"; From 2cddbdbbf9e0889ffa34f54d061526ffc7838d23 Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Fri, 17 Apr 2026 10:50:32 -0400 Subject: [PATCH 007/112] hide close button --- frontend/src/features/drawer/components/base.tsx | 3 ++- frontend/src/features/drawer/props.ts | 6 ++++++ frontend/src/features/system-feedback/dialog/base.tsx | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/frontend/src/features/drawer/components/base.tsx b/frontend/src/features/drawer/components/base.tsx index 7f1d0650f..c39a1dc32 100644 --- a/frontend/src/features/drawer/components/base.tsx +++ b/frontend/src/features/drawer/components/base.tsx @@ -27,6 +27,7 @@ export default function BaseDrawer({ modal = true, showOverlay = !frostedGlass && modal, nested = false, + hideCloseButton = false, ...rest }: DrawerProps) { useDrawerResize(); @@ -235,7 +236,7 @@ export default function BaseDrawer({
- {_type !== "morphing" && ( + {_type !== "morphing" && !hideCloseButton && (
} + hideCloseButton >
{icon} From 9ad3e78e58b0c1c2e8afff8b7420819b8f99f95f Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Sat, 18 Apr 2026 12:07:44 -0400 Subject: [PATCH 008/112] refactor header logic --- frontend/src/app/layout.tsx | 2 +- .../account.tsx} | 79 +++++----- .../dashboard.tsx} | 2 +- .../header/components/buttons/login.tsx | 15 ++ .../new-event.tsx} | 4 +- .../shrinking-header.tsx} | 0 .../components/{ => buttons}/theme-toggle.tsx | 4 +- .../src/features/header/components/header.tsx | 135 ++---------------- .../features/header/components/logo-area.tsx | 1 - .../header/components/shrinking-header.tsx | 124 ++++++++++++++++ 10 files changed, 192 insertions(+), 174 deletions(-) rename frontend/src/features/header/components/{account-button.tsx => buttons/account.tsx} (52%) rename frontend/src/features/header/components/{dashboard-button.tsx => buttons/dashboard.tsx} (95%) create mode 100644 frontend/src/features/header/components/buttons/login.tsx rename frontend/src/features/header/components/{new-event-button.tsx => buttons/new-event.tsx} (95%) rename frontend/src/features/header/components/{shrinking-header-button.tsx => buttons/shrinking-header.tsx} (100%) rename frontend/src/features/header/components/{ => buttons}/theme-toggle.tsx (94%) create mode 100644 frontend/src/features/header/components/shrinking-header.tsx diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 6d07cb69a..bf1d70bd2 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -2,7 +2,7 @@ import { Analytics } from "@vercel/analytics/next"; import type { Metadata } from "next"; import { Modak, Nunito } from "next/font/google"; -import Header from "@/components/header/header"; +import Header from "@/features/header/components/header"; import { Providers } from "@/lib/providers"; import "@/styles/globals.css"; diff --git a/frontend/src/features/header/components/account-button.tsx b/frontend/src/features/header/components/buttons/account.tsx similarity index 52% rename from frontend/src/features/header/components/account-button.tsx rename to frontend/src/features/header/components/buttons/account.tsx index d6b445243..be214a8b5 100644 --- a/frontend/src/features/header/components/account-button.tsx +++ b/frontend/src/features/header/components/buttons/account.tsx @@ -4,11 +4,11 @@ import { LogOutIcon, UserIcon } from "lucide-react"; import { useRouter } from "next/navigation"; import KebabMenu from "@/components/kebab-menu"; -import { getSession } from "@/features/account/get-session"; +import { AccountDetails } from "@/features/account/type"; import ActionButton from "@/features/button/components/action"; import EmptyButton from "@/features/button/components/empty"; import LinkButton from "@/features/button/components/link"; -import ShrinkingHeaderButton from "@/features/header/components/shrinking-header-button"; +import ShrinkingHeaderButton from "@/features/header/components/buttons/shrinking-header"; import { useHeaderSize } from "@/features/header/context"; import { useToast } from "@/features/system-feedback"; import { MESSAGES } from "@/lib/messages"; @@ -16,9 +16,12 @@ import { clientPost } from "@/lib/utils/api/client-fetch"; import { ROUTES } from "@/lib/utils/api/endpoints"; import { ApiErrorResponse } from "@/lib/utils/api/fetch-wrapper"; -export default function AccountButton() { +export default function AccountButton({ + accountDetails, +}: { + accountDetails: AccountDetails; +}) { const { activeMenu, setActiveMenu } = useHeaderSize(); - const { loginState, logout, accountDetails } = useAccount(); const router = useRouter(); const { addToast } = useToast(); @@ -27,13 +30,14 @@ export default function AccountButton() { const signOut = async () => { try { await clientPost(ROUTES.auth.logout); - redirect("/login"); - // cannot use toast after redirect bc toast does not work on server components + addToast("success", MESSAGES.SUCCESS_LOGOUT); + + router.push("/login"); + router.refresh(); } catch (e) { const error = e as ApiErrorResponse; console.error("Logout error:", error); - // addToast("error", error.formattedMessage); - return false; + addToast("error", error.formattedMessage); } }; @@ -55,43 +59,30 @@ export default function AccountButton() { /> ); - if (accountDetails) { - return ( - } - > - setActiveMenu(isOpen ? "account" : null)} - trigger={ - } - aria-label="Account settings" - /> - } - > -

- {accountDetails?.email} -

- {accountSettingsButton} - {signOutButton} -
-
- ); - } - return ( - - + } + > + setActiveMenu(isOpen ? "account" : null)} + trigger={ + } + aria-label="Account settings" + /> + } + > +

+ {accountDetails?.email} +

+ {accountSettingsButton} + {signOutButton} +
); } diff --git a/frontend/src/features/header/components/dashboard-button.tsx b/frontend/src/features/header/components/buttons/dashboard.tsx similarity index 95% rename from frontend/src/features/header/components/dashboard-button.tsx rename to frontend/src/features/header/components/buttons/dashboard.tsx index 741f3332a..592fa738c 100644 --- a/frontend/src/features/header/components/dashboard-button.tsx +++ b/frontend/src/features/header/components/buttons/dashboard.tsx @@ -1,7 +1,7 @@ import { LayoutDashboardIcon } from "lucide-react"; import LinkButton from "@/features/button/components/link"; -import ShrinkingHeaderButton from "@/features/header/components/shrinking-header-button"; +import ShrinkingHeaderButton from "@/features/header/components/buttons/shrinking-header"; export default function DashboardButton() { return ( diff --git a/frontend/src/features/header/components/buttons/login.tsx b/frontend/src/features/header/components/buttons/login.tsx new file mode 100644 index 000000000..11f997d9f --- /dev/null +++ b/frontend/src/features/header/components/buttons/login.tsx @@ -0,0 +1,15 @@ +import LinkButton from "@/features/button/components/link"; +import ShrinkingHeaderButton from "@/features/header/components/buttons/shrinking-header"; + +export default function LoginButton() { + return ( + + + + ); +} diff --git a/frontend/src/features/header/components/new-event-button.tsx b/frontend/src/features/header/components/buttons/new-event.tsx similarity index 95% rename from frontend/src/features/header/components/new-event-button.tsx rename to frontend/src/features/header/components/buttons/new-event.tsx index f44f4bf36..b9e8129da 100644 --- a/frontend/src/features/header/components/new-event-button.tsx +++ b/frontend/src/features/header/components/buttons/new-event.tsx @@ -1,11 +1,9 @@ -"use client"; - import { PlusIcon } from "lucide-react"; import { usePathname } from "next/navigation"; import LinkButton from "@/features/button/components/link"; import { ButtonStyle } from "@/features/button/props"; -import ShrinkingHeaderButton from "@/features/header/components/shrinking-header-button"; +import ShrinkingHeaderButton from "@/features/header/components/buttons/shrinking-header"; export default function NewEventButton() { const pathname = usePathname(); diff --git a/frontend/src/features/header/components/shrinking-header-button.tsx b/frontend/src/features/header/components/buttons/shrinking-header.tsx similarity index 100% rename from frontend/src/features/header/components/shrinking-header-button.tsx rename to frontend/src/features/header/components/buttons/shrinking-header.tsx diff --git a/frontend/src/features/header/components/theme-toggle.tsx b/frontend/src/features/header/components/buttons/theme-toggle.tsx similarity index 94% rename from frontend/src/features/header/components/theme-toggle.tsx rename to frontend/src/features/header/components/buttons/theme-toggle.tsx index 113bda349..39df86a9a 100644 --- a/frontend/src/features/header/components/theme-toggle.tsx +++ b/frontend/src/features/header/components/buttons/theme-toggle.tsx @@ -1,10 +1,8 @@ -"use client"; - import { MoonIcon, SunIcon } from "lucide-react"; import { useTheme } from "next-themes"; import ActionButton from "@/features/button/components/action"; -import ShrinkingHeaderButton from "@/features/header/components/shrinking-header-button"; +import ShrinkingHeaderButton from "@/features/header/components/buttons/shrinking-header"; export default function ThemeToggle() { const { setTheme, resolvedTheme } = useTheme(); diff --git a/frontend/src/features/header/components/header.tsx b/frontend/src/features/header/components/header.tsx index 1aad1b1cc..635be99f9 100644 --- a/frontend/src/features/header/components/header.tsx +++ b/frontend/src/features/header/components/header.tsx @@ -1,126 +1,19 @@ -"use client"; +import { getSession } from "@/features/account/get-session"; +import AccountButton from "@/features/header/components/buttons/account"; +import LoginButton from "@/features/header/components/buttons/login"; +import ShrinkingHeader from "@/features/header/components/shrinking-header"; -import { useEffect, useRef, useState } from "react"; - -import { motion } from "framer-motion"; - -import AccountButton from "@/features/header/components/account-button"; -import DashboardButton from "@/features/header/components/dashboard-button"; -import LogoArea from "@/features/header/components/logo-area"; -import NewEventButton from "@/features/header/components/new-event-button"; -import ThemeToggle from "@/features/header/components/theme-toggle"; -import { useHeaderSize } from "@/features/header/context"; -import useCheckMobile from "@/lib/hooks/use-check-mobile"; -import { cn } from "@/lib/utils/classname"; - -const SCROLL_THRESHOLD = 50; - -export default function Header() { - const [mounted, setMounted] = useState(false); - - const isMobile = useCheckMobile(); - const lastScrollPoint = useRef(0); - const scrollCheckpoint = useRef(0); - - const { isShrunk, heightClass, shrink, expand, activeMenu } = useHeaderSize(); - - useEffect(() => { - setMounted(true); - - if (!isMobile) { - expand(); - return; - } - - const handleScroll = () => { - const currentScrollPoint = Math.min( - Math.max(window.scrollY, 0), - document.documentElement.scrollHeight - window.innerHeight, - ); - const scrollingDown = currentScrollPoint > lastScrollPoint.current; - lastScrollPoint.current = currentScrollPoint; - - if (currentScrollPoint <= 0) { - expand(); - return; - } - - if (isShrunk) { - if (scrollingDown) { - scrollCheckpoint.current = currentScrollPoint; - } else if ( - currentScrollPoint < - scrollCheckpoint.current - SCROLL_THRESHOLD - ) { - expand(); - } - } else { - if (!scrollingDown) { - scrollCheckpoint.current = currentScrollPoint; - } else if ( - currentScrollPoint > - scrollCheckpoint.current + SCROLL_THRESHOLD - ) { - shrink(); - } - } - }; - - window.addEventListener("scroll", handleScroll); - return () => { - window.removeEventListener("scroll", handleScroll); - }; - }, [isMobile, isShrunk, shrink, expand]); - - if (!mounted) { - return null; - } +export default async function Header() { + const session = await getSession(); + const isLoggedIn = session !== null; return ( -
- -
+ + {isLoggedIn ? ( + + ) : ( + + )} + ); } diff --git a/frontend/src/features/header/components/logo-area.tsx b/frontend/src/features/header/components/logo-area.tsx index b1a2ef0fb..d0440c00b 100644 --- a/frontend/src/features/header/components/logo-area.tsx +++ b/frontend/src/features/header/components/logo-area.tsx @@ -8,7 +8,6 @@ import { cn } from "@/lib/utils/classname"; export default function LogoArea({ isShrunk = false }: { isShrunk?: boolean }) { return (
- {/* Text Container */} diff --git a/frontend/src/features/header/components/shrinking-header.tsx b/frontend/src/features/header/components/shrinking-header.tsx new file mode 100644 index 000000000..d57f4c3b1 --- /dev/null +++ b/frontend/src/features/header/components/shrinking-header.tsx @@ -0,0 +1,124 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; + +import { motion } from "framer-motion"; + +import DashboardButton from "@/features/header/components/buttons/dashboard"; +import NewEventButton from "@/features/header/components/buttons/new-event"; +import ThemeToggle from "@/features/header/components/buttons/theme-toggle"; +import LogoArea from "@/features/header/components/logo-area"; +import { useHeaderSize } from "@/features/header/context"; +import useCheckMobile from "@/lib/hooks/use-check-mobile"; +import { cn } from "@/lib/utils/classname"; + +const SCROLL_THRESHOLD = 50; + +export default function ShrinkingHeader({ + children: accountButton, +}: { + children: React.ReactNode; +}) { + const [mounted, setMounted] = useState(false); + + const isMobile = useCheckMobile(); + const lastScrollPoint = useRef(0); + const scrollCheckpoint = useRef(0); + + const { isShrunk, heightClass, shrink, expand, activeMenu } = useHeaderSize(); + + useEffect(() => { + setMounted(true); + + if (!isMobile) { + expand(); + return; + } + + const handleScroll = () => { + const currentScrollPoint = Math.min( + Math.max(window.scrollY, 0), + document.documentElement.scrollHeight - window.innerHeight, + ); + const scrollingDown = currentScrollPoint > lastScrollPoint.current; + lastScrollPoint.current = currentScrollPoint; + + if (currentScrollPoint <= 0) { + expand(); + return; + } + + if (isShrunk) { + if (scrollingDown) { + scrollCheckpoint.current = currentScrollPoint; + } else if ( + currentScrollPoint < + scrollCheckpoint.current - SCROLL_THRESHOLD + ) { + expand(); + } + } else { + if (!scrollingDown) { + scrollCheckpoint.current = currentScrollPoint; + } else if ( + currentScrollPoint > + scrollCheckpoint.current + SCROLL_THRESHOLD + ) { + shrink(); + } + } + }; + + window.addEventListener("scroll", handleScroll); + return () => { + window.removeEventListener("scroll", handleScroll); + }; + }, [isMobile, isShrunk, shrink, expand]); + + if (!mounted) return null; + + return ( +
+ +
+ ); +} From c07087227976ce6b6b6a1f5c20c84f4374b8ed55 Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Sat, 18 Apr 2026 12:13:03 -0400 Subject: [PATCH 009/112] fix new event icon blip --- .../features/header/components/buttons/shrinking-header.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/features/header/components/buttons/shrinking-header.tsx b/frontend/src/features/header/components/buttons/shrinking-header.tsx index 542150711..b9f596170 100644 --- a/frontend/src/features/header/components/buttons/shrinking-header.tsx +++ b/frontend/src/features/header/components/buttons/shrinking-header.tsx @@ -18,10 +18,9 @@ export default function ShrinkingHeaderButton({ label?: string; children: React.ReactNode; }) { - const [showButton, setShowButton] = useState(false); - const buttonShowTimeout = useRef(null); - const { isShrunk } = useHeaderSize(); + const [showButton, setShowButton] = useState(!isShrunk); + const buttonShowTimeout = useRef(null); useEffect(() => { if (buttonShowTimeout.current) { From 62c7680d132ea60db81dde0c1299fa567f48dff0 Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Sat, 18 Apr 2026 12:27:55 -0400 Subject: [PATCH 010/112] expand dashboard loading --- frontend/src/app/dashboard/loading.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/dashboard/loading.tsx b/frontend/src/app/dashboard/loading.tsx index 2a7a2f3ac..a0abdf5b1 100644 --- a/frontend/src/app/dashboard/loading.tsx +++ b/frontend/src/app/dashboard/loading.tsx @@ -6,7 +6,23 @@ export default function Loading() {

Dashboard

-
+
+
+
+
+ + {/* 2. Skeleton for the Event Grid */} +
+
+ {[1, 2, 3].map((i) => ( +
+ ))} +
+
+
); } From 5fb0150cd82837524c285e4a9a0d8ba58afd35bc Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Sun, 19 Apr 2026 10:14:29 -0400 Subject: [PATCH 011/112] create url toast listener --- frontend/src/app/layout.tsx | 6 +++ .../system-feedback/toast/listener.tsx | 44 +++++++++++++++++++ frontend/src/middleware.ts | 11 +++-- 3 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 frontend/src/features/system-feedback/toast/listener.tsx diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index bf1d70bd2..0306263de 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,8 +1,11 @@ +import { Suspense } from "react"; + import { Analytics } from "@vercel/analytics/next"; import type { Metadata } from "next"; import { Modak, Nunito } from "next/font/google"; import Header from "@/features/header/components/header"; +import ToastListener from "@/features/system-feedback/toast/listener"; import { Providers } from "@/lib/providers"; import "@/styles/globals.css"; @@ -76,6 +79,9 @@ export default async function RootLayout({
+ + +
{children} diff --git a/frontend/src/features/system-feedback/toast/listener.tsx b/frontend/src/features/system-feedback/toast/listener.tsx new file mode 100644 index 000000000..725490ec4 --- /dev/null +++ b/frontend/src/features/system-feedback/toast/listener.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { useEffect } from "react"; + +import { useSearchParams } from "next/navigation"; + +import { useToast } from "@/features/system-feedback"; +import { MESSAGES } from "@/lib/messages"; + +export default function ToastListener() { + const searchParams = useSearchParams(); + const { addToast } = useToast(); + + /** + * This effect listens for specific query parameters that we set in the middleware + * when redirecting users: + * + * - "unauthorized": set when a user tries to access a protected page without + * being logged in + * - "alreadyLoggedIn": set when a user tries to access the login page while they + * are already logged in + * + * If it sees either of those, it fires the appropriate toast and then removes the + * query parameter from the URL to prevent duplicate toasts on page refresh. + */ + useEffect(() => { + const isUnauthorized = searchParams.get("unauthorized") === "true"; + const isAlreadyLoggedIn = searchParams.get("alreadyLoggedIn") === "true"; + if (!isUnauthorized && !isAlreadyLoggedIn) return; + + const message = isUnauthorized + ? MESSAGES.INFO_NOT_LOGGED_IN + : MESSAGES.INFO_ALREADY_LOGGED_IN; + addToast("info", message); + + // Clean up URL + const paramToRemove = isUnauthorized ? "unauthorized" : "alreadyLoggedIn"; + const newUrl = new URL(window.location.href); + newUrl.searchParams.delete(paramToRemove); + window.history.replaceState({}, "", newUrl.toString()); + }, [searchParams, addToast]); + + return null; +} diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts index 44889c1ff..1b4431db7 100644 --- a/frontend/src/middleware.ts +++ b/frontend/src/middleware.ts @@ -17,22 +17,25 @@ export function middleware(request: NextRequest) { const hasAccountSessToken = request.cookies.has("account_sess_token"); const isAuthRoute = authRoutes.some((route) => path.startsWith(route)); - const isPretectedRoute = protectedRoutes.some((route) => + const isProtectedRoute = protectedRoutes.some((route) => path.startsWith(route), ); // If the user is logged in and tries to access an auth route, redirect them - // to the dashboard. + // to the dashboard and attach a flag. if (hasAccountSessToken && isAuthRoute) { - return NextResponse.redirect(new URL("/dashboard", request.nextUrl)); + const dashboardUrl = new URL("/dashboard", request.nextUrl); + dashboardUrl.searchParams.set("alreadyLoggedIn", "true"); + return NextResponse.redirect(dashboardUrl); } // If the user is not logged in and tries to access a protected route (like // settings), redirect them to the login page. A callbackUrl is included so users // can be redirected back after logging in. - if (!hasAccountSessToken && isPretectedRoute) { + if (!hasAccountSessToken && isProtectedRoute) { const loginUrl = new URL("/login", request.nextUrl); loginUrl.searchParams.set("callbackUrl", path); + loginUrl.searchParams.set("unauthorized", "true"); return NextResponse.redirect(loginUrl); } From aafef6b18cec987e19bb9cf6e1f7f507f323e119 Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Sun, 19 Apr 2026 10:29:03 -0400 Subject: [PATCH 012/112] move get-session to global lib --- frontend/src/app/dashboard/page.tsx | 2 +- frontend/src/app/settings/layout.tsx | 2 +- .../src/features/header/components/header.tsx | 2 +- .../account => lib/utils}/get-session.ts | 17 ++++++++++++++--- 4 files changed, 17 insertions(+), 6 deletions(-) rename frontend/src/{features/account => lib/utils}/get-session.ts (52%) diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx index 3fd207e5a..23371db31 100644 --- a/frontend/src/app/dashboard/page.tsx +++ b/frontend/src/app/dashboard/page.tsx @@ -1,13 +1,13 @@ import { Metadata } from "next"; import ClientPage from "@/app/dashboard/page-client"; -import { getSession } from "@/features/account/get-session"; import { ROUTES } from "@/lib/utils/api/endpoints"; import { ApiErrorResponse } from "@/lib/utils/api/fetch-wrapper"; import handleErrorResponse from "@/lib/utils/api/handle-api-error"; import { processDashboardData } from "@/lib/utils/api/processors/process-dashboard-data"; import { serverGet } from "@/lib/utils/api/server-fetch"; import { constructMetadata } from "@/lib/utils/construct-metadata"; +import { getSession } from "@/lib/utils/get-session"; // Explicitly set this page to be dynamic so Next.js doesn't try to statically build it export const dynamic = "force-dynamic"; diff --git a/frontend/src/app/settings/layout.tsx b/frontend/src/app/settings/layout.tsx index 76ab8e228..5b14586bf 100644 --- a/frontend/src/app/settings/layout.tsx +++ b/frontend/src/app/settings/layout.tsx @@ -1,11 +1,11 @@ import { Metadata } from "next"; import { redirect } from "next/navigation"; -import { getSession } from "@/features/account/get-session"; import { SettingsProvider } from "@/features/account/settings/context"; import SettingsNav from "@/features/account/settings/sidebar-nav"; import HeaderSpacer from "@/features/header/components/header-spacer"; import { constructMetadata } from "@/lib/utils/construct-metadata"; +import { getSession } from "@/lib/utils/get-session"; export function generateMetadata(): Metadata { return constructMetadata( diff --git a/frontend/src/features/header/components/header.tsx b/frontend/src/features/header/components/header.tsx index 635be99f9..e0f02481c 100644 --- a/frontend/src/features/header/components/header.tsx +++ b/frontend/src/features/header/components/header.tsx @@ -1,7 +1,7 @@ -import { getSession } from "@/features/account/get-session"; import AccountButton from "@/features/header/components/buttons/account"; import LoginButton from "@/features/header/components/buttons/login"; import ShrinkingHeader from "@/features/header/components/shrinking-header"; +import { getSession } from "@/lib/utils/get-session"; export default async function Header() { const session = await getSession(); diff --git a/frontend/src/features/account/get-session.ts b/frontend/src/lib/utils/get-session.ts similarity index 52% rename from frontend/src/features/account/get-session.ts rename to frontend/src/lib/utils/get-session.ts index 2f8130a6c..5e026af60 100644 --- a/frontend/src/features/account/get-session.ts +++ b/frontend/src/lib/utils/get-session.ts @@ -8,7 +8,15 @@ import { ROUTES } from "@/lib/utils/api/endpoints"; import { ApiErrorResponse } from "@/lib/utils/api/fetch-wrapper"; import { serverGet } from "@/lib/utils/api/server-fetch"; -// 2. Wrap your entire async function in cache() +/** + * This function retrieves the current user's session information by checking for + * the presence of an authentication cookie and then making a server-side API call + * to validate the session and fetch the user's account details. + * + * It is wrapped inReact's `cache` function to optimize performance by caching the + * result of the session retrieval, but it is designed to bypass caching when + * necessary to ensure that it always returns the correct session data for each user. + */ export const getSession = cache(async (): Promise => { const cookieString = await getAuthCookieString(); console.log("Retrieved cookie string:", cookieString); @@ -19,8 +27,11 @@ export const getSession = cache(async (): Promise => { try { const data = await serverGet(ROUTES.auth.checkAccountAuth, undefined, { - // Keep this! It stops Next.js from aggressively caching the - // result across DIFFERENT users/requests. + // By default, Next.js may cache the result of this function and serve it to + // multiple users, which is a problem because this function returns + // user-specific data. By setting cache: "no-store", we ensure that Next.js + // does not cache the result and instead calls this function on every request, + // allowing it to return the correct session data for each user. cache: "no-store", }); return { From 7d16cd66829a7fe286a19f5f8e78604fe0dcfc7a Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Sun, 19 Apr 2026 10:40:48 -0400 Subject: [PATCH 013/112] update painting page to use server auth --- .../[event-code]/painting/page-client.tsx | 28 +++++++++---------- .../(event)/[event-code]/painting/page.tsx | 3 ++ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/frontend/src/app/(event)/[event-code]/painting/page-client.tsx b/frontend/src/app/(event)/[event-code]/painting/page-client.tsx index 1e3fa4ce8..ad65739e0 100644 --- a/frontend/src/app/(event)/[event-code]/painting/page-client.tsx +++ b/frontend/src/app/(event)/[event-code]/painting/page-client.tsx @@ -10,7 +10,7 @@ import Checkbox from "@/components/checkbox"; import MobileFooterTray from "@/components/mobile-footer-tray"; import { useAvailability } from "@/core/availability/use-availability"; import { EventRange } from "@/core/event/types"; -import { useAccount } from "@/features/account/context"; +import { AccountDetails } from "@/features/account/type"; import ActionButton from "@/features/button/components/action"; import LinkButton from "@/features/button/components/link"; import { validateAvailabilityData } from "@/features/event/availability/validate-data"; @@ -37,12 +37,14 @@ export default function ClientPage({ eventRange, timeslots, initialData, + accountDetails, }: { eventCode: string; eventName: string; eventRange: EventRange; timeslots: Date[]; initialData: SelfAvailability | null; + accountDetails: AccountDetails | null; }) { const router = useRouter(); @@ -124,19 +126,21 @@ export default function ClientPage({ const [saveDefaultName, setSaveDefaultName] = useState(false); // DEFAULT NAME APPLICATION - // This also accounts for the situation where a user directly opens the painting page - // instead of coming from the results page. - const { loginState, accountDetails, login } = useAccount(); // If editing, don't try to autofill the name const nameInitialized = useRef(!!initialData); useEffect(() => { - if (nameInitialized.current) return; - if (loginState !== "logged_in") return; - if (!accountDetails || !accountDetails.defaultName) { - nameInitialized.current = true; // don't try again after setting the name + // If the name is already initialized (either by user input or because we're + // editing), or if we don't have account details, do nothing + if (nameInitialized.current || !accountDetails) return; + + // If the user doesn't have a default name, mark the name as initialized to + // avoid trying to autofill on every render + if (!accountDetails.defaultName) { + nameInitialized.current = true; return; } + // If the user has a default name, use it to autofill the name field const newName = accountDetails.defaultName; setDisplayName(newName); handleNameChange(newName); @@ -144,7 +148,7 @@ export default function ClientPage({ title: "NAME AUTOFILLED", }); nameInitialized.current = true; - }, [loginState, accountDetails, setDisplayName, addToast, handleNameChange]); + }, [accountDetails, setDisplayName, addToast, handleNameChange]); // SUBMIT AVAILABILITY const handleSubmitAvailability = async () => { @@ -178,10 +182,6 @@ export default function ClientPage({ await clientPost(ROUTES.account.setDefaultName, { display_name: displayName, }); - login({ - ...accountDetails, - defaultName: displayName, - }); addToast("success", MESSAGES.SUCCESS_DEFAULT_NAME_SAVED); } catch (e) { const error = e as ApiErrorResponse; @@ -298,7 +298,7 @@ export default function ClientPage({
add your availabilities here
- {loginState === "logged_in" && !accountDetails!.defaultName && ( + {accountDetails && !accountDetails!.defaultName && (
); } From b64d4a9954ed3924714e9bc86958c90923eef15a Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Sun, 19 Apr 2026 10:53:00 -0400 Subject: [PATCH 014/112] create dedicated session type --- .../[event-code]/painting/page-client.tsx | 20 +++++++++---------- .../(event)/[event-code]/painting/page.tsx | 4 ++-- frontend/src/app/dashboard/page.tsx | 4 ++-- frontend/src/app/settings/layout.tsx | 6 +++--- frontend/src/features/account/type.ts | 2 -- .../src/features/header/components/header.tsx | 5 ++--- frontend/src/lib/utils/get-session.ts | 17 +++++++++++----- 7 files changed, 31 insertions(+), 27 deletions(-) diff --git a/frontend/src/app/(event)/[event-code]/painting/page-client.tsx b/frontend/src/app/(event)/[event-code]/painting/page-client.tsx index ad65739e0..e94ea5e27 100644 --- a/frontend/src/app/(event)/[event-code]/painting/page-client.tsx +++ b/frontend/src/app/(event)/[event-code]/painting/page-client.tsx @@ -10,7 +10,6 @@ import Checkbox from "@/components/checkbox"; import MobileFooterTray from "@/components/mobile-footer-tray"; import { useAvailability } from "@/core/availability/use-availability"; import { EventRange } from "@/core/event/types"; -import { AccountDetails } from "@/features/account/type"; import ActionButton from "@/features/button/components/action"; import LinkButton from "@/features/button/components/link"; import { validateAvailabilityData } from "@/features/event/availability/validate-data"; @@ -30,21 +29,22 @@ import { ApiErrorResponse } from "@/lib/utils/api/fetch-wrapper"; import { SelfAvailability } from "@/lib/utils/api/types"; import { cn } from "@/lib/utils/classname"; import { timeslotToISOString } from "@/lib/utils/date-time-format"; +import { Session } from "@/lib/utils/get-session"; export default function ClientPage({ + session, eventCode, eventName, eventRange, timeslots, initialData, - accountDetails, }: { + session: Session; eventCode: string; eventName: string; eventRange: EventRange; timeslots: Date[]; initialData: SelfAvailability | null; - accountDetails: AccountDetails | null; }) { const router = useRouter(); @@ -130,25 +130,25 @@ export default function ClientPage({ const nameInitialized = useRef(!!initialData); useEffect(() => { // If the name is already initialized (either by user input or because we're - // editing), or if we don't have account details, do nothing - if (nameInitialized.current || !accountDetails) return; + // editing), or if the user is not logged in, don't try to autofill the name + if (nameInitialized.current || !session.isLoggedIn) return; // If the user doesn't have a default name, mark the name as initialized to // avoid trying to autofill on every render - if (!accountDetails.defaultName) { + if (!session.user.defaultName) { nameInitialized.current = true; return; } // If the user has a default name, use it to autofill the name field - const newName = accountDetails.defaultName; + const newName = session.user.defaultName; setDisplayName(newName); handleNameChange(newName); addToast("success", MESSAGES.INFO_NAME_AUTOFILLED, { title: "NAME AUTOFILLED", }); nameInitialized.current = true; - }, [accountDetails, setDisplayName, addToast, handleNameChange]); + }, [session, setDisplayName, addToast, handleNameChange]); // SUBMIT AVAILABILITY const handleSubmitAvailability = async () => { @@ -177,7 +177,7 @@ export default function ClientPage({ // Save the default name if checkbox checked if (saveDefaultName) { - if (accountDetails) { + if (session.isLoggedIn) { try { await clientPost(ROUTES.account.setDefaultName, { display_name: displayName, @@ -298,7 +298,7 @@ export default function ClientPage({
add your availabilities here
- {accountDetails && !accountDetails!.defaultName && ( + {session.isLoggedIn && !session.user.defaultName && (
); } diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx index 23371db31..bb6e50232 100644 --- a/frontend/src/app/dashboard/page.tsx +++ b/frontend/src/app/dashboard/page.tsx @@ -17,13 +17,13 @@ export function generateMetadata(): Metadata { } export default async function Page() { - const accountDetails = await getSession(); + const session = await getSession(); try { const eventData = await serverGet(ROUTES.dashboard.get, undefined, { cache: "no-store", }); const processedData = processDashboardData(eventData); - return ; + return ; } catch (e) { const error = e as ApiErrorResponse; handleErrorResponse(error.status, error.data); diff --git a/frontend/src/app/settings/layout.tsx b/frontend/src/app/settings/layout.tsx index 5b14586bf..bb2b032d7 100644 --- a/frontend/src/app/settings/layout.tsx +++ b/frontend/src/app/settings/layout.tsx @@ -19,9 +19,9 @@ export default async function SettingsLayout({ }: { children: React.ReactNode; }) { - const accountDetails = await getSession(); + const session = await getSession(); - if (!accountDetails) { + if (!session.isLoggedIn) { redirect("/login?redirect=/settings"); } @@ -42,7 +42,7 @@ export default async function SettingsLayout({
- + {children}
diff --git a/frontend/src/features/account/type.ts b/frontend/src/features/account/type.ts index 5ffbc2ce2..c6b2979cb 100644 --- a/frontend/src/features/account/type.ts +++ b/frontend/src/features/account/type.ts @@ -1,5 +1,3 @@ -export type LoginState = "logged_in" | "logged_out"; - export type AccountDetails = { email: string; defaultName: string | null; diff --git a/frontend/src/features/header/components/header.tsx b/frontend/src/features/header/components/header.tsx index e0f02481c..5e4b94af8 100644 --- a/frontend/src/features/header/components/header.tsx +++ b/frontend/src/features/header/components/header.tsx @@ -5,12 +5,11 @@ import { getSession } from "@/lib/utils/get-session"; export default async function Header() { const session = await getSession(); - const isLoggedIn = session !== null; return ( - {isLoggedIn ? ( - + {session.isLoggedIn ? ( + ) : ( )} diff --git a/frontend/src/lib/utils/get-session.ts b/frontend/src/lib/utils/get-session.ts index 5e026af60..5089bfd87 100644 --- a/frontend/src/lib/utils/get-session.ts +++ b/frontend/src/lib/utils/get-session.ts @@ -8,6 +8,10 @@ import { ROUTES } from "@/lib/utils/api/endpoints"; import { ApiErrorResponse } from "@/lib/utils/api/fetch-wrapper"; import { serverGet } from "@/lib/utils/api/server-fetch"; +export type Session = + | { isLoggedIn: true; user: AccountDetails } + | { isLoggedIn: false; user: null }; + /** * This function retrieves the current user's session information by checking for * the presence of an authentication cookie and then making a server-side API call @@ -17,12 +21,12 @@ import { serverGet } from "@/lib/utils/api/server-fetch"; * result of the session retrieval, but it is designed to bypass caching when * necessary to ensure that it always returns the correct session data for each user. */ -export const getSession = cache(async (): Promise => { +export const getSession = cache(async (): Promise => { const cookieString = await getAuthCookieString(); console.log("Retrieved cookie string:", cookieString); if (!cookieString.includes("account_sess_token")) { - return null; + return { isLoggedIn: false, user: null }; } try { @@ -35,14 +39,17 @@ export const getSession = cache(async (): Promise => { cache: "no-store", }); return { - email: data.email, - defaultName: data.default_display_name, + isLoggedIn: true, + user: { + email: data.email, + defaultName: data.default_display_name, + }, }; } catch (e) { const error = e as ApiErrorResponse; if (error.status === 401 || error.status === 403) { - return null; + return { isLoggedIn: false, user: null }; } throw error; From 8a9debf58903822194e7ca978961d5853420395f Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Sun, 19 Apr 2026 11:35:47 -0400 Subject: [PATCH 015/112] adjust redirect url for settings page --- frontend/src/app/settings/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/settings/layout.tsx b/frontend/src/app/settings/layout.tsx index bb2b032d7..8cf64ff1e 100644 --- a/frontend/src/app/settings/layout.tsx +++ b/frontend/src/app/settings/layout.tsx @@ -22,7 +22,7 @@ export default async function SettingsLayout({ const session = await getSession(); if (!session.isLoggedIn) { - redirect("/login?redirect=/settings"); + redirect("/login?callbackUrl=/settings&unauthorized=true"); } return ( From 11f8e18c3c3f476a86fec505ac58cbcb0eec01d0 Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Sun, 19 Apr 2026 11:41:49 -0400 Subject: [PATCH 016/112] safe callback url --- frontend/src/app/(auth)/login/page.tsx | 3 ++- frontend/src/lib/utils/url.ts | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 frontend/src/lib/utils/url.ts diff --git a/frontend/src/app/(auth)/login/page.tsx b/frontend/src/app/(auth)/login/page.tsx index ef7629964..0d90179a9 100644 --- a/frontend/src/app/(auth)/login/page.tsx +++ b/frontend/src/app/(auth)/login/page.tsx @@ -15,6 +15,7 @@ import { MESSAGES } from "@/lib/messages"; import { clientPost } from "@/lib/utils/api/client-fetch"; import { ROUTES } from "@/lib/utils/api/endpoints"; import { ApiErrorResponse } from "@/lib/utils/api/fetch-wrapper"; +import { getSafeRedirectUrl } from "@/lib/utils/url"; export default function Page() { const [email, setEmail] = useState(""); @@ -23,7 +24,7 @@ export default function Page() { const router = useRouter(); const searchParams = useSearchParams(); - const callbackUrl = searchParams.get("callbackUrl") || "/dashboard"; + const callbackUrl = getSafeRedirectUrl(searchParams.get("callbackUrl")); // TOASTS AND ERROR STATES const { errors, handleError, clearAllErrors } = useFormErrors(); diff --git a/frontend/src/lib/utils/url.ts b/frontend/src/lib/utils/url.ts new file mode 100644 index 000000000..9e5959d40 --- /dev/null +++ b/frontend/src/lib/utils/url.ts @@ -0,0 +1,23 @@ +/** + * Validates and sanitizes a potential redirect URL to prevent Open Redirect + * vulnerabilities. + * + * @param url - The raw URL string to validate (e.g., from search params). + * @param fallback - The default path to return if the URL is invalid. + * Defaults to "/dashboard". + * @returns A safe, same-origin relative path. + */ +export function getSafeRedirectUrl( + url: string | null | undefined, + fallback: string = "/dashboard", +): string { + if (!url) return fallback; + + // Ensure it's a relative path (starts with '/') + // AND is not a protocol-relative absolute URL (starts with '//') + if (url.startsWith("/") && !url.startsWith("//")) { + return url; + } + + return fallback; +} From da704414db01bbac8da6ac6e2e4b31c0b6c07b0d Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Sun, 19 Apr 2026 11:42:51 -0400 Subject: [PATCH 017/112] remove console log and fix typo --- frontend/src/lib/utils/get-session.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/lib/utils/get-session.ts b/frontend/src/lib/utils/get-session.ts index 5089bfd87..457b4206d 100644 --- a/frontend/src/lib/utils/get-session.ts +++ b/frontend/src/lib/utils/get-session.ts @@ -17,14 +17,12 @@ export type Session = * the presence of an authentication cookie and then making a server-side API call * to validate the session and fetch the user's account details. * - * It is wrapped inReact's `cache` function to optimize performance by caching the + * It is wrapped in React's `cache` function to optimize performance by caching the * result of the session retrieval, but it is designed to bypass caching when * necessary to ensure that it always returns the correct session data for each user. */ export const getSession = cache(async (): Promise => { const cookieString = await getAuthCookieString(); - console.log("Retrieved cookie string:", cookieString); - if (!cookieString.includes("account_sess_token")) { return { isLoggedIn: false, user: null }; } From 4bf7c07ad83d8f0ec0f08395210e4e8ced8fc2ad Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Sun, 19 Apr 2026 11:45:59 -0400 Subject: [PATCH 018/112] return response at end --- frontend/src/middleware.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts index 1b4431db7..da390139d 100644 --- a/frontend/src/middleware.ts +++ b/frontend/src/middleware.ts @@ -11,9 +11,9 @@ const authRoutes = [ const protectedRoutes = ["/settings"]; export function middleware(request: NextRequest) { - const response = NextResponse.next(); - const path = request.nextUrl.pathname; + let response = NextResponse.next(); + const path = request.nextUrl.pathname; const hasAccountSessToken = request.cookies.has("account_sess_token"); const isAuthRoute = authRoutes.some((route) => path.startsWith(route)); @@ -26,17 +26,17 @@ export function middleware(request: NextRequest) { if (hasAccountSessToken && isAuthRoute) { const dashboardUrl = new URL("/dashboard", request.nextUrl); dashboardUrl.searchParams.set("alreadyLoggedIn", "true"); - return NextResponse.redirect(dashboardUrl); + response = NextResponse.redirect(dashboardUrl); } // If the user is not logged in and tries to access a protected route (like // settings), redirect them to the login page. A callbackUrl is included so users // can be redirected back after logging in. - if (!hasAccountSessToken && isProtectedRoute) { + else if (!hasAccountSessToken && isProtectedRoute) { const loginUrl = new URL("/login", request.nextUrl); loginUrl.searchParams.set("callbackUrl", path); loginUrl.searchParams.set("unauthorized", "true"); - return NextResponse.redirect(loginUrl); + response = NextResponse.redirect(loginUrl); } if (process.env.NEXT_PUBLIC_DEBUG !== "true") { From aade4ee857bba73c4961767975c0ceae1c695fc2 Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Sun, 19 Apr 2026 11:46:52 -0400 Subject: [PATCH 019/112] import type --- frontend/src/app/(event)/[event-code]/painting/page-client.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/(event)/[event-code]/painting/page-client.tsx b/frontend/src/app/(event)/[event-code]/painting/page-client.tsx index e94ea5e27..5d801d0fa 100644 --- a/frontend/src/app/(event)/[event-code]/painting/page-client.tsx +++ b/frontend/src/app/(event)/[event-code]/painting/page-client.tsx @@ -29,7 +29,7 @@ import { ApiErrorResponse } from "@/lib/utils/api/fetch-wrapper"; import { SelfAvailability } from "@/lib/utils/api/types"; import { cn } from "@/lib/utils/classname"; import { timeslotToISOString } from "@/lib/utils/date-time-format"; -import { Session } from "@/lib/utils/get-session"; +import type { Session } from "@/lib/utils/get-session"; export default function ClientPage({ session, From 113f4503e707adf09c2c73a0751b3f8cfaad871d Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Mon, 20 Apr 2026 10:25:56 -0400 Subject: [PATCH 020/112] revise comment string --- frontend/src/lib/utils/get-session.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/frontend/src/lib/utils/get-session.ts b/frontend/src/lib/utils/get-session.ts index 457b4206d..d17c3aeb4 100644 --- a/frontend/src/lib/utils/get-session.ts +++ b/frontend/src/lib/utils/get-session.ts @@ -13,13 +13,14 @@ export type Session = | { isLoggedIn: false; user: null }; /** - * This function retrieves the current user's session information by checking for - * the presence of an authentication cookie and then making a server-side API call - * to validate the session and fetch the user's account details. + * Retrieves the current user's session information by checking for the presence + * of an authentication cookie and validating the session against the server. * - * It is wrapped in React's `cache` function to optimize performance by caching the - * result of the session retrieval, but it is designed to bypass caching when - * necessary to ensure that it always returns the correct session data for each user. + * This function is memoized using React's `cache()` to optimize performance by + * preventing redundant network requests during a single page render. It also + * uses `cache: "no-store"` for the underlying fetch request to ensure that session + * data is never shared across different requests or users, guaranteeing fresh + * authentication checks. */ export const getSession = cache(async (): Promise => { const cookieString = await getAuthCookieString(); From 9af677bb78123b95303ad4bd0a8bcddd481811f0 Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Mon, 20 Apr 2026 10:45:29 -0400 Subject: [PATCH 021/112] add headers --- frontend/src/app/settings/layout.tsx | 7 ++++++- frontend/src/middleware.ts | 15 ++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/settings/layout.tsx b/frontend/src/app/settings/layout.tsx index 8cf64ff1e..8f16163fd 100644 --- a/frontend/src/app/settings/layout.tsx +++ b/frontend/src/app/settings/layout.tsx @@ -1,4 +1,5 @@ import { Metadata } from "next"; +import { headers } from "next/headers"; import { redirect } from "next/navigation"; import { SettingsProvider } from "@/features/account/settings/context"; @@ -22,7 +23,11 @@ export default async function SettingsLayout({ const session = await getSession(); if (!session.isLoggedIn) { - redirect("/login?callbackUrl=/settings&unauthorized=true"); + const headersList = await headers(); + const currentPath = headersList.get("x-pathname") || "/settings"; + + const encodedCallback = encodeURIComponent(currentPath); + redirect(`/login?callbackUrl=${encodedCallback}&unauthorized=true`); } return ( diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts index da390139d..c52dbf8ee 100644 --- a/frontend/src/middleware.ts +++ b/frontend/src/middleware.ts @@ -11,7 +11,17 @@ const authRoutes = [ const protectedRoutes = ["/settings"]; export function middleware(request: NextRequest) { - let response = NextResponse.next(); + // Clone the request headers and inject the exact pathname into a custom header. + // This allows server-side code (like getSession()) to determine the user's + // intended destination. + const requestHeaders = new Headers(request.headers); + requestHeaders.set("x-pathname", request.nextUrl.pathname); + + let response = NextResponse.next({ + request: { + headers: requestHeaders, + }, + }); const path = request.nextUrl.pathname; const hasAccountSessToken = request.cookies.has("account_sess_token"); @@ -56,6 +66,9 @@ export function middleware(request: NextRequest) { // to be safe, since long session cookies have a 1 year lifetime cookieNames.forEach((name) => { + // Because we initialized `response` with NextResponse.next() above, + // we can safely append headers to it here. If response was reassigned + // to a redirect, appending headers still works. response.headers.append( "Set-Cookie", `${name}=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax`, From 7451a36ea6ba4fb79b4a6fce60e89486a416aa52 Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Mon, 20 Apr 2026 10:50:44 -0400 Subject: [PATCH 022/112] move auth route checks to layout.tsx --- frontend/src/app/(auth)/layout.tsx | 12 +++++++++++- frontend/src/middleware.ts | 19 +------------------ 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/frontend/src/app/(auth)/layout.tsx b/frontend/src/app/(auth)/layout.tsx index ff87ae0a0..77352b8e8 100644 --- a/frontend/src/app/(auth)/layout.tsx +++ b/frontend/src/app/(auth)/layout.tsx @@ -1,7 +1,17 @@ -export default function AuthLayout({ +import { redirect } from "next/navigation"; + +import { getSession } from "@/lib/utils/get-session"; + +export default async function AuthLayout({ children, }: { children: React.ReactNode; }) { + const session = await getSession(); + + if (session.isLoggedIn) { + redirect("/dashboard?alreadyLoggedIn=true"); + } + return <>{children}; } diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts index c52dbf8ee..c42febd30 100644 --- a/frontend/src/middleware.ts +++ b/frontend/src/middleware.ts @@ -1,13 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -const authRoutes = [ - "/login", - "/register", - "/forgot-password", - "/reset-password", - "/verify-email", -]; - const protectedRoutes = ["/settings"]; export function middleware(request: NextRequest) { @@ -26,23 +18,14 @@ export function middleware(request: NextRequest) { const path = request.nextUrl.pathname; const hasAccountSessToken = request.cookies.has("account_sess_token"); - const isAuthRoute = authRoutes.some((route) => path.startsWith(route)); const isProtectedRoute = protectedRoutes.some((route) => path.startsWith(route), ); - // If the user is logged in and tries to access an auth route, redirect them - // to the dashboard and attach a flag. - if (hasAccountSessToken && isAuthRoute) { - const dashboardUrl = new URL("/dashboard", request.nextUrl); - dashboardUrl.searchParams.set("alreadyLoggedIn", "true"); - response = NextResponse.redirect(dashboardUrl); - } - // If the user is not logged in and tries to access a protected route (like // settings), redirect them to the login page. A callbackUrl is included so users // can be redirected back after logging in. - else if (!hasAccountSessToken && isProtectedRoute) { + if (!hasAccountSessToken && isProtectedRoute) { const loginUrl = new URL("/login", request.nextUrl); loginUrl.searchParams.set("callbackUrl", path); loginUrl.searchParams.set("unauthorized", "true"); From e8e0d403e58cfec7124d5afd899e644a4d8bcfcb Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Fri, 1 May 2026 09:20:35 -0400 Subject: [PATCH 023/112] create folder --- .../features/system-feedback/dialog/{ => components}/base.tsx | 0 .../system-feedback/dialog/{ => components}/confirmation.tsx | 2 +- .../features/system-feedback/dialog/{ => components}/form.tsx | 2 +- .../features/system-feedback/dialog/{ => components}/icon.tsx | 0 frontend/src/features/system-feedback/index.ts | 4 ++-- 5 files changed, 4 insertions(+), 4 deletions(-) rename frontend/src/features/system-feedback/dialog/{ => components}/base.tsx (100%) rename frontend/src/features/system-feedback/dialog/{ => components}/confirmation.tsx (96%) rename frontend/src/features/system-feedback/dialog/{ => components}/form.tsx (96%) rename frontend/src/features/system-feedback/dialog/{ => components}/icon.tsx (100%) diff --git a/frontend/src/features/system-feedback/dialog/base.tsx b/frontend/src/features/system-feedback/dialog/components/base.tsx similarity index 100% rename from frontend/src/features/system-feedback/dialog/base.tsx rename to frontend/src/features/system-feedback/dialog/components/base.tsx diff --git a/frontend/src/features/system-feedback/dialog/confirmation.tsx b/frontend/src/features/system-feedback/dialog/components/confirmation.tsx similarity index 96% rename from frontend/src/features/system-feedback/dialog/confirmation.tsx rename to frontend/src/features/system-feedback/dialog/components/confirmation.tsx index ec3587b7d..a142a081d 100644 --- a/frontend/src/features/system-feedback/dialog/confirmation.tsx +++ b/frontend/src/features/system-feedback/dialog/components/confirmation.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect } from "react"; import ActionButton from "@/features/button/components/action"; -import BaseModal from "@/features/system-feedback/dialog/base"; +import BaseModal from "@/features/system-feedback/dialog/components/base"; import { DIALOG_CONFIG } from "@/features/system-feedback/dialog/config"; import { ConfirmationDialogProps } from "@/features/system-feedback/dialog/props"; import { cn } from "@/lib/utils/classname"; diff --git a/frontend/src/features/system-feedback/dialog/form.tsx b/frontend/src/features/system-feedback/dialog/components/form.tsx similarity index 96% rename from frontend/src/features/system-feedback/dialog/form.tsx rename to frontend/src/features/system-feedback/dialog/components/form.tsx index f1f34d36a..e61e0617e 100644 --- a/frontend/src/features/system-feedback/dialog/form.tsx +++ b/frontend/src/features/system-feedback/dialog/components/form.tsx @@ -2,7 +2,7 @@ import { useCallback } from "react"; import ActionButton from "@/features/button/components/action"; import EmptyButton from "@/features/button/components/empty"; -import BaseModal from "@/features/system-feedback/dialog/base"; +import BaseModal from "@/features/system-feedback/dialog/components/base"; import { DIALOG_CONFIG } from "@/features/system-feedback/dialog/config"; import { FormDialogProps } from "@/features/system-feedback/dialog/props"; import { cn } from "@/lib/utils/classname"; diff --git a/frontend/src/features/system-feedback/dialog/icon.tsx b/frontend/src/features/system-feedback/dialog/components/icon.tsx similarity index 100% rename from frontend/src/features/system-feedback/dialog/icon.tsx rename to frontend/src/features/system-feedback/dialog/components/icon.tsx diff --git a/frontend/src/features/system-feedback/index.ts b/frontend/src/features/system-feedback/index.ts index 746a8df6d..7e9a8af26 100644 --- a/frontend/src/features/system-feedback/index.ts +++ b/frontend/src/features/system-feedback/index.ts @@ -1,8 +1,8 @@ // Export Components export { Banner } from "@/features/system-feedback/banner/base"; export { default as RateLimitBanner } from "@/features/system-feedback/banner/rate-limit"; -export { default as ConfirmationDialog } from "@/features/system-feedback/dialog/confirmation"; -export { default as FormDialog } from "@/features/system-feedback/dialog/form"; +export { default as ConfirmationDialog } from "@/features/system-feedback/dialog/components/confirmation"; +export { default as FormDialog } from "@/features/system-feedback/dialog/components/form"; export { default as ToastProvider } from "@/features/system-feedback/toast/provider"; export { useToast } from "@/features/system-feedback/toast/context"; From b7b406b4a75fdd14fc9d1f73634a9447227f8f97 Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Fri, 1 May 2026 18:31:32 -0400 Subject: [PATCH 024/112] adjust listener --- .../dialog/components/confirmation.tsx | 64 ++++++++++++------- 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/frontend/src/features/system-feedback/dialog/components/confirmation.tsx b/frontend/src/features/system-feedback/dialog/components/confirmation.tsx index a142a081d..a222dda0e 100644 --- a/frontend/src/features/system-feedback/dialog/components/confirmation.tsx +++ b/frontend/src/features/system-feedback/dialog/components/confirmation.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from "react"; +import { useCallback } from "react"; import ActionButton from "@/features/button/components/action"; import BaseModal from "@/features/system-feedback/dialog/components/base"; @@ -34,15 +34,31 @@ export default function ConfirmationDialog({ return success; }, [autoClose, onConfirm, onOpenChange]); - /* KEY LISTENERS */ - useEffect(() => { - if (!open) return; - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Enter") handleConfirm(); - }; - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [handleConfirm, open]); + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + const target = e.target as HTMLElement; + + // Ignore if focus is inside an input, textarea, or contentEditable + const isTextInput = + target.tagName === "INPUT" || + target.tagName === "TEXTAREA" || + target.isContentEditable; + + // Ignore if focus is on a button or link (native click handles it) + const isActionable = + target.tagName === "BUTTON" || target.tagName === "A"; + + if (isTextInput || isActionable) { + return; + } + + e.preventDefault(); + handleConfirm(); + } + }, + [handleConfirm], + ); return ( - {children} -
- onOpenChange?.(false)} - /> - +
+ {children} +
+ onOpenChange?.(false)} + /> + +
); From 37a9d6dd0526adf7fefdf9495e2c8665fda0d7b7 Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Fri, 1 May 2026 18:35:22 -0400 Subject: [PATCH 025/112] remove extra console log --- .../src/features/system-feedback/dialog/components/form.tsx | 1 - .../src/features/system-feedback/dialog/components/icon.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/features/system-feedback/dialog/components/form.tsx b/frontend/src/features/system-feedback/dialog/components/form.tsx index e61e0617e..9dd3ba0b7 100644 --- a/frontend/src/features/system-feedback/dialog/components/form.tsx +++ b/frontend/src/features/system-feedback/dialog/components/form.tsx @@ -25,7 +25,6 @@ export default function FormDialog({ const handleSubmit = useCallback( async (e: React.FormEvent) => { - console.log("Form submitted"); e.preventDefault(); const success = await onSubmit(); if (success) { diff --git a/frontend/src/features/system-feedback/dialog/components/icon.tsx b/frontend/src/features/system-feedback/dialog/components/icon.tsx index eb81ad0cc..a9e0aa0b3 100644 --- a/frontend/src/features/system-feedback/dialog/components/icon.tsx +++ b/frontend/src/features/system-feedback/dialog/components/icon.tsx @@ -3,7 +3,7 @@ import { cn } from "@/lib/utils/classname"; type DialogIconProps = { dialogConfig: { - icon: React.ComponentType<{ className?: string }>; + icon: React.ComponentType<{ className?: string }> | null; iconStyle: string; }; type?: DialogType; From 73400ef7ee8f26cb80693711d55eaab568b49aa3 Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Fri, 1 May 2026 18:53:16 -0400 Subject: [PATCH 026/112] remove icons --- .../dialog/components/base.tsx | 3 -- .../dialog/components/confirmation.tsx | 37 ++++++++++++----- .../dialog/components/form.tsx | 28 +++++++++---- .../dialog/components/icon.tsx | 40 ------------------- .../features/system-feedback/dialog/config.ts | 21 ++-------- .../features/system-feedback/dialog/props.ts | 12 +----- 6 files changed, 52 insertions(+), 89 deletions(-) delete mode 100644 frontend/src/features/system-feedback/dialog/components/icon.tsx diff --git a/frontend/src/features/system-feedback/dialog/components/base.tsx b/frontend/src/features/system-feedback/dialog/components/base.tsx index cdfc3737b..7cbf3827b 100644 --- a/frontend/src/features/system-feedback/dialog/components/base.tsx +++ b/frontend/src/features/system-feedback/dialog/components/base.tsx @@ -15,7 +15,6 @@ export default function BaseDialog({ onOpenChange, asNestedDrawer = false, triggerDisabled = false, - icon, overlayClassName, }: BaseDialogProps) { /* OPEN STATE MANAGEMENT */ @@ -48,7 +47,6 @@ export default function BaseDialog({ hideCloseButton >
- {icon}

{title}

{children}
@@ -91,7 +89,6 @@ export default function BaseDialog({ >
- {icon}

{title}

diff --git a/frontend/src/features/system-feedback/dialog/components/confirmation.tsx b/frontend/src/features/system-feedback/dialog/components/confirmation.tsx index a222dda0e..83525bcb2 100644 --- a/frontend/src/features/system-feedback/dialog/components/confirmation.tsx +++ b/frontend/src/features/system-feedback/dialog/components/confirmation.tsx @@ -1,4 +1,4 @@ -import { useCallback } from "react"; +import { useCallback, useState } from "react"; import ActionButton from "@/features/button/components/action"; import BaseModal from "@/features/system-feedback/dialog/components/base"; @@ -16,36 +16,48 @@ export default function ConfirmationDialog({ triggerDisabled = false, autoClose = false, asNestedDrawer = false, - open, + open: controlledOpen, onOpenChange, }: ConfirmationDialogProps) { const config = DIALOG_CONFIG[type] || DIALOG_CONFIG.info; + const [uncontrolledOpen, setUncontrolledOpen] = useState(false); + const isControlled = controlledOpen !== undefined; + const open = isControlled ? controlledOpen : uncontrolledOpen; + + const handleOpenChange = useCallback( + (newOpen: boolean) => { + if (!isControlled) { + setUncontrolledOpen(newOpen); + } + onOpenChange?.(newOpen); + }, + [isControlled, onOpenChange], + ); + const handleConfirm = useCallback(async () => { if (autoClose) { - onOpenChange?.(false); + handleOpenChange(false); onConfirm(); return true; } const success = await onConfirm(); if (success) { - onOpenChange?.(false); + handleOpenChange(false); } return success; - }, [autoClose, onConfirm, onOpenChange]); + }, [autoClose, onConfirm, handleOpenChange]); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === "Enter") { const target = e.target as HTMLElement; - // Ignore if focus is inside an input, textarea, or contentEditable const isTextInput = target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable; - // Ignore if focus is on a button or link (native click handles it) const isActionable = target.tagName === "BUTTON" || target.tagName === "A"; @@ -66,7 +78,7 @@ export default function ConfirmationDialog({ description={description} trigger={trigger} open={open} - onOpenChange={onOpenChange} + onOpenChange={handleOpenChange} asNestedDrawer={asNestedDrawer} triggerDisabled={triggerDisabled} overlayClassName={cn( @@ -75,12 +87,19 @@ export default function ConfirmationDialog({ )} >
+ {description && !children && ( +

+ {description} +

+ )} + {children} +
onOpenChange?.(false)} + onClick={() => handleOpenChange(false)} /> { + if (!isControlled) { + setUncontrolledOpen(newOpen); + } + onOpenChange?.(newOpen); + }, + [isControlled, onOpenChange], + ); + const handleSubmit = useCallback( async (e: React.FormEvent) => { e.preventDefault(); const success = await onSubmit(); if (success) { - onOpenChange?.(false); + handleOpenChange(false); } }, - [onSubmit, onOpenChange], + [onSubmit, handleOpenChange], ); return ( @@ -40,9 +53,8 @@ export default function FormDialog({ description={description} trigger={trigger} open={open} - onOpenChange={onOpenChange} + onOpenChange={handleOpenChange} asNestedDrawer={asNestedDrawer} - icon={icon} overlayClassName={cn( type === "error" && "bg-[color-mix(in_oklab,var(--color-error)_15%,black_20%)]", @@ -59,7 +71,7 @@ export default function FormDialog({ type="button" buttonStyle="transparent" label={cancelLabel} - onClick={() => onOpenChange?.(false)} + onClick={() => handleOpenChange(false)} /> | null; - iconStyle: string; - }; - type?: DialogType; - showIcon?: boolean; -}; - -export default function DialogIcon({ - dialogConfig, - type = "info", - showIcon = true, -}: DialogIconProps) { - if (!showIcon) return null; - - const Icon = dialogConfig.icon; - - if (type === "warning") { - return ( -
-
!
-
- ); - } - - return ( -
- {Icon && } -
- ); -} diff --git a/frontend/src/features/system-feedback/dialog/config.ts b/frontend/src/features/system-feedback/dialog/config.ts index 077fa0c92..f5aca7f11 100644 --- a/frontend/src/features/system-feedback/dialog/config.ts +++ b/frontend/src/features/system-feedback/dialog/config.ts @@ -1,39 +1,24 @@ -import type { ComponentType } from "react"; - -import { CheckIcon, InfoIcon, TriangleAlertIcon } from "lucide-react"; - import { ButtonStyle } from "@/features/button/props"; +import { DialogType } from "@/features/system-feedback/type"; // Assuming path export type DialogConfig = { - icon: ComponentType<{ className?: string }> | null; - iconStyle: string; buttonStyle: ButtonStyle; }; -export const DIALOG_CONFIG: Record = { +export const DIALOG_CONFIG = { warning: { - icon: null, // special case for the text "!" - iconStyle: "bg-lion", buttonStyle: "primary", }, delete: { - icon: TriangleAlertIcon, - iconStyle: "bg-error/40", buttonStyle: "danger", }, success: { - icon: CheckIcon, - iconStyle: "bg-foreground/40", buttonStyle: "primary", }, error: { - icon: TriangleAlertIcon, - iconStyle: "bg-error/40", buttonStyle: "danger", }, info: { - icon: InfoIcon, - iconStyle: "bg-blue/40", buttonStyle: "primary", }, -} as const; +} satisfies Record; diff --git a/frontend/src/features/system-feedback/dialog/props.ts b/frontend/src/features/system-feedback/dialog/props.ts index 7b07b7133..5efcd3a40 100644 --- a/frontend/src/features/system-feedback/dialog/props.ts +++ b/frontend/src/features/system-feedback/dialog/props.ts @@ -62,13 +62,6 @@ type CommonDialogProps = { * @default false */ asNestedDrawer?: boolean; - /** - * Optional icon to display in the dialog header, next to the title. - * - * @type {React.ReactNode} - * @default undefined - */ - icon?: React.ReactNode; /** * Additional className to apply to the dialog overlay, for further customization. * @@ -80,7 +73,7 @@ type CommonDialogProps = { export type BaseDialogProps = CommonDialogProps; -export type ConfirmationDialogProps = Omit & { +export type ConfirmationDialogProps = CommonDialogProps & { type: DialogType; /** * Enforced strictly as a string for semantic standard confirmations. @@ -90,9 +83,6 @@ export type ConfirmationDialogProps = Omit & { /** Callback function that is called when the user confirms the action */ onConfirm: () => boolean | Promise; - /** Whether to display the default icon associated with the dialog 'type' */ - showIcon?: boolean; - /** Whether to automatically close the dialog after confirming */ autoClose?: boolean; }; From 80fa5523ffbe4d9512e7371d1c018eb5835f00cc Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 4 May 2026 14:26:24 -0400 Subject: [PATCH 027/112] Create MobileFooterIsland component --- .../src/components/mobile-footer-island.tsx | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 frontend/src/components/mobile-footer-island.tsx diff --git a/frontend/src/components/mobile-footer-island.tsx b/frontend/src/components/mobile-footer-island.tsx new file mode 100644 index 000000000..e7e69d5bd --- /dev/null +++ b/frontend/src/components/mobile-footer-island.tsx @@ -0,0 +1,36 @@ +import { ButtonArray } from "@/features/button/button-array"; +import { cn } from "@/lib/utils/classname"; + +export default function MobileFooterIsland({ + children, + leftButtons, + rightButtons, +}: { + children?: React.ReactNode; + leftButtons?: ButtonArray; + rightButtons?: ButtonArray; +}) { + return ( +
+ {children} +
+ {leftButtons ? ( + leftButtons.map((button, index) =>
{button}
) + ) : ( + // Placeholder to keep right buttons aligned +
+ )} + {rightButtons && + rightButtons.map((button, index) =>
{button}
)} +
+
+ ); +} From 9e6b1fab4725edca599b326c1de0a295824c688b Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 4 May 2026 14:26:33 -0400 Subject: [PATCH 028/112] Add hidePadding prop to SegmentedControl --- frontend/src/components/segmented-control.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/segmented-control.tsx b/frontend/src/components/segmented-control.tsx index fafa6301b..97cf9af95 100644 --- a/frontend/src/components/segmented-control.tsx +++ b/frontend/src/components/segmented-control.tsx @@ -8,6 +8,7 @@ type SegmentedControlProps = { options: { label: React.ReactNode; value: T }[]; value: T; onChange: (value: T) => void; + hidePadding?: boolean; className?: string; }; @@ -15,6 +16,7 @@ export default function SegmentedControl({ options, value, onChange, + hidePadding = false, className, }: SegmentedControlProps) { const activeIndex = options.findIndex((opt) => opt.value === value); @@ -22,19 +24,20 @@ export default function SegmentedControl({ // Calculate dynamic style for the sliding pill const pillStyle = useMemo(() => { - const pillWidth = `(100% - 16px) / ${count}`; - const leftOffset = `calc(8px + (${pillWidth}) * ${activeIndex})`; + const pillWidth = `(100% - ${hidePadding ? "0px" : "16px"}) / ${count}`; + const leftOffset = `calc(${hidePadding ? "0px" : "8px"} + (${pillWidth}) * ${activeIndex})`; return { width: `calc(${pillWidth})`, left: activeIndex === -1 ? "8px" : leftOffset, }; - }, [activeIndex, count]); + }, [hidePadding, activeIndex, count]); return (
({ }} >
From c6c97c7b47e31b4133df0712d8b38991b6f28269 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 4 May 2026 14:26:46 -0400 Subject: [PATCH 029/112] Add MobileFooterIsland to event editor --- frontend/src/features/event/editor/editor.tsx | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/frontend/src/features/event/editor/editor.tsx b/frontend/src/features/event/editor/editor.tsx index 14907a89e..e760495e9 100644 --- a/frontend/src/features/event/editor/editor.tsx +++ b/frontend/src/features/event/editor/editor.tsx @@ -5,7 +5,7 @@ import { memo, useState } from "react"; import { TriangleAlertIcon } from "lucide-react"; import { useRouter } from "next/navigation"; -import MobileFooterTray from "@/components/mobile-footer-tray"; +import MobileFooterIsland from "@/components/mobile-footer-island"; import SegmentedControl from "@/components/segmented-control"; import TextInputField from "@/components/text-input-field"; import { EventProvider, useEventContext } from "@/core/event/context"; @@ -130,17 +130,6 @@ function EventEditorContent({ type, initialData }: EventEditorProps) {
-
- -
-
- + + +
); From 1043568662feb2036d496642ee9354b908aec3e8 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 4 May 2026 14:45:51 -0400 Subject: [PATCH 030/112] Update segmented control style --- frontend/src/components/segmented-control.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/segmented-control.tsx b/frontend/src/components/segmented-control.tsx index 97cf9af95..e28a0ee25 100644 --- a/frontend/src/components/segmented-control.tsx +++ b/frontend/src/components/segmented-control.tsx @@ -46,7 +46,7 @@ export default function SegmentedControl({ >
({ type="button" onClick={() => onChange(option.value)} className={cn( - "z-10 flex w-full items-center justify-center rounded-full py-2 text-sm font-medium transition-colors duration-300 focus:outline-none", + "z-10 flex w-full items-center justify-center rounded-full py-2 text-sm font-medium focus:outline-none", isSelected - ? "text-white" - : "text-foreground hover:bg-accent/25 cursor-pointer", + ? "text-accent-text font-bold" + : "text-foreground hover:bg-accent/15 cursor-pointer", )} > {option.label} From 74088406ca87d3c406fd42dfbe8296545c704380 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 4 May 2026 14:56:00 -0400 Subject: [PATCH 031/112] Remove redundant className --- frontend/src/features/event/editor/editor.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/features/event/editor/editor.tsx b/frontend/src/features/event/editor/editor.tsx index e760495e9..d48e901f3 100644 --- a/frontend/src/features/event/editor/editor.tsx +++ b/frontend/src/features/event/editor/editor.tsx @@ -202,7 +202,6 @@ function EventEditorContent({ type, initialData }: EventEditorProps) { { label: "Grid Preview", value: "preview" }, ]} hidePadding - className="" />
From 5dca0fbab0415f899621d8ad86c98f07ea23f6f8 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 4 May 2026 15:04:06 -0400 Subject: [PATCH 032/112] Add built-in spacer for mobile footer island --- .../src/components/mobile-footer-island.tsx | 62 +++++++++++++------ frontend/src/features/event/editor/editor.tsx | 1 - 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/frontend/src/components/mobile-footer-island.tsx b/frontend/src/components/mobile-footer-island.tsx index e7e69d5bd..98d534e7a 100644 --- a/frontend/src/components/mobile-footer-island.tsx +++ b/frontend/src/components/mobile-footer-island.tsx @@ -1,3 +1,5 @@ +import { useEffect, useRef, useState } from "react"; + import { ButtonArray } from "@/features/button/button-array"; import { cn } from "@/lib/utils/classname"; @@ -10,27 +12,49 @@ export default function MobileFooterIsland({ leftButtons?: ButtonArray; rightButtons?: ButtonArray; }) { + const [islandHeight, setIslandHeight] = useState(0); + const islandRef = useRef(null); + const extraTopPadding = 48; + + useEffect(() => { + if (!islandRef.current) return; + const observer = new ResizeObserver((entries) => { + for (const entry of entries) { + setIslandHeight(entry.contentRect.height + extraTopPadding); + } + }); + observer.observe(islandRef.current); + return () => observer.disconnect(); + }, []); + return ( -
- {children} -
- {leftButtons ? ( - leftButtons.map((button, index) =>
{button}
) - ) : ( - // Placeholder to keep right buttons aligned -
+ <> + {/* Placeholder div to avoid content overlap */} +
+
{button}
)} + > + {children} +
+ {leftButtons ? ( + leftButtons.map((button, index) =>
{button}
) + ) : ( + // Placeholder to keep right buttons aligned +
+ )} + {rightButtons && + rightButtons.map((button, index) => ( +
{button}
+ ))} +
-
+ ); } diff --git a/frontend/src/features/event/editor/editor.tsx b/frontend/src/features/event/editor/editor.tsx index d48e901f3..951732de5 100644 --- a/frontend/src/features/event/editor/editor.tsx +++ b/frontend/src/features/event/editor/editor.tsx @@ -186,7 +186,6 @@ function EventEditorContent({ type, initialData }: EventEditorProps) { timeslots={timeslots} />
-
{/* This z-index is necessary to avoid the time column overlapping */}
From 159cc83c53a26b296b076c42980080d299e36eb8 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 4 May 2026 16:12:13 -0400 Subject: [PATCH 033/112] Add desktop hiding on footer island --- frontend/src/components/mobile-footer-island.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/mobile-footer-island.tsx b/frontend/src/components/mobile-footer-island.tsx index 98d534e7a..d7971c068 100644 --- a/frontend/src/components/mobile-footer-island.tsx +++ b/frontend/src/components/mobile-footer-island.tsx @@ -28,7 +28,7 @@ export default function MobileFooterIsland({ }, []); return ( - <> +
{/* Placeholder div to avoid content overlap */}
- +
); } From 751876aefeff57e4625ecfce8312e02169b5f146 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 4 May 2026 16:12:41 -0400 Subject: [PATCH 034/112] Add footer island to painting page --- .../[event-code]/painting/page-client.tsx | 126 +++++++++++++----- 1 file changed, 89 insertions(+), 37 deletions(-) diff --git a/frontend/src/app/(event)/[event-code]/painting/page-client.tsx b/frontend/src/app/(event)/[event-code]/painting/page-client.tsx index 1e3fa4ce8..e995aeae1 100644 --- a/frontend/src/app/(event)/[event-code]/painting/page-client.tsx +++ b/frontend/src/app/(event)/[event-code]/painting/page-client.tsx @@ -7,10 +7,11 @@ import { useRouter } from "next/navigation"; import { useDebouncedCallback } from "use-debounce"; import Checkbox from "@/components/checkbox"; -import MobileFooterTray from "@/components/mobile-footer-tray"; +import MobileFooterIsland from "@/components/mobile-footer-island"; import { useAvailability } from "@/core/availability/use-availability"; import { EventRange } from "@/core/event/types"; import { useAccount } from "@/features/account/context"; +import { AccountDetails } from "@/features/account/type"; import ActionButton from "@/features/button/components/action"; import LinkButton from "@/features/button/components/link"; import { validateAvailabilityData } from "@/features/event/availability/validate-data"; @@ -272,41 +273,17 @@ export default function ClientPage({ "h-fit w-full shrink-0 space-y-4 overflow-y-auto md:sticky md:w-80", )} > -
-
-

- {errors.displayName ? errors.displayName : "Error Placeholder"} -

- Hi,{" "} - { - setDisplayName(e.target.value); - handleNameChange(e.target.value); - }} - placeholder="add your name" - className={`inline-block w-auto border-b bg-transparent px-1 focus:outline-none ${ - errors.displayName - ? "border-error placeholder:text-error" - : "border-gray-400" - }`} - /> -
- add your availabilities here -
- {loginState === "logged_in" && !accountDetails!.defaultName && ( -
- setSaveDefaultName(checked)} - > -
- )} +
+
@@ -338,7 +315,23 @@ export default function ClientPage({ {/* This z-index is necessary to avoid the time column overlapping */}
- + +
+ +
+
); } + +function DisplayNameInput({ + errors, + displayName, + setDisplayName, + handleNameChange, + loginState, + accountDetails, + saveDefaultName, + setSaveDefaultName, +}: { + errors: Record; + displayName: string; + setDisplayName: (name: string) => void; + handleNameChange: (name: string) => void; + loginState: string; + accountDetails: AccountDetails | null; + saveDefaultName: boolean; + setSaveDefaultName: (save: boolean) => void; +}) { + return ( +
+
+

+ {errors.displayName ? errors.displayName : "Error Placeholder"} +

+ Hi,{" "} + { + setDisplayName(e.target.value); + handleNameChange(e.target.value); + }} + placeholder="add your name" + className={`inline-block w-auto border-b bg-transparent px-1 focus:outline-none ${ + errors.displayName + ? "border-error placeholder:text-error" + : "border-gray-400" + }`} + /> +
+ add your availabilities here +
+ {loginState === "logged_in" && !accountDetails!.defaultName && ( +
+ setSaveDefaultName(checked)} + > +
+ )} +
+ ); +} From 035e50e3ea8598d7240401552fb0daa9975dfff9 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 4 May 2026 16:13:03 -0400 Subject: [PATCH 035/112] Remove MobileFooterTray --- frontend/src/components/mobile-footer-tray.tsx | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 frontend/src/components/mobile-footer-tray.tsx diff --git a/frontend/src/components/mobile-footer-tray.tsx b/frontend/src/components/mobile-footer-tray.tsx deleted file mode 100644 index 885781f5d..000000000 --- a/frontend/src/components/mobile-footer-tray.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { ButtonArray } from "@/features/button/button-array"; - -export default function MobileFooterTray({ - buttons, -}: { - buttons: ButtonArray; -}) { - return ( -
-
- {buttons.map((button, index) => ( -
{button}
- ))} -
-
- ); -} From 7e8b4d8cb15a328fda3cf17d9dd0888ccae9e2e9 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 4 May 2026 16:24:29 -0400 Subject: [PATCH 036/112] Simplify dashboard layout --- frontend/src/app/dashboard/page-client.tsx | 48 ++++++++++------------ 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/frontend/src/app/dashboard/page-client.tsx b/frontend/src/app/dashboard/page-client.tsx index 5ed7d6517..0fbbf6d21 100644 --- a/frontend/src/app/dashboard/page-client.tsx +++ b/frontend/src/app/dashboard/page-client.tsx @@ -100,34 +100,28 @@ export default function ClientPage({
)} -
-
- -
-
- {currentTabEvents.length ? ( - - ) : ( -
-
- {tab === "created" - ? "You haven't created any events yet." - : "You haven't participated in any events yet."} -
-
{`When you do, it'll show up here for quick access!`}
+
+ + {currentTabEvents.length ? ( + + ) : ( +
+
+ {tab === "created" + ? "You haven't created any events yet." + : "You haven't participated in any events yet."}
- )} -
+
{`When you do, it'll show up here for quick access!`}
+
+ )}
Date: Mon, 4 May 2026 17:06:22 -0400 Subject: [PATCH 037/112] Fix DisplayNameInput typing --- .../src/app/(event)/[event-code]/painting/page-client.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/(event)/[event-code]/painting/page-client.tsx b/frontend/src/app/(event)/[event-code]/painting/page-client.tsx index e995aeae1..9cf743491 100644 --- a/frontend/src/app/(event)/[event-code]/painting/page-client.tsx +++ b/frontend/src/app/(event)/[event-code]/painting/page-client.tsx @@ -11,7 +11,7 @@ import MobileFooterIsland from "@/components/mobile-footer-island"; import { useAvailability } from "@/core/availability/use-availability"; import { EventRange } from "@/core/event/types"; import { useAccount } from "@/features/account/context"; -import { AccountDetails } from "@/features/account/type"; +import { AccountDetails, LoginState } from "@/features/account/type"; import ActionButton from "@/features/button/components/action"; import LinkButton from "@/features/button/components/link"; import { validateAvailabilityData } from "@/features/event/availability/validate-data"; @@ -378,7 +378,7 @@ function DisplayNameInput({ displayName: string; setDisplayName: (name: string) => void; handleNameChange: (name: string) => void; - loginState: string; + loginState: LoginState; accountDetails: AccountDetails | null; saveDefaultName: boolean; setSaveDefaultName: (save: boolean) => void; @@ -410,7 +410,7 @@ function DisplayNameInput({
add your availabilities here
- {loginState === "logged_in" && !accountDetails!.defaultName && ( + {loginState === "logged_in" && !accountDetails?.defaultName && (
Date: Mon, 4 May 2026 17:07:09 -0400 Subject: [PATCH 038/112] Update segmented control fallback padding --- frontend/src/components/segmented-control.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/segmented-control.tsx b/frontend/src/components/segmented-control.tsx index e28a0ee25..baa198bca 100644 --- a/frontend/src/components/segmented-control.tsx +++ b/frontend/src/components/segmented-control.tsx @@ -29,7 +29,7 @@ export default function SegmentedControl({ return { width: `calc(${pillWidth})`, - left: activeIndex === -1 ? "8px" : leftOffset, + left: activeIndex === -1 ? (hidePadding ? "0px" : "8px") : leftOffset, }; }, [hidePadding, activeIndex, count]); From 553ab6f3212769a28ff25d233dec48f8d3e21a0a Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 4 May 2026 17:08:54 -0400 Subject: [PATCH 039/112] Simplify fallback padding --- frontend/src/components/segmented-control.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/segmented-control.tsx b/frontend/src/components/segmented-control.tsx index baa198bca..3a5875e3b 100644 --- a/frontend/src/components/segmented-control.tsx +++ b/frontend/src/components/segmented-control.tsx @@ -24,12 +24,14 @@ export default function SegmentedControl({ // Calculate dynamic style for the sliding pill const pillStyle = useMemo(() => { - const pillWidth = `(100% - ${hidePadding ? "0px" : "16px"}) / ${count}`; - const leftOffset = `calc(${hidePadding ? "0px" : "8px"} + (${pillWidth}) * ${activeIndex})`; + const baseOffset = hidePadding ? 0 : 8; + + const pillWidth = `(100% - ${baseOffset * 2}px) / ${count}`; + const leftOffset = `calc(${baseOffset}px + (${pillWidth}) * ${activeIndex})`; return { width: `calc(${pillWidth})`, - left: activeIndex === -1 ? (hidePadding ? "0px" : "8px") : leftOffset, + left: activeIndex === -1 ? `${baseOffset}px` : leftOffset, }; }, [hidePadding, activeIndex, count]); From 7e49868a4efe3d98c48a8a9773118447dfbac47c Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 4 May 2026 17:45:52 -0400 Subject: [PATCH 040/112] Create CookieGuard --- frontend/src/app/layout.tsx | 7 ++-- frontend/src/components/cookie-guard.tsx | 41 ++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/cookie-guard.tsx diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 7379ab33c..0f4f276b2 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -2,6 +2,7 @@ import { Analytics } from "@vercel/analytics/next"; import type { Metadata } from "next"; import { Modak, Nunito } from "next/font/google"; +import { CookieGuard } from "@/components/cookie-guard"; import { AccountDetails } from "@/features/account/type"; import Header from "@/features/header/components/header"; import { Providers } from "@/lib/providers"; @@ -100,8 +101,10 @@ export default async function RootLayout({
-
- {children} + +
+ {children} +
diff --git a/frontend/src/components/cookie-guard.tsx b/frontend/src/components/cookie-guard.tsx new file mode 100644 index 000000000..ffe397aaf --- /dev/null +++ b/frontend/src/components/cookie-guard.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { useEffect, useState } from "react"; + +import { cn } from "@/lib/utils/classname"; + +export function CookieGuard({ children }: { children: React.ReactNode }) { + const [cookiesEnabled, setCookiesEnabled] = useState(null); + + useEffect(() => { + const testKey = "plancake_test"; + try { + document.cookie = `${testKey}=1; SameSite=Lax`; + const exists = document.cookie.includes(testKey); + document.cookie = `${testKey}=; Max-Age=0; SameSite=Lax`; + setCookiesEnabled(exists); + } catch { + setCookiesEnabled(false); + } + }, []); + + if (cookiesEnabled === false) { + return ( +
+

Cookies Required

+

+ Plancake requires cookies for the site to work properly. Please + enable/unblock them in your browser settings and refresh. +

+
+ ); + } + + // Still checking or enabled + return <>{children}; +} From 0ca38a9359332ee26f4ebd1e5c94c6139977ceba Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 4 May 2026 18:02:36 -0400 Subject: [PATCH 041/112] Update cookie check logic --- frontend/src/components/cookie-guard.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/cookie-guard.tsx b/frontend/src/components/cookie-guard.tsx index ffe397aaf..6be2f26bd 100644 --- a/frontend/src/components/cookie-guard.tsx +++ b/frontend/src/components/cookie-guard.tsx @@ -11,7 +11,9 @@ export function CookieGuard({ children }: { children: React.ReactNode }) { const testKey = "plancake_test"; try { document.cookie = `${testKey}=1; SameSite=Lax`; - const exists = document.cookie.includes(testKey); + const exists = document.cookie + .split(";") + .some((item) => item.trim().startsWith(`${testKey}=`)); document.cookie = `${testKey}=; Max-Age=0; SameSite=Lax`; setCookiesEnabled(exists); } catch { From 1f3235e0c3ac19e8fb9241fb7773139432df0d4d Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 4 May 2026 18:17:41 -0400 Subject: [PATCH 042/112] Remove redundant text color class --- frontend/src/components/cookie-guard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/cookie-guard.tsx b/frontend/src/components/cookie-guard.tsx index 6be2f26bd..74dbfd205 100644 --- a/frontend/src/components/cookie-guard.tsx +++ b/frontend/src/components/cookie-guard.tsx @@ -30,7 +30,7 @@ export function CookieGuard({ children }: { children: React.ReactNode }) { )} >

Cookies Required

-

+

Plancake requires cookies for the site to work properly. Please enable/unblock them in your browser settings and refresh.

From 05b5cdaad99b17a37a308f0a0bb82fceeda809e3 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 4 May 2026 18:18:53 -0400 Subject: [PATCH 043/112] Update comment in CookieGuard --- frontend/src/components/cookie-guard.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/cookie-guard.tsx b/frontend/src/components/cookie-guard.tsx index 74dbfd205..2e9091db7 100644 --- a/frontend/src/components/cookie-guard.tsx +++ b/frontend/src/components/cookie-guard.tsx @@ -38,6 +38,8 @@ export function CookieGuard({ children }: { children: React.ReactNode }) { ); } - // Still checking or enabled + // Show while still checking or enabled + // We prioritize the experience of users who have cookies enabled rather than strictly + // enforcing the guard while checking return <>{children}; } From 08730f3f863eef5f083371d1eb5038945a1e5b5d Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Wed, 6 May 2026 17:20:40 -0400 Subject: [PATCH 044/112] Update possible dates text for weekday events --- frontend/src/features/event/editor/date-range/selector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/features/event/editor/date-range/selector.tsx b/frontend/src/features/event/editor/date-range/selector.tsx index 90ddc369f..5217eca4d 100644 --- a/frontend/src/features/event/editor/date-range/selector.tsx +++ b/frontend/src/features/event/editor/date-range/selector.tsx @@ -30,7 +30,7 @@ export default function DateRangeSelection({

- Possible Dates + {rangeType === "specific" ? "Possible Dates" : "Possible Days"} {errors.dateRange && ( )} From 48d2febfea2c3c0ea53a7975037f8ee0d06b88ff Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Wed, 6 May 2026 17:36:20 -0400 Subject: [PATCH 045/112] Remove mobile event type switch --- .../editor/date-range/event-type-select.tsx | 6 +++-- .../event/editor/date-range/selector.tsx | 23 +++---------------- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/frontend/src/features/event/editor/date-range/event-type-select.tsx b/frontend/src/features/event/editor/date-range/event-type-select.tsx index fa0d02f66..b37cce438 100644 --- a/frontend/src/features/event/editor/date-range/event-type-select.tsx +++ b/frontend/src/features/event/editor/date-range/event-type-select.tsx @@ -1,6 +1,6 @@ import { useEventContext } from "@/core/event/context"; import { EventType } from "@/core/event/types"; -import Dropdown from "@/features/selector/components/dropdown"; +import Selector from "@/features/selector/components/selector"; type EventTypeSelectProps = { id: string; @@ -15,12 +15,14 @@ export default function EventTypeSelect({ const rangeType = state.eventRange?.type || "specific"; return ( - setEventType(value)} diff --git a/frontend/src/features/event/editor/date-range/selector.tsx b/frontend/src/features/event/editor/date-range/selector.tsx index 5217eca4d..73336525e 100644 --- a/frontend/src/features/event/editor/date-range/selector.tsx +++ b/frontend/src/features/event/editor/date-range/selector.tsx @@ -1,7 +1,6 @@ import { parseISO } from "date-fns"; import { TriangleAlertIcon } from "lucide-react"; -import Switch from "@/components/switch"; import { useEventContext } from "@/core/event/context"; import { SpecificDateRange } from "@/core/event/types"; import WeekdayCalendar from "@/features/event/editor/date-range/calendars/weekday"; @@ -9,24 +8,23 @@ import { DateRangeProps } from "@/features/event/editor/date-range/date-range-pr import DateRangeDrawer from "@/features/event/editor/date-range/drawer"; import EventTypeSelect from "@/features/event/editor/date-range/event-type-select"; import DateRangePopover from "@/features/event/editor/date-range/popover"; -import FormSelectorField from "@/features/selector/components/selector-field"; import useCheckMobile from "@/lib/hooks/use-check-mobile"; export default function DateRangeSelection({ editing = false, }: DateRangeProps) { - const { state, setWeekdayRange, setEventType, errors } = useEventContext(); + const { state, setWeekdayRange, errors } = useEventContext(); const { eventRange, originalEventRange } = state; const rangeType = eventRange?.type ?? "specific"; return (

-
+
-
+

@@ -36,21 +34,6 @@ export default function DateRangeSelection({ )}

- - - setEventType(checked ? "weekday" : "specific") - } - disabled={editing} - /> - - {eventRange?.type === "specific" ? ( Date: Wed, 6 May 2026 17:37:42 -0400 Subject: [PATCH 046/112] Shorten gap between form text and field on editor --- .../event/editor/date-range/selector.tsx | 4 +- frontend/src/features/event/editor/editor.tsx | 46 ++++++++++--------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/frontend/src/features/event/editor/date-range/selector.tsx b/frontend/src/features/event/editor/date-range/selector.tsx index 73336525e..6be7678ce 100644 --- a/frontend/src/features/event/editor/date-range/selector.tsx +++ b/frontend/src/features/event/editor/date-range/selector.tsx @@ -20,11 +20,11 @@ export default function DateRangeSelection({ return (
-
+
-
+

diff --git a/frontend/src/features/event/editor/editor.tsx b/frontend/src/features/event/editor/editor.tsx index 14907a89e..fb2ad7b5a 100644 --- a/frontend/src/features/event/editor/editor.tsx +++ b/frontend/src/features/event/editor/editor.tsx @@ -150,28 +150,30 @@ function EventEditorContent({ type, initialData }: EventEditorProps) { > -

- Possible Times - {errors.timeRange && } -

-
- - - - - - - +
+

+ Possible Times + {errors.timeRange && } +

+
+ + + + + + + +
From 93d89ab4d9de1a639882489ffec3708544309e0a Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Wed, 6 May 2026 19:05:55 -0400 Subject: [PATCH 047/112] Simplify weekday row rounding logic --- .../features/event/editor/date-range/calendars/weekday.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frontend/src/features/event/editor/date-range/calendars/weekday.tsx b/frontend/src/features/event/editor/date-range/calendars/weekday.tsx index f8ec3e97c..86238c44d 100644 --- a/frontend/src/features/event/editor/date-range/calendars/weekday.tsx +++ b/frontend/src/features/event/editor/date-range/calendars/weekday.tsx @@ -62,10 +62,6 @@ export default function WeekdayCalendar({ // Contiguous Rounding Logic isActive && isRangeStart && "rounded-l-full", isActive && isRangeEnd && "rounded-r-full", - isActive && !isRangeStart && !isRangeEnd && "rounded-none", - - // Single Day Case (Start == End) - isActive && isRangeStart && isRangeEnd && "rounded-full", )} > {day} From e2f53fba0f42f45391e949ca8cffe1ddb4e9e0a1 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Thu, 7 May 2026 18:17:45 -0400 Subject: [PATCH 048/112] Update selection style on weekday row --- .../editor/date-range/calendars/weekday.tsx | 68 +++++++++++++------ 1 file changed, 48 insertions(+), 20 deletions(-) diff --git a/frontend/src/features/event/editor/date-range/calendars/weekday.tsx b/frontend/src/features/event/editor/date-range/calendars/weekday.tsx index 86238c44d..985a166b3 100644 --- a/frontend/src/features/event/editor/date-range/calendars/weekday.tsx +++ b/frontend/src/features/event/editor/date-range/calendars/weekday.tsx @@ -15,6 +15,7 @@ export default function WeekdayCalendar({ onChange, }: WeekdayCalendarProps) { const [anchorIndex, setAnchorIndex] = useState(null); + const [hoverIndex, setHoverIndex] = useState(null); const handleDayClick = (index: number) => { if (anchorIndex === null) { @@ -35,37 +36,64 @@ export default function WeekdayCalendar({ .map((d) => ALL_WEEKDAYS.indexOf(d)) .filter((i) => i !== -1); - const start = selectedIndices.length > 0 ? Math.min(...selectedIndices) : -1; - const end = selectedIndices.length > 0 ? Math.max(...selectedIndices) : -1; + let highlightState = selectedIndices.length > 0 ? "active" : "inactive"; + let highlightStart = -1; + let highlightEnd = -1; + if (selectedIndices.length > 0 && anchorIndex === null) { + highlightState = "active"; + highlightStart = Math.min(...selectedIndices); + highlightEnd = Math.max(...selectedIndices); + } else if (anchorIndex !== null) { + highlightState = "hover"; + highlightStart = Math.min(anchorIndex, hoverIndex ?? anchorIndex); + highlightEnd = Math.max(anchorIndex, hoverIndex ?? anchorIndex); + } return (
{ALL_WEEKDAYS.map((day, index) => { - const isActive = index >= start && index <= end; - const isRangeStart = index === start; - const isRangeEnd = index === end; + const isHighlighted = + index >= highlightStart && + index <= highlightEnd && + highlightState !== "inactive"; + const isRangeStart = index === highlightStart; + const isRangeEnd = index === highlightEnd; return ( - + // Hovered state + isHighlighted && + highlightState === "hover" && + "border-foreground/75 border-b-2 border-t-2 border-dashed" + + (isRangeStart ? " pl-5.5 border-l-2" : "") + + (isRangeEnd ? " pr-5.5 border-r-2" : ""), + + // Contiguous Rounding Logic + isRangeStart && "rounded-l-full", + isRangeEnd && "rounded-r-full", + )} + > + {day} + +
); })}
From d4f8bdae715e965a2d7db30048f11d6fba778b28 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Thu, 7 May 2026 18:25:21 -0400 Subject: [PATCH 049/112] Simplify hover highlight classes --- .../event/editor/date-range/calendars/weekday.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/src/features/event/editor/date-range/calendars/weekday.tsx b/frontend/src/features/event/editor/date-range/calendars/weekday.tsx index 985a166b3..e8b84efe6 100644 --- a/frontend/src/features/event/editor/date-range/calendars/weekday.tsx +++ b/frontend/src/features/event/editor/date-range/calendars/weekday.tsx @@ -36,7 +36,7 @@ export default function WeekdayCalendar({ .map((d) => ALL_WEEKDAYS.indexOf(d)) .filter((i) => i !== -1); - let highlightState = selectedIndices.length > 0 ? "active" : "inactive"; + let highlightState = "inactive"; let highlightStart = -1; let highlightEnd = -1; if (selectedIndices.length > 0 && anchorIndex === null) { @@ -82,9 +82,11 @@ export default function WeekdayCalendar({ // Hovered state isHighlighted && highlightState === "hover" && - "border-foreground/75 border-b-2 border-t-2 border-dashed" + - (isRangeStart ? " pl-5.5 border-l-2" : "") + - (isRangeEnd ? " pr-5.5 border-r-2" : ""), + cn( + "border-foreground/75 border-b-2 border-t-2 border-dashed", + isRangeStart ? "pl-5.5 border-l-2" : "", + isRangeEnd ? "pr-5.5 border-r-2" : "", + ), // Contiguous Rounding Logic isRangeStart && "rounded-l-full", From 6e61674553500aa6361891c8552f0c43ad4b0c03 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Thu, 7 May 2026 18:25:36 -0400 Subject: [PATCH 050/112] Add hover color back to unhighlighted state --- .../src/features/event/editor/date-range/calendars/weekday.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/features/event/editor/date-range/calendars/weekday.tsx b/frontend/src/features/event/editor/date-range/calendars/weekday.tsx index e8b84efe6..e570a2c20 100644 --- a/frontend/src/features/event/editor/date-range/calendars/weekday.tsx +++ b/frontend/src/features/event/editor/date-range/calendars/weekday.tsx @@ -72,7 +72,7 @@ export default function WeekdayCalendar({ "active:bg-accent/40 text-foreground/50", // Inactive State - !isHighlighted && "rounded-full", + !isHighlighted && "hover:bg-accent/15 rounded-full", // Active State isHighlighted && From 9fc65643fb21406bf2cb9b9c3775caf4e46a77ac Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Thu, 7 May 2026 18:28:32 -0400 Subject: [PATCH 051/112] Add cursor-pointer to weekday row items --- .../src/features/event/editor/date-range/calendars/weekday.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/features/event/editor/date-range/calendars/weekday.tsx b/frontend/src/features/event/editor/date-range/calendars/weekday.tsx index e570a2c20..4964af220 100644 --- a/frontend/src/features/event/editor/date-range/calendars/weekday.tsx +++ b/frontend/src/features/event/editor/date-range/calendars/weekday.tsx @@ -69,7 +69,7 @@ export default function WeekdayCalendar({
); } diff --git a/frontend/src/features/selector/components/drawer.tsx b/frontend/src/features/selector/components/drawer.tsx index aee0cf3b2..89e11bb35 100644 --- a/frontend/src/features/selector/components/drawer.tsx +++ b/frontend/src/features/selector/components/drawer.tsx @@ -1,5 +1,7 @@ import { cloneElement, useEffect, useRef, useState } from "react"; +import { ChevronDownIcon } from "lucide-react"; + import { FloatingDrawer, StandardDrawer } from "@/features/drawer"; import { DrawerProps } from "@/features/selector/types"; import { cn } from "@/lib/utils/classname"; @@ -78,7 +80,7 @@ export default function SelectorDrawer({ aria-disabled={disabled} className={cn( "relative flex items-center rounded-2xl text-start focus:outline-none", - "bg-accent/15 text-accent-text px-3 py-1", + "bg-accent/15 text-accent-text gap-2 px-3 py-1", open && !disabled && "ring-accent ring-1", // Interactive states only when enabled !disabled && @@ -89,6 +91,7 @@ export default function SelectorDrawer({ )} > {selectLabel} + ) } From 12f5b2ff6c73adad00fba5c795fd37b76897c499 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Thu, 7 May 2026 19:11:00 -0400 Subject: [PATCH 054/112] Adjust text styling on event editor --- .../event/editor/advanced-options.tsx | 72 ++++++++++--------- .../event/editor/date-range/selector.tsx | 6 +- .../date-range/specific-date-display.tsx | 4 +- frontend/src/features/event/editor/editor.tsx | 2 +- .../selector/components/selector-field.tsx | 2 +- 5 files changed, 46 insertions(+), 40 deletions(-) diff --git a/frontend/src/features/event/editor/advanced-options.tsx b/frontend/src/features/event/editor/advanced-options.tsx index 2d76df465..325baa4d6 100644 --- a/frontend/src/features/event/editor/advanced-options.tsx +++ b/frontend/src/features/event/editor/advanced-options.tsx @@ -6,7 +6,6 @@ import { useDebouncedCallback } from "use-debounce"; import { useEventContext } from "@/core/event/context"; import TimeZoneSelector from "@/features/event/components/selectors/timezone"; -import FormSelectorField from "@/features/selector/components/selector-field"; import { MESSAGES } from "@/lib/messages"; import { clientPost } from "@/lib/utils/api/client-fetch"; import { ROUTES } from "@/lib/utils/api/endpoints"; @@ -37,9 +36,7 @@ export default function AdvancedOptions(props: AdvancedOptionsProps) { >
- - Advanced Options - + Advanced Options
@@ -80,37 +77,44 @@ function Options({ isEditing = false, errors }: AdvancedOptionsProps) { return ( <> - - - +
+ +
+ +
+
- - +
+ + +
); } diff --git a/frontend/src/features/event/editor/date-range/selector.tsx b/frontend/src/features/event/editor/date-range/selector.tsx index 6be7678ce..a4c41cc30 100644 --- a/frontend/src/features/event/editor/date-range/selector.tsx +++ b/frontend/src/features/event/editor/date-range/selector.tsx @@ -21,12 +21,14 @@ export default function DateRangeSelection({ return (
- +

{rangeType === "specific" ? "Possible Dates" : "Possible Days"} {errors.dateRange && ( diff --git a/frontend/src/features/event/editor/date-range/specific-date-display.tsx b/frontend/src/features/event/editor/date-range/specific-date-display.tsx index 3eff2dd5d..cc7a756e8 100644 --- a/frontend/src/features/event/editor/date-range/specific-date-display.tsx +++ b/frontend/src/features/event/editor/date-range/specific-date-display.tsx @@ -23,7 +23,7 @@ export default function SpecificDateRangeDisplay({ open={open} /> - TO + TO -

{mobileLabel}

+

{mobileLabel}

Possible Times {errors.timeRange && } diff --git a/frontend/src/features/selector/components/selector-field.tsx b/frontend/src/features/selector/components/selector-field.tsx index 033261647..e475caf47 100644 --- a/frontend/src/features/selector/components/selector-field.tsx +++ b/frontend/src/features/selector/components/selector-field.tsx @@ -23,7 +23,7 @@ export default function FormSelectorField({ classname, )} > -

- + @@ -93,7 +93,7 @@ function Options({ isEditing = false, errors }: AdvancedOptionsProps) {