-
+
);
}
diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx
index 46e7ba5a..7399ae7e 100644
--- a/app/dashboard/page.tsx
+++ b/app/dashboard/page.tsx
@@ -1,5 +1,13 @@
-import React from "react";
+import { getAuthCookieString } from "../_utils/cookie-utils";
+import { fetchDashboard } from "../_utils/fetch-data";
+import { processDashboardData } from "../_utils/process-dashboard-data";
+import DashboardPage from "../ui/layout/dashboard-page";
-export default function page() {
- return
dashboard page
;
+export default async function Page() {
+ const authCookies = await getAuthCookieString();
+
+ const eventData = await fetchDashboard(authCookies);
+ const processedData = processDashboardData(eventData);
+
+ return
;
}
diff --git a/app/error.tsx b/app/error.tsx
new file mode 100644
index 00000000..358d3469
--- /dev/null
+++ b/app/error.tsx
@@ -0,0 +1,33 @@
+"use client";
+
+import { useEffect } from "react";
+
+export default function EventErrorPage({
+ error,
+ reset,
+}: {
+ error: Error & { digest?: string };
+ reset: () => void;
+}) {
+ useEffect(() => {
+ // You can log the error to an error reporting service like Sentry
+ console.error(error);
+ }, [error]);
+
+ return (
+
+
+ Oops! Something went wrong.
+
+
+
{error.message}
+
+
+
+ );
+}
diff --git a/app/forgot-password/page.tsx b/app/forgot-password/page.tsx
new file mode 100644
index 00000000..af1b4b87
--- /dev/null
+++ b/app/forgot-password/page.tsx
@@ -0,0 +1,99 @@
+"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";
+import LinkText from "../ui/components/link-text";
+import TextInputField from "../ui/components/auth/text-input-field";
+
+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"),
+ },
+ ]}
+ />
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/app/globals.css b/app/globals.css
index 159f72d2..388d12a0 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -4,58 +4,139 @@
: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);
+ --calendar-accent-background: color-mix(
+ in srgb,
+ var(--color-red-200) 25%,
+ transparent
+ );
}
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-lion {
+ color: var(--lion);
+}
+
+.text-violet {
+ color: var(--violet);
+}
+
+.text-outline {
+ -webkit-text-stroke: 2px currentColor;
+ color: transparent;
+}
+
+.text-outline-dark {
+ -webkit-text-stroke: 2px var(--violet);
+ color: transparent;
+ text-shadow: 2px 2px 0px var(--violet);
}
-.text-stroke-white {
- -webkit-text-stroke: 2px var(--color-white);
+.text-outline-light {
+ -webkit-text-stroke: 2px var(--bone);
+ color: transparent;
+ text-shadow: 2px 2px 0px var(--bone);
}
-.text-stroke-violet {
- -webkit-text-stroke: 2px var(--color-violet);
+.text-outline-golden {
+ -webkit-text-stroke: 2px var(--lion);
+ color: transparent;
}
-.text-stroke-bone {
- -webkit-text-stroke: 2px var(--color-bone);
+.bubble-text {
+ font-family: var(--font-modak);
+ letter-spacing: 0.05em;
+ line-height: 1.1;
}
-.text-stroke-blue {
- -webkit-text-stroke: 2px var(--color-blue);
+.rdp-root {
+ --rdp-accent-color: var(--calendar-accent);
+ --rdp-accent-background-color: var(--calendar-accent-background);
+ --rdp-day_button-border: 3px solid transparent;
}
-.text-stroke-red {
- -webkit-text-stroke: 2px var(--color-red);
+.rdp-months {
+ display: flex;
+ justify-content: center;
}
-.text-stroke-lion {
- -webkit-text-stroke: 2px var(--color-lion);
+.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, var(--color-violet) 20%, transparent);
+ @supports (color: color-mix(in lab, red, red)) {
+ background-color: color-mix(in oklab, var(--color-violet) 20%, transparent);
+ }
+ /* dark border */
+ border-color: color-mix(in srgb, var(--color-violet) 40%, transparent);
+ @supports (color: color-mix(in lab, red, red)) {
+ border-color: color-mix(in oklab, var(--color-violet) 40%, 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);
+ }
+ }
+
+ /* shadow */
+ box-shadow: var(--shadow-md);
+}
+
+.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);
+ }
+
+ /* no shadow on nested glass */
+ box-shadow: none;
}
@custom-variant dark (&:where(.dark, .dark *));
@@ -80,12 +161,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 +212,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 +236,7 @@ body {
opacity: 0;
transform: translateX(2px);
}
+
100% {
opacity: 1;
transform: translateX(0);
@@ -128,6 +247,7 @@ body {
0% {
transform: translateY(100%);
}
+
100% {
transform: translateY(0);
}
@@ -137,6 +257,7 @@ body {
0% {
transform: translateY(0);
}
+
100% {
transform: translateY(100%);
}
diff --git a/app/layout.tsx b/app/layout.tsx
index f64983ca..1636f0a3 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/schedule/results/loading.tsx b/app/loading.tsx
similarity index 51%
rename from app/schedule/results/loading.tsx
rename to app/loading.tsx
index 3c7a2b8b..44ebee43 100644
--- a/app/schedule/results/loading.tsx
+++ b/app/loading.tsx
@@ -1,7 +1,9 @@
export default function Loading() {
return (
);
}
diff --git a/app/login/page.tsx b/app/login/page.tsx
new file mode 100644
index 00000000..1114b36d
--- /dev/null
+++ b/app/login/page.tsx
@@ -0,0 +1,116 @@
+"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";
+import Checkbox from "../ui/components/checkbox";
+import TextInputField from "../ui/components/auth/text-input-field";
+import LinkText from "../ui/components/link-text";
+
+export default function Page() {
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [rememberMe, setRememberMe] = useState(false);
+ 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, remember_me: rememberMe }),
+ })
+ .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 (
+
+ );
+}
diff --git a/app/new-event/page.tsx b/app/new-event/page.tsx
index c8cfe65c..db953b5e 100644
--- a/app/new-event/page.tsx
+++ b/app/new-event/page.tsx
@@ -1,140 +1,5 @@
-"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 EventEditor from "../ui/layout/event-editor";
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);
- };
-
- return (
-
-
-
- {/* Prompt */}
-
- What times and dates is this event?
-
-
- {/* Date range picker */}
-
-
-
-
- {/* From/To */}
-
-
- handleTimeChange("from", from)}
- />
-
-
-
- handleTimeChange("to", to)}
- />
-
-
- {/* Timezone */}
-
-
-
-
-
- {/*
-
-
-
-
-
*/}
-
-
-
-
- );
+ return ;
}
diff --git a/app/page.tsx b/app/page.tsx
index fcd0ebc6..d0eb8e28 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,29 +1,172 @@
-"use client";
-
-import { useRouter } from "next/navigation";
+import Link from "next/link";
+import Logo from "./ui/components/logo";
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.
+
+
+
+
+ Easy Coordination
+
+
+ Share a simple link and watch as responses stack up
+ without the back-and-forth.
+
+
+
+
+ Perfect Results
+
+
+ Get the ideal meeting time that works for everyone with an
+ intuitive graph view.
+
+
+
+
+
+
+
+
+
+ {/* 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
+ a flash
+
+
+
+
+ 🥞
+
+
Flip & Serve
+
+ Review the results and serve up the ideal meeting time for
+ everyone
+
+
+
+
+
+
+
+ {/* Plan Today Section */}
+
+
+
+ PLAN TODAY
+
+
+
+ Start Planning
+
+
+
+
+
+
+
+ © 2025 Plancake. Stacking up perfect plans, one pancake at a
+ time.
+
+
+
+
+
);
}
diff --git a/app/register/email-sent/page.tsx b/app/register/email-sent/page.tsx
new file mode 100644
index 00000000..b8c70f5d
--- /dev/null
+++ b/app/register/email-sent/page.tsx
@@ -0,0 +1,80 @@
+"use client";
+
+import formatApiError from "@/app/_utils/format-api-error";
+import { useRouter } from "next/navigation";
+import { useEffect, useRef, useState } from "react";
+import MessagePage from "../../ui/layout/message-page";
+
+export default function Page() {
+ const router = useRouter();
+ const lastEmailResend = useRef(Date.now());
+ const [email, setEmail] = useState("");
+
+ useEffect(() => {
+ const storedEmail = sessionStorage.getItem("register_email");
+ if (!storedEmail) {
+ // the user shouldn't be here
+ router.push("/login");
+ } else {
+ setEmail(storedEmail);
+ // don't clear the email from storage, it creates problems when testing
+ // it should be deleted after the session ends anyway
+ }
+ }, [router]); // empty dependency array to run once on initial mount
+
+ if (!email) {
+ // don't render until there is an 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/register/page.tsx b/app/register/page.tsx
new file mode 100644
index 00000000..b2a4b436
--- /dev/null
+++ b/app/register/page.tsx
@@ -0,0 +1,169 @@
+"use client";
+
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import React, { useEffect, useState } from "react";
+import formatApiError from "../_utils/format-api-error";
+import TextInputField from "../ui/components/auth/text-input-field";
+import { useDebounce } from "../_lib/use-debounce";
+import PasswordCriteria from "../ui/components/auth/password-criteria";
+import LinkText from "../ui/components/link-text";
+
+export default function Page() {
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [confirmPassword, setConfirmPassword] = useState("");
+ const [passwordCriteria, setPasswordCriteria] = useState({});
+ const isSubmitting = React.useRef(false);
+ const router = useRouter();
+
+ function passwordIsStrong() {
+ return Object.keys(passwordCriteria).length === 0;
+ }
+
+ useDebounce(() => {
+ if (password.length === 0) {
+ setPasswordCriteria({});
+ return;
+ }
+
+ // Check that the password is strong enough with the API
+ fetch("/api/auth/check-password/", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ password }),
+ })
+ .then((res) => {
+ if (res.ok) {
+ res.json().then((data) => {
+ if (data.is_strong) {
+ setPasswordCriteria({});
+ return;
+ } else {
+ setPasswordCriteria(data.criteria || {});
+ }
+ });
+ } else {
+ console.error("Fetch error:", res.status);
+ }
+ })
+ .catch((err) => {
+ console.error("Fetch error:", err);
+ });
+ }, [password]);
+
+ useEffect(() => {
+ if (password.length === 0) {
+ setPasswordCriteria({});
+ return;
+ }
+ }, [password]);
+
+ 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 (!passwordIsStrong()) {
+ alert("Password is not strong enough");
+ 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("register_email", email);
+ router.push("/register/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 (
+
+ );
+}
diff --git a/app/reset-password/page.tsx b/app/reset-password/page.tsx
new file mode 100644
index 00000000..1fab2e87
--- /dev/null
+++ b/app/reset-password/page.tsx
@@ -0,0 +1,154 @@
+"use client";
+
+import { useRouter, useSearchParams } from "next/navigation";
+import React, { useEffect, useRef, useState } from "react";
+import formatApiError from "../_utils/format-api-error";
+import { useDebounce } from "../_lib/use-debounce";
+import PasswordCriteria from "../ui/components/auth/password-criteria";
+import TextInputField from "../ui/components/auth/text-input-field";
+
+export default function Page() {
+ const [newPassword, setNewPassword] = useState("");
+ const [confirmPassword, setConfirmPassword] = useState("");
+ const [passwordCriteria, setPasswordCriteria] = useState({});
+ const isSubmitting = useRef(false);
+ const router = useRouter();
+
+ const searchParams = useSearchParams();
+ const pwdResetToken = searchParams.get("token");
+
+ function passwordIsStrong() {
+ return Object.keys(passwordCriteria).length === 0;
+ }
+
+ useDebounce(() => {
+ if (newPassword.length === 0) {
+ setPasswordCriteria({});
+ return;
+ }
+
+ // Check that the password is strong enough with the API
+ fetch("/api/auth/check-password/", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ password: newPassword }),
+ })
+ .then((res) => {
+ if (res.ok) {
+ res.json().then((data) => {
+ if (data.is_strong) {
+ setPasswordCriteria({});
+ return;
+ } else {
+ setPasswordCriteria(data.criteria || {});
+ }
+ });
+ } else {
+ console.error("Fetch error:", res.status);
+ }
+ })
+ .catch((err) => {
+ console.error("Fetch error:", err);
+ });
+ }, [newPassword]);
+
+ useEffect(() => {
+ if (newPassword.length === 0) {
+ setPasswordCriteria({});
+ return;
+ }
+ }, [newPassword]);
+
+ 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 (!passwordIsStrong()) {
+ alert("Password is not strong enough");
+ 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 (
+
+ );
+}
diff --git a/app/reset-password/success/page.tsx b/app/reset-password/success/page.tsx
new file mode 100644
index 00000000..ef81fc1c
--- /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/not-found.tsx b/app/schedule/availability/not-found.tsx
deleted file mode 100644
index abaee81f..00000000
--- a/app/schedule/availability/not-found.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function NotFound() {
- return 404 - Page Not Found
;
-}
diff --git a/app/schedule/availability/page.tsx b/app/schedule/availability/page.tsx
deleted file mode 100644
index c5a8f3c6..00000000
--- a/app/schedule/availability/page.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import React from "react";
-
-export default function page() {
- return availability page
;
-}
diff --git a/app/schedule/layout.tsx b/app/schedule/layout.tsx
deleted file mode 100644
index e353ee11..00000000
--- a/app/schedule/layout.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-export default function ScheduleLayout({
- children,
-}: {
- children: React.ReactNode;
-}) {
- return (
- <>
- {children}
- >
- );
-}
diff --git a/app/schedule/results/not-found.tsx b/app/schedule/results/not-found.tsx
deleted file mode 100644
index abaee81f..00000000
--- a/app/schedule/results/not-found.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function NotFound() {
- return 404 - Page Not Found
;
-}
diff --git a/app/schedule/results/page.tsx b/app/schedule/results/page.tsx
deleted file mode 100644
index 5e395bb3..00000000
--- a/app/schedule/results/page.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import React from "react";
-
-export default function page() {
- return results page
;
-}
diff --git a/app/ui/components/archive/date-range-drawer copy.tsx b/app/ui/components/archive/date-range-drawer copy.tsx
deleted file mode 100644
index 48d271e9..00000000
--- a/app/ui/components/archive/date-range-drawer copy.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import { Drawer } from "vaul";
-import { Calendar } from "./month-calendar";
-import { format } from "date-fns";
-
-type Props = {
- specificRange: { from: Date | null; to: Date | null };
- onChangeSpecific: (range: { from: Date | null; to: Date | null }) => void;
-};
-
-export default function DateRangeDrawer({
- specificRange,
- onChangeSpecific,
-}: Props) {
- const displayFrom = specificRange.from
- ? format(specificRange.from, "EEE, MMM d")
- : "";
- const displayTo = specificRange.to
- ? format(specificRange.to, "EEE, MMM d")
- : "";
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- Select a Date Range
-
-
onChangeSpecific({ from, to })}
- />
-
-
-
-
- );
-}
-
-const DateRange = ({ specificRange, onChangeSpecific }: Props) => {
- const displayFrom = specificRange.from
- ? format(specificRange.from, "EEE, MMM d")
- : "";
- const displayTo = specificRange.to
- ? format(specificRange.to, "EEE, MMM d")
- : "";
- return (
-
- );
-};
diff --git a/app/ui/components/archive/month-calendar.tsx b/app/ui/components/archive/month-calendar.tsx
deleted file mode 100644
index 75ad862b..00000000
--- a/app/ui/components/archive/month-calendar.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-"use client";
-
-import { ChangeEventHandler, useEffect, useState } from "react";
-import useCheckMobile from "@/app/_utils/use-check-mobile";
-
-import { format, isAfter, isBefore, isValid, parse } from "date-fns";
-import {
- DateRange,
- DayPicker,
- SelectRangeEventHandler,
- getDefaultClassNames,
-} from "react-day-picker";
-import "react-day-picker/dist/style.css";
-
-type CalendarProps = {
- className?: string;
- onRangeSelect?: (from: string | null, to: string | null) => void;
-};
-
-export function Calendar({ className, onRangeSelect }: CalendarProps) {
- const defaultClassNames = getDefaultClassNames();
-
- const isMobile = useCheckMobile();
- const numberOfMonths = isMobile ? 12 : 2;
- const hideNavigation = isMobile ? true : false;
-
- const today = new Date();
-
- const [month, setMonth] = useState(today);
- const [selectedRange, setSelectedRange] = useState();
-
- // const [fromValue, setFromValue] = useState("");
- // const [toValue, setToValue] = useState("");
-
- // const handleFromChange: ChangeEventHandler = (e) => {
- // setFromValue(e.target.value);
- // const date = parse(e.target.value, "EEE, MMM d", new Date());
- // if (!isValid(date)) {
- // return setSelectedRange({ from: undefined, to: undefined });
- // }
- // if (selectedRange?.to && isAfter(date, selectedRange.to)) {
- // setSelectedRange({ from: selectedRange.to, to: date });
- // } else {
- // setSelectedRange({ from: date, to: selectedRange?.to });
- // }
- // };
-
- // const handleToChange: ChangeEventHandler = (e) => {
- // setToValue(e.target.value);
- // const date = parse(e.target.value, "EEE, MMM d", new Date());
-
- // if (!isValid(date)) {
- // return setSelectedRange({ from: selectedRange?.from, to: undefined });
- // }
- // if (selectedRange?.from && isBefore(date, selectedRange.from)) {
- // setSelectedRange({ from: date, to: selectedRange.from });
- // } else {
- // setSelectedRange({ from: selectedRange?.from, to: date });
- // }
- // };
-
- const handleRangeSelect: SelectRangeEventHandler = (
- range: DateRange | undefined,
- ) => {
- setSelectedRange(range);
- const from = range?.from ? format(range.from, "EEE, MMM d") : "";
- const to = range?.to ? format(range.to, "EEE, MMM d") : "";
- // setFromValue(from);
- // setToValue(to);
- onRangeSelect?.(from, to);
- };
-
- return (
-
- {/*
-
-
-
*/}
-
-
-
- );
-}
diff --git a/app/ui/components/auth/password-criteria.tsx b/app/ui/components/auth/password-criteria.tsx
new file mode 100644
index 00000000..724b9a42
--- /dev/null
+++ b/app/ui/components/auth/password-criteria.tsx
@@ -0,0 +1,26 @@
+import { CheckIcon, Cross2Icon } from "@radix-ui/react-icons";
+import { cn } from "../../../_lib/classname";
+
+type PasswordCriteriaProps = {
+ criteria: { [key: string]: boolean };
+};
+
+export default function PasswordCriteria(props: PasswordCriteriaProps) {
+ return (
+
+
Your password must:
+ {Object.entries(props.criteria).map(([key, value], index) => (
+
+ {value ? : }
+ {key}
+
+ ))}
+
+ );
+}
diff --git a/app/ui/components/auth/text-input-field.tsx b/app/ui/components/auth/text-input-field.tsx
new file mode 100644
index 00000000..7370a7dd
--- /dev/null
+++ b/app/ui/components/auth/text-input-field.tsx
@@ -0,0 +1,44 @@
+import { EyeNoneIcon, EyeOpenIcon } from "@radix-ui/react-icons";
+import { useState } from "react";
+
+type FieldType = "text" | "email" | "password";
+
+type TextInputFieldProps = {
+ type: FieldType;
+ placeholder: string;
+ value: string;
+ onChange: (value: string) => void;
+};
+
+export default function TextInputField(props: TextInputFieldProps) {
+ const { type, placeholder, value, onChange } = props;
+ const [showPassword, setShowPassword] = useState(false);
+
+ return (
+
+ onChange(e.target.value)}
+ className={
+ "w-full rounded-full border px-4 py-2 focus:ring-2 focus:outline-none" +
+ (type === "password" ? " pr-10" : "")
+ }
+ />
+ {type === "password" && (
+
+ )}
+
+ );
+}
diff --git a/app/ui/components/checkbox.tsx b/app/ui/components/checkbox.tsx
index f9371175..c3d20fcb 100644
--- a/app/ui/components/checkbox.tsx
+++ b/app/ui/components/checkbox.tsx
@@ -11,11 +11,14 @@ export default function Checkbox(props: CheckboxProps) {
onChange(e.target.checked)}
/>
-
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/dashboard/dashboard-copy-button.tsx b/app/ui/components/dashboard/dashboard-copy-button.tsx
new file mode 100644
index 00000000..7bd48dc3
--- /dev/null
+++ b/app/ui/components/dashboard/dashboard-copy-button.tsx
@@ -0,0 +1,51 @@
+import { cn } from "@/app/_lib/classname";
+import { useToast } from "@/app/_lib/toast-context";
+import { CopyIcon } from "@radix-ui/react-icons";
+import { MouseEvent } from "react";
+
+export type DashboardCopyButtonProps = {
+ code: string;
+};
+
+export default function DashboardCopyButton({
+ code,
+}: DashboardCopyButtonProps) {
+ const { addToast } = useToast();
+ const eventUrl =
+ typeof window !== "undefined" ? `${window.location.origin}/${code}` : "";
+
+ const copyToClipboard = async (e: MouseEvent) => {
+ e.preventDefault(); // avoid triggering the parent link
+ try {
+ await navigator.clipboard.writeText(eventUrl);
+ addToast({
+ type: "success",
+ id: Date.now() + Math.random(),
+ title: "COPIED EVENT LINK!",
+ message: eventUrl,
+ icon: ,
+ });
+ } catch (err) {
+ console.error("Failed to copy: ", err);
+ addToast({
+ type: "error",
+ id: Date.now() + Math.random(),
+ title: "COPY FAILED",
+ message: "Could not copy link to clipboard.",
+ });
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/app/ui/components/dashboard/dashboard-event.tsx b/app/ui/components/dashboard/dashboard-event.tsx
new file mode 100644
index 00000000..2c76f582
--- /dev/null
+++ b/app/ui/components/dashboard/dashboard-event.tsx
@@ -0,0 +1,95 @@
+import { cn } from "@/app/_lib/classname";
+import { ClockIcon, Pencil1Icon } from "@radix-ui/react-icons";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import DashboardCopyButton from "./dashboard-copy-button";
+import DateRangeRow from "./date-range-row";
+import WeekdayRow from "./weekday-row";
+import { MouseEvent } from "react";
+
+export type DashboardEventProps = {
+ myEvent: boolean;
+ code: string;
+ title: string;
+ type: "specific" | "weekday";
+ startHour: number;
+ endHour: number;
+ startDate?: string;
+ endDate?: string;
+ startWeekday?: number;
+ endWeekday?: number;
+};
+
+export default function DashboardEvent({
+ myEvent = false,
+ code,
+ title,
+ type,
+ startHour,
+ endHour,
+ startDate,
+ endDate,
+ startWeekday,
+ endWeekday,
+}: DashboardEventProps) {
+ const router = useRouter();
+
+ function navigateToEdit(e: MouseEvent) {
+ e.preventDefault(); // prevent the link behind it triggering
+ router.push(`/${code}/edit`);
+ }
+
+ return (
+
+
+
+ {title}
+
+
{code}
+
+ {type === "specific" && (
+
+ )}
+ {type === "weekday" && (
+
+ )}
+
+
+
+ {formatTimeRange(startHour, endHour)}
+
+
+
+ {myEvent && (
+
+ )}
+
+
+
+ );
+}
+
+function formatHour(hour: number): string {
+ if (hour === 0 || hour === 24) {
+ return "12am";
+ }
+ const period = hour >= 12 ? "pm" : "am";
+ const adjustedHour = hour % 12 === 0 ? 12 : hour % 12;
+ return `${adjustedHour}${period}`;
+}
+
+function formatTimeRange(startHour: number, endHour: number): string {
+ if (startHour === 0 && endHour === 24) {
+ return "All day";
+ }
+ return `${formatHour(startHour)} - ${formatHour(endHour)}`;
+}
diff --git a/app/ui/components/dashboard/date-range-row.tsx b/app/ui/components/dashboard/date-range-row.tsx
new file mode 100644
index 00000000..e5650465
--- /dev/null
+++ b/app/ui/components/dashboard/date-range-row.tsx
@@ -0,0 +1,31 @@
+type DateRangeRowProps = {
+ startDate: string;
+ endDate: string;
+};
+
+export default function DateRangeRow({
+ startDate,
+ endDate,
+}: DateRangeRowProps) {
+ return (
+
+ {formatDates(startDate, endDate)}
+
+ );
+}
+
+function formatDates(startDate: string, endDate: string): string {
+ const start = new Date(startDate);
+ const end = new Date(endDate);
+ if (start.getUTCMonth() === end.getUTCMonth()) {
+ if (start.getUTCDate() === end.getUTCDate()) {
+ return `${start.toLocaleString("en-US", { month: "long" })} ${start.getUTCDate()}`;
+ } else {
+ return `${start.toLocaleString("en-US", { month: "long" })} ${start.getUTCDate()} - ${end.getUTCDate()}`;
+ }
+ } else {
+ return `${start.toLocaleString("en-US", { month: "long" })} ${start.getUTCDate()} - ${end.toLocaleString("en-US", { month: "long" })} ${end.getUTCDate()}`;
+ }
+}
diff --git a/app/ui/components/dashboard/event-grid.tsx b/app/ui/components/dashboard/event-grid.tsx
new file mode 100644
index 00000000..ad6cb4b1
--- /dev/null
+++ b/app/ui/components/dashboard/event-grid.tsx
@@ -0,0 +1,13 @@
+import DashboardEvent, { DashboardEventProps } from "./dashboard-event";
+
+export type EventGridProps = DashboardEventProps[];
+
+export default function EventGrid({ events }: { events: EventGridProps }) {
+ return (
+
+ {events.map((data: DashboardEventProps) => (
+
+ ))}
+
+ );
+}
diff --git a/app/ui/components/dashboard/weekday-row.tsx b/app/ui/components/dashboard/weekday-row.tsx
new file mode 100644
index 00000000..878fc7cd
--- /dev/null
+++ b/app/ui/components/dashboard/weekday-row.tsx
@@ -0,0 +1,53 @@
+import { cn } from "@/app/_lib/classname";
+
+type WeekdayRowProps = {
+ startWeekday: number;
+ endWeekday: number;
+};
+
+export default function WeekdayRow({
+ startWeekday,
+ endWeekday,
+}: WeekdayRowProps) {
+ return (
+
+ {["S", "M", "T", "W", "T", "F", "S"].map((initial, index) => (
+
+ ))}
+
+ );
+}
+
+function WeekdayRowIcon({
+ label,
+ index,
+ start,
+ end,
+}: {
+ label: string;
+ index: number;
+ start: number;
+ end: number;
+}) {
+ const isActive = index >= start && index <= end;
+ const isStart = index === start;
+ const isEnd = index === end;
+ return (
+
+ {label}
+
+ );
+}
diff --git a/app/ui/components/date-range/date-range-drawer.tsx b/app/ui/components/date-range/date-range-drawer.tsx
index 23a5b82b..70ead59a 100644
--- a/app/ui/components/date-range/date-range-drawer.tsx
+++ b/app/ui/components/date-range/date-range-drawer.tsx
@@ -1,41 +1,43 @@
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";
+import { DateRange } from "react-day-picker";
+import { useState } from "react";
+import { checkInvalidDateRangeLength } from "@/app/_lib/schedule/utils";
+import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
export default function DateRangeDrawer({
+ earliestDate,
eventRange,
- onChangeRangeType,
- onChangeSpecific,
- onChangeWeekday,
+ editing = false,
+ setEventType = () => {},
+ setWeekdayRange = () => {},
+ setDateRange = () => {},
}: DateRangeProps) {
const rangeType = eventRange?.type ?? "specific";
+ const [tooManyDays, setTooManyDays] = useState(false);
+
+ const checkDateRange = (range: DateRange | undefined) => {
+ setTooManyDays(checkInvalidDateRangeLength(range));
+ setDateRange(range);
+ };
- const select = (
-
- onChangeRangeType?.(value === "Specific Dates" ? "specific" : "weekday")
- }
- className="min-h-9 min-w-[100px] border-none px-2"
- />
- );
return (
-
@@ -44,21 +46,27 @@ 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}
+ Select Date Range
+
@@ -68,52 +76,62 @@ export default function DateRangeDrawer({
}
const DateRangeDrawerSelector = ({
+ earliestDate,
eventRange,
- onChangeSpecific,
- onChangeWeekday = () => {},
- displayCalendar = false,
+ displayCalendar,
+ tooManyDays,
+ 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}
/>
+ ) : (
+ <>
+
+ Possible Dates
+ {tooManyDays && (
+
+ )}
+
+
+ >
)}
);
}
return (
-
+ {!displayCalendar && Dates}
+
+ onChange={setWeekdayRange}
+ inDrawer={true}
+ />
+
);
};
diff --git a/app/ui/components/date-range/date-range-input.tsx b/app/ui/components/date-range/date-range-input.tsx
index c9badbe7..5c28ed84 100644
--- a/app/ui/components/date-range/date-range-input.tsx
+++ b/app/ui/components/date-range/date-range-input.tsx
@@ -1,36 +1,47 @@
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 (
-
);
}
diff --git a/app/ui/components/date-range/date-range-popover.tsx b/app/ui/components/date-range/date-range-popover.tsx
index 82d6b50c..11c4a9ea 100644
--- a/app/ui/components/date-range/date-range-popover.tsx
+++ b/app/ui/components/date-range/date-range-popover.tsx
@@ -1,23 +1,32 @@
-"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,
+ earliestDate,
+ 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 (
-
+
@@ -28,19 +37,13 @@ export default function DateRangePopover({
aria-label="Date range picker"
>
{
- 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..f4205506 100644
--- a/app/ui/components/date-range/date-range-selector.tsx
+++ b/app/ui/components/date-range/date-range-selector.tsx
@@ -1,124 +1,89 @@
-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";
+import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
+import { useState } from "react";
+import { DateRange } from "react-day-picker";
+import { checkInvalidDateRangeLength } from "@/app/_lib/schedule/utils";
export default function DateRangeSelector({
+ earliestDate,
eventRange,
- onChangeEventRange,
+ editing = false,
+ setEventType = () => {},
+ setWeekdayRange = () => {},
+ setDateRange = () => {},
}: DateRangeProps) {
const isMobile = useCheckMobile();
-
const rangeType = eventRange?.type ?? "specific";
+ const [tooManyDays, setTooManyDays] = useState(false);
- 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 checkDateRange = (range: DateRange | undefined) => {
+ setTooManyDays(checkInvalidDateRangeLength(range));
+ setDateRange(range);
};
- const select = (
-
- );
-
if (isMobile) {
return (
);
+ } else {
+ return (
+
+
+ Type
+
+
+
+ {eventRange?.type === "specific" ? (
+ <>
+
+ Possible Dates
+ {tooManyDays && (
+
+ )}
+
+
+ >
+ ) : (
+
+ )}
+
+
+ );
}
-
- 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..566d98f8
--- /dev/null
+++ b/app/ui/components/event-info-drawer.tsx
@@ -0,0 +1,116 @@
+"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 > 0 && (
+
+ {eventRange.duration} minutes
+
+ )}
+
+
+ );
+}
+
+function InfoRow({
+ label,
+ children,
+}: {
+ label: string;
+ children: React.ReactNode;
+}) {
+ return (
+
+ );
+}
+
+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..2da642e7
--- /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();
+ }, [loggedIn, setLoggedIn]);
+
+ 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..4de397c4
--- /dev/null
+++ b/app/ui/components/header/account-dropdown.tsx
@@ -0,0 +1,82 @@
+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 { ExitIcon } from "@radix-ui/react-icons";
+import { useRouter } from "next/navigation";
+
+import { forwardRef, ReactNode, useContext, useRef } from "react";
+
+export default function AccountDropdown({ children }: { children: ReactNode }) {
+ const isSubmitting = useRef(false);
+ const { setLoggedIn } = useContext(LoginContext);
+ const router = useRouter();
+
+ const signOut = 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()}
+ >
+
+
+ Sign Out
+
+
+
+
+ );
+}
+
+type DropdownItemProps = {
+ onSelect?: () => void;
+ children: ReactNode;
+};
+
+// this is a forwardRef for compatibility with Radix UI and its accessibility features
+const DropdownItem = forwardRef(
+ ({ onSelect, children }, ref) => {
+ return (
+
+ {children}
+
+ );
+ },
+);
+
+DropdownItem.displayName = "DropdownItem";
diff --git a/app/ui/components/header/dashboard-button.tsx b/app/ui/components/header/dashboard-button.tsx
new file mode 100644
index 00000000..9ecbbb14
--- /dev/null
+++ b/app/ui/components/header/dashboard-button.tsx
@@ -0,0 +1,17 @@
+import { DashboardIcon } from "@radix-ui/react-icons";
+import { useRouter } from "next/navigation";
+
+export default function DashboardButton() {
+ const router = useRouter();
+
+ return (
+
+ );
+}
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-spacer.tsx b/app/ui/components/header/header-spacer.tsx
new file mode 100644
index 00000000..a1e281a0
--- /dev/null
+++ b/app/ui/components/header/header-spacer.tsx
@@ -0,0 +1,5 @@
+export default function HeaderSpacer() {
+ return (
+
+ );
+}
diff --git a/app/ui/components/header/header.tsx b/app/ui/components/header/header.tsx
new file mode 100644
index 00000000..961b9494
--- /dev/null
+++ b/app/ui/components/header/header.tsx
@@ -0,0 +1,35 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import LogoArea from "./logo-area";
+import AccountButton from "./account-button";
+import ThemeToggle from "./theme-toggle";
+import NewEventButton from "./new-event-button";
+import DashboardButton from "./dashboard-button";
+
+export default function Header() {
+ const [mounted, setMounted] = useState(false);
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ if (!mounted) {
+ return null;
+ }
+
+ return (
+
+ );
+}
diff --git a/app/ui/components/header/logo-area.tsx b/app/ui/components/header/logo-area.tsx
new file mode 100644
index 00000000..aa1b9f5d
--- /dev/null
+++ b/app/ui/components/header/logo-area.tsx
@@ -0,0 +1,27 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import Link from "next/link";
+import Logo from "../logo";
+
+export default function LogoArea() {
+ const [mounted, setMounted] = useState(false);
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ if (!mounted) {
+ return null;
+ }
+
+ return (
+
+ {/* Text Container */}
+
+
+
+ v0.1.0
+
+ );
+}
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..1bc61e7d
--- /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/link-text.tsx b/app/ui/components/link-text.tsx
new file mode 100644
index 00000000..44ad953a
--- /dev/null
+++ b/app/ui/components/link-text.tsx
@@ -0,0 +1,13 @@
+import { ReactNode } from "react";
+
+export default function LinkText({ children }: { children: ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/app/ui/components/logo.tsx b/app/ui/components/logo.tsx
new file mode 100644
index 00000000..02e12e72
--- /dev/null
+++ b/app/ui/components/logo.tsx
@@ -0,0 +1,8 @@
+export default function Logo() {
+ return (
+
+ );
+}
diff --git a/app/ui/components/month-calendar.tsx b/app/ui/components/month-calendar.tsx
index 61e6b6ad..473d4cf2 100644
--- a/app/ui/components/month-calendar.tsx
+++ b/app/ui/components/month-calendar.tsx
@@ -1,25 +1,25 @@
"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";
+
+import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
+import { checkInvalidDateRangeLength } from "@/app/_lib/schedule/utils";
type CalendarProps = {
+ earliestDate?: Date;
className?: string;
selectedRange: DateRange;
- onRangeSelect: (range: { from: Date | null; to: Date | null }) => void;
+ setDateRange: (range: DateRange | undefined) => void;
};
export function Calendar({
+ earliestDate,
className,
selectedRange,
- onRangeSelect,
+ setDateRange,
}: CalendarProps) {
const defaultClassNames = getDefaultClassNames();
@@ -29,14 +29,23 @@ export function Calendar({
const today = new Date();
- const [month, setMonth] = useState(today);
+ const startDate =
+ earliestDate && earliestDate < today
+ ? new Date(
+ earliestDate.getUTCFullYear(),
+ earliestDate.getUTCMonth(),
+ earliestDate.getUTCDate(),
+ )
+ : today;
+
+ const [month, setMonth] = useState(startDate);
+ const [tooManyDays, setTooManyDays] = useState(() => {
+ return checkInvalidDateRangeLength(selectedRange);
+ });
- const handleRangeSelect: SelectRangeEventHandler = (
- range: DateRange | undefined,
- ) => {
- const from = range?.from || null;
- const to = range?.to || null;
- onRangeSelect({ from, to });
+ const checkDateRange = (range: DateRange | undefined) => {
+ setTooManyDays(checkInvalidDateRangeLength(range));
+ setDateRange(range);
};
return (
@@ -55,12 +64,18 @@ export function Calendar({
month={month}
onMonthChange={setMonth}
selected={selectedRange}
- onSelect={handleRangeSelect}
- disabled={{ before: new Date() }}
+ onSelect={checkDateRange}
+ disabled={{ before: startDate }}
classNames={{
root: `${defaultClassNames.root} flex justify-center items-center`,
}}
/>
+ {!isMobile && tooManyDays && (
+
+
+ Too many days selected. Max is 30 days.
+
+ )}
);
}
diff --git a/app/ui/components/schedule/grid-preview-dialog.tsx b/app/ui/components/schedule/grid-preview-dialog.tsx
index e33f3a1c..dfd25f20 100644
--- a/app/ui/components/schedule/grid-preview-dialog.tsx
+++ b/app/ui/components/schedule/grid-preview-dialog.tsx
@@ -1,14 +1,14 @@
"use client";
-import { EnterFullScreenIcon, Cross2Icon } from "@radix-ui/react-icons";
-import { motion } from "framer-motion";
-import ScheduleGrid from "./schedule-grid";
-import { EventRange } from "@/app/_types/schedule-types";
import { useState } from "react";
-import TimezoneSelect from "../timezone-select";
-import InteractiveScheduleGrid from "./interactive-schedule-grid";
-import { UserAvailability } from "@/app/_types/user-availability";
+import { motion } from "framer-motion";
+import { EnterFullScreenIcon, Cross2Icon } from "@radix-ui/react-icons";
+
+import { EventRange } from "@/app/_lib/schedule/types";
+
+import ScheduleGrid from "@/app/ui/components/schedule/schedule-grid";
+import TimeZoneSelector from "../selectors/timezone-selector";
interface GridPreviewDialogProps {
eventRange: EventRange;
@@ -17,11 +17,6 @@ interface GridPreviewDialogProps {
export default function GridPreviewDialog({
eventRange,
}: GridPreviewDialogProps) {
- const [userAvailability, setUserAvailability] = useState({
- type: "specific",
- selections: {},
- });
-
const [isOpen, setIsOpen] = useState(false);
const [timezone, setTimezone] = useState(eventRange.timezone);
@@ -30,7 +25,7 @@ export default function GridPreviewDialog({
};
return (
-
+
{isOpen && (
{isOpen ? (
-
+
- {/* */}
-
-
-
+
+
+ See event in{" "}
+
+
+
+
+
Original Event in{" "}
{eventRange.timezone}
@@ -93,11 +94,12 @@ export default function GridPreviewDialog({
) : (
-
+
)}
diff --git a/app/ui/components/schedule/interactive-schedule-grid.tsx b/app/ui/components/schedule/interactive-schedule-grid.tsx
deleted file mode 100644
index 7048c276..00000000
--- a/app/ui/components/schedule/interactive-schedule-grid.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-// components/interactive-schedule-grid.tsx
-
-"use client";
-
-import { useCallback, useState, useRef } from "react";
-import ScheduleGrid from "./schedule-grid";
-import {
- UserAvailability,
- toggleAvailability,
- isAvailable,
- addAvailability,
- removeAvailability,
-} from "@/app/_types/user-availability";
-import { EventRange } from "@/app/_types/schedule-types";
-
-interface InteractiveScheduleGridProps {
- eventRange: EventRange;
- timezone: string;
- userAvailability: UserAvailability;
- setUserAvailability: (ua: UserAvailability) => void;
-}
-
-export default function InteractiveScheduleGrid({
- eventRange,
- timezone,
- userAvailability,
- setUserAvailability,
-}: InteractiveScheduleGridProps) {
- const [isDragging, setIsDragging] = useState(false);
- const dragMode = useRef<"add" | "remove">("add");
-
- const getDayKeys = (): string[] => {
- if (eventRange.type === "specific") {
- const { dateRange } = eventRange;
- const days: string[] = [];
- if (dateRange?.from && dateRange?.to) {
- const current = new Date(dateRange.from);
- while (current <= dateRange.to) {
- days.push(current.toISOString().split("T")[0]);
- current.setDate(current.getDate() + 1);
- }
- }
- return days;
- } else {
- return Object.entries(eventRange.weekdays)
- .filter(([, enabled]) => enabled === 1)
- .map(([day]) => day);
- }
- };
-
- const hours = (): number[] => {
- const from = eventRange.timeRange.from;
- const to = eventRange.timeRange.to;
- if (!from || !to) return [];
- const fromHour = from.getHours();
- const toHour = to.getHours();
- return Array.from(
- { length: toHour - fromHour + 1 },
- (_, i) => fromHour + i,
- );
- };
-
- const dayKeys = getDayKeys();
- const hourList = hours();
-
- const handleStart = (day: string, hour: number) => {
- const selected = isAvailable(userAvailability, day, hour);
- dragMode.current = selected ? "remove" : "add";
- setIsDragging(true);
- updateSelection(day, hour);
- };
-
- const handleEnter = (day: string, hour: number) => {
- if (isDragging) updateSelection(day, hour);
- };
-
- const handleEnd = () => {
- setIsDragging(false);
- };
-
- const updateSelection = (day: string, hour: number) => {
- const updated =
- dragMode.current === "add"
- ? addAvailability(userAvailability, day, hour)
- : removeAvailability(userAvailability, day, hour);
- setUserAvailability(updated);
- };
-
- return (
-
-
-
-
- {hourList.map((hour) =>
- dayKeys.map((day) => {
- const selected = isAvailable(userAvailability, day, hour);
- return (
-
handleStart(day, hour)}
- onMouseEnter={() => handleEnter(day, hour)}
- onTouchStart={() => handleStart(day, hour)}
- onTouchMove={(e) => {
- const target = document.elementFromPoint(
- e.touches[0].clientX,
- e.touches[0].clientY,
- );
- const dataset = target?.getAttribute("data-key-hour");
- if (dataset) {
- const [d, h] = dataset.split(":");
- handleEnter(d, parseInt(h));
- }
- }}
- data-key-hour={`${day}:${hour}`}
- />
- );
- }),
- )}
-
-
- );
-}
diff --git a/app/ui/components/schedule/schedule-grid.tsx b/app/ui/components/schedule/schedule-grid.tsx
index b4e48c34..1806bb19 100644
--- a/app/ui/components/schedule/schedule-grid.tsx
+++ b/app/ui/components/schedule/schedule-grid.tsx
@@ -1,222 +1,156 @@
"use client";
-import { useState, useMemo } from "react";
-import {
- ChevronLeftIcon,
- ChevronRightIcon,
- ExclamationTriangleIcon,
-} from "@radix-ui/react-icons";
+import { useState } from "react";
+
+import { toZonedTime } from "date-fns-tz";
+import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
+import { EventRange } from "@/app/_lib/schedule/types";
import {
- EventRange,
- getDateLabels,
- getEnabledWeekdays,
-} from "@/app/_types/schedule-types";
-import useCheckMobile from "@/app/_utils/use-check-mobile";
+ AvailabilitySet,
+ ResultsAvailabilityMap,
+} from "@/app/_lib/availability/types";
+import { createEmptyUserAvailability } from "@/app/_lib/availability/utils";
+import useCheckMobile from "@/app/_lib/use-check-mobile";
+import useGenerateTimeSlots from "@/app/_lib/use-generate-timeslots";
+
+import ScheduleHeader from "./schedule-header";
+import PreviewTimeBlock from "./timeblocks/preview-timeblock";
+import InteractiveTimeBlock from "./timeblocks/interactive-timeblock";
+import ResultsTimeBlock from "./timeblocks/results-timeblock";
interface ScheduleGridProps {
+ mode: "paint" | "view" | "preview";
eventRange: EventRange;
timezone: string;
+
disableSelect?: boolean;
+
+ // for "view" mode
+ availabilities?: ResultsAvailabilityMap;
+ numParticipants?: number;
+ hoveredSlot?: string | null;
+ setHoveredSlot?: (slotIso: string | null) => void;
+
+ // for "paint" mode
+ userAvailability?: AvailabilitySet;
+ onToggleSlot?: (slotIso: string, togglingOn: boolean) => void;
}
export default function ScheduleGrid({
- disableSelect = false,
eventRange,
- timezone, // current timezone of the user, not necessarily the original
+ timezone,
+ mode = "preview",
+ availabilities = {},
+ numParticipants = 0,
+ hoveredSlot,
+ setHoveredSlot = () => {},
+ userAvailability = createEmptyUserAvailability(),
+ onToggleSlot = () => {},
}: ScheduleGridProps) {
const isMobile = useCheckMobile();
- let numHours = 0;
- let numDays = 0;
- let daysLabel: string[] = [];
-
- const { timeRange } = eventRange;
- numHours =
- timeRange.to && timeRange.from
- ? timeRange.to.getHours() - timeRange.from.getHours() + 1
- : 0;
-
- const hours = useMemo(() => {
- const range: Date[] = [];
- for (let i = 0; i < numHours; i++) {
- const hour = timeRange.from
- ? new Date(timeRange.from.getTime() + i * 3600000)
- : new Date(); // Fallback to current time or handle appropriately
- range.push(hour);
- }
- return range;
- }, [timeRange, numHours]);
-
- if (eventRange.type === "specific") {
- const { dateRange } = eventRange;
- if (!dateRange || !timeRange) {
- return GridError({ message: "Invalid date or time range" });
- }
-
- numDays =
- Math.ceil(
- ((dateRange.to?.getTime() ?? 0) - (dateRange.from?.getTime() ?? 0)) /
- (1000 * 60 * 60 * 24),
- ) + 1;
- daysLabel = getDateLabels(dateRange);
- } else if (eventRange.type === "weekday") {
- const { weekdays } = eventRange;
- const enabledWeekdays = getEnabledWeekdays(weekdays);
- if (!enabledWeekdays || !timeRange) {
- return GridError({ message: "Invalid weekdays or time range" });
- }
-
- numDays = enabledWeekdays.length;
- daysLabel = enabledWeekdays.map((day) => day.toUpperCase());
- }
+ const { timeBlocks, dayGroupedSlots, numDays, error } = useGenerateTimeSlots(
+ eventRange,
+ timezone,
+ );
const maxDaysVisible = isMobile ? 4 : 7;
const [currentPage, setCurrentPage] = useState(0);
- const totalPages = Math.ceil(numDays / maxDaysVisible);
+ const totalPages = Math.max(1, Math.ceil(numDays / maxDaysVisible));
const startIndex = currentPage * maxDaysVisible;
const endIndex = Math.min(startIndex + maxDaysVisible, numDays);
- const visibleDays = daysLabel.slice(startIndex, endIndex);
- const timeColWidth = 50;
- const rightArrowWidth = 20;
+ const visibleDays = dayGroupedSlots.slice(startIndex, endIndex);
+ const visibleTimeSlots = visibleDays.flatMap((day) => day.timeslots);
- if (numHours <= 0) {
- return GridError({ message: "Invalid time range" });
- } else if (numDays <= 0) {
- return GridError({ message: "Invalid or missing date range" });
- }
+ if (numDays <= 0)
+ return
;
+ if (error) return
;
return (
-
- {/* Arrows */}
- {currentPage > 0 && (
-
- )}
- {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}
+ availabilities={availabilities}
+ numParticipants={numParticipants}
+ 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..10c75ebf
--- /dev/null
+++ b/app/ui/components/schedule/time-slot.tsx
@@ -0,0 +1,61 @@
+"use client";
+
+import React, { memo } from "react";
+import { cn } from "@/app/_lib/classname";
+
+interface TimeSlotProps {
+ slotIso: string;
+ isSelected?: boolean;
+ isHovered?: boolean;
+
+ disableSelect?: boolean;
+ dynamicStyle?: React.CSSProperties & {
+ [key: `--${string}`]: string | number;
+ };
+ gridColumn: number;
+ gridRow: number;
+
+ cellClasses?: string;
+
+ // Event handlers
+ onPointerDown?: () => void;
+ onPointerEnter?: () => void;
+ onPointerLeave?: () => void;
+ onTouchMove?: (e: React.TouchEvent
) => void;
+}
+
+function TimeSlot({
+ slotIso,
+ isHovered,
+ disableSelect,
+ dynamicStyle: style,
+ 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..489af76a
--- /dev/null
+++ b/app/ui/components/schedule/timeblocks/interactive-timeblock.tsx
@@ -0,0 +1,107 @@
+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 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, togglingOn: boolean) => void;
+}
+
+export default function InteractiveTimeBlock({
+ timeColWidth,
+ numQuarterHours,
+ startHour,
+ timeslots,
+ numVisibleDays,
+ visibleDayKeys,
+ userTimezone,
+ availability,
+ onToggle,
+}: InteractiveTimeBlockProps) {
+ 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);
+ const isToggling =
+ dragHandlers.draggedSlots.has(slotIso) &&
+ dragHandlers.togglingOn === !isSelected;
+ // don't highlight if we're toggling, in case the user is hovering a slot that
+ // won't be toggled
+ const isHovered =
+ dragHandlers.hoveredSlot === slotIso &&
+ dragHandlers.draggedSlots.size === 0;
+
+ if (isHovered || isToggling) {
+ cellClasses.push("bg-blue-200 dark:bg-red-200");
+ } else if (isSelected) {
+ cellClasses.push("dark:bg-red bg-blue");
+ }
+
+ return (
+
+ dragHandlers.onPointerDown(slotIso, false, isSelected)
+ }
+ onPointerEnter={() => {
+ dragHandlers.onPointerEnter(slotIso, false);
+ }}
+ onPointerLeave={() => {
+ dragHandlers.onPointerLeave();
+ }}
+ 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..ac74b5b7
--- /dev/null
+++ b/app/ui/components/schedule/timeblocks/preview-timeblock.tsx
@@ -0,0 +1,72 @@
+import BaseTimeBlock from "./base-timeblock";
+
+import { toZonedTime } from "date-fns-tz";
+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) {
+ 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");
+ }
+ }
+
+ 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..db7df40d
--- /dev/null
+++ b/app/ui/components/schedule/timeblocks/results-timeblock.tsx
@@ -0,0 +1,107 @@
+import { ResultsAvailabilityMap } from "@/app/_lib/availability/types";
+
+import BaseTimeBlock from "./base-timeblock";
+
+import { formatInTimeZone, toZonedTime } from "date-fns-tz";
+import TimeSlot from "../time-slot";
+
+interface ResultsTimeBlockProps {
+ timeColWidth: number;
+ numQuarterHours: number;
+ startHour: number;
+ timeslots: Date[];
+ numVisibleDays: number;
+ visibleDayKeys: string[];
+ hoveredSlot: string | null | undefined;
+
+ availabilities: ResultsAvailabilityMap;
+ numParticipants: number;
+
+ userTimezone: string;
+ onHoverSlot?: (iso: string | null) => void;
+}
+
+export default function ResultsTimeBlock({
+ timeColWidth,
+ numQuarterHours,
+ startHour,
+ timeslots,
+ numVisibleDays,
+ visibleDayKeys,
+ userTimezone,
+ availabilities,
+ numParticipants,
+ hoveredSlot,
+ onHoverSlot,
+}: ResultsTimeBlockProps) {
+ return (
+
+ {timeslots.map((timeslot, timeslotIdx) => {
+ const localSlot = toZonedTime(timeslot, userTimezone);
+ const localSlotIso = formatInTimeZone(
+ localSlot,
+ userTimezone,
+ "yyyy-MM-dd'T'HH:mm:ss",
+ );
+
+ 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[] = ["cursor-default"];
+ 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 matchCount =
+ availabilities[localSlotIso]?.length > 0
+ ? availabilities[localSlotIso].length
+ : 0;
+ const opacity = matchCount / numParticipants || 0;
+ const isHovered = hoveredSlot === localSlotIso;
+
+ // background colors
+ const opacityPercent = Math.round(opacity * 100);
+ const dynamicStyle = {
+ "--opacity-percent": `${opacityPercent}%`,
+ };
+ cellClasses.push(
+ `bg-[color-mix(in_srgb,var(--color-blue)_var(--opacity-percent),var(--color-white))] dark:bg-[color-mix(in_srgb,var(--color-red)_var(--opacity-percent),var(--color-violet))]`,
+ );
+
+ return (
+ {
+ onHoverSlot?.(localSlotIso);
+ }}
+ dynamicStyle={dynamicStyle}
+ />
+ );
+ })}
+
+ );
+}
diff --git a/app/ui/components/selectors/custom-select.tsx b/app/ui/components/selectors/custom-select.tsx
new file mode 100644
index 00000000..d5f4a3a2
--- /dev/null
+++ b/app/ui/components/selectors/custom-select.tsx
@@ -0,0 +1,94 @@
+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 = {
+ id: string;
+ value: string | number;
+ options: Option[];
+ disabled?: boolean;
+ onValueChange: (value: string | number) => void;
+ placeholder?: string;
+ className?: string;
+};
+
+// --- Refactored Component ---
+export default function CustomSelect({
+ id,
+ value,
+ options,
+ disabled,
+ onValueChange,
+ placeholder,
+ className,
+}: CustomSelectProps) {
+ return (
+ onValueChange(isNaN(Number(v)) ? v : Number(v))}
+ >
+
+
+
+
+ {disabled ? null : (
+
+
+
+ )}
+
+
+
+
+
+ {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/duration-selector.tsx b/app/ui/components/selectors/duration-selector.tsx
new file mode 100644
index 00000000..e2fd705a
--- /dev/null
+++ b/app/ui/components/selectors/duration-selector.tsx
@@ -0,0 +1,134 @@
+import { useState, useEffect, useRef } from "react";
+
+import * as Dialog from "@radix-ui/react-dialog";
+import useCheckMobile from "@/app/_lib/use-check-mobile";
+import CustomSelect from "./custom-select";
+import { cn } from "@/app/_lib/classname";
+import { ChevronDownIcon } from "@radix-ui/react-icons";
+
+const durationOptions = [
+ { label: "None", value: 0 },
+ { label: "30 minutes", value: 30 },
+ { label: "45 minutes", value: 45 },
+ { label: "1 hour", value: 60 },
+];
+
+type DurationSelectorProps = {
+ id: string;
+ onChange: (duration: string | number) => void;
+ value: number;
+};
+
+export default function DurationSelector({
+ id,
+ onChange,
+ value,
+}: DurationSelectorProps) {
+ const isMobile = useCheckMobile();
+
+ if (!isMobile) {
+ return (
+
+ );
+ } else {
+ return (
+
+ );
+ }
+}
+
+function DurationDrawer({
+ id,
+ value,
+ options,
+ onChange,
+}: {
+ id: string;
+ value: number;
+ options: { label: string; value: number }[];
+ onChange: (duration: number) => void;
+}) {
+ const valueLabel = options.find((opt) => opt.value === value)?.label || "";
+
+ const [open, setOpen] = useState(false);
+ const selectedItemRef = useRef(null);
+
+ useEffect(() => {
+ if (open) {
+ const timer = setTimeout(() => {
+ selectedItemRef.current?.scrollIntoView({
+ block: "center",
+ behavior: "auto",
+ });
+ }, 0);
+
+ return () => clearTimeout(timer);
+ }
+ }, [open]);
+
+ return (
+
+
+
+ {valueLabel}
+
+
+
+
+
+
+
+
+
+
+ Select Time
+
+
+ {options.map((option) => {
+ const isSelected = option.value === value;
+ return (
+
{
+ onChange(option.value);
+ setOpen(false);
+ }}
+ className={cn(
+ "p-4 text-center",
+ isSelected && "rounded-full bg-blue text-white dark:bg-red",
+ )}
+ >
+ {option.label}
+
+ );
+ })}
+
+
+
+
+ );
+}
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..6015d0e1
--- /dev/null
+++ b/app/ui/components/selectors/event-type-select.tsx
@@ -0,0 +1,31 @@
+import CustomSelect from "./custom-select";
+
+type EventTypeSelectProps = {
+ id: string;
+ eventType: string;
+ disabled?: boolean;
+ onEventTypeChange: (type: "specific" | "weekday") => void;
+};
+
+export default function EventTypeSelect({
+ id,
+ eventType,
+ disabled = false,
+ onEventTypeChange,
+}: EventTypeSelectProps) {
+ return (
+
+ onEventTypeChange?.(value === "specific" ? "specific" : "weekday")
+ }
+ className="min-h-9 min-w-[100px] border-none"
+ />
+ );
+}
diff --git a/app/ui/components/selectors/time-selector.tsx b/app/ui/components/selectors/time-selector.tsx
new file mode 100644
index 00000000..98d71b75
--- /dev/null
+++ b/app/ui/components/selectors/time-selector.tsx
@@ -0,0 +1,136 @@
+import { useState, useEffect, useRef } from "react";
+
+import * as Dialog from "@radix-ui/react-dialog";
+import useCheckMobile from "@/app/_lib/use-check-mobile";
+import CustomSelect from "./custom-select";
+import { cn } from "@/app/_lib/classname";
+import { ChevronDownIcon } from "@radix-ui/react-icons";
+
+type TimeSelectorProps = {
+ id: string;
+ onChange: (time: number) => void;
+ value: number;
+};
+
+export default function TimeSelector({
+ id,
+ onChange,
+ value,
+}: TimeSelectorProps) {
+ const isMobile = useCheckMobile();
+
+ 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);
+ onChange(hour);
+ };
+
+ if (!isMobile) {
+ return (
+
+ );
+ } else {
+ return (
+
+ );
+ }
+}
+
+function TimeDrawer({
+ id,
+ value,
+ options,
+ onChange,
+}: {
+ id: string;
+ value: number;
+ options: { label: string; value: number }[];
+ onChange: (time: number) => void;
+}) {
+ const valueLabel = options.find((opt) => opt.value === value)?.label || "";
+
+ const [open, setOpen] = useState(false);
+ const selectedItemRef = useRef(null);
+
+ useEffect(() => {
+ if (open) {
+ const timer = setTimeout(() => {
+ selectedItemRef.current?.scrollIntoView({
+ block: "center",
+ behavior: "auto",
+ });
+ }, 0);
+
+ return () => clearTimeout(timer);
+ }
+ }, [open]);
+
+ return (
+
+
+
+ {valueLabel}
+
+
+
+
+
+
+
+
+
+
+ Select Time
+
+
+ {options.map((option) => {
+ const isSelected = option.value === value;
+ return (
+
{
+ onChange(option.value);
+ setOpen(false);
+ }}
+ className={cn(
+ "p-4 text-center",
+ isSelected && "rounded-full bg-blue text-white dark:bg-red",
+ )}
+ >
+ {option.label}
+
+ );
+ })}
+
+
+
+
+ );
+}
diff --git a/app/ui/components/selectors/timezone-selector.tsx b/app/ui/components/selectors/timezone-selector.tsx
new file mode 100644
index 00000000..0e01c655
--- /dev/null
+++ b/app/ui/components/selectors/timezone-selector.tsx
@@ -0,0 +1,140 @@
+import { useState, useEffect, useRef } from "react";
+import * as Dialog from "@radix-ui/react-dialog";
+import useCheckMobile from "@/app/_lib/use-check-mobile";
+import CustomSelect from "./custom-select";
+import { cn } from "@/app/_lib/classname";
+import { ChevronDownIcon } from "@radix-ui/react-icons";
+import { useTimezoneSelect, allTimezones } from "react-timezone-select";
+
+const labelStyle = "original";
+const timezones = allTimezones;
+
+type TimeZoneSelectorProps = {
+ id: string;
+ onChange: (tz: string) => void;
+ value: string;
+ className?: string;
+};
+
+export default function TimeZoneSelector({
+ id,
+ onChange,
+ value,
+ className,
+}: TimeZoneSelectorProps) {
+ const isMobile = useCheckMobile();
+
+ const { options, parseTimezone } = useTimezoneSelect({
+ labelStyle,
+ timezones,
+ });
+
+ if (!isMobile) {
+ return (
+
+ onChange(String(v))}
+ className="w-full"
+ />
+
+ );
+ } else {
+ return (
+
+ );
+ }
+}
+
+function TimeZoneDrawer({
+ id,
+ value,
+ onChange,
+ options,
+}: {
+ id: string;
+ value: string;
+ options: { label: string; value: string }[];
+ onChange: (tz: string) => void;
+}) {
+ const valueLabel = options.find((opt) => opt.value === value)?.label || "";
+
+ const [open, setOpen] = useState(false);
+ const selectedItemRef = useRef(null);
+
+ useEffect(() => {
+ if (open) {
+ const timer = setTimeout(() => {
+ selectedItemRef.current?.scrollIntoView({
+ block: "center",
+ behavior: "auto",
+ });
+ }, 0);
+
+ return () => clearTimeout(timer);
+ }
+ }, [open]);
+
+ return (
+
+
+
+ {valueLabel}
+
+
+
+
+
+
+
+
+
+
+ Select Time
+
+
+ {options.map((option) => {
+ const isSelected = option.value === value;
+ return (
+
{
+ onChange(option.value);
+ setOpen(false);
+ }}
+ className={cn(
+ "p-4",
+ isSelected && "rounded-full bg-blue text-white dark:bg-red",
+ )}
+ >
+ {option.label}
+
+ );
+ })}
+
+
+
+
+ );
+}
diff --git a/app/ui/components/time-dropdown.tsx b/app/ui/components/time-dropdown.tsx
index b5c197e9..9a0be62f 100644
--- a/app/ui/components/time-dropdown.tsx
+++ b/app/ui/components/time-dropdown.tsx
@@ -1,9 +1,9 @@
-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;
+ id: string;
+ onChange: (time: number) => void;
+ value: number;
defaultTZ: string;
duration: number;
};
@@ -11,8 +11,7 @@ type TimeDropdownProps = {
export default function TimeDropdown({
onChange,
value,
- defaultTZ,
- duration,
+ id,
}: TimeDropdownProps) {
const options = Array.from({ length: 24 }, (_, i) => {
const hour = i % 12 === 0 ? 12 : i % 12;
@@ -20,68 +19,20 @@ export default function TimeDropdown({
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 (
-
-
- {label}
-
-
-
- );
-}
diff --git a/app/ui/components/toasts/copy-toast.tsx b/app/ui/components/toasts/copy-toast.tsx
new file mode 100644
index 00000000..48c297f0
--- /dev/null
+++ b/app/ui/components/toasts/copy-toast.tsx
@@ -0,0 +1,40 @@
+"use client";
+
+import { CopyIcon } from "@radix-ui/react-icons";
+import { useToast } from "@/app/_lib/toast-context";
+
+export default function CopyToast() {
+ const { addToast } = useToast();
+ const currentURL = typeof window !== "undefined" ? window.location.href : "";
+
+ const copyToClipboard = async () => {
+ try {
+ await navigator.clipboard.writeText(currentURL);
+ addToast({
+ type: "success",
+ id: Date.now() + Math.random(),
+ title: "COPIED EVENT LINK!",
+ message: currentURL,
+ icon: ,
+ });
+ } catch (err) {
+ console.error("Failed to copy: ", err);
+ addToast({
+ type: "error",
+ id: Date.now() + Math.random(),
+ title: "COPY FAILED",
+ message: "Could not copy link to clipboard.",
+ });
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/app/ui/components/toasts/error-toast.tsx b/app/ui/components/toasts/error-toast.tsx
new file mode 100644
index 00000000..61231b7f
--- /dev/null
+++ b/app/ui/components/toasts/error-toast.tsx
@@ -0,0 +1,36 @@
+import * as Toast from "@radix-ui/react-toast";
+import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
+import { cn } from "@/app/_lib/classname";
+
+export default function ErrorToast({
+ error = "An error occurred",
+ label = "ERROR",
+ open,
+ onOpenChange,
+}: {
+ error?: string;
+ label?: string;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}) {
+ return (
+
+
+
+ {label}
+
+
+ {error}
+
+
+ );
+}
diff --git a/app/ui/components/toasts/success-toast.tsx b/app/ui/components/toasts/success-toast.tsx
new file mode 100644
index 00000000..de6af08a
--- /dev/null
+++ b/app/ui/components/toasts/success-toast.tsx
@@ -0,0 +1,39 @@
+import * as Toast from "@radix-ui/react-toast";
+import { cn } from "@/app/_lib/classname";
+
+export default function SuccessToast({
+ open,
+ onOpenChange,
+ title,
+ message,
+ icon,
+}: {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ title: string;
+ message: string;
+ icon: React.ReactNode;
+}) {
+ return (
+
+ {icon}
+
+ {title}
+
+
+
+ {message}
+
+
+
+ );
+}
diff --git a/app/ui/components/weekday-calendar.tsx b/app/ui/components/weekday-calendar.tsx
index c4b59122..f21a4e1d 100644
--- a/app/ui/components/weekday-calendar.tsx
+++ b/app/ui/components/weekday-calendar.tsx
@@ -1,64 +1,106 @@
"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 { days, WeekdayMap, Weekday } from "@/app/_lib/schedule/types";
type WeekdayCalendarProps = {
selectedDays: WeekdayMap;
onChange: (map: WeekdayMap) => void;
+ inDrawer?: boolean;
};
-let days: Array = [
- "Sun",
- "Mon",
- "Tue",
- "Wed",
- "Thu",
- "Fri",
- "Sat",
-];
-
export default function WeekdayCalendar({
selectedDays,
onChange,
+ inDrawer = false,
}: WeekdayCalendarProps) {
- const [startMonday, setStartMonday] = useState(false);
- days = startMonday ? [...days.slice(1), days[0]] : days;
+ // const [startMonday, setStartMonday] = useState(false);
+ // const reorderedDays = startMonday ? [...days.slice(1), days[0]] : days;
+ const [startDay, setStartDay] = useState(null);
useEffect(() => {
+ const hasSelection = Object.values(selectedDays).some((val) => val === 1);
+ if (hasSelection) {
+ return;
+ }
+
const today = new Date();
const dayOfWeek = today.getDay();
- selectedDays[days[dayOfWeek]] = 1;
- onChange(selectedDays);
- }, []);
-
- const handleRangeSelect = (day: keyof WeekdayMap) => {
- const newSelectedDays = { ...selectedDays };
- newSelectedDays[day] = newSelectedDays[day] === 1 ? 0 : 1;
- onChange(newSelectedDays);
+ const todayKey = days[dayOfWeek];
+
+ const newSelection: WeekdayMap = { ...selectedDays };
+ newSelection[todayKey] = 1;
+
+ onChange(newSelection);
+ }, [selectedDays, onChange]);
+
+ // for toggling only one day at a time
+ // currently not in use
+ // const handleDayClick = (day: Weekday) => {
+ // const newSelectedDays = { ...selectedDays };
+ // newSelectedDays[day] = newSelectedDays[day] === 1 ? 0 : 1;
+ // onChange(newSelectedDays);
+ // };
+
+ const handleRangeSelect = (day: Weekday) => {
+ if (!startDay) {
+ // set it as the start of the range
+ setStartDay(day);
+
+ // clear previous selections and select this day
+ const newSelection: WeekdayMap = { ...selectedDays };
+ days.forEach((d) => (newSelection[d] = 0));
+ newSelection[day] = 1;
+ onChange(newSelection);
+ } else {
+ // complete range
+ const newSelection: WeekdayMap = { ...selectedDays };
+ days.forEach((d) => (newSelection[d] = 0));
+
+ const startIndex = days.indexOf(startDay);
+ const endIndex = days.indexOf(day);
+
+ // determine the range boundaries
+ const [min, max] = [
+ Math.min(startIndex, endIndex),
+ Math.max(startIndex, endIndex),
+ ];
+
+ for (let i = min; i <= max; i++) {
+ newSelection[days[i]] = 1;
+ }
+
+ setStartDay(null);
+ onChange(newSelection);
+ }
};
return (
-
- {days.map((day) => (
-
- ))}
+
+ {days.map((day) => {
+ const isSelected = selectedDays[day] === 1;
+ return (
+
+ );
+ })}
- {/*
-
>({});
+
+ const createErrorToast = (message: string) => {
+ addToast({
+ type: "error",
+ id: Date.now() + Math.random(),
+ title: "ERROR",
+ message: message,
+ });
+ };
+
+ const handleNameChange = (e: React.ChangeEvent) => {
+ if (errors.displayName) setErrors((prev) => ({ ...prev, displayName: "" }));
+ else if (e.target.value === "") {
+ setErrors((prev) => ({
+ ...prev,
+ displayName: "Please enter your name.",
+ }));
+ }
+ setDisplayName(e.target.value);
+ };
+
+ // SUBMIT AVAILABILITY
+ const handleSubmitAvailability = async () => {
+ setErrors({}); // reset errors
+
+ try {
+ const validationErrors = await validateAvailabilityData(state, eventCode);
+ if (Object.keys(validationErrors).length > 0) {
+ setErrors(validationErrors);
+ Object.values(validationErrors).forEach(createErrorToast);
+ return;
+ }
+
+ const availabilityGrid = convertAvailabilityToGrid(
+ userAvailability,
+ eventRange,
+ );
+
+ const payload = {
+ event_code: eventCode,
+ display_name: displayName,
+ availability: availabilityGrid,
+ time_zone: timeZone,
+ };
+
+ const response = await fetch("/api/availability/add/", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ });
+
+ if (response.ok) router.push(`/${eventCode}/results`);
+ else createErrorToast(formatApiError(await response.json()));
+ } catch (error) {
+ console.error("Error submitting availability:", error);
+ createErrorToast("An unexpected error occurred. Please try again.");
+ }
+ };
+
+ return (
+
+
+ {/* Header and Button Row */}
+
+
+
{eventName}
+
+
+
+
+
+
+
+
+
+ {/* Main Content */}
+
+ {/* Left Panel */}
+
+
+
+ {errors.displayName ? errors.displayName : "Error Placeholder"}
+
+ Hi,{" "}
+
+
+ add your availabilities here
+
+
+ {/* Desktop-only Event Info */}
+
+
+
+
+
+ Displaying event in
+
+
+
+
+
+
+ {/* Right Panel */}
+
+
+
+
+
+ Submit Availability
+
+
+
+ );
+}
diff --git a/app/ui/layout/dashboard-page.tsx b/app/ui/layout/dashboard-page.tsx
new file mode 100644
index 00000000..2e403e63
--- /dev/null
+++ b/app/ui/layout/dashboard-page.tsx
@@ -0,0 +1,103 @@
+"use client";
+
+import { cn } from "@/app/_lib/classname";
+import { LoginContext } from "@/app/_lib/providers";
+import useCheckMobile from "@/app/_lib/use-check-mobile";
+import { InfoCircledIcon } from "@radix-ui/react-icons";
+import Link from "next/link";
+import { useContext, useState } from "react";
+import HeaderSpacer from "../../ui/components/header/header-spacer";
+import EventGrid, { EventGridProps } from "../components/dashboard/event-grid";
+
+type DashboardTab = "created" | "participated";
+
+export type DashboardPageProps = {
+ created_events: EventGridProps;
+ participated_events: EventGridProps;
+};
+
+export default function DashboardPage({
+ created_events,
+ participated_events,
+}: DashboardPageProps) {
+ const [tab, setTab] = useState("created");
+ const isMobile = useCheckMobile();
+ const { loggedIn } = useContext(LoginContext);
+
+ return (
+
+
+
Dashboard
+ {loggedIn === false && (
+
+
+
+
Logged in as a Guest
+
+ This data is only available from this browser.{" "}
+
+ Create an account
+ {" "}
+ to sync your data across devices.
+
+
+
+ )}
+
+
+ );
+}
+
+function DashboardTabButton({
+ label,
+ value,
+ currentTab,
+ isMobile,
+ setTab,
+}: {
+ label: string;
+ value: DashboardTab;
+ currentTab: DashboardTab;
+ isMobile: boolean;
+ setTab: (value: DashboardTab) => void;
+}) {
+ return (
+
+ );
+}
diff --git a/app/ui/layout/event-editor.tsx b/app/ui/layout/event-editor.tsx
new file mode 100644
index 00000000..d887be47
--- /dev/null
+++ b/app/ui/layout/event-editor.tsx
@@ -0,0 +1,296 @@
+"use client";
+
+import TimeSelector from "../components/selectors/time-selector";
+import TimeZoneSelector from "../components/selectors/timezone-selector";
+import DateRangeSelector from "@/app/ui/components/date-range/date-range-selector";
+import DurationSelector from "../components/selectors/duration-selector";
+import GridPreviewDialog from "@/app/ui/components/schedule/grid-preview-dialog";
+import { useEventInfo } from "../../_lib/schedule/use-event-info";
+import { useRef } from "react";
+import { useRouter } from "next/navigation";
+import submitEvent from "@/app/_utils/submit-event";
+import { cn } from "@/app/_lib/classname";
+
+import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
+import { useState } from "react";
+import { useToast } from "@/app/_lib/toast-context";
+import { validateEventData } from "@/app/_utils/validate-data";
+import { EventRange, SpecificDateRange } from "@/app/_lib/schedule/types";
+import HeaderSpacer from "../components/header/header-spacer";
+
+export type EventEditorType = "new" | "edit";
+
+type EventEditorProps = {
+ type: EventEditorType;
+ initialData?: {
+ title: string;
+ code: string;
+ eventRange: EventRange;
+ };
+};
+
+export default function EventEditor({ type, initialData }: EventEditorProps) {
+ const {
+ state,
+ setTitle,
+ setEventType,
+ setCustomCode,
+ setTimezone,
+ setDuration,
+ setTimeRange,
+ setDateRange,
+ setWeekdayRange,
+ } = useEventInfo(initialData);
+ const { title, customCode, eventRange } = state;
+ const isSubmitting = useRef(false);
+ const router = useRouter();
+
+ // TOASTS AND ERROR STATES
+ const { addToast } = useToast();
+ const [errors, setErrors] = useState>({});
+
+ const createErrorToast = (message: string) => {
+ addToast({
+ type: "error",
+ id: Date.now() + Math.random(),
+ title: "ERROR",
+ message: message,
+ });
+ };
+
+ const handleNameChange = (e: React.ChangeEvent) => {
+ if (errors.title) setErrors((prev) => ({ ...prev, title: "" }));
+ else if (e.target.value === "") {
+ setErrors((prev) => ({ ...prev, title: "Please enter an event name." }));
+ }
+ setTitle(e.target.value);
+ };
+
+ const handleCustomCodeChange = (e: React.ChangeEvent) => {
+ if (errors.customCode) setErrors((prev) => ({ ...prev, customCode: "" }));
+ setCustomCode(e.target.value);
+ };
+
+ // SUBMIT EVENT INFO
+ const submitEventInfo = async () => {
+ if (isSubmitting.current) return;
+ isSubmitting.current = true;
+ setErrors({}); // reset errors
+
+ try {
+ const validationErrors = await validateEventData(state);
+ if (Object.keys(validationErrors).length > 0) {
+ setErrors(validationErrors);
+ Object.values(validationErrors).forEach(createErrorToast);
+ return;
+ }
+
+ await submitEvent(
+ { title, code: customCode, eventRange },
+ type,
+ eventRange.type,
+ (code: string) => router.push(`/${code}/results`),
+ );
+ } catch (error) {
+ console.error("Submission failed:", error);
+ createErrorToast("An unexpected error occurred. Please try again.");
+ } finally {
+ isSubmitting.current = false;
+ }
+ };
+
+ const earliestCalendarDate = new Date(
+ (initialData?.eventRange as SpecificDateRange)?.dateRange?.from,
+ );
+
+ return (
+
+
+
+
+
+ {errors.title ? errors.title : "Error Placeholder"}
+
+
+
+
+
+
+
+ {/* Date range picker */}
+
+
+
+
+ {/* From/To */}
+
Possible Times
+
+
+ FROM
+
+
+ setTimeRange({ ...eventRange.timeRange, from: value })
+ }
+ />
+
+
+
+ UNTIL
+
+
+ setTimeRange({ ...eventRange.timeRange, to: value })
+ }
+ />
+
+
+ {/* Timezone & Duration */}
+
+ {/* Desktop: show all options */}
+
+ Advanced Options
+
+
+ Timezone
+
+
+
+
+
+ Duration
+
+
+ setDuration((v as number) || 0)}
+ />
+
+
+
+ {type === "new" && "Custom"} Event Code
+ {errors.customCode && (
+
+ )}
+
+
+
+
+
+ {/* Mobile: expandable section */}
+
+
+ Advanced Options
+
+
+
+ Timezone
+
+
+
+ Duration
+
+ setDuration((v as number) || 0)}
+ />
+
+ {type === "new" && "Custom"} Event Code
+ {errors.customCode && (
+
+ )}
+
+ setCustomCode(e.target.value)}
+ placeholder="optional"
+ className={cn(
+ "w-full border-b-1 border-gray-300 focus:outline-none dark:border-gray-400",
+ type === "new" && "text-blue dark:text-red",
+ type === "edit" && "cursor-not-allowed opacity-50",
+ errors.customCode ? "border-red placeholder:text-red" : "",
+ )}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {type === "edit" ? "Update Event" : "Create Event"}
+
+
+
+ );
+}
diff --git a/app/ui/layout/header.tsx b/app/ui/layout/header.tsx
deleted file mode 100644
index 48a88d10..00000000
--- a/app/ui/layout/header.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import Image from "next/image";
-import ToggleDarkMode from "./theme-toggle";
-export default function Header() {
- return (
-
- );
-}
diff --git a/app/ui/layout/message-page.tsx b/app/ui/layout/message-page.tsx
new file mode 100644
index 00000000..be573a87
--- /dev/null
+++ b/app/ui/layout/message-page.tsx
@@ -0,0 +1,40 @@
+type ButtonData = {
+ type: "primary" | "secondary";
+ 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/results-page.tsx b/app/ui/layout/results-page.tsx
new file mode 100644
index 00000000..38924fbf
--- /dev/null
+++ b/app/ui/layout/results-page.tsx
@@ -0,0 +1,131 @@
+"use client";
+
+import ScheduleGrid from "@/app/ui/components/schedule/schedule-grid";
+import EventInfoDrawer from "@/app/ui/components/event-info-drawer";
+import CopyToast from "@/app/ui/components/toasts/copy-toast";
+import TimeZoneSelector from "../components/selectors/timezone-selector";
+
+import { EventInfo } from "@/app/ui/components/event-info-drawer";
+import { Pencil1Icon, Pencil2Icon } from "@radix-ui/react-icons";
+import { useState } from "react";
+import { EventRange } from "@/app/_lib/schedule/types";
+import { ResultsAvailabilityMap } from "@/app/_lib/availability/types";
+import Link from "next/link";
+import { AvailabilityDataResponse } from "@/app/_utils/fetch-data";
+import HeaderSpacer from "../components/header/header-spacer";
+
+export default function ResultsPage({
+ eventCode,
+ eventName,
+ eventRange,
+ initialAvailabilityData,
+}: {
+ eventCode: string;
+ eventName: string;
+ eventRange: EventRange;
+ initialAvailabilityData: AvailabilityDataResponse;
+}) {
+ const [timezone, setTimezone] = useState(
+ Intl.DateTimeFormat().resolvedOptions().timeZone,
+ );
+ const [hoveredSlot, setHoveredSlot] = useState(null);
+
+ const handleTZChange = (newTZ: string | number) => {
+ setTimezone(newTZ.toString());
+ };
+
+ const participated: boolean =
+ initialAvailabilityData.user_display_name != null;
+ const isCreator: boolean = initialAvailabilityData.is_creator || false;
+ const participants: string[] = initialAvailabilityData.participants || [];
+ const availabilities: ResultsAvailabilityMap =
+ initialAvailabilityData.availability || {};
+
+ return (
+
+
+
+
+
{eventName}
+
+
+
+ {isCreator && (
+
+
+
Edit Event
+
+ )}
+
+
+
+
{participated ? "Edit" : "Add"} Availability
+
+
+
+
+
+
+
+
+
+ {/* Sidebar for attendees */}
+
+
+
Attendees
+
+ {participants.length === 0 && (
+ - No attendees yet
+ )}
+ {participants.map((person: string) => {
+ const isAvailable =
+ availabilities[hoveredSlot || ""]?.includes(person);
+ return (
+ -
+ {person}
+
+ );
+ })}
+
+
+
+
+
+
+
+
+ Displaying event in
+
+
+
+
+
+
+
+ );
+}
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 (
-
- );
-
- 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..bbc6007b
--- /dev/null
+++ b/app/verify-email/page.tsx
@@ -0,0 +1,77 @@
+"use client";
+
+import { useRouter, useSearchParams } from "next/navigation";
+import { useEffect, useState } from "react";
+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("/register"),
+ },
+ ]}
+ />
+ )}
+
+ );
+}
diff --git a/example.env b/example.env
new file mode 100644
index 00000000..3f6c132e
--- /dev/null
+++ b/example.env
@@ -0,0 +1 @@
+API_URL=https://example.com
diff --git a/next.config.ts b/next.config.ts
index 28f40ec5..1370614a 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.API_URL}/:path*/`,
+ },
+ ];
+ },
};
export default nextConfig;
diff --git a/package-lock.json b/package-lock.json
index 73947a84..f26096d5 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.4.7",
"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,10 @@
}
},
"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==",
+ "license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
@@ -86,10 +219,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 +362,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 +376,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 +420,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 +441,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,13 +548,24 @@
"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==",
+ "license": "MIT",
+ "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"
],
+ "license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
@@ -327,16 +577,17 @@
"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"
],
+ "license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
@@ -348,16 +599,17 @@
"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"
],
+ "license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
@@ -367,12 +619,13 @@
}
},
"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"
],
+ "license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
@@ -382,12 +635,13 @@
}
},
"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"
],
+ "license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
@@ -397,12 +651,13 @@
}
},
"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"
],
+ "license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
@@ -412,12 +667,13 @@
}
},
"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"
],
+ "license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
@@ -427,12 +683,13 @@
}
},
"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"
],
+ "license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
@@ -442,12 +699,13 @@
}
},
"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"
],
+ "license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
@@ -457,12 +715,13 @@
}
},
"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"
],
+ "license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
@@ -472,12 +731,13 @@
}
},
"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"
],
+ "license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
@@ -487,12 +747,13 @@
}
},
"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"
],
+ "license": "Apache-2.0",
"optional": true,
"os": [
"linux"
@@ -504,16 +765,39 @@
"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"
],
+ "license": "Apache-2.0",
+ "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-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"
+ ],
+ "license": "Apache-2.0",
"optional": true,
"os": [
"linux"
@@ -525,16 +809,17 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linux-arm64": "1.1.0"
+ "@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"
],
+ "license": "Apache-2.0",
"optional": true,
"os": [
"linux"
@@ -546,16 +831,17 @@
"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"
],
+ "license": "Apache-2.0",
"optional": true,
"os": [
"linux"
@@ -567,16 +853,17 @@
"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"
],
+ "license": "Apache-2.0",
"optional": true,
"os": [
"linux"
@@ -588,16 +875,17 @@
"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"
],
+ "license": "Apache-2.0",
"optional": true,
"os": [
"linux"
@@ -609,20 +897,40 @@
"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"
],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"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"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
@@ -631,12 +939,13 @@
}
},
"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"
],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
@@ -649,12 +958,13 @@
}
},
"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"
],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
@@ -666,6 +976,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 +1020,10 @@
}
},
"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.4.7",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-15.4.7.tgz",
+ "integrity": "sha512-PrBIpO8oljZGTOe9HH0miix1w5MUiGJ/q83Jge03mHEE0E3pyqzAy2+l5G6aJDbXoobmxPJTVhbCuwlLtjSHwg==",
+ "license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
"version": "15.3.1",
@@ -693,12 +1035,13 @@
}
},
"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.4.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.7.tgz",
+ "integrity": "sha512-2Dkb+VUTp9kHHkSqtws4fDl2Oxms29HcZBwFIda1X7Ztudzy7M6XF9HDS2dq85TmdN47VpuhjE+i6wgnIboVzQ==",
"cpu": [
"arm64"
],
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -708,12 +1051,13 @@
}
},
"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.4.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.4.7.tgz",
+ "integrity": "sha512-qaMnEozKdWezlmh1OGDVFueFv2z9lWTcLvt7e39QA3YOvZHNpN2rLs/IQLwZaUiw2jSvxW07LxMCWtOqsWFNQg==",
"cpu": [
"x64"
],
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -723,12 +1067,13 @@
}
},
"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.4.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.4.7.tgz",
+ "integrity": "sha512-ny7lODPE7a15Qms8LZiN9wjNWIeI+iAZOFDOnv2pcHStncUr7cr9lD5XF81mdhrBXLUP9yT9RzlmSWKIazWoDw==",
"cpu": [
"arm64"
],
+ "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -738,12 +1083,13 @@
}
},
"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.4.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.4.7.tgz",
+ "integrity": "sha512-4SaCjlFR/2hGJqZLLWycccy1t+wBrE/vyJWnYaZJhUVHccpGLG5q0C+Xkw4iRzUIkE+/dr90MJRUym3s1+vO8A==",
"cpu": [
"arm64"
],
+ "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -753,12 +1099,13 @@
}
},
"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.4.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.4.7.tgz",
+ "integrity": "sha512-2uNXjxvONyRidg00VwvlTYDwC9EgCGNzPAPYbttIATZRxmOZ3hllk/YYESzHZb65eyZfBR5g9xgCZjRAl9YYGg==",
"cpu": [
"x64"
],
+ "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -768,12 +1115,13 @@
}
},
"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.4.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.4.7.tgz",
+ "integrity": "sha512-ceNbPjsFgLscYNGKSu4I6LYaadq2B8tcK116nVuInpHHdAWLWSwVK6CHNvCi0wVS9+TTArIFKJGsEyVD1H+4Kg==",
"cpu": [
"x64"
],
+ "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -783,12 +1131,13 @@
}
},
"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.4.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.4.7.tgz",
+ "integrity": "sha512-pZyxmY1iHlZJ04LUL7Css8bNvsYAMYOY9JRwFA3HZgpaNKsJSowD09Vg2R9734GxAcLJc2KDQHSCR91uD6/AAw==",
"cpu": [
"arm64"
],
+ "license": "MIT",
"optional": true,
"os": [
"win32"
@@ -798,12 +1147,13 @@
}
},
"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.4.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.4.7.tgz",
+ "integrity": "sha512-HjuwPJ7BeRzgl3KrjKqD2iDng0eQIpIReyhpF5r4yeAHFwWRuAhfW92rWv/r3qeQHEwHsLRzFDvMqRjyM5DI6A==",
"cpu": [
"x64"
],
+ "license": "MIT",
"optional": true,
"os": [
"win32"
@@ -1016,6 +1366,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 +1493,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 +2086,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 +2119,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 +2144,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 +2170,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 +2193,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 +2215,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 +2421,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 +2719,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 +2741,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 +2882,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 +3198,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 +3473,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 +3494,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 +3515,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 +3566,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 +3618,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 +3634,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 +3642,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 +3679,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 +3764,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,10 +3817,11 @@
}
},
"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.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"devOptional": true,
+ "license": "Apache-2.0",
"engines": {
"node": ">=8"
}
@@ -2857,6 +3843,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 +3885,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 +4063,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 +4071,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 +4094,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 +4388,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 +4404,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 +4416,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 +4553,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 +4638,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 +4888,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 +4895,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 +4916,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 +4968,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 +5047,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 +5358,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 +5372,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 +5701,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 +5731,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 +5747,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 +5811,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 +5852,13 @@
"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.4.7",
+ "resolved": "https://registry.npmjs.org/next/-/next-15.4.7.tgz",
+ "integrity": "sha512-OcqRugwF7n7mC8OSYjvsZhhG1AYSvulor1EIUsIkbbEbf1qoE5EbH36Swj8WhF4cHqmDgkiam3z1c1W0J1Wifg==",
+ "license": "MIT",
"dependencies": {
- "@next/env": "15.3.1",
- "@swc/counter": "0.1.3",
+ "@next/env": "15.4.7",
"@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 +5870,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.4.7",
+ "@next/swc-darwin-x64": "15.4.7",
+ "@next/swc-linux-arm64-gnu": "15.4.7",
+ "@next/swc-linux-arm64-musl": "15.4.7",
+ "@next/swc-linux-x64-gnu": "15.4.7",
+ "@next/swc-linux-x64-musl": "15.4.7",
+ "@next/swc-win32-arm64-msvc": "15.4.7",
+ "@next/swc-win32-x64-msvc": "15.4.7",
+ "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 +5943,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 +6121,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 +6128,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 +6166,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 +6335,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 +6420,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 +6467,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 +6508,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 +6583,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 +6602,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,10 +6706,11 @@
"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.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"devOptional": true,
+ "license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
@@ -5665,15 +6765,16 @@
}
},
"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,
+ "license": "Apache-2.0",
"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 +6783,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 +6900,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 +6916,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 +7077,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 +7098,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 +7129,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 +7401,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 +7556,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..02d842cd 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.4.7",
"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/public/tomeeto-dark.png b/public/tomeeto-dark.png
deleted file mode 100644
index 46cfe913..00000000
Binary files a/public/tomeeto-dark.png and /dev/null differ
diff --git a/public/tomeeto-light.png b/public/tomeeto-light.png
deleted file mode 100644
index 65160f4a..00000000
Binary files a/public/tomeeto-light.png and /dev/null differ
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: [],
+};