diff --git a/app/ui/components/schedule/time-slot.tsx b/src/features/event/grid/time-slot.tsx
similarity index 81%
rename from app/ui/components/schedule/time-slot.tsx
rename to src/features/event/grid/time-slot.tsx
index 10c75ebf..7fbde783 100644
--- a/app/ui/components/schedule/time-slot.tsx
+++ b/src/features/event/grid/time-slot.tsx
@@ -1,7 +1,8 @@
"use client";
import React, { memo } from "react";
-import { cn } from "@/app/_lib/classname";
+
+import { cn } from "@/lib/utils/classname";
interface TimeSlotProps {
slotIso: string;
@@ -39,10 +40,10 @@ function TimeSlot({
data-slot-iso={slotIso}
draggable={false}
className={cn(
- "relative bg-white dark:bg-violet",
- isHovered && "ring-1 ring-blue ring-inset dark:ring-red",
+ "bg-background relative",
+ isHovered && "ring-accent ring-1 ring-inset",
disableSelect
- ? "pointer-events-none cursor-not-allowed bg-[#FFFFFF] dark:bg-[#343249]"
+ ? "bg-panel pointer-events-none cursor-not-allowed"
: "cursor-cell",
cellClasses,
)}
diff --git a/app/ui/components/schedule/timeblocks/base-timeblock.tsx b/src/features/event/grid/timeblocks/base.tsx
similarity index 100%
rename from app/ui/components/schedule/timeblocks/base-timeblock.tsx
rename to src/features/event/grid/timeblocks/base.tsx
diff --git a/app/ui/components/schedule/timeblocks/interactive-timeblock.tsx b/src/features/event/grid/timeblocks/interactive.tsx
similarity index 81%
rename from app/ui/components/schedule/timeblocks/interactive-timeblock.tsx
rename to src/features/event/grid/timeblocks/interactive.tsx
index 489af76a..da405a18 100644
--- a/app/ui/components/schedule/timeblocks/interactive-timeblock.tsx
+++ b/src/features/event/grid/timeblocks/interactive.tsx
@@ -1,10 +1,9 @@
-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";
+
+import { AvailabilitySet } from "@/core/availability/types";
+import useScheduleDrag from "@/features/event/grid/lib/use-schedule-drag";
+import TimeSlot from "@/features/event/grid/time-slot";
+import BaseTimeBlock from "@/features/event/grid/timeblocks/base";
interface InteractiveTimeBlockProps {
timeColWidth: number;
@@ -75,10 +74,16 @@ export default function InteractiveTimeBlock({
dragHandlers.hoveredSlot === slotIso &&
dragHandlers.draggedSlots.size === 0;
- if (isHovered || isToggling) {
- cellClasses.push("bg-blue-200 dark:bg-red-200");
+ if (isSelected && (isHovered || isToggling)) {
+ cellClasses.push(
+ "bg-[color-mix(in_srgb,var(--color-accent),var(--color-white)_30%)]",
+ );
+ } else if (isHovered || isToggling) {
+ cellClasses.push(
+ "bg-[color-mix(in_srgb,var(--color-background),var(--color-accent)_40%)]",
+ );
} else if (isSelected) {
- cellClasses.push("dark:bg-red bg-blue");
+ cellClasses.push("bg-accent");
}
return (
diff --git a/app/ui/components/schedule/timeblocks/preview-timeblock.tsx b/src/features/event/grid/timeblocks/preview.tsx
similarity index 93%
rename from app/ui/components/schedule/timeblocks/preview-timeblock.tsx
rename to src/features/event/grid/timeblocks/preview.tsx
index ac74b5b7..fbf50e18 100644
--- a/app/ui/components/schedule/timeblocks/preview-timeblock.tsx
+++ b/src/features/event/grid/timeblocks/preview.tsx
@@ -1,7 +1,7 @@
-import BaseTimeBlock from "./base-timeblock";
-
import { toZonedTime } from "date-fns-tz";
-import TimeSlot from "../time-slot";
+
+import TimeSlot from "@/features/event/grid/time-slot";
+import BaseTimeBlock from "@/features/event/grid/timeblocks/base";
interface PreviewTimeBlockProps {
timeColWidth: number;
diff --git a/app/ui/components/schedule/timeblocks/results-timeblock.tsx b/src/features/event/grid/timeblocks/results.tsx
similarity index 79%
rename from app/ui/components/schedule/timeblocks/results-timeblock.tsx
rename to src/features/event/grid/timeblocks/results.tsx
index db7df40d..4b984d37 100644
--- a/app/ui/components/schedule/timeblocks/results-timeblock.tsx
+++ b/src/features/event/grid/timeblocks/results.tsx
@@ -1,9 +1,8 @@
-import { ResultsAvailabilityMap } from "@/app/_lib/availability/types";
+import { format, formatInTimeZone, toZonedTime } from "date-fns-tz";
-import BaseTimeBlock from "./base-timeblock";
-
-import { formatInTimeZone, toZonedTime } from "date-fns-tz";
-import TimeSlot from "../time-slot";
+import { ResultsAvailabilityMap } from "@/core/availability/types";
+import TimeSlot from "@/features/event/grid/time-slot";
+import BaseTimeBlock from "@/features/event/grid/timeblocks/base";
interface ResultsTimeBlockProps {
timeColWidth: number;
@@ -42,9 +41,11 @@ export default function ResultsTimeBlock({
visibleDaysCount={numVisibleDays}
>
{timeslots.map((timeslot, timeslotIdx) => {
+ const timeslotIso = format(timeslot, "yyyy-MM-dd'T'HH:mm:ss");
+
const localSlot = toZonedTime(timeslot, userTimezone);
const localSlotIso = formatInTimeZone(
- localSlot,
+ timeslot,
userTimezone,
"yyyy-MM-dd'T'HH:mm:ss",
);
@@ -72,11 +73,11 @@ export default function ResultsTimeBlock({
}
const matchCount =
- availabilities[localSlotIso]?.length > 0
- ? availabilities[localSlotIso].length
+ availabilities[timeslotIso]?.length > 0
+ ? availabilities[timeslotIso].length
: 0;
const opacity = matchCount / numParticipants || 0;
- const isHovered = hoveredSlot === localSlotIso;
+ const isHovered = hoveredSlot === timeslotIso;
// background colors
const opacityPercent = Math.round(opacity * 100);
@@ -84,7 +85,7 @@ export default function ResultsTimeBlock({
"--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))]`,
+ "bg-[color-mix(in_srgb,var(--color-accent)_var(--opacity-percent),var(--color-background))]",
);
return (
@@ -96,7 +97,7 @@ export default function ResultsTimeBlock({
gridColumn={gridColumn}
gridRow={gridRow}
onPointerEnter={() => {
- onHoverSlot?.(localSlotIso);
+ onHoverSlot?.(timeslotIso);
}}
dynamicStyle={dynamicStyle}
/>
diff --git a/app/ui/components/event-info-drawer.tsx b/src/features/event/info-drawer.tsx
similarity index 55%
rename from app/ui/components/event-info-drawer.tsx
rename to src/features/event/info-drawer.tsx
index 566d98f8..12be9c2b 100644
--- a/app/ui/components/event-info-drawer.tsx
+++ b/src/features/event/info-drawer.tsx
@@ -2,9 +2,37 @@
import * as Dialog from "@radix-ui/react-dialog";
import { InfoCircledIcon } from "@radix-ui/react-icons";
+import { format } from "date-fns/format";
+import { parse } from "date-fns/parse";
-import { EventRange } from "@/app/_lib/schedule/types";
-import { formatLabel } from "@/app/_lib/timezone-file-generator";
+import { EventRange } from "@/core/event/types";
+
+function formatLabel(tz: string): string {
+ try {
+ const now = new Date();
+ const offset =
+ new Intl.DateTimeFormat("en-US", {
+ timeZone: tz,
+ hour12: false,
+ hour: "2-digit",
+ minute: "2-digit",
+ timeZoneName: "shortOffset",
+ })
+ .formatToParts(now)
+ .find((p) => p.type === "timeZoneName")?.value || "";
+
+ // Try to normalize to GMT+/-HH:MM
+ const match = offset.match(/GMT([+-]\d{1,2})(?::(\d{2}))?/);
+ const hours = match?.[1] ?? "0";
+ const minutes = match?.[2] ?? "00";
+ const fullOffset = `GMT${hours}:${minutes}`;
+
+ const city = tz.split("/").slice(-1)[0].replaceAll("_", " ");
+ return `${city} (${fullOffset})`;
+ } catch {
+ return tz;
+ }
+}
export default function EventInfoDrawer({
eventRange,
@@ -14,7 +42,7 @@ export default function EventInfoDrawer({
return (
-
@@ -22,7 +50,7 @@ export default function EventInfoDrawer({
original event's timezone{" "}
which is{" "}
-
+
{formatLabel(eventRange.timezone)}
@@ -54,8 +82,8 @@ export function EventInfo({ eventRange }: { eventRange: EventRange }) {
{eventRange.type === "specific" ? (
- {prettyDate(new Date(eventRange.dateRange.from!), "date")} –{" "}
- {prettyDate(new Date(eventRange.dateRange.to!), "date")}
+ {formatDate(eventRange.dateRange.from, "EEE, MMMM d")} {" - "}
+ {formatDate(eventRange.dateRange.to, "EEE, MMMM d")}
) : (
@@ -69,13 +97,7 @@ export function EventInfo({ eventRange }: { eventRange: EventRange }) {
{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",
- )}`}
+ : `${formatTime(eventRange.timeRange.from, "hh:mm a")} - ${formatTime(eventRange.timeRange.to, "hh:mm a")}`}
{eventRange.duration > 0 && (
@@ -98,19 +120,19 @@ function InfoRow({
return (
{label}
-
{children}
+
{children}
);
}
-function prettyDate(date: Date, type?: "date" | "time") {
- return new Intl.DateTimeFormat("en-US", {
- weekday: type === "date" ? "short" : undefined,
- month: type === "date" ? "long" : undefined,
- day: type === "date" ? "numeric" : undefined,
- year: undefined,
- hour: type === "time" ? "numeric" : undefined,
- minute: type === "time" ? "numeric" : undefined,
- hour12: true,
- }).format(date);
+// Helper functions to format date and time
+function formatDate(date: string, fmt: string): string {
+ const parsedDate = parse(date, "yyyy-MM-dd", new Date());
+ return format(parsedDate, fmt);
+}
+
+function formatTime(hour: number, fmt: string): string {
+ const date = new Date();
+ date.setHours(hour, 0, 0, 0);
+ return format(date, fmt);
}
diff --git a/src/features/toast/base.tsx b/src/features/toast/base.tsx
new file mode 100644
index 00000000..69d3f42c
--- /dev/null
+++ b/src/features/toast/base.tsx
@@ -0,0 +1,69 @@
+import { Cross2Icon } from "@radix-ui/react-icons";
+import * as Toast from "@radix-ui/react-toast";
+
+import { cn } from "@/lib/utils/classname";
+
+export default function BaseToast({
+ open,
+ onOpenChange,
+ title,
+ message,
+ icon,
+ toastStyle,
+}: {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ title: string;
+ message: string;
+ icon: React.ReactNode;
+ toastStyle: string;
+}) {
+ return (
+
+ {icon}
+
+
+
{title}
+
+ {message}
+
+
+
+
+ {
+ if (
+ document.activeElement &&
+ document.activeElement instanceof HTMLElement
+ ) {
+ // After clicking this button, the focus would be on the toasts.
+ // If there is more than 1 toast, the focus causes the timers for all the
+ // toasts to be paused until the user clicked on something else.
+ // This just removes that focus.
+ document.activeElement.blur();
+ }
+ }}
+ className={cn(
+ "col-start-3 row-span-2 flex h-6 w-6 items-center justify-center rounded-full",
+ "opacity-0 transition-all",
+ "focus:opacity-100 group-hover:opacity-100",
+ "hover:bg-black/20 focus:outline-none focus:ring-2 focus:ring-white/50",
+ )}
+ >
+
+
+
+
+ );
+}
diff --git a/app/_lib/toast-context.tsx b/src/features/toast/context.ts
similarity index 70%
rename from app/_lib/toast-context.tsx
rename to src/features/toast/context.ts
index e78af0e3..aee898e4 100644
--- a/app/_lib/toast-context.tsx
+++ b/src/features/toast/context.ts
@@ -1,15 +1,9 @@
import { createContext, useContext } from "react";
-export type ToastData = {
- id: number;
- type: "error" | "success";
- title: string;
- message: string;
- icon?: React.ReactNode;
-};
+import { ToastType } from "@/features/toast/type";
export const ToastContext = createContext<{
- addToast: (data: ToastData) => void;
+ addToast: (type: ToastType, message: string) => void;
removeToast: (id: number) => void;
}>({
addToast: () => {},
diff --git a/src/features/toast/provider.tsx b/src/features/toast/provider.tsx
new file mode 100644
index 00000000..0b73daf0
--- /dev/null
+++ b/src/features/toast/provider.tsx
@@ -0,0 +1,116 @@
+"use client";
+
+import { useCallback, useState } from "react";
+
+import {
+ CheckIcon,
+ CopyIcon,
+ InfoCircledIcon,
+ ExclamationTriangleIcon,
+} from "@radix-ui/react-icons";
+import * as Toast from "@radix-ui/react-toast";
+
+import BaseToast from "@/features/toast/base";
+import ToastContext from "@/features/toast/context";
+import { ToastData, ToastType } from "@/features/toast/type";
+
+function getToastIcon(iconType: ToastType) {
+ const iconClass = "col-start-1 row-span-2 h-5 w-5";
+
+ switch (iconType) {
+ case "error":
+ return ;
+ case "copy":
+ return ;
+ case "success":
+ return ;
+ default:
+ return ;
+ }
+}
+
+function getToastStyle(type: ToastType) {
+ switch (type) {
+ case "error":
+ return "bg-red";
+ case "copy":
+ case "success":
+ return "bg-foreground text-background";
+ default:
+ return "bg-blue";
+ }
+}
+
+function getToastTitle(type: ToastType) {
+ switch (type) {
+ case "error":
+ return "ERROR";
+ case "copy":
+ return "COPIED";
+ case "success":
+ return "SUCCESS";
+ default:
+ return "INFORMATION";
+ }
+}
+
+export default function ToastProvider({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const [toasts, setToasts] = useState([]);
+
+ const addToast = useCallback((type: ToastType, message: string) => {
+ const data = {
+ id: Date.now() + Math.random(),
+ type,
+ title: getToastTitle(type),
+ message,
+ open: true,
+ };
+
+ setToasts((prevToasts) => [...prevToasts, data]);
+ }, []);
+
+ const removeToast = useCallback((id: number) => {
+ setToasts((prevToasts) =>
+ prevToasts.map((t) => (t.id === id ? { ...t, open: false } : t)),
+ );
+
+ setTimeout(() => {
+ setToasts((prevToasts) => prevToasts.filter((t) => t.id !== id));
+ }, 400);
+ }, []);
+
+ return (
+
+
+ {children}
+
+ {toasts.map((toast) => {
+ const toastIcon = getToastIcon(toast.type);
+ const toastStyle = getToastStyle(toast.type);
+
+ return (
+ {
+ if (!isOpen) {
+ removeToast(toast.id);
+ }
+ }}
+ icon={toastIcon}
+ toastStyle={toastStyle}
+ />
+ );
+ })}
+
+
+
+
+ );
+}
diff --git a/src/features/toast/type.ts b/src/features/toast/type.ts
new file mode 100644
index 00000000..e8130aeb
--- /dev/null
+++ b/src/features/toast/type.ts
@@ -0,0 +1,14 @@
+export type ToastType = "error" | "copy" | "success" | "info";
+
+export type ToastData = {
+ id: number;
+ type: ToastType;
+ title: string;
+ message: string;
+ open: boolean;
+};
+
+export interface ToastErrorMessage {
+ id: number;
+ message: string;
+}
diff --git a/app/_lib/use-check-mobile.tsx b/src/lib/hooks/use-check-mobile.ts
similarity index 100%
rename from app/_lib/use-check-mobile.tsx
rename to src/lib/hooks/use-check-mobile.ts
diff --git a/app/_lib/use-debounce.tsx b/src/lib/hooks/use-debounce.ts
similarity index 100%
rename from app/_lib/use-debounce.tsx
rename to src/lib/hooks/use-debounce.ts
diff --git a/app/_lib/providers.tsx b/src/lib/providers.tsx
similarity index 92%
rename from app/_lib/providers.tsx
rename to src/lib/providers.tsx
index c489c21a..10487abc 100644
--- a/app/_lib/providers.tsx
+++ b/src/lib/providers.tsx
@@ -1,8 +1,10 @@
"use client";
-import { ThemeProvider } from "next-themes";
import { createContext, useState } from "react";
-import ToastProvider from "./toast-provider";
+
+import { ThemeProvider } from "next-themes";
+
+import ToastProvider from "@/features/toast/provider";
export const LoginContext = createContext<{
loggedIn: boolean | null;
diff --git a/app/_utils/cookie-utils.tsx b/src/lib/utils/api/cookie-utils.ts
similarity index 100%
rename from app/_utils/cookie-utils.tsx
rename to src/lib/utils/api/cookie-utils.ts
diff --git a/app/_utils/format-api-error.tsx b/src/lib/utils/api/format-api-error.ts
similarity index 100%
rename from app/_utils/format-api-error.tsx
rename to src/lib/utils/api/format-api-error.ts
diff --git a/app/_utils/process-dashboard-data.tsx b/src/lib/utils/api/process-dashboard-data.ts
similarity index 85%
rename from app/_utils/process-dashboard-data.tsx
rename to src/lib/utils/api/process-dashboard-data.ts
index 3422744d..e83dc56f 100644
--- a/app/_utils/process-dashboard-data.tsx
+++ b/src/lib/utils/api/process-dashboard-data.ts
@@ -1,6 +1,9 @@
-import { DashboardEventProps } from "../ui/components/dashboard/dashboard-event";
-import { DashboardPageProps } from "../ui/layout/dashboard-page";
-import { DashboardEventResponse, DashboardResponse } from "./fetch-data";
+import { DashboardPageProps } from "@/app/dashboard/page-client";
+import { DashboardEventProps } from "@/features/dashboard/components/event";
+import {
+ DashboardEventResponse,
+ DashboardResponse,
+} from "@/features/dashboard/fetch-data";
function processSingleEvent(
myEvent: boolean,
diff --git a/app/_utils/process-event-data.tsx b/src/lib/utils/api/process-event-data.ts
similarity index 83%
rename from app/_utils/process-event-data.tsx
rename to src/lib/utils/api/process-event-data.ts
index 439efe4e..5b0473fe 100644
--- a/app/_utils/process-event-data.tsx
+++ b/src/lib/utils/api/process-event-data.ts
@@ -1,6 +1,6 @@
-import { generateWeekdayMap } from "@/app/_lib/schedule/utils";
-import { EventRange } from "../_lib/schedule/types";
-import { EventDetailsResponse } from "./fetch-data";
+import { EventRange } from "@/core/event/types";
+import { generateWeekdayMap } from "@/core/event/weekday-utils";
+import { EventDetailsResponse } from "@/features/event/editor/fetch-data";
export function processEventData(eventData: EventDetailsResponse): {
eventName: string;
diff --git a/app/_utils/submit-event.tsx b/src/lib/utils/api/submit-event.ts
similarity index 76%
rename from app/_utils/submit-event.tsx
rename to src/lib/utils/api/submit-event.ts
index 3f98a964..af7bf844 100644
--- a/app/_utils/submit-event.tsx
+++ b/src/lib/utils/api/submit-event.ts
@@ -2,10 +2,10 @@ import {
EventRange,
SpecificDateRange,
WeekdayRange,
-} from "../_lib/schedule/types";
-import { findRangeFromWeekdayMap } from "../_lib/schedule/utils";
-import { EventEditorType } from "../ui/layout/event-editor";
-import formatApiError from "./format-api-error";
+} from "@/core/event/types";
+import { findRangeFromWeekdayMap } from "@/core/event/weekday-utils";
+import { EventEditorType } from "@/features/event/editor/types";
+import formatApiError from "@/lib/utils/api/format-api-error";
export type EventSubmitData = {
title: string;
@@ -39,7 +39,7 @@ export default async function submitEvent(
type: EventEditorType,
eventType: "specific" | "weekday",
onSuccess: (code: string) => void,
-): Promise {
+): Promise {
let apiRoute = "";
let jsonBody: EventSubmitJsonBody;
@@ -56,7 +56,7 @@ export default async function submitEvent(
);
if (toDate.getTime() - fromDate.getTime() > 30 * 24 * 60 * 60 * 1000) {
alert("Too many days selected. Max is 30 days.");
- return;
+ return false;
}
jsonBody = {
@@ -80,7 +80,7 @@ export default async function submitEvent(
);
if (weekdayRange.startDay === null || weekdayRange.endDay === null) {
alert("Please select at least one weekday.");
- return;
+ return false;
}
const dayNameToIndex: { [key: string]: number } = {
@@ -113,26 +113,30 @@ export default async function submitEvent(
jsonBody.event_code = data.code;
}
- await fetch(apiRoute, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(jsonBody),
- })
- .then(async (res) => {
- if (res.ok) {
- const code = (await res.json()).event_code;
- if (type === "new") {
- onSuccess(code);
- } else {
- // endpoint does not return code on edit
- onSuccess(data.code);
- }
+ try {
+
+ const res = await fetch(apiRoute, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(jsonBody),
+ });
+ if (res.ok) {
+ const code = (await res.json()).event_code;
+ if (type === "new") {
+ onSuccess(code);
+ return true;
} else {
- alert(formatApiError(await res.json()));
+ // endpoint does not return code on edit
+ onSuccess(data.code);
+ return true;
}
- })
- .catch((err) => {
- console.error("Fetch error:", err);
- alert("An error occurred. Please try again.");
- });
+ } else {
+ alert(formatApiError(await res.json()));
+ return false;
+ }
+ } catch (err) {
+ console.error("Fetch error:", err);
+ alert("An error occurred. Please try again.");
+ return false;
+ }
}
diff --git a/app/_lib/classname.ts b/src/lib/utils/classname.ts
similarity index 100%
rename from app/_lib/classname.ts
rename to src/lib/utils/classname.ts
diff --git a/src/styles/centered-absolute.css b/src/styles/centered-absolute.css
new file mode 100644
index 00000000..c590e874
--- /dev/null
+++ b/src/styles/centered-absolute.css
@@ -0,0 +1,9 @@
+/* CENTERED ABSOLUTE ELEMENTS */
+
+/* Instead of defining a class with traditional CSS rules, this operates within Tailwind's
+framework where it will combine transform rules instead of overriding */
+@layer components {
+ .centered-absolute {
+ @apply absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2;
+ }
+}
diff --git a/src/styles/frosted-glass.css b/src/styles/frosted-glass.css
new file mode 100644
index 00000000..5f791dc7
--- /dev/null
+++ b/src/styles/frosted-glass.css
@@ -0,0 +1,78 @@
+/* FROSTED GLASS */
+
+.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, var(--color-white) 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, var(--color-white) 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, var(--color-white) 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;
+}
+
+.frosted-glass .frosted-glass-button {
+ &:hover {
+ background-color: color-mix(in srgb, var(--color-white) 30%, transparent);
+ @supports (color: color-mix(in lab, red, red)) {
+ background-color: color-mix(
+ in oklab,
+ var(--color-white) 30%,
+ transparent
+ );
+ }
+ }
+
+ &:active {
+ background-color: transparent;
+ }
+}
+
+.frosted-glass .frosted-glass-button-loading {
+ background-color: color-mix(in srgb, var(--color-white) 30%, transparent);
+ @supports (color: color-mix(in lab, red, red)) {
+ background-color: color-mix(in oklab, var(--color-white) 30%, transparent);
+ }
+
+ &:where(.dark, .dark *) {
+ background-color: transparent;
+ }
+}
diff --git a/src/styles/globals.css b/src/styles/globals.css
new file mode 100644
index 00000000..c4fa875f
--- /dev/null
+++ b/src/styles/globals.css
@@ -0,0 +1,16 @@
+@import "tailwindcss";
+@import "react-day-picker/style.css";
+
+@import "./tailwind.css";
+@import "./text.css";
+@import "./frosted-glass.css";
+@import "./month-calendar.css";
+@import "./centered-absolute.css";
+
+/* GLOBAL STYLES */
+
+body {
+ background: var(--background);
+ color: var(--foreground);
+ font-family: var(--font-nunito);
+}
diff --git a/src/styles/month-calendar.css b/src/styles/month-calendar.css
new file mode 100644
index 00000000..8cb95178
--- /dev/null
+++ b/src/styles/month-calendar.css
@@ -0,0 +1,12 @@
+/* MONTH CALENDAR STYLES */
+
+.rdp-root {
+ --rdp-accent-color: var(--calendar-accent);
+ --rdp-accent-background-color: var(--calendar-accent-background);
+ --rdp-day_button-border: 3px solid transparent;
+}
+
+.rdp-months {
+ display: flex;
+ justify-content: center;
+}
diff --git a/app/globals.css b/src/styles/tailwind.css
similarity index 58%
rename from app/globals.css
rename to src/styles/tailwind.css
index 388d12a0..822fd05a 100644
--- a/app/globals.css
+++ b/src/styles/tailwind.css
@@ -1,150 +1,110 @@
-@import "tailwindcss";
-@import "react-day-picker/style.css";
-
-: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);
-}
-
-.dark {
- --background: var(--violet);
- --foreground: var(--bone);
- --calendar-accent: var(--color-red);
- --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);
-}
-
-.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);
-}
+@custom-variant dark (&:where(.dark, .dark *));
-.text-violet {
- color: var(--violet);
-}
+/* COLOR PALETTE */
+@theme {
+ --color-white: oklch(0.9702 0 0);
+ --color-violet: oklch(0.368 0.0393 288.29);
+ --color-bone: oklch(0.904 0.0293 82.59);
+ --color-red: oklch(0.6909 0.1987 23.91);
+ --color-blue: oklch(0.54 0.0951 247.39);
+ --color-lion: oklch(0.8045 0.0779 73.42);
-.text-outline {
- -webkit-text-stroke: 2px currentColor;
- color: transparent;
-}
+ --color-violet-100: oklch(0.9568 0.0041 301.42);
+ --color-violet-200: oklch(0.8583 0.0137 290.71);
+ --color-violet-300: oklch(0.7358 0.0258 290.86);
+ --color-violet-400: oklch(0.6137 0.0375 288.91);
+ --color-violet-500: oklch(0.4915 0.0461 287.73);
+ --color-violet-600: oklch(0.368 0.0393 288.29);
+ --color-violet-700: oklch(0.2428 0.0265 287.52);
-.text-outline-dark {
- -webkit-text-stroke: 2px var(--violet);
- color: transparent;
- text-shadow: 2px 2px 0px var(--violet);
-}
+ --color-bone-100: oklch(0.904 0.0293 82.59);
+ --color-bone-200: oklch(0.7811 0.0391 82.99);
+ --color-bone-300: oklch(0.6571 0.0328 83.59);
+ --color-bone-400: oklch(0.5349 0.0266 82.03);
+ --color-bone-500: oklch(0.4113 0.0209 84.56);
+ --color-bone-600: oklch(0.2869 0.0137 81.69);
+ --color-bone-700: oklch(0.1785 0.0086 84.57);
-.text-outline-light {
- -webkit-text-stroke: 2px var(--bone);
- color: transparent;
- text-shadow: 2px 2px 0px var(--bone);
-}
+ --color-red-100: oklch(0.9193 0.0413 17.93);
+ --color-red-200: oklch(0.8034 0.1122 19.82);
+ --color-red-300: oklch(0.6909 0.1987 23.91);
+ --color-red-400: oklch(0.5732 0.2352 29.23);
+ --color-red-500: oklch(0.4432 0.181862 29.2339);
+ --color-red-600: oklch(0.3138 0.128763 29.2339);
+ --color-red-700: oklch(0.1866 0.0766 29.23);
-.text-outline-golden {
- -webkit-text-stroke: 2px var(--lion);
- color: transparent;
-}
+ --color-blue-100: oklch(0.911 0.033 256.76);
+ --color-blue-200: oklch(0.7858 0.0852 252.63);
+ --color-blue-300: oklch(0.6638 0.1168 247.31);
+ --color-blue-400: oklch(0.54 0.0951 247.39);
+ --color-blue-500: oklch(0.4159 0.0737 247.64);
+ --color-blue-600: oklch(0.2933 0.0525 248.23);
+ --color-blue-700: oklch(0.1768 0.0317 246.64);
-.bubble-text {
- font-family: var(--font-modak);
- letter-spacing: 0.05em;
- line-height: 1.1;
-}
+ --color-lion-100: oklch(0.9523 0.0135 53.35);
+ --color-lion-200: oklch(0.8323 0.0604 57.73);
+ --color-lion-300: oklch(0.7103 0.0803 60.22);
+ --color-lion-400: oklch(0.5868 0.0663 59.49);
+ --color-lion-500: oklch(0.4619 0.0522 61.59);
+ --color-lion-600: oklch(0.3382 0.038 61.11);
+ --color-lion-700: oklch(0.2158 0.0243 60.3);
-.rdp-root {
- --rdp-accent-color: var(--calendar-accent);
- --rdp-accent-background-color: var(--calendar-accent-background);
- --rdp-day_button-border: 3px solid transparent;
+ --color-gray-100: oklch(0.9709 0.0011 17.18);
+ --color-gray-200: oklch(0.8492 0.0055 17.27);
+ --color-gray-300: oklch(0.7257 0.014 17.48);
+ --color-gray-400: oklch(0.6034 0.0273 17.93);
+ --color-gray-500: oklch(0.4808 0.0223 17.96);
+ --color-gray-600: oklch(0.3573 0.017 17.98);
+ --color-gray-700: oklch(0.2347 0.011 17.96);
}
-.rdp-months {
- display: flex;
- justify-content: center;
-}
+/* OTHER VARIABLES */
-.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);
- }
+@layer base {
+ :root {
+ --background: var(--color-white);
+ --foreground: var(--color-violet);
+ --accent: var(--color-blue);
+ --accent-text: var(--color-blue-500);
+ --panel-color: oklch(1 0 0);
+ --loading-color: var(--color-gray-200);
}
- /* 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);
+ .dark {
+ --background: var(--color-violet);
+ --foreground: var(--color-bone);
+ --accent: var(--color-red);
+ --accent-text: var(--color-red-100);
+ --panel-color: oklch(0.3295 0.0403 287.95);
+ --loading-color: oklch(0.3295 0.0403 287.95);
}
-
- /* no shadow on nested glass */
- box-shadow: none;
}
-@custom-variant dark (&:where(.dark, .dark *));
-
@theme {
+ /* HELPER COLORS */
+ --color-error: oklch(0.6993 0.1534 12.87);
+
+ /* DYNAMIC COLORS */
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-accent: var(--accent);
+ --color-accent-text: var(--accent-text);
+ --color-panel: var(--panel-color);
+ --color-loading: var(--loading-color);
+
+ /* CALENDAR COLORS */
+ --calendar-accent: var(--color-accent);
+ --calendar-accent-background: color-mix(
+ in srgb,
+ var(--color-accent) 20%,
+ transparent
+ );
+
+ /* FONTS */
--font-modak: var(--font-modak);
--font-nunito: var(--font-nunito);
+ /* STROKES */
--text-stroke-white: var(--text-stroke-white);
--text-stroke-violet: var(--text-stroke-violet);
--text-stroke-bone: var(--text-stroke-bone);
@@ -152,6 +112,7 @@ body {
--text-stroke-red: var(--text-stroke-red);
--text-stroke-lion: var(--text-stroke-lion);
+ /* ANIMATIONS */
--animate-slideUpAndFade: slideUpAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1);
--animate-slideRightAndFade: slideRightAndFade 400ms
cubic-bezier(0.16, 1, 0.3, 1);
@@ -163,6 +124,7 @@ body {
--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-slideOut: slideOut 400ms cubic-bezier(0.16, 1, 0.3, 1);
--animate-swipeOut: swipeOut 100ms ease-out;
@keyframes hide {
@@ -185,6 +147,17 @@ body {
}
}
+ @keyframes slideOut {
+ 0% {
+ opacity: 1;
+ transform: translateX(0);
+ }
+ 100% {
+ opacity: 0;
+ transform: translateX(calc(100% + var(--viewport-padding)));
+ }
+ }
+
@keyframes swipeOut {
0% {
transform: translateX(var(--radix-toast-swipe-end-x));
@@ -219,7 +192,7 @@ body {
}
}
- @keyframes slideDownAndFade: {
+ @keyframes slideDownAndFade {
0% {
opacity: 0;
transform: translateY(-2px);
@@ -263,60 +236,3 @@ body {
}
}
}
-
-@theme {
- --color-white: oklch(0.9702 0 0);
- --color-violet: oklch(0.368 0.0393 288.29);
- --color-bone: oklch(0.904 0.0293 82.59);
- --color-red: oklch(0.6909 0.1987 23.91);
- --color-blue: oklch(0.54 0.0951 247.39);
- --color-lion: oklch(0.7103 0.0803 60.22);
-
- --color-violet-100: oklch(0.9568 0.0041 301.42);
- --color-violet-200: oklch(0.8583 0.0137 290.71);
- --color-violet-300: oklch(0.7358 0.0258 290.86);
- --color-violet-400: oklch(0.6137 0.0375 288.91);
- --color-violet-500: oklch(0.4915 0.0461 287.73);
- --color-violet-600: oklch(0.368 0.0393 288.29);
- --color-violet-700: oklch(0.2428 0.0265 287.52);
-
- --color-bone-100: oklch(0.904 0.0293 82.59);
- --color-bone-200: oklch(0.7811 0.0391 82.99);
- --color-bone-300: oklch(0.6571 0.0328 83.59);
- --color-bone-400: oklch(0.5349 0.0266 82.03);
- --color-bone-500: oklch(0.4113 0.0209 84.56);
- --color-bone-600: oklch(0.2869 0.0137 81.69);
- --color-bone-700: oklch(0.1785 0.0086 84.57);
-
- --color-red-100: oklch(0.9193 0.0413 17.93);
- --color-red-200: oklch(0.8034 0.1122 19.82);
- --color-red-300: oklch(0.6909 0.1987 23.91);
- --color-red-400: oklch(0.5732 0.2352 29.23);
- --color-red-500: oklch(0.4432 0.181862 29.2339);
- --color-red-600: oklch(0.3138 0.128763 29.2339);
- --color-red-700: oklch(0.1866 0.0766 29.23);
-
- --color-blue-100: oklch(0.911 0.033 256.76);
- --color-blue-200: oklch(0.7858 0.0852 252.63);
- --color-blue-300: oklch(0.6638 0.1168 247.31);
- --color-blue-400: oklch(0.54 0.0951 247.39);
- --color-blue-500: oklch(0.4159 0.0737 247.64);
- --color-blue-600: oklch(0.2933 0.0525 248.23);
- --color-blue-700: oklch(0.1768 0.0317 246.64);
-
- --color-lion-100: oklch(0.9523 0.0135 53.35);
- --color-lion-200: oklch(0.8323 0.0604 57.73);
- --color-lion-300: oklch(0.7103 0.0803 60.22);
- --color-lion-400: oklch(0.5868 0.0663 59.49);
- --color-lion-500: oklch(0.4619 0.0522 61.59);
- --color-lion-600: oklch(0.3382 0.038 61.11);
- --color-lion-700: oklch(0.2158 0.0243 60.3);
-
- --color-gray-100: oklch(0.9709 0.0011 17.18);
- --color-gray-200: oklch(0.8492 0.0055 17.27);
- --color-gray-300: oklch(0.7257 0.014 17.48);
- --color-gray-400: oklch(0.6034 0.0273 17.93);
- --color-gray-500: oklch(0.4808 0.0223 17.96);
- --color-gray-600: oklch(0.3573 0.017 17.98);
- --color-gray-700: oklch(0.2347 0.011 17.96);
-}
diff --git a/src/styles/text.css b/src/styles/text.css
new file mode 100644
index 00000000..85c129c7
--- /dev/null
+++ b/src/styles/text.css
@@ -0,0 +1,47 @@
+.font-display {
+ font-family: var(--font-modak);
+ letter-spacing: -0.02em;
+ line-height: 0.9;
+}
+
+.text-bone {
+ color: var(--color-bone);
+}
+
+.text-lion {
+ color: var(--color-lion);
+}
+
+.text-violet {
+ color: var(--color-violet);
+}
+
+/* BUBBLE TEXT */
+
+.bubble-text {
+ font-family: var(--font-modak);
+ letter-spacing: 0.05em;
+ line-height: 1.1;
+}
+
+.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-outline-light {
+ -webkit-text-stroke: 2px var(--bone);
+ color: transparent;
+ text-shadow: 2px 2px 0px var(--bone);
+}
+
+.text-outline-golden {
+ -webkit-text-stroke: 2px var(--lion);
+ color: transparent;
+}
diff --git a/tailwind.config.js b/tailwind.config.js
deleted file mode 100644
index d1cd40d4..00000000
--- a/tailwind.config.js
+++ /dev/null
@@ -1,42 +0,0 @@
-/** @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: [],
-};
diff --git a/tsconfig.json b/tsconfig.json
index d8b93235..44a24564 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -18,6 +18,7 @@
"name": "next"
}
],
+ "baseUrl": "./src",
"paths": {
"@/*": ["./*"]
}