From 43cb44d6c305c69f527a59eca2445c666af4fd55 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Fri, 3 Jul 2026 14:31:07 -0400 Subject: [PATCH 01/71] Create guest data summary endpoint --- backend/api/guest_import/urls.py | 7 ++++ backend/api/guest_import/utils.py | 38 ++++++++++++++++++++ backend/api/guest_import/views.py | 58 +++++++++++++++++++++++++++++++ backend/api/urls.py | 1 + 4 files changed, 104 insertions(+) create mode 100644 backend/api/guest_import/urls.py create mode 100644 backend/api/guest_import/utils.py create mode 100644 backend/api/guest_import/views.py diff --git a/backend/api/guest_import/urls.py b/backend/api/guest_import/urls.py new file mode 100644 index 00000000..c46c1cd1 --- /dev/null +++ b/backend/api/guest_import/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + path("get-summary/", views.get_summary), +] diff --git a/backend/api/guest_import/utils.py b/backend/api/guest_import/utils.py new file mode 100644 index 00000000..4d49701a --- /dev/null +++ b/backend/api/guest_import/utils.py @@ -0,0 +1,38 @@ +import logging + +from django.db import transaction + +from api.models import UserSession +from api.settings import GUEST_COOKIE_NAME +from api.utils import get_session, set_session_cookie, update_session_metadata + +logger = logging.getLogger("api") + + +def get_guest_account(request, response): + """ + Returns the guest account associated with the request, refreshing the session token + cookie and updating the metadata on the session. + + If there is no guest account, this will return `None`. + + This does NOT make the guest user available on the `request` object. + """ + guest_token = request.COOKIES.get(GUEST_COOKIE_NAME) + + if guest_token: + logger.debug("Guest session token: %s", guest_token) + try: + with transaction.atomic(): + session = get_session(guest_token) + if not session: + raise UserSession.DoesNotExist + + update_session_metadata(request, session) + + set_session_cookie(response, GUEST_COOKIE_NAME, session.session_token, True) + return session.user_account + except UserSession.DoesNotExist: + logger.info("Guest session expired.") + + return None diff --git a/backend/api/guest_import/views.py b/backend/api/guest_import/views.py new file mode 100644 index 00000000..12f757a7 --- /dev/null +++ b/backend/api/guest_import/views.py @@ -0,0 +1,58 @@ +import logging + +from django.db.models import Q +from rest_framework import serializers +from rest_framework.response import Response + +from api.decorators import ( + api_endpoint, + require_account_auth, + validate_output, +) +from api.guest_import.utils import get_guest_account +from api.models import ( + EventParticipant, + UserEvent, +) + +logger = logging.getLogger("api") + + +class GuestDataSummarySerializer(serializers.Serializer): + created_events = serializers.IntegerField() + participated_events = serializers.IntegerField() + + +@api_endpoint("GET") +@require_account_auth +@validate_output(GuestDataSummarySerializer) +def get_summary(request): + """ + Returns a count of events created and participated in by the guest user. + """ + response = Response() + + guest_user = get_guest_account(request, response) + + if guest_user is None: + response.data = { + "created_events": 0, + "participated_events": 0, + } + response.status_code = 200 + return response + + created_events = UserEvent.objects.filter( + user_account=guest_user, url_code__isnull=False + ).count() + # Don't include events that the user both created and participated in + participations = EventParticipant.objects.filter( + ~Q(user_event__user_account=guest_user), + user_account=guest_user, + user_event__url_code__isnull=False, + ).count() + + return Response( + {"created_events": created_events, "participated_events": participations}, + status=200, + ) diff --git a/backend/api/urls.py b/backend/api/urls.py index 4f12fec0..6c4a104e 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -7,4 +7,5 @@ path("availability/", include("api.availability.urls")), path("dashboard/", include("api.dashboard.urls")), path("account/", include("api.account.urls")), + path("guest-import/", include("api.guest_import.urls")), ] From 5fe22512216b935ed31414afbb1934dfcd4f878b Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Fri, 3 Jul 2026 14:33:23 -0400 Subject: [PATCH 02/71] Create frontend mapping for guest data summary --- frontend/src/lib/utils/api/endpoints.ts | 8 ++++++++ frontend/src/lib/utils/api/types.ts | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/frontend/src/lib/utils/api/endpoints.ts b/frontend/src/lib/utils/api/endpoints.ts index 72797104..9b79c704 100644 --- a/frontend/src/lib/utils/api/endpoints.ts +++ b/frontend/src/lib/utils/api/endpoints.ts @@ -24,6 +24,7 @@ import { ActiveSessionList, SessionId, TrueCode, + GuestDataSummary, } from "@/lib/utils/api/types"; /** @@ -270,4 +271,11 @@ export const ROUTES = { */ deleteAccount: route("/account/delete-account/"), }, + guestImport: { + /** + * Gets a count of events created and participated in by the guest user. + * @method GET + */ + getSummary: route("/guest-import/get-summary/"), + } } as const; diff --git a/frontend/src/lib/utils/api/types.ts b/frontend/src/lib/utils/api/types.ts index e8761362..d476404e 100644 --- a/frontend/src/lib/utils/api/types.ts +++ b/frontend/src/lib/utils/api/types.ts @@ -162,3 +162,8 @@ export type AuthedPasswordResetData = { new_password: string; prune_sessions?: boolean; } + +export type GuestDataSummary = { + created_events: number; + participated_events: number; +} From 26428336e29113199028c818127a88c4f2e648e6 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Fri, 3 Jul 2026 17:08:13 -0400 Subject: [PATCH 03/71] Remove event creation participation filter --- backend/api/guest_import/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/api/guest_import/views.py b/backend/api/guest_import/views.py index 12f757a7..7ee8680a 100644 --- a/backend/api/guest_import/views.py +++ b/backend/api/guest_import/views.py @@ -45,9 +45,7 @@ def get_summary(request): created_events = UserEvent.objects.filter( user_account=guest_user, url_code__isnull=False ).count() - # Don't include events that the user both created and participated in participations = EventParticipant.objects.filter( - ~Q(user_event__user_account=guest_user), user_account=guest_user, user_event__url_code__isnull=False, ).count() From 7b3f19a97bc01fd9b8815690d498f7301860931f Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Fri, 3 Jul 2026 17:21:46 -0400 Subject: [PATCH 04/71] Create initial guest import page --- .../settings/(submenus)/guest-import/page.tsx | 73 +++++++++++++++++++ .../features/account/settings/sidebar-nav.tsx | 1 + 2 files changed, 74 insertions(+) create mode 100644 frontend/src/app/settings/(submenus)/guest-import/page.tsx diff --git a/frontend/src/app/settings/(submenus)/guest-import/page.tsx b/frontend/src/app/settings/(submenus)/guest-import/page.tsx new file mode 100644 index 00000000..a641449a --- /dev/null +++ b/frontend/src/app/settings/(submenus)/guest-import/page.tsx @@ -0,0 +1,73 @@ +import { TriangleAlertIcon } from "lucide-react"; + +import EmptyButton from "@/features/button/components/empty"; +import { ROUTES } from "@/lib/utils/api/endpoints"; +import { serverGet } from "@/lib/utils/api/server-fetch"; +import { cn } from "@/lib/utils/classname"; + +export default async function Page() { + let guestSummary: { + created_events: number; + participated_events: number; + } | null = null; + try { + guestSummary = await serverGet(ROUTES.guestImport.getSummary); + } catch { + // Do nothing, keep guestSummary as null to indicate an error occurred + } + + const guestDataFound = + guestSummary !== null && + (guestSummary?.created_events > 0 || guestSummary?.participated_events > 0); + const createdEvents = guestSummary?.created_events ?? 0; + const participatedEvents = guestSummary?.participated_events ?? 0; + + return ( +
+
+
+

Guest Import

+

+ If you created events or submitted availability while not signed in, + you can transfer that data to your account here. +

+
+ + {guestDataFound ? ( + <> +
+
Guest data found:
+
+ • {createdEvents} event{createdEvents !== 1 ? "s" : ""} that you + created +
+
+ • {participatedEvents} event + {participatedEvents !== 1 ? "s" : ""} that you added your + availability to +
+
+
+ +
+ + ) : guestSummary === null ? ( +
+ + Something went wrong trying to fetch guest data. Please try again + later. +
+ ) : ( +
No guest data found.
+ )} +
+
+ ); +} diff --git a/frontend/src/features/account/settings/sidebar-nav.tsx b/frontend/src/features/account/settings/sidebar-nav.tsx index 2961dcc2..6d4a0ae4 100644 --- a/frontend/src/features/account/settings/sidebar-nav.tsx +++ b/frontend/src/features/account/settings/sidebar-nav.tsx @@ -7,6 +7,7 @@ import { cn } from "@/lib/utils/classname"; const SETTINGS_TABS = [ { href: "/settings", label: "General" }, + { href: "/settings/guest-import", label: "Guest Import" }, { href: "/settings/security", label: "Security" }, { href: "/settings/remove", label: "Account Removal" }, ] as const; From c5fc9c2b95d9893b69b015e46d3a7376225aba83 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Fri, 3 Jul 2026 17:44:28 -0400 Subject: [PATCH 05/71] Change summary endpoint to give full data --- backend/api/guest_import/urls.py | 2 +- backend/api/guest_import/views.py | 68 ++++++++++++++++--- .../settings/(submenus)/guest-import/page.tsx | 36 +++++----- frontend/src/lib/utils/api/endpoints.ts | 6 +- frontend/src/lib/utils/api/types.ts | 18 ++++- 5 files changed, 96 insertions(+), 34 deletions(-) diff --git a/backend/api/guest_import/urls.py b/backend/api/guest_import/urls.py index c46c1cd1..a0aa690c 100644 --- a/backend/api/guest_import/urls.py +++ b/backend/api/guest_import/urls.py @@ -3,5 +3,5 @@ from . import views urlpatterns = [ - path("get-summary/", views.get_summary), + path("get-data/", views.get_data), ] diff --git a/backend/api/guest_import/views.py b/backend/api/guest_import/views.py index 7ee8680a..c836f835 100644 --- a/backend/api/guest_import/views.py +++ b/backend/api/guest_import/views.py @@ -18,39 +18,87 @@ logger = logging.getLogger("api") +class GuestEventSerializer(serializers.Serializer): + url_code = serializers.CharField() + title = serializers.CharField() + + +class GuestParticipationSerializer(serializers.Serializer): + url_code = serializers.CharField() + title = serializers.CharField() + guest_display_name = serializers.CharField() + account_display_name = serializers.CharField(allow_null=True) + + class GuestDataSummarySerializer(serializers.Serializer): - created_events = serializers.IntegerField() - participated_events = serializers.IntegerField() + created_events = serializers.ListField( + child=GuestEventSerializer(), + allow_empty=True, + ) + participated_events = serializers.ListField( + child=GuestParticipationSerializer(), + allow_empty=True, + ) @api_endpoint("GET") @require_account_auth @validate_output(GuestDataSummarySerializer) -def get_summary(request): +def get_data(request): """ - Returns a count of events created and participated in by the guest user. + Returns brief information about events created and participated in by the guest user. """ response = Response() guest_user = get_guest_account(request, response) + account_user = request.user if guest_user is None: response.data = { - "created_events": 0, - "participated_events": 0, + "created_events": [], + "participated_events": [], } response.status_code = 200 return response - created_events = UserEvent.objects.filter( + events = UserEvent.objects.filter( user_account=guest_user, url_code__isnull=False - ).count() + ).select_related("url_code") + created_events = [ + { + "url_code": event.url_code.url_code, + "title": event.title, + } + for event in events + ] + participations = EventParticipant.objects.filter( user_account=guest_user, user_event__url_code__isnull=False, - ).count() + ).select_related("user_event__url_code") + participated_events = [ + { + "url_code": participation.user_event.url_code.url_code, + "title": participation.user_event.title, + "guest_display_name": participation.display_name, + "account_display_name": None, + } + for participation in participations + ] + + conflicts = EventParticipant.objects.filter( + user_account=account_user, + user_event__in=participations.values_list("user_event", flat=True), + ).select_related("user_event__url_code") + for conflict in conflicts: + for participation in participated_events: + if participation["url_code"] == conflict.user_event.url_code.url_code: + participation["account_display_name"] = conflict.display_name return Response( - {"created_events": created_events, "participated_events": participations}, + { + "created_events": created_events, + "participated_events": participated_events, + }, status=200, ) diff --git a/frontend/src/app/settings/(submenus)/guest-import/page.tsx b/frontend/src/app/settings/(submenus)/guest-import/page.tsx index a641449a..7a6adbaf 100644 --- a/frontend/src/app/settings/(submenus)/guest-import/page.tsx +++ b/frontend/src/app/settings/(submenus)/guest-import/page.tsx @@ -3,24 +3,23 @@ import { TriangleAlertIcon } from "lucide-react"; import EmptyButton from "@/features/button/components/empty"; import { ROUTES } from "@/lib/utils/api/endpoints"; import { serverGet } from "@/lib/utils/api/server-fetch"; +import { GuestData } from "@/lib/utils/api/types"; import { cn } from "@/lib/utils/classname"; export default async function Page() { - let guestSummary: { - created_events: number; - participated_events: number; - } | null = null; + let guestData: GuestData | null = null; try { - guestSummary = await serverGet(ROUTES.guestImport.getSummary); + guestData = await serverGet(ROUTES.guestImport.getData); } catch { - // Do nothing, keep guestSummary as null to indicate an error occurred + // Do nothing, keep guestData as null to indicate an error occurred } const guestDataFound = - guestSummary !== null && - (guestSummary?.created_events > 0 || guestSummary?.participated_events > 0); - const createdEvents = guestSummary?.created_events ?? 0; - const participatedEvents = guestSummary?.participated_events ?? 0; + guestData !== null && + (guestData?.created_events.length > 0 || + guestData?.participated_events.length > 0); + const createdEvents = guestData?.created_events ?? []; + const participatedEvents = guestData?.participated_events ?? []; return (
@@ -38,19 +37,22 @@ export default async function Page() {
Guest data found:
- • {createdEvents} event{createdEvents !== 1 ? "s" : ""} that you - created + • {createdEvents.length} event + {createdEvents.length !== 1 ? "s" : ""} that you created
- • {participatedEvents} event - {participatedEvents !== 1 ? "s" : ""} that you added your + • {participatedEvents.length} event + {participatedEvents.length !== 1 ? "s" : ""} that you added your availability to
@@ -58,7 +60,7 @@ export default async function Page() {
- ) : guestSummary === null ? ( + ) : guestData === null ? (
Something went wrong trying to fetch guest data. Please try again diff --git a/frontend/src/lib/utils/api/endpoints.ts b/frontend/src/lib/utils/api/endpoints.ts index 9b79c704..54c907a6 100644 --- a/frontend/src/lib/utils/api/endpoints.ts +++ b/frontend/src/lib/utils/api/endpoints.ts @@ -24,7 +24,7 @@ import { ActiveSessionList, SessionId, TrueCode, - GuestDataSummary, + GuestData, } from "@/lib/utils/api/types"; /** @@ -273,9 +273,9 @@ export const ROUTES = { }, guestImport: { /** - * Gets a count of events created and participated in by the guest user. + * Gets data about the guest user's created and participated events. * @method GET */ - getSummary: route("/guest-import/get-summary/"), + getData: route("/guest-import/get-data/"), } } as const; diff --git a/frontend/src/lib/utils/api/types.ts b/frontend/src/lib/utils/api/types.ts index d476404e..5fa97e61 100644 --- a/frontend/src/lib/utils/api/types.ts +++ b/frontend/src/lib/utils/api/types.ts @@ -163,7 +163,19 @@ export type AuthedPasswordResetData = { prune_sessions?: boolean; } -export type GuestDataSummary = { - created_events: number; - participated_events: number; +type GuestCreatedEvent = { + url_code: string; + title: string; +} + +type GuestParticipatedEvent = { + url_code: string; + title: string; + guest_display_name: string; + account_display_name: string | null; +} + +export type GuestData = { + created_events: GuestCreatedEvent[]; + participated_events: GuestParticipatedEvent[]; } From 2893a33889347da0bc811e16b601f65e080e76cb Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Sat, 4 Jul 2026 14:52:28 -0400 Subject: [PATCH 06/71] Add use client to client components For some reason, they were preventing me from using the banner component in a server component. --- frontend/src/features/drawer/components/base.tsx | 2 ++ .../src/features/system-feedback/dialog/components/base.tsx | 2 ++ .../features/system-feedback/dialog/components/confirmation.tsx | 2 ++ .../src/features/system-feedback/dialog/components/form.tsx | 2 ++ frontend/src/features/system-feedback/toast/context.ts | 2 ++ 5 files changed, 10 insertions(+) diff --git a/frontend/src/features/drawer/components/base.tsx b/frontend/src/features/drawer/components/base.tsx index 04d03d6c..d01070b3 100644 --- a/frontend/src/features/drawer/components/base.tsx +++ b/frontend/src/features/drawer/components/base.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useEffect, useRef, useState } from "react"; import { XIcon } from "lucide-react"; diff --git a/frontend/src/features/system-feedback/dialog/components/base.tsx b/frontend/src/features/system-feedback/dialog/components/base.tsx index 8dd0d5d4..9f4e6612 100644 --- a/frontend/src/features/system-feedback/dialog/components/base.tsx +++ b/frontend/src/features/system-feedback/dialog/components/base.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useCallback, useState } from "react"; import * as Dialog from "@radix-ui/react-dialog"; diff --git a/frontend/src/features/system-feedback/dialog/components/confirmation.tsx b/frontend/src/features/system-feedback/dialog/components/confirmation.tsx index 648a3db0..c693875f 100644 --- a/frontend/src/features/system-feedback/dialog/components/confirmation.tsx +++ b/frontend/src/features/system-feedback/dialog/components/confirmation.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useCallback, useState } from "react"; import ActionButton from "@/features/button/components/action"; diff --git a/frontend/src/features/system-feedback/dialog/components/form.tsx b/frontend/src/features/system-feedback/dialog/components/form.tsx index babb8bf5..c7d68ad7 100644 --- a/frontend/src/features/system-feedback/dialog/components/form.tsx +++ b/frontend/src/features/system-feedback/dialog/components/form.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useCallback, useState } from "react"; import ActionButton from "@/features/button/components/action"; diff --git a/frontend/src/features/system-feedback/toast/context.ts b/frontend/src/features/system-feedback/toast/context.ts index 189b541d..4f07c6b1 100644 --- a/frontend/src/features/system-feedback/toast/context.ts +++ b/frontend/src/features/system-feedback/toast/context.ts @@ -1,3 +1,5 @@ +"use client"; + import { createContext, useContext } from "react"; import { ToastOptions } from "@/features/system-feedback/toast/type"; From 1f4ce8a7a1ec84e57ed4d5c40889a87ba56d3bba Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Sat, 4 Jul 2026 14:56:47 -0400 Subject: [PATCH 07/71] Add conflict indicator and information --- .../settings/(submenus)/guest-import/page.tsx | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/settings/(submenus)/guest-import/page.tsx b/frontend/src/app/settings/(submenus)/guest-import/page.tsx index 7a6adbaf..695d1f64 100644 --- a/frontend/src/app/settings/(submenus)/guest-import/page.tsx +++ b/frontend/src/app/settings/(submenus)/guest-import/page.tsx @@ -1,6 +1,7 @@ -import { TriangleAlertIcon } from "lucide-react"; +import { CornerDownRightIcon, TriangleAlertIcon } from "lucide-react"; import EmptyButton from "@/features/button/components/empty"; +import { Banner } from "@/features/system-feedback"; import { ROUTES } from "@/lib/utils/api/endpoints"; import { serverGet } from "@/lib/utils/api/server-fetch"; import { GuestData } from "@/lib/utils/api/types"; @@ -20,6 +21,10 @@ export default async function Page() { guestData?.participated_events.length > 0); const createdEvents = guestData?.created_events ?? []; const participatedEvents = guestData?.participated_events ?? []; + const conflictCount = + guestData?.participated_events.filter( + (event) => event.account_display_name !== null, + ).length ?? 0; return (
@@ -55,7 +60,21 @@ export default async function Page() { {participatedEvents.length !== 1 ? "s" : ""} that you added your availability to
+ {conflictCount > 0 && ( +
+ + {conflictCount} conflict{conflictCount !== 1 ? "s" : ""} found +
+ )}
+ {conflictCount > 0 && ( + + Conflicts occur when you{"'"}ve added availability to the same + event both as a guest and from your account. You can only keep + one when importing, since each user can only have one submission + to an event. + + )}
From df04288ae79a717c1f78c9f686d8c804e587383b Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Sat, 4 Jul 2026 14:57:28 -0400 Subject: [PATCH 08/71] Adjust guest data found text --- frontend/src/app/settings/(submenus)/guest-import/page.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/settings/(submenus)/guest-import/page.tsx b/frontend/src/app/settings/(submenus)/guest-import/page.tsx index 695d1f64..ef298897 100644 --- a/frontend/src/app/settings/(submenus)/guest-import/page.tsx +++ b/frontend/src/app/settings/(submenus)/guest-import/page.tsx @@ -40,7 +40,9 @@ export default async function Page() { {guestDataFound ? ( <>
-
Guest data found:
+
+ Guest data found on this browser: +
Date: Sat, 4 Jul 2026 15:12:39 -0400 Subject: [PATCH 09/71] Add mobile drawer conversion to FormDialog --- .../src/features/system-feedback/dialog/components/form.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/src/features/system-feedback/dialog/components/form.tsx b/frontend/src/features/system-feedback/dialog/components/form.tsx index c7d68ad7..03c3dfd9 100644 --- a/frontend/src/features/system-feedback/dialog/components/form.tsx +++ b/frontend/src/features/system-feedback/dialog/components/form.tsx @@ -7,6 +7,7 @@ import EmptyButton from "@/features/button/components/empty"; 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 useCheckMobile from "@/lib/hooks/use-check-mobile"; import { cn } from "@/lib/utils/classname"; export default function FormDialog({ @@ -29,6 +30,11 @@ export default function FormDialog({ const isControlled = controlledOpen !== undefined; const open = isControlled ? controlledOpen : uncontrolledOpen; + const isMobile = useCheckMobile(); + if (isMobile && !asNestedDrawer) { + asNestedDrawer = true; + } + const handleOpenChange = useCallback( (newOpen: boolean) => { if (!isControlled) { From 9cd3eefc8b64b5217ac76a793b988cb7e0f78a52 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Sat, 4 Jul 2026 15:14:17 -0400 Subject: [PATCH 10/71] Remove unnecessary asNestedDrawer props --- .../account/setting-dialogs/change-password/main-dialog.tsx | 3 --- .../src/features/account/setting-dialogs/delete-account.tsx | 5 +---- 2 files changed, 1 insertion(+), 7 deletions(-) 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 63826de1..6994d3b4 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 @@ -8,10 +8,8 @@ import ResetStep from "@/features/account/setting-dialogs/change-password/steps/ import { useChangePasswordFlow } from "@/features/account/setting-dialogs/change-password/use-change-password"; import EmptyButton from "@/features/button/components/empty"; import { FormDialog } from "@/features/system-feedback"; -import useCheckMobile from "@/lib/hooks/use-check-mobile"; export default function ChangePasswordDialog() { - const isMobile = useCheckMobile(); const flow = useChangePasswordFlow(); const [renderedStep, setRenderedStep] = useState(flow.step); @@ -56,7 +54,6 @@ export default function ChangePasswordDialog() { return ( Date: Sat, 4 Jul 2026 17:51:59 -0400 Subject: [PATCH 11/71] Add submitDisabled prop to FormDialog --- .../src/features/system-feedback/dialog/components/form.tsx | 2 ++ frontend/src/features/system-feedback/dialog/props.ts | 3 +++ 2 files changed, 5 insertions(+) diff --git a/frontend/src/features/system-feedback/dialog/components/form.tsx b/frontend/src/features/system-feedback/dialog/components/form.tsx index 03c3dfd9..5065179a 100644 --- a/frontend/src/features/system-feedback/dialog/components/form.tsx +++ b/frontend/src/features/system-feedback/dialog/components/form.tsx @@ -16,6 +16,7 @@ export default function FormDialog({ description, onSubmit, submitLabel = "Save", + submitDisabled = false, cancelLabel = "Cancel", children, trigger, @@ -87,6 +88,7 @@ export default function FormDialog({ type="submit" buttonStyle={config.buttonStyle} label={submitLabel} + disabled={submitDisabled} />
diff --git a/frontend/src/features/system-feedback/dialog/props.ts b/frontend/src/features/system-feedback/dialog/props.ts index 08349db8..5a7fc651 100644 --- a/frontend/src/features/system-feedback/dialog/props.ts +++ b/frontend/src/features/system-feedback/dialog/props.ts @@ -104,6 +104,9 @@ export type FormDialogProps = CommonDialogProps & { /** Text for the submit button (Defaults to "Save" or "Submit") */ submitLabel?: string; + /** Whether the submit button is disabled */ + submitDisabled?: boolean; + /** Text for the cancel button (Defaults to "Cancel") */ cancelLabel?: string; }; From d38882a82183145e8ab9b51569c78304e09bb13a Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Sun, 5 Jul 2026 21:01:23 -0400 Subject: [PATCH 12/71] Update conflict banner --- .../settings/(submenus)/guest-import/page.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/settings/(submenus)/guest-import/page.tsx b/frontend/src/app/settings/(submenus)/guest-import/page.tsx index ef298897..f1c28c65 100644 --- a/frontend/src/app/settings/(submenus)/guest-import/page.tsx +++ b/frontend/src/app/settings/(submenus)/guest-import/page.tsx @@ -65,16 +65,22 @@ export default async function Page() { {conflictCount > 0 && (
- {conflictCount} conflict{conflictCount !== 1 ? "s" : ""} found + {conflictCount} conflict{conflictCount !== 1 ? "s" : ""}
)}
{conflictCount > 0 && ( - + Conflicts occur when you{"'"}ve added availability to the same - event both as a guest and from your account. You can only keep - one when importing, since each user can only have one submission - to an event. + event both as a guest and from your account. You can{" "} + + choose which submission to keep + {" "} + in the next step when resolving each conflict, and the other + will be deleted. )}
From 5e6967ce2396ca3d7fd2ce332773380e203680f5 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Sun, 5 Jul 2026 21:15:06 -0400 Subject: [PATCH 13/71] Create guest import dialog --- .../settings/(submenus)/guest-import/page.tsx | 4 +- .../account/setting-dialogs/guest-import.tsx | 169 ++++++++++++++++++ 2 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 frontend/src/features/account/setting-dialogs/guest-import.tsx diff --git a/frontend/src/app/settings/(submenus)/guest-import/page.tsx b/frontend/src/app/settings/(submenus)/guest-import/page.tsx index f1c28c65..b98df4e6 100644 --- a/frontend/src/app/settings/(submenus)/guest-import/page.tsx +++ b/frontend/src/app/settings/(submenus)/guest-import/page.tsx @@ -1,6 +1,6 @@ import { CornerDownRightIcon, TriangleAlertIcon } from "lucide-react"; -import EmptyButton from "@/features/button/components/empty"; +import GuestImportDialog from "@/features/account/setting-dialogs/guest-import"; import { Banner } from "@/features/system-feedback"; import { ROUTES } from "@/lib/utils/api/endpoints"; import { serverGet } from "@/lib/utils/api/server-fetch"; @@ -84,7 +84,7 @@ export default async function Page() { )}
- +
) : guestData === null ? ( diff --git a/frontend/src/features/account/setting-dialogs/guest-import.tsx b/frontend/src/features/account/setting-dialogs/guest-import.tsx new file mode 100644 index 00000000..b63c35e4 --- /dev/null +++ b/frontend/src/features/account/setting-dialogs/guest-import.tsx @@ -0,0 +1,169 @@ +"use client"; + +import { ExternalLinkIcon, TriangleAlertIcon } from "lucide-react"; +import Link from "next/link"; + +import EmptyButton from "@/features/button/components/empty"; +import Selector from "@/features/selector/components/selector"; +import { FormDialog } from "@/features/system-feedback"; +import Tooltip from "@/features/system-feedback/tooltip/base"; +import { GuestData } from "@/lib/utils/api/types"; +import { cn } from "@/lib/utils/classname"; + +export default function GuestImportDialog({ + guestData, +}: { + guestData: GuestData; +}) { + const unresolvedConflicts = guestData.participated_events.some( + (event) => event.account_display_name !== null, + ); + + return ( + } + onSubmit={() => false} + submitLabel="Confirm" + submitDisabled={unresolvedConflicts} + > +
+

Are you sure you want to import this guest data?

+

This action cannot be undone.

+
+ +
+ + {guestData.created_events.map((event) => ( + + ))} + + + {guestData.participated_events.map((event) => { + const conflict = event.account_display_name !== null; + + return ( + + {conflict ? ( +
+
+
+

Guest

+

{event.guest_display_name}

+
+
+

Account

+

{event.account_display_name}

+
+
+
+ {}} + options={[ + { label: "Keep Guest", value: "guest" }, + { + label: "Keep Account", + value: "account", + }, + ]} + value={null} + placeholder="Resolve Conflict" + /> +
+
+ ) : ( +

Name: {event.guest_display_name}

+ )} +
+ ); + })} +
+
+
+ + Please resolve conflicts. +
+
+ ); +} + +function DataSection({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) { + return ( +
+
{title}
+ {children} +
+ ); +} + +function EventDisplay({ + title, + url_code, + conflict, + children, +}: { + title: string; + url_code: string; + conflict?: boolean; + children?: React.ReactNode; +}) { + return ( +
+ + {children} +
+ ); +} + +function EventHeader({ + title, + url_code, + conflict, +}: { + title: string; + url_code: string; + conflict?: boolean; +}) { + return ( +
+
+ {conflict && } +

{title}

+
+ + + View + + + +
+ ); +} From 53fab724282903694dcdf6a4e825f9938fc72723 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 6 Jul 2026 17:06:14 -0400 Subject: [PATCH 14/71] Add conflict resolution logic --- .../account/setting-dialogs/guest-import.tsx | 112 ++++++++++++++---- 1 file changed, 91 insertions(+), 21 deletions(-) diff --git a/frontend/src/features/account/setting-dialogs/guest-import.tsx b/frontend/src/features/account/setting-dialogs/guest-import.tsx index b63c35e4..bad1a926 100644 --- a/frontend/src/features/account/setting-dialogs/guest-import.tsx +++ b/frontend/src/features/account/setting-dialogs/guest-import.tsx @@ -1,6 +1,8 @@ "use client"; -import { ExternalLinkIcon, TriangleAlertIcon } from "lucide-react"; +import { useMemo, useState } from "react"; + +import { CheckIcon, ExternalLinkIcon, TriangleAlertIcon } from "lucide-react"; import Link from "next/link"; import EmptyButton from "@/features/button/components/empty"; @@ -10,15 +12,48 @@ import Tooltip from "@/features/system-feedback/tooltip/base"; import { GuestData } from "@/lib/utils/api/types"; import { cn } from "@/lib/utils/classname"; +type AvailabilityImportChoice = "guest" | "account"; +type ImportPayload = { + [url_code: string]: AvailabilityImportChoice; +}; + export default function GuestImportDialog({ guestData, }: { guestData: GuestData; }) { - const unresolvedConflicts = guestData.participated_events.some( - (event) => event.account_display_name !== null, + const [importPayload, setImportPayload] = useState<{ + [url_code: string]: AvailabilityImportChoice; + }>( + guestData.participated_events.reduce((acc, event) => { + if (event.account_display_name === null) { + acc[event.url_code] = "guest"; + } + return acc; + }, {} as ImportPayload), ); + const hasUnresolvedConflicts = + guestData.participated_events.length !== Object.keys(importPayload).length; + const conflictedEvents = useMemo(() => { + return new Set( + guestData.participated_events + .filter((event) => event.account_display_name !== null) + .map((event) => event.url_code), + ); + }, [guestData.participated_events]); + + const resolveConflict = ( + url_code: string, + choice: AvailabilityImportChoice | null, + ) => { + if (choice === null) return; + setImportPayload((prev) => ({ + ...prev, + [url_code]: choice, + })); + }; + return ( } onSubmit={() => false} submitLabel="Confirm" - submitDisabled={unresolvedConflicts} + submitDisabled={hasUnresolvedConflicts} >

Are you sure you want to import this guest data?

@@ -46,26 +81,29 @@ export default function GuestImportDialog({ {guestData.participated_events.map((event) => { - const conflict = event.account_display_name !== null; + const choice = importPayload[event.url_code] ?? null; + const hasConflict = conflictedEvents.has(event.url_code); return ( - {conflict ? ( + {hasConflict ? (
-
-

Guest

-

{event.guest_display_name}

-
-
-

Account

-

{event.account_display_name}

-
+ +
{}} + onChange={(choice) => + resolveConflict(event.url_code, choice) + } options={[ { label: "Keep Guest", value: "guest" }, { @@ -81,10 +121,15 @@ export default function GuestImportDialog({ value: "account", }, ]} - value={null} + value={choice} placeholder="Resolve Conflict" />
+ {choice !== null && ( +
+ The other submission will be deleted. +
+ )}
) : (

Name: {event.guest_display_name}

@@ -94,10 +139,12 @@ export default function GuestImportDialog({ })}
-
- - Please resolve conflicts. -
+ {hasUnresolvedConflicts && ( +
+ + Please resolve conflicts. +
+ )}
); } @@ -167,3 +214,26 @@ function EventHeader({
); } + +function ConflictOption({ + label, + name, + selected, +}: { + label: string; + name: string; + selected: boolean | null; +}) { + return ( +
+

{label}

+

{name}

+
+ ); +} From d0a3a043feb1fbfc85e5f327e41bb3194cc1b8c0 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 6 Jul 2026 17:23:06 -0400 Subject: [PATCH 15/71] Simplify styling for conflict option --- .../src/features/account/setting-dialogs/guest-import.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/features/account/setting-dialogs/guest-import.tsx b/frontend/src/features/account/setting-dialogs/guest-import.tsx index bad1a926..b2faddbe 100644 --- a/frontend/src/features/account/setting-dialogs/guest-import.tsx +++ b/frontend/src/features/account/setting-dialogs/guest-import.tsx @@ -227,13 +227,13 @@ function ConflictOption({ return (

{label}

-

{name}

+

{name}

); } From b39b118a4582f3851f1853f1d3083790cd46659c Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Tue, 7 Jul 2026 12:57:19 -0400 Subject: [PATCH 16/71] Convert guest import review to page --- .../settings/(submenus)/guest-import/page.tsx | 11 +++++--- .../guest-import/review/page-client.tsx} | 24 +++++------------- .../app/settings/guest-import/review/page.tsx | 25 +++++++++++++++++++ 3 files changed, 38 insertions(+), 22 deletions(-) rename frontend/src/{features/account/setting-dialogs/guest-import.tsx => app/settings/guest-import/review/page-client.tsx} (91%) create mode 100644 frontend/src/app/settings/guest-import/review/page.tsx diff --git a/frontend/src/app/settings/(submenus)/guest-import/page.tsx b/frontend/src/app/settings/(submenus)/guest-import/page.tsx index b98df4e6..b8c8eea2 100644 --- a/frontend/src/app/settings/(submenus)/guest-import/page.tsx +++ b/frontend/src/app/settings/(submenus)/guest-import/page.tsx @@ -1,6 +1,6 @@ import { CornerDownRightIcon, TriangleAlertIcon } from "lucide-react"; -import GuestImportDialog from "@/features/account/setting-dialogs/guest-import"; +import LinkButton from "@/features/button/components/link"; import { Banner } from "@/features/system-feedback"; import { ROUTES } from "@/lib/utils/api/endpoints"; import { serverGet } from "@/lib/utils/api/server-fetch"; @@ -83,9 +83,12 @@ export default async function Page() { will be deleted.
)} -
- -
+ ) : guestData === null ? (
diff --git a/frontend/src/features/account/setting-dialogs/guest-import.tsx b/frontend/src/app/settings/guest-import/review/page-client.tsx similarity index 91% rename from frontend/src/features/account/setting-dialogs/guest-import.tsx rename to frontend/src/app/settings/guest-import/review/page-client.tsx index b2faddbe..bcbc8bce 100644 --- a/frontend/src/features/account/setting-dialogs/guest-import.tsx +++ b/frontend/src/app/settings/guest-import/review/page-client.tsx @@ -2,12 +2,11 @@ import { useMemo, useState } from "react"; -import { CheckIcon, ExternalLinkIcon, TriangleAlertIcon } from "lucide-react"; +import { ExternalLinkIcon, TriangleAlertIcon } from "lucide-react"; import Link from "next/link"; -import EmptyButton from "@/features/button/components/empty"; +import HeaderSpacer from "@/features/header/components/header-spacer"; import Selector from "@/features/selector/components/selector"; -import { FormDialog } from "@/features/system-feedback"; import Tooltip from "@/features/system-feedback/tooltip/base"; import { GuestData } from "@/lib/utils/api/types"; import { cn } from "@/lib/utils/classname"; @@ -17,11 +16,7 @@ type ImportPayload = { [url_code: string]: AvailabilityImportChoice; }; -export default function GuestImportDialog({ - guestData, -}: { - guestData: GuestData; -}) { +export default function ClientPage({ guestData }: { guestData: GuestData }) { const [importPayload, setImportPayload] = useState<{ [url_code: string]: AvailabilityImportChoice; }>( @@ -55,15 +50,8 @@ export default function GuestImportDialog({ }; return ( - } - onSubmit={() => false} - submitLabel="Confirm" - submitDisabled={hasUnresolvedConflicts} - > +
+

Are you sure you want to import this guest data?

This action cannot be undone.

@@ -145,7 +133,7 @@ export default function GuestImportDialog({ Please resolve conflicts.
)} - +
); } diff --git a/frontend/src/app/settings/guest-import/review/page.tsx b/frontend/src/app/settings/guest-import/review/page.tsx new file mode 100644 index 00000000..9a4ce1bf --- /dev/null +++ b/frontend/src/app/settings/guest-import/review/page.tsx @@ -0,0 +1,25 @@ +import { redirect } from "next/navigation"; + +import ClientPage from "@/app/settings/guest-import/review/page-client"; +import { ROUTES } from "@/lib/utils/api/endpoints"; +import { serverGet } from "@/lib/utils/api/server-fetch"; +import { GuestData } from "@/lib/utils/api/types"; + +export default async function Page() { + let guestData: GuestData | null = null; + try { + guestData = await serverGet(ROUTES.guestImport.getData); + } catch { + // Do nothing, keep guestData as null + } + + const guestDataFound = + guestData !== null && + (guestData?.created_events.length > 0 || + guestData?.participated_events.length > 0); + if (!guestDataFound) { + redirect("/settings/guest-import"); + } + + return ; +} From 820bee8c3ae5b56d845151ccaa2f73466b1b1091 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Tue, 7 Jul 2026 16:13:12 -0400 Subject: [PATCH 17/71] Overhaul guest data review layout --- .../guest-import/review/page-client.tsx | 68 ++++++++++++++----- 1 file changed, 52 insertions(+), 16 deletions(-) diff --git a/frontend/src/app/settings/guest-import/review/page-client.tsx b/frontend/src/app/settings/guest-import/review/page-client.tsx index bcbc8bce..a2d9e9dd 100644 --- a/frontend/src/app/settings/guest-import/review/page-client.tsx +++ b/frontend/src/app/settings/guest-import/review/page-client.tsx @@ -5,6 +5,8 @@ import { useMemo, useState } from "react"; import { ExternalLinkIcon, TriangleAlertIcon } from "lucide-react"; import Link from "next/link"; +import MobileFooterIsland from "@/components/mobile-footer-island"; +import LinkButton from "@/features/button/components/link"; import HeaderSpacer from "@/features/header/components/header-spacer"; import Selector from "@/features/selector/components/selector"; import Tooltip from "@/features/system-feedback/tooltip/base"; @@ -28,8 +30,8 @@ export default function ClientPage({ guestData }: { guestData: GuestData }) { }, {} as ImportPayload), ); - const hasUnresolvedConflicts = - guestData.participated_events.length !== Object.keys(importPayload).length; + const unresolvedConflicts = + guestData.participated_events.length - Object.keys(importPayload).length; const conflictedEvents = useMemo(() => { return new Set( guestData.participated_events @@ -49,15 +51,42 @@ export default function ClientPage({ guestData }: { guestData: GuestData }) { })); }; + const cancelButton = ( + + ); + + const importButton = ( + 0} + /> + ); + + const actionText = ( +

+ Are you sure you want to import this guest data? This action cannot be + undone. +

+ ); + return ( -
+
-
-

Are you sure you want to import this guest data?

-

This action cannot be undone.

+
+

Guest Import Review

+
+ {cancelButton} + {importButton} +
- -
+

{actionText}

+
{guestData.created_events.map((event) => (
- {hasUnresolvedConflicts && ( -
- - Please resolve conflicts. -
- )} + + {unresolvedConflicts > 0 ? ( +
+ + Please resolve conflicts. +
+ ) : ( + actionText + )} +
); } @@ -145,7 +181,7 @@ function DataSection({ children: React.ReactNode; }) { return ( -
+
{title}
{children}
@@ -164,7 +200,7 @@ function EventDisplay({ children?: React.ReactNode; }) { return ( -
+
{children}
From 4dc3bc3798c3a7a904d9c0376d86743f2ff1092c Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Tue, 7 Jul 2026 16:14:39 -0400 Subject: [PATCH 18/71] Update submission deletion message --- frontend/src/app/settings/guest-import/review/page-client.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/settings/guest-import/review/page-client.tsx b/frontend/src/app/settings/guest-import/review/page-client.tsx index a2d9e9dd..7d4530b0 100644 --- a/frontend/src/app/settings/guest-import/review/page-client.tsx +++ b/frontend/src/app/settings/guest-import/review/page-client.tsx @@ -144,7 +144,8 @@ export default function ClientPage({ guestData }: { guestData: GuestData }) {
{choice !== null && (
- The other submission will be deleted. + The {choice === "guest" ? "account" : "guest"}{" "} + submission will be deleted.
)}
From 1d8dde77ffb01e74a5c11a72d48807893b748ba9 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Tue, 7 Jul 2026 16:34:18 -0400 Subject: [PATCH 19/71] Add conflict banner to review page --- .../settings/guest-import/review/page-client.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/frontend/src/app/settings/guest-import/review/page-client.tsx b/frontend/src/app/settings/guest-import/review/page-client.tsx index 7d4530b0..88ea9074 100644 --- a/frontend/src/app/settings/guest-import/review/page-client.tsx +++ b/frontend/src/app/settings/guest-import/review/page-client.tsx @@ -9,6 +9,7 @@ import MobileFooterIsland from "@/components/mobile-footer-island"; import LinkButton from "@/features/button/components/link"; import HeaderSpacer from "@/features/header/components/header-spacer"; import Selector from "@/features/selector/components/selector"; +import { Banner } from "@/features/system-feedback"; import Tooltip from "@/features/system-feedback/tooltip/base"; import { GuestData } from "@/lib/utils/api/types"; import { cn } from "@/lib/utils/classname"; @@ -97,6 +98,18 @@ export default function ClientPage({ guestData }: { guestData: GuestData }) { ))} + {unresolvedConflicts > 0 && ( + + Conflicts occur when you{"'"}ve added availability to the same + event both as a guest and from your account. You must{" "} + choose which submission to keep{" "} + when resolving each conflict. + + )} + {guestData.participated_events.map((event) => { const choice = importPayload[event.url_code] ?? null; const hasConflict = conflictedEvents.has(event.url_code); From 0d0cf399f578ca2ea813ef953872f6d4b830e609 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Tue, 7 Jul 2026 16:35:57 -0400 Subject: [PATCH 20/71] Create serializers file --- backend/api/guest_import/serializers.py | 24 +++++++++++++++++++++++ backend/api/guest_import/views.py | 26 +------------------------ 2 files changed, 25 insertions(+), 25 deletions(-) create mode 100644 backend/api/guest_import/serializers.py diff --git a/backend/api/guest_import/serializers.py b/backend/api/guest_import/serializers.py new file mode 100644 index 00000000..635b9569 --- /dev/null +++ b/backend/api/guest_import/serializers.py @@ -0,0 +1,24 @@ +from rest_framework import serializers + + +class GuestEventSerializer(serializers.Serializer): + url_code = serializers.CharField() + title = serializers.CharField() + + +class GuestParticipationSerializer(serializers.Serializer): + url_code = serializers.CharField() + title = serializers.CharField() + guest_display_name = serializers.CharField() + account_display_name = serializers.CharField(allow_null=True) + + +class GuestDataSummarySerializer(serializers.Serializer): + created_events = serializers.ListField( + child=GuestEventSerializer(), + allow_empty=True, + ) + participated_events = serializers.ListField( + child=GuestParticipationSerializer(), + allow_empty=True, + ) diff --git a/backend/api/guest_import/views.py b/backend/api/guest_import/views.py index c836f835..93207eae 100644 --- a/backend/api/guest_import/views.py +++ b/backend/api/guest_import/views.py @@ -1,7 +1,5 @@ import logging -from django.db.models import Q -from rest_framework import serializers from rest_framework.response import Response from api.decorators import ( @@ -9,6 +7,7 @@ require_account_auth, validate_output, ) +from api.guest_import.serializers import GuestDataSummarySerializer from api.guest_import.utils import get_guest_account from api.models import ( EventParticipant, @@ -18,29 +17,6 @@ logger = logging.getLogger("api") -class GuestEventSerializer(serializers.Serializer): - url_code = serializers.CharField() - title = serializers.CharField() - - -class GuestParticipationSerializer(serializers.Serializer): - url_code = serializers.CharField() - title = serializers.CharField() - guest_display_name = serializers.CharField() - account_display_name = serializers.CharField(allow_null=True) - - -class GuestDataSummarySerializer(serializers.Serializer): - created_events = serializers.ListField( - child=GuestEventSerializer(), - allow_empty=True, - ) - participated_events = serializers.ListField( - child=GuestParticipationSerializer(), - allow_empty=True, - ) - - @api_endpoint("GET") @require_account_auth @validate_output(GuestDataSummarySerializer) From 54e1b71e4856efe55e4978fee10d9f49913371df Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Tue, 7 Jul 2026 16:39:15 -0400 Subject: [PATCH 21/71] Fix guest data endpoint response --- backend/api/guest_import/views.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/api/guest_import/views.py b/backend/api/guest_import/views.py index 93207eae..55c8ee96 100644 --- a/backend/api/guest_import/views.py +++ b/backend/api/guest_import/views.py @@ -71,10 +71,10 @@ def get_data(request): if participation["url_code"] == conflict.user_event.url_code.url_code: participation["account_display_name"] = conflict.display_name - return Response( - { - "created_events": created_events, - "participated_events": participated_events, - }, - status=200, - ) + response.data = { + "created_events": created_events, + "participated_events": participated_events, + } + response.status_code = 200 + + return response From ecc296cd2673f1ca996acd41b9c631434961846f Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Tue, 7 Jul 2026 16:42:23 -0400 Subject: [PATCH 22/71] Re-add guest data summary endpoint --- backend/api/guest_import/serializers.py | 7 ++++- backend/api/guest_import/urls.py | 1 + backend/api/guest_import/views.py | 39 ++++++++++++++++++++++++- frontend/src/lib/utils/api/endpoints.ts | 12 ++++++-- frontend/src/lib/utils/api/types.ts | 5 ++++ 5 files changed, 59 insertions(+), 5 deletions(-) diff --git a/backend/api/guest_import/serializers.py b/backend/api/guest_import/serializers.py index 635b9569..e5081d0e 100644 --- a/backend/api/guest_import/serializers.py +++ b/backend/api/guest_import/serializers.py @@ -1,6 +1,11 @@ from rest_framework import serializers +class GuestDataSummarySerializer(serializers.Serializer): + created_events = serializers.IntegerField() + participated_events = serializers.IntegerField() + + class GuestEventSerializer(serializers.Serializer): url_code = serializers.CharField() title = serializers.CharField() @@ -13,7 +18,7 @@ class GuestParticipationSerializer(serializers.Serializer): account_display_name = serializers.CharField(allow_null=True) -class GuestDataSummarySerializer(serializers.Serializer): +class GuestDataSerializer(serializers.Serializer): created_events = serializers.ListField( child=GuestEventSerializer(), allow_empty=True, diff --git a/backend/api/guest_import/urls.py b/backend/api/guest_import/urls.py index a0aa690c..b8baa6c5 100644 --- a/backend/api/guest_import/urls.py +++ b/backend/api/guest_import/urls.py @@ -3,5 +3,6 @@ from . import views urlpatterns = [ + path("get-summary/", views.get_summary), path("get-data/", views.get_data), ] diff --git a/backend/api/guest_import/views.py b/backend/api/guest_import/views.py index 55c8ee96..8d94d284 100644 --- a/backend/api/guest_import/views.py +++ b/backend/api/guest_import/views.py @@ -7,7 +7,7 @@ require_account_auth, validate_output, ) -from api.guest_import.serializers import GuestDataSummarySerializer +from api.guest_import.serializers import GuestDataSerializer, GuestDataSummarySerializer from api.guest_import.utils import get_guest_account from api.models import ( EventParticipant, @@ -20,6 +20,43 @@ @api_endpoint("GET") @require_account_auth @validate_output(GuestDataSummarySerializer) +def get_summary(request): + """ + Returns counts of events created and participated in by the guest user. + """ + response = Response() + + guest_user = get_guest_account(request, response) + + if guest_user is None: + response.data = { + "created_events": 0, + "participated_events": 0, + } + response.status_code = 200 + return response + + events = UserEvent.objects.filter( + user_account=guest_user, url_code__isnull=False + ).count() + + participations = EventParticipant.objects.filter( + user_account=guest_user, + user_event__url_code__isnull=False, + ).count() + + response.data = { + "created_events": events, + "participated_events": participations, + } + response.status_code = 200 + + return response + + +@api_endpoint("GET") +@require_account_auth +@validate_output(GuestDataSerializer) def get_data(request): """ Returns brief information about events created and participated in by the guest user. diff --git a/frontend/src/lib/utils/api/endpoints.ts b/frontend/src/lib/utils/api/endpoints.ts index 54c907a6..fc396a24 100644 --- a/frontend/src/lib/utils/api/endpoints.ts +++ b/frontend/src/lib/utils/api/endpoints.ts @@ -1,5 +1,6 @@ import { AccountData, + ActiveSessionList, AllAvailability, AuthedPasswordResetCode, AuthedPasswordResetData, @@ -12,6 +13,8 @@ import { EventDetails, EventDisplayNameData, EventEditData, + GuestData, + GuestDataSummary, LoginData, MessageResponse, NewEventData, @@ -20,11 +23,9 @@ import { PasswordResetData, RegisterData, SelfAvailability, - VerificationCode, - ActiveSessionList, SessionId, TrueCode, - GuestData, + VerificationCode, } from "@/lib/utils/api/types"; /** @@ -272,6 +273,11 @@ export const ROUTES = { deleteAccount: route("/account/delete-account/"), }, guestImport: { + /** + * Gets a summary of the guest user's created and participated events. + * @method GET + */ + getSummary: route("/guest-import/get-summary/"), /** * Gets data about the guest user's created and participated events. * @method GET diff --git a/frontend/src/lib/utils/api/types.ts b/frontend/src/lib/utils/api/types.ts index 5fa97e61..b88a23e7 100644 --- a/frontend/src/lib/utils/api/types.ts +++ b/frontend/src/lib/utils/api/types.ts @@ -163,6 +163,11 @@ export type AuthedPasswordResetData = { prune_sessions?: boolean; } +export type GuestDataSummary = { + created_events: number; + participated_events: number; +} + type GuestCreatedEvent = { url_code: string; title: string; From 51cb64b8d64ae64ea05174bbd8f8786049776a3b Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Tue, 7 Jul 2026 16:55:16 -0400 Subject: [PATCH 23/71] Convert settings page to use summary --- .../settings/(submenus)/guest-import/page.tsx | 65 +++++-------------- 1 file changed, 18 insertions(+), 47 deletions(-) diff --git a/frontend/src/app/settings/(submenus)/guest-import/page.tsx b/frontend/src/app/settings/(submenus)/guest-import/page.tsx index b8c8eea2..c64cfb81 100644 --- a/frontend/src/app/settings/(submenus)/guest-import/page.tsx +++ b/frontend/src/app/settings/(submenus)/guest-import/page.tsx @@ -1,30 +1,24 @@ -import { CornerDownRightIcon, TriangleAlertIcon } from "lucide-react"; +import { TriangleAlertIcon } from "lucide-react"; import LinkButton from "@/features/button/components/link"; -import { Banner } from "@/features/system-feedback"; import { ROUTES } from "@/lib/utils/api/endpoints"; import { serverGet } from "@/lib/utils/api/server-fetch"; -import { GuestData } from "@/lib/utils/api/types"; +import { GuestDataSummary } from "@/lib/utils/api/types"; import { cn } from "@/lib/utils/classname"; export default async function Page() { - let guestData: GuestData | null = null; + let guestSummary: GuestDataSummary | null = null; try { - guestData = await serverGet(ROUTES.guestImport.getData); + guestSummary = await serverGet(ROUTES.guestImport.getSummary); } catch { - // Do nothing, keep guestData as null to indicate an error occurred + // Do nothing, keep guestSummary as null to indicate an error occurred } - const guestDataFound = - guestData !== null && - (guestData?.created_events.length > 0 || - guestData?.participated_events.length > 0); - const createdEvents = guestData?.created_events ?? []; - const participatedEvents = guestData?.participated_events ?? []; - const conflictCount = - guestData?.participated_events.filter( - (event) => event.account_display_name !== null, - ).length ?? 0; + const guestSummaryFound = + guestSummary !== null && + (guestSummary?.created_events > 0 || guestSummary?.participated_events > 0); + const createdEvents = guestSummary?.created_events ?? 0; + const participatedEvents = guestSummary?.participated_events ?? 0; return (
@@ -37,52 +31,29 @@ export default async function Page() {

- {guestDataFound ? ( + {guestSummaryFound ? ( <>
Guest data found on this browser:
- • {createdEvents.length} event - {createdEvents.length !== 1 ? "s" : ""} that you created + • {createdEvents} event + {createdEvents !== 1 ? "s" : ""} that you created
- • {participatedEvents.length} event - {participatedEvents.length !== 1 ? "s" : ""} that you added your + • {participatedEvents} event + {participatedEvents !== 1 ? "s" : ""} that you added your availability to
- {conflictCount > 0 && ( -
- - {conflictCount} conflict{conflictCount !== 1 ? "s" : ""} -
- )}
- {conflictCount > 0 && ( - - Conflicts occur when you{"'"}ve added availability to the same - event both as a guest and from your account. You can{" "} - - choose which submission to keep - {" "} - in the next step when resolving each conflict, and the other - will be deleted. - - )} - ) : guestData === null ? ( + ) : guestSummary === null ? (
Something went wrong trying to fetch guest data. Please try again From 1bad28ab3f5da307dfc4e6448a4f81f71ee387fc Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Tue, 7 Jul 2026 17:02:33 -0400 Subject: [PATCH 24/71] Simplify error checking --- .../settings/(submenus)/guest-import/page.tsx | 43 +++++-------------- .../app/settings/guest-import/review/page.tsx | 19 +++----- 2 files changed, 16 insertions(+), 46 deletions(-) diff --git a/frontend/src/app/settings/(submenus)/guest-import/page.tsx b/frontend/src/app/settings/(submenus)/guest-import/page.tsx index c64cfb81..6a465b21 100644 --- a/frontend/src/app/settings/(submenus)/guest-import/page.tsx +++ b/frontend/src/app/settings/(submenus)/guest-import/page.tsx @@ -1,24 +1,12 @@ -import { TriangleAlertIcon } from "lucide-react"; - import LinkButton from "@/features/button/components/link"; import { ROUTES } from "@/lib/utils/api/endpoints"; import { serverGet } from "@/lib/utils/api/server-fetch"; -import { GuestDataSummary } from "@/lib/utils/api/types"; import { cn } from "@/lib/utils/classname"; export default async function Page() { - let guestSummary: GuestDataSummary | null = null; - try { - guestSummary = await serverGet(ROUTES.guestImport.getSummary); - } catch { - // Do nothing, keep guestSummary as null to indicate an error occurred - } - - const guestSummaryFound = - guestSummary !== null && - (guestSummary?.created_events > 0 || guestSummary?.participated_events > 0); - const createdEvents = guestSummary?.created_events ?? 0; - const participatedEvents = guestSummary?.participated_events ?? 0; + const guestSummary = await serverGet(ROUTES.guestImport.getSummary); + const events = guestSummary.created_events; + const availabilities = guestSummary.participated_events; return (
@@ -31,26 +19,21 @@ export default async function Page() {

- {guestSummaryFound ? ( + {events > 0 || availabilities > 0 ? ( <>
Guest data found on this browser:
-
- • {createdEvents} event - {createdEvents !== 1 ? "s" : ""} that you created +
+ • {events} event + {events !== 1 ? "s" : ""} that you created
- • {participatedEvents} event - {participatedEvents !== 1 ? "s" : ""} that you added your + • {availabilities} event + {availabilities !== 1 ? "s" : ""} that you added your availability to
@@ -61,12 +44,6 @@ export default async function Page() { className="w-fit" /> - ) : guestSummary === null ? ( -
- - Something went wrong trying to fetch guest data. Please try again - later. -
) : (
No guest data found.
)} diff --git a/frontend/src/app/settings/guest-import/review/page.tsx b/frontend/src/app/settings/guest-import/review/page.tsx index 9a4ce1bf..f883c50a 100644 --- a/frontend/src/app/settings/guest-import/review/page.tsx +++ b/frontend/src/app/settings/guest-import/review/page.tsx @@ -3,23 +3,16 @@ import { redirect } from "next/navigation"; import ClientPage from "@/app/settings/guest-import/review/page-client"; import { ROUTES } from "@/lib/utils/api/endpoints"; import { serverGet } from "@/lib/utils/api/server-fetch"; -import { GuestData } from "@/lib/utils/api/types"; export default async function Page() { - let guestData: GuestData | null = null; - try { - guestData = await serverGet(ROUTES.guestImport.getData); - } catch { - // Do nothing, keep guestData as null - } + const guestData = await serverGet(ROUTES.guestImport.getData); - const guestDataFound = - guestData !== null && - (guestData?.created_events.length > 0 || - guestData?.participated_events.length > 0); - if (!guestDataFound) { + if ( + guestData.created_events.length === 0 && + guestData.participated_events.length === 0 + ) { redirect("/settings/guest-import"); } - return ; + return ; } From 8b744e69d8e787bdcb094e0d625e03a0747af5a2 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Tue, 7 Jul 2026 17:10:29 -0400 Subject: [PATCH 25/71] Simplify conflict resolution logic --- frontend/src/app/settings/guest-import/review/page-client.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/app/settings/guest-import/review/page-client.tsx b/frontend/src/app/settings/guest-import/review/page-client.tsx index 88ea9074..88edaec2 100644 --- a/frontend/src/app/settings/guest-import/review/page-client.tsx +++ b/frontend/src/app/settings/guest-import/review/page-client.tsx @@ -43,9 +43,8 @@ export default function ClientPage({ guestData }: { guestData: GuestData }) { const resolveConflict = ( url_code: string, - choice: AvailabilityImportChoice | null, + choice: AvailabilityImportChoice, ) => { - if (choice === null) return; setImportPayload((prev) => ({ ...prev, [url_code]: choice, From 69cf59ad3894325a99d9b9608c62696815aa49ed Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Tue, 7 Jul 2026 17:13:57 -0400 Subject: [PATCH 26/71] Add none display for import data --- .../src/app/settings/guest-import/review/page-client.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/src/app/settings/guest-import/review/page-client.tsx b/frontend/src/app/settings/guest-import/review/page-client.tsx index 88edaec2..eae4d53f 100644 --- a/frontend/src/app/settings/guest-import/review/page-client.tsx +++ b/frontend/src/app/settings/guest-import/review/page-client.tsx @@ -75,6 +75,10 @@ export default function ClientPage({ guestData }: { guestData: GuestData }) {

); + const noneText = ( +
None
+ ); + return (
@@ -88,6 +92,7 @@ export default function ClientPage({ guestData }: { guestData: GuestData }) {

{actionText}

+ {guestData.created_events.length === 0 && noneText} {guestData.created_events.map((event) => ( )} + {guestData.participated_events.length === 0 && noneText} + {guestData.participated_events.map((event) => { const choice = importPayload[event.url_code] ?? null; const hasConflict = conflictedEvents.has(event.url_code); From 136498918e0df44e168f8c470a9bb0d360fb0926 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Tue, 7 Jul 2026 17:15:53 -0400 Subject: [PATCH 27/71] Remove drawer nesting on conflict selector --- frontend/src/app/settings/guest-import/review/page-client.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/app/settings/guest-import/review/page-client.tsx b/frontend/src/app/settings/guest-import/review/page-client.tsx index eae4d53f..d93cd6d5 100644 --- a/frontend/src/app/settings/guest-import/review/page-client.tsx +++ b/frontend/src/app/settings/guest-import/review/page-client.tsx @@ -146,7 +146,6 @@ export default function ClientPage({ guestData }: { guestData: GuestData }) { dialogDescription="Resolve the submission conflict" dialogTitle="Resolve Conflict" id={`resolve-conflict-${event.url_code}`} - drawerNesting={2} onChange={(choice) => resolveConflict(event.url_code, choice) } From 7f4062d45bd38cb1664523c1ceb5ef443b3120af Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Wed, 8 Jul 2026 16:49:36 -0400 Subject: [PATCH 28/71] Create import data endpoint --- backend/api/guest_import/serializers.py | 8 ++ backend/api/guest_import/urls.py | 1 + backend/api/guest_import/views.py | 101 +++++++++++++++++++++++- frontend/src/lib/utils/api/endpoints.ts | 7 ++ frontend/src/lib/utils/api/types.ts | 6 ++ 5 files changed, 122 insertions(+), 1 deletion(-) diff --git a/backend/api/guest_import/serializers.py b/backend/api/guest_import/serializers.py index e5081d0e..b9863d79 100644 --- a/backend/api/guest_import/serializers.py +++ b/backend/api/guest_import/serializers.py @@ -27,3 +27,11 @@ class GuestDataSerializer(serializers.Serializer): child=GuestParticipationSerializer(), allow_empty=True, ) + + +class GuestImportDataSerializer(serializers.Serializer): + availability_choices = serializers.DictField( + required=True, + child=serializers.ChoiceField(required=True, choices=["guest", "account"]), + allow_empty=True, + ) diff --git a/backend/api/guest_import/urls.py b/backend/api/guest_import/urls.py index b8baa6c5..a4b48434 100644 --- a/backend/api/guest_import/urls.py +++ b/backend/api/guest_import/urls.py @@ -5,4 +5,5 @@ urlpatterns = [ path("get-summary/", views.get_summary), path("get-data/", views.get_data), + path("import-data/", views.import_data), ] diff --git a/backend/api/guest_import/views.py b/backend/api/guest_import/views.py index 8d94d284..2232b1f6 100644 --- a/backend/api/guest_import/views.py +++ b/backend/api/guest_import/views.py @@ -1,18 +1,25 @@ import logging +from django.db import transaction from rest_framework.response import Response from api.decorators import ( api_endpoint, require_account_auth, + validate_json_input, validate_output, ) -from api.guest_import.serializers import GuestDataSerializer, GuestDataSummarySerializer +from api.guest_import.serializers import ( + GuestDataSerializer, + GuestDataSummarySerializer, + GuestImportDataSerializer, +) from api.guest_import.utils import get_guest_account from api.models import ( EventParticipant, UserEvent, ) +from api.utils import MessageOutputSerializer logger = logging.getLogger("api") @@ -115,3 +122,95 @@ def get_data(request): response.status_code = 200 return response + + +@api_endpoint("POST") +@require_account_auth +@validate_json_input(GuestImportDataSerializer) +@validate_output(MessageOutputSerializer) +def import_data(request): + """ + Transfers ownership of events and availabilities created by the guest to the account. + + The body of the request should contain a mapping of event URL codes to the user's + chosen submission to keep, for each event the guest had submitted availability to. If + there is no conflict, "guest" should be specified. + + Every conflicted submission that is not kept will be deleted when calling this + endpoint. + """ + availability_choices = request.data.get("availability_choices", {}) + + response = Response() + + guest_user = get_guest_account(request, response) + account_user = request.user + + def no_data_found(): + response.data = { + "error": {"general": ["No guest data found."]}, + } + response.status_code = 400 + return response + + if guest_user is None: + return no_data_found() + + with transaction.atomic(): + # Check if the guest has any data to import + guest_events = UserEvent.objects.filter( + user_account=guest_user, url_code__isnull=False + ) + guest_submissions = EventParticipant.objects.filter( + user_account=guest_user + ).select_related("user_event__url_code") + if not guest_events.exists() and not guest_submissions.exists(): + return no_data_found() + + # Check if all the guest user's submissions are accounted for in the choices + if set(availability_choices.keys()) != set( + submission.user_event.url_code.url_code for submission in guest_submissions + ): + response.data = { + "error": { + "availability_choices": [ + "Availability choices do not match guest submissions.", + ] + } + } + response.status_code = 400 + return response + + # Transfer event ownership + UserEvent.objects.filter( + user_account=guest_user, url_code__isnull=False + ).update(user_account=account_user) + + # === DELETE DISCARDED SUBMISSIONS === + account_chosen = [] + guest_chosen = [] + for url_code, choice in availability_choices.items(): + match choice: + case "account": + account_chosen.append(url_code) + case "guest": + guest_chosen.append(url_code) + + # Start by removing the guest's submissions from events that have "account" specified + EventParticipant.objects.filter( + user_account=guest_user, user_event__url_code__url_code__in=account_chosen + ).delete() + # Then remove the account's submissions from events that have "guest" specified + EventParticipant.objects.filter( + user_account=account_user, user_event__url_code__url_code__in=guest_chosen + ).delete() + + # Then of all the remaining guest submissions transfer ownership to the account + EventParticipant.objects.filter(user_account=guest_user).update( + user_account=account_user + ) + + response.data = {"message": ["Guest data imported successfully."]} + response.status_code = 200 + + return response diff --git a/frontend/src/lib/utils/api/endpoints.ts b/frontend/src/lib/utils/api/endpoints.ts index fc396a24..c98abac0 100644 --- a/frontend/src/lib/utils/api/endpoints.ts +++ b/frontend/src/lib/utils/api/endpoints.ts @@ -15,6 +15,7 @@ import { EventEditData, GuestData, GuestDataSummary, + GuestImportData, LoginData, MessageResponse, NewEventData, @@ -283,5 +284,11 @@ export const ROUTES = { * @method GET */ getData: route("/guest-import/get-data/"), + /** + * Imports the guest user's data into the logged-in user's account, resolving any + * availability submission conflicts based on the provided choices. + * @method POST + */ + importData: route("/guest-import/import-data/"), } } as const; diff --git a/frontend/src/lib/utils/api/types.ts b/frontend/src/lib/utils/api/types.ts index b88a23e7..0ac34d6d 100644 --- a/frontend/src/lib/utils/api/types.ts +++ b/frontend/src/lib/utils/api/types.ts @@ -184,3 +184,9 @@ export type GuestData = { created_events: GuestCreatedEvent[]; participated_events: GuestParticipatedEvent[]; } + +export type GuestImportData = { + availability_choices: { + [url_code: string]: "guest" | "account"; + }; +} From c8ee72af384d1bbe51f487328c09cc17c1563f9e Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Wed, 8 Jul 2026 16:52:00 -0400 Subject: [PATCH 29/71] Add backend functionality to guest review page --- .../guest-import/review/page-client.tsx | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/settings/guest-import/review/page-client.tsx b/frontend/src/app/settings/guest-import/review/page-client.tsx index d93cd6d5..11fec28d 100644 --- a/frontend/src/app/settings/guest-import/review/page-client.tsx +++ b/frontend/src/app/settings/guest-import/review/page-client.tsx @@ -4,13 +4,17 @@ import { useMemo, useState } from "react"; import { ExternalLinkIcon, TriangleAlertIcon } from "lucide-react"; import Link from "next/link"; +import { useRouter } from "next/navigation"; import MobileFooterIsland from "@/components/mobile-footer-island"; +import ActionButton from "@/features/button/components/action"; import LinkButton from "@/features/button/components/link"; import HeaderSpacer from "@/features/header/components/header-spacer"; import Selector from "@/features/selector/components/selector"; -import { Banner } from "@/features/system-feedback"; +import { Banner, useToast } from "@/features/system-feedback"; import Tooltip from "@/features/system-feedback/tooltip/base"; +import { clientPost } from "@/lib/utils/api/client-fetch"; +import { ROUTES } from "@/lib/utils/api/endpoints"; import { GuestData } from "@/lib/utils/api/types"; import { cn } from "@/lib/utils/classname"; @@ -20,6 +24,9 @@ type ImportPayload = { }; export default function ClientPage({ guestData }: { guestData: GuestData }) { + const { addToast } = useToast(); + const router = useRouter(); + const [importPayload, setImportPayload] = useState<{ [url_code: string]: AvailabilityImportChoice; }>( @@ -51,6 +58,21 @@ export default function ClientPage({ guestData }: { guestData: GuestData }) { })); }; + const submitImport = async () => { + try { + await clientPost(ROUTES.guestImport.importData, { + availability_choices: importPayload, + }); + addToast("success", "Guest data imported successfully."); + router.push("/dashboard"); + } catch { + addToast( + "error", + "Something went wrong with your request. Please refresh the page and try again.", + ); + } + }; + const cancelButton = ( 0} /> ); From edf832f0d22e58360d682b4e90ec7f385a5319f1 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Wed, 8 Jul 2026 16:55:36 -0400 Subject: [PATCH 30/71] Add tooltip to disabled submit button --- .../src/app/settings/guest-import/review/page-client.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/app/settings/guest-import/review/page-client.tsx b/frontend/src/app/settings/guest-import/review/page-client.tsx index 11fec28d..6e84aa10 100644 --- a/frontend/src/app/settings/guest-import/review/page-client.tsx +++ b/frontend/src/app/settings/guest-import/review/page-client.tsx @@ -87,6 +87,11 @@ export default function ClientPage({ guestData }: { guestData: GuestData }) { label="Import Data" onClick={submitImport} disabled={unresolvedConflicts > 0} + tooltip={ + unresolvedConflicts > 0 + ? "Please resolve conflicts before importing." + : undefined + } /> ); From 4f6ef02b1941be58d71b5b450c1a6a3b0ef4e48b Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Wed, 8 Jul 2026 17:21:18 -0400 Subject: [PATCH 31/71] Move guest import to separate route --- .../src/app/{settings => }/guest-import/review/page-client.tsx | 0 frontend/src/app/{settings => }/guest-import/review/page.tsx | 2 +- frontend/src/app/settings/(submenus)/guest-import/page.tsx | 2 +- frontend/src/middleware.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename frontend/src/app/{settings => }/guest-import/review/page-client.tsx (100%) rename frontend/src/app/{settings => }/guest-import/review/page.tsx (86%) diff --git a/frontend/src/app/settings/guest-import/review/page-client.tsx b/frontend/src/app/guest-import/review/page-client.tsx similarity index 100% rename from frontend/src/app/settings/guest-import/review/page-client.tsx rename to frontend/src/app/guest-import/review/page-client.tsx diff --git a/frontend/src/app/settings/guest-import/review/page.tsx b/frontend/src/app/guest-import/review/page.tsx similarity index 86% rename from frontend/src/app/settings/guest-import/review/page.tsx rename to frontend/src/app/guest-import/review/page.tsx index f883c50a..28667520 100644 --- a/frontend/src/app/settings/guest-import/review/page.tsx +++ b/frontend/src/app/guest-import/review/page.tsx @@ -1,6 +1,6 @@ import { redirect } from "next/navigation"; -import ClientPage from "@/app/settings/guest-import/review/page-client"; +import ClientPage from "@/app/guest-import/review/page-client"; import { ROUTES } from "@/lib/utils/api/endpoints"; import { serverGet } from "@/lib/utils/api/server-fetch"; diff --git a/frontend/src/app/settings/(submenus)/guest-import/page.tsx b/frontend/src/app/settings/(submenus)/guest-import/page.tsx index 6a465b21..a7fb692c 100644 --- a/frontend/src/app/settings/(submenus)/guest-import/page.tsx +++ b/frontend/src/app/settings/(submenus)/guest-import/page.tsx @@ -40,7 +40,7 @@ export default async function Page() { diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts index c42febd3..c754239d 100644 --- a/frontend/src/middleware.ts +++ b/frontend/src/middleware.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; -const protectedRoutes = ["/settings"]; +const protectedRoutes = ["/settings", "/guest-import"]; export function middleware(request: NextRequest) { // Clone the request headers and inject the exact pathname into a custom header. From 1e0732d4b96b02dd034ff7a1a8780b947f185bed Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Wed, 8 Jul 2026 17:37:24 -0400 Subject: [PATCH 32/71] Add support for children in MessagePage --- frontend/src/components/layout/message-page.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/layout/message-page.tsx b/frontend/src/components/layout/message-page.tsx index 969ab9e8..04a35aee 100644 --- a/frontend/src/components/layout/message-page.tsx +++ b/frontend/src/components/layout/message-page.tsx @@ -3,18 +3,21 @@ import { ButtonArray } from "@/features/button/button-array"; type MessagePageProps = { title: string; description?: string; + children?: React.ReactNode; buttons: ButtonArray; }; export default function MessagePage({ title, description, + children, buttons, }: MessagePageProps) { return ( -
-

{title}

- {description &&

{description}

} +
+

{title}

+ {description &&

{description}

} + {children}
{buttons.map((button, index) => (
{button}
From 866a48e427fdd804b90642d4dcfdd0e30fb9452b Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Wed, 8 Jul 2026 17:57:44 -0400 Subject: [PATCH 33/71] Move guest data summary to component --- .../settings/(submenus)/guest-import/page.tsx | 19 ++------------- .../guest-import/components/summary.tsx | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 17 deletions(-) create mode 100644 frontend/src/features/guest-import/components/summary.tsx diff --git a/frontend/src/app/settings/(submenus)/guest-import/page.tsx b/frontend/src/app/settings/(submenus)/guest-import/page.tsx index a7fb692c..eb4c7856 100644 --- a/frontend/src/app/settings/(submenus)/guest-import/page.tsx +++ b/frontend/src/app/settings/(submenus)/guest-import/page.tsx @@ -1,7 +1,7 @@ import LinkButton from "@/features/button/components/link"; +import GuestDataSummary from "@/features/guest-import/components/summary"; import { ROUTES } from "@/lib/utils/api/endpoints"; import { serverGet } from "@/lib/utils/api/server-fetch"; -import { cn } from "@/lib/utils/classname"; export default async function Page() { const guestSummary = await serverGet(ROUTES.guestImport.getSummary); @@ -21,22 +21,7 @@ export default async function Page() { {events > 0 || availabilities > 0 ? ( <> -
-
- Guest data found on this browser: -
-
- • {events} event - {events !== 1 ? "s" : ""} that you created -
-
- • {availabilities} event - {availabilities !== 1 ? "s" : ""} that you added your - availability to -
-
+ +
Guest data found on this browser:
+
+ • {events} event + {events !== 1 ? "s" : ""} that you created +
+
+ • {availabilities} event + {availabilities !== 1 ? "s" : ""} that you added your availability to +
+
+ ); +} From 7a457def2e29f6398345bc1f7963fb4db2111bfc Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Wed, 8 Jul 2026 17:59:26 -0400 Subject: [PATCH 34/71] Create guest import login page and redirect --- frontend/src/app/(auth)/login/page.tsx | 5 ++- frontend/src/app/guest-import/login/page.tsx | 46 ++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 frontend/src/app/guest-import/login/page.tsx diff --git a/frontend/src/app/(auth)/login/page.tsx b/frontend/src/app/(auth)/login/page.tsx index 0d90179a..95adb537 100644 --- a/frontend/src/app/(auth)/login/page.tsx +++ b/frontend/src/app/(auth)/login/page.tsx @@ -24,7 +24,10 @@ export default function Page() { const router = useRouter(); const searchParams = useSearchParams(); - const callbackUrl = getSafeRedirectUrl(searchParams.get("callbackUrl")); + const callbackUrl = getSafeRedirectUrl( + searchParams.get("callbackUrl"), + "/guest-import/login", + ); // TOASTS AND ERROR STATES const { errors, handleError, clearAllErrors } = useFormErrors(); diff --git a/frontend/src/app/guest-import/login/page.tsx b/frontend/src/app/guest-import/login/page.tsx new file mode 100644 index 00000000..72765607 --- /dev/null +++ b/frontend/src/app/guest-import/login/page.tsx @@ -0,0 +1,46 @@ +import { redirect } from "next/navigation"; + +import MessagePage from "@/components/layout/message-page"; +import LinkButton from "@/features/button/components/link"; +import GuestDataSummary from "@/features/guest-import/components/summary"; +import { ROUTES } from "@/lib/utils/api/endpoints"; +import { serverGet } from "@/lib/utils/api/server-fetch"; + +export default async function Page() { + const guestSummary = await serverGet(ROUTES.guestImport.getSummary); + const events = guestSummary.created_events; + const availabilities = guestSummary.participated_events; + + if (events === 0 && availabilities === 0) { + redirect("/dashboard"); + } + + return ( +
+ , + , + ]} + > +
+ +
+
+
+ ); +} From 26368272dbbecb161e52adf366b097502f63b86b Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Wed, 8 Jul 2026 18:12:23 -0400 Subject: [PATCH 35/71] Create client page for login guest import --- .../app/guest-import/login/page-client.tsx | 42 +++++++++++++++++++ frontend/src/app/guest-import/login/page.tsx | 33 +-------------- .../settings/(submenus)/guest-import/page.tsx | 4 +- .../{summary.tsx => guest-summary.tsx} | 2 +- 4 files changed, 47 insertions(+), 34 deletions(-) create mode 100644 frontend/src/app/guest-import/login/page-client.tsx rename frontend/src/features/guest-import/components/{summary.tsx => guest-summary.tsx} (93%) diff --git a/frontend/src/app/guest-import/login/page-client.tsx b/frontend/src/app/guest-import/login/page-client.tsx new file mode 100644 index 00000000..f903bcc9 --- /dev/null +++ b/frontend/src/app/guest-import/login/page-client.tsx @@ -0,0 +1,42 @@ +import MessagePage from "@/components/layout/message-page"; +import LinkButton from "@/features/button/components/link"; +import GuestSummary from "@/features/guest-import/components/guest-summary"; +import { GuestDataSummary } from "@/lib/utils/api/types"; + +export default function ClientPage({ + guestSummary, +}: { + guestSummary: GuestDataSummary; +}) { + return ( +
+ , + , + ]} + > +
+ +
+
+
+ ); +} diff --git a/frontend/src/app/guest-import/login/page.tsx b/frontend/src/app/guest-import/login/page.tsx index 72765607..28724fb5 100644 --- a/frontend/src/app/guest-import/login/page.tsx +++ b/frontend/src/app/guest-import/login/page.tsx @@ -1,8 +1,6 @@ import { redirect } from "next/navigation"; -import MessagePage from "@/components/layout/message-page"; -import LinkButton from "@/features/button/components/link"; -import GuestDataSummary from "@/features/guest-import/components/summary"; +import ClientPage from "@/app/guest-import/login/page-client"; import { ROUTES } from "@/lib/utils/api/endpoints"; import { serverGet } from "@/lib/utils/api/server-fetch"; @@ -15,32 +13,5 @@ export default async function Page() { redirect("/dashboard"); } - return ( -
- , - , - ]} - > -
- -
-
-
- ); + return ; } diff --git a/frontend/src/app/settings/(submenus)/guest-import/page.tsx b/frontend/src/app/settings/(submenus)/guest-import/page.tsx index eb4c7856..cb9ad0eb 100644 --- a/frontend/src/app/settings/(submenus)/guest-import/page.tsx +++ b/frontend/src/app/settings/(submenus)/guest-import/page.tsx @@ -1,5 +1,5 @@ import LinkButton from "@/features/button/components/link"; -import GuestDataSummary from "@/features/guest-import/components/summary"; +import GuestSummary from "@/features/guest-import/components/guest-summary"; import { ROUTES } from "@/lib/utils/api/endpoints"; import { serverGet } from "@/lib/utils/api/server-fetch"; @@ -21,7 +21,7 @@ export default async function Page() { {events > 0 || availabilities > 0 ? ( <> - + Date: Wed, 8 Jul 2026 18:32:02 -0400 Subject: [PATCH 36/71] Add don't show again toggle and local storage --- .../app/guest-import/login/page-client.tsx | 58 +++++++++++++++---- .../src/features/guest-import/constants.ts | 1 + 2 files changed, 47 insertions(+), 12 deletions(-) create mode 100644 frontend/src/features/guest-import/constants.ts diff --git a/frontend/src/app/guest-import/login/page-client.tsx b/frontend/src/app/guest-import/login/page-client.tsx index f903bcc9..2c1a611e 100644 --- a/frontend/src/app/guest-import/login/page-client.tsx +++ b/frontend/src/app/guest-import/login/page-client.tsx @@ -1,6 +1,14 @@ +"use client"; + +import { useState } from "react"; + +import { useRouter } from "next/navigation"; + +import Checkbox from "@/components/checkbox"; import MessagePage from "@/components/layout/message-page"; -import LinkButton from "@/features/button/components/link"; +import ActionButton from "@/features/button/components/action"; import GuestSummary from "@/features/guest-import/components/guest-summary"; +import { DONT_SHOW_AGAIN_KEY } from "@/features/guest-import/constants"; import { GuestDataSummary } from "@/lib/utils/api/types"; export default function ClientPage({ @@ -8,33 +16,59 @@ export default function ClientPage({ }: { guestSummary: GuestDataSummary; }) { + const [dontShowAgain, setDontShowAgain] = useState(false); + const router = useRouter(); + + const navigate = (route: string) => { + if (dontShowAgain) { + localStorage.setItem(DONT_SHOW_AGAIN_KEY, "true"); + } + router.push(route); + }; + return (
{ + navigate("/dashboard"); + }} />, - { + navigate("/guest-import/review"); + }} />, ]} > -
- +
+
+ +
+
+ + {dontShowAgain && ( +
+ You can import guest data at any time in account settings. +
+ )} +
diff --git a/frontend/src/features/guest-import/constants.ts b/frontend/src/features/guest-import/constants.ts new file mode 100644 index 00000000..6e4c71a0 --- /dev/null +++ b/frontend/src/features/guest-import/constants.ts @@ -0,0 +1 @@ +export const DONT_SHOW_AGAIN_KEY = "login-guest-import-dont-show-again"; From cdd7f32d126e110d00f9ecdafb5c0f86aeef3fdc Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Wed, 8 Jul 2026 18:35:33 -0400 Subject: [PATCH 37/71] Add don't show again to login redirect --- frontend/src/app/(auth)/login/page.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/(auth)/login/page.tsx b/frontend/src/app/(auth)/login/page.tsx index 95adb537..ceedbafa 100644 --- a/frontend/src/app/(auth)/login/page.tsx +++ b/frontend/src/app/(auth)/login/page.tsx @@ -10,6 +10,7 @@ import AuthPageLayout from "@/components/layout/auth-page"; import LinkText from "@/components/link-text"; import TextInputField from "@/components/text-input-field"; import ActionButton from "@/features/button/components/action"; +import { DONT_SHOW_AGAIN_KEY } from "@/features/guest-import/constants"; import { useFormErrors } from "@/lib/hooks/use-form-errors"; import { MESSAGES } from "@/lib/messages"; import { clientPost } from "@/lib/utils/api/client-fetch"; @@ -24,9 +25,11 @@ export default function Page() { const router = useRouter(); const searchParams = useSearchParams(); + const dontShowGuestImport = + localStorage.getItem(DONT_SHOW_AGAIN_KEY) === "true"; const callbackUrl = getSafeRedirectUrl( searchParams.get("callbackUrl"), - "/guest-import/login", + dontShowGuestImport ? undefined : "/guest-import/login", ); // TOASTS AND ERROR STATES From 686e4bb65b9e292e5714189361a70a9c25ba4f3b Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Wed, 8 Jul 2026 18:47:31 -0400 Subject: [PATCH 38/71] Create loading page for import review --- .../src/app/guest-import/review/loading.tsx | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 frontend/src/app/guest-import/review/loading.tsx diff --git a/frontend/src/app/guest-import/review/loading.tsx b/frontend/src/app/guest-import/review/loading.tsx new file mode 100644 index 00000000..98c580f7 --- /dev/null +++ b/frontend/src/app/guest-import/review/loading.tsx @@ -0,0 +1,41 @@ +import HeaderSpacer from "@/features/header/components/header-spacer"; + +export default function Loading() { + return ( +
+ +

Guest Import Review

+ +
+ +
+
+
+
+
+ {[1, 2, 3].map((i) => ( +
+ ))} +
+
+
+
+
+
+
+ {[1, 2, 3].map((i) => ( +
+ ))} +
+
+
+
+
+ ); +} From 2c697f03af46cec6b39e2920bd9cf66156f354ee Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Wed, 8 Jul 2026 21:08:19 -0400 Subject: [PATCH 39/71] Adjust stroke width to match bold text --- frontend/src/app/guest-import/review/page-client.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/guest-import/review/page-client.tsx b/frontend/src/app/guest-import/review/page-client.tsx index 6e84aa10..b7cab5f4 100644 --- a/frontend/src/app/guest-import/review/page-client.tsx +++ b/frontend/src/app/guest-import/review/page-client.tsx @@ -208,7 +208,7 @@ export default function ClientPage({ guestData }: { guestData: GuestData }) { > {unresolvedConflicts > 0 ? (
- + Please resolve conflicts.
) : ( @@ -265,7 +265,9 @@ function EventHeader({ return (
- {conflict && } + {conflict && ( + + )}

{title}

From 2bbc633dc7f19c4c1ee264ce83af80e4ccf37ac2 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Wed, 8 Jul 2026 21:11:00 -0400 Subject: [PATCH 40/71] Add padding to login import page --- frontend/src/app/guest-import/login/page-client.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/guest-import/login/page-client.tsx b/frontend/src/app/guest-import/login/page-client.tsx index 2c1a611e..cd1dcca4 100644 --- a/frontend/src/app/guest-import/login/page-client.tsx +++ b/frontend/src/app/guest-import/login/page-client.tsx @@ -27,7 +27,7 @@ export default function ClientPage({ }; return ( -
+
Date: Wed, 8 Jul 2026 21:12:10 -0400 Subject: [PATCH 41/71] Fix button activation area --- .../app/settings/(submenus)/guest-import/page.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/settings/(submenus)/guest-import/page.tsx b/frontend/src/app/settings/(submenus)/guest-import/page.tsx index cb9ad0eb..5c011145 100644 --- a/frontend/src/app/settings/(submenus)/guest-import/page.tsx +++ b/frontend/src/app/settings/(submenus)/guest-import/page.tsx @@ -22,12 +22,13 @@ export default async function Page() { {events > 0 || availabilities > 0 ? ( <> - +
+ +
) : (
No guest data found.
From 089189aed5e4d87b795bdac93296f1ab68e5c1a1 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Wed, 8 Jul 2026 21:16:20 -0400 Subject: [PATCH 42/71] Add guest-import to reserved keywords --- backend/api/event/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/api/event/utils.py b/backend/api/event/utils.py index b8c1453c..d8234fbf 100644 --- a/backend/api/event/utils.py +++ b/backend/api/event/utils.py @@ -33,6 +33,7 @@ def check_custom_code(code): "api", "dashboard", "forgot-password", + "guest-import", "login", "new-event", "settings", From 9ee37fcd3cc5c30ecd78cd71223a1c327cb4b967 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Wed, 8 Jul 2026 21:27:28 -0400 Subject: [PATCH 43/71] Add metadata to guest import pages --- frontend/src/app/guest-import/layout.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 frontend/src/app/guest-import/layout.tsx diff --git a/frontend/src/app/guest-import/layout.tsx b/frontend/src/app/guest-import/layout.tsx new file mode 100644 index 00000000..f2b1f34d --- /dev/null +++ b/frontend/src/app/guest-import/layout.tsx @@ -0,0 +1,14 @@ +import { Metadata } from "next"; + +import { constructMetadata } from "@/lib/utils/construct-metadata"; + +export function generateMetadata(): Metadata { + return constructMetadata( + "Guest Import", + "Transfer guest data into your account for easy access across devices.", + ); +} + +export default function Layout({ children }: { children: React.ReactNode }) { + return children; +} From a9fb9cca90b607045cd98483cf3c3596d7a985c9 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Wed, 8 Jul 2026 21:30:02 -0400 Subject: [PATCH 44/71] Rename and reroute guest import review page --- frontend/src/app/guest-import/{review => import}/loading.tsx | 0 .../src/app/guest-import/{review => import}/page-client.tsx | 2 +- frontend/src/app/guest-import/{review => import}/page.tsx | 2 +- frontend/src/app/guest-import/login/page-client.tsx | 2 +- frontend/src/app/settings/(submenus)/guest-import/page.tsx | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename frontend/src/app/guest-import/{review => import}/loading.tsx (100%) rename frontend/src/app/guest-import/{review => import}/page-client.tsx (99%) rename frontend/src/app/guest-import/{review => import}/page.tsx (87%) diff --git a/frontend/src/app/guest-import/review/loading.tsx b/frontend/src/app/guest-import/import/loading.tsx similarity index 100% rename from frontend/src/app/guest-import/review/loading.tsx rename to frontend/src/app/guest-import/import/loading.tsx diff --git a/frontend/src/app/guest-import/review/page-client.tsx b/frontend/src/app/guest-import/import/page-client.tsx similarity index 99% rename from frontend/src/app/guest-import/review/page-client.tsx rename to frontend/src/app/guest-import/import/page-client.tsx index b7cab5f4..d298ae86 100644 --- a/frontend/src/app/guest-import/review/page-client.tsx +++ b/frontend/src/app/guest-import/import/page-client.tsx @@ -110,7 +110,7 @@ export default function ClientPage({ guestData }: { guestData: GuestData }) {
-

Guest Import Review

+

Guest Import

{cancelButton} {importButton} diff --git a/frontend/src/app/guest-import/review/page.tsx b/frontend/src/app/guest-import/import/page.tsx similarity index 87% rename from frontend/src/app/guest-import/review/page.tsx rename to frontend/src/app/guest-import/import/page.tsx index 28667520..eba95172 100644 --- a/frontend/src/app/guest-import/review/page.tsx +++ b/frontend/src/app/guest-import/import/page.tsx @@ -1,6 +1,6 @@ import { redirect } from "next/navigation"; -import ClientPage from "@/app/guest-import/review/page-client"; +import ClientPage from "@/app/guest-import/import/page-client"; import { ROUTES } from "@/lib/utils/api/endpoints"; import { serverGet } from "@/lib/utils/api/server-fetch"; diff --git a/frontend/src/app/guest-import/login/page-client.tsx b/frontend/src/app/guest-import/login/page-client.tsx index cd1dcca4..4381bb5a 100644 --- a/frontend/src/app/guest-import/login/page-client.tsx +++ b/frontend/src/app/guest-import/login/page-client.tsx @@ -45,7 +45,7 @@ export default function ClientPage({ buttonStyle="primary" label="Review and Import" onClick={() => { - navigate("/guest-import/review"); + navigate("/guest-import/import"); }} />, ]} diff --git a/frontend/src/app/settings/(submenus)/guest-import/page.tsx b/frontend/src/app/settings/(submenus)/guest-import/page.tsx index 5c011145..4f027187 100644 --- a/frontend/src/app/settings/(submenus)/guest-import/page.tsx +++ b/frontend/src/app/settings/(submenus)/guest-import/page.tsx @@ -26,7 +26,7 @@ export default async function Page() {
From 2c2d5aa4bbd7c9d09989424a483f0e040433b632 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Wed, 8 Jul 2026 21:46:58 -0400 Subject: [PATCH 45/71] Update dashboard guest banner text --- frontend/src/app/dashboard/page-client.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/dashboard/page-client.tsx b/frontend/src/app/dashboard/page-client.tsx index aac3c7af..56b68702 100644 --- a/frontend/src/app/dashboard/page-client.tsx +++ b/frontend/src/app/dashboard/page-client.tsx @@ -81,7 +81,7 @@ export default function ClientPage({

Dashboard

{!logged_in && ( - +
This data is only available from this browser.{" "} Create an account {" "} - to sync data across devices. -
-
- Currently, guest data cannot be transferred to an account. Keep an - eye out for updates! + to sync data across devices. You can import this data at any time in + account settings.
)} From 4a708a6b66da6d7ca0725fd8045924dbea636a11 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Thu, 9 Jul 2026 13:59:24 -0400 Subject: [PATCH 46/71] Add public ID to UserEvent --- .../migrations/0034_userevent_public_id.py | 20 ++++++++++++++++ .../api/migrations/0035_populate_public_id.py | 23 +++++++++++++++++++ .../0036_apply_unique_constraint.py | 20 ++++++++++++++++ backend/api/models.py | 1 + 4 files changed, 64 insertions(+) create mode 100644 backend/api/migrations/0034_userevent_public_id.py create mode 100644 backend/api/migrations/0035_populate_public_id.py create mode 100644 backend/api/migrations/0036_apply_unique_constraint.py diff --git a/backend/api/migrations/0034_userevent_public_id.py b/backend/api/migrations/0034_userevent_public_id.py new file mode 100644 index 00000000..82888254 --- /dev/null +++ b/backend/api/migrations/0034_userevent_public_id.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2 on 2026-07-09 17:46 + +import uuid + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("api", "0033_urlcode_unique_case_insensitive_url_code"), + ] + + operations = [ + migrations.AddField( + model_name="userevent", + name="public_id", + field=models.UUIDField(default=uuid.uuid4, editable=False, null=True), + ), + ] diff --git a/backend/api/migrations/0035_populate_public_id.py b/backend/api/migrations/0035_populate_public_id.py new file mode 100644 index 00000000..a94f7731 --- /dev/null +++ b/backend/api/migrations/0035_populate_public_id.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2 on 2026-07-09 17:47 + +import uuid + +from django.db import migrations + + +def gen_uuid(apps, _): + UserEvent = apps.get_model("api", "UserEvent") + for row in UserEvent.objects.iterator(): + row.public_id = uuid.uuid4() + row.save(update_fields=["public_id"]) + + +class Migration(migrations.Migration): + + dependencies = [ + ("api", "0034_userevent_public_id"), + ] + + operations = [ + migrations.RunPython(gen_uuid, reverse_code=migrations.RunPython.noop), + ] diff --git a/backend/api/migrations/0036_apply_unique_constraint.py b/backend/api/migrations/0036_apply_unique_constraint.py new file mode 100644 index 00000000..e44d5468 --- /dev/null +++ b/backend/api/migrations/0036_apply_unique_constraint.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2 on 2026-07-09 17:47 + +import uuid + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("api", "0035_populate_public_id"), + ] + + operations = [ + migrations.AlterField( + model_name="userevent", + name="public_id", + field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True), + ), + ] diff --git a/backend/api/models.py b/backend/api/models.py index 813fd612..f21023fc 100644 --- a/backend/api/models.py +++ b/backend/api/models.py @@ -88,6 +88,7 @@ class UserLogin(models.Model): class UserEvent(models.Model): user_event_id = models.AutoField(primary_key=True) + public_id = models.UUIDField(unique=True, editable=False, default=uuid.uuid4) user_account = models.ForeignKey( UserAccount, on_delete=models.CASCADE, related_name="events" ) From 3f317a0ced68da778ac49d14a6513732e4a9cf31 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Thu, 9 Jul 2026 14:05:39 -0400 Subject: [PATCH 47/71] Add UUIDField support to docs --- backend/api/docs/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/api/docs/utils.py b/backend/api/docs/utils.py index 20623732..afdd920f 100644 --- a/backend/api/docs/utils.py +++ b/backend/api/docs/utils.py @@ -46,6 +46,8 @@ def get_readable_field_name(field_name): return "string" case "RegexField": return "string" + case "UUIDField": + return "string" case _: return "object" From cb5c955bc45dd79a902e648f8ec707ee7f9fbcdd Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Thu, 9 Jul 2026 14:13:31 -0400 Subject: [PATCH 48/71] Add public ID and remove null URL code filter --- backend/api/guest_import/serializers.py | 6 ++-- backend/api/guest_import/views.py | 46 +++++++++++-------------- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/backend/api/guest_import/serializers.py b/backend/api/guest_import/serializers.py index b9863d79..6b6e14de 100644 --- a/backend/api/guest_import/serializers.py +++ b/backend/api/guest_import/serializers.py @@ -7,12 +7,14 @@ class GuestDataSummarySerializer(serializers.Serializer): class GuestEventSerializer(serializers.Serializer): - url_code = serializers.CharField() + url_code = serializers.CharField(allow_null=True) + public_id = serializers.UUIDField() title = serializers.CharField() class GuestParticipationSerializer(serializers.Serializer): - url_code = serializers.CharField() + url_code = serializers.CharField(allow_null=True) + public_id = serializers.UUIDField() title = serializers.CharField() guest_display_name = serializers.CharField() account_display_name = serializers.CharField(allow_null=True) diff --git a/backend/api/guest_import/views.py b/backend/api/guest_import/views.py index 2232b1f6..cd699c69 100644 --- a/backend/api/guest_import/views.py +++ b/backend/api/guest_import/views.py @@ -43,14 +43,9 @@ def get_summary(request): response.status_code = 200 return response - events = UserEvent.objects.filter( - user_account=guest_user, url_code__isnull=False - ).count() + events = UserEvent.objects.filter(user_account=guest_user).count() - participations = EventParticipant.objects.filter( - user_account=guest_user, - user_event__url_code__isnull=False, - ).count() + participations = EventParticipant.objects.filter(user_account=guest_user).count() response.data = { "created_events": events, @@ -81,12 +76,13 @@ def get_data(request): response.status_code = 200 return response - events = UserEvent.objects.filter( - user_account=guest_user, url_code__isnull=False - ).select_related("url_code") + events = UserEvent.objects.filter(user_account=guest_user).select_related( + "url_code" + ) created_events = [ { "url_code": event.url_code.url_code, + "public_id": event.public_id, "title": event.title, } for event in events @@ -94,11 +90,11 @@ def get_data(request): participations = EventParticipant.objects.filter( user_account=guest_user, - user_event__url_code__isnull=False, ).select_related("user_event__url_code") participated_events = [ { "url_code": participation.user_event.url_code.url_code, + "public_id": participation.user_event.public_id, "title": participation.user_event.title, "guest_display_name": participation.display_name, "account_display_name": None, @@ -109,10 +105,10 @@ def get_data(request): conflicts = EventParticipant.objects.filter( user_account=account_user, user_event__in=participations.values_list("user_event", flat=True), - ).select_related("user_event__url_code") + ) for conflict in conflicts: for participation in participated_events: - if participation["url_code"] == conflict.user_event.url_code.url_code: + if participation["public_id"] == conflict.user_event.public_id: participation["account_display_name"] = conflict.display_name response.data = { @@ -158,18 +154,16 @@ def no_data_found(): with transaction.atomic(): # Check if the guest has any data to import - guest_events = UserEvent.objects.filter( - user_account=guest_user, url_code__isnull=False - ) + guest_events = UserEvent.objects.filter(user_account=guest_user) guest_submissions = EventParticipant.objects.filter( user_account=guest_user - ).select_related("user_event__url_code") + ).select_related("user_event") if not guest_events.exists() and not guest_submissions.exists(): return no_data_found() # Check if all the guest user's submissions are accounted for in the choices if set(availability_choices.keys()) != set( - submission.user_event.url_code.url_code for submission in guest_submissions + submission.user_event.public_id for submission in guest_submissions ): response.data = { "error": { @@ -182,27 +176,27 @@ def no_data_found(): return response # Transfer event ownership - UserEvent.objects.filter( - user_account=guest_user, url_code__isnull=False - ).update(user_account=account_user) + UserEvent.objects.filter(user_account=guest_user).update( + user_account=account_user + ) # === DELETE DISCARDED SUBMISSIONS === account_chosen = [] guest_chosen = [] - for url_code, choice in availability_choices.items(): + for public_id, choice in availability_choices.items(): match choice: case "account": - account_chosen.append(url_code) + account_chosen.append(public_id) case "guest": - guest_chosen.append(url_code) + guest_chosen.append(public_id) # Start by removing the guest's submissions from events that have "account" specified EventParticipant.objects.filter( - user_account=guest_user, user_event__url_code__url_code__in=account_chosen + user_account=guest_user, user_event__public_id__in=account_chosen ).delete() # Then remove the account's submissions from events that have "guest" specified EventParticipant.objects.filter( - user_account=account_user, user_event__url_code__url_code__in=guest_chosen + user_account=account_user, user_event__public_id__in=guest_chosen ).delete() # Then of all the remaining guest submissions transfer ownership to the account From 594d3d881854bcf980d1e4dc4e1c3bd209f582c7 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Thu, 9 Jul 2026 14:14:36 -0400 Subject: [PATCH 49/71] Add public ID to endpoint type mappings --- frontend/src/lib/utils/api/types.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/utils/api/types.ts b/frontend/src/lib/utils/api/types.ts index 0ac34d6d..595bd9b2 100644 --- a/frontend/src/lib/utils/api/types.ts +++ b/frontend/src/lib/utils/api/types.ts @@ -170,11 +170,13 @@ export type GuestDataSummary = { type GuestCreatedEvent = { url_code: string; + public_id: string; title: string; } type GuestParticipatedEvent = { url_code: string; + public_id: string; title: string; guest_display_name: string; account_display_name: string | null; @@ -187,6 +189,6 @@ export type GuestData = { export type GuestImportData = { availability_choices: { - [url_code: string]: "guest" | "account"; + [public_id: string]: "guest" | "account"; }; } From 13edb89a4be6fa862e27c922335a2b65f4de9ab2 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Thu, 9 Jul 2026 14:47:19 -0400 Subject: [PATCH 50/71] Fix UUID comparison --- backend/api/guest_import/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/api/guest_import/views.py b/backend/api/guest_import/views.py index cd699c69..91bb5edc 100644 --- a/backend/api/guest_import/views.py +++ b/backend/api/guest_import/views.py @@ -163,7 +163,7 @@ def no_data_found(): # Check if all the guest user's submissions are accounted for in the choices if set(availability_choices.keys()) != set( - submission.user_event.public_id for submission in guest_submissions + str(submission.user_event.public_id) for submission in guest_submissions ): response.data = { "error": { From a759555b49f0b633b37ea244ff249fb67211f0a7 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Thu, 9 Jul 2026 14:47:26 -0400 Subject: [PATCH 51/71] Add support for public ID and null URL code --- .../app/guest-import/import/page-client.tsx | 56 ++++++++++--------- frontend/src/lib/utils/api/types.ts | 4 +- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/frontend/src/app/guest-import/import/page-client.tsx b/frontend/src/app/guest-import/import/page-client.tsx index d298ae86..ed0fe967 100644 --- a/frontend/src/app/guest-import/import/page-client.tsx +++ b/frontend/src/app/guest-import/import/page-client.tsx @@ -20,7 +20,7 @@ import { cn } from "@/lib/utils/classname"; type AvailabilityImportChoice = "guest" | "account"; type ImportPayload = { - [url_code: string]: AvailabilityImportChoice; + [public_id: string]: AvailabilityImportChoice; }; export default function ClientPage({ guestData }: { guestData: GuestData }) { @@ -28,11 +28,11 @@ export default function ClientPage({ guestData }: { guestData: GuestData }) { const router = useRouter(); const [importPayload, setImportPayload] = useState<{ - [url_code: string]: AvailabilityImportChoice; + [public_id: string]: AvailabilityImportChoice; }>( guestData.participated_events.reduce((acc, event) => { if (event.account_display_name === null) { - acc[event.url_code] = "guest"; + acc[event.public_id] = "guest"; } return acc; }, {} as ImportPayload), @@ -44,17 +44,17 @@ export default function ClientPage({ guestData }: { guestData: GuestData }) { return new Set( guestData.participated_events .filter((event) => event.account_display_name !== null) - .map((event) => event.url_code), + .map((event) => event.public_id), ); }, [guestData.participated_events]); const resolveConflict = ( - url_code: string, + public_id: string, choice: AvailabilityImportChoice, ) => { setImportPayload((prev) => ({ ...prev, - [url_code]: choice, + [public_id]: choice, })); }; @@ -122,7 +122,7 @@ export default function ClientPage({ guestData }: { guestData: GuestData }) { {guestData.created_events.length === 0 && noneText} {guestData.created_events.map((event) => ( @@ -144,12 +144,12 @@ export default function ClientPage({ guestData }: { guestData: GuestData }) { {guestData.participated_events.length === 0 && noneText} {guestData.participated_events.map((event) => { - const choice = importPayload[event.url_code] ?? null; - const hasConflict = conflictedEvents.has(event.url_code); + const choice = importPayload[event.public_id] ?? null; + const hasConflict = conflictedEvents.has(event.public_id); return ( - resolveConflict(event.url_code, choice) + resolveConflict(event.public_id, choice) } options={[ { label: "Keep Guest", value: "guest" }, @@ -241,7 +241,7 @@ function EventDisplay({ children, }: { title: string; - url_code: string; + url_code: string | null; conflict?: boolean; children?: React.ReactNode; }) { @@ -259,7 +259,7 @@ function EventHeader({ conflict, }: { title: string; - url_code: string; + url_code: string | null; conflict?: boolean; }) { return ( @@ -270,19 +270,21 @@ function EventHeader({ )}

{title}

- - - View - - - + {url_code && ( + + + View + + + + )}
); } diff --git a/frontend/src/lib/utils/api/types.ts b/frontend/src/lib/utils/api/types.ts index 595bd9b2..43e12461 100644 --- a/frontend/src/lib/utils/api/types.ts +++ b/frontend/src/lib/utils/api/types.ts @@ -169,13 +169,13 @@ export type GuestDataSummary = { } type GuestCreatedEvent = { - url_code: string; + url_code: string | null; public_id: string; title: string; } type GuestParticipatedEvent = { - url_code: string; + url_code: string | null; public_id: string; title: string; guest_display_name: string; From c5d21d9283a384aa408b13f1e38bbca770775c0d Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Thu, 9 Jul 2026 16:32:41 -0400 Subject: [PATCH 52/71] Add error handling for null URL code --- backend/api/guest_import/views.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/backend/api/guest_import/views.py b/backend/api/guest_import/views.py index 91bb5edc..4a447e1c 100644 --- a/backend/api/guest_import/views.py +++ b/backend/api/guest_import/views.py @@ -1,5 +1,6 @@ import logging +from django.core.exceptions import ObjectDoesNotExist from django.db import transaction from rest_framework.response import Response @@ -76,12 +77,18 @@ def get_data(request): response.status_code = 200 return response + def get_url_code(event): + try: + return event.url_code.url_code + except ObjectDoesNotExist: + return None + events = UserEvent.objects.filter(user_account=guest_user).select_related( "url_code" ) created_events = [ { - "url_code": event.url_code.url_code, + "url_code": get_url_code(event), "public_id": event.public_id, "title": event.title, } @@ -93,7 +100,7 @@ def get_data(request): ).select_related("user_event__url_code") participated_events = [ { - "url_code": participation.user_event.url_code.url_code, + "url_code": get_url_code(participation.user_event), "public_id": participation.user_event.public_id, "title": participation.user_event.title, "guest_display_name": participation.display_name, From e27390e38294fab33728d6e99aca80ab7165e987 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Thu, 9 Jul 2026 16:40:47 -0400 Subject: [PATCH 53/71] Add logging to guest import endpoints --- backend/api/guest_import/views.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/backend/api/guest_import/views.py b/backend/api/guest_import/views.py index 4a447e1c..6313f0bb 100644 --- a/backend/api/guest_import/views.py +++ b/backend/api/guest_import/views.py @@ -117,6 +117,11 @@ def get_url_code(event): for participation in participated_events: if participation["public_id"] == conflict.user_event.public_id: participation["account_display_name"] = conflict.display_name + logger.info( + "%s conflicted events found during guest import for user: %s", + len(conflicts), + account_user.email, + ) response.data = { "created_events": created_events, @@ -166,12 +171,20 @@ def no_data_found(): user_account=guest_user ).select_related("user_event") if not guest_events.exists() and not guest_submissions.exists(): + logger.warning( + "No guest data found for import attempt for user: %s", + account_user.email, + ) return no_data_found() # Check if all the guest user's submissions are accounted for in the choices if set(availability_choices.keys()) != set( str(submission.user_event.public_id) for submission in guest_submissions ): + logger.warning( + "Availability choices do not match guest submissions for user: %s", + account_user.email, + ) response.data = { "error": { "availability_choices": [ From e5b781ebe73f012f01e0e45f8a8f920d9a16e8a8 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Thu, 9 Jul 2026 17:45:57 -0400 Subject: [PATCH 54/71] Update guest mode banner text --- frontend/src/app/dashboard/page-client.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/dashboard/page-client.tsx b/frontend/src/app/dashboard/page-client.tsx index 56b68702..aabf6106 100644 --- a/frontend/src/app/dashboard/page-client.tsx +++ b/frontend/src/app/dashboard/page-client.tsx @@ -83,15 +83,15 @@ export default function ClientPage({ {!logged_in && (
- This data is only available from this browser.{" "} + This data is only available from this browser,{" "} - Create an account + create an account {" "} - to sync data across devices. You can import this data at any time in - account settings. + to sync across devices. You can import this data into your account + at any time in account settings.
)} From 35cccd417efc068616e53c5d91c2764fa26f4f88 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Thu, 9 Jul 2026 17:49:17 -0400 Subject: [PATCH 55/71] Update guest summary wording --- .../src/features/guest-import/components/guest-summary.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/features/guest-import/components/guest-summary.tsx b/frontend/src/features/guest-import/components/guest-summary.tsx index f3239db7..3bc27fd4 100644 --- a/frontend/src/features/guest-import/components/guest-summary.tsx +++ b/frontend/src/features/guest-import/components/guest-summary.tsx @@ -15,8 +15,9 @@ export default function GuestSummary({ {events !== 1 ? "s" : ""} that you created
- • {availabilities} event - {availabilities !== 1 ? "s" : ""} that you added your availability to + • {availabilities} availabilit + {availabilities !== 1 ? "ies" : "y"} that you added to{" "} + {availabilities === 1 ? "an event" : "events"}
); From 1397ca60554eaf0539511e740050d949c6655b7f Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Thu, 9 Jul 2026 18:10:44 -0400 Subject: [PATCH 56/71] Add className support to SelectorDrawer --- frontend/src/features/selector/components/drawer.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/features/selector/components/drawer.tsx b/frontend/src/features/selector/components/drawer.tsx index 685f2b8e..965e9275 100644 --- a/frontend/src/features/selector/components/drawer.tsx +++ b/frontend/src/features/selector/components/drawer.tsx @@ -20,6 +20,7 @@ export default function SelectorDrawer({ drawerNesting = false, trigger, disabled = false, + className, }: DrawerProps) { const [internalOpen, setInternalOpen] = useState(false); @@ -89,6 +90,7 @@ export default function SelectorDrawer({ // Visual styles for disabled state disabled && "bg-foreground/20 text-foreground hover:bg-foreground/20 active:bg-foreground/20 cursor-not-allowed opacity-50 hover:cursor-not-allowed", + className, )} > Date: Thu, 9 Jul 2026 18:10:48 -0400 Subject: [PATCH 57/71] Change mobile settings nav to dropdown --- .../features/account/settings/sidebar-nav.tsx | 64 +++++++++++++------ 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/frontend/src/features/account/settings/sidebar-nav.tsx b/frontend/src/features/account/settings/sidebar-nav.tsx index 6d4a0ae4..723c3de4 100644 --- a/frontend/src/features/account/settings/sidebar-nav.tsx +++ b/frontend/src/features/account/settings/sidebar-nav.tsx @@ -1,8 +1,9 @@ "use client"; import Link from "next/link"; -import { usePathname } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; +import Selector from "@/features/selector/components/selector"; import { cn } from "@/lib/utils/classname"; const SETTINGS_TABS = [ @@ -14,26 +15,49 @@ const SETTINGS_TABS = [ export default function SettingsNav() { const pathname = usePathname(); + const router = useRouter(); return ( - + <> + + + ); } From fc11d97397c5254e759620efce58983cf3b4fe8d Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Thu, 9 Jul 2026 18:11:51 -0400 Subject: [PATCH 58/71] Update mobile loading skeleton --- frontend/src/app/settings/loading.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/settings/loading.tsx b/frontend/src/app/settings/loading.tsx index 5df2a735..b14ebb03 100644 --- a/frontend/src/app/settings/loading.tsx +++ b/frontend/src/app/settings/loading.tsx @@ -13,7 +13,7 @@ export default function Loading() {
-
+
From ae0d96372b64cae65164364b4b7cd69a9bba2547 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Thu, 9 Jul 2026 21:14:59 -0400 Subject: [PATCH 59/71] Update endpoint comment with errors --- frontend/src/lib/utils/api/endpoints.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/lib/utils/api/endpoints.ts b/frontend/src/lib/utils/api/endpoints.ts index c98abac0..74d53ff0 100644 --- a/frontend/src/lib/utils/api/endpoints.ts +++ b/frontend/src/lib/utils/api/endpoints.ts @@ -288,6 +288,8 @@ export const ROUTES = { * Imports the guest user's data into the logged-in user's account, resolving any * availability submission conflicts based on the provided choices. * @method POST + * @throws 400 - If there is no guest data found. + * @throws 400 - If the availability choices do not match the guest submissions. */ importData: route("/guest-import/import-data/"), } From de5ddd4b240a1ca56772df535428f68a6e2614a2 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Thu, 9 Jul 2026 21:17:35 -0400 Subject: [PATCH 60/71] Move session logic outside of try block --- backend/api/guest_import/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/api/guest_import/utils.py b/backend/api/guest_import/utils.py index 4d49701a..6ebe4444 100644 --- a/backend/api/guest_import/utils.py +++ b/backend/api/guest_import/utils.py @@ -29,10 +29,10 @@ def get_guest_account(request, response): raise UserSession.DoesNotExist update_session_metadata(request, session) - - set_session_cookie(response, GUEST_COOKIE_NAME, session.session_token, True) - return session.user_account except UserSession.DoesNotExist: logger.info("Guest session expired.") + else: + set_session_cookie(response, GUEST_COOKIE_NAME, session.session_token, True) + return session.user_account return None From 1fe1e791d1bea50c0f9f83df8c2e6987163af218 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Thu, 9 Jul 2026 21:19:52 -0400 Subject: [PATCH 61/71] Add select related call --- backend/api/guest_import/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/api/guest_import/views.py b/backend/api/guest_import/views.py index 6313f0bb..737e3013 100644 --- a/backend/api/guest_import/views.py +++ b/backend/api/guest_import/views.py @@ -112,7 +112,7 @@ def get_url_code(event): conflicts = EventParticipant.objects.filter( user_account=account_user, user_event__in=participations.values_list("user_event", flat=True), - ) + ).select_related("user_event") for conflict in conflicts: for participation in participated_events: if participation["public_id"] == conflict.user_event.public_id: From ee9cb7bd119141a34893b7b80ed066713bf37f44 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Thu, 9 Jul 2026 21:22:48 -0400 Subject: [PATCH 62/71] Optimize conflict assignment --- backend/api/guest_import/views.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/api/guest_import/views.py b/backend/api/guest_import/views.py index 737e3013..18bdd723 100644 --- a/backend/api/guest_import/views.py +++ b/backend/api/guest_import/views.py @@ -113,10 +113,11 @@ def get_url_code(event): user_account=account_user, user_event__in=participations.values_list("user_event", flat=True), ).select_related("user_event") + participation_map = {p["public_id"]: p for p in participated_events} for conflict in conflicts: - for participation in participated_events: - if participation["public_id"] == conflict.user_event.public_id: - participation["account_display_name"] = conflict.display_name + participation = participation_map.get(conflict.user_event.public_id) + if participation: + participation["account_display_name"] = conflict.display_name logger.info( "%s conflicted events found during guest import for user: %s", len(conflicts), From 2fc7c979eb60dd9c3f9a90b72330f5a41f17de61 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Thu, 9 Jul 2026 21:24:52 -0400 Subject: [PATCH 63/71] Update docstring for import endpoint --- backend/api/guest_import/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/api/guest_import/views.py b/backend/api/guest_import/views.py index 18bdd723..e852628c 100644 --- a/backend/api/guest_import/views.py +++ b/backend/api/guest_import/views.py @@ -141,7 +141,7 @@ def import_data(request): """ Transfers ownership of events and availabilities created by the guest to the account. - The body of the request should contain a mapping of event URL codes to the user's + The body of the request should contain a mapping of event's public ID to the user's chosen submission to keep, for each event the guest had submitted availability to. If there is no conflict, "guest" should be specified. From e6244b507bc65ecb269093baba18e011b698fe28 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Thu, 9 Jul 2026 21:29:12 -0400 Subject: [PATCH 64/71] Guard local storage read on server --- frontend/src/app/(auth)/login/page.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/(auth)/login/page.tsx b/frontend/src/app/(auth)/login/page.tsx index ceedbafa..9674e59e 100644 --- a/frontend/src/app/(auth)/login/page.tsx +++ b/frontend/src/app/(auth)/login/page.tsx @@ -25,12 +25,7 @@ export default function Page() { const router = useRouter(); const searchParams = useSearchParams(); - const dontShowGuestImport = - localStorage.getItem(DONT_SHOW_AGAIN_KEY) === "true"; - const callbackUrl = getSafeRedirectUrl( - searchParams.get("callbackUrl"), - dontShowGuestImport ? undefined : "/guest-import/login", - ); + const callbackUrl = searchParams.get("callbackUrl"); // TOASTS AND ERROR STATES const { errors, handleError, clearAllErrors } = useFormErrors(); @@ -60,12 +55,19 @@ export default function Page() { } try { + const dontShowGuestImport = + localStorage.getItem(DONT_SHOW_AGAIN_KEY) === "true"; + const redirectUrl = getSafeRedirectUrl( + callbackUrl, + dontShowGuestImport ? undefined : "/guest-import/login", + ); + await clientPost(ROUTES.auth.login, { email, password, remember_me: rememberMe, }); - router.push(callbackUrl); + router.push(redirectUrl); router.refresh(); return true; } catch (e) { From 493d66024365c3ad54eb06e1ef29cf2665fea0dd Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Thu, 9 Jul 2026 21:33:46 -0400 Subject: [PATCH 65/71] Add unchecked don't show again logic --- frontend/src/app/guest-import/login/page-client.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/app/guest-import/login/page-client.tsx b/frontend/src/app/guest-import/login/page-client.tsx index 4381bb5a..692bf032 100644 --- a/frontend/src/app/guest-import/login/page-client.tsx +++ b/frontend/src/app/guest-import/login/page-client.tsx @@ -22,6 +22,8 @@ export default function ClientPage({ const navigate = (route: string) => { if (dontShowAgain) { localStorage.setItem(DONT_SHOW_AGAIN_KEY, "true"); + } else { + localStorage.removeItem(DONT_SHOW_AGAIN_KEY); } router.push(route); }; From ee47898e13ad5c05b31071653a198a0408ee2a2e Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Thu, 9 Jul 2026 21:34:44 -0400 Subject: [PATCH 66/71] Add aria-current to settings nav --- frontend/src/features/account/settings/sidebar-nav.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/features/account/settings/sidebar-nav.tsx b/frontend/src/features/account/settings/sidebar-nav.tsx index 723c3de4..bcddf11c 100644 --- a/frontend/src/features/account/settings/sidebar-nav.tsx +++ b/frontend/src/features/account/settings/sidebar-nav.tsx @@ -46,6 +46,7 @@ export default function SettingsNav() { Date: Tue, 14 Jul 2026 18:32:42 -0400 Subject: [PATCH 67/71] Fix data get from request --- backend/api/guest_import/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/api/guest_import/views.py b/backend/api/guest_import/views.py index e852628c..05806524 100644 --- a/backend/api/guest_import/views.py +++ b/backend/api/guest_import/views.py @@ -148,7 +148,7 @@ def import_data(request): Every conflicted submission that is not kept will be deleted when calling this endpoint. """ - availability_choices = request.data.get("availability_choices", {}) + availability_choices = request.validated_data.get("availability_choices", {}) response = Response() From f5e85b6d99d1f7d9eab6a8e62d4c88b80822804f Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Tue, 14 Jul 2026 18:34:54 -0400 Subject: [PATCH 68/71] Add try-catch guards to localStorage access --- frontend/src/app/(auth)/login/page.tsx | 7 +++++-- frontend/src/app/guest-import/login/page-client.tsx | 12 +++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/(auth)/login/page.tsx b/frontend/src/app/(auth)/login/page.tsx index 9674e59e..8ca06b1b 100644 --- a/frontend/src/app/(auth)/login/page.tsx +++ b/frontend/src/app/(auth)/login/page.tsx @@ -55,8 +55,11 @@ export default function Page() { } try { - const dontShowGuestImport = - localStorage.getItem(DONT_SHOW_AGAIN_KEY) === "true"; + let dontShowGuestImport = false; + try { + dontShowGuestImport = + localStorage.getItem(DONT_SHOW_AGAIN_KEY) === "true"; + } catch {} const redirectUrl = getSafeRedirectUrl( callbackUrl, dontShowGuestImport ? undefined : "/guest-import/login", diff --git a/frontend/src/app/guest-import/login/page-client.tsx b/frontend/src/app/guest-import/login/page-client.tsx index 692bf032..c4a50d41 100644 --- a/frontend/src/app/guest-import/login/page-client.tsx +++ b/frontend/src/app/guest-import/login/page-client.tsx @@ -20,11 +20,13 @@ export default function ClientPage({ const router = useRouter(); const navigate = (route: string) => { - if (dontShowAgain) { - localStorage.setItem(DONT_SHOW_AGAIN_KEY, "true"); - } else { - localStorage.removeItem(DONT_SHOW_AGAIN_KEY); - } + try { + if (dontShowAgain) { + localStorage.setItem(DONT_SHOW_AGAIN_KEY, "true"); + } else { + localStorage.removeItem(DONT_SHOW_AGAIN_KEY); + } + } catch {} router.push(route); }; From 0a470fb9996173b061803b1a197590c0c575c6c5 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Tue, 14 Jul 2026 18:37:38 -0400 Subject: [PATCH 69/71] Add actual error message to guest import --- frontend/src/app/guest-import/import/page-client.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/guest-import/import/page-client.tsx b/frontend/src/app/guest-import/import/page-client.tsx index ed0fe967..5f17bbda 100644 --- a/frontend/src/app/guest-import/import/page-client.tsx +++ b/frontend/src/app/guest-import/import/page-client.tsx @@ -15,6 +15,7 @@ import { Banner, useToast } from "@/features/system-feedback"; import Tooltip from "@/features/system-feedback/tooltip/base"; import { clientPost } from "@/lib/utils/api/client-fetch"; import { ROUTES } from "@/lib/utils/api/endpoints"; +import { ApiErrorResponse } from "@/lib/utils/api/fetch-wrapper"; import { GuestData } from "@/lib/utils/api/types"; import { cn } from "@/lib/utils/classname"; @@ -65,10 +66,12 @@ export default function ClientPage({ guestData }: { guestData: GuestData }) { }); addToast("success", "Guest data imported successfully."); router.push("/dashboard"); - } catch { + } catch (e) { + const error = e as ApiErrorResponse; addToast( "error", - "Something went wrong with your request. Please refresh the page and try again.", + error.formattedMessage || + "Something went wrong with your request. Please refresh the page and try again.", ); } }; From 000f80ac846eed6647bafc8433189beb8538ec73 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Tue, 14 Jul 2026 18:39:28 -0400 Subject: [PATCH 70/71] Fix improper HTML semantics --- frontend/src/app/guest-import/import/page-client.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/guest-import/import/page-client.tsx b/frontend/src/app/guest-import/import/page-client.tsx index 5f17bbda..a371ed2d 100644 --- a/frontend/src/app/guest-import/import/page-client.tsx +++ b/frontend/src/app/guest-import/import/page-client.tsx @@ -99,10 +99,10 @@ export default function ClientPage({ guestData }: { guestData: GuestData }) { ); const actionText = ( -

+

Are you sure you want to import this guest data? This action cannot be undone. -

+
); const noneText = ( From 4fdd22e1ff1d561f4bb4876f8aedb417ee62bcb3 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Tue, 14 Jul 2026 18:58:02 -0400 Subject: [PATCH 71/71] Remove parameter reassigning --- .../system-feedback/dialog/components/confirmation.tsx | 6 ++---- .../src/features/system-feedback/dialog/components/form.tsx | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/frontend/src/features/system-feedback/dialog/components/confirmation.tsx b/frontend/src/features/system-feedback/dialog/components/confirmation.tsx index c693875f..ce42a703 100644 --- a/frontend/src/features/system-feedback/dialog/components/confirmation.tsx +++ b/frontend/src/features/system-feedback/dialog/components/confirmation.tsx @@ -30,9 +30,7 @@ export default function ConfirmationDialog({ const open = isControlled ? controlledOpen : uncontrolledOpen; const isMobile = useCheckMobile(); - if (isMobile && !asNestedDrawer) { - asNestedDrawer = true; - } + const effectiveAsNestedDrawer = asNestedDrawer || isMobile; const handleOpenChange = useCallback( (newOpen: boolean) => { @@ -88,7 +86,7 @@ export default function ConfirmationDialog({ trigger={trigger} open={open} onOpenChange={handleOpenChange} - asNestedDrawer={asNestedDrawer} + asNestedDrawer={effectiveAsNestedDrawer} triggerDisabled={triggerDisabled} showCloseButton={showCloseButton} overlayClassName={cn( diff --git a/frontend/src/features/system-feedback/dialog/components/form.tsx b/frontend/src/features/system-feedback/dialog/components/form.tsx index 5065179a..33605ce4 100644 --- a/frontend/src/features/system-feedback/dialog/components/form.tsx +++ b/frontend/src/features/system-feedback/dialog/components/form.tsx @@ -32,9 +32,7 @@ export default function FormDialog({ const open = isControlled ? controlledOpen : uncontrolledOpen; const isMobile = useCheckMobile(); - if (isMobile && !asNestedDrawer) { - asNestedDrawer = true; - } + const effectiveAsNestedDrawer = asNestedDrawer || isMobile; const handleOpenChange = useCallback( (newOpen: boolean) => { @@ -64,7 +62,7 @@ export default function FormDialog({ trigger={trigger} open={open} onOpenChange={handleOpenChange} - asNestedDrawer={asNestedDrawer} + asNestedDrawer={effectiveAsNestedDrawer} showCloseButton={showCloseButton} overlayClassName={cn( type === "error" &&