Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
0b59f86
Add share button
jzgom067 Apr 11, 2026
88335f1
Add check for web share support
jzgom067 Apr 11, 2026
111eb84
Merge branch 'v0.4.3' into share-button
jzgom067 Apr 11, 2026
33f64fc
Update share button check comment
jzgom067 Apr 11, 2026
60d5b26
Centralize button style type
jzgom067 Apr 11, 2026
fab3246
Merge branch 'share-button' of https://github.com/plan-cake/plancake …
jzgom067 Apr 11, 2026
654d0c5
Change hovered slot scaling to a fixed amount
jzgom067 Apr 11, 2026
81b0c8f
Increase hovered slot border thickness
jzgom067 Apr 11, 2026
cb37ab1
Slightly decrease hovered slot height
jzgom067 Apr 11, 2026
a13d47e
Add icon prop to time slot component
jzgom067 Apr 11, 2026
014d319
Add sparkles icon for best times
jzgom067 Apr 11, 2026
8da41b3
Create getHighestMatchCount util function
jzgom067 Apr 11, 2026
3154ec3
Add thumbs up icon for best but not ideal times
jzgom067 Apr 11, 2026
288722c
Change grid to scrollable on all desktop views
jzgom067 Apr 12, 2026
176ce42
Add extra bottom padding for painting and results
jzgom067 Apr 12, 2026
56c69cc
Remove sticky classes from results and painting
jzgom067 Apr 12, 2026
3fe3c35
Remove unused variables and imports
jzgom067 Apr 12, 2026
b0827ff
Change participant chips to lion color
jzgom067 Apr 12, 2026
0f5fb81
Merge pull request #322 from plan-cake/participant-chip-recolor
mirmirmirr Apr 13, 2026
1884e53
Merge pull request #315 from plan-cake/share-button
mirmirmirr Apr 13, 2026
ce00839
Merge branch 'main' into v0.4.3
jzgom067 Apr 13, 2026
9136841
Remove transition from morphing drawer
jzgom067 Apr 15, 2026
90c19da
Slightly decrease floating drawer side padding
jzgom067 Apr 15, 2026
0f7bd99
Fix drawer content scaling pop-in
jzgom067 Apr 17, 2026
fb50c48
Fix padding transition definition
jzgom067 Apr 17, 2026
d92d3ee
Move time slot icon to right side
jzgom067 Apr 17, 2026
7bcec58
Update time slot icons
jzgom067 Apr 17, 2026
40b470e
Update banners for no mutual times
jzgom067 Apr 17, 2026
9ea7464
Add separate message for results page banner
jzgom067 Apr 17, 2026
bce1483
Remove unused time slot property
jzgom067 Apr 17, 2026
d6ca305
Tighten icon prop type
jzgom067 Apr 17, 2026
605e529
Merge branch 'v0.4.3' into grid-color-distinction
jzgom067 Apr 17, 2026
8dc019e
Remove unnecessary side panel classes
jzgom067 May 3, 2026
49c2754
Adjust side panel max height
jzgom067 May 3, 2026
a2b3cad
Add dynamic schedule header for scrollbar spacing
jzgom067 May 3, 2026
ef9ae1a
Merge branch 'v0.4.3' into scrollable-grid
jzgom067 May 3, 2026
5d58996
Create kebab menu for theme picker
jzgom067 May 3, 2026
5d19a81
Add header activeMenu functionality
jzgom067 May 3, 2026
edec6a6
Add closeOnClick option to KebabMenu
jzgom067 May 3, 2026
e406168
Set closeOnClick on theme picker
jzgom067 May 3, 2026
295756e
Add anchor point options to kebab menu
jzgom067 May 4, 2026
aeddf24
Add top center anchor point to theme picker
jzgom067 May 4, 2026
ef9e775
Add more content to theme picker
jzgom067 May 4, 2026
8272a3c
Add aria-label to theme picker button
jzgom067 May 4, 2026
ccf831d
Rename ThemeToggle to ThemePicker
jzgom067 May 4, 2026
7434318
Add ariaLabel to segmented control options
jzgom067 May 4, 2026
e9d383d
Add ariaLabels to theme picker
jzgom067 May 4, 2026
8e0c9c7
Replace leave event icon with door open
jzgom067 May 4, 2026
055ce77
Reword self removal
jzgom067 May 4, 2026
2fafb5e
Update mobile self removal text
jzgom067 May 4, 2026
f511fc3
Update equality check for consistency
jzgom067 May 4, 2026
0cb2b15
Add isort to requirements.txt
jzgom067 May 5, 2026
ff71baa
Merge pull request #321 from plan-cake/scrollable-grid
mirmirmirr May 9, 2026
bf40e83
Merge pull request #324 from plan-cake/grid-color-distinction
mirmirmirr May 9, 2026
7bd14a4
Merge pull request #334 from plan-cake/match-system-theme
mirmirmirr May 9, 2026
f566552
Merge pull request #335 from plan-cake/leave-event-icon
mirmirmirr May 9, 2026
9c57f3a
Merge pull request #338 from plan-cake/isort-dependency
mirmirmirr May 9, 2026
ac9635a
Merge pull request #323 from plan-cake/results-drawer-fix
mirmirmirr May 9, 2026
4d4d1dd
Add version data
jzgom067 May 9, 2026
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
1 change: 1 addition & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ django-environ==0.12.0
django-ses==4.4.0
djangorestframework==3.16.0
gunicorn==23.0.0
isort==8.0.1
jmespath==1.0.1
kombu==5.5.4
packaging==25.0
Expand Down
69 changes: 53 additions & 16 deletions frontend/src/app/(event)/[event-code]/page-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

import { useState } from "react";

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

import CopyToastButton from "@/components/copy-toast-button";
import KebabMenu from "@/components/kebab-menu";
import { EventInformation } from "@/core/event/types";
import ActionButton from "@/features/button/components/action";
import LinkButton from "@/features/button/components/link";
import ScheduleGrid from "@/features/event/grid/grid";
import AttendeesPanel from "@/features/event/results/attendee-panel/panel";
Expand All @@ -19,7 +20,8 @@ import DisplaySettings from "@/features/event/results/display-settings";
import ResultsDrawer from "@/features/event/results/drawer";
import { ResultsInformation } from "@/features/event/results/lib/types";
import HeaderSpacer from "@/features/header/components/header-spacer";
import { useHeaderSize } from "@/features/header/context";
import { useToast } from "@/features/system-feedback";
import { MESSAGES } from "@/lib/messages";
import { cn } from "@/lib/utils/classname";

export default function ClientPage({
Expand All @@ -36,6 +38,8 @@ export default function ClientPage({
);
}

type HeaderButtonStyle = "frosted glass inset" | "secondary";

function EventResults({ eventData }: { eventData: EventInformation }) {
const {
hoveredSlot,
Expand All @@ -57,6 +61,9 @@ function EventResults({ eventData }: { eventData: EventInformation }) {
timeslots,
} = eventData;

/* TOAST PROVIDER */
const { addToast } = useToast();

/* TIMEZONE HANDLING */
const handleTZChange = (newTZ: string | number) => {
setTimezone(newTZ.toString());
Expand All @@ -75,9 +82,6 @@ function EventResults({ eventData }: { eventData: EventInformation }) {
return drawerSnap;
};

/* HEADER SPACING */
const { topMarginClass } = useHeaderSize();

/* BANNERS */
const banners = getResultBanners(
availabilities,
Expand All @@ -97,7 +101,7 @@ function EventResults({ eventData }: { eventData: EventInformation }) {
/>
);

const editButton = (buttonStyle: "frosted glass inset" | "secondary") => (
const editButton = (buttonStyle: HeaderButtonStyle) => (
<LinkButton
buttonStyle={buttonStyle}
icon={<PencilIcon />}
Expand All @@ -106,12 +110,48 @@ function EventResults({ eventData }: { eventData: EventInformation }) {
/>
);

const copyButton = (buttonStyle: "frosted glass inset" | "secondary") => (
const shareButton = (buttonStyle: HeaderButtonStyle) => {
// Check if sharing is supported
if (typeof navigator !== "undefined" && !navigator.share) {
/* This condition means it will be rendered until mounted on the client, then it
* disappears if not supported. There are more browsers that support the API than
* don't, so this is a better trade-off than having the button appear after initial
* mount on supported browsers.
*
* This also won't be visible on mobile anyway, since the buttons are hidden in the
* kebab menu.
*/
return null;
} else {
return (
<ActionButton
buttonStyle={buttonStyle}
icon={<ShareIcon />}
label="Share Event"
onClick={async () => {
try {
await navigator.share({
title: eventTitle,
url: window.location.href,
});
} catch (error) {
// An error is thrown if sharing is cancelled, ignore that
if (error instanceof Error && error.name !== "AbortError") {
addToast("error", MESSAGES.ERROR_GENERIC);
}
}
}}
/>
);
}
};

const copyButton = (buttonStyle: HeaderButtonStyle) => (
<CopyToastButton code={eventCode} buttonStyle={buttonStyle} />
);

return (
<div className="flex flex-col space-y-4 pl-6 pr-6">
<div className="flex flex-col space-y-4 pl-6 pr-6 md:h-screen">
<HeaderSpacer />

{/* Header */}
Expand All @@ -121,20 +161,22 @@ function EventResults({ eventData }: { eventData: EventInformation }) {
<div className="md:hidden">
<KebabMenu>
{isCreator && editButton("frosted glass inset")}
{shareButton("frosted glass inset")}
{copyButton("frosted glass inset")}
</KebabMenu>
</div>

<div className="ml-auto hidden flex-wrap justify-end gap-2 md:flex">
{isCreator && editButton("secondary")}
{shareButton("secondary")}
{copyButton("secondary")}
{paintingButton}
</div>
</div>

<div className="md:hidden">{banners}</div>

<div className="flex h-fit flex-col md:flex-row md:gap-4">
<div className="flex min-h-0 flex-1 flex-col md:flex-row md:gap-4">
<ScheduleGrid
mode="view"
isWeekdayEvent={eventRange.type === "weekday"}
Expand Down Expand Up @@ -164,17 +206,12 @@ function EventResults({ eventData }: { eventData: EventInformation }) {
<div
className={cn(
"hidden md:block",
"fixed bottom-1 left-0 z-10 w-full shrink-0 px-6",
"z-10 w-full shrink-0",
"relative bottom-auto left-auto w-80 space-y-4 px-0",
)}
>
{banners}
<div
className={cn(
"sticky flex max-h-[calc(100vh-8rem)] flex-col gap-y-4",
topMarginClass,
)}
>
<div className="flex max-h-[calc(100vh-18rem)] flex-col gap-y-4">
<AttendeesPanel />
<div className="bg-panel shrink-0 rounded-3xl p-6 text-sm">
<DisplaySettings
Expand Down
16 changes: 3 additions & 13 deletions frontend/src/app/(event)/[event-code]/painting/page-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import { validateAvailabilityData } from "@/features/event/availability/validate
import TimeZoneSelector from "@/features/event/components/selectors/timezone";
import { ScheduleGrid } from "@/features/event/grid";
import HeaderSpacer from "@/features/header/components/header-spacer";
import { useHeaderSize } from "@/features/header/context";
import {
ConfirmationDialog,
RateLimitBanner,
Expand All @@ -28,7 +27,6 @@ import { clientPost } from "@/lib/utils/api/client-fetch";
import { ROUTES } from "@/lib/utils/api/endpoints";
import { ApiErrorResponse } from "@/lib/utils/api/fetch-wrapper";
import { SelfAvailability } from "@/lib/utils/api/types";
import { cn } from "@/lib/utils/classname";
import { timeslotToISOString } from "@/lib/utils/date-time-format";

export default function ClientPage({
Expand All @@ -46,9 +44,6 @@ export default function ClientPage({
}) {
const router = useRouter();

// HEADER SIZE CONTEXT
const { topMarginClass } = useHeaderSize();

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

return (
<div className="flex flex-col space-y-4 pl-6 pr-6">
<div className="flex flex-col space-y-4 pl-6 pr-6 md:h-screen">
<HeaderSpacer />

{/* Rate Limit Error */}
Expand All @@ -264,14 +259,9 @@ export default function ClientPage({
</div>

{/* Main Content */}
<div className="mb-12 flex h-fit flex-col gap-4 md:mb-0 md:flex-row">
<div className="mb-12 flex min-h-0 flex-1 flex-col gap-4 md:mb-0 md:flex-row">
{/* Left Panel */}
<div
className={cn(
topMarginClass,
"h-fit w-full shrink-0 space-y-4 overflow-y-auto md:sticky md:w-80",
)}
>
<div className="h-fit w-full shrink-0 space-y-4 overflow-y-auto md:w-80">
<div className="space-y-2">
<div className="w-fit">
<p
Expand Down
30 changes: 23 additions & 7 deletions frontend/src/components/kebab-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,24 @@ const morphTransition: Transition = {
mass: 0.8,
};

export type AnchorPoint = "top-left" | "top-center" | "top-right";

export default function KebabMenu({
children,
trigger,
open: controlledOpen,
onOpenChange,
anchorPoint = "top-right",
nested = false,
closeOnClick = true,
}: {
children: React.ReactNode;
trigger?: React.ReactNode;
open?: boolean;
onOpenChange?: (open: boolean) => void;
anchorPoint?: AnchorPoint;
nested?: boolean;
closeOnClick?: boolean;
}) {
const id = useId();
const containerRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -83,7 +89,12 @@ export default function KebabMenu({
key="trigger"
layoutId={"popover-morph-" + id}
transition={morphTransition}
className="absolute right-0 top-0 z-10 inline-block cursor-pointer rounded-full"
className={cn(
"absolute top-0 z-10 inline-block cursor-pointer rounded-full",
anchorPoint === "top-right" && "right-0",
anchorPoint === "top-left" && "left-0",
anchorPoint === "top-center" && "left-1/2 -translate-x-1/2",
)}
aria-haspopup="menu"
aria-expanded={open}
onClick={() => handleOpenChange(true)}
Expand All @@ -104,18 +115,23 @@ export default function KebabMenu({
layoutId={"popover-morph-" + id}
transition={morphTransition}
className={cn(
"absolute right-2 top-1 z-[100]",
"flex min-w-[200px] origin-top-right flex-col gap-2",
"absolute top-1 z-[100]",
anchorPoint === "top-right" && "right-2 origin-top-right",
anchorPoint === "top-left" && "left-2 origin-top-left",
anchorPoint === "top-center" &&
"left-1/2 origin-top -translate-x-1/2",
"flex min-w-[200px] flex-col gap-2",
"frosted-glass overflow-hidden rounded-3xl p-4 shadow-lg",
nested && "scale-110",
)}
onClick={(e) => {
const target = e.target as HTMLElement;
if (
target.closest("button") ||
target.closest("a") ||
target.closest('[role="button"]') ||
target.closest('[role="menuitem"]')
(target.closest("button") ||
target.closest("a") ||
target.closest('[role="button"]') ||
target.closest('[role="menuitem"]')) &&
closeOnClick
) {
handleOpenChange(false);
}
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/segmented-control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useMemo } from "react";
import { cn } from "@/lib/utils/classname";

type SegmentedControlProps<T extends string> = {
options: { label: React.ReactNode; value: T }[];
options: { label: React.ReactNode; value: T; ariaLabel?: string }[];
value: T;
onChange: (value: T) => void;
className?: string;
Expand Down Expand Up @@ -53,6 +53,7 @@ export default function SegmentedControl<T extends string>({
key={option.value}
type="button"
onClick={() => onChange(option.value)}
aria-label={option.ariaLabel}
className={cn(
"z-10 flex w-full items-center justify-center rounded-full py-2 text-sm font-medium transition-colors duration-300 focus:outline-none",
isSelected
Expand Down
24 changes: 17 additions & 7 deletions frontend/src/features/drawer/components/base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export default function BaseDrawer({
style={{ zIndex: contentZIndex }}
>
<div
className="flex w-full flex-col transition-all duration-300 ease-[cubic-bezier(0.32,0.72,0,1)]"
className="flex w-full flex-col"
style={{
height:
_type === "floating"
Expand All @@ -159,7 +159,7 @@ export default function BaseDrawer({
{/* Invisible spacer that pushes the morphing pill down */}
{_type === "morphing" && (
<div
className="pointer-events-none transition-all duration-300 ease-[cubic-bezier(0.32,0.72,0,1)]"
className="pointer-events-none"
style={{
flexGrow: isPill ? 1 : 0,
flexBasis: isPill ? "auto" : "0px",
Expand All @@ -172,7 +172,7 @@ export default function BaseDrawer({
className={cn(
"mx-auto flex w-full flex-col transition-[max-width,border-radius,margin,padding] duration-300",
isPill
? "border-foreground/10 rounded-4xl mb-4 max-h-[calc(100svh-2rem)] max-w-[calc(100%-2rem)] overflow-hidden border"
? "border-foreground/10 rounded-4xl mb-4 max-h-[calc(100svh-2rem)] max-w-[calc(100%-1rem)] overflow-hidden border"
: "border-foreground/10 max-w-full overflow-hidden rounded-t-[32px] border",
frostedGlass ? "frosted-glass" : "bg-panel",
(_type === "floating" || !isPill) &&
Expand Down Expand Up @@ -252,16 +252,26 @@ export default function BaseDrawer({

<div
className={cn(
"min-h-0 flex-1 px-7 transition-opacity duration-300 ease-[cubic-bezier(0.32,0.72,0,1)]",
"min-h-0 flex-1 px-7 transition-[padding] duration-300",
scrollableBody && "overflow-y-auto",
bodyClassName,
isPill && _type === "morphing"
? "pointer-events-none pb-0 opacity-0"
: "pb-4 opacity-100",
? "pointer-events-none pb-0 pr-3"
: "pb-4",
)}
data-vaul-no-drag
>
{(!isPill || _type !== "morphing") && children}
{/* Wrap in an extra div to manage the opacity transition separately */}
<div
className={cn(
"transition-opacity duration-300 ease-[cubic-bezier(0.32,0.72,0,1)]",
isPill && _type === "morphing"
? "opacity-0"
: "opacity-100",
)}
>
{(!isPill || _type !== "morphing") && children}
</div>
</div>

{footerContent && (
Expand Down
Loading
Loading