Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions frontend/src/app/(event)/[event-code]/page-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import { PencilIcon, ShareIcon, SquarePenIcon } from "lucide-react";

import KebabMenu from "@/components/kebab-menu";
import { EventInformation } from "@/core/event/types";
import ActionButton from "@/features/button/components/action";
import EmptyButton from "@/features/button/components/empty";
import LinkButton from "@/features/button/components/link";
import ScheduleGrid from "@/features/event/grid/grid";
import { GRID_ID_SELECTOR } from "@/features/event/grid/lib/constants";
import AttendeesPanel from "@/features/event/results/attendees/desktop-panel";
import AttendeesDrawer from "@/features/event/results/attendees/mobile-drawer";
import { getResultBanners } from "@/features/event/results/components/banners";
Expand All @@ -20,6 +22,7 @@ import {
import { ResultsInformation } from "@/features/event/results/lib/types";
import HeaderSpacer from "@/features/header/components/header-spacer";
import ShareMenu from "@/features/share-menu/menu";
import { useViewTransition } from "@/lib/hooks/use-view-transition";
import { cn } from "@/lib/utils/classname";

export default function ClientPage({
Expand Down Expand Up @@ -86,13 +89,18 @@ function EventResults({ eventData }: { eventData: EventInformation }) {
currentUser !== null,
);

const doViewTransition = useViewTransition();

/* BUTTONS */
const paintingButton = (
<LinkButton
<ActionButton
buttonStyle="primary"
icon={<SquarePenIcon />}
label={(currentUser ? "Edit" : "Add") + " Availability"}
href={`/${eventCode}/painting`}
onClick={() => {
doViewTransition(`/${eventCode}/painting`, GRID_ID_SELECTOR);
}}
loadOnSuccess
/>
);

Expand Down Expand Up @@ -172,6 +180,13 @@ function EventResults({ eventData }: { eventData: EventInformation }) {
eventTitle={eventTitle}
eventCode={eventCode}
/>
<div
aria-hidden="true"
className="pointer-events-none fixed left-0 right-0 top-[100vh] w-[100vw]"
style={{
viewTransitionName: "painting-island",
}}
/>
</div>

{/* Desktop Sidebar */}
Expand Down
21 changes: 15 additions & 6 deletions frontend/src/app/(event)/[event-code]/painting/page-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import { useEffect, useRef, useState } from "react";

import { parseISO } from "date-fns";
import { useRouter } from "next/navigation";
import { useDebouncedCallback } from "use-debounce";

import Checkbox from "@/components/checkbox";
Expand All @@ -12,17 +11,18 @@ import TextInputField from "@/components/text-input-field";
import { useAvailability } from "@/core/availability/use-availability";
import { EventRange } from "@/core/event/types";
import ActionButton from "@/features/button/components/action";
import LinkButton from "@/features/button/components/link";
import { MAX_DISPLAY_NAME_LENGTH } from "@/features/event/availability/constants";
import { validateAvailabilityData } from "@/features/event/availability/validate-data";
import TimeZoneSelector from "@/features/event/components/selectors/timezone";
import { ScheduleGrid } from "@/features/event/grid";
import { GRID_ID_SELECTOR } from "@/features/event/grid/lib/constants";
import HeaderSpacer from "@/features/header/components/header-spacer";
import {
ConfirmationDialog,
RateLimitBanner,
useToast,
} from "@/features/system-feedback";
import { useViewTransition } from "@/lib/hooks/use-view-transition";
import { MESSAGES } from "@/lib/messages";
import { clientPost } from "@/lib/utils/api/client-fetch";
import { ROUTES } from "@/lib/utils/api/endpoints";
Expand All @@ -46,7 +46,7 @@ export default function ClientPage({
timeslots: Date[];
initialData: SelfAvailability | null;
}) {
const router = useRouter();
const doViewTransition = useViewTransition();

// AVAILABILITY STATE
const { state, setDisplayName, setTimeZone, toggleSlot } = useAvailability(
Expand Down Expand Up @@ -212,7 +212,7 @@ export default function ClientPage({

try {
await clientPost(ROUTES.availability.add, payload);
router.push(`/${eventCode}`);
doViewTransition(`/${eventCode}`, GRID_ID_SELECTOR);
return true;
} catch (e) {
const error = e as ApiErrorResponse;
Expand All @@ -230,10 +230,11 @@ export default function ClientPage({

// BUTTONS
const cancelButton = (
<LinkButton
<ActionButton
buttonStyle="transparent"
label={initialData?.display_name ? "Cancel Edits" : "Cancel"}
href={`/${eventCode}`}
onClick={() => doViewTransition(`/${eventCode}`, GRID_ID_SELECTOR)}
loadOnSuccess
/>
);
const submitButton = (
Expand Down Expand Up @@ -321,6 +322,7 @@ export default function ClientPage({
<MobileFooterIsland
leftButtons={[cancelButton]}
rightButtons={[submitButton]}
viewTransitionName="painting-island"
>
<div className="mx-3 -mt-2">
<DisplayNameInput
Expand All @@ -333,6 +335,13 @@ export default function ClientPage({
/>
</div>
</MobileFooterIsland>
<div
aria-hidden="true"
className="pointer-events-none fixed left-0 right-0 top-[100vh] w-[100vw] md:hidden"
style={{
viewTransitionName: "results-drawer",
}}
/>
</div>

<ConfirmationDialog
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/components/mobile-footer-island.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ export default function MobileFooterIsland({
children,
leftButtons,
rightButtons,
viewTransitionName,
}: {
children?: React.ReactNode;
leftButtons?: ButtonArray;
rightButtons?: ButtonArray;
viewTransitionName?: string;
}) {
const [islandHeight, setIslandHeight] = useState(0);
const islandRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -40,6 +42,11 @@ export default function MobileFooterIsland({
"border-foreground/10 border",
"flex flex-col gap-3",
)}
style={{
viewTransitionName: viewTransitionName
? viewTransitionName
: undefined,
}}
>
{children}
<div className="flex w-full justify-between gap-2">
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/features/drawer/components/base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default function BaseDrawer({
showOverlay = !frostedGlass && modal,
nested = false,
hideCloseButton = false,
viewTransitionName,
...rest
}: DrawerProps) {
useDrawerResize();
Expand Down Expand Up @@ -155,7 +156,12 @@ export default function BaseDrawer({
_type !== "floating" && "h-[100dvh]",
contentClassName,
)}
style={{ zIndex: contentZIndex }}
style={{
zIndex: contentZIndex,
viewTransitionName: viewTransitionName
? viewTransitionName
: undefined,
}}
>
<div
className="flex w-full flex-col"
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/features/drawer/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ type BaseDrawerProps = {
* @default false
*/
hideCloseButton?: boolean;
/**
* Optional name identifier to apply to the drawer content for smooth page transitions.
*/
viewTransitionName?: string;
};

export type StandardDrawerProps = BaseDrawerProps & {
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/features/event/grid/grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ResultsAvailabilityMap,
} from "@/core/availability/types";
import { createEmptyUserAvailability } from "@/core/availability/utils";
import { GRID_ID } from "@/features/event/grid/lib/constants";
import useGridinfo from "@/features/event/grid/lib/use-grid";
import ScheduleHeader from "@/features/event/grid/schedule-header";
import TimeColumn from "@/features/event/grid/time-column";
Expand Down Expand Up @@ -129,7 +130,14 @@ export default function ScheduleGrid({
if (error) return <GridMessage error={true} message={error} />;

return (
<div className="relative grid h-full w-full grid-cols-[1fr] grid-rows-[auto_1fr]">
<div
className={cn(
"relative grid h-full w-full grid-cols-[1fr] grid-rows-[auto_1fr]",
mode === "preview" ? "bg-background md:bg-panel" : "bg-background",
)}
style={{ viewTransitionName: "grid" }}
id={GRID_ID}
>
<ScheduleHeader
preview={mode === "preview"}
visibleDays={visibleDays}
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/features/event/grid/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
export const SIDE_WIDTH = 30;
export const TIME_LABEL_WIDTH = 50;

export const GRID_ID = "schedule-grid";
export const GRID_ID_SELECTOR = `#${GRID_ID}`;
33 changes: 27 additions & 6 deletions frontend/src/features/event/results/attendees/mobile-drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ import { useEffect, useState } from "react";

import { ShareIcon, SquarePenIcon } from "lucide-react";

import ActionButton from "@/features/button/components/action";
import EmptyButton from "@/features/button/components/empty";
import LinkButton from "@/features/button/components/link";
import { MorphingDrawer } from "@/features/drawer";
import { GRID_ID_SELECTOR } from "@/features/event/grid/lib/constants";
import PanelHeader from "@/features/event/results/attendees/panel-header";
import ParticipantList from "@/features/event/results/attendees/participant-list";
import {
RemoveParticipantDialog,
useParticipantRemoval,
} from "@/features/event/results/attendees/remove-participant";
import ShareMenu from "@/features/share-menu/menu";
import { useViewTransition } from "@/lib/hooks/use-view-transition";

export default function AttendeesDrawer({
onSnapChange,
Expand All @@ -35,8 +37,11 @@ export default function AttendeesDrawer({
clearSelectedParticipants,
} = useParticipantRemoval();

/* TABS */
const [activeSnap, setActiveSnap] = useState<number | string | null>(0.22);
/* SNAP POINTS */
const [activeSnap, setActiveSnap] = useState<number | string | null>(0);
const [currentSnapPoints, setCurrentSnapPoints] = useState<number[]>([
0, 0.22, 0.37,
]);

useEffect(() => {
onSnapChange(activeSnap);
Expand All @@ -51,16 +56,31 @@ export default function AttendeesDrawer({
}
}, [isCollapsed, isRemoving, clearSelectedParticipants, setIsRemoving]);

const doViewTransition = useViewTransition();

/* BUTTONS */
const paintingButton = (
<LinkButton
<ActionButton
buttonStyle="primary"
icon={<SquarePenIcon />}
label={(currentUser ? "Edit" : "Add") + " Availability"}
href={`/${eventCode}/painting`}
onClick={() => {
doViewTransition(`/${eventCode}/painting`, GRID_ID_SELECTOR);
}}
loadOnSuccess
/>
);

useEffect(() => {
// Wait for the view transition to finish, then animate the drawer in
const timer = setTimeout(() => {
setCurrentSnapPoints([0.22, 0.37]);
setActiveSnap(0.22);
}, 350);

return () => clearTimeout(timer);
}, []);

return (
<MorphingDrawer
open
Expand All @@ -70,7 +90,7 @@ export default function AttendeesDrawer({
setActiveSnapPoint={setActiveSnap}
title="Attendees List"
description="View attendees for this event"
snapPoints={[0.22, 0.37]}
snapPoints={currentSnapPoints}
modal={false}
floatingAtLowestSnap
scrollableBody
Expand Down Expand Up @@ -100,6 +120,7 @@ export default function AttendeesDrawer({
{paintingButton}
</div>
}
viewTransitionName="results-drawer"
>
<ParticipantList
isRemoving={isRemoving}
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/features/header/components/theme-picker.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
"use client";

import { MonitorIcon, MoonIcon, SunIcon, SunMoonIcon } from "lucide-react";
import { useTheme } from "next-themes";

import KebabMenu from "@/components/kebab-menu";
import SegmentedControl from "@/components/segmented-control";
import EmptyButton from "@/features/button/components/empty";
import ShrinkingHeaderButton from "@/features/header/components/buttons/shrinking-header";
import { useHeaderSize } from "@/features/header/context";
import { useThemeToggle } from "@/lib/hooks/use-theme-toggle";

export default function ThemePicker() {
const { activeMenu, setActiveMenu } = useHeaderSize();
const { theme = "system", setTheme } = useTheme();
const { theme = "system", toggleTheme } = useThemeToggle();

const isMenuOpen = activeMenu === "theme";

Expand Down Expand Up @@ -46,7 +46,7 @@ export default function ThemePicker() {
{ value: "dark", label: <MoonIcon />, ariaLabel: "Dark Theme" },
]}
value={theme}
onChange={setTheme}
onChange={toggleTheme}
className="frosted-glass-inset"
/>
<div className="text-center text-sm opacity-75">
Expand Down
30 changes: 30 additions & 0 deletions frontend/src/lib/hooks/use-theme-toggle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useTheme } from "next-themes";

export function useThemeToggle() {
const { theme, resolvedTheme, setTheme } = useTheme();

const toggleTheme = (newTheme: string) => {
let targetTheme = newTheme;

if (newTheme === "system") {
targetTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}

if (targetTheme === resolvedTheme) {
setTheme(newTheme);
return;
}

if (!document.startViewTransition) {
setTheme(newTheme);
} else {
document.startViewTransition(() => {
setTheme(newTheme);
});
}
};

return { theme, toggleTheme };
}
Loading
Loading