diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index f53f78c9..82466215 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -8,19 +8,19 @@ jobs: runs-on: ubuntu-latest permissions: - contents: write # Required to push to the repo + contents: write # Required to push to the repo steps: - name: Checkout code uses: actions/checkout@v4 with: - ref: ${{ github.head_ref }} # Checkout PR branch + ref: ${{ github.head_ref }} # Checkout PR branch - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' - cache: 'npm' + node-version: "20" + cache: "npm" - name: Install dependencies run: npm ci diff --git a/.gitignore b/.gitignore index 5ef6a520..3eb32276 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +certificates diff --git a/README.md b/README.md index 2b001613..14a243b5 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,12 @@ To clone and run this application, [Node.js](https://nodejs.org/en/download/) (w npm install npm@latest -g ``` +### `.env` Setup + +Create a file called `.env` in the root directory, copying the contents of `example.env`. + +Replace all values in the file with the relevant information. + ### Installation ```bash diff --git a/app/_lib/availability/availability-reducer.tsx b/app/_lib/availability/availability-reducer.tsx new file mode 100644 index 00000000..59ea387b --- /dev/null +++ b/app/_lib/availability/availability-reducer.tsx @@ -0,0 +1,58 @@ +import { AvailabilitySet } from "./types"; +import { createEmptyUserAvailability, toggleUtcSlot } from "./utils"; + +export interface AvailabilityState { + displayName: string; + timeZone: string; + userAvailability: AvailabilitySet; +} + +export type AvailabilityAction = + | { type: "RESET_AVAILABILITY" } + | { type: "SET_DISPLAY_NAME"; payload: string } + | { type: "SET_TIME_ZONE"; payload: string } + | { + type: "TOGGLE_SLOT"; + payload: { slot: string }; + }; + +// 3. Create the reducer function +export function availabilityReducer( + state: AvailabilityState, + action: AvailabilityAction, +): AvailabilityState { + switch (action.type) { + case "RESET_AVAILABILITY": { + return { + ...state, + userAvailability: createEmptyUserAvailability(), + }; + } + + case "SET_DISPLAY_NAME": { + return { + ...state, + displayName: action.payload, + }; + } + + case "SET_TIME_ZONE": { + return { + ...state, + timeZone: action.payload, + }; + } + + case "TOGGLE_SLOT": { + const { slot } = action.payload; + return { + ...state, + userAvailability: toggleUtcSlot(state.userAvailability, slot), + }; + } + + default: { + return state; + } + } +} diff --git a/app/_lib/availability/types.tsx b/app/_lib/availability/types.tsx new file mode 100644 index 00000000..ed7ecdab --- /dev/null +++ b/app/_lib/availability/types.tsx @@ -0,0 +1,2 @@ +// a set containing ISO strings of selected UTC date-time slots +export type AvailabilitySet = Set; diff --git a/app/_lib/availability/use-availability.tsx b/app/_lib/availability/use-availability.tsx new file mode 100644 index 00000000..632b3513 --- /dev/null +++ b/app/_lib/availability/use-availability.tsx @@ -0,0 +1,41 @@ +import { useReducer, useCallback } from "react"; +import { availabilityReducer, AvailabilityState } from "./availability-reducer"; +import { createEmptyUserAvailability } from "./utils"; + +export function useAvailability( + initialDisplayName = "", + initialTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone, +) { + const initialState: AvailabilityState = { + displayName: initialDisplayName, + timeZone: initialTimeZone, + userAvailability: createEmptyUserAvailability(), + }; + + const [state, dispatch] = useReducer(availabilityReducer, initialState); + + // DISPATCHERS + const setDisplayName = useCallback((name: string) => { + dispatch({ type: "SET_DISPLAY_NAME", payload: name }); + }, []); + + const setTimeZone = useCallback((tz: string) => { + dispatch({ type: "SET_TIME_ZONE", payload: tz }); + }, []); + + const toggleSlot = useCallback((slot: string) => { + dispatch({ type: "TOGGLE_SLOT", payload: { slot } }); + }, []); + + const resetAvailability = useCallback(() => { + dispatch({ type: "RESET_AVAILABILITY" }); + }, []); + + return { + state, + setDisplayName, + setTimeZone, + toggleSlot, + resetAvailability, + }; +} diff --git a/app/_lib/availability/utils.tsx b/app/_lib/availability/utils.tsx new file mode 100644 index 00000000..8c87571e --- /dev/null +++ b/app/_lib/availability/utils.tsx @@ -0,0 +1,55 @@ +// app/_lib/availability.ts + +import { fromZonedTime } from "date-fns-tz"; +import { AvailabilitySet } from "@/app/_lib/availability/types"; + +// Creates an empty UserAvailability object +export const createEmptyUserAvailability = (): AvailabilitySet => { + return new Set(); +}; + +// Toggles a single time slot in the user's availability +export function toggleUtcSlot( + prev: AvailabilitySet, + timeSlot: string, // ISO string +): AvailabilitySet { + const updated = new Set(prev); + if (updated.has(timeSlot)) { + updated.delete(timeSlot); + } else { + updated.add(timeSlot); + } + return updated; +} + +// Checks if a specific time slot is in the user's availability +export function isSlotSelected( + availability: AvailabilitySet, + timeSlot: Date, +): boolean { + return availability.has(timeSlot.toISOString()); +} + +// Converts a local time representation to a UTC ISO string +export function getUtcIsoString( + dateKey: string, // "YYYY-MM-DD" + hour: number, + minute: number, + timezone: string, +): string { + const localDateTimeString = `${dateKey}T${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}:00`; + const utcDate = fromZonedTime(localDateTimeString, timezone); + return utcDate.toISOString(); +} + +// Gets a UTC ISO slot for a specific date and time +export function getUtcIsoSlot( + dateKey: string, + hour: number, + minute: number, + timezone: string, +): { utcDate: Date; isoString: string } { + const localDateTimeString = `${dateKey}T${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}:00`; + const utcDate = fromZonedTime(localDateTimeString, timezone); + return { utcDate, isoString: utcDate.toISOString() }; +} diff --git a/app/_lib/providers.tsx b/app/_lib/providers.tsx new file mode 100644 index 00000000..4dc3276c --- /dev/null +++ b/app/_lib/providers.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { ThemeProvider } from "next-themes"; +import { createContext, useState } from "react"; + +export const LoginContext = createContext<{ + loggedIn: boolean; + setLoggedIn: (loggedIn: boolean) => void; +}>({ + loggedIn: false, + setLoggedIn: () => {}, +}); + +export function Providers({ children }: { children: React.ReactNode }) { + const [loggedIn, setLoggedIn] = useState(false); + + return ( + + + {children} + + + ); +} diff --git a/app/_lib/schedule/event-info-reducer.tsx b/app/_lib/schedule/event-info-reducer.tsx new file mode 100644 index 00000000..4b35b2a6 --- /dev/null +++ b/app/_lib/schedule/event-info-reducer.tsx @@ -0,0 +1,52 @@ +import { EventInformation } from "./types"; +import { EventRangeReducer, EventRangeAction } from "./event-range-reducer"; + +export type EventInfoAction = + | { type: "SET_TITLE"; payload: string } + | { type: "SET_CUSTOM_CODE"; payload: string } + | EventRangeAction + | { type: "RESET" }; + +export function EventInfoReducer( + state: EventInformation, + action: EventInfoAction, +): EventInformation { + switch (action.type) { + case "SET_TITLE": + return { + ...state, + title: action.payload, + }; + case "SET_CUSTOM_CODE": + return { + ...state, + customCode: action.payload, + }; + case "RESET": + return { + title: "", + customCode: "", + eventRange: { + type: "specific", + duration: 60, + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + dateRange: { + from: new Date().toISOString(), + to: new Date().toISOString(), + }, + timeRange: { + from: 9, + to: 17, + }, + }, + }; + default: + return { + ...state, + eventRange: EventRangeReducer( + state.eventRange, + action as EventRangeAction, + ), + }; + } +} diff --git a/app/_lib/schedule/event-range-reducer.tsx b/app/_lib/schedule/event-range-reducer.tsx new file mode 100644 index 00000000..20badc54 --- /dev/null +++ b/app/_lib/schedule/event-range-reducer.tsx @@ -0,0 +1,127 @@ +import { EventRange, WeekdayMap } from "@/app/_lib/schedule/types"; + +export type EventRangeAction = + | { type: "SET_RANGE_TYPE"; payload: "specific" | "weekday" } + | { type: "SET_DATE_RANGE"; payload: { from: string; to: string } } + | { type: "SET_TIME_RANGE"; payload: { from: number; to: number } } + | { + type: "SET_WEEKDAYS"; + payload: { weekdays: Partial> }; + } + | { type: "SET_DURATION"; payload: number } + | { type: "SET_TIMEZONE"; payload: string } + | { type: "RESET" }; + +export function EventRangeReducer( + state: EventRange, + action: EventRangeAction, +): EventRange { + switch (action.type) { + case "SET_RANGE_TYPE": { + if (action.payload === state.type) { + return state; + } + + const baseEvent = { + duration: state.duration, + timezone: state.timezone, + timeRange: state.timeRange, + }; + + if (action.payload === "specific") { + return { + ...baseEvent, + type: "specific", + dateRange: { + from: new Date().toISOString(), + to: new Date().toISOString(), + }, + }; + } else { + return { + ...baseEvent, + type: "weekday", + weekdays: { Sun: 0, Mon: 0, Tue: 0, Wed: 0, Thu: 0, Fri: 0, Sat: 0 }, + }; + } + } + + case "SET_DATE_RANGE": { + if (state.type !== "specific") { + return state; + } + + return { + ...state, + dateRange: { + from: action.payload.from, + to: action.payload.to, + }, + }; + } + + case "SET_TIME_RANGE": { + return { + ...state, + timeRange: { + from: action.payload.from, + to: action.payload.to, + }, + }; + } + + case "SET_WEEKDAYS": { + if (state.type !== "weekday") { + return state; + } + + return { + ...state, + weekdays: { + ...state.weekdays, + ...action.payload.weekdays, + }, + }; + } + + case "SET_DURATION": { + return { + ...state, + duration: action.payload, + }; + } + + case "SET_TIMEZONE": { + return { + ...state, + timezone: action.payload, + }; + } + + case "RESET": { + if (state.type === "specific") { + return { + type: "specific", + duration: 30, + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + dateRange: { + from: new Date().toISOString(), + to: new Date().toISOString(), + }, + timeRange: { from: 9, to: 17 }, + }; + } else { + return { + type: "weekday", + duration: 30, + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + weekdays: { Sun: 0, Mon: 0, Tue: 0, Wed: 0, Thu: 0, Fri: 0, Sat: 0 }, + timeRange: { from: 9, to: 17 }, + }; + } + } + + default: + return state; + } +} diff --git a/app/_lib/schedule/types.tsx b/app/_lib/schedule/types.tsx new file mode 100644 index 00000000..1d2a6212 --- /dev/null +++ b/app/_lib/schedule/types.tsx @@ -0,0 +1,52 @@ +// app/_types/schedule.ts + +export type EventInformation = { + title: string; + customCode: string; + eventRange: EventRange; +}; + +// discriminated union for event ranges - this is your single source of truth +export type EventRange = SpecificDateRange | WeekdayRange; + +// represents selected weekdays +export type WeekdayMap = { + [day in "Sun" | "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat"]: 0 | 1; +}; + +/* EVENT RANGE MODELS */ + +export type SpecificDateRange = { + type: "specific"; + duration: number; + timezone: string; + dateRange: { + from: string; + to: string; + }; + timeRange: { + from: number; // hour in 24h format, e.g., 9 for 9:00 AM + to: number; // hour in 24h format, e.g., 17 for 5:00 PM + }; +}; + +export type WeekdayRange = { + type: "weekday"; + duration: number; + timezone: string; + weekdays: WeekdayMap; + timeRange: { + from: number; // hour in 24h format, e.g., 9 for 9:00 AM + to: number; // hour in 24h format, e.g., 17 for 5:00 PM + }; +}; + +/* SLOTS TYPES FOR UI */ +// these types are generated from the core models above for rendering + +export type DaySlot = { + date: Date; // date for the entire day + dayLabel: string; // e.g., "MON MAY 10" + dayKey: string; // e.g., "2025-05-10" + timeslots: Date[]; +}; diff --git a/app/_lib/schedule/use-event-info.tsx b/app/_lib/schedule/use-event-info.tsx new file mode 100644 index 00000000..3429671e --- /dev/null +++ b/app/_lib/schedule/use-event-info.tsx @@ -0,0 +1,86 @@ +import { useReducer, useCallback } from "react"; +import { EventInfoReducer } from "./event-info-reducer"; +import { EventInformation, WeekdayMap } from "./types"; +import { DateRange } from "react-day-picker"; + +export function useEventInfo() { + const initalState: EventInformation = { + title: "", + customCode: "", + eventRange: { + type: "specific", + duration: 60, + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + dateRange: { + from: new Date().toISOString(), + to: new Date().toISOString(), + }, + timeRange: { + from: 9, + to: 17, + }, + }, + }; + + const [state, dispatch] = useReducer(EventInfoReducer, initalState); + + // DISPATCHERS + const setTitle = useCallback((title: string) => { + dispatch({ type: "SET_TITLE", payload: title }); + }, []); + + const setCustomCode = useCallback((code: string) => { + dispatch({ type: "SET_CUSTOM_CODE", payload: code }); + }, []); + + const setEventType = useCallback((type: "specific" | "weekday") => { + dispatch({ type: "SET_RANGE_TYPE", payload: type }); + }, []); + + const setTimezone = useCallback((tz: string) => { + dispatch({ type: "SET_TIMEZONE", payload: tz }); + }, []); + + const setDuration = useCallback((duration: number) => { + dispatch({ type: "SET_DURATION", payload: duration }); + }, []); + + const setTimeRange = useCallback( + (timeRange: { from: number; to: number }) => { + dispatch({ type: "SET_TIME_RANGE", payload: timeRange }); + }, + [], + ); + + const setDateRange = useCallback((dateRange: DateRange | undefined) => { + if (dateRange?.from && dateRange?.to) { + const from = dateRange.from.toISOString(); + const to = dateRange.to.toISOString(); + dispatch({ + type: "SET_DATE_RANGE", + payload: { from, to }, + }); + } + }, []); + + const setWeekdayRange = useCallback((weekdays: WeekdayMap) => { + dispatch({ type: "SET_WEEKDAYS", payload: { weekdays } }); + }, []); + + const resetEventInfo = useCallback(() => { + dispatch({ type: "RESET" }); + }, []); + + return { + state, + setTitle, + setEventType, + setCustomCode, + setTimezone, + setDuration, + setTimeRange, + setDateRange, + setWeekdayRange, + resetEventInfo, + }; +} diff --git a/app/_lib/schedule/utils.tsx b/app/_lib/schedule/utils.tsx new file mode 100644 index 00000000..272b27da --- /dev/null +++ b/app/_lib/schedule/utils.tsx @@ -0,0 +1,152 @@ +// app/_lib/schedule.ts + +import { + EventRange, + WeekdayRange, + WeekdayMap, +} from "@/app/_lib/schedule/types"; +import { formatInTimeZone, fromZonedTime, toZonedTime } from "date-fns-tz"; +import { getHours, getMinutes, startOfDay } from "date-fns"; + +/** + * expands a high-level EventRange into a concrete list of days and time slots + * for the user's local timezone + */ +export function expandEventRange(range: EventRange): Date[] { + if (range.type === "specific") { + return generateSlotsForSpecificRange(range); + } else if (range.type === "weekday") { + return generateSlotsForWeekdayRange(range); + } + return []; +} + +function generateSlotsForSpecificRange(range: EventRange): Date[] { + const slots: Date[] = []; + if ( + range.type !== "specific" || + !range.dateRange.from || + !range.dateRange.to + ) { + return []; + } + + // Get the absolute start and end times in UTC + const startDateString = range.dateRange.from.split("T")[0]; + const endDateString = range.dateRange.to.split("T")[0]; + const startTimeString = String(range.timeRange.from).padStart(2, "0"); + const endTimeString = + range.timeRange.to == 24 + ? "23:59" + : String(range.timeRange.to).padStart(2, "0"); + const eventStartUTC = fromZonedTime( + `${startDateString}T${startTimeString}`, + range.timezone, + ); + const eventEndUTC = fromZonedTime( + `${endDateString}T${endTimeString}`, + range.timezone, + ); + + // Get the valid time range for any given day in UTC + const validStartHour = range.timeRange.from; + const validEndHour = range.timeRange.to; + + let currentUTC = new Date(eventStartUTC); + + while (currentUTC <= eventEndUTC) { + // Get the time-of-day part of the current date + const zonedCurrent = toZonedTime(currentUTC, range.timezone); + + const currentHour = getHours(zonedCurrent); + const currentMinute = getMinutes(zonedCurrent); + + const isAfterStartTime = + currentHour > validStartHour || + (currentHour === validStartHour && currentMinute >= 0); + + const isBeforeEndTime = + currentHour < validEndHour || + (currentHour === validEndHour && currentMinute < 0); + + if (isAfterStartTime && isBeforeEndTime) { + slots.push(new Date(currentUTC)); + } + + currentUTC.setUTCMinutes(currentUTC.getUTCMinutes() + 15); + } + + return slots; +} + +function generateSlotsForWeekdayRange(range: WeekdayRange): Date[] { + const slots: Date[] = []; + if (range.type !== "weekday") { + return []; + } + + const dayNameToIndex: { [key: string]: number } = { + Sun: 0, + Mon: 1, + Tue: 2, + Wed: 3, + Thu: 4, + Fri: 5, + Sat: 6, + }; + + const selectedDayIndexes = new Set(); + for (const dayName in range.weekdays) { + if (range.weekdays[dayName as keyof WeekdayMap] === 1) { + selectedDayIndexes.add(dayNameToIndex[dayName]); + } + } + + if (selectedDayIndexes.size === 0) { + return []; + } + + const now = new Date(); + const startOfTodayInTz = startOfDay(toZonedTime(now, range.timezone)); + const startOfWeekInViewerTz = new Date(startOfTodayInTz); + startOfWeekInViewerTz.setDate( + startOfTodayInTz.getDate() - startOfTodayInTz.getDay(), + ); + + for (let i = 0; i < 7; i++) { + const currentDay = new Date(startOfWeekInViewerTz); + currentDay.setDate(startOfWeekInViewerTz.getDate() + i); + if (selectedDayIndexes.has(currentDay.getDay())) { + const dateString = formatInTimeZone( + currentDay, + range.timezone, + "yyyy-MM-dd", + ); + + const startTimeString = + String(range.timeRange.from).padStart(2, "0") + ":00"; + const endTimeString = + range.timeRange.to == 24 + ? "23:59" + : String(range.timeRange.to).padStart(2, "0"); + + let slotTimeUTC = fromZonedTime( + `${dateString}T${startTimeString}`, + range.timezone, + ); + const dayEndUTC = fromZonedTime( + `${dateString}T${endTimeString}`, + range.timezone, + ); + + while (slotTimeUTC < dayEndUTC) { + if (slotTimeUTC >= startOfWeekInViewerTz) { + slots.push(new Date(slotTimeUTC)); + } + slotTimeUTC.setUTCMinutes(slotTimeUTC.getUTCMinutes() + 15); + } + } + } + + return slots; +} diff --git a/app/_utils/timezone-file-generator.tsx b/app/_lib/timezone-file-generator.tsx similarity index 98% rename from app/_utils/timezone-file-generator.tsx rename to app/_lib/timezone-file-generator.tsx index 95398cb9..3efebb28 100644 --- a/app/_utils/timezone-file-generator.tsx +++ b/app/_lib/timezone-file-generator.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; -function formatLabel(tz: string): string { +export function formatLabel(tz: string): string { try { const now = new Date(); const offsetMinutes = -now.getTimezoneOffset(); diff --git a/app/_lib/types/date-range-props.tsx b/app/_lib/types/date-range-props.tsx new file mode 100644 index 00000000..52489b71 --- /dev/null +++ b/app/_lib/types/date-range-props.tsx @@ -0,0 +1,18 @@ +import { EventRange, WeekdayMap } from "@/app/_lib/schedule/types"; +import { DateRange } from "react-day-picker"; + +export type DateRangeProps = { + eventRange: EventRange; + + // update functions + setTitle?: (title: string) => void; + setCustomCode?: (code: string) => void; + setEventType?: (type: "specific" | "weekday") => void; + setTimezone?: (tz: string) => void; + setDuration?: (duration: number) => void; + setTimeRange?: (timeRange: { from: number; to: number }) => void; + setDateRange?: (dateRange: DateRange | undefined) => void; + setWeekdayRange?: (weekdays: WeekdayMap) => void; + + displayCalendar?: boolean; +}; diff --git a/app/_utils/use-check-mobile.tsx b/app/_lib/use-check-mobile.tsx similarity index 100% rename from app/_utils/use-check-mobile.tsx rename to app/_lib/use-check-mobile.tsx diff --git a/app/_lib/use-generate-timeslots.tsx b/app/_lib/use-generate-timeslots.tsx new file mode 100644 index 00000000..fcfc5de4 --- /dev/null +++ b/app/_lib/use-generate-timeslots.tsx @@ -0,0 +1,80 @@ +import { useMemo } from "react"; +import { toZonedTime } from "date-fns-tz"; +import { differenceInCalendarDays } from "date-fns"; +import { EventRange } from "@/app/_lib/schedule/types"; +import { expandEventRange } from "@/app/_lib/schedule/utils"; + +export default function useGenerateTimeSlots( + eventRange: EventRange, + timezone: string, +) { + return useMemo(() => { + const daySlots = expandEventRange(eventRange); + if (daySlots.length === 0) { + return { + timeBlocks: [], + dayGroupedSlots: [], + numDays: 0, + numHours: 0, + error: "Invalid or missing date range", + }; + } + + const localStartTime = toZonedTime(daySlots[0], timezone); + const localEndTime = toZonedTime(daySlots[daySlots.length - 1], timezone); + + const localStartHour = localStartTime.getHours(); + const localEndHour = localEndTime.getHours(); + + let timeBlocks = []; + let numHours = 0; + // Handle overnight ranges + if (localEndHour < localStartHour) { + timeBlocks.push({ startHour: 0, endHour: localEndHour }); + timeBlocks.push({ startHour: localStartHour, endHour: 23 }); + numHours += localEndHour; + numHours += 24 - localStartHour; + } else { + timeBlocks.push({ startHour: localStartHour, endHour: localEndHour }); + numHours += localEndHour - localStartHour; + } + + const dayGroupedSlots = Array.from( + daySlots + .reduce((daysMap, slot) => { + const zonedDate = toZonedTime(slot, timezone); + const dayKey = zonedDate.toLocaleDateString("en-CA"); + + if (!daysMap.has(dayKey)) { + let dayLabel = ""; + if (eventRange.type === "weekday") { + dayLabel = zonedDate + .toLocaleDateString("en-US", { + weekday: "short", + }) + .toUpperCase() as keyof typeof eventRange.weekdays; + } else { + dayLabel = zonedDate.toLocaleDateString("en-US", { + weekday: "short", + month: "short", + day: "numeric", + }); + } + daysMap.set(dayKey, { + dayKey, + dayLabel, + date: zonedDate, + timeslots: [], + }); + } + daysMap.get(dayKey)!.timeslots.push(slot); + return daysMap; + }, new Map()) + .values(), + ); + + const numDays = differenceInCalendarDays(localEndTime, localStartTime) + 1; + + return { timeBlocks, dayGroupedSlots, numDays, numHours, error: null }; + }, [eventRange, timezone]); +} diff --git a/app/_lib/use-schedule-drag.tsx b/app/_lib/use-schedule-drag.tsx new file mode 100644 index 00000000..fbd22f40 --- /dev/null +++ b/app/_lib/use-schedule-drag.tsx @@ -0,0 +1,108 @@ +import { useState, useEffect, useRef, useCallback } from "react"; + +/** + * hook to manage the drag-to-select logic for the schedule grid. + * it includes state, event handlers, and global listeners for both mouse and touch events. + */ +export default function useScheduleDrag( + onToggle: (slotIso: string) => void, + mode: "paint" | "view" | "preview", +) { + const [isDragging, setIsDragging] = useState(false); + const [didTouch, setDidTouch] = useState(false); // prevents mousedown from firing after touchend + const draggedSlots = useRef>(new Set()); + + // keeps onToggle ref up to date + const onToggleRef = useRef(onToggle); + useEffect(() => { + onToggleRef.current = onToggle; + }, [onToggle]); + + // handle stopping drag on mouseup/touchend anywhere + useEffect(() => { + const stopDragging = () => { + if (isDragging) { + setIsDragging(false); + draggedSlots.current.clear(); + } + if (didTouch) { + // reset didTouch after a short delay to prevent + // immediate re-triggering + setTimeout(() => setDidTouch(false), 50); + } + }; + + window.addEventListener("mouseup", stopDragging); + window.addEventListener("touchend", stopDragging); + + return () => { + window.removeEventListener("mouseup", stopDragging); + window.removeEventListener("touchend", stopDragging); + }; + }, [isDragging, didTouch]); + + /* EVENT HANDLERS */ + + const handleMouseDown = useCallback( + (slotIso: string, isDisabled: boolean) => { + if (mode !== "paint" || isDisabled || didTouch) return; + setIsDragging(true); + draggedSlots.current = new Set([slotIso]); + onToggleRef.current(slotIso); + }, + [mode, didTouch], + ); + + const handleMouseEnter = useCallback( + (slotIso: string, isDisabled: boolean) => { + if ( + mode !== "paint" || + !isDragging || + isDisabled || + draggedSlots.current.has(slotIso) + ) + return; + draggedSlots.current.add(slotIso); + onToggleRef.current(slotIso); + }, + [mode, isDragging], + ); + + const handleTouchStart = useCallback( + (slotIso: string, isDisabled: boolean) => { + if (mode !== "paint" || isDisabled) return; + setDidTouch(true); + setIsDragging(true); + draggedSlots.current = new Set([slotIso]); + onToggleRef.current(slotIso); + }, + [mode], + ); + + const handleTouchMove = useCallback( + (event: React.TouchEvent) => { + if (mode !== "paint" || !isDragging) return; + + // get touchpoint + const touch = event.touches[0]; + const target = document.elementFromPoint(touch.clientX, touch.clientY); + + if (target instanceof HTMLElement && target.dataset.slotIso) { + const currentSlotIso = target.dataset.slotIso; + // check that the slot is not disabled + if (!draggedSlots.current.has(currentSlotIso)) { + draggedSlots.current.add(currentSlotIso); + onToggleRef.current(currentSlotIso); + } + } + }, + [mode, isDragging], + ); + + return { + onMouseDown: handleMouseDown, + onMouseEnter: handleMouseEnter, + onTouchStart: handleTouchStart, + onTouchMove: handleTouchMove, + }; +} diff --git a/app/_types/date-range-types.tsx b/app/_types/date-range-types.tsx deleted file mode 100644 index 5b0ebabd..00000000 --- a/app/_types/date-range-types.tsx +++ /dev/null @@ -1,17 +0,0 @@ -// _types/date-range-types.ts - -import { EventRange, WeekdayMap } from "@/app/_types/schedule-types"; - -export type DateRangeProps = { - eventRange?: EventRange; - onChangeEventRange?: (range: EventRange) => void; - displayCalendar?: boolean; - - // optional legacy support props - rangeType?: "specific" | "weekday"; - onChangeRangeType?: (type: "specific" | "weekday") => void; - specificRange?: { from: Date | null; to: Date | null }; - onChangeSpecific?: (key: "from" | "to", value: Date) => void; - weekdayRange?: WeekdayMap; - onChangeWeekday?: (map: WeekdayMap) => void; -}; diff --git a/app/_types/schedule-types.tsx b/app/_types/schedule-types.tsx deleted file mode 100644 index 24f73644..00000000 --- a/app/_types/schedule-types.tsx +++ /dev/null @@ -1,115 +0,0 @@ -export type TimeDateRange = { - from: Date | null; - to: Date | null; -}; - -// generic weekday mode map -export type WeekdayMap = { - [day in "Sun" | "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat"]: 0 | 1; -}; - -// specific date mode -export type SpecificDateRange = { - type: "specific"; - duration: number; - timezone: string; - dateRange: TimeDateRange; - timeRange: TimeDateRange; -}; - -// generic weekday mode -export type WeekdayRange = { - type: "weekday"; - duration: number; - timezone: string; - weekdays: WeekdayMap; - timeRange: TimeDateRange; -}; - -// unified type -export type EventRange = SpecificDateRange | WeekdayRange; - -export type DateTimeSlot = { - day: string; - from: Date; - to: Date; -}; - -// Combine a date and a time-only object into a full Date object -export function combineDateAndTime(date: Date, time: Date): Date { - const result = new Date(date); - result.setHours(time.getHours(), time.getMinutes(), 0, 0); - return result; -} - -// Generate all concrete weekday instances for a given reference week -export function generateConcreteInstancesForWeek( - range: WeekdayRange, - weekStart: Date, -): DateTimeSlot[] { - const slots: DateTimeSlot[] = []; - - const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; - const baseFrom = range.timeRange.from; - const baseTo = range.timeRange.to; - - if (!baseFrom || !baseTo) return []; - - for (let i = 0; i < 7; i++) { - const day = days[i] as keyof WeekdayMap; - if (range.weekdays[day]) { - const date = new Date(weekStart); - date.setDate(weekStart.getDate() + i); - - const from = combineDateAndTime(date, baseFrom); - const to = combineDateAndTime(date, baseTo); - - slots.push({ day, from, to }); - } - } - - return slots; -} - -// Expand a specific date range into a list of dates (e.g., for previewing day-by-day) -export function expandDateRange(range: TimeDateRange): Date[] { - const { from, to } = range; - if (!from || !to) return []; - const days: Date[] = []; - - let current = new Date(from); - current.setHours(0, 0, 0, 0); - - const end = new Date(to); - end.setHours(0, 0, 0, 0); - - while (current <= end) { - days.push(new Date(current)); - current.setDate(current.getDate() + 1); - } - - return days; -} - -// Format a weekday map into a list of active days (e.g., ["Mon", "Wed"]) -export function getEnabledWeekdays(weekdays: WeekdayMap): string[] { - return Object.entries(weekdays) - .filter(([, v]) => v === 1) - .map(([k]) => k); -} - -// Create a list of visible day labels from a specific date range -export function getDateLabels({ from, to }: TimeDateRange): string[] { - const labels: string[] = []; - const range = expandDateRange({ from, to }); - for (const date of range) { - const weekday = date - .toLocaleDateString("en-US", { weekday: "short" }) - .toUpperCase(); - const monthDay = date - .toLocaleDateString("en-US", { month: "short", day: "numeric" }) - .toUpperCase(); - labels.push(`${weekday} ${monthDay}`); - } - return labels; -} diff --git a/app/_types/user-availability.tsx b/app/_types/user-availability.tsx deleted file mode 100644 index 7be7009c..00000000 --- a/app/_types/user-availability.tsx +++ /dev/null @@ -1,72 +0,0 @@ -// keys are either weekdays or specific date strings: -// for specific days: "2025-05-10" -// for weekdays: "Mon", "Tue", etc. -// values are sets timeslots where the user is available -export type AvailabilityMap = Record>; - -export type UserAvailability = { - type: "specific" | "weekday"; - selections: AvailabilityMap; -}; - -// Initialize empty availability -export const createEmptyUserAvailability = ( - type: "specific" | "weekday" = "specific", -): UserAvailability => ({ - type, - selections: {}, -}); - -// Toggle a cell's availability (add/remove hour from Set) -export function toggleAvailability( - prev: UserAvailability, - key: string, - hour: number, -): UserAvailability { - const updated = { ...prev, selections: { ...prev.selections } }; - const hours = new Set(updated.selections[key] || []); - - if (hours.has(hour)) { - hours.delete(hour); - } else { - hours.add(hour); - } - - updated.selections[key] = hours; - return updated; -} - -// Check if a user is available at a specific key + hour -export function isAvailable( - user: UserAvailability, - key: string, - hour: number, -): boolean { - return user.selections[key]?.has(hour) ?? false; -} - -// Add multiple hours for drag selection -export function addAvailability( - prev: UserAvailability, - key: string, - hour: number, -): UserAvailability { - const updated = { ...prev, selections: { ...prev.selections } }; - const hours = new Set(updated.selections[key] || []); - hours.add(hour); - updated.selections[key] = hours; - return updated; -} - -// Remove hours for drag deselection (optional if you want toggling behavior) -export function removeAvailability( - prev: UserAvailability, - key: string, - hour: number, -): UserAvailability { - const updated = { ...prev, selections: { ...prev.selections } }; - const hours = new Set(updated.selections[key] || []); - hours.delete(hour); - updated.selections[key] = hours; - return updated; -} diff --git a/app/_utils/format-api-error.tsx b/app/_utils/format-api-error.tsx new file mode 100644 index 00000000..3b06fc13 --- /dev/null +++ b/app/_utils/format-api-error.tsx @@ -0,0 +1,36 @@ +function snakeToTitleCase(str: string): string { + return str + .split("_") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); +} + +export default function formatApiError(errors: any): string { + let errorMessage = ""; + let generalMessage = ""; + errors = errors.error; + + if (errors.general) { + generalMessage = errors.general[0]; + } + + for (const field in errors) { + if (field !== "general" && Array.isArray(errors[field])) { + for (const msg of errors[field]) { + const fieldTitle = snakeToTitleCase(field); + errorMessage += `${fieldTitle}: ${msg}\n`; + } + } + } + + if (errorMessage) { + if (generalMessage) { + return generalMessage + "\n" + errorMessage.trim(); + } + return errorMessage.trim(); + } else if (generalMessage) { + return generalMessage; + } + + return "An unknown error has occurred."; +} diff --git a/app/_utils/providers.tsx b/app/_utils/providers.tsx deleted file mode 100644 index d0da32bd..00000000 --- a/app/_utils/providers.tsx +++ /dev/null @@ -1,11 +0,0 @@ -"use client"; - -import { ThemeProvider } from "next-themes"; - -export function Providers({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ); -} diff --git a/app/_utils/use-theme.tsx b/app/_utils/use-theme.tsx deleted file mode 100644 index d6d191c2..00000000 --- a/app/_utils/use-theme.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { useState, useEffect, useCallback } from "react"; - -export default function useDarkMode() { - const [isDarkMode, setIsDarkMode] = useState(null); - const [isClientReady, setIsClientReady] = useState(false); - - useEffect(() => { - const savedTheme = localStorage.getItem("theme"); - const prefersDark = window.matchMedia( - "(prefers-color-scheme: dark)", - ).matches; - - const dark = savedTheme ? savedTheme === "dark" : prefersDark; - - setIsDarkMode(dark); - setIsClientReady(true); - - const html = document.documentElement; - html.classList.toggle("dark", dark); - }, []); - - const toggleDarkMode = useCallback(() => { - if (isDarkMode === null) return; - - const newMode = !isDarkMode; - setIsDarkMode(newMode); - const html = document.documentElement; - - if (newMode) { - html.classList.add("dark"); - localStorage.setItem("theme", "dark"); - } else { - html.classList.remove("dark"); - localStorage.setItem("theme", "light"); - } - }, [isDarkMode]); - - return { - isDarkMode: isDarkMode ?? false, - isClientReady, - toggleDarkMode, - }; -} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 46e7ba5a..695210ee 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,5 +1,100 @@ -import React from "react"; +"use client"; -export default function page() { - return
dashboard page
; +type Event = { + id: string; + title: string; + participants: string[]; +}; + +export default function Page() { + // Mock data for testing + const joinedEvents: Event[] = [ + { + id: "1", + title: "Dinner With Friends", + participants: ["A", "B", "C", "D", "E", "F"], + }, + { id: "2", title: "Board Game Night", participants: ["D", "E"] }, + { + id: "3", + title: "Dungeons & Dragons ", + participants: ["F", "G", "H", "I"], + }, + { id: "4", title: "Project Meeting", participants: ["I", "J", "K"] }, + { + id: "5", + title: "Competitive Slug Racing", + participants: ["L", "M", "N", "O", "P"], + }, + ]; + + const createdEvents: Event[] = [ + { id: "6", title: "Study Session", participants: ["I", "J", "K"] }, + { + id: "7", + title: "Eboard Meeting", + participants: ["L", "M", "N", "O", "P"], + }, + ]; + + const renderParticipants = (participants: string[]) => { + const visible = participants.slice(0, 4); + const extraCount = participants.length - visible.length; + + return ( + <> + {visible.map((p, i) => ( +
+ {p} +
+ ))} + {extraCount > 0 && ( +
+ +{extraCount} +
+ )} + + ); + }; + + return ( +
+ {/* Events You Joined */} +
+

Events You Joined

+
+ {joinedEvents.map((event) => ( +
+
+
+ {renderParticipants(event.participants)} +
+
+

{event.title}

+
+ ))} +
+
+ + {/* Events You Created */} +
+

Events You Created

+
+ {createdEvents.map((event) => ( +
+
+
+ {renderParticipants(event.participants)} +
+
+

{event.title}

+
+ ))} +
+
+
+ ); } diff --git a/app/forgot-password/page.tsx b/app/forgot-password/page.tsx new file mode 100644 index 00000000..c05be2ab --- /dev/null +++ b/app/forgot-password/page.tsx @@ -0,0 +1,98 @@ +"use client"; + +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import React, { useRef, useState } from "react"; +import formatApiError from "../_utils/format-api-error"; +import MessagePage from "../ui/layout/message-page"; + +export default function Page() { + const [email, setEmail] = useState(""); + const [emailSent, setEmailSent] = useState(false); + const isSubmitting = useRef(false); + const router = useRouter(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (isSubmitting.current) return; + isSubmitting.current = true; + + if (!email) { + alert("Missing email"); + isSubmitting.current = false; + return; + } + + await fetch("/api/auth/start-password-reset/", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email }), + }) + .then(async (res) => { + if (res.ok) { + setEmailSent(true); + } else { + alert(formatApiError(await res.json())); + } + }) + .catch((err) => { + console.error("Fetch error:", err); + alert("An error occurred. Please try again."); + }); + + isSubmitting.current = false; + }; + + return ( +
+ {emailSent ? ( + router.push("/login"), + }, + ]} + /> + ) : ( +
+ {/* Title */} +

+ reset password +

+ + {/* Email */} + setEmail(e.target.value)} + className="mb-4 w-full rounded-full border px-4 py-2 focus:ring-2 focus:outline-none" + /> + +
+ {/* Forgot Password */} + + Remembered password? + + + {/* Email Button */} + +
+
+ )} +
+ ); +} diff --git a/app/globals.css b/app/globals.css index 159f72d2..f2f97161 100644 --- a/app/globals.css +++ b/app/globals.css @@ -4,22 +4,21 @@ :root { --background: #f5f5f5; --foreground: #3e3c53; + --bone-base: #e9deca; + --red-base: #ff5c5c; + --white-base: #ffffff; + --bone: #e9deca; + --lion: #deb887; + --violet: #3e3c53; + --stone: #9ca3af; + --red: #ff6b6b; --calendar-accent: var(--color-blue); --calendar-accent-background: var(--color-blue-100); } -@media (prefers-color-scheme: dark) { - :root { - --background: #3e3c53; - --foreground: #e9deca; - --calendar-accent: var(--color-red); - --calendar-accent-background: var(--color-red-200); - } -} - .dark { - --background: #3e3c53; - --foreground: #e9deca; + --background: var(--violet); + --foreground: var(--bone); --calendar-accent: var(--color-red); --calendar-accent-background: var(--color-red-200); } @@ -27,35 +26,106 @@ body { background: var(--background); color: var(--foreground); + font-family: var(--font-nunito); } -.rdp-root { - --rdp-accent-color: var(--calendar-accent); - --rdp-accent-background-color: var(--calendar-accent-background); +.font-display { + font-family: var(--font-modak); + letter-spacing: -0.02em; + line-height: 0.9; +} + +.text-bone { + color: var(--bone); } -.text-stroke-white { - -webkit-text-stroke: 2px var(--color-white); +.text-lion { + color: var(--lion); } -.text-stroke-violet { - -webkit-text-stroke: 2px var(--color-violet); +.text-violet { + color: var(--violet); } -.text-stroke-bone { - -webkit-text-stroke: 2px var(--color-bone); +.text-outline { + -webkit-text-stroke: 2px currentColor; + color: transparent; } -.text-stroke-blue { - -webkit-text-stroke: 2px var(--color-blue); +.text-outline-dark { + -webkit-text-stroke: 2px var(--violet); + color: transparent; + text-shadow: 2px 2px 0px var(--violet); } -.text-stroke-red { - -webkit-text-stroke: 2px var(--color-red); +.text-outline-light { + -webkit-text-stroke: 2px var(--bone); + color: transparent; + text-shadow: 2px 2px 0px var(--bone); } -.text-stroke-lion { - -webkit-text-stroke: 2px var(--color-lion); +.text-outline-golden { + -webkit-text-stroke: 2px var(--lion); + color: transparent; +} + +.bubble-text { + font-family: var(--font-modak); + letter-spacing: 0.05em; + line-height: 1.1; +} + +.rdp-root { + --rdp-accent-color: var(--calendar-accent); + --rdp-accent-background-color: var(--calendar-accent-background); +} + +.rdp-months { + display: flex; + justify-content: center; +} + +.frosted-glass { + backdrop-filter: blur(var(--blur-sm)); + /* copied from the generated tailwind css */ + border-style: var(--tw-border-style); + border-width: 1px; + /* dark background */ + background-color: color-mix(in srgb, #000 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-black) 10%, transparent); + } + /* dark border */ + border-color: color-mix(in srgb, #000 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-black) 20%, transparent); + } + /* dark mode stuff */ + &:where(.dark, .dark *) { + /* light background */ + background-color: color-mix(in srgb, oklch(0.9702 0 0) 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix( + in oklab, + var(--color-white) 10%, + transparent + ); + } + /* light border */ + border-color: color-mix(in srgb, oklch(0.9702 0 0) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-white) 20%, transparent); + } + } +} + +.frosted-glass .frosted-glass { + /* copied from the generated tailwind css */ + /* ALWAYS light background so it's not too dark against the parent glass */ + background-color: color-mix(in srgb, oklch(0.9702 0 0) 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-white) 10%, transparent); + } } @custom-variant dark (&:where(.dark, .dark *)); @@ -80,12 +150,46 @@ body { cubic-bezier(0.16, 1, 0.3, 1); --animate-slideUp: slideUp 400ms cubic-bezier(0.16, 1, 0.3, 1); --animate-slideDown: slideDown 400ms cubic-bezier(0.16, 1, 0.3, 1); + --animate-hide: hide 100ms ease-in; + --animate-slideIn: slideIn 150ms cubic-bezier(0.16, 1, 0.3, 1); + --animate-swipeOut: swipeOut 100ms ease-out; + + @keyframes hide { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + } + } + + @keyframes slideIn { + 0% { + transform: translateX(calc(100% + var(--viewport-padding))); + } + + 100% { + transform: translateX(0); + } + } + + @keyframes swipeOut { + 0% { + transform: translateX(var(--radix-toast-swipe-end-x)); + } + + 100% { + transform: translateX(calc(100% + var(--viewport-padding))); + } + } @keyframes slideUpAndFade { 0% { opacity: 0; transform: translateY(2px); } + 100% { opacity: 1; transform: translateY(0); @@ -97,16 +201,19 @@ body { opacity: 0; transform: translateX(-2px); } + 100% { opacity: 1; transform: translateX(0); } } + @keyframes slideDownAndFade: { 0% { opacity: 0; transform: translateY(-2px); } + 100% { opacity: 1; transform: translateY(0); @@ -118,6 +225,7 @@ body { opacity: 0; transform: translateX(2px); } + 100% { opacity: 1; transform: translateX(0); @@ -128,6 +236,7 @@ body { 0% { transform: translateY(100%); } + 100% { transform: translateY(0); } @@ -137,6 +246,7 @@ body { 0% { transform: translateY(0); } + 100% { transform: translateY(100%); } diff --git a/app/layout.tsx b/app/layout.tsx index f64983ca..c2b45131 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,7 +1,7 @@ import type { Metadata } from "next"; import { Modak, Nunito } from "next/font/google"; -import Header from "./ui/layout/header"; -import { Providers } from "./_utils/providers"; +import Header from "./ui/components/header/header"; +import { Providers } from "@/app/_lib/providers"; import "./globals.css"; const modak = Modak({ @@ -19,15 +19,15 @@ const nunito = Nunito({ }); export const metadata: Metadata = { - title: "tomeeto", - description: "to meet or not to meet", + title: "plancake", + description: "Stacking up perfect plans, one pancake at a time", }; export default function RootLayout({ children, -}: Readonly<{ +}: { children: React.ReactNode; -}>) { +}) { return ( -
+
{children} diff --git a/app/login/page.tsx b/app/login/page.tsx new file mode 100644 index 00000000..467db9e4 --- /dev/null +++ b/app/login/page.tsx @@ -0,0 +1,106 @@ +"use client"; + +import React, { useRef, useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import formatApiError from "../_utils/format-api-error"; +import { LoginContext } from "@/app/_lib/providers"; +import { useContext } from "react"; + +export default function Page() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const { setLoggedIn } = useContext(LoginContext); + const isSubmitting = useRef(false); + const router = useRouter(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (isSubmitting.current) return; + isSubmitting.current = true; + + if (!email) { + alert("Missing email"); + isSubmitting.current = false; + return; + } + if (!password) { + alert("Missing password"); + isSubmitting.current = false; + return; + } + + await fetch("/api/auth/login/", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + }) + .then(async (res) => { + if (res.ok) { + setLoggedIn(true); + router.push("/dashboard"); + } else { + alert(formatApiError(await res.json())); + } + }) + .catch((err) => { + console.error("Fetch error:", err); + alert("An error occurred. Please try again."); + }); + + isSubmitting.current = false; + }; + + return ( +
+
+ {/* Title */} +

+ login +

+ + {/* Email */} + setEmail(e.target.value)} + className="mb-4 w-full rounded-full border px-4 py-2 focus:ring-2 focus:outline-none" + /> + + {/* Password */} + setPassword(e.target.value)} + className="mb-4 w-full rounded-full border px-4 py-2 focus:ring-2 focus:outline-none" + /> + +
+ {/* Forgot Password */} + + Forgot Password? + + + {/* Login Button */} + +
+ + {/* Sign up Link */} +
+ No account?{" "} + + Sign up! + +
+
+
+ ); +} diff --git a/app/new-event/page.tsx b/app/new-event/page.tsx index c8cfe65c..77aebed5 100644 --- a/app/new-event/page.tsx +++ b/app/new-event/page.tsx @@ -1,140 +1,172 @@ "use client"; -import { useState } from "react"; - -import TimeDropdown from "../ui/components/time-dropdown"; -import DateRangeSelector from "../ui/components/date-range/date-range-selector"; -import TimezoneSelect from "../ui/components/timezone-select"; -import CustomSelect from "../ui/components/custom-select"; -import GridPreviewDialog from "../ui/components/schedule/grid-preview-dialog"; - -import { EventRange } from "../_types/schedule-types"; +import TimeDropdown from "@/app/ui/components/time-dropdown"; +import DateRangeSelector from "@/app/ui/components/date-range/date-range-selector"; +import TimezoneSelect from "@/app/ui/components/selectors/timezone-select"; +import CustomSelect from "@/app/ui/components/selectors/custom-select"; +import GridPreviewDialog from "@/app/ui/components/schedule/grid-preview-dialog"; +import { useEventInfo } from "../_lib/schedule/use-event-info"; + +const durationOptions = [ + { label: "30 minutes", value: 30 }, + { label: "45 minutes", value: 45 }, + { label: "1 hour", value: 60 }, +]; export default function Page() { const defaultTZ = Intl.DateTimeFormat().resolvedOptions().timeZone; - - const [eventRange, setEventRange] = useState({ - type: "specific", - duration: 60, - timezone: defaultTZ, - dateRange: { from: new Date(), to: new Date() }, - timeRange: { from: new Date(), to: new Date() }, - }); - - const handleTZChange = (newTZ: string | number) => { - setEventRange((prev) => ({ - ...prev, - timezone: newTZ.toString(), - })); - }; - - const duationOptions = [ - { label: "30 minutes", value: 30 }, - { label: "45 minutes", value: 45 }, - { label: "1 hour", value: 60 }, - ]; - - const handleDurationChange = (newDuration: string | number) => { - const duration = Number(newDuration); - setEventRange((prev) => ({ - ...prev, - duration, - })); - }; - - const handleTimeChange = (key: "from" | "to", value: Date) => { - setEventRange((prev) => ({ - ...prev, - timeRange: { - ...prev.timeRange, - [key]: value, - }, - })); - }; - - const handleEventRangeChange = (range: EventRange) => { - setEventRange(range); - }; + const { + state, + setTitle, + setEventType, + setCustomCode, + setTimezone, + setDuration, + setTimeRange, + setDateRange, + setWeekdayRange, + } = useEventInfo(); + const { title, customCode, eventRange } = state; return ( -
- -
- {/* Prompt */} -
- What times and dates is this event? -
+
+
+ setTitle(e.target.value)} + placeholder="add event name" + className="w-full border-b-1 border-violet p-1 text-2xl focus:outline-none md:w-2/4 dark:border-gray-400" + /> + +
+
{/* Date range picker */} -
+
{/* From/To */} -
- + +
+ handleTimeChange("from", from)} + onChange={(value) => + setTimeRange({ ...eventRange.timeRange, from: value }) + } />
-
- +
+ handleTimeChange("to", to)} + onChange={(to) => setTimeRange({ ...eventRange.timeRange, to })} />
- {/* Timezone */} -
-
- + {/* Timezone & Duration */} +
+ {/* Desktop: show all options */} + + +
+ +
+ +
setDuration((v as number) || 60)} />
- + + +
+ setCustomCode(e.target.value)} + placeholder="optional" + className="w-full border-b-1 border-gray-300 text-blue focus:outline-none dark:border-gray-400 dark:text-red" + /> +
+ + {/* Mobile: expandable section */} +
+ + Advanced Options + +
+ + + + setDuration((v as number) || 60)} + /> + + setCustomCode(e.target.value)} + placeholder="optional" + className="w-full border-b-1 border-gray-300 text-blue focus:outline-none dark:border-gray-400 dark:text-red" + /> +
+
-
+
+
+
- {/*
- - - -
- */} +
+ +
+ +
+
+ Create Event
-
); } diff --git a/app/page.tsx b/app/page.tsx index fcd0ebc6..b886929c 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,29 +1,167 @@ "use client"; -import { useRouter } from "next/navigation"; +import Link from "next/link"; export default function Home() { - const router = useRouter().push; - return ( -
- - { - if (e.key === "Enter") { - router("/schedule/availability/" + e.currentTarget.value); - } - }} - placeholder="or enter code here" - className="w-3/4 border-b-2 text-center focus:outline-none" - /> -
+
+ {/* Hero Section */} +
+
+

+ + planning made + + + stack +
+ simple +
+

+

+ The fluffiest way to coordinate schedules and plan group events. + Stack up availability and serve the perfect meeting time. +

+
+ + Mix your first plan + + + View Dashboard + +
+
+
+ + {/* Why Plancake Section */} +
+
+
+ {/* Pancake emoji - centered on mobile */} +
+
🥞
+
+ + {/* Content - centered on mobile, left-aligned on desktop */} +
+

+ why +
+ plancake? +

+
+
+

+ Smart Planning +

+

+ Intelligently suggest optimal meeting times based on + everyone's availability and preferences. +

+
+
+

+ Easy Coordination +

+

+ Share a simple link and watch as responses stack up in + real-time without the back-and-forth. +

+
+
+

+ Perfect Results +

+

+ Get the ideal meeting time that works for everyone with + automatic calendar integration. +

+
+
+
+
+
+
+ + {/* Golden Stack Recipe */} +
+
+
+

+ golden +
+ stack recipe +

+

+ Follow these simple steps to cook up the perfect schedule every + time. +

+
+ +
+
+
+
+ 🍳 +
+

Mix your event

+

+ Set up your meeting details, add time options, and customize + your preferences +

+
+
+
+ 📤 +
+

Share & Stack

+

+ Send the link to participants and watch responses stack up in + real-time +

+
+
+
+ 🥞 +
+

Flip & Serve

+

+ Review the results, pick the best time, and serve up calendar + invites to all +

+
+
+
+
+
+ + {/* Plan Today Section */} +
+
+

PLAN TODAY

+
+ + Start Planning + +
+
+
plancake
+

+ © 2025 plancake. Stacking up perfect plans, one pancake at a + time. +

+
+
+
+
); } diff --git a/app/reset-password/page.tsx b/app/reset-password/page.tsx new file mode 100644 index 00000000..455787da --- /dev/null +++ b/app/reset-password/page.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { useRouter, useSearchParams } from "next/navigation"; +import React, { useRef, useState } from "react"; +import formatApiError from "../_utils/format-api-error"; + +export default function Page() { + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const isSubmitting = useRef(false); + const router = useRouter(); + + const searchParams = useSearchParams(); + const pwdResetToken = searchParams.get("token"); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (isSubmitting.current) return; + isSubmitting.current = true; + + if (!pwdResetToken) { + alert("This link is expired or invalid."); + isSubmitting.current = false; + return; + } + + if (!newPassword) { + alert("Missing new password."); + isSubmitting.current = false; + return; + } + + if (newPassword !== confirmPassword) { + alert("Passwords do not match."); + isSubmitting.current = false; + return; + } + await fetch("/api/auth/reset-password/", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + reset_token: pwdResetToken, + new_password: newPassword, + }), + }) + .then(async (res) => { + if (res.ok) { + router.push("/reset-password/success"); + } else { + alert(formatApiError(await res.json())); + } + }) + .catch((err) => { + console.error("Fetch error:", err); + alert("An error occurred. Please try again."); + }); + + isSubmitting.current = false; + }; + + return ( +
+
+ {/* Title */} +

+ reset password +

+ + {/* New Password */} + setNewPassword(e.target.value)} + className="mb-4 w-full rounded-full border px-4 py-2 focus:ring-2 focus:outline-none" + /> + + {/* Confirm Password */} + setConfirmPassword(e.target.value)} + className="mb-4 w-full rounded-full border px-4 py-2 focus:ring-2 focus:outline-none" + /> + + {/* Change Password Button */} +
+ +
+
+
+ ); +} diff --git a/app/reset-password/success/page.tsx b/app/reset-password/success/page.tsx new file mode 100644 index 00000000..cab6c194 --- /dev/null +++ b/app/reset-password/success/page.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import MessagePage from "../../ui/layout/message-page"; + +export default function Page() { + const router = useRouter(); + + return ( +
+ router.push("/login"), + }, + ]} + /> +
+ ); +} diff --git a/app/schedule/availability/page.tsx b/app/schedule/availability/page.tsx index c5a8f3c6..c16a8529 100644 --- a/app/schedule/availability/page.tsx +++ b/app/schedule/availability/page.tsx @@ -1,5 +1,114 @@ -import React from "react"; +"use client"; -export default function page() { - return
availability page
; +import { useState } from "react"; +import { useAvailability } from "@/app/_lib/availability/use-availability"; + +import { EventRange } from "@/app/_lib/schedule/types"; + +import ScheduleGrid from "@/app/ui/components/schedule/schedule-grid"; +import EventInfoDrawer from "@/app/ui/components/event-info-drawer"; +import CopyToast from "@/app/ui/components/copy-toast"; +import TimezoneSelect from "@/app/ui/components/selectors/timezone-select"; +import { EventInfo } from "@/app/ui/components/event-info-drawer"; + +export default function Page() { + // AVAILABILITY STATE + const { state, setDisplayName, setTimeZone, toggleSlot } = + useAvailability("John Doe"); + const { displayName, timeZone, userAvailability } = state; + + const eventName = "Sample Event"; + + // --- CORRECTED --- + // 1. Create dates in UTC to avoid browser timezone issues. + const today = new Date(); + const formatDate = (date: Date): string => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); // Months are 0-indexed + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; + }; + + const eventRange: EventRange = { + type: "specific", + duration: 60, + // 2. Set the event's *original* timezone, not the user's. + timezone: "America/New_York", + dateRange: { + from: formatDate(today), + to: formatDate(today), + }, + // This timeRange is for the valid times within a day + timeRange: { + from: 9, + to: 20, + }, + }; + + return ( +
+ {/* Header and Button Row */} +
+
+

{eventName}

+ +
+ +
+ + +
+
+ + {/* Main Content */} +
+ {/* Left Panel */} +
+
+ + Hi,{" "} + setDisplayName(e.target.value)} + placeholder="add your name" + className="inline-block w-auto border-b border-violet bg-transparent px-1 focus:outline-none dark:border-gray-400" + /> +
+ add your availabilities here +
+
+ + {/* Desktop-only Event Info */} +
+ +
+ +
+ Displaying event in + + + +
+
+ + {/* Right Panel */} + +
+ +
+
+ Submit Availability +
+
+
+ ); } diff --git a/app/schedule/layout.tsx b/app/schedule/layout.tsx index e353ee11..a600a5e6 100644 --- a/app/schedule/layout.tsx +++ b/app/schedule/layout.tsx @@ -4,8 +4,9 @@ export default function ScheduleLayout({ children: React.ReactNode; }) { return ( - <> -
{children}
- +
+
+ {children} +
); } diff --git a/app/schedule/results/page.tsx b/app/schedule/results/page.tsx index 5e395bb3..795d53a2 100644 --- a/app/schedule/results/page.tsx +++ b/app/schedule/results/page.tsx @@ -1,5 +1,128 @@ -import React from "react"; +"use client"; -export default function page() { - return
results page
; +import { useState, useEffect } from "react"; + +import ScheduleGrid from "@/app/ui/components/schedule/schedule-grid"; +import EventInfoDrawer from "@/app/ui/components/event-info-drawer"; +import CopyToast from "@/app/ui/components/copy-toast"; +import TimezoneSelect from "@/app/ui/components/selectors/timezone-select"; + +import { EventInfo } from "@/app/ui/components/event-info-drawer"; +import { Pencil1Icon } from "@radix-ui/react-icons"; +import { EventRange } from "@/app/_lib/schedule/types"; + +export default function Page() { + const [timezone, setTimezone] = useState( + Intl.DateTimeFormat().resolvedOptions().timeZone, + ); + + const handleTZChange = (newTZ: string | number) => { + setTimezone(newTZ.toString()); + }; + + const [userName, setUserName] = useState("John Doe"); + const [eventName, setEventName] = useState("Event Name"); + const [attendees, setAttendees] = useState(""); + const [eventCode, setEventCode] = useState(""); + const [isOwner, setIsOwner] = useState(true); + const [hoveredSlot, setHoveredSlot] = useState(null); + + // Placeholder eventRange + const today = new Date(); + const formatDate = (date: Date): string => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); // Months are 0-indexed + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; + }; + + const eventRange: EventRange = { + type: "specific", + duration: 60, + // 2. Set the event's *original* timezone, not the user's. + timezone: "America/New_York", + dateRange: { + from: formatDate(today), + to: formatDate(today), + }, + // This timeRange is for the valid times within a day + timeRange: { + from: 9, + to: 20, + }, + }; + + return ( +
+
+
+

{eventName}

+ +
+
+ {isOwner && ( + + )} + +
+
+ +
+ + + {/* Sidebar for attendees */} +
+
+

Attendees

+
    + {/* {attendees.map((attendee) => { + const isAvailable = hoveredSlot + ? attendee.availability.has(hoveredSlot) + : true; + return ( +
  • + {attendee.name} +
  • + ); + })} */} +
+
+ +
+ +
+ +
+ Displaying event in + + + +
+
+
+
+ ); } diff --git a/app/sign-up/email-sent/page.tsx b/app/sign-up/email-sent/page.tsx new file mode 100644 index 00000000..32e3c9a2 --- /dev/null +++ b/app/sign-up/email-sent/page.tsx @@ -0,0 +1,77 @@ +"use client"; + +import formatApiError from "@/app/_utils/format-api-error"; +import { useRouter } from "next/navigation"; +import { useEffect, useRef } from "react"; +import MessagePage from "../../ui/layout/message-page"; + +export default function Page() { + const router = useRouter(); + const email = sessionStorage.getItem("sign_up_email"); + const lastEmailResend = useRef(Date.now()); + + useEffect(() => { + if (!email) { + // the user shouldn't be here + router.push("/login"); + } + // clear the email from storage + sessionStorage.removeItem("sign_up_email"); + }, []); + + if (!email) { + // stop rendering if there's no email + return null; + } + + const handleResendEmail = async () => { + const emailResendCooldown = 30000; // 30 seconds + let timeLeft = + (emailResendCooldown - (Date.now() - lastEmailResend.current)) / 1000; + timeLeft = Math.ceil(timeLeft); + if (timeLeft > 0) { + alert(`Slow down! ${timeLeft} seconds until you can send again.`); + return; + } + + await fetch("/api/auth/resend-register-email/", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email }), + }) + .then(async (res) => { + if (res.ok) { + alert("Email resent. Please check your inbox."); + } else { + alert(formatApiError(await res.json())); + } + }) + .catch((err) => { + console.error("Fetch error:", err); + alert("An error occurred. Please try again."); + }); + + lastEmailResend.current = Date.now(); + }; + + return ( +
+ router.push("/login"), + }, + ]} + /> +
+ ); +} diff --git a/app/sign-up/page.tsx b/app/sign-up/page.tsx new file mode 100644 index 00000000..cf1fed8a --- /dev/null +++ b/app/sign-up/page.tsx @@ -0,0 +1,113 @@ +"use client"; + +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import React, { useState } from "react"; +import formatApiError from "../_utils/format-api-error"; + +export default function Page() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const isSubmitting = React.useRef(false); + const router = useRouter(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (isSubmitting.current) return; + isSubmitting.current = true; + + if (!email) { + alert("Missing email"); + isSubmitting.current = false; + return; + } + if (!password) { + alert("Missing password"); + isSubmitting.current = false; + return; + } + if (confirmPassword !== password) { + alert("Passwords do not match"); + isSubmitting.current = false; + return; + } + + await fetch("/api/auth/register/", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + }) + .then(async (res) => { + if (res.ok) { + sessionStorage.setItem("sign_up_email", email); + router.push("/sign-up/email-sent"); + } else { + alert(formatApiError(await res.json())); + } + }) + .catch((err) => { + console.error("Fetch error:", err); + alert("An error occurred. Please try again."); + }); + + isSubmitting.current = false; + }; + + return ( +
+
+ {/* Title */} +

+ sign up +

+ + {/* Email */} + setEmail(e.target.value)} + className="mb-4 w-full rounded-full border px-4 py-2 focus:ring-2 focus:outline-none" + /> + + {/* Password */} + setPassword(e.target.value)} + className="mb-4 w-full rounded-full border px-4 py-2 focus:ring-2 focus:outline-none" + /> + + {/* Retype Password */} + setConfirmPassword(e.target.value)} + className="mb-4 w-full rounded-full border px-4 py-2 focus:ring-2 focus:outline-none" + /> + + {/* Sign Up Button */} +
+ +
+ + {/* Login Link */} +
+ Already have an account?{" "} + + Login! + +
+
+
+ ); +} diff --git a/app/ui/components/archive/month-calendar.tsx b/app/ui/components/archive/month-calendar.tsx index 75ad862b..677355fe 100644 --- a/app/ui/components/archive/month-calendar.tsx +++ b/app/ui/components/archive/month-calendar.tsx @@ -1,7 +1,7 @@ "use client"; import { ChangeEventHandler, useEffect, useState } from "react"; -import useCheckMobile from "@/app/_utils/use-check-mobile"; +import useCheckMobile from "@/app/_lib/use-check-mobile"; import { format, isAfter, isBefore, isValid, parse } from "date-fns"; import { diff --git a/app/ui/components/copy-toast.tsx b/app/ui/components/copy-toast.tsx new file mode 100644 index 00000000..c5c245fb --- /dev/null +++ b/app/ui/components/copy-toast.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import * as Toast from "@radix-ui/react-toast"; +import { CopyIcon } from "@radix-ui/react-icons"; + +export default function CopyToast({ + eventLink = "plancake.com/event/12345", + label = "Event Link", +}) { + const [open, setOpen] = useState(false); + const eventDateRef = useRef(new Date()); + const timerRef = useRef(0); + + useEffect(() => { + return () => clearTimeout(timerRef.current); + }, []); + + const copyToClipboard = async () => { + try { + await navigator.clipboard.writeText(eventLink); + setOpen(true); + window.clearTimeout(timerRef.current); + timerRef.current = window.setTimeout(() => { + setOpen(false); + }, 2000); + } catch (err) { + console.error("Failed to copy: ", err); + } + }; + + return ( + + + + + + Copied: Event Link + + +
+ {eventLink} +
+
+
+ +
+ ); +} diff --git a/app/ui/components/custom-group-select.tsx b/app/ui/components/custom-group-select.tsx deleted file mode 100644 index 9083d553..00000000 --- a/app/ui/components/custom-group-select.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import * as Select from "@radix-ui/react-select"; -import { CheckIcon, ChevronDownIcon } from "@radix-ui/react-icons"; -import { cn } from "@/app/_lib/classname"; -import { forwardRef } from "react"; - -type Option = { label: string; value: string }; -type GroupedOption = { label: string; options: Option[] }; - -type CustomSelectProps = { - value: string; - onValueChange: (value: string) => void; - groupedOptions?: GroupedOption[]; - className?: string; -}; - -export default function CustomGroupSelect({ - groupedOptions, - value, - onValueChange, - className, -}: CustomSelectProps) { - const allOptions = groupedOptions?.flatMap((g) => g.options) ?? []; - const current = allOptions.find((o) => o.value === value); - - return ( - - - {current?.label || "Select"} - - - - - - - - {groupedOptions?.map((group) => ( - - {group.options.map((option) => ( - - {option.label} - - ))} - - ))} - - - - - ); -} - -type SelectItemProps = { - value: string; - children: React.ReactNode; -}; - -const SelectItem = forwardRef( - ({ children, value }, ref) => { - return ( - - {children} - - - - - ); - }, -); - -SelectItem.displayName = "SelectItem"; - -const SelectGroup = forwardRef( - ({ children, value }, ref) => { - return ( - - - {value} - - {children} - - - ); - }, -); -SelectGroup.displayName = "SelectGroup"; diff --git a/app/ui/components/custom-select.tsx b/app/ui/components/custom-select.tsx deleted file mode 100644 index 1ddf5a6f..00000000 --- a/app/ui/components/custom-select.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import * as Select from "@radix-ui/react-select"; -import { CheckIcon, ChevronDownIcon } from "@radix-ui/react-icons"; -import { forwardRef } from "react"; -import { cn } from "@/app/_lib/classname"; - -type Option = { label: string; value: string | number }; -type GroupedOption = { label: string; options: Option[] }; - -type CustomSelectOptions = Option | GroupedOption; - -type CustomSelectProps = { - value: string | number; - options: CustomSelectOptions[]; - isGrouped?: boolean; - onValueChange: (value: string | number) => void; - className?: string; -}; - -export default function CustomSelect({ - value, - options, - isGrouped = false, - onValueChange, - className, -}: CustomSelectProps) { - // flatten options if they are grouped and fine the current selected option - const allOptions = isGrouped - ? (options as GroupedOption[]).flatMap((g) => g.options) - : (options as Option[]); - const current = allOptions.find((o) => o.value === value); - - return ( - onValueChange(isNaN(Number(v)) ? v : Number(v))} - > - - - - - - - - - - - {isGrouped - ? (options as GroupedOption[]).map((group) => ( - - {group.options.map((option) => ( - - {option.label} - - ))} - - )) - : (options as Option[]).map((option) => ( - - {option.label} - - ))} - - - - - ); -} - -type SelectProps = { - value: string | number; - children: React.ReactNode; -}; - -const SelectItem = forwardRef( - ({ children, value }, ref) => { - return ( - - {children} - - - - - ); - }, -); -SelectItem.displayName = "SelectItem"; - -const SelectGroup = forwardRef( - ({ children, value }, ref) => { - return ( - - - {value} - - {children} - - - ); - }, -); -SelectGroup.displayName = "SelectGroup"; diff --git a/app/ui/components/date-range/date-range-drawer.tsx b/app/ui/components/date-range/date-range-drawer.tsx index 23a5b82b..b04b6765 100644 --- a/app/ui/components/date-range/date-range-drawer.tsx +++ b/app/ui/components/date-range/date-range-drawer.tsx @@ -1,39 +1,28 @@ import * as Dialog from "@radix-ui/react-dialog"; -import { Calendar } from "../month-calendar"; -import CustomSelect from "../custom-select"; -import WeekdayCalendar from "../weekday-calendar"; -import DateRangeInput from "./date-range-input"; -import { DateRangeProps } from "@/app/_types/date-range-types"; + +import { DateRangeProps } from "@/app/_lib/types/date-range-props"; +import { fromZonedTime } from "date-fns-tz"; + +import { Calendar } from "@/app/ui/components/month-calendar"; +import WeekdayCalendar from "@/app/ui/components/weekday-calendar"; +import DateRangeInput from "@/app/ui/components/date-range/date-range-input"; +import EventTypeSelect from "@/app/ui/components/selectors/event-type-select"; export default function DateRangeDrawer({ eventRange, - onChangeRangeType, - onChangeSpecific, - onChangeWeekday, + setEventType = () => {}, + setWeekdayRange = () => {}, + setDateRange = () => {}, }: DateRangeProps) { const rangeType = eventRange?.type ?? "specific"; - const select = ( - - onChangeRangeType?.(value === "Specific Dates" ? "specific" : "weekday") - } - className="min-h-9 min-w-[100px] border-none px-2" - /> - ); return ( @@ -44,21 +33,24 @@ export default function DateRangeDrawer({ className="fixed right-0 bottom-0 left-0 z-50 flex h-[500px] w-full animate-slideUp flex-col data-[state=closed]:animate-slideDown" aria-label="Date range picker" > -
+
- Select Date Range - {select} + +
@@ -69,51 +61,56 @@ export default function DateRangeDrawer({ const DateRangeDrawerSelector = ({ eventRange, - onChangeSpecific, - onChangeWeekday = () => {}, - displayCalendar = false, + displayCalendar, + setWeekdayRange = () => {}, + setDateRange = () => {}, }: DateRangeProps) => { if (eventRange?.type === "specific") { - const specificRange = eventRange.dateRange; + const startDate = fromZonedTime( + eventRange.dateRange.from, + eventRange.timezone, + ); + const endDate = fromZonedTime(eventRange.dateRange.to, eventRange.timezone); return ( -
- - {displayCalendar && ( +
+ {displayCalendar ? ( { - if (range?.from) { - onChangeSpecific?.("from", range.from); - } - if (range?.to) { - onChangeSpecific?.("to", range.to); - } + from: startDate || undefined, + to: endDate || undefined, }} + setDateRange={setDateRange} /> + ) : ( + <> + + + )}
); } return ( - + {!displayCalendar && ( + + )} + + onChange={setWeekdayRange} + /> +
); }; diff --git a/app/ui/components/date-range/date-range-input.tsx b/app/ui/components/date-range/date-range-input.tsx index c9badbe7..ac4b1cce 100644 --- a/app/ui/components/date-range/date-range-input.tsx +++ b/app/ui/components/date-range/date-range-input.tsx @@ -1,36 +1,41 @@ import { format } from "date-fns"; type DateRangeInputProps = { - specificRange: { from: Date | null; to: Date | null } | undefined; - onChangeSpecific?: (key: "from" | "to", value: Date) => void; + startDate: Date; + endDate: Date; }; export default function DateRangeInput({ - specificRange, - onChangeSpecific, + startDate, + endDate, }: DateRangeInputProps) { - const displayFrom = specificRange?.from - ? format(specificRange.from, "EEE, MMM d") - : ""; - const displayTo = specificRange?.to - ? format(specificRange.to, "EEE, MMM d") - : ""; + const displayFrom = startDate ? format(startDate, "EEE MMMM d, yyyy") : ""; + const displayTo = endDate ? format(endDate, "EEE MMMM d, yyyy") : ""; return ( -
- onChangeSpecific?.("from", new Date(e.target.value))} - className="rounded-l-full border-1 border-violet-500 px-4 py-1 text-center hover:border-lion focus:outline-none dark:border-gray-400 dark:hover:border-bone" - aria-label="Start date" - /> - onChangeSpecific?.("to", new Date(e.target.value))} - className="rounded-r-full border-1 border-violet-500 px-4 py-1 text-center hover:border-lion focus:outline-none dark:border-gray-400 dark:hover:border-bone" - aria-label="End date" - /> + + {/* Start Date */} +
+ + + {displayFrom} + +
+ + TO + + {/* End Date */} +
+ + + {displayTo} + +
); } diff --git a/app/ui/components/date-range/date-range-popover.tsx b/app/ui/components/date-range/date-range-popover.tsx index 82d6b50c..1f398445 100644 --- a/app/ui/components/date-range/date-range-popover.tsx +++ b/app/ui/components/date-range/date-range-popover.tsx @@ -1,23 +1,31 @@ -"use client"; - import * as Popover from "@radix-ui/react-popover"; -import { Calendar } from "../month-calendar"; -import { format } from "date-fns"; -import { DateRangeProps } from "@/app/_types/date-range-types"; -import DateRangeInput from "./date-range-input"; +import { fromZonedTime } from "date-fns-tz"; + +import { Calendar } from "@/app/ui/components/month-calendar"; +import DateRangeInput from "@/app/ui/components/date-range/date-range-input"; + +import { DateRangeProps } from "@/app/_lib/types/date-range-props"; export default function DateRangePopover({ - specificRange, - onChangeSpecific, + eventRange, + setDateRange = () => {}, }: DateRangeProps) { + // If the event range is not specific, return null + if (eventRange.type !== "specific") { + return null; + } + + const startDate = fromZonedTime( + eventRange.dateRange.from, + eventRange.timezone, + ); + const endDate = fromZonedTime(eventRange.dateRange.to, eventRange.timezone); + return (
- +
@@ -30,17 +38,10 @@ export default function DateRangePopover({ { - if (range?.from) { - onChangeSpecific?.("from", range.from); - } - if (range?.to) { - onChangeSpecific?.("to", range.to); - } + from: startDate || undefined, + to: endDate || undefined, }} + setDateRange={setDateRange} /> diff --git a/app/ui/components/date-range/date-range-selector.tsx b/app/ui/components/date-range/date-range-selector.tsx index a1cc4ea5..661a8838 100644 --- a/app/ui/components/date-range/date-range-selector.tsx +++ b/app/ui/components/date-range/date-range-selector.tsx @@ -1,124 +1,67 @@ -import CustomSelect from "../custom-select"; -import DateRangeDrawer from "./date-range-drawer"; -import DateRangePopover from "./date-range-popover"; -import WeekdayCalendar from "../weekday-calendar"; -import { DateRangeProps } from "@/app/_types/date-range-types"; -import { WeekdayMap } from "@/app/_types/schedule-types"; +import { DateRangeProps } from "@/app/_lib/types/date-range-props"; -import useCheckMobile from "@/app/_utils/use-check-mobile"; +// Import child components +import useCheckMobile from "@/app/_lib/use-check-mobile"; +import WeekdayCalendar from "@/app/ui/components/weekday-calendar"; +import DateRangeDrawer from "@/app/ui/components/date-range/date-range-drawer"; +import DateRangePopover from "@/app/ui/components/date-range/date-range-popover"; +import EventTypeSelect from "@/app/ui/components/selectors/event-type-select"; export default function DateRangeSelector({ eventRange, - onChangeEventRange, + setEventType = () => {}, + setWeekdayRange = () => {}, + setDateRange = () => {}, }: DateRangeProps) { const isMobile = useCheckMobile(); - const rangeType = eventRange?.type ?? "specific"; - const handleRangeTypeChange = (value: string | number) => { - console.log("handleRangeTypeChange", value); - const newType = value === "specific" ? "specific" : "weekday"; - if (newType !== eventRange?.type) { - onChangeEventRange?.( - newType === "specific" - ? { - type: "specific", - duration: 60, - dateRange: { from: new Date(), to: new Date() }, - timeRange: eventRange?.timeRange ?? { from: null, to: null }, - timezone: - eventRange?.timezone ?? - Intl.DateTimeFormat().resolvedOptions().timeZone, - } - : { - type: "weekday", - duration: 60, - weekdays: { - Sun: 0, - Mon: 0, - Tue: 0, - Wed: 0, - Thu: 0, - Fri: 0, - Sat: 0, - }, - timeRange: eventRange?.timeRange ?? { from: null, to: null }, - timezone: - eventRange?.timezone ?? - Intl.DateTimeFormat().resolvedOptions().timeZone, - }, - ); - } - - console.log("handleRangeTypeChange", newType); - console.log("eventRange", eventRange); - }; - - const updateSpecificRange = (key: "from" | "to", value: Date) => { - if (eventRange?.type === "specific") { - onChangeEventRange?.({ - ...eventRange, - dateRange: { - ...eventRange.dateRange, - [key]: value, - }, - }); - } - }; - - const updateWeekdayRange = (map: WeekdayMap) => { - if (eventRange?.type === "weekday") { - onChangeEventRange?.({ ...eventRange, weekdays: map }); - } - }; - - const select = ( - - ); - if (isMobile) { return ( ); + } else { + return ( +
+
+ + +
+
+ {eventRange?.type === "specific" ? ( + <> + + + + ) : ( + + )} +
+
+ ); } - - return ( -
- {select} - {eventRange?.type === "specific" ? ( - - ) : ( - - )} -
- ); } diff --git a/app/ui/components/event-info-drawer.tsx b/app/ui/components/event-info-drawer.tsx new file mode 100644 index 00000000..5bcd8d4c --- /dev/null +++ b/app/ui/components/event-info-drawer.tsx @@ -0,0 +1,109 @@ +"use client"; + +import * as Dialog from "@radix-ui/react-dialog"; +import { InfoCircledIcon } from "@radix-ui/react-icons"; + +import { EventRange } from "@/app/_lib/schedule/types"; +import { formatLabel } from "@/app/_lib/timezone-file-generator"; + +export default function EventInfoDrawer({ + eventRange, +}: { + eventRange: EventRange; +}) { + return ( + + + + + + + + + + + + + ); +} + +export function EventInfo({ eventRange }: { eventRange: EventRange }) { + return ( +
+
+

Event Details

+

+ Please note that these details are presented in respect to the{" "} + original event's timezone which is{" "} + + {formatLabel(eventRange.timezone)} + +

+
+ +
+ {eventRange.type === "specific" ? ( + + {prettyDate(new Date(eventRange.dateRange.from!), "date")} –{" "} + {prettyDate(new Date(eventRange.dateRange.to!), "date")} + + ) : ( + + {Object.entries(eventRange.weekdays) + .filter(([_, val]) => val === 1) + .map(([day]) => day) + .join(", ")} + + )} + + + {eventRange.timeRange.from === 0 && eventRange.timeRange.to === 24 + ? "Anytime" + : `${prettyDate( + new Date(new Date().setHours(eventRange.timeRange.from, 0)), + "time", + )} - ${prettyDate( + new Date(new Date().setHours(eventRange.timeRange.to, 0)), + "time", + )}`} + + + + {eventRange.duration} minutes + +
+
+ ); +} + +function InfoRow({ + label, + children, +}: { + label: string; + children: React.ReactNode; +}) { + return ( +
+
{label}
+
{children}
+
+ ); +} + +function prettyDate(date: Date, type?: "date" | "time") { + return new Intl.DateTimeFormat("en-US", { + weekday: type === "date" ? "short" : undefined, + month: type === "date" ? "long" : undefined, + day: type === "date" ? "numeric" : undefined, + year: undefined, + hour: type === "time" ? "numeric" : undefined, + minute: type === "time" ? "numeric" : undefined, + hour12: true, + }).format(date); +} diff --git a/app/ui/components/header/account-button.tsx b/app/ui/components/header/account-button.tsx new file mode 100644 index 00000000..68116b1d --- /dev/null +++ b/app/ui/components/header/account-button.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { PersonIcon } from "@radix-ui/react-icons"; +import Link from "next/link"; +import { useContext, useEffect } from "react"; +import { LoginContext } from "@/app/_lib/providers"; +import AccountDropdown from "./account-dropdown"; + +export default function AccountButton() { + const { loggedIn, setLoggedIn } = useContext(LoginContext); + + useEffect(() => { + const checkLogin = async () => { + if (loggedIn) return; + + try { + const res = await fetch("/api/auth/check-account-auth/", { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); + if (res.ok) { + setLoggedIn(true); + } else { + setLoggedIn(false); + } + } catch (err) { + console.error("Fetch error:", err); + setLoggedIn(false); + } + }; + checkLogin(); + }, []); + + if (loggedIn) { + return ( + + + + ); + } else { + return ( + + Log In + + ); + } +} diff --git a/app/ui/components/header/account-dropdown.tsx b/app/ui/components/header/account-dropdown.tsx new file mode 100644 index 00000000..a0207cfe --- /dev/null +++ b/app/ui/components/header/account-dropdown.tsx @@ -0,0 +1,85 @@ +import { cn } from "@/app/_lib/classname"; +import formatApiError from "@/app/_utils/format-api-error"; +import { LoginContext } from "@/app/_lib/providers"; +import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; +import { DashboardIcon, ExitIcon } from "@radix-ui/react-icons"; +import { useRouter } from "next/navigation"; + +import { forwardRef, ReactNode, useContext, useRef } from "react"; + +const AccountDropdown = ({ children }: { children: ReactNode }) => { + const isSubmitting = useRef(false); + const { setLoggedIn } = useContext(LoginContext); + const router = useRouter(); + + const logout = async () => { + if (isSubmitting.current) return; + isSubmitting.current = true; + + await fetch("/api/auth/logout/", { + method: "POST", + headers: { "Content-Type": "application/json" }, + }) + .then(async (res) => { + if (res.ok) { + setLoggedIn(false); + router.push("/login"); + } else { + alert(formatApiError(await res.json())); + } + }) + .catch((err) => { + console.error("Fetch error:", err); + alert("An error occurred. Please try again."); + }); + + isSubmitting.current = false; + }; + + return ( + + {children} + + + e.preventDefault()} + > + router.push("/dashboard")}> + + Dashboard + + + + Log Out + + + + + ); +}; + +type DropdownItemProps = { + onSelect?: () => void; + children: ReactNode; +}; + +const DropdownItem = forwardRef( + ({ onSelect, children }, ref) => { + return ( + + {children} + + ); + }, +); + +export default AccountDropdown; diff --git a/app/ui/components/header/hamburger-menu.tsx b/app/ui/components/header/hamburger-menu.tsx new file mode 100644 index 00000000..b0dd7b14 --- /dev/null +++ b/app/ui/components/header/hamburger-menu.tsx @@ -0,0 +1,79 @@ +import React, { useState } from "react"; +import Link from "next/link"; + +export default function HamburgerMenu() { + const [isOpen, setIsOpen] = useState(false); + + const toggleMenu = () => setIsOpen(!isOpen); + + return ( +
+ + + {isOpen && ( +
+ {/* Vertical Line */} +
+ + {/* Dropdown Menu */} +
+
    +
  • + + Mix Your First Plan + +
  • +
  • + + Dashboard + +
  • +
  • + + About Plancake + +
  • +
  • + + Login + +
  • +
+
+
+ )} +
+ ); +} diff --git a/app/ui/components/header/header.tsx b/app/ui/components/header/header.tsx new file mode 100644 index 00000000..dd5d3ede --- /dev/null +++ b/app/ui/components/header/header.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Logo from "./logo"; +import AccountButton from "./account-button"; +import ThemeToggle from "./theme-toggle"; +import NewEventButton from "./new-event-button"; + +export default function Header() { + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) { + return null; + } + + return ( + <> + + + {/* Theme and account buttons */} +
+ + + +
+ + ); +} diff --git a/app/ui/components/header/logo.tsx b/app/ui/components/header/logo.tsx new file mode 100644 index 00000000..57763a3c --- /dev/null +++ b/app/ui/components/header/logo.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { useEffect, useState } from "react"; +import HamburgerMenu from "./hamburger-menu"; +import Link from "next/link"; + +export default function Logo() { + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) { + return null; + } + + return ( +
+ {/* Background shape */} +
+ + {/* Text Container */} +
+ +
+ plan +
+
+ cake +
+ + +
+
+ ); +} diff --git a/app/ui/components/header/new-event-button.tsx b/app/ui/components/header/new-event-button.tsx new file mode 100644 index 00000000..a45c2d44 --- /dev/null +++ b/app/ui/components/header/new-event-button.tsx @@ -0,0 +1,24 @@ +"use client"; + +import Link from "next/link"; +import { PlusIcon } from "@radix-ui/react-icons"; +import { usePathname } from "next/navigation"; + +export default function NewEventButton() { + const pathname = usePathname(); + + if (pathname === "/new-event") { + return null; + } + + return ( + + + New Event + + ); +} diff --git a/app/ui/components/header/theme-toggle.tsx b/app/ui/components/header/theme-toggle.tsx new file mode 100644 index 00000000..29dd4a8c --- /dev/null +++ b/app/ui/components/header/theme-toggle.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { FiSun, FiMoon } from "react-icons/fi"; +import { useTheme } from "next-themes"; + +export default function FixedThemeToggle() { + const { setTheme, resolvedTheme } = useTheme(); + + const toggleTheme = () => { + setTheme(resolvedTheme === "dark" ? "light" : "dark"); + }; + + return ( + + ); +} diff --git a/app/ui/components/month-calendar.tsx b/app/ui/components/month-calendar.tsx index 61e6b6ad..414ebaed 100644 --- a/app/ui/components/month-calendar.tsx +++ b/app/ui/components/month-calendar.tsx @@ -1,25 +1,20 @@ "use client"; import { useState } from "react"; -import useCheckMobile from "@/app/_utils/use-check-mobile"; +import useCheckMobile from "@/app/_lib/use-check-mobile"; -import { - DateRange, - DayPicker, - SelectRangeEventHandler, - getDefaultClassNames, -} from "react-day-picker"; +import { DateRange, DayPicker, getDefaultClassNames } from "react-day-picker"; type CalendarProps = { className?: string; selectedRange: DateRange; - onRangeSelect: (range: { from: Date | null; to: Date | null }) => void; + setDateRange: (range: DateRange | undefined) => void; }; export function Calendar({ className, selectedRange, - onRangeSelect, + setDateRange, }: CalendarProps) { const defaultClassNames = getDefaultClassNames(); @@ -31,14 +26,6 @@ export function Calendar({ const [month, setMonth] = useState(today); - const handleRangeSelect: SelectRangeEventHandler = ( - range: DateRange | undefined, - ) => { - const from = range?.from || null; - const to = range?.to || null; - onRangeSelect({ from, to }); - }; - return (
{/* - )} - {currentPage < totalPages - 1 && ( - - )} - - {/* Grid */} -
- {Array.from({ - length: (numHours + 1) * (visibleDays.length + 2) + 1, - }).map((_, i) => ( -
- ))} -
- - {/* Time labels */} -
-
- {Array.from({ length: numHours }).map((_, i) => { - const hour = timeRange.from - ? new Date(timeRange.from.getTime() + i * 3600000) - : new Date(); // Fallback to current time or handle appropriately - const formatter = new Intl.DateTimeFormat("en-US", { - timeZone: timezone, - hour: "numeric", - hour12: true, +
+ setCurrentPage((p) => Math.max(p - 1, 0))} + onNextPage={() => + setCurrentPage((p) => Math.min(p + 1, totalPages - 1)) + } + /> + +
+ {timeBlocks.map((block, i) => { + // filter visibleTimeSlots to those within this block's hours + const blockTimeSlots = visibleTimeSlots.filter((slot) => { + const localSlot = toZonedTime(slot, timezone); + const hour = localSlot.getHours(); + return hour >= block.startHour && hour <= block.endHour; }); - return ( -
- {formatter.format(hour)} -
- ); - })} -
- {/* Column headers */} -
- {visibleDays.map((day, dayIndex) => { - const type = eventRange.type; - - if (type === "specific") { - // split the day string into date and month - const [weekday, month, date] = day.split(" "); + const numQuarterHours = (block.endHour - block.startHour + 1) * 4; + + if (mode === "preview") { + return ( + d.dayKey)} + userTimezone={timezone} + /> + ); + } else if (mode === "paint") { return ( -
-
{weekday}
-
- {month} {date} -
-
+ d.dayKey)} + userTimezone={timezone} + availability={userAvailability} + onToggle={onToggleSlot} + /> ); - } else if (type === "weekday") { + } else if (mode === "view") { return ( -
- {day.toUpperCase()} -
+ d.dayKey)} + userTimezone={timezone} + hoveredSlot={hoveredSlot} + allAvailabilities={attendees.map((a) => a.availability)} + onHoverSlot={setHoveredSlot} + /> ); } })}
- - {/* Right border */} -
); } -const GridError = ({ message }: { message: string }) => { - return ( -
- - {message} -
- ); -}; +const GridError = ({ message }: { message: string }) => ( +
+ + {message} +
+); diff --git a/app/ui/components/schedule/schedule-header.tsx b/app/ui/components/schedule/schedule-header.tsx new file mode 100644 index 00000000..c5fb50a0 --- /dev/null +++ b/app/ui/components/schedule/schedule-header.tsx @@ -0,0 +1,73 @@ +import { cn } from "@/app/_lib/classname"; +import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons"; + +interface Day { + dayKey: string; + dayLabel: string; +} + +interface ScheduleHeaderProps { + preview?: boolean; + visibleDays: Day[]; + currentPage: number; + totalPages: number; + onPrevPage: () => void; + onNextPage: () => void; +} + +export default function ScheduleHeader({ + preview = false, + visibleDays, + currentPage, + totalPages, + onPrevPage, + onNextPage, +}: ScheduleHeaderProps) { + return ( +
+ {currentPage > 0 ? ( + + ) : ( +
+ )} + + {visibleDays.map((day, i) => { + const [weekday, month, date] = day.dayLabel.split(" "); + return ( +
+
{weekday}
+
+ {month} {date} +
+
+ ); + })} + + {currentPage < totalPages - 1 ? ( + + ) : ( +
+ )} +
+ ); +} diff --git a/app/ui/components/schedule/time-slot.tsx b/app/ui/components/schedule/time-slot.tsx new file mode 100644 index 00000000..15ba58b1 --- /dev/null +++ b/app/ui/components/schedule/time-slot.tsx @@ -0,0 +1,67 @@ +"use client"; + +import React, { memo } from "react"; +import { cn } from "@/app/_lib/classname"; + +interface TimeSlotProps { + slotIso: string; + isSelected?: boolean; + isHovered?: boolean; + + disableSelect?: boolean; + backgroundColor: string; + gridColumn: number; + gridRow: number; + + cellClasses?: string; + + // Event handlers + onMouseDown?: () => void; + onMouseEnter?: () => void; + onTouchStart?: (e: React.TouchEvent) => void; + onTouchMove?: (e: React.TouchEvent) => void; +} + +function TimeSlot({ + slotIso, + isSelected, + isHovered, + disableSelect, + backgroundColor, + gridColumn, + gridRow, + cellClasses = "", + ...eventHandlers +}: TimeSlotProps) { + return ( +
+
+
+ ); +} + +export default memo(TimeSlot); diff --git a/app/ui/components/schedule/timeblocks/base-timeblock.tsx b/app/ui/components/schedule/timeblocks/base-timeblock.tsx new file mode 100644 index 00000000..36d9d3eb --- /dev/null +++ b/app/ui/components/schedule/timeblocks/base-timeblock.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { useMemo } from "react"; + +interface TimeBlockProps { + timeColWidth: number; + numQuarterHours: number; + startHour: number; + visibleDaysCount: number; + children: React.ReactNode; +} + +export default function BaseTimeBlock({ + timeColWidth, + numQuarterHours, + startHour, + visibleDaysCount, + children, +}: TimeBlockProps) { + // generate hour labels for the time column + const hoursLabel = useMemo(() => { + return Array.from({ length: numQuarterHours }, (_, i) => { + const hour24 = startHour + Math.floor(i / 4); + const hour12 = hour24 % 12 || 12; + const period = hour24 < 12 ? "AM" : "PM"; + return `${hour12} ${period}`; + }); + }, [startHour, numQuarterHours]); + + return ( +
+ {/* time labels */} +
+ {Array.from({ length: numQuarterHours }).map((_, i) => + i % 4 === 0 ? ( +
+ {hoursLabel[i]} +
+ ) : ( +
+ ), + )} +
+ +
+ {children} +
+
+ ); +} diff --git a/app/ui/components/schedule/timeblocks/interactive-timeblock.tsx b/app/ui/components/schedule/timeblocks/interactive-timeblock.tsx new file mode 100644 index 00000000..2974af6c --- /dev/null +++ b/app/ui/components/schedule/timeblocks/interactive-timeblock.tsx @@ -0,0 +1,103 @@ +import { AvailabilitySet } from "@/app/_lib/availability/types"; + +import useScheduleDrag from "@/app/_lib/use-schedule-drag"; +import BaseTimeBlock from "./base-timeblock"; + +import { toZonedTime } from "date-fns-tz"; +import { useTheme } from "next-themes"; +import TimeSlot from "../time-slot"; + +interface InteractiveTimeBlockProps { + timeColWidth: number; + numQuarterHours: number; + startHour: number; + timeslots: Date[]; + numVisibleDays: number; + visibleDayKeys: string[]; + + userTimezone: string; + availability: AvailabilitySet; + onToggle: (slotIso: string) => void; +} + +export default function InteractiveTimeBlock({ + timeColWidth, + numQuarterHours, + startHour, + timeslots, + numVisibleDays, + visibleDayKeys, + userTimezone, + availability, + onToggle, +}: InteractiveTimeBlockProps) { + const { resolvedTheme } = useTheme(); + const isDark = resolvedTheme === "dark"; + + const dragHandlers = useScheduleDrag(onToggle, "paint"); + + return ( + + {timeslots.map((timeslot, timeslotIdx) => { + const slotIso = timeslot.toISOString(); + const localSlot = toZonedTime(timeslot, userTimezone); + + const currentDayKey = localSlot.toLocaleDateString("en-CA"); + const dayIndex = visibleDayKeys.indexOf(currentDayKey); + if (dayIndex === -1) return null; + + const gridColumn = dayIndex + 1; + const gridRow = + (localSlot.getHours() - startHour) * 4 + + Math.floor(localSlot.getMinutes() / 15) + + 1; + + // borders + const cellClasses: string[] = []; + if (gridRow < numQuarterHours) { + cellClasses.push("border-b"); + + if (gridRow % 4 === 0) { + cellClasses.push("border-solid border-gray-400"); + } else { + cellClasses.push("border-dashed border-gray-400"); + } + } + + const isSelected = availability.has(slotIso); + + let backgroundColor; + if (isSelected) { + backgroundColor = isDark + ? "rgba(225, 92, 92, 1)" + : "rgba(61, 115, 163, 1)"; + } else { + backgroundColor = ""; + } + + return ( + dragHandlers.onMouseDown(slotIso, false)} + onMouseEnter={() => { + dragHandlers.onMouseEnter(slotIso, false); + }} + onTouchStart={() => dragHandlers.onTouchStart(slotIso, false)} + onTouchMove={dragHandlers.onTouchMove} + /> + ); + })} + + ); +} diff --git a/app/ui/components/schedule/timeblocks/preview-timeblock.tsx b/app/ui/components/schedule/timeblocks/preview-timeblock.tsx new file mode 100644 index 00000000..2dcbef46 --- /dev/null +++ b/app/ui/components/schedule/timeblocks/preview-timeblock.tsx @@ -0,0 +1,78 @@ +import BaseTimeBlock from "./base-timeblock"; + +import { toZonedTime } from "date-fns-tz"; +import { useTheme } from "next-themes"; +import TimeSlot from "../time-slot"; + +interface PreviewTimeBlockProps { + timeColWidth: number; + numQuarterHours: number; + startHour: number; + timeslots: Date[]; + numVisibleDays: number; + visibleDayKeys: string[]; + + userTimezone: string; +} + +export default function PreviewTimeBlock({ + timeColWidth, + numQuarterHours, + startHour, + timeslots, + numVisibleDays, + visibleDayKeys, + userTimezone, +}: PreviewTimeBlockProps) { + const { resolvedTheme } = useTheme(); + const isDark = resolvedTheme === "dark"; + + return ( + + {timeslots.map((timeslot, timeslotIdx) => { + const slotIso = timeslot.toISOString(); + const localSlot = toZonedTime(timeslot, userTimezone); + + const currentDayKey = localSlot.toLocaleDateString("en-CA"); + const dayIndex = visibleDayKeys.indexOf(currentDayKey); + if (dayIndex === -1) return null; + + const gridColumn = dayIndex + 1; + const gridRow = + (localSlot.getHours() - startHour) * 4 + + Math.floor(localSlot.getMinutes() / 15) + + 1; + + // borders + const cellClasses: string[] = []; + if (gridRow < numQuarterHours) { + cellClasses.push("border-b"); + + if (gridRow % 4 === 0) { + cellClasses.push("border-solid border-gray-400"); + } else { + cellClasses.push("border-dashed border-gray-400"); + } + } + + let backgroundColor = ""; + return ( + + ); + })} + + ); +} diff --git a/app/ui/components/schedule/timeblocks/results-timeblock.tsx b/app/ui/components/schedule/timeblocks/results-timeblock.tsx new file mode 100644 index 00000000..56876658 --- /dev/null +++ b/app/ui/components/schedule/timeblocks/results-timeblock.tsx @@ -0,0 +1,103 @@ +import { AvailabilitySet } from "@/app/_lib/availability/types"; + +import BaseTimeBlock from "./base-timeblock"; + +import { toZonedTime } from "date-fns-tz"; +import { useTheme } from "next-themes"; +import TimeSlot from "../time-slot"; + +interface ResultsTimeBlockProps { + timeColWidth: number; + numQuarterHours: number; + startHour: number; + timeslots: Date[]; + numVisibleDays: number; + visibleDayKeys: string[]; + hoveredSlot: string | null | undefined; + + allAvailabilities?: AvailabilitySet[]; + + userTimezone: string; + onHoverSlot?: (iso: string | null) => void; +} + +export default function ResultsTimeBlock({ + timeColWidth, + numQuarterHours, + startHour, + timeslots, + numVisibleDays, + visibleDayKeys, + userTimezone, + allAvailabilities = [], + hoveredSlot, + onHoverSlot, +}: ResultsTimeBlockProps) { + const { resolvedTheme } = useTheme(); + const isDark = resolvedTheme === "dark"; + + return ( + + {timeslots.map((timeslot, timeslotIdx) => { + const slotIso = timeslot.toISOString(); + const localSlot = toZonedTime(timeslot, userTimezone); + + const currentDayKey = localSlot.toLocaleDateString("en-CA"); + const dayIndex = visibleDayKeys.indexOf(currentDayKey); + if (dayIndex === -1) return null; + + const gridColumn = dayIndex + 1; + const gridRow = + (localSlot.getHours() - startHour) * 4 + + Math.floor(localSlot.getMinutes() / 15) + + 1; + + // borders + const cellClasses: string[] = []; + if (gridRow < numQuarterHours) { + cellClasses.push("border-b"); + + if (gridRow % 4 === 0) { + cellClasses.push("border-solid border-gray-400"); + } else { + cellClasses.push("border-dashed border-gray-400"); + } + } + + // REPLACE THIS WITH API DATA !!!!!!!!! + const matchCount = allAvailabilities.reduce( + (acc, set) => acc + (set.has(slotIso) ? 1 : 0), + 0, + ); + const total = allAvailabilities.length || 1; + const opacity = matchCount / total; + const isHovered = hoveredSlot === slotIso; + + let backgroundColor; + backgroundColor = isDark + ? `rgba(225, 92, 92, ${opacity})` + : `rgba(61, 115, 163, ${opacity})`; + + return ( + { + onHoverSlot?.(slotIso); + }} + /> + ); + })} + + ); +} diff --git a/app/ui/components/selectors/custom-select.tsx b/app/ui/components/selectors/custom-select.tsx new file mode 100644 index 00000000..64422bc3 --- /dev/null +++ b/app/ui/components/selectors/custom-select.tsx @@ -0,0 +1,86 @@ +import { forwardRef } from "react"; +import * as Select from "@radix-ui/react-select"; +import { CheckIcon, ChevronDownIcon } from "@radix-ui/react-icons"; +import { cn } from "@/app/_lib/classname"; + +// --- Simplified Types --- +type Option = { + label: string; + value: string | number; +}; + +type CustomSelectProps = { + value: string | number; + options: Option[]; + onValueChange: (value: string | number) => void; + placeholder?: string; + className?: string; +}; + +// --- Refactored Component --- +export default function CustomSelect({ + value, + options, + onValueChange, + placeholder, + className, +}: CustomSelectProps) { + return ( + onValueChange(isNaN(Number(v)) ? v : Number(v))} + > + + + + + + + + + + + + + {options.map((option) => ( + + {option.label} + + ))} + + + + + ); +} + +// --- SelectItem (Unchanged) --- +type SelectItemProps = { + value: string | number; + children: React.ReactNode; +}; + +const SelectItem = forwardRef( + ({ children, value }, ref) => { + return ( + + {children} + + + + + ); + }, +); +SelectItem.displayName = "SelectItem"; diff --git a/app/ui/components/selectors/event-type-select.tsx b/app/ui/components/selectors/event-type-select.tsx new file mode 100644 index 00000000..ce45026e --- /dev/null +++ b/app/ui/components/selectors/event-type-select.tsx @@ -0,0 +1,25 @@ +import CustomSelect from "./custom-select"; + +type EventTypeSelectProps = { + eventType: string; + onEventTypeChange: (type: "specific" | "weekday") => void; +}; + +export default function EventTypeSelect({ + eventType, + onEventTypeChange, +}: EventTypeSelectProps) { + return ( + + onEventTypeChange?.(value === "specific" ? "specific" : "weekday") + } + className="min-h-9 min-w-[100px] border-none pt-2" + /> + ); +} diff --git a/app/ui/components/selectors/timezone-select.tsx b/app/ui/components/selectors/timezone-select.tsx new file mode 100644 index 00000000..8d892d2e --- /dev/null +++ b/app/ui/components/selectors/timezone-select.tsx @@ -0,0 +1,33 @@ +import CustomSelect from "@/app/ui/components/selectors/custom-select"; +import { useTimezoneSelect, allTimezones } from "react-timezone-select"; + +type TimezoneSelectProps = { + value: string; + onChange: (tz: string) => void; + className?: string; +}; + +const labelStyle = "original"; +const timezones = allTimezones; + +export default function TimezoneSelect({ + value, + className, + onChange, +}: TimezoneSelectProps) { + const { options, parseTimezone } = useTimezoneSelect({ + labelStyle, + timezones, + }); + + return ( +
+ onChange(String(v))} + className="w-full" + /> +
+ ); +} diff --git a/app/ui/components/time-dropdown.tsx b/app/ui/components/time-dropdown.tsx index b5c197e9..0e63490e 100644 --- a/app/ui/components/time-dropdown.tsx +++ b/app/ui/components/time-dropdown.tsx @@ -1,87 +1,32 @@ -import CustomSelect from "./custom-select"; -import { fromZonedTime } from "date-fns-tz"; +import CustomSelect from "@/app/ui/components/selectors/custom-select"; type TimeDropdownProps = { - onChange: (time: Date) => void; - value: Date | null | undefined; + onChange: (time: number) => void; + value: number; defaultTZ: string; duration: number; }; -export default function TimeDropdown({ - onChange, - value, - defaultTZ, - duration, -}: TimeDropdownProps) { - const options = Array.from({ length: 24 }, (_, i) => { +export default function TimeDropdown({ onChange, value }: TimeDropdownProps) { + let options = Array.from({ length: 24 }, (_, i) => { const hour = i % 12 === 0 ? 12 : i % 12; const period = i < 12 ? "am" : "pm"; return { label: `${hour}:00 ${period}`, value: i }; }); + options.push({ label: "12:00 am", value: 24 }); + const handleValueChange = (selectedValue: string | number) => { const hour = Number(selectedValue); - - const zonedSelectedTime = new Date(); - zonedSelectedTime.setHours(hour, 0, 0, 0); - onChange(fromZonedTime(zonedSelectedTime, defaultTZ)); + onChange(hour); }; - // Format current `value` to match an option like "2 pm" - const formattedValue = (() => { - if (!value) return ""; - const hour = value.getHours(); - return hour; - // const displayHour = hour % 12 === 0 ? 12 : hour % 12; - // const period = hour < 12 ? "am" : "pm"; - // return `${displayHour} ${period}`; - })(); - return ( ); } - -// const options = Array.from({ length: Math.ceil(1440 / duration) }, (_, i) => { -// const totalMinutes = i * duration; -// const hours = Math.floor(totalMinutes / 60); -// const minutes = totalMinutes % 60; -// const hour = hours % 12 === 0 ? 12 : hours % 12; -// const period = hours < 12 ? "am" : "pm"; -// const label = `${hour}:${minutes.toString().padStart(2, "0")} ${period}`; -// return { label, value: totalMinutes }; -// }); - -// const dateToMinutes = (date: Date, duration: number) => { -// const totalMinutes = date.getHours() * 60 + date.getMinutes(); -// return Math.ceil(totalMinutes / duration) * duration; -// }; - -// const handleValueChange = (selectedValue: string | number) => { -// const totalMinutes = Number(selectedValue); -// const hour = Math.floor(totalMinutes / 60); -// const minutes = totalMinutes % 60; - -// const zonedSelectedTime = new Date(); -// zonedSelectedTime.setHours(hour, minutes, 0, 0); -// onChange(fromZonedTime(zonedSelectedTime, defaultTZ)); -// }; - -// return ( -//
-//
-// -//
-//
-// ); -// } diff --git a/app/ui/components/timezone-select.tsx b/app/ui/components/timezone-select.tsx deleted file mode 100644 index 14a01643..00000000 --- a/app/ui/components/timezone-select.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { groupedTimezones } from "@/app/_lib/grouped-timezones"; -import CustomSelect from "./custom-select"; - -type TimezoneSelectProps = { - label?: string; - value: string; - onChange: (tz: string | number) => void; - className?: string; -}; - -export default function TimezoneSelect({ - label = "Event Timezone", - value, - onChange, - className, -}: TimezoneSelectProps) { - return ( -
- - -
- ); -} diff --git a/app/ui/components/weekday-calendar.tsx b/app/ui/components/weekday-calendar.tsx index c4b59122..52a5eed3 100644 --- a/app/ui/components/weekday-calendar.tsx +++ b/app/ui/components/weekday-calendar.tsx @@ -1,9 +1,10 @@ "use client"; import { useState, useEffect } from "react"; -import Checkbox from "./checkbox"; +import { cn } from "@/app/_lib/classname"; -import { WeekdayMap } from "@/app/_types/schedule-types"; +import { WeekdayMap } from "@/app/_lib/schedule/types"; +import Checkbox from "@/app/ui/components/checkbox"; type WeekdayCalendarProps = { selectedDays: WeekdayMap; @@ -42,23 +43,28 @@ export default function WeekdayCalendar({ return (
-
- {days.map((day) => ( - - ))} +
+ {days.map((day) => { + const isSelected = selectedDays[day] === 1; + return ( + + ); + })}
- {/* - -
- Tomeeto Logo - Tomeeto Logo -
-
- -
-
- ); -} diff --git a/app/ui/layout/message-page.tsx b/app/ui/layout/message-page.tsx new file mode 100644 index 00000000..30f54dc2 --- /dev/null +++ b/app/ui/layout/message-page.tsx @@ -0,0 +1,42 @@ +enum ButtonTypeEnum { + primary = "primary", + secondary = "secondary", +} +type ButtonType = keyof typeof ButtonTypeEnum; + +type ButtonData = { type: ButtonType; label: string; onClick: () => void }; + +type MessagePageProps = { + title: string; + description?: string; + buttons: ButtonData[]; +}; + +export default function MessagePage({ + title, + description, + buttons, +}: MessagePageProps) { + return ( +
+

{title}

+ {description &&

{description}

} +
+ {buttons.map((button, index) => ( + + ))} +
+
+ ); +} diff --git a/app/ui/layout/theme-toggle.tsx b/app/ui/layout/theme-toggle.tsx deleted file mode 100644 index b8c0a95f..00000000 --- a/app/ui/layout/theme-toggle.tsx +++ /dev/null @@ -1,34 +0,0 @@ -"use client"; - -import { FiSun, FiMoon } from "react-icons/fi"; -import { useState, useEffect } from "react"; -import { useTheme } from "next-themes"; -import Image from "next/image"; - -export default function ToggleDarkMode() { - const [mounted, setMounted] = useState(false); - const { setTheme, resolvedTheme } = useTheme(); - - useEffect(() => setMounted(true), []); - - if (!mounted) - return ( - Loading Light/Dark Toggle - ); - - if (resolvedTheme === "dark") { - return setTheme("light")} />; - } - - if (resolvedTheme === "light") { - return setTheme("dark")} />; - } -} diff --git a/app/verify-email/page.tsx b/app/verify-email/page.tsx new file mode 100644 index 00000000..e599c33d --- /dev/null +++ b/app/verify-email/page.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { useRouter, useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import formatApiError from "../_utils/format-api-error"; +import MessagePage from "../ui/layout/message-page"; + +export default function Page() { + const [verifying, setVerifying] = useState(true); + const [emailVerified, setEmailVerified] = useState(false); + const router = useRouter(); + + const searchParams = useSearchParams(); + const token = searchParams.get("code"); + + useEffect(() => { + const verifyEmail = async () => { + if (!token) { + setVerifying(false); + setEmailVerified(false); + return; + } + + await fetch("/api/auth/verify-email/", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ verification_code: token }), + }) + .then(async (res) => { + if (res.ok) { + setEmailVerified(true); + } + }) + .catch((err) => { + console.error("Fetch error:", err); + alert("An error occurred. Please try again."); + }); + + setVerifying(false); + }; + + verifyEmail(); + }, [token]); + + return ( +
+ {verifying ? ( +
+

Verifying...

+
+ ) : emailVerified ? ( + router.push("/login"), + }, + ]} + /> + ) : ( + router.push("/sign-up"), + }, + ]} + /> + )} +
+ ); +} diff --git a/example.env b/example.env new file mode 100644 index 00000000..d5f10262 --- /dev/null +++ b/example.env @@ -0,0 +1 @@ +NEXT_PUBLIC_API_URL=https://example.com diff --git a/next.config.ts b/next.config.ts index 28f40ec5..a9f37824 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,6 +2,16 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { allowedDevOrigins: ["129.161.139.75"], + + // Redirect API calls to the backend server for CORS stuff + async rewrites() { + return [ + { + source: "/api/:path*", + destination: `${process.env.NEXT_PUBLIC_API_URL}/:path*/`, + }, + ]; + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 73947a84..b5ca3bd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,19 +9,23 @@ "version": "0.1.0", "dependencies": { "@radix-ui/react-dialog": "^1.1.13", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-popover": "^1.1.13", "@radix-ui/react-select": "^2.2.4", + "@radix-ui/react-toast": "^1.2.14", "clsx": "^2.1.1", "date-fns-tz": "^3.2.0", "framer-motion": "^12.11.4", - "next": "15.3.1", + "next": "^15.5.4", "next-themes": "^0.4.6", "postcss": "^8.5.3", "react": "^19.0.0", "react-day-picker": "^9.6.7", "react-dom": "^19.0.0", "react-icons": "^5.5.0", + "react-select": "^5.10.2", + "react-timezone-select": "^3.2.8", "tailwind-merge": "^3.2.0", "vaul": "^1.1.2" }, @@ -51,6 +55,134 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@date-fns/tz": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz", @@ -68,9 +200,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", - "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", "optional": true, "dependencies": { "tslib": "^2.4.0" @@ -86,10 +218,113 @@ "tslib": "^2.4.0" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==" + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==" + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" + }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", - "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -126,9 +361,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "dependencies": { "@eslint/object-schema": "^2.1.6", @@ -140,18 +375,18 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", - "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.15" @@ -184,12 +419,15 @@ } }, "node_modules/@eslint/js": { - "version": "9.25.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.1.tgz", - "integrity": "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==", + "version": "9.36.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.36.0.tgz", + "integrity": "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { @@ -202,12 +440,12 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", - "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "dependencies": { - "@eslint/core": "^0.13.0", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { @@ -309,10 +547,19 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "optional": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.1.tgz", - "integrity": "sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz", + "integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==", "cpu": [ "arm64" ], @@ -327,13 +574,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.1.0" + "@img/sharp-libvips-darwin-arm64": "1.2.3" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.1.tgz", - "integrity": "sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz", + "integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==", "cpu": [ "x64" ], @@ -348,13 +595,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.1.0" + "@img/sharp-libvips-darwin-x64": "1.2.3" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz", - "integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==", "cpu": [ "arm64" ], @@ -367,9 +614,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz", - "integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==", "cpu": [ "x64" ], @@ -382,9 +629,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz", - "integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz", + "integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==", "cpu": [ "arm" ], @@ -397,9 +644,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz", - "integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz", + "integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==", "cpu": [ "arm64" ], @@ -412,9 +659,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz", - "integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz", + "integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==", "cpu": [ "ppc64" ], @@ -427,9 +674,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz", - "integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz", + "integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==", "cpu": [ "s390x" ], @@ -442,9 +689,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz", - "integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz", + "integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==", "cpu": [ "x64" ], @@ -457,9 +704,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz", - "integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz", + "integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==", "cpu": [ "arm64" ], @@ -472,9 +719,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz", - "integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz", + "integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==", "cpu": [ "x64" ], @@ -487,9 +734,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.1.tgz", - "integrity": "sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz", + "integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==", "cpu": [ "arm" ], @@ -504,13 +751,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.1.0" + "@img/sharp-libvips-linux-arm": "1.2.3" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.1.tgz", - "integrity": "sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz", + "integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==", "cpu": [ "arm64" ], @@ -525,13 +772,34 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.1.0" + "@img/sharp-libvips-linux-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz", + "integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.3" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.1.tgz", - "integrity": "sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz", + "integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==", "cpu": [ "s390x" ], @@ -546,13 +814,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.1.0" + "@img/sharp-libvips-linux-s390x": "1.2.3" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.1.tgz", - "integrity": "sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz", + "integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==", "cpu": [ "x64" ], @@ -567,13 +835,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.1.0" + "@img/sharp-libvips-linux-x64": "1.2.3" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.1.tgz", - "integrity": "sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz", + "integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==", "cpu": [ "arm64" ], @@ -588,13 +856,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.1.tgz", - "integrity": "sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz", + "integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==", "cpu": [ "x64" ], @@ -609,20 +877,38 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.1.0" + "@img/sharp-libvips-linuxmusl-x64": "1.2.3" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.1.tgz", - "integrity": "sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz", + "integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==", "cpu": [ "wasm32" ], "optional": true, "dependencies": { - "@emnapi/runtime": "^1.4.0" + "@emnapi/runtime": "^1.5.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz", + "integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -631,9 +917,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.1.tgz", - "integrity": "sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz", + "integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==", "cpu": [ "ia32" ], @@ -649,9 +935,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.1.tgz", - "integrity": "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz", + "integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==", "cpu": [ "x64" ], @@ -666,6 +952,37 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.9.tgz", @@ -679,9 +996,9 @@ } }, "node_modules/@next/env": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.1.tgz", - "integrity": "sha512-cwK27QdzrMblHSn9DZRV+DQscHXRuJv6MydlJRpFSqJWZrTYMLzKDeyueJNN9MGd8NNiUKzDQADAf+dMLXX7YQ==" + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.4.tgz", + "integrity": "sha512-27SQhYp5QryzIT5uO8hq99C69eLQ7qkzkDPsk3N+GuS2XgOgoYEeOav7Pf8Tn4drECOVDsDg8oj+/DVy8qQL2A==" }, "node_modules/@next/eslint-plugin-next": { "version": "15.3.1", @@ -693,9 +1010,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.1.tgz", - "integrity": "sha512-hjDw4f4/nla+6wysBL07z52Gs55Gttp5Bsk5/8AncQLJoisvTBP0pRIBK/B16/KqQyH+uN4Ww8KkcAqJODYH3w==", + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.4.tgz", + "integrity": "sha512-nopqz+Ov6uvorej8ndRX6HlxCYWCO3AHLfKK2TYvxoSB2scETOcfm/HSS3piPqc3A+MUgyHoqE6je4wnkjfrOA==", "cpu": [ "arm64" ], @@ -708,9 +1025,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.1.tgz", - "integrity": "sha512-q+aw+cJ2ooVYdCEqZVk+T4Ni10jF6Fo5DfpEV51OupMaV5XL6pf3GCzrk6kSSZBsMKZtVC1Zm/xaNBFpA6bJ2g==", + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.4.tgz", + "integrity": "sha512-QOTCFq8b09ghfjRJKfb68kU9k2K+2wsC4A67psOiMn849K9ZXgCSRQr0oVHfmKnoqCbEmQWG1f2h1T2vtJJ9mA==", "cpu": [ "x64" ], @@ -723,9 +1040,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.1.tgz", - "integrity": "sha512-wBQ+jGUI3N0QZyWmmvRHjXjTWFy8o+zPFLSOyAyGFI94oJi+kK/LIZFJXeykvgXUk1NLDAEFDZw/NVINhdk9FQ==", + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.4.tgz", + "integrity": "sha512-eRD5zkts6jS3VfE/J0Kt1VxdFqTnMc3QgO5lFE5GKN3KDI/uUpSyK3CjQHmfEkYR4wCOl0R0XrsjpxfWEA++XA==", "cpu": [ "arm64" ], @@ -738,9 +1055,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.1.tgz", - "integrity": "sha512-IIxXEXRti/AulO9lWRHiCpUUR8AR/ZYLPALgiIg/9ENzMzLn3l0NSxVdva7R/VDcuSEBo0eGVCe3evSIHNz0Hg==", + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.4.tgz", + "integrity": "sha512-TOK7iTxmXFc45UrtKqWdZ1shfxuL4tnVAOuuJK4S88rX3oyVV4ZkLjtMT85wQkfBrOOvU55aLty+MV8xmcJR8A==", "cpu": [ "arm64" ], @@ -753,9 +1070,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.1.tgz", - "integrity": "sha512-bfI4AMhySJbyXQIKH5rmLJ5/BP7bPwuxauTvVEiJ/ADoddaA9fgyNNCcsbu9SlqfHDoZmfI6g2EjzLwbsVTr5A==", + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.4.tgz", + "integrity": "sha512-7HKolaj+481FSW/5lL0BcTkA4Ueam9SPYWyN/ib/WGAFZf0DGAN8frNpNZYFHtM4ZstrHZS3LY3vrwlIQfsiMA==", "cpu": [ "x64" ], @@ -768,9 +1085,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.1.tgz", - "integrity": "sha512-FeAbR7FYMWR+Z+M5iSGytVryKHiAsc0x3Nc3J+FD5NVbD5Mqz7fTSy8CYliXinn7T26nDMbpExRUI/4ekTvoiA==", + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.4.tgz", + "integrity": "sha512-nlQQ6nfgN0nCO/KuyEUwwOdwQIGjOs4WNMjEUtpIQJPR2NUfmGpW2wkJln1d4nJ7oUzd1g4GivH5GoEPBgfsdw==", "cpu": [ "x64" ], @@ -783,9 +1100,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.1.tgz", - "integrity": "sha512-yP7FueWjphQEPpJQ2oKmshk/ppOt+0/bB8JC8svPUZNy0Pi3KbPx2Llkzv1p8CoQa+D2wknINlJpHf3vtChVBw==", + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.4.tgz", + "integrity": "sha512-PcR2bN7FlM32XM6eumklmyWLLbu2vs+D7nJX8OAIoWy69Kef8mfiN4e8TUv2KohprwifdpFKPzIP1njuCjD0YA==", "cpu": [ "arm64" ], @@ -798,9 +1115,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.1.tgz", - "integrity": "sha512-3PMvF2zRJAifcRNni9uMk/gulWfWS+qVI/pagd+4yLF5bcXPZPPH2xlYRYOsUjmCJOXSTAC2PjRzbhsRzR2fDQ==", + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.4.tgz", + "integrity": "sha512-1ur2tSHZj8Px/KMAthmuI9FMp/YFusMMGoRNJaRZMOlSkgvLjzosSdQI0cJAKogdHl3qXUQKL9MGaYvKwA7DXg==", "cpu": [ "x64" ], @@ -1016,6 +1333,82 @@ } } }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "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-dropdown-menu/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-dropdown-menu/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-dropdown-menu/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-focus-guards": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", @@ -1067,7 +1460,588 @@ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@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-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.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-menu/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-menu/node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.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-menu/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-menu/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "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-menu/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "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-menu/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "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-menu/node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@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-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "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-menu/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "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-menu/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "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-menu/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-menu/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-popover": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.13.tgz", + "integrity": "sha512-84uqQV3omKDR076izYgcha6gdpN8m3z6w/AeJ83MSBJYVG/AbOHdLjAgsPZkeC/kt+k64moXFCnio8BbqXszlw==", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.9", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.6", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.6", + "@radix-ui/react-portal": "1.1.8", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-slot": "1.2.2", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.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-popper": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.6.tgz", + "integrity": "sha512-7iqXaOWIjDBfIG7aq8CUEeCSsQMLFdn7VEE8TaFz704DtEzpPHR7w/uuzRflvKgltqSAImgcmxQ7fFX3X7wasg==", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.6", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "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-portal": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.8.tgz", + "integrity": "sha512-hQsTUIn7p7fxCPvao/q6wpbxmCwgLrlz+nOrJgC+RwfZqWY/WN+UMqkXzrtKbPrF82P43eCTl3ekeKuyAQbFeg==", + "dependencies": { + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-layout-effect": "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-presence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "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-primitive": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz", + "integrity": "sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==", + "dependencies": { + "@radix-ui/react-slot": "1.2.2" + }, + "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-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@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-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "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-roving-focus/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-roving-focus/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-roving-focus/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-roving-focus/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-select": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.4.tgz", + "integrity": "sha512-/OOm58Gil4Ev5zT8LyVzqfBcij4dTHYdeyuF5lMHZ2bIp0Lk9oETocYiJ5QC0dHekEQnK6L/FNJCceeb4AkZ6Q==", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.6", + "@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-dismissable-layer": "1.1.9", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.6", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.6", + "@radix-ui/react-portal": "1.1.8", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-slot": "1.2.2", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@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-visually-hidden": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.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-slot": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", + "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", @@ -1079,26 +2053,23 @@ } } }, - "node_modules/@radix-ui/react-popover": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.13.tgz", - "integrity": "sha512-84uqQV3omKDR076izYgcha6gdpN8m3z6w/AeJ83MSBJYVG/AbOHdLjAgsPZkeC/kt+k64moXFCnio8BbqXszlw==", + "node_modules/@radix-ui/react-toast": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.14.tgz", + "integrity": "sha512-nAP5FBxBJGQ/YfUB+r+O6USFVkWq3gAInkxyEnmvEV5jtSbfDhfa4hwX8CraCnbjMLsE7XSf/K75l9xXY7joWg==", "dependencies": { "@radix-ui/primitive": "1.1.2", + "@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-dismissable-layer": "1.1.9", - "@radix-ui/react-focus-guards": "1.1.2", - "@radix-ui/react-focus-scope": "1.1.6", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.6", - "@radix-ui/react-portal": "1.1.8", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-slot": "1.2.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -1115,21 +2086,15 @@ } } }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.6.tgz", - "integrity": "sha512-7iqXaOWIjDBfIG7aq8CUEeCSsQMLFdn7VEE8TaFz704DtEzpPHR7w/uuzRflvKgltqSAImgcmxQ7fFX3X7wasg==", + "node_modules/@radix-ui/react-toast/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==", "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.6", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -1146,13 +2111,16 @@ } } }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.8.tgz", - "integrity": "sha512-hQsTUIn7p7fxCPvao/q6wpbxmCwgLrlz+nOrJgC+RwfZqWY/WN+UMqkXzrtKbPrF82P43eCTl3ekeKuyAQbFeg==", + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", + "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", "dependencies": { - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -1169,12 +2137,12 @@ } } }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", - "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { @@ -1192,12 +2160,12 @@ } } }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz", - "integrity": "sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==", + "node_modules/@radix-ui/react-toast/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==", "dependencies": { - "@radix-ui/react-slot": "1.2.2" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -1214,62 +2182,42 @@ } } }, - "node_modules/@radix-ui/react-select": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.4.tgz", - "integrity": "sha512-/OOm58Gil4Ev5zT8LyVzqfBcij4dTHYdeyuF5lMHZ2bIp0Lk9oETocYiJ5QC0dHekEQnK6L/FNJCceeb4AkZ6Q==", + "node_modules/@radix-ui/react-toast/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==", "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collection": "1.1.6", - "@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-dismissable-layer": "1.1.9", - "@radix-ui/react-focus-guards": "1.1.2", - "@radix-ui/react-focus-scope": "1.1.6", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.6", - "@radix-ui/react-portal": "1.1.8", - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-slot": "1.2.2", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@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-visually-hidden": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" + "@radix-ui/react-compose-refs": "1.1.2" }, "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" + "react": "^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-slot": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", - "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" + "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@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 } } }, @@ -1440,11 +2388,6 @@ "integrity": "sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==", "dev": true }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" - }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1743,11 +2686,15 @@ "undici-types": "~6.19.2" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + }, "node_modules/@types/react": { "version": "19.1.2", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz", "integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==", - "devOptional": true, "dependencies": { "csstype": "^3.0.2" } @@ -1761,6 +2708,14 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.31.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.1.tgz", @@ -1894,9 +2849,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "dependencies": { "balanced-match": "^1.0.0" @@ -2210,9 +3165,9 @@ ] }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -2485,6 +3440,20 @@ "node": ">= 0.4" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2492,9 +3461,9 @@ "dev": true }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "dependencies": { "balanced-match": "^1.0.0", @@ -2513,17 +3482,6 @@ "node": ">=8" } }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -2575,7 +3533,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "engines": { "node": ">=6" } @@ -2628,24 +3585,11 @@ "node": ">=6" } }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "optional": true, - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "devOptional": true, + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -2657,17 +3601,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "devOptional": true - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "optional": true, - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } + "dev": true }, "node_modules/concat-map": { "version": "0.0.1", @@ -2675,6 +3609,26 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2692,8 +3646,7 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -2778,7 +3731,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, "dependencies": { "ms": "^2.1.3" }, @@ -2832,9 +3784,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz", + "integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==", "devOptional": true, "engines": { "node": ">=8" @@ -2857,6 +3809,15 @@ "node": ">=0.10.0" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2890,6 +3851,14 @@ "node": ">=10.13.0" } }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-abstract": { "version": "1.23.9", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", @@ -3060,7 +4029,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "engines": { "node": ">=10" }, @@ -3069,19 +4037,19 @@ } }, "node_modules/eslint": { - "version": "9.25.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.25.1.tgz", - "integrity": "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==", + "version": "9.36.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz", + "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", "dev": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.13.0", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.25.1", - "@eslint/plugin-kit": "^0.2.8", + "@eslint/js": "9.36.0", + "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -3092,9 +4060,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -3386,9 +4354,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -3402,9 +4370,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3414,14 +4382,14 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3551,6 +4519,11 @@ "node": ">=8" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3631,7 +4604,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3882,7 +4854,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -3890,6 +4861,14 @@ "node": ">= 0.4" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3903,7 +4882,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -3956,10 +4934,9 @@ } }, "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "optional": true + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, "node_modules/is-async-function": { "version": "2.1.1", @@ -4036,7 +5013,6 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, "dependencies": { "hasown": "^2.0.2" }, @@ -4348,8 +5324,7 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -4363,12 +5338,28 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -4676,6 +5667,11 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4701,7 +5697,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -4718,6 +5713,11 @@ "node": ">= 0.4" } }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4777,8 +5777,7 @@ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/nanoid": { "version": "3.3.11", @@ -4819,14 +5818,12 @@ "dev": true }, "node_modules/next": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/next/-/next-15.3.1.tgz", - "integrity": "sha512-8+dDV0xNLOgHlyBxP1GwHGVaNXsmp+2NhZEYrXr24GWLHtt27YrBPbPuHvzlhi7kZNYjeJNR93IF5zfFu5UL0g==", + "version": "15.5.4", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.4.tgz", + "integrity": "sha512-xH4Yjhb82sFYQfY3vbkJfgSDgXvBB6a8xPs9i35k6oZJRoQRihZH+4s9Yo2qsWpzBmZ3lPXaJ2KPXLfkvW4LnA==", "dependencies": { - "@next/env": "15.3.1", - "@swc/counter": "0.1.3", + "@next/env": "15.5.4", "@swc/helpers": "0.5.15", - "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -4838,19 +5835,19 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.3.1", - "@next/swc-darwin-x64": "15.3.1", - "@next/swc-linux-arm64-gnu": "15.3.1", - "@next/swc-linux-arm64-musl": "15.3.1", - "@next/swc-linux-x64-gnu": "15.3.1", - "@next/swc-linux-x64-musl": "15.3.1", - "@next/swc-win32-arm64-msvc": "15.3.1", - "@next/swc-win32-x64-msvc": "15.3.1", - "sharp": "^0.34.1" + "@next/swc-darwin-arm64": "15.5.4", + "@next/swc-darwin-x64": "15.5.4", + "@next/swc-linux-arm64-gnu": "15.5.4", + "@next/swc-linux-arm64-musl": "15.5.4", + "@next/swc-linux-x64-gnu": "15.5.4", + "@next/swc-linux-x64-musl": "15.5.4", + "@next/swc-win32-arm64-msvc": "15.5.4", + "@next/swc-win32-x64-msvc": "15.5.4", + "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.41.2", + "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", @@ -4911,7 +5908,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -5090,7 +6086,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "dependencies": { "callsites": "^3.0.0" }, @@ -5098,6 +6093,23 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5119,8 +6131,15 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } }, "node_modules/picocolors": { "version": "1.1.1", @@ -5281,7 +6300,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -5367,8 +6385,7 @@ "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/react-remove-scroll": { "version": "2.6.3", @@ -5415,6 +6432,26 @@ } } }, + "node_modules/react-select": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.10.2.tgz", + "integrity": "sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -5436,6 +6473,35 @@ } } }, + "node_modules/react-timezone-select": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/react-timezone-select/-/react-timezone-select-3.2.8.tgz", + "integrity": "sha512-efEIVmYAHtm+oS+YlE/9DbieMka1Lop0v1LsW1TdLq0yCBnnAzROKDUY09CICY8TCijZlo0fk+wHZZkV5NpVNw==", + "dependencies": { + "spacetime": "^7.6.0", + "timezone-soft": "^1.5.2" + }, + "peerDependencies": { + "react": "^16 || ^17.0.1 || ^18 || ^19.0.0-0", + "react-dom": "^16 || ^17.0.1 || ^18 || ^19.0.0-0", + "react-select": "^5.8.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5482,7 +6548,6 @@ "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", @@ -5502,7 +6567,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "engines": { "node": ">=4" } @@ -5607,9 +6671,9 @@ "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==" }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "devOptional": true, "bin": { "semver": "bin/semver.js" @@ -5665,15 +6729,15 @@ } }, "node_modules/sharp": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.1.tgz", - "integrity": "sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz", + "integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==", "hasInstallScript": true, "optional": true, "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.7.1" + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.0", + "semver": "^7.7.2" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -5682,26 +6746,28 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.1", - "@img/sharp-darwin-x64": "0.34.1", - "@img/sharp-libvips-darwin-arm64": "1.1.0", - "@img/sharp-libvips-darwin-x64": "1.1.0", - "@img/sharp-libvips-linux-arm": "1.1.0", - "@img/sharp-libvips-linux-arm64": "1.1.0", - "@img/sharp-libvips-linux-ppc64": "1.1.0", - "@img/sharp-libvips-linux-s390x": "1.1.0", - "@img/sharp-libvips-linux-x64": "1.1.0", - "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", - "@img/sharp-libvips-linuxmusl-x64": "1.1.0", - "@img/sharp-linux-arm": "0.34.1", - "@img/sharp-linux-arm64": "0.34.1", - "@img/sharp-linux-s390x": "0.34.1", - "@img/sharp-linux-x64": "0.34.1", - "@img/sharp-linuxmusl-arm64": "0.34.1", - "@img/sharp-linuxmusl-x64": "0.34.1", - "@img/sharp-wasm32": "0.34.1", - "@img/sharp-win32-ia32": "0.34.1", - "@img/sharp-win32-x64": "0.34.1" + "@img/sharp-darwin-arm64": "0.34.4", + "@img/sharp-darwin-x64": "0.34.4", + "@img/sharp-libvips-darwin-arm64": "1.2.3", + "@img/sharp-libvips-darwin-x64": "1.2.3", + "@img/sharp-libvips-linux-arm": "1.2.3", + "@img/sharp-libvips-linux-arm64": "1.2.3", + "@img/sharp-libvips-linux-ppc64": "1.2.3", + "@img/sharp-libvips-linux-s390x": "1.2.3", + "@img/sharp-libvips-linux-x64": "1.2.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", + "@img/sharp-libvips-linuxmusl-x64": "1.2.3", + "@img/sharp-linux-arm": "0.34.4", + "@img/sharp-linux-arm64": "0.34.4", + "@img/sharp-linux-ppc64": "0.34.4", + "@img/sharp-linux-s390x": "0.34.4", + "@img/sharp-linux-x64": "0.34.4", + "@img/sharp-linuxmusl-arm64": "0.34.4", + "@img/sharp-linuxmusl-x64": "0.34.4", + "@img/sharp-wasm32": "0.34.4", + "@img/sharp-win32-arm64": "0.34.4", + "@img/sharp-win32-ia32": "0.34.4", + "@img/sharp-win32-x64": "0.34.4" } }, "node_modules/shebang-command": { @@ -5797,13 +6863,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "optional": true, - "dependencies": { - "is-arrayish": "^0.3.1" + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" } }, "node_modules/source-map-js": { @@ -5814,20 +6879,17 @@ "node": ">=0.10.0" } }, + "node_modules/spacetime": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/spacetime/-/spacetime-7.11.0.tgz", + "integrity": "sha512-B1UvApzFBSHvxAvaOM/0RM2XfEahEL4EeuwRqlo3sycARP1fiGSmhm+PXDeZ/m49wZahHcnAex8j56SyRP2NJg==" + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", "dev": true }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -5978,6 +7040,11 @@ } } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -5994,7 +7061,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -6026,6 +7092,11 @@ "node": ">=6" } }, + "node_modules/timezone-soft": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/timezone-soft/-/timezone-soft-1.5.2.tgz", + "integrity": "sha512-BUr+CfBfeWXJwFAuEzPO9uF+v6sy3pL5SKLkDg4vdEhsyXgbBnpFoBCW8oEKSNTqNq9YHbVOjNb31xE7WyGmrA==" + }, "node_modules/tinyglobby": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", @@ -6293,6 +7364,19 @@ } } }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", + "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sidecar": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", @@ -6435,6 +7519,14 @@ "node": ">=0.10.0" } }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index f537f45e..4dfd1dd0 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --turbopack", + "dev": "next dev --turbopack --experimental-https", "build": "next build", "start": "next start", "lint": "next lint", @@ -12,19 +12,23 @@ }, "dependencies": { "@radix-ui/react-dialog": "^1.1.13", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-popover": "^1.1.13", "@radix-ui/react-select": "^2.2.4", + "@radix-ui/react-toast": "^1.2.14", "clsx": "^2.1.1", "date-fns-tz": "^3.2.0", "framer-motion": "^12.11.4", - "next": "15.3.1", + "next": "^15.5.4", "next-themes": "^0.4.6", "postcss": "^8.5.3", "react": "^19.0.0", "react-day-picker": "^9.6.7", "react-dom": "^19.0.0", "react-icons": "^5.5.0", + "react-select": "^5.10.2", + "react-timezone-select": "^3.2.8", "tailwind-merge": "^3.2.0", "vaul": "^1.1.2" }, diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 00000000..d1cd40d4 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,42 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./app/**/*.{js,ts,jsx,tsx,mdx}", + "./pages/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{js,ts,jsx,tsx,mdx}", + ], + darkMode: "class", + theme: { + extend: { + colors: { + bone: { + DEFAULT: "var(--bone)", + base: "var(--bone)", + }, + lion: "var(--lion)", + violet: "var(--violet)", + stone: { + 400: "var(--stone)", + }, + red: { + base: "var(--red)", + 500: "var(--red)", + }, + }, + fontFamily: { + modak: ["var(--font-modak)"], + nunito: ["var(--font-nunito)"], + }, + gridTemplateColumns: { + 1: "repeat(1, 1fr)", + 2: "repeat(2, 1fr)", + 3: "repeat(3, 1fr)", + 4: "repeat(4, 1fr)", + 5: "repeat(5, 1fr)", + 6: "repeat(6, 1fr)", + 7: "repeat(7, 1fr)", + }, + }, + }, + plugins: [], +};