From 42c785aaeab696811149304d13a96505a158278d Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Fri, 1 May 2026 10:32:52 -0400 Subject: [PATCH 01/45] refactor use-results and add min participants --- .../features/event/results/lib/use-results.ts | 185 +++++++++++------- .../src/features/event/results/lib/utils.ts | 23 ++- 2 files changed, 124 insertions(+), 84 deletions(-) diff --git a/frontend/src/features/event/results/lib/use-results.ts b/frontend/src/features/event/results/lib/use-results.ts index b5dbc2f7e..763117fe2 100644 --- a/frontend/src/features/event/results/lib/use-results.ts +++ b/frontend/src/features/event/results/lib/use-results.ts @@ -16,7 +16,6 @@ import { MESSAGES } from "@/lib/messages"; export function useEventResults(initialData: ResultsInformation) { const { addToast } = useToast(); - const { eventCode, isCreator, participants, availability, currentUser } = initialData; @@ -29,17 +28,17 @@ export function useEventResults(initialData: ResultsInformation) { ); const [hoveredSlot, setHoveredSlot] = useState(null); const [showOnlyBestTimes, setShowOnlyBestTimes] = useState(false); + const [minAvailability, setMinAvailability] = useState(1); + const [timezone, setTimezone] = useState( - // Lazy initialization to avoid the lookup on every render () => Intl.DateTimeFormat().resolvedOptions().timeZone, ); /* OPTIMISTIC STATES */ const [optimisticParticipants, removeOptimisticParticipant] = useOptimistic( participants || [], - (state, personToRemove: string) => { - return state.filter((p) => p !== personToRemove); - }, + (state, personToRemove: string) => + state.filter((p) => p !== personToRemove), ); const [optimisticAvailabilities, updateOptimisticAvailabilities] = @@ -54,9 +53,7 @@ export function useEventResults(initialData: ResultsInformation) { /* ACTIONS */ const handleSetHoveredParticipant = useCallback((person: string | null) => { setHoveredParticipant(person); - if (person) { - setHoveredSlot(null); - } + if (person) setHoveredSlot(null); }, []); const toggleParticipant = (person: string) => { @@ -70,7 +67,6 @@ export function useEventResults(initialData: ResultsInformation) { const handleRemoveParticipant = async (person: string) => { const isRemovingSelf = currentUser === person; - // Immediate UI update if (selectedParticipants.includes(person)) { setSelectedParticipants((prev) => prev.filter((p) => p !== person)); } @@ -80,88 +76,124 @@ export function useEventResults(initialData: ResultsInformation) { updateOptimisticAvailabilities(person); }); - // Server Action const result = await removePerson(eventCode, person, isCreator); - if (!result.success) { addToast("error", result.error || "Error removing participant"); } else { addToast( "success", - isRemovingSelf - ? "You have been removed from the event." - : `${person} has been removed from the event.`, + isRemovingSelf ? "You have been removed." : `${person} removed.`, ); } - return result.success; }; - /* DERIVED LOGIC */ - const { filteredAvailabilities, gridNumParticipants, hasNoConsensus } = - useMemo(() => { - if (showOnlyBestTimes) { - const { allAvailableSlots } = findConsensusAndConflicts( - optimisticAvailabilities, - optimisticParticipants, - ); - - const noConsensus = allAvailableSlots.length === 0; - - const filtered: ResultsAvailabilityMap = {}; - for (const slot of allAvailableSlots) { - filtered[slot] = optimisticAvailabilities[slot]; - } - - return { - filteredAvailabilities: filtered, - gridNumParticipants: optimisticParticipants.length, - hasNoConsensus: noConsensus, - }; - } - - let activeParticipants: string[] = []; - - if (selectedParticipants.length > 0) { - activeParticipants = selectedParticipants; - } else if (hoveredParticipant) { - activeParticipants = [hoveredParticipant]; - } else { - return { - filteredAvailabilities: optimisticAvailabilities, - gridNumParticipants: optimisticParticipants.length, - }; - } + /* FILTERING LOGIC */ - const filtered: ResultsAvailabilityMap = {}; - for (const slot in optimisticAvailabilities) { - const availablePeople = optimisticAvailabilities[slot]; - const intersection = availablePeople.filter((p) => - activeParticipants.includes(p), - ); - if (intersection.length > 0) { - filtered[slot] = intersection; - } - } - - return { - filteredAvailabilities: filtered, - gridNumParticipants: activeParticipants.length, - hasNoConsensus: false, - }; - }, [ - showOnlyBestTimes, + /** + * Returns a cache of the best timeslots where all participants are available. + * Since this can be an expensive computation, it is memoized and only recalcuated + * when availabilities or participants change + */ + const bestTimesCache = useMemo(() => { + const { allAvailableSlots } = findConsensusAndConflicts( + optimisticAvailabilities, + optimisticParticipants.length, + ); + return { + slotsSet: new Set(allAvailableSlots), + hasNoConsensus: allAvailableSlots.length === 0, + }; + }, [optimisticAvailabilities, optimisticParticipants]); + + /** + * Determines the "target participants" based in order of priority: + * - selected participants (via click) + * - hovered participant (via mouseover) + * - all participants (if no selection or hover, show everyone) + * + * @returns An object containing: + * - list: the array of participants to show in the grid (based on priority) + * - set: a Set version of the above list for O(1) lookups + * - isFiltering: boolean indicating if we are currently filtering (i.e. not showing everyone) + */ + const targetParticipants = useMemo(() => { + const active = + selectedParticipants.length > 0 + ? selectedParticipants + : hoveredParticipant + ? [hoveredParticipant] + : []; + + return { + list: active.length > 0 ? active : optimisticParticipants, + set: new Set(active), + isFiltering: active.length > 0, + }; + }, [selectedParticipants, hoveredParticipant, optimisticParticipants]); + + /** + * Filters the results map to only include participants that are in the "target + * participants" list. This is to ensure that the results grid and participant list + * are always in sync. + */ + const participantFilteredMap = useMemo(() => { + if (!targetParticipants.isFiltering) return optimisticAvailabilities; + + const resultMap: ResultsAvailabilityMap = {}; + for (const [slot, availablePeople] of Object.entries( optimisticAvailabilities, - optimisticParticipants, - selectedParticipants, - hoveredParticipant, - ]); + )) { + const relevantPeople = availablePeople.filter((p) => + targetParticipants.set.has(p), + ); + if (relevantPeople.length > 0) resultMap[slot] = relevantPeople; + } + return resultMap; + }, [optimisticAvailabilities, targetParticipants]); + + /** + * This is the final filtered availabilities map that is used for rendering the + * grid. It applies the following filters before rendering: + * - slider filter (based on min availability) + * - best times filter (if showOnlyBestTimes is true) + * + * @returns The filtered availabilities map and the list of valid participants to + * show in the participant list. + */ + const { filteredAvailabilities, validParticipantsForList } = useMemo(() => { + const finalMap: ResultsAvailabilityMap = {}; + const validParticipants = new Set(); + + for (const [slot, availablePeople] of Object.entries( + participantFilteredMap, + )) { + // Apply Slider Filter + if (availablePeople.length < minAvailability) continue; + + // Apply Best Times Filter (using the O(1) cache) + if (showOnlyBestTimes && !bestTimesCache.slotsSet.has(slot)) continue; + + finalMap[slot] = availablePeople; + availablePeople.forEach((p) => validParticipants.add(p)); + } + + return { + filteredAvailabilities: finalMap, + validParticipantsForList: Array.from(validParticipants), + }; + }, [ + participantFilteredMap, + minAvailability, + showOnlyBestTimes, + bestTimesCache, + ]); useEffect(() => { - if (showOnlyBestTimes && hasNoConsensus) { + if (showOnlyBestTimes && bestTimesCache.hasNoConsensus) { addToast("info", MESSAGES.INFO_NO_MUTUAL_AVAILABILITY); } - }, [hasNoConsensus, showOnlyBestTimes, addToast]); + }, [bestTimesCache.hasNoConsensus, showOnlyBestTimes, addToast]); return { // Data @@ -169,7 +201,8 @@ export function useEventResults(initialData: ResultsInformation) { participants: optimisticParticipants, availabilities: optimisticAvailabilities, filteredAvailabilities, - gridNumParticipants, + gridNumParticipants: targetParticipants.list.length, + validParticipantsForList, // User Info currentUser, @@ -181,6 +214,7 @@ export function useEventResults(initialData: ResultsInformation) { selectedParticipants, showOnlyBestTimes, timezone, + minAvailability, // Actions clearSelectedParticipants: () => setSelectedParticipants([]), @@ -190,5 +224,6 @@ export function useEventResults(initialData: ResultsInformation) { handleRemoveParticipant, setShowOnlyBestTimes, setTimezone, + setMinAvailability, }; } diff --git a/frontend/src/features/event/results/lib/utils.ts b/frontend/src/features/event/results/lib/utils.ts index ad33d9d4b..9b66d0290 100644 --- a/frontend/src/features/event/results/lib/utils.ts +++ b/frontend/src/features/event/results/lib/utils.ts @@ -9,10 +9,12 @@ import { ResultsAvailabilityMap } from "@/core/availability/types"; */ export function hasMutualAvailability( availabilities: ResultsAvailabilityMap, - participants: string[], + participantCount: number, ): boolean { + if (participantCount === 0) return false; + for (const slot in availabilities) { - if (availabilities[slot].length === participants.length) { + if (availabilities[slot].length === participantCount) { return true; } } @@ -29,7 +31,7 @@ export function hasMutualAvailability( */ export function findConsensusAndConflicts( availabilities: ResultsAvailabilityMap, - participants: string[], + participantCount: number, ): { allAvailableSlots: string[]; noOneAvailableSlots: string[]; @@ -37,14 +39,17 @@ export function findConsensusAndConflicts( const allAvailableSlots: string[] = []; const noOneAvailableSlots: string[] = []; - for (const slot in availabilities) { - const availableParticipants = availabilities[slot]; + // early return for no participants + if (participantCount === 0) { + return { allAvailableSlots, noOneAvailableSlots }; + } - if (availableParticipants.length === participants.length) { - allAvailableSlots.push(slot); - } + for (const [slot, availableParticipants] of Object.entries(availabilities)) { + const count = availableParticipants.length; - if (availableParticipants.length === 0) { + if (count === participantCount) { + allAvailableSlots.push(slot); + } else if (count === 0) { noOneAvailableSlots.push(slot); } } From 742c5f2394b3b1a1f892a7265ecf6d3d2ca62678 Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Fri, 1 May 2026 10:37:55 -0400 Subject: [PATCH 02/45] initial slider --- .../attendee-panel/participant-list.tsx | 11 +++- .../event/results/display-settings.tsx | 66 +++++++++++++------ 2 files changed, 56 insertions(+), 21 deletions(-) diff --git a/frontend/src/features/event/results/attendee-panel/participant-list.tsx b/frontend/src/features/event/results/attendee-panel/participant-list.tsx index a013aadac..dde266bd4 100644 --- a/frontend/src/features/event/results/attendee-panel/participant-list.tsx +++ b/frontend/src/features/event/results/attendee-panel/participant-list.tsx @@ -18,6 +18,7 @@ export default function ParticipantList({ isCreator, setHoveredParticipant, toggleParticipant: onParticipantToggle, + validParticipantsForList, } = useResultsContext(); const listClassNames = mobile @@ -42,8 +43,14 @@ export default function ParticipantList({ isAvailable={ !hoveredSlot || availabilities[hoveredSlot]?.includes(person) } - isSelected={selectedParticipants.includes(person)} - areSelected={selectedParticipants.length > 0} + isSelected={ + selectedParticipants.includes(person) || + validParticipantsForList.includes(person) + } + areSelected={ + selectedParticipants.length > 0 || + validParticipantsForList.length > 0 + } isRemoving={isRemoving && isCreator} onRemove={() => promptRemove(person)} onHoverChange={(isHovering) => diff --git a/frontend/src/features/event/results/display-settings.tsx b/frontend/src/features/event/results/display-settings.tsx index d8b9bf8ed..93cbbb701 100644 --- a/frontend/src/features/event/results/display-settings.tsx +++ b/frontend/src/features/event/results/display-settings.tsx @@ -1,6 +1,6 @@ -// import Checkbox from "@/components/checkbox"; +import Checkbox from "@/components/checkbox"; import TimeZoneSelector from "@/features/event/components/selectors/timezone"; -// import { useResultsContext } from "@/features/event/results/context"; +import { useResultsContext } from "@/features/event/results/context"; export default function DisplaySettings({ timezone, @@ -15,28 +15,56 @@ export default function DisplaySettings({ setOpen?: React.Dispatch>; drawerNesting?: number; }) { - // const { showOnlyBestTimes, setShowOnlyBestTimes } = useResultsContext(); + const { + participants, + minAvailability, + setMinAvailability, + showOnlyBestTimes, + setShowOnlyBestTimes, + } = useResultsContext(); return ( - <> - {/* + */} - {/*
*/} - Displaying event in - - + {/* Slider for Minimum Availability */} +
+ + setMinAvailability(Number(e.target.value))} + className="w-full cursor-pointer accent-[var(--color-accent)]" /> - - {/*
*/} - +
+ + {/* Timezone Selector */} +
+ Displaying event in + + + +
+ ); } From 7e9e6e93d47c485a59a2e98dc9bc2fab8f24be3d Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Fri, 1 May 2026 11:01:23 -0400 Subject: [PATCH 03/45] lower opacity of chips not included in the slider --- .../event/results/attendee-panel/participant-chip.tsx | 5 +++++ .../event/results/attendee-panel/participant-list.tsx | 11 +++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/src/features/event/results/attendee-panel/participant-chip.tsx b/frontend/src/features/event/results/attendee-panel/participant-chip.tsx index 04ca6137e..547f211cd 100644 --- a/frontend/src/features/event/results/attendee-panel/participant-chip.tsx +++ b/frontend/src/features/event/results/attendee-panel/participant-chip.tsx @@ -5,6 +5,7 @@ import { cn } from "@/lib/utils/classname"; export default function ParticipantChip({ person, index, + includedInSlider, isAvailable, isRemoving, onRemove, @@ -15,6 +16,7 @@ export default function ParticipantChip({ }: { person: string; index: number; + includedInSlider: boolean; isAvailable: boolean; isRemoving: boolean; onRemove: () => void; @@ -56,6 +58,9 @@ export default function ParticipantChip({ !isAvailable && isSelected && "text-violet", + // Slider Inclusion Styling + !includedInSlider && "opacity-50", + // Selection Styling isSelected && "bg-lion ring-lion ring-offset-background ring-2 ring-offset-1", diff --git a/frontend/src/features/event/results/attendee-panel/participant-list.tsx b/frontend/src/features/event/results/attendee-panel/participant-list.tsx index dde266bd4..050085d9d 100644 --- a/frontend/src/features/event/results/attendee-panel/participant-list.tsx +++ b/frontend/src/features/event/results/attendee-panel/participant-list.tsx @@ -40,17 +40,12 @@ export default function ParticipantList({ key={person} index={index} person={person} + includedInSlider={validParticipantsForList.includes(person)} isAvailable={ !hoveredSlot || availabilities[hoveredSlot]?.includes(person) } - isSelected={ - selectedParticipants.includes(person) || - validParticipantsForList.includes(person) - } - areSelected={ - selectedParticipants.length > 0 || - validParticipantsForList.length > 0 - } + isSelected={selectedParticipants.includes(person)} + areSelected={selectedParticipants.length > 0} isRemoving={isRemoving && isCreator} onRemove={() => promptRemove(person)} onHoverChange={(isHovering) => From d6044502bb760fc29c9754fdfeacba34f4a5e99e Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Fri, 12 Jun 2026 14:50:41 -0400 Subject: [PATCH 04/45] initial slider implementation --- frontend/package-lock.json | 121 ++++++++++++++++++ frontend/package.json | 1 + .../app/(event)/[event-code]/page-client.tsx | 10 +- .../attendee-panel/participant-chip.tsx | 2 +- .../event/results/availability-filters.tsx | 48 +++++++ .../event/results/display-settings.tsx | 73 ++++------- .../src/features/event/results/drawer.tsx | 62 ++++++--- .../features/event/results/lib/use-results.ts | 111 ++++++++-------- 8 files changed, 294 insertions(+), 134 deletions(-) create mode 100644 frontend/src/features/event/results/availability-filters.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8150b1c82..65d053ea8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "@radix-ui/react-popover": "^1.1.13", "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-select": "^2.2.4", + "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-toast": "^1.2.15", "@vercel/analytics": "^2.0.1", @@ -2724,6 +2725,112 @@ } } }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", @@ -3505,6 +3612,7 @@ "version": "19.1.2", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz", "integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -3514,6 +3622,7 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.2.tgz", "integrity": "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==", "devOptional": true, + "peer": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -3560,6 +3669,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.31.1.tgz", "integrity": "sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.31.1", "@typescript-eslint/types": "8.31.1", @@ -4032,6 +4142,7 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4572,6 +4683,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -4895,6 +5007,7 @@ "version": "9.36.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz", "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5081,6 +5194,7 @@ "version": "2.32.0", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6809,6 +6923,7 @@ "resolved": "https://registry.npmjs.org/next/-/next-15.4.10.tgz", "integrity": "sha512-itVlc79QjpKMFMRhP+kbGKaSG/gZM6RCvwhEbwmCNF06CdDiNaoHcbeg0PqkEa2GOcn8KJ0nnc7+yL7EjoYLHQ==", "license": "MIT", + "peer": true, "dependencies": { "@next/env": "15.4.10", "@swc/helpers": "0.5.15", @@ -7260,6 +7375,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -7398,6 +7514,7 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -7432,6 +7549,7 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -7493,6 +7611,7 @@ "version": "5.10.2", "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.10.2.tgz", "integrity": "sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.0", "@emotion/cache": "^11.4.0", @@ -8225,6 +8344,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, @@ -8378,6 +8498,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/frontend/package.json b/frontend/package.json index 0ade39b45..83cb4c1de 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,6 +19,7 @@ "@radix-ui/react-popover": "^1.1.13", "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-select": "^2.2.4", + "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-toast": "^1.2.15", "@vercel/analytics": "^2.0.1", diff --git a/frontend/src/app/(event)/[event-code]/page-client.tsx b/frontend/src/app/(event)/[event-code]/page-client.tsx index 828c8a87c..846502155 100644 --- a/frontend/src/app/(event)/[event-code]/page-client.tsx +++ b/frontend/src/app/(event)/[event-code]/page-client.tsx @@ -213,12 +213,10 @@ function EventResults({ eventData }: { eventData: EventInformation }) { {banners}
-
- -
+
diff --git a/frontend/src/features/event/results/attendee-panel/participant-chip.tsx b/frontend/src/features/event/results/attendee-panel/participant-chip.tsx index 547f211cd..7a38d2766 100644 --- a/frontend/src/features/event/results/attendee-panel/participant-chip.tsx +++ b/frontend/src/features/event/results/attendee-panel/participant-chip.tsx @@ -59,7 +59,7 @@ export default function ParticipantChip({ !isAvailable && isSelected && "text-violet", // Slider Inclusion Styling - !includedInSlider && "opacity-50", + !includedInSlider && "bg-gray-200/25 opacity-50", // Selection Styling isSelected && diff --git a/frontend/src/features/event/results/availability-filters.tsx b/frontend/src/features/event/results/availability-filters.tsx new file mode 100644 index 000000000..8eb7018a6 --- /dev/null +++ b/frontend/src/features/event/results/availability-filters.tsx @@ -0,0 +1,48 @@ +import * as Slider from "@radix-ui/react-slider"; + +import Checkbox from "@/components/checkbox"; +import { useResultsContext } from "@/features/event/results/context"; + +export default function AvailabilityFilters() { + const { + participants, + minAvailability, + setMinAvailability, + showOnlyBestTimes, + setShowOnlyBestTimes, + } = useResultsContext(); + + return ( +
+ + +
+ + setMinAvailability(value[0])} + > + + + + + +
+
+ ); +} diff --git a/frontend/src/features/event/results/display-settings.tsx b/frontend/src/features/event/results/display-settings.tsx index 93cbbb701..0d5c5f03f 100644 --- a/frontend/src/features/event/results/display-settings.tsx +++ b/frontend/src/features/event/results/display-settings.tsx @@ -1,6 +1,5 @@ -import Checkbox from "@/components/checkbox"; import TimeZoneSelector from "@/features/event/components/selectors/timezone"; -import { useResultsContext } from "@/features/event/results/context"; +import AvailabilityFilters from "@/features/event/results/availability-filters"; export default function DisplaySettings({ timezone, @@ -15,56 +14,30 @@ export default function DisplaySettings({ setOpen?: React.Dispatch>; drawerNesting?: number; }) { - const { - participants, - minAvailability, - setMinAvailability, - showOnlyBestTimes, - setShowOnlyBestTimes, - } = useResultsContext(); - return ( -
- - {/* Slider for Minimum Availability */} -
- - setMinAvailability(Number(e.target.value))} - className="w-full cursor-pointer accent-[var(--color-accent)]" - /> + <> +
+
+
+ + Displaying event in + + + + +
+
- - {/* Timezone Selector */} -
- Displaying event in - - - +
+
-
+ ); } diff --git a/frontend/src/features/event/results/drawer.tsx b/frontend/src/features/event/results/drawer.tsx index a9810a01e..7de07ae78 100644 --- a/frontend/src/features/event/results/drawer.tsx +++ b/frontend/src/features/event/results/drawer.tsx @@ -1,13 +1,14 @@ import { useEffect, useState } from "react"; -import { GlobeIcon, SquarePenIcon } from "lucide-react"; +import { GlobeIcon, SlidersHorizontalIcon, SquarePenIcon } from "lucide-react"; import EmptyButton from "@/features/button/components/empty"; import LinkButton from "@/features/button/components/link"; -import { MorphingDrawer } from "@/features/drawer"; +import { FloatingDrawer, MorphingDrawer } from "@/features/drawer"; import TimeZoneSelector from "@/features/event/components/selectors/timezone"; import PanelHeader from "@/features/event/results/attendee-panel/panel-header"; import ParticipantList from "@/features/event/results/attendee-panel/participant-list"; +import AvailabilityFilters from "@/features/event/results/availability-filters"; import { useResultsContext } from "@/features/event/results/context"; import ConfirmationDialog from "@/features/system-feedback/confirmation/base"; import { tzEqual } from "@/lib/utils/date-time-format"; @@ -42,6 +43,7 @@ export default function ResultsDrawer({ /* TABS */ const [activeSnap, setActiveSnap] = useState(0.22); + const [openViewOptions, setOpenViewOptions] = useState(false); useEffect(() => { onSnapChange(activeSnap); @@ -68,6 +70,43 @@ export default function ResultsDrawer({ ); /* BUTTONS */ + const timezoneButton = ( + } + aria-label="Change Timezone" + /> + } + /> + ); + + const filtersButton = ( + } + aria-label="View Options" + /> + } + > + + + ); + const paintingButton = ( - } - aria-label="Change Timezone" - /> - } - /> +
+ {timezoneButton} + {filtersButton} +
{paintingButton}
} diff --git a/frontend/src/features/event/results/lib/use-results.ts b/frontend/src/features/event/results/lib/use-results.ts index 763117fe2..ce54f0e2c 100644 --- a/frontend/src/features/event/results/lib/use-results.ts +++ b/frontend/src/features/event/results/lib/use-results.ts @@ -106,51 +106,30 @@ export function useEventResults(initialData: ResultsInformation) { }; }, [optimisticAvailabilities, optimisticParticipants]); - /** - * Determines the "target participants" based in order of priority: - * - selected participants (via click) - * - hovered participant (via mouseover) - * - all participants (if no selection or hover, show everyone) - * - * @returns An object containing: - * - list: the array of participants to show in the grid (based on priority) - * - set: a Set version of the above list for O(1) lookups - * - isFiltering: boolean indicating if we are currently filtering (i.e. not showing everyone) - */ - const targetParticipants = useMemo(() => { - const active = - selectedParticipants.length > 0 - ? selectedParticipants - : hoveredParticipant - ? [hoveredParticipant] - : []; - - return { - list: active.length > 0 ? active : optimisticParticipants, - set: new Set(active), - isFiltering: active.length > 0, - }; - }, [selectedParticipants, hoveredParticipant, optimisticParticipants]); - - /** - * Filters the results map to only include participants that are in the "target - * participants" list. This is to ensure that the results grid and participant list - * are always in sync. - */ - const participantFilteredMap = useMemo(() => { - if (!targetParticipants.isFiltering) return optimisticAvailabilities; + const { globalFilteredMap, validParticipantsForList } = useMemo(() => { + const map: ResultsAvailabilityMap = {}; + const validSet = new Set(); - const resultMap: ResultsAvailabilityMap = {}; for (const [slot, availablePeople] of Object.entries( optimisticAvailabilities, )) { - const relevantPeople = availablePeople.filter((p) => - targetParticipants.set.has(p), - ); - if (relevantPeople.length > 0) resultMap[slot] = relevantPeople; + if (availablePeople.length < minAvailability) continue; + if (showOnlyBestTimes && !bestTimesCache.slotsSet.has(slot)) continue; + + map[slot] = availablePeople; + availablePeople.forEach((p) => validSet.add(p)); } - return resultMap; - }, [optimisticAvailabilities, targetParticipants]); + + return { + globalFilteredMap: map, + validParticipantsForList: Array.from(validSet), + }; + }, [ + optimisticAvailabilities, + minAvailability, + showOnlyBestTimes, + bestTimesCache, + ]); /** * This is the final filtered availabilities map that is used for rendering the @@ -161,32 +140,44 @@ export function useEventResults(initialData: ResultsInformation) { * @returns The filtered availabilities map and the list of valid participants to * show in the participant list. */ - const { filteredAvailabilities, validParticipantsForList } = useMemo(() => { - const finalMap: ResultsAvailabilityMap = {}; - const validParticipants = new Set(); - - for (const [slot, availablePeople] of Object.entries( - participantFilteredMap, - )) { - // Apply Slider Filter - if (availablePeople.length < minAvailability) continue; - - // Apply Best Times Filter (using the O(1) cache) - if (showOnlyBestTimes && !bestTimesCache.slotsSet.has(slot)) continue; + const { filteredAvailabilities, gridNumParticipants } = useMemo(() => { + const hasSelections = selectedParticipants.length > 0; + const isHovering = !hasSelections && hoveredParticipant !== null; + + const active = hasSelections + ? selectedParticipants + : isHovering + ? [hoveredParticipant] + : []; + + const targetSet = new Set(active); + const isFiltering = active.length > 0; + + const sourceMap = isHovering ? optimisticAvailabilities : globalFilteredMap; + + if (!isFiltering) { + return { + filteredAvailabilities: sourceMap, + gridNumParticipants: optimisticParticipants.length, + }; + } - finalMap[slot] = availablePeople; - availablePeople.forEach((p) => validParticipants.add(p)); + const finalMap: ResultsAvailabilityMap = {}; + for (const [slot, availablePeople] of Object.entries(sourceMap)) { + const relevantPeople = availablePeople.filter((p) => targetSet.has(p)); + if (relevantPeople.length > 0) finalMap[slot] = relevantPeople; } return { filteredAvailabilities: finalMap, - validParticipantsForList: Array.from(validParticipants), + gridNumParticipants: active.length, }; }, [ - participantFilteredMap, - minAvailability, - showOnlyBestTimes, - bestTimesCache, + selectedParticipants, + hoveredParticipant, + optimisticAvailabilities, + globalFilteredMap, + optimisticParticipants.length, ]); useEffect(() => { @@ -201,7 +192,7 @@ export function useEventResults(initialData: ResultsInformation) { participants: optimisticParticipants, availabilities: optimisticAvailabilities, filteredAvailabilities, - gridNumParticipants: targetParticipants.list.length, + gridNumParticipants, validParticipantsForList, // User Info From 371544e9c0d29bf61b36ffd33e70abd4f4a5195a Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Sat, 13 Jun 2026 11:49:09 -0400 Subject: [PATCH 05/45] add filters button to drawer --- .../event/results/attendees/mobile-drawer.tsx | 58 +++++++++++++------ 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/frontend/src/features/event/results/attendees/mobile-drawer.tsx b/frontend/src/features/event/results/attendees/mobile-drawer.tsx index e294bf98f..5b856121b 100644 --- a/frontend/src/features/event/results/attendees/mobile-drawer.tsx +++ b/frontend/src/features/event/results/attendees/mobile-drawer.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; -import { ShareIcon, SquarePenIcon } from "lucide-react"; +import { ShareIcon, SlidersHorizontalIcon, SquarePenIcon } from "lucide-react"; import EmptyButton from "@/features/button/components/empty"; import LinkButton from "@/features/button/components/link"; @@ -11,6 +11,7 @@ import { RemoveParticipantDialog, useParticipantRemoval, } from "@/features/event/results/attendees/remove-participant"; +import AvailabilityFilters from "@/features/event/results/availability-filters"; import ShareMenu from "@/features/event/results/share-menu"; export default function AttendeesDrawer({ @@ -37,6 +38,7 @@ export default function AttendeesDrawer({ /* TABS */ const [activeSnap, setActiveSnap] = useState(0.22); + const [openViewOptions, setOpenViewOptions] = useState(false); useEffect(() => { onSnapChange(activeSnap); @@ -61,6 +63,25 @@ export default function AttendeesDrawer({ /> ); + const filtersButton = ( + } + aria-label="View Options" + /> + } + > + + + ); + /* SHARE MENU */ const [shareMenuOpen, setShareMenuOpen] = useState(false); @@ -88,22 +109,25 @@ export default function AttendeesDrawer({ } footerContent={
- } - aria-label="Share Event" - /> - } - nested={true} - > - - +
+ } + aria-label="Share Event" + /> + } + nested={true} + > + + + {filtersButton} +
{paintingButton}
} From c80b7c89671a2426775f7dc76c841b443bd919fa Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Sat, 13 Jun 2026 12:06:57 -0400 Subject: [PATCH 06/45] make slider the master filter --- frontend/src/features/event/results/lib/use-results.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/features/event/results/lib/use-results.ts b/frontend/src/features/event/results/lib/use-results.ts index ce54f0e2c..da041c41e 100644 --- a/frontend/src/features/event/results/lib/use-results.ts +++ b/frontend/src/features/event/results/lib/use-results.ts @@ -153,7 +153,7 @@ export function useEventResults(initialData: ResultsInformation) { const targetSet = new Set(active); const isFiltering = active.length > 0; - const sourceMap = isHovering ? optimisticAvailabilities : globalFilteredMap; + const sourceMap = globalFilteredMap; if (!isFiltering) { return { From 3d0bac819da2cfc5b800a3a1e84daec161396fc9 Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Sat, 13 Jun 2026 12:07:10 -0400 Subject: [PATCH 07/45] disable chips not included in slider --- .../event/results/attendees/participant-chip.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/src/features/event/results/attendees/participant-chip.tsx b/frontend/src/features/event/results/attendees/participant-chip.tsx index 7a38d2766..369f3aae6 100644 --- a/frontend/src/features/event/results/attendees/participant-chip.tsx +++ b/frontend/src/features/event/results/attendees/participant-chip.tsx @@ -32,7 +32,9 @@ export default function ParticipantChip({
  • { - if (window.matchMedia("(hover: hover)").matches) { + if (window.matchMedia("(hover: hover)").matches && includedInSlider) { + // Only trigger hover effects on devices that support hover and if the + // participant is included in the slider onHoverChange(true); } }} @@ -40,7 +42,10 @@ export default function ParticipantChip({ onClick={() => { if (isRemoving) { onRemove(); - } else { + } else if (includedInSlider || isSelected) { + // Only allow clicking if included in the slider OR already selected + // (The already selected check allows them to un-click themselves if state + // got weird) onHoverChange(false); onClick(); } @@ -59,7 +64,8 @@ export default function ParticipantChip({ !isAvailable && isSelected && "text-violet", // Slider Inclusion Styling - !includedInSlider && "bg-gray-200/25 opacity-50", + !includedInSlider && + "pointer-events-none bg-gray-200/25 opacity-50 grayscale", // Selection Styling isSelected && From a7b8ee1cc9db5e71abe1d1b074409040937369ed Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Sat, 13 Jun 2026 12:10:10 -0400 Subject: [PATCH 08/45] clamp min availability --- frontend/src/features/event/results/lib/use-results.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/features/event/results/lib/use-results.ts b/frontend/src/features/event/results/lib/use-results.ts index da041c41e..33b35a828 100644 --- a/frontend/src/features/event/results/lib/use-results.ts +++ b/frontend/src/features/event/results/lib/use-results.ts @@ -175,17 +175,22 @@ export function useEventResults(initialData: ResultsInformation) { }, [ selectedParticipants, hoveredParticipant, - optimisticAvailabilities, globalFilteredMap, optimisticParticipants.length, ]); + // show toast if user toggles on best times but there are no mutual availability useEffect(() => { if (showOnlyBestTimes && bestTimesCache.hasNoConsensus) { addToast("info", MESSAGES.INFO_NO_MUTUAL_AVAILABILITY); } }, [bestTimesCache.hasNoConsensus, showOnlyBestTimes, addToast]); + // clamp min availability to never to higher than the total participants + useEffect(() => { + setMinAvailability((prev) => Math.min(prev, optimisticParticipants.length)); + }, [optimisticParticipants.length]); + return { // Data eventType: initialData.eventType, From e39999b73daeaa855e4493f3aa64592a21d8fee8 Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Sat, 13 Jun 2026 12:43:00 -0400 Subject: [PATCH 09/45] reword filters --- .../src/features/event/results/availability-filters.tsx | 6 ++---- frontend/src/lib/messages.ts | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/frontend/src/features/event/results/availability-filters.tsx b/frontend/src/features/event/results/availability-filters.tsx index 8eb7018a6..bb47b23c2 100644 --- a/frontend/src/features/event/results/availability-filters.tsx +++ b/frontend/src/features/event/results/availability-filters.tsx @@ -15,16 +15,14 @@ export default function AvailabilityFilters() { return (
    Date: Sat, 13 Jun 2026 13:57:24 -0400 Subject: [PATCH 10/45] style filter --- .../event/results/availability-filters.tsx | 105 ++++++++++++++++-- 1 file changed, 96 insertions(+), 9 deletions(-) diff --git a/frontend/src/features/event/results/availability-filters.tsx b/frontend/src/features/event/results/availability-filters.tsx index bb47b23c2..7d0761b35 100644 --- a/frontend/src/features/event/results/availability-filters.tsx +++ b/frontend/src/features/event/results/availability-filters.tsx @@ -2,7 +2,40 @@ import * as Slider from "@radix-ui/react-slider"; import Checkbox from "@/components/checkbox"; import { useResultsContext } from "@/features/event/results/context"; +import { cn } from "@/lib/utils/classname"; +interface NumberedPinProps { + value: number; + size?: number; +} + +function NumberedPin({ value, size = 30 }: NumberedPinProps) { + return ( +
    + + {value} + +
    + ); +} + +// 2. Updated AvailabilityFilters export default function AvailabilityFilters() { const { participants, @@ -12,8 +45,17 @@ export default function AvailabilityFilters() { setShowOnlyBestTimes, } = useResultsContext(); + const max = participants.length; + const ticks = []; + for (let i = 0; i <= max; i += 5) { + ticks.push(i); + } + if (max > 0 && max % 5 !== 0) { + ticks.push(max); + } + return ( -
    +
    -
    From 2cede5794f5a4c89699208b7cf5293d6a3d8d646 Mon Sep 17 00:00:00 2001 From: Miranda Zheng <123515762+mirmirmirr@users.noreply.github.com> Date: Sat, 13 Jun 2026 13:59:23 -0400 Subject: [PATCH 11/45] move sliders and share menu into components --- frontend/src/app/(event)/[event-code]/page-client.tsx | 2 +- .../src/features/event/results/attendees/mobile-drawer.tsx | 4 ++-- .../event/results/{ => components}/availability-filters.tsx | 4 ++-- .../features/event/results/components/display-settings.tsx | 2 +- .../features/event/results/{ => components}/share-menu.tsx | 0 5 files changed, 6 insertions(+), 6 deletions(-) rename frontend/src/features/event/results/{ => components}/availability-filters.tsx (98%) rename frontend/src/features/event/results/{ => components}/share-menu.tsx (100%) diff --git a/frontend/src/app/(event)/[event-code]/page-client.tsx b/frontend/src/app/(event)/[event-code]/page-client.tsx index 7dae5cec7..e49dadad3 100644 --- a/frontend/src/app/(event)/[event-code]/page-client.tsx +++ b/frontend/src/app/(event)/[event-code]/page-client.tsx @@ -18,7 +18,7 @@ import { useResultsContext, } from "@/features/event/results/context"; import { ResultsInformation } from "@/features/event/results/lib/types"; -import ShareMenu from "@/features/event/results/share-menu"; +import ShareMenu from "@/features/event/results/components/share-menu"; import HeaderSpacer from "@/features/header/components/header-spacer"; import BaseDialog from "@/features/system-feedback/dialog/components/base"; import { cn } from "@/lib/utils/classname"; diff --git a/frontend/src/features/event/results/attendees/mobile-drawer.tsx b/frontend/src/features/event/results/attendees/mobile-drawer.tsx index 5b856121b..97168d6be 100644 --- a/frontend/src/features/event/results/attendees/mobile-drawer.tsx +++ b/frontend/src/features/event/results/attendees/mobile-drawer.tsx @@ -11,8 +11,8 @@ import { RemoveParticipantDialog, useParticipantRemoval, } from "@/features/event/results/attendees/remove-participant"; -import AvailabilityFilters from "@/features/event/results/availability-filters"; -import ShareMenu from "@/features/event/results/share-menu"; +import AvailabilityFilters from "@/features/event/results/components/availability-filters"; +import ShareMenu from "@/features/event/results/components/share-menu"; export default function AttendeesDrawer({ onSnapChange, diff --git a/frontend/src/features/event/results/availability-filters.tsx b/frontend/src/features/event/results/components/availability-filters.tsx similarity index 98% rename from frontend/src/features/event/results/availability-filters.tsx rename to frontend/src/features/event/results/components/availability-filters.tsx index 7d0761b35..3489093fa 100644 --- a/frontend/src/features/event/results/availability-filters.tsx +++ b/frontend/src/features/event/results/components/availability-filters.tsx @@ -55,7 +55,7 @@ export default function AvailabilityFilters() { } return ( -
    +
    -