From 963bf9ea0604ea4e4e4f5df3d59f34e09b5998b6 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Thu, 5 Feb 2026 15:32:53 +0530 Subject: [PATCH 1/7] feat: calendar --- .../src/core/protocol/graphql-server.ts | 7 + platforms/calendar-api/package.json | 30 + platforms/calendar-api/src/constants.ts | 25 + .../src/controllers/AuthController.ts | 124 ++ .../src/controllers/EventsController.ts | 109 ++ platforms/calendar-api/src/index.ts | 54 + platforms/calendar-api/src/middleware/auth.ts | 37 + .../src/services/EVaultService.ts | 305 +++++ platforms/calendar-api/src/utils/version.ts | 19 + platforms/calendar-api/tsconfig.json | 18 + pnpm-lock.yaml | 1082 ++++++++++++++++- 11 files changed, 1809 insertions(+), 1 deletion(-) create mode 100644 platforms/calendar-api/package.json create mode 100644 platforms/calendar-api/src/constants.ts create mode 100644 platforms/calendar-api/src/controllers/AuthController.ts create mode 100644 platforms/calendar-api/src/controllers/EventsController.ts create mode 100644 platforms/calendar-api/src/index.ts create mode 100644 platforms/calendar-api/src/middleware/auth.ts create mode 100644 platforms/calendar-api/src/services/EVaultService.ts create mode 100644 platforms/calendar-api/src/utils/version.ts create mode 100644 platforms/calendar-api/tsconfig.json diff --git a/infrastructure/evault-core/src/core/protocol/graphql-server.ts b/infrastructure/evault-core/src/core/protocol/graphql-server.ts index 608c99fe..67afd4f0 100644 --- a/infrastructure/evault-core/src/core/protocol/graphql-server.ts +++ b/infrastructure/evault-core/src/core/protocol/graphql-server.ts @@ -886,6 +886,13 @@ export class GraphQLServer { graphiql: { defaultQuery: exampleQueries, }, + maskedErrors: false, + logging: { + debug: (...args) => console.log('[GraphQL Debug]', ...args), + info: (...args) => console.log('[GraphQL Info]', ...args), + warn: (...args) => console.warn('[GraphQL Warn]', ...args), + error: (...args) => console.error('[GraphQL Error]', ...args), + }, context: async ({ request }) => { const authHeader = request.headers.get("authorization") ?? ""; const token = authHeader.replace("Bearer ", ""); diff --git a/platforms/calendar-api/package.json b/platforms/calendar-api/package.json new file mode 100644 index 00000000..e617bda5 --- /dev/null +++ b/platforms/calendar-api/package.json @@ -0,0 +1,30 @@ +{ + "name": "calendar-api", + "version": "1.0.0", + "description": "W3DS auth and eVault-backed calendar events API", + "main": "dist/index.js", + "scripts": { + "start": "node dist/index.js", + "dev": "ts-node src/index.ts", + "build": "tsc" + }, + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.18.2", + "graphql-request": "^6.1.0", + "jsonwebtoken": "^9.0.2", + "signature-validator": "workspace:*", + "uuid": "^9.0.1", + "axios": "^1.6.7" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.5", + "@types/node": "^20.11.19", + "@types/uuid": "^9.0.8", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" + } +} diff --git a/platforms/calendar-api/src/constants.ts b/platforms/calendar-api/src/constants.ts new file mode 100644 index 00000000..0a34d66d --- /dev/null +++ b/platforms/calendar-api/src/constants.ts @@ -0,0 +1,25 @@ +export const CALENDAR_EVENT_ONTOLOGY_ID = + "880e8400-e29b-41d4-a716-446655440099"; + +const SESSION_TTL_MS = 5 * 60 * 1000; // 5 minutes + +export interface StoredSession { + createdAt: number; +} + +export const sessionStore = new Map(); + +export function addSession(sessionId: string): void { + sessionStore.set(sessionId, { createdAt: Date.now() }); +} + +export function isSessionValid(sessionId: string): boolean { + const s = sessionStore.get(sessionId); + if (!s) return false; + if (Date.now() - s.createdAt > SESSION_TTL_MS) { + sessionStore.delete(sessionId); + return false; + } + sessionStore.delete(sessionId); // one-time use + return true; +} diff --git a/platforms/calendar-api/src/controllers/AuthController.ts b/platforms/calendar-api/src/controllers/AuthController.ts new file mode 100644 index 00000000..8cfd7d95 --- /dev/null +++ b/platforms/calendar-api/src/controllers/AuthController.ts @@ -0,0 +1,124 @@ +import { Request, Response } from "express"; +import { EventEmitter } from "events"; +import { v4 as uuidv4 } from "uuid"; +import jwt from "jsonwebtoken"; +import { verifySignature } from "signature-validator"; +import { isVersionValid } from "../utils/version"; +import { addSession, isSessionValid } from "../constants"; + +const MIN_REQUIRED_VERSION = "0.4.0"; +const JWT_EXPIRES_IN = "7d"; + +export class AuthController { + private eventEmitter = new EventEmitter(); + + getOffer = async (_req: Request, res: Response) => { + console.log("[auth] GET /api/auth/offer hit"); + const baseUrl = process.env.PUBLIC_CALENDAR_BASE_URL; + const redirectUri = new URL("/api/auth", baseUrl).toString(); + const session = uuidv4(); + addSession(session); + const offer = `w3ds://auth?redirect=${encodeURIComponent(redirectUri)}&session=${session}&platform=calendar`; + console.log("[auth] offer created, redirectUri=", redirectUri); + res.json({ uri: offer, sessionId: session }); + }; + + sseStream = async (req: Request, res: Response) => { + const { id } = req.params; + console.log("[auth] GET /api/auth/sessions/:id hit, sessionId=", id); + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + "Access-Control-Allow-Origin": "*", + }); + const handler = (data: unknown) => { + res.write(`data: ${JSON.stringify(data)}\n\n`); + }; + this.eventEmitter.on(id, handler); + const heartbeat = setInterval(() => { + try { + res.write(": heartbeat\n\n"); + } catch { + clearInterval(heartbeat); + } + }, 30000); + req.on("close", () => { + clearInterval(heartbeat); + this.eventEmitter.off(id, handler); + res.end(); + }); + }; + + login = async (req: Request, res: Response) => { + console.log("[auth] POST /api/auth hit"); + try { + const { ename, session, signature, appVersion } = req.body; + console.log("[auth] body: ename=", ename, "session=", session?.slice(0, 8) + "...", "appVersion=", appVersion, "signature present=", !!signature); + + if (!ename) { + console.log("[auth] reject: ename missing"); + return res.status(400).json({ error: "ename is required" }); + } + if (!session) { + console.log("[auth] reject: session missing"); + return res.status(400).json({ error: "session is required" }); + } + if (!signature) { + console.log("[auth] reject: signature missing"); + return res.status(400).json({ error: "signature is required" }); + } + + if (!isSessionValid(session)) { + console.log("[auth] reject: invalid or expired session"); + return res + .status(400) + .json({ error: "Invalid or expired session", message: "Please request a new login offer." }); + } + + if (!appVersion || !isVersionValid(appVersion, MIN_REQUIRED_VERSION)) { + console.log("[auth] reject: app version too old", appVersion); + return res.status(400).json({ + error: "App version too old", + message: `Please update eID Wallet to version ${MIN_REQUIRED_VERSION} or later.`, + }); + } + + const registryBaseUrl = process.env.PUBLIC_REGISTRY_URL; + if (!registryBaseUrl) { + console.log("[auth] reject: PUBLIC_REGISTRY_URL not set"); + return res.status(500).json({ error: "Server configuration error" }); + } + + console.log("[auth] verifying signature with registry", registryBaseUrl); + const verificationResult = await verifySignature({ + eName: ename, + signature, + payload: session, + registryBaseUrl, + }); + + if (!verificationResult.valid) { + console.log("[auth] reject: signature invalid", verificationResult.error); + return res.status(401).json({ + error: "Invalid signature", + message: verificationResult.error, + }); + } + + const secret = process.env.JWT_SECRET || "calendar-api-dev-secret"; + const token = jwt.sign( + { ename }, + secret, + { expiresIn: JWT_EXPIRES_IN } + ); + + console.log("[auth] login success, ename=", ename); + this.eventEmitter.emit(session, { token }); + res.status(200).json({ token }); + } catch (error) { + console.error("[auth] login error:", error); + res.status(500).json({ error: "Internal server error" }); + } + }; +} diff --git a/platforms/calendar-api/src/controllers/EventsController.ts b/platforms/calendar-api/src/controllers/EventsController.ts new file mode 100644 index 00000000..3051d752 --- /dev/null +++ b/platforms/calendar-api/src/controllers/EventsController.ts @@ -0,0 +1,109 @@ +import { Response } from "express"; +import { EVaultService } from "../services/EVaultService"; +import { AuthenticatedRequest } from "../middleware/auth"; + +const evaultService = new EVaultService(); + +export class EventsController { + list = async (req: AuthenticatedRequest, res: Response) => { + try { + const ename = req.user?.ename; + if (!ename) { + res.status(401).json({ error: "Unauthorized" }); + return; + } + const first = Math.min( + parseInt(String(req.query.first), 10) || 100, + 500 + ); + const after = (req.query.after as string) || undefined; + const events = await evaultService.listEvents(ename, first, after); + res.json(events); + } catch (error) { + console.error("List events error:", error); + res.status(500).json({ + error: "Failed to list events", + message: error instanceof Error ? error.message : "Unknown error", + }); + } + }; + + create = async (req: AuthenticatedRequest, res: Response) => { + try { + const ename = req.user?.ename; + if (!ename) { + res.status(401).json({ error: "Unauthorized" }); + return; + } + const { title, color, start, end } = req.body; + if (!title || !start || !end) { + res.status(400).json({ + error: "Missing required fields", + message: "title, start, and end are required", + }); + return; + } + const event = await evaultService.createEvent(ename, { + title, + color: color ?? "", + start, + end, + }); + res.status(201).json(event); + } catch (error) { + console.error("Create event error:", error); + res.status(500).json({ + error: "Failed to create event", + message: error instanceof Error ? error.message : "Unknown error", + }); + } + }; + + update = async (req: AuthenticatedRequest, res: Response) => { + try { + const ename = req.user?.ename; + if (!ename) { + res.status(401).json({ error: "Unauthorized" }); + return; + } + const id = req.params.id; + const { title, color, start, end } = req.body; + const payload: Record = {}; + if (title !== undefined) payload.title = title; + if (color !== undefined) payload.color = color; + if (start !== undefined) payload.start = start; + if (end !== undefined) payload.end = end; + if (Object.keys(payload).length === 0) { + res.status(400).json({ error: "No fields to update" }); + return; + } + const event = await evaultService.updateEvent(ename, id, payload); + res.json(event); + } catch (error) { + console.error("Update event error:", error); + res.status(500).json({ + error: "Failed to update event", + message: error instanceof Error ? error.message : "Unknown error", + }); + } + }; + + remove = async (req: AuthenticatedRequest, res: Response) => { + try { + const ename = req.user?.ename; + if (!ename) { + res.status(401).json({ error: "Unauthorized" }); + return; + } + const id = req.params.id; + await evaultService.removeEvent(ename, id); + res.status(204).send(); + } catch (error) { + console.error("Remove event error:", error); + res.status(500).json({ + error: "Failed to delete event", + message: error instanceof Error ? error.message : "Unknown error", + }); + } + } +} diff --git a/platforms/calendar-api/src/index.ts b/platforms/calendar-api/src/index.ts new file mode 100644 index 00000000..a736e16c --- /dev/null +++ b/platforms/calendar-api/src/index.ts @@ -0,0 +1,54 @@ +import dotenv from "dotenv"; +import fs from "fs"; +import path from "path"; +import express from "express"; +import cors from "cors"; + +// Load .env: try monorepo root, then calendar-api parent, then cwd (no fallback) +const candidates = [ + path.resolve(__dirname, "../../../.env"), // repo root from dist/ + path.resolve(__dirname, "../../.env"), // repo root from src/ or platforms/.env + path.resolve(process.cwd(), ".env"), +]; +const envPath = candidates.find((p) => fs.existsSync(p)); +if (envPath) { + dotenv.config({ path: envPath }); +} else { + console.warn( + "No .env found at", + candidates.join(", "), + "- env vars must be set by shell or elsewhere" + ); +} +import { AuthController } from "./controllers/AuthController"; +import { EventsController } from "./controllers/EventsController"; +import { authMiddleware } from "./middleware/auth"; + +const app = express(); +const port = process.env.PORT ?? 4001; + +app.use( + cors({ + origin: true, + methods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"], + allowedHeaders: ["Content-Type", "Authorization"], + credentials: true, + }) +); +app.use(express.json()); + +const authController = new AuthController(); +const eventsController = new EventsController(); + +app.get("/api/auth/offer", authController.getOffer); +app.get("/api/auth/sessions/:id", authController.sseStream); +app.post("/api/auth", authController.login); + +app.get("/api/events", authMiddleware, eventsController.list); +app.post("/api/events", authMiddleware, eventsController.create); +app.patch("/api/events/:id", authMiddleware, eventsController.update); +app.delete("/api/events/:id", authMiddleware, eventsController.remove); + +app.listen(port, () => { + console.log(`Calendar API running on port ${port}`); +}); diff --git a/platforms/calendar-api/src/middleware/auth.ts b/platforms/calendar-api/src/middleware/auth.ts new file mode 100644 index 00000000..28a2c00c --- /dev/null +++ b/platforms/calendar-api/src/middleware/auth.ts @@ -0,0 +1,37 @@ +import { Request, Response, NextFunction } from "express"; +import jwt from "jsonwebtoken"; + +export interface AuthPayload { + ename: string; +} + +export interface AuthenticatedRequest extends Request { + user?: AuthPayload; +} + +export function authMiddleware( + req: AuthenticatedRequest, + res: Response, + next: NextFunction +): void { + const authHeader = req.headers.authorization; + if (!authHeader?.startsWith("Bearer ")) { + res.status(401).json({ error: "Missing or invalid Authorization header" }); + return; + } + + const token = authHeader.slice(7); + const secret = process.env.JWT_SECRET || "calendar-api-dev-secret"; + + try { + const decoded = jwt.verify(token, secret) as AuthPayload; + if (!decoded.ename) { + res.status(401).json({ error: "Invalid token payload" }); + return; + } + req.user = { ename: decoded.ename }; + next(); + } catch { + res.status(401).json({ error: "Invalid or expired token" }); + } +} diff --git a/platforms/calendar-api/src/services/EVaultService.ts b/platforms/calendar-api/src/services/EVaultService.ts new file mode 100644 index 00000000..4c59383b --- /dev/null +++ b/platforms/calendar-api/src/services/EVaultService.ts @@ -0,0 +1,305 @@ +import { GraphQLClient } from "graphql-request"; +import { CALENDAR_EVENT_ONTOLOGY_ID } from "../constants"; + +const META_ENVELOPES_QUERY = ` + query MetaEnvelopes($filter: MetaEnvelopeFilterInput, $first: Int, $after: String) { + metaEnvelopes(filter: $filter, first: $first, after: $after) { + edges { + cursor + node { + id + ontology + parsed + } + } + pageInfo { + hasNextPage + endCursor + } + totalCount + } + } +`; + +const CREATE_MUTATION = ` + mutation CreateMetaEnvelope($input: MetaEnvelopeInput!) { + createMetaEnvelope(input: $input) { + metaEnvelope { + id + ontology + parsed + } + errors { field message code } + } + } +`; + +const UPDATE_MUTATION = ` + mutation UpdateMetaEnvelope($id: ID!, $input: MetaEnvelopeInput!) { + updateMetaEnvelope(id: $id, input: $input) { + metaEnvelope { + id + ontology + parsed + } + errors { message code } + } + } +`; + +const META_ENVELOPE_QUERY = ` + query MetaEnvelope($id: ID!) { + metaEnvelope(id: $id) { + id + parsed + } + } +`; + +const REMOVE_MUTATION = ` + mutation RemoveMetaEnvelope($id: ID!) { + removeMetaEnvelope(id: $id) { + deletedId + success + errors { message code } + } + } +`; + +export interface CalendarEventPayload { + title: string; + color?: string; + start: string; + end: string; +} + +export interface CalendarEventResponse { + id: string; + title: string; + color: string; + start: string; + end: string; +} + +function getEvaultGraphqlUrl(): string { + const base = process.env.PUBLIC_EVAULT_SERVER_URI || "http://localhost:4000"; + const normalized = base.replace(/\/$/, ""); + return `${normalized}/graphql`; +} + +interface PlatformTokenResponse { + token: string; + expiresAt?: number; +} + +export class EVaultService { + private platformToken: string | null = null; + private tokenExpiresAt: number = 0; + + private async ensurePlatformToken(): Promise { + const now = Date.now(); + if (this.platformToken && this.tokenExpiresAt > now + 5 * 60 * 1000) { + return this.platformToken; + } + + const registryUrl = process.env.PUBLIC_REGISTRY_URL; + if (!registryUrl) { + throw new Error("PUBLIC_REGISTRY_URL not configured"); + } + + const baseUrl = process.env.PUBLIC_CALENDAR_BASE_URL; + if (!baseUrl) { + throw new Error("PUBLIC_CALENDAR_BASE_URL not configured"); + } + + const response = await fetch( + new URL("/platforms/certification", registryUrl).toString(), + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ platform: baseUrl }), + } + ); + + if (!response.ok) { + throw new Error(`Failed to get platform token: HTTP ${response.status}`); + } + + const data = (await response.json()) as PlatformTokenResponse; + this.platformToken = data.token; + this.tokenExpiresAt = data.expiresAt || now + 3600000; + console.log("[EVaultService] Platform token obtained for", baseUrl, "expires at", new Date(this.tokenExpiresAt).toISOString()); + return this.platformToken; + } + + private async getClient(eName: string): Promise { + const endpoint = getEvaultGraphqlUrl(); + const token = await this.ensurePlatformToken(); + console.log("[EVaultService] Creating client for endpoint:", endpoint, "eName:", eName); + return new GraphQLClient(endpoint, { + headers: { + Authorization: `Bearer ${token}`, + "X-ENAME": eName, + }, + }); + } + + async listEvents( + eName: string, + first = 100, + after?: string + ): Promise { + console.log("[EVaultService.listEvents] eName:", eName, "first:", first, "after:", after); + const client = await this.getClient(eName); + const variables: { + filter: { ontologyId: string }; + first: number; + after?: string; + } = { + filter: { ontologyId: CALENDAR_EVENT_ONTOLOGY_ID }, + first, + }; + if (after != null && after !== "") { + variables.after = after; + } + console.log("[EVaultService.listEvents] variables:", JSON.stringify(variables)); + type MetaEnvelopesResult = { + metaEnvelopes: { + edges: Array<{ + node: { id: string; parsed: Record }; + }>; + }; + }; + let result: MetaEnvelopesResult; + try { + result = await client.request(META_ENVELOPES_QUERY, variables); + } catch (err: unknown) { + const msg = + err && + typeof err === "object" && + "response" in err + ? JSON.stringify((err as { response?: unknown }).response) + : String(err); + console.error("[EVaultService] metaEnvelopes request failed:", msg); + throw new Error(`eVault query failed: ${msg}`); + } + + return result.metaEnvelopes.edges.map((edge) => { + const p = edge.node.parsed as Record; + return { + id: edge.node.id, + title: (p.title as string) ?? "", + color: (p.color as string) ?? "", + start: (p.start as string) ?? "", + end: (p.end as string) ?? "", + }; + }); + } + + async createEvent( + eName: string, + payload: CalendarEventPayload + ): Promise { + const client = await this.getClient(eName); + const result = await client.request<{ + createMetaEnvelope: { + metaEnvelope: { id: string; parsed: Record } | null; + errors: Array<{ message: string }>; + }; + }>(CREATE_MUTATION, { + input: { + ontology: CALENDAR_EVENT_ONTOLOGY_ID, + payload: { + title: payload.title, + color: payload.color ?? "", + start: payload.start, + end: payload.end, + }, + acl: ["*"], + }, + }); + + const { metaEnvelope, errors } = result.createMetaEnvelope; + if (errors?.length) { + throw new Error(errors.map((e) => e.message).join("; ")); + } + if (!metaEnvelope) { + throw new Error("Create failed: no metaEnvelope returned"); + } + + const p = metaEnvelope.parsed as Record; + return { + id: metaEnvelope.id, + title: (p.title as string) ?? "", + color: (p.color as string) ?? "", + start: (p.start as string) ?? "", + end: (p.end as string) ?? "", + }; + } + + async updateEvent( + eName: string, + id: string, + partial: Partial + ): Promise { + const client = await this.getClient(eName); + const existing = await client.request<{ + metaEnvelope: { id: string; parsed: Record } | null; + }>(META_ENVELOPE_QUERY, { id }); + if (!existing.metaEnvelope?.parsed) { + throw new Error("Event not found"); + } + const current = existing.metaEnvelope.parsed as Record; + const payload: CalendarEventPayload = { + title: (partial.title as string) ?? (current.title as string), + color: (partial.color as string) ?? (current.color as string) ?? "", + start: (partial.start as string) ?? (current.start as string), + end: (partial.end as string) ?? (current.end as string), + }; + + const result = await client.request<{ + updateMetaEnvelope: { + metaEnvelope: { id: string; parsed: Record } | null; + errors: Array<{ message: string }>; + }; + }>(UPDATE_MUTATION, { + id, + input: { + ontology: CALENDAR_EVENT_ONTOLOGY_ID, + payload, + acl: ["*"], + }, + }); + + const { metaEnvelope, errors } = result.updateMetaEnvelope; + if (errors?.length) { + throw new Error(errors.map((e) => e.message).join("; ")); + } + if (!metaEnvelope) { + throw new Error("Update failed: no metaEnvelope returned"); + } + + const p = metaEnvelope.parsed as Record; + return { + id: metaEnvelope.id, + title: (p.title as string) ?? "", + color: (p.color as string) ?? "", + start: (p.start as string) ?? "", + end: (p.end as string) ?? "", + }; + } + + async removeEvent(eName: string, id: string): Promise { + const client = await this.getClient(eName); + const result = await client.request<{ + removeMetaEnvelope: { success: boolean; errors: Array<{ message: string }> }; + }>(REMOVE_MUTATION, { id }); + + if (result.removeMetaEnvelope.errors?.length) { + throw new Error( + result.removeMetaEnvelope.errors.map((e) => e.message).join("; ") + ); + } + return result.removeMetaEnvelope.success; + } +} diff --git a/platforms/calendar-api/src/utils/version.ts b/platforms/calendar-api/src/utils/version.ts new file mode 100644 index 00000000..3f856902 --- /dev/null +++ b/platforms/calendar-api/src/utils/version.ts @@ -0,0 +1,19 @@ +export function compareVersions(version1: string, version2: string): number { + const v1Parts = version1.split(".").map(Number); + const v2Parts = version2.split(".").map(Number); + + for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) { + const v1Part = v1Parts[i] ?? 0; + const v2Part = v2Parts[i] ?? 0; + if (v1Part < v2Part) return -1; + if (v1Part > v2Part) return 1; + } + return 0; +} + +export function isVersionValid( + appVersion: string, + minVersion: string +): boolean { + return compareVersions(appVersion, minVersion) >= 0; +} diff --git a/platforms/calendar-api/tsconfig.json b/platforms/calendar-api/tsconfig.json new file mode 100644 index 00000000..1b1efaaf --- /dev/null +++ b/platforms/calendar-api/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "moduleResolution": "node", + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9073f325..c578be2e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -897,6 +897,236 @@ importers: specifier: ^5.3.3 version: 5.8.2 + platforms/calendar: + dependencies: + '@hookform/resolvers': + specifier: ^3.9.1 + version: 3.10.0(react-hook-form@7.68.0(react@18.3.1)) + '@radix-ui/react-accordion': + specifier: ^1.2.2 + version: 1.2.12(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-alert-dialog': + specifier: ^1.1.4 + version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-aspect-ratio': + specifier: ^1.1.1 + version: 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-avatar': + specifier: ^1.1.2 + version: 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-checkbox': + specifier: ^1.1.3 + version: 1.3.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collapsible': + specifier: ^1.1.2 + version: 1.1.12(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-context-menu': + specifier: ^2.2.4 + version: 2.2.16(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dialog': + specifier: ^1.1.4 + version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.4 + version: 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-hover-card': + specifier: ^1.1.4 + version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-label': + specifier: ^2.1.1 + version: 2.1.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-menubar': + specifier: ^1.1.4 + version: 1.1.16(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-navigation-menu': + specifier: ^1.2.3 + version: 1.2.14(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popover': + specifier: ^1.1.4 + version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-progress': + specifier: ^1.1.1 + version: 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-radio-group': + specifier: ^1.2.2 + version: 1.3.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-scroll-area': + specifier: ^1.2.2 + version: 1.2.10(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-select': + specifier: ^2.1.4 + version: 2.2.6(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-separator': + specifier: ^1.1.1 + version: 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slider': + specifier: ^1.2.2 + version: 1.3.6(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': + specifier: ^1.1.1 + version: 1.2.4(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-switch': + specifier: ^1.1.2 + version: 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tabs': + specifier: ^1.1.2 + version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toast': + specifier: ^1.2.4 + version: 1.2.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toggle': + specifier: ^1.1.1 + version: 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toggle-group': + specifier: ^1.1.1 + version: 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tooltip': + specifier: ^1.1.6 + version: 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@remixicon/react': + specifier: ^4.6.0 + version: 4.9.0(react@18.3.1) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + cmdk: + specifier: 1.0.0 + version: 1.0.0(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + date-fns: + specifier: ^4.1.0 + version: 4.1.0 + embla-carousel-react: + specifier: ^8.5.1 + version: 8.6.0(react@18.3.1) + framer-motion: + specifier: ^11.17.0 + version: 11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + input-otp: + specifier: ^1.4.1 + version: 1.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + lucide-react: + specifier: ^0.469.0 + version: 0.469.0(react@18.3.1) + next: + specifier: 15.1.2 + version: 15.1.2(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.96.0) + next-themes: + specifier: ^0.4.4 + version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: + specifier: 18.3.1 + version: 18.3.1 + react-day-picker: + specifier: 8.10.1 + version: 8.10.1(date-fns@4.1.0)(react@18.3.1) + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) + react-hook-form: + specifier: ^7.54.1 + version: 7.68.0(react@18.3.1) + react-resizable-panels: + specifier: ^2.1.7 + version: 2.1.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + recharts: + specifier: ^2.15.0 + version: 2.15.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + sonner: + specifier: ^1.7.1 + version: 1.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tailwind-merge: + specifier: ^2.5.5 + version: 2.6.0 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.1)) + vaul: + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + zod: + specifier: ^3.24.1 + version: 3.25.76 + devDependencies: + '@eslint/eslintrc': + specifier: ^3 + version: 3.3.3 + '@types/node': + specifier: ^20 + version: 20.19.26 + '@types/react': + specifier: 18.3.27 + version: 18.3.27 + '@types/react-dom': + specifier: 18.3.7 + version: 18.3.7(@types/react@18.3.27) + eslint: + specifier: ^9 + version: 9.39.1(jiti@2.6.1) + eslint-config-next: + specifier: 15.1.2 + version: 15.1.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2) + postcss: + specifier: ^8 + version: 8.5.6 + tailwindcss: + specifier: ^3.4.1 + version: 3.4.18(tsx@4.21.0)(yaml@2.8.1) + typescript: + specifier: ^5 + version: 5.8.2 + + platforms/calendar-api: + dependencies: + axios: + specifier: ^1.6.7 + version: 1.13.2 + cors: + specifier: ^2.8.5 + version: 2.8.5 + dotenv: + specifier: ^16.4.5 + version: 16.6.1 + express: + specifier: ^4.18.2 + version: 4.22.1 + graphql-request: + specifier: ^6.1.0 + version: 6.1.0(encoding@0.1.13)(graphql@16.12.0) + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.3 + signature-validator: + specifier: workspace:* + version: link:../../infrastructure/signature-validator + uuid: + specifier: ^9.0.1 + version: 9.0.1 + devDependencies: + '@types/cors': + specifier: ^2.8.17 + version: 2.8.19 + '@types/express': + specifier: ^4.17.21 + version: 4.17.25 + '@types/jsonwebtoken': + specifier: ^9.0.5 + version: 9.0.10 + '@types/node': + specifier: ^20.11.19 + version: 20.19.26 + '@types/uuid': + specifier: ^9.0.8 + version: 9.0.8 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@20.19.26)(typescript@5.8.2) + typescript: + specifier: ^5.3.3 + version: 5.8.2 + platforms/cerberus: dependencies: axios: @@ -6090,33 +6320,65 @@ packages: resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} engines: {node: '>=18'} + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + '@img/sharp-darwin-arm64@0.34.5': resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + '@img/sharp-darwin-x64@0.34.5': resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + '@img/sharp-libvips-darwin-arm64@1.2.4': resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} cpu: [arm64] os: [darwin] + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + '@img/sharp-libvips-darwin-x64@1.2.4': resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} cpu: [x64] os: [darwin] + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + '@img/sharp-libvips-linux-arm64@1.2.4': resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] @@ -6132,32 +6394,64 @@ packages: cpu: [riscv64] os: [linux] + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -6176,30 +6470,59 @@ packages: cpu: [riscv64] os: [linux] + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -6211,12 +6534,24 @@ packages: cpu: [arm64] os: [win32] + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + '@img/sharp-win32-ia32@0.34.5': resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@img/sharp-win32-x64@0.34.5': resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -6752,54 +7087,108 @@ packages: '@neondatabase/serverless@0.10.4': resolution: {integrity: sha512-2nZuh3VUO9voBauuh+IGYRhGU/MskWHt1IuZvHcJw6GLjDgtqj/KViKo7SIrLdGLdot7vFbiRRw+BgEy3wT9HA==} + '@next/env@15.1.2': + resolution: {integrity: sha512-Hm3jIGsoUl6RLB1vzY+dZeqb+/kWPZ+h34yiWxW0dV87l8Im/eMOwpOA+a0L78U0HM04syEjXuRlCozqpwuojQ==} + '@next/env@15.5.9': resolution: {integrity: sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==} + '@next/eslint-plugin-next@15.1.2': + resolution: {integrity: sha512-sgfw3+WdaYOGPKCvM1L+UucBmRfh8V2Ygefp7ELON0+0vY7uohQwXXnVWg3rY7mXDKharQR3o7uedpfvnU2hlQ==} + '@next/eslint-plugin-next@15.5.9': resolution: {integrity: sha512-kUzXx0iFiXw27cQAViE1yKWnz/nF8JzRmwgMRTMh8qMY90crNsdXJRh2e+R0vBpFR3kk1yvAR7wev7+fCCb79Q==} + '@next/swc-darwin-arm64@15.1.2': + resolution: {integrity: sha512-b9TN7q+j5/7+rGLhFAVZiKJGIASuo8tWvInGfAd8wsULjB1uNGRCj1z1WZwwPWzVQbIKWFYqc+9L7W09qwt52w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + '@next/swc-darwin-arm64@15.5.7': resolution: {integrity: sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] + '@next/swc-darwin-x64@15.1.2': + resolution: {integrity: sha512-caR62jNDUCU+qobStO6YJ05p9E+LR0EoXh1EEmyU69cYydsAy7drMcOlUlRtQihM6K6QfvNwJuLhsHcCzNpqtA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + '@next/swc-darwin-x64@15.5.7': resolution: {integrity: sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] + '@next/swc-linux-arm64-gnu@15.1.2': + resolution: {integrity: sha512-fHHXBusURjBmN6VBUtu6/5s7cCeEkuGAb/ZZiGHBLVBXMBy4D5QpM8P33Or8JD1nlOjm/ZT9sEE5HouQ0F+hUA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + '@next/swc-linux-arm64-gnu@15.5.7': resolution: {integrity: sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + '@next/swc-linux-arm64-musl@15.1.2': + resolution: {integrity: sha512-9CF1Pnivij7+M3G74lxr+e9h6o2YNIe7QtExWq1KUK4hsOLTBv6FJikEwCaC3NeYTflzrm69E5UfwEAbV2U9/g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + '@next/swc-linux-arm64-musl@15.5.7': resolution: {integrity: sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + '@next/swc-linux-x64-gnu@15.1.2': + resolution: {integrity: sha512-tINV7WmcTUf4oM/eN3Yuu/f8jQ5C6AkueZPKeALs/qfdfX57eNv4Ij7rt0SA6iZ8+fMobVfcFVv664Op0caCCg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + '@next/swc-linux-x64-gnu@15.5.7': resolution: {integrity: sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + '@next/swc-linux-x64-musl@15.1.2': + resolution: {integrity: sha512-jf2IseC4WRsGkzeUw/cK3wci9pxR53GlLAt30+y+B+2qAQxMw6WAC3QrANIKxkcoPU3JFh/10uFfmoMDF9JXKg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + '@next/swc-linux-x64-musl@15.5.7': resolution: {integrity: sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + '@next/swc-win32-arm64-msvc@15.1.2': + resolution: {integrity: sha512-wvg7MlfnaociP7k8lxLX4s2iBJm4BrNiNFhVUY+Yur5yhAJHfkS8qPPeDEUH8rQiY0PX3u/P7Q/wcg6Mv6GSAA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + '@next/swc-win32-arm64-msvc@15.5.7': resolution: {integrity: sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] + '@next/swc-win32-x64-msvc@15.1.2': + resolution: {integrity: sha512-D3cNA8NoT3aWISWmo7HF5Eyko/0OdOO+VagkoJuiTk7pyX3P/b+n8XA/MYvyR+xSVcbKn68B1rY9fgqjNISqzQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + '@next/swc-win32-x64-msvc@15.5.7': resolution: {integrity: sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==} engines: {node: '>= 10'} @@ -7026,6 +7415,9 @@ packages: '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + '@radix-ui/primitive@1.0.1': + resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} + '@radix-ui/primitive@1.1.3': resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} @@ -7133,6 +7525,15 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-compose-refs@1.0.1': + resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} + peerDependencies: + '@types/react': 18.3.27 + react: 18.3.1 + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-compose-refs@1.1.2': resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} peerDependencies: @@ -7155,6 +7556,15 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-context@1.0.1': + resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==} + peerDependencies: + '@types/react': 18.3.27 + react: 18.3.1 + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-context@1.1.2': resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} peerDependencies: @@ -7173,6 +7583,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-dialog@1.0.5': + resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==} + peerDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7 + react: 18.3.1 + react-dom: 18.3.1 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-dialog@1.1.15': resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} peerDependencies: @@ -7195,6 +7618,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-dismissable-layer@1.0.5': + resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==} + peerDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7 + react: 18.3.1 + react-dom: 18.3.1 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-dismissable-layer@1.1.11': resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} peerDependencies: @@ -7221,6 +7657,15 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-focus-guards@1.0.1': + resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==} + peerDependencies: + '@types/react': 18.3.27 + react: 18.3.1 + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-focus-guards@1.1.3': resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} peerDependencies: @@ -7230,6 +7675,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-focus-scope@1.0.4': + resolution: {integrity: sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==} + peerDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7 + react: 18.3.1 + react-dom: 18.3.1 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-focus-scope@1.1.7': resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} peerDependencies: @@ -7256,6 +7714,15 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-id@1.0.1': + resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==} + peerDependencies: + '@types/react': 18.3.27 + react: 18.3.1 + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-id@1.1.1': resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} peerDependencies: @@ -7343,6 +7810,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-portal@1.0.4': + resolution: {integrity: sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==} + peerDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7 + react: 18.3.1 + react-dom: 18.3.1 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-portal@1.1.9': resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} peerDependencies: @@ -7356,6 +7836,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-presence@1.0.1': + resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==} + peerDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7 + react: 18.3.1 + react-dom: 18.3.1 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-presence@1.1.5': resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} peerDependencies: @@ -7369,6 +7862,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-primitive@1.0.3': + resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==} + peerDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7 + react: 18.3.1 + react-dom: 18.3.1 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-primitive@2.1.3': resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} peerDependencies: @@ -7486,6 +7992,15 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-slot@1.0.2': + resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} + peerDependencies: + '@types/react': 18.3.27 + react: 18.3.1 + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-slot@1.2.3': resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} peerDependencies: @@ -7582,6 +8097,15 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-use-callback-ref@1.0.1': + resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==} + peerDependencies: + '@types/react': 18.3.27 + react: 18.3.1 + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-callback-ref@1.1.1': resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} peerDependencies: @@ -7591,6 +8115,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-controllable-state@1.0.1': + resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==} + peerDependencies: + '@types/react': 18.3.27 + react: 18.3.1 + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-controllable-state@1.2.2': resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} peerDependencies: @@ -7609,6 +8142,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-escape-keydown@1.0.3': + resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==} + peerDependencies: + '@types/react': 18.3.27 + react: 18.3.1 + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-escape-keydown@1.1.1': resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} peerDependencies: @@ -7627,6 +8169,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-layout-effect@1.0.1': + resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==} + peerDependencies: + '@types/react': 18.3.27 + react: 18.3.1 + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-layout-effect@1.1.1': resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} peerDependencies: @@ -7682,6 +8233,11 @@ packages: '@remirror/core-constants@3.0.0': resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} + '@remixicon/react@4.9.0': + resolution: {integrity: sha512-5/jLDD4DtKxH2B4QVXTobvV1C2uL8ab9D5yAYNtFt+w80O0Ys1xFOrspqROL3fjrZi+7ElFUWE37hBfaAl6U+Q==} + peerDependencies: + react: 18.3.1 + '@repeaterjs/repeater@3.0.6': resolution: {integrity: sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA==} @@ -8318,6 +8874,9 @@ packages: resolution: {integrity: sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==} engines: {node: '>=14'} + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -10657,6 +11216,12 @@ packages: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} + cmdk@1.0.0: + resolution: {integrity: sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==} + peerDependencies: + react: 18.3.1 + react-dom: 18.3.1 + cmdk@1.1.1: resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==} peerDependencies: @@ -10683,10 +11248,17 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + color-support@1.1.3: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} hasBin: true + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + colord@2.9.3: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} @@ -11947,6 +12519,15 @@ packages: engines: {node: '>=6.0'} hasBin: true + eslint-config-next@15.1.2: + resolution: {integrity: sha512-PrMm1/4zWSJ689wd/ypWIR5ZF1uvmp3EkgpgBV1Yu6PhEobBjXMGgT8bVNelwl17LXojO8D5ePFRiI4qXjsPRA==} + peerDependencies: + eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 + typescript: '>=3.3.1' + peerDependenciesMeta: + typescript: + optional: true + eslint-config-next@15.5.9: resolution: {integrity: sha512-852JYI3NkFNzW8CqsMhI0K2CDRxTObdZ2jQJj5CtpEaOkYHn13107tHpNuD/h0WRpU4FAbCdUaxQsrfBtNK9Kw==} peerDependencies: @@ -13242,6 +13823,9 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-arrayish@0.3.4: + resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} + is-async-function@2.1.1: resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} engines: {node: '>= 0.4'} @@ -14244,6 +14828,11 @@ packages: lru-queue@0.1.0: resolution: {integrity: sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==} + lucide-react@0.469.0: + resolution: {integrity: sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw==} + peerDependencies: + react: 18.3.1 + lucide-react@0.561.0: resolution: {integrity: sha512-Y59gMY38tl4/i0qewcqohPdEbieBy7SovpBL9IFebhc2mDd8x4PZSOsiFRkpPcOq6bj1r/mjH/Rk73gSlIJP2A==} peerDependencies: @@ -14826,6 +15415,28 @@ packages: next-tick@1.1.0: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} + next@15.1.2: + resolution: {integrity: sha512-nLJDV7peNy+0oHlmY2JZjzMfJ8Aj0/dd3jCwSZS8ZiO5nkQfcZRqDrRN3U5rJtqVTQneIOGZzb6LCNrk7trMCQ==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details. + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.41.2 + babel-plugin-react-compiler: '*' + react: 18.3.1 + react-dom: 18.3.1 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + next@15.5.9: resolution: {integrity: sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} @@ -16381,6 +16992,16 @@ packages: '@types/react': optional: true + react-remove-scroll@2.5.5: + resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': 18.3.27 + react: 18.3.1 + peerDependenciesMeta: + '@types/react': + optional: true + react-remove-scroll@2.7.1: resolution: {integrity: sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==} engines: {node: '>=10'} @@ -16909,6 +17530,10 @@ packages: shallowequal@1.1.0: resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -16957,6 +17582,9 @@ packages: simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + simple-swizzle@0.2.4: + resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} + simple-update-notifier@2.0.0: resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} engines: {node: '>=10'} @@ -17014,6 +17642,12 @@ packages: sonic-boom@4.2.0: resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + sonner@1.7.4: + resolution: {integrity: sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==} + peerDependencies: + react: 18.3.1 + react-dom: 18.3.1 + sorcery@0.11.1: resolution: {integrity: sha512-o7npfeJE6wi6J9l0/5LKshFzZ2rMatRiCDwYeDQaOzqdzRJwALhX7mk/A/ecg6wjMu7wdZbmXfD2S/vpOg0bdQ==} hasBin: true @@ -22642,25 +23276,47 @@ snapshots: '@img/colour@1.0.0': optional: true + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + '@img/sharp-darwin-arm64@0.34.5': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.2.4 optional: true + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + '@img/sharp-darwin-x64@0.34.5': optionalDependencies: '@img/sharp-libvips-darwin-x64': 1.2.4 optional: true + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + '@img/sharp-libvips-darwin-arm64@1.2.4': optional: true + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + '@img/sharp-libvips-darwin-x64@1.2.4': optional: true + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + '@img/sharp-libvips-linux-arm64@1.2.4': optional: true + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + '@img/sharp-libvips-linux-arm@1.2.4': optional: true @@ -22670,23 +23326,45 @@ snapshots: '@img/sharp-libvips-linux-riscv64@1.2.4': optional: true + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + '@img/sharp-libvips-linux-s390x@1.2.4': optional: true + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + '@img/sharp-libvips-linux-x64@1.2.4': optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': optional: true + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + '@img/sharp-libvips-linuxmusl-x64@1.2.4': optional: true + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + '@img/sharp-linux-arm64@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-arm64': 1.2.4 optional: true + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + '@img/sharp-linux-arm@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-arm': 1.2.4 @@ -22702,26 +23380,51 @@ snapshots: '@img/sharp-libvips-linux-riscv64': 1.2.4 optional: true + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + '@img/sharp-linux-s390x@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-s390x': 1.2.4 optional: true + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + '@img/sharp-linux-x64@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-x64': 1.2.4 optional: true + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + '@img/sharp-linuxmusl-arm64@0.34.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 optional: true + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + '@img/sharp-linuxmusl-x64@0.34.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-x64': 1.2.4 optional: true + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.7.1 + optional: true + '@img/sharp-wasm32@0.34.5': dependencies: '@emnapi/runtime': 1.7.1 @@ -22730,9 +23433,15 @@ snapshots: '@img/sharp-win32-arm64@0.34.5': optional: true + '@img/sharp-win32-ia32@0.33.5': + optional: true + '@img/sharp-win32-ia32@0.34.5': optional: true + '@img/sharp-win32-x64@0.33.5': + optional: true + '@img/sharp-win32-x64@0.34.5': optional: true @@ -23775,33 +24484,63 @@ snapshots: dependencies: '@types/pg': 8.11.6 + '@next/env@15.1.2': {} + '@next/env@15.5.9': {} + '@next/eslint-plugin-next@15.1.2': + dependencies: + fast-glob: 3.3.1 + '@next/eslint-plugin-next@15.5.9': dependencies: fast-glob: 3.3.1 + '@next/swc-darwin-arm64@15.1.2': + optional: true + '@next/swc-darwin-arm64@15.5.7': optional: true + '@next/swc-darwin-x64@15.1.2': + optional: true + '@next/swc-darwin-x64@15.5.7': optional: true + '@next/swc-linux-arm64-gnu@15.1.2': + optional: true + '@next/swc-linux-arm64-gnu@15.5.7': optional: true + '@next/swc-linux-arm64-musl@15.1.2': + optional: true + '@next/swc-linux-arm64-musl@15.5.7': optional: true + '@next/swc-linux-x64-gnu@15.1.2': + optional: true + '@next/swc-linux-x64-gnu@15.5.7': optional: true + '@next/swc-linux-x64-musl@15.1.2': + optional: true + '@next/swc-linux-x64-musl@15.5.7': optional: true + '@next/swc-win32-arm64-msvc@15.1.2': + optional: true + '@next/swc-win32-arm64-msvc@15.5.7': optional: true + '@next/swc-win32-x64-msvc@15.1.2': + optional: true + '@next/swc-win32-x64-msvc@15.5.7': optional: true @@ -24045,6 +24784,10 @@ snapshots: '@radix-ui/number@1.1.1': {} + '@radix-ui/primitive@1.0.1': + dependencies: + '@babel/runtime': 7.28.4 + '@radix-ui/primitive@1.1.3': {} '@radix-ui/react-accordion@1.2.12(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -24153,6 +24896,13 @@ snapshots: '@types/react': 18.3.27 '@types/react-dom': 18.3.7(@types/react@18.3.27) + '@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.27)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.27)(react@18.3.1)': dependencies: react: 18.3.1 @@ -24173,6 +24923,13 @@ snapshots: '@types/react': 18.3.27 '@types/react-dom': 18.3.7(@types/react@18.3.27) + '@radix-ui/react-context@1.0.1(@types/react@18.3.27)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + '@radix-ui/react-context@1.1.2(@types/react@18.3.27)(react@18.3.1)': dependencies: react: 18.3.1 @@ -24185,6 +24942,29 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 + '@radix-ui/react-dialog@1.0.5(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-context': 1.0.1(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.0.1(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.27)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.5.5(@types/react@18.3.27)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) + '@radix-ui/react-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -24213,6 +24993,20 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 + '@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -24241,12 +25035,31 @@ snapshots: '@types/react': 18.3.27 '@types/react-dom': 18.3.7(@types/react@18.3.27) + '@radix-ui/react-focus-guards@1.0.1(@types/react@18.3.27)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + '@radix-ui/react-focus-guards@1.1.3(@types/react@18.3.27)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: '@types/react': 18.3.27 + '@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) @@ -24275,6 +25088,14 @@ snapshots: '@types/react': 18.3.27 '@types/react-dom': 18.3.7(@types/react@18.3.27) + '@radix-ui/react-id@1.0.1(@types/react@18.3.27)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + '@radix-ui/react-id@1.1.1(@types/react@18.3.27)(react@18.3.1)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.27)(react@18.3.1) @@ -24398,6 +25219,16 @@ snapshots: '@types/react': 18.3.27 '@types/react-dom': 18.3.7(@types/react@18.3.27) + '@radix-ui/react-portal@1.0.4(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) + '@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -24408,6 +25239,17 @@ snapshots: '@types/react': 18.3.27 '@types/react-dom': 18.3.7(@types/react@18.3.27) + '@radix-ui/react-presence@1.0.1(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) + '@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) @@ -24418,6 +25260,16 @@ snapshots: '@types/react': 18.3.27 '@types/react-dom': 18.3.7(@types/react@18.3.27) + '@radix-ui/react-primitive@1.0.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) + '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-slot': 1.2.3(@types/react@18.3.27)(react@18.3.1) @@ -24555,6 +25407,14 @@ snapshots: '@types/react': 18.3.27 '@types/react-dom': 18.3.7(@types/react@18.3.27) + '@radix-ui/react-slot@1.0.2(@types/react@18.3.27)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + '@radix-ui/react-slot@1.2.3(@types/react@18.3.27)(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) @@ -24666,12 +25526,27 @@ snapshots: '@types/react': 18.3.27 '@types/react-dom': 18.3.7(@types/react@18.3.27) + '@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.3.27)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.27)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: '@types/react': 18.3.27 + '@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.3.27)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.27)(react@18.3.1)': dependencies: '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.27)(react@18.3.1) @@ -24687,6 +25562,14 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 + '@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.3.27)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.27)(react@18.3.1)': dependencies: '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.27)(react@18.3.1) @@ -24701,6 +25584,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 + '@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.3.27)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.27)(react@18.3.1)': dependencies: react: 18.3.1 @@ -24740,6 +25630,10 @@ snapshots: '@remirror/core-constants@3.0.0': {} + '@remixicon/react@4.9.0(react@18.3.1)': + dependencies: + react: 18.3.1 + '@repeaterjs/repeater@3.0.6': {} '@replit/vite-plugin-cartographer@0.4.4': @@ -25618,6 +26512,8 @@ snapshots: - supports-color - typescript + '@swc/counter@0.1.3': {} + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -28447,7 +29343,7 @@ snapshots: caniuse-api@3.0.0: dependencies: browserslist: 4.28.0 - caniuse-lite: 1.0.30001756 + caniuse-lite: 1.0.30001765 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 @@ -28640,6 +29536,16 @@ snapshots: cluster-key-slot@1.1.2: optional: true + cmdk@1.0.0(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + cmdk@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) @@ -28674,9 +29580,21 @@ snapshots: color-name@1.1.4: {} + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.4 + optional: true + color-support@1.1.3: optional: true + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + optional: true + colord@2.9.3: {} colorette@2.0.20: {} @@ -30109,6 +31027,26 @@ snapshots: optionalDependencies: source-map: 0.6.1 + eslint-config-next@15.1.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2): + dependencies: + '@next/eslint-plugin-next': 15.1.2 + '@rushstack/eslint-patch': 1.15.0 + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2) + '@typescript-eslint/parser': 5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2) + eslint: 9.39.1(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-react-hooks: 5.2.0(eslint@9.39.1(jiti@2.6.1)) + optionalDependencies: + typescript: 5.8.2 + transitivePeerDependencies: + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + eslint-config-next@15.5.9(eslint@8.57.1)(typescript@5.0.4): dependencies: '@next/eslint-plugin-next': 15.5.9 @@ -30161,6 +31099,21 @@ snapshots: transitivePeerDependencies: - supports-color + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3(supports-color@5.5.0) + eslint: 9.39.1(jiti@2.6.1) + get-tsconfig: 4.13.0 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.15 + unrs-resolver: 1.11.1 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 @@ -30202,6 +31155,17 @@ snapshots: transitivePeerDependencies: - supports-color + eslint-module-utils@2.12.1(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2) + eslint: 9.39.1(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + eslint-module-utils@2.12.1(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 @@ -30242,6 +31206,35 @@ snapshots: - eslint-import-resolver-webpack - supports-color + eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.39.1(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 @@ -32043,6 +33036,9 @@ snapshots: is-arrayish@0.2.1: {} + is-arrayish@0.3.4: + optional: true + is-async-function@2.1.1: dependencies: async-function: 1.0.0 @@ -33534,6 +34530,10 @@ snapshots: dependencies: es5-ext: 0.10.64 + lucide-react@0.469.0(react@18.3.1): + dependencies: + react: 18.3.1 + lucide-react@0.561.0(react@18.3.1): dependencies: react: 18.3.1 @@ -34448,6 +35448,33 @@ snapshots: next-tick@1.1.0: {} + next@15.1.2(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.96.0): + dependencies: + '@next/env': 15.1.2 + '@swc/counter': 0.1.3 + '@swc/helpers': 0.5.15 + busboy: 1.6.0 + caniuse-lite: 1.0.30001765 + postcss: 8.4.31 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-jsx: 5.1.6(babel-plugin-macros@3.1.0)(react@18.3.1) + optionalDependencies: + '@next/swc-darwin-arm64': 15.1.2 + '@next/swc-darwin-x64': 15.1.2 + '@next/swc-linux-arm64-gnu': 15.1.2 + '@next/swc-linux-arm64-musl': 15.1.2 + '@next/swc-linux-x64-gnu': 15.1.2 + '@next/swc-linux-x64-musl': 15.1.2 + '@next/swc-win32-arm64-msvc': 15.1.2 + '@next/swc-win32-x64-msvc': 15.1.2 + '@opentelemetry/api': 1.9.0 + sass: 1.96.0 + sharp: 0.33.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + next@15.5.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.96.0): dependencies: '@next/env': 15.5.9 @@ -36054,6 +37081,11 @@ snapshots: date-fns: 3.6.0 react: 18.3.1 + react-day-picker@8.10.1(date-fns@4.1.0)(react@18.3.1): + dependencies: + date-fns: 4.1.0 + react: 18.3.1 + react-day-picker@9.11.1(react@18.3.1): dependencies: '@date-fns/tz': 1.4.1 @@ -36182,6 +37214,17 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 + react-remove-scroll@2.5.5(@types/react@18.3.27)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.8(@types/react@18.3.27)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.27)(react@18.3.1) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@18.3.27)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@18.3.27)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + react-remove-scroll@2.7.1(@types/react@18.3.27)(react@18.3.1): dependencies: react: 18.3.1 @@ -36928,6 +37971,33 @@ snapshots: shallowequal@1.1.0: {} + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.1.2 + semver: 7.7.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + optional: true + sharp@0.34.5: dependencies: '@img/colour': 1.0.0 @@ -37010,6 +38080,11 @@ snapshots: once: 1.4.0 simple-concat: 1.0.1 + simple-swizzle@0.2.4: + dependencies: + is-arrayish: 0.3.4 + optional: true + simple-update-notifier@2.0.0: dependencies: semver: 7.7.3 @@ -37081,6 +38156,11 @@ snapshots: dependencies: atomic-sleep: 1.0.0 + sonner@1.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + sorcery@0.11.1: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 From 652bdb4c75d802556213f2071e4154e896e0d202 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Thu, 5 Feb 2026 15:42:38 +0530 Subject: [PATCH 2/7] fix: coderabbit suggestions --- .../src/core/protocol/graphql-server.ts | 7 ------- .../src/controllers/AuthController.ts | 18 +++++++++++++++--- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/infrastructure/evault-core/src/core/protocol/graphql-server.ts b/infrastructure/evault-core/src/core/protocol/graphql-server.ts index 67afd4f0..608c99fe 100644 --- a/infrastructure/evault-core/src/core/protocol/graphql-server.ts +++ b/infrastructure/evault-core/src/core/protocol/graphql-server.ts @@ -886,13 +886,6 @@ export class GraphQLServer { graphiql: { defaultQuery: exampleQueries, }, - maskedErrors: false, - logging: { - debug: (...args) => console.log('[GraphQL Debug]', ...args), - info: (...args) => console.log('[GraphQL Info]', ...args), - warn: (...args) => console.warn('[GraphQL Warn]', ...args), - error: (...args) => console.error('[GraphQL Error]', ...args), - }, context: async ({ request }) => { const authHeader = request.headers.get("authorization") ?? ""; const token = authHeader.replace("Bearer ", ""); diff --git a/platforms/calendar-api/src/controllers/AuthController.ts b/platforms/calendar-api/src/controllers/AuthController.ts index 8cfd7d95..2be480ee 100644 --- a/platforms/calendar-api/src/controllers/AuthController.ts +++ b/platforms/calendar-api/src/controllers/AuthController.ts @@ -15,11 +15,23 @@ export class AuthController { getOffer = async (_req: Request, res: Response) => { console.log("[auth] GET /api/auth/offer hit"); const baseUrl = process.env.PUBLIC_CALENDAR_BASE_URL; - const redirectUri = new URL("/api/auth", baseUrl).toString(); + if (!baseUrl) { + console.error("[auth] PUBLIC_CALENDAR_BASE_URL is not set"); + return res.status(500).json({ error: "Server configuration error: PUBLIC_CALENDAR_BASE_URL not set" }); + } + + let redirectUri: string; + try { + redirectUri = new URL("/api/auth", baseUrl).toString(); + } catch (err) { + console.error("[auth] Invalid PUBLIC_CALENDAR_BASE_URL:", baseUrl, err); + return res.status(500).json({ error: "Server configuration error: invalid base URL" }); + } + const session = uuidv4(); addSession(session); - const offer = `w3ds://auth?redirect=${encodeURIComponent(redirectUri)}&session=${session}&platform=calendar`; - console.log("[auth] offer created, redirectUri=", redirectUri); + const offer = `w3ds://auth?redirect=${encodeURIComponent(redirectUri)}&session=${session}&platform=${encodeURIComponent(baseUrl)}`; + console.log("[auth] offer created, redirectUri=", redirectUri, "platform=", baseUrl); res.json({ uri: offer, sessionId: session }); }; From 40ead1db99839cc6c3cf3c969615bb2d53920db6 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Thu, 5 Feb 2026 15:49:06 +0530 Subject: [PATCH 3/7] chore: add calendar --- platforms/calendar/.gitignore | 41 + platforms/calendar/README.md | 77 ++ platforms/calendar/app/favicon.ico | Bin 0 -> 25931 bytes platforms/calendar/app/globals.css | 113 +++ platforms/calendar/app/layout.tsx | 54 ++ platforms/calendar/app/page.tsx | 5 + platforms/calendar/components.json | 21 + .../calendar/components/auth/login-screen.tsx | 138 ++++ .../calendar/components/calendar-demo.tsx | 88 ++ .../calendar/body/calendar-body-header.tsx | 35 + .../calendar/body/calendar-body.tsx | 16 + .../body/day/calendar-body-day-calendar.tsx | 13 + .../body/day/calendar-body-day-content.tsx | 27 + .../body/day/calendar-body-day-events.tsx | 35 + .../calendar/body/day/calendar-body-day.tsx | 25 + .../day/calendar-body-margin-day-margin.tsx | 32 + .../body/month/calendar-body-month.tsx | 139 ++++ .../calendar/body/week/calendar-body-week.tsx | 31 + .../components/calendar/calendar-context.tsx | 14 + .../components/calendar/calendar-event.tsx | 151 ++++ .../calendar/calendar-mode-icon-map.tsx | 8 + .../components/calendar/calendar-provider.tsx | 56 ++ .../calendar/calendar-tailwind-classes.ts | 69 ++ .../components/calendar/calendar-types.ts | 30 + .../calendar/components/calendar/calendar.tsx | 41 + .../dialog/calendar-manage-event-dialog.tsx | 243 ++++++ .../dialog/calendar-new-event-dialog.tsx | 166 ++++ .../actions/calendar-header-actions-add.tsx | 16 + .../actions/calendar-header-actions-mode.tsx | 129 +++ .../actions/calendar-header-actions.tsx | 11 + .../calendar/header/calendar-header.tsx | 11 + .../date/calendar-header-date-badge.tsx | 14 + .../date/calendar-header-date-chevrons.tsx | 68 ++ .../header/date/calendar-header-date-icon.tsx | 16 + .../header/date/calendar-header-date.tsx | 21 + .../calendar/components/form/color-picker.tsx | 34 + .../components/form/date-time-picker.tsx | 159 ++++ .../components/header/header-github.tsx | 18 + .../components/header/header-theme-toggle.tsx | 56 ++ .../calendar/components/header/header.tsx | 31 + .../calendar/components/ui/accordion.tsx | 57 ++ .../calendar/components/ui/alert-dialog.tsx | 141 ++++ platforms/calendar/components/ui/alert.tsx | 59 ++ .../calendar/components/ui/aspect-ratio.tsx | 7 + platforms/calendar/components/ui/avatar.tsx | 50 ++ platforms/calendar/components/ui/badge.tsx | 36 + .../calendar/components/ui/breadcrumb.tsx | 115 +++ platforms/calendar/components/ui/button.tsx | 57 ++ platforms/calendar/components/ui/calendar.tsx | 76 ++ platforms/calendar/components/ui/card.tsx | 76 ++ platforms/calendar/components/ui/carousel.tsx | 262 ++++++ platforms/calendar/components/ui/chart.tsx | 365 +++++++++ platforms/calendar/components/ui/checkbox.tsx | 30 + .../calendar/components/ui/collapsible.tsx | 11 + platforms/calendar/components/ui/command.tsx | 153 ++++ .../calendar/components/ui/context-menu.tsx | 200 +++++ platforms/calendar/components/ui/dialog.tsx | 122 +++ platforms/calendar/components/ui/drawer.tsx | 118 +++ .../calendar/components/ui/dropdown-menu.tsx | 201 +++++ platforms/calendar/components/ui/form.tsx | 178 ++++ .../calendar/components/ui/hover-card.tsx | 29 + .../calendar/components/ui/input-otp.tsx | 71 ++ platforms/calendar/components/ui/input.tsx | 22 + platforms/calendar/components/ui/label.tsx | 26 + platforms/calendar/components/ui/menubar.tsx | 236 ++++++ .../components/ui/navigation-menu.tsx | 128 +++ .../calendar/components/ui/pagination.tsx | 117 +++ platforms/calendar/components/ui/popover.tsx | 33 + platforms/calendar/components/ui/progress.tsx | 28 + .../calendar/components/ui/radio-group.tsx | 44 + .../calendar/components/ui/resizable.tsx | 45 ++ .../calendar/components/ui/scroll-area.tsx | 48 ++ platforms/calendar/components/ui/select.tsx | 159 ++++ .../calendar/components/ui/separator.tsx | 31 + platforms/calendar/components/ui/sheet.tsx | 140 ++++ platforms/calendar/components/ui/sidebar.tsx | 763 ++++++++++++++++++ platforms/calendar/components/ui/skeleton.tsx | 15 + platforms/calendar/components/ui/slider.tsx | 28 + platforms/calendar/components/ui/sonner.tsx | 31 + platforms/calendar/components/ui/switch.tsx | 29 + platforms/calendar/components/ui/table.tsx | 120 +++ platforms/calendar/components/ui/tabs.tsx | 55 ++ platforms/calendar/components/ui/textarea.tsx | 22 + platforms/calendar/components/ui/toast.tsx | 129 +++ platforms/calendar/components/ui/toaster.tsx | 35 + .../calendar/components/ui/toggle-group.tsx | 61 ++ platforms/calendar/components/ui/toggle.tsx | 45 ++ platforms/calendar/components/ui/tooltip.tsx | 32 + platforms/calendar/contexts/auth-context.tsx | 60 ++ platforms/calendar/eslint.config.mjs | 16 + platforms/calendar/hooks/use-mobile.tsx | 19 + platforms/calendar/hooks/use-toast.ts | 194 +++++ platforms/calendar/lib/calendar-api.ts | 111 +++ .../calendar/lib/mock-calendar-events.ts | 62 ++ platforms/calendar/lib/utils.ts | 6 + platforms/calendar/next.config.ts | 7 + platforms/calendar/package.json | 75 ++ platforms/calendar/postcss.config.mjs | 8 + platforms/calendar/public/file.svg | 1 + platforms/calendar/public/globe.svg | 1 + platforms/calendar/public/next.svg | 1 + platforms/calendar/public/vercel.svg | 1 + platforms/calendar/public/window.svg | 1 + platforms/calendar/tailwind.config.ts | 98 +++ platforms/calendar/tsconfig.json | 27 + 105 files changed, 7776 insertions(+) create mode 100644 platforms/calendar/.gitignore create mode 100644 platforms/calendar/README.md create mode 100644 platforms/calendar/app/favicon.ico create mode 100644 platforms/calendar/app/globals.css create mode 100644 platforms/calendar/app/layout.tsx create mode 100644 platforms/calendar/app/page.tsx create mode 100644 platforms/calendar/components.json create mode 100644 platforms/calendar/components/auth/login-screen.tsx create mode 100644 platforms/calendar/components/calendar-demo.tsx create mode 100644 platforms/calendar/components/calendar/body/calendar-body-header.tsx create mode 100644 platforms/calendar/components/calendar/body/calendar-body.tsx create mode 100644 platforms/calendar/components/calendar/body/day/calendar-body-day-calendar.tsx create mode 100644 platforms/calendar/components/calendar/body/day/calendar-body-day-content.tsx create mode 100644 platforms/calendar/components/calendar/body/day/calendar-body-day-events.tsx create mode 100644 platforms/calendar/components/calendar/body/day/calendar-body-day.tsx create mode 100644 platforms/calendar/components/calendar/body/day/calendar-body-margin-day-margin.tsx create mode 100644 platforms/calendar/components/calendar/body/month/calendar-body-month.tsx create mode 100644 platforms/calendar/components/calendar/body/week/calendar-body-week.tsx create mode 100644 platforms/calendar/components/calendar/calendar-context.tsx create mode 100644 platforms/calendar/components/calendar/calendar-event.tsx create mode 100644 platforms/calendar/components/calendar/calendar-mode-icon-map.tsx create mode 100644 platforms/calendar/components/calendar/calendar-provider.tsx create mode 100644 platforms/calendar/components/calendar/calendar-tailwind-classes.ts create mode 100644 platforms/calendar/components/calendar/calendar-types.ts create mode 100644 platforms/calendar/components/calendar/calendar.tsx create mode 100644 platforms/calendar/components/calendar/dialog/calendar-manage-event-dialog.tsx create mode 100644 platforms/calendar/components/calendar/dialog/calendar-new-event-dialog.tsx create mode 100644 platforms/calendar/components/calendar/header/actions/calendar-header-actions-add.tsx create mode 100644 platforms/calendar/components/calendar/header/actions/calendar-header-actions-mode.tsx create mode 100644 platforms/calendar/components/calendar/header/actions/calendar-header-actions.tsx create mode 100644 platforms/calendar/components/calendar/header/calendar-header.tsx create mode 100644 platforms/calendar/components/calendar/header/date/calendar-header-date-badge.tsx create mode 100644 platforms/calendar/components/calendar/header/date/calendar-header-date-chevrons.tsx create mode 100644 platforms/calendar/components/calendar/header/date/calendar-header-date-icon.tsx create mode 100644 platforms/calendar/components/calendar/header/date/calendar-header-date.tsx create mode 100644 platforms/calendar/components/form/color-picker.tsx create mode 100644 platforms/calendar/components/form/date-time-picker.tsx create mode 100644 platforms/calendar/components/header/header-github.tsx create mode 100644 platforms/calendar/components/header/header-theme-toggle.tsx create mode 100644 platforms/calendar/components/header/header.tsx create mode 100644 platforms/calendar/components/ui/accordion.tsx create mode 100644 platforms/calendar/components/ui/alert-dialog.tsx create mode 100644 platforms/calendar/components/ui/alert.tsx create mode 100644 platforms/calendar/components/ui/aspect-ratio.tsx create mode 100644 platforms/calendar/components/ui/avatar.tsx create mode 100644 platforms/calendar/components/ui/badge.tsx create mode 100644 platforms/calendar/components/ui/breadcrumb.tsx create mode 100644 platforms/calendar/components/ui/button.tsx create mode 100644 platforms/calendar/components/ui/calendar.tsx create mode 100644 platforms/calendar/components/ui/card.tsx create mode 100644 platforms/calendar/components/ui/carousel.tsx create mode 100644 platforms/calendar/components/ui/chart.tsx create mode 100644 platforms/calendar/components/ui/checkbox.tsx create mode 100644 platforms/calendar/components/ui/collapsible.tsx create mode 100644 platforms/calendar/components/ui/command.tsx create mode 100644 platforms/calendar/components/ui/context-menu.tsx create mode 100644 platforms/calendar/components/ui/dialog.tsx create mode 100644 platforms/calendar/components/ui/drawer.tsx create mode 100644 platforms/calendar/components/ui/dropdown-menu.tsx create mode 100644 platforms/calendar/components/ui/form.tsx create mode 100644 platforms/calendar/components/ui/hover-card.tsx create mode 100644 platforms/calendar/components/ui/input-otp.tsx create mode 100644 platforms/calendar/components/ui/input.tsx create mode 100644 platforms/calendar/components/ui/label.tsx create mode 100644 platforms/calendar/components/ui/menubar.tsx create mode 100644 platforms/calendar/components/ui/navigation-menu.tsx create mode 100644 platforms/calendar/components/ui/pagination.tsx create mode 100644 platforms/calendar/components/ui/popover.tsx create mode 100644 platforms/calendar/components/ui/progress.tsx create mode 100644 platforms/calendar/components/ui/radio-group.tsx create mode 100644 platforms/calendar/components/ui/resizable.tsx create mode 100644 platforms/calendar/components/ui/scroll-area.tsx create mode 100644 platforms/calendar/components/ui/select.tsx create mode 100644 platforms/calendar/components/ui/separator.tsx create mode 100644 platforms/calendar/components/ui/sheet.tsx create mode 100644 platforms/calendar/components/ui/sidebar.tsx create mode 100644 platforms/calendar/components/ui/skeleton.tsx create mode 100644 platforms/calendar/components/ui/slider.tsx create mode 100644 platforms/calendar/components/ui/sonner.tsx create mode 100644 platforms/calendar/components/ui/switch.tsx create mode 100644 platforms/calendar/components/ui/table.tsx create mode 100644 platforms/calendar/components/ui/tabs.tsx create mode 100644 platforms/calendar/components/ui/textarea.tsx create mode 100644 platforms/calendar/components/ui/toast.tsx create mode 100644 platforms/calendar/components/ui/toaster.tsx create mode 100644 platforms/calendar/components/ui/toggle-group.tsx create mode 100644 platforms/calendar/components/ui/toggle.tsx create mode 100644 platforms/calendar/components/ui/tooltip.tsx create mode 100644 platforms/calendar/contexts/auth-context.tsx create mode 100644 platforms/calendar/eslint.config.mjs create mode 100644 platforms/calendar/hooks/use-mobile.tsx create mode 100644 platforms/calendar/hooks/use-toast.ts create mode 100644 platforms/calendar/lib/calendar-api.ts create mode 100644 platforms/calendar/lib/mock-calendar-events.ts create mode 100644 platforms/calendar/lib/utils.ts create mode 100644 platforms/calendar/next.config.ts create mode 100644 platforms/calendar/package.json create mode 100644 platforms/calendar/postcss.config.mjs create mode 100644 platforms/calendar/public/file.svg create mode 100644 platforms/calendar/public/globe.svg create mode 100644 platforms/calendar/public/next.svg create mode 100644 platforms/calendar/public/vercel.svg create mode 100644 platforms/calendar/public/window.svg create mode 100644 platforms/calendar/tailwind.config.ts create mode 100644 platforms/calendar/tsconfig.json diff --git a/platforms/calendar/.gitignore b/platforms/calendar/.gitignore new file mode 100644 index 00000000..5ef6a520 --- /dev/null +++ b/platforms/calendar/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/platforms/calendar/README.md b/platforms/calendar/README.md new file mode 100644 index 00000000..235e446e --- /dev/null +++ b/platforms/calendar/README.md @@ -0,0 +1,77 @@ +# React/ShadCN Calendar + +This calendar is built with shadcn & tailwind. Has all the aspects you'd want in a calendar for a dashboard/app! + +Make sure to give this repo a star! + +## Features + +- 🎨 Fully customizable with Tailwind CSS +- 🌓 Dark mode support +- 🎯 Accessible components using Radix UI +- 📱 Responsive design +- 🔄 Multiple view modes +- 📅 Advanced event management + +## Modes + +### Day + +- Detailed single day view +- Hour-by-hour breakdown +- Event management +- Time-slot selection + +### Week + +- 7-day view +- Multiple event display +- Quick navigation + +### Month + +- Full month overview +- Event preview +- Quick date selection +- Today highlighting + +## Dialog Features + +### Create Event Dialog + +- 📝 Event title input +- 🕒 Date/time picker for start and end times +- 🎨 Color picker for event categorization +- ✅ Form validation +- 🔄 Real-time preview + +### Manage Event Dialog + +- ✏️ Edit existing events +- 🗑️ Delete events +- 🕒 Modify date/time +- 🎨 Update event color +- ⚡ Quick actions + +### Date/Time Picker + +- 📅 Calendar date selection +- ⏰ Hour selection (24-hour format) + +[From RDSX](https://time.rdsx.dev/) + +## Contributing + +Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. + +## Original Inspiriation + +[Synergy CRM](https://synergy-platform.vercel.app/calendar) + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=charlietlamb/calendar&type=Date)](https://star-history.com/#charlietlamb/calendar&Date) + +## License + +MIT diff --git a/platforms/calendar/app/favicon.ico b/platforms/calendar/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/platforms/calendar/app/globals.css b/platforms/calendar/app/globals.css new file mode 100644 index 00000000..8e192839 --- /dev/null +++ b/platforms/calendar/app/globals.css @@ -0,0 +1,113 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + font-family: Arial, Helvetica, sans-serif; +} + +@layer base { + :root, + .light { + --background: 0 0% 95.69%; + --foreground: 0 0% 14.51%; + + --card: var(--background); + --card-foreground: var(--foreground); + + --popover: 0 0% 91.37%; + --popover-foreground: 0 0% 3.92%; + + --primary: 194 100% 44%; + --primary-foreground: 210 40% 98.04%; + + --secondary: 0 0% 86.27%; + --secondary-foreground: 0 0% 10.2%; + + --muted: 210 15.38% 89.8%; + --muted-foreground: 216 10.2% 48.04%; + + --accent: 0 0% 91.37%; + --accent-foreground: 0 0% 10.2%; + + --destructive: 0 63.87% 53.33%; + --destructive-foreground: 0 0% 98.04%; + + --border: 0 0% 81.96%; + --input: 0 0% 81.96%; + + --ring: 0 0% 3.92%; + + --chart-1: var(--primary); + --chart-2: 220 85% 65%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + + --sidebar-background: 0 0% 91.37%; + --sidebar-foreground: 0 0% 3.92%; + --sidebar-primary: 267 85.86% 38.82%; + --sidebar-primary-foreground: 210 40% 98.04%; + + --sidebar-accent: 0 0% 91.37%; + --sidebar-accent-foreground: 0 0% 10.2%; + --sidebar-border: 0 0% 81.96%; + --sidebar-ring: 0 0% 3.92%; + + --radius: 0.75rem; + } + .dark { + --background: 0 0% 7.06%; + --foreground: 0 0% 89.8%; + + --card: 0 0% 11.37%; + --card-foreground: 0 0% 94.51%; + + --popover: 0 0% 11.37%; + --popover-foreground: 0 0% 94.51%; + + --primary: 194 100% 56%; + --primary-foreground: 0 0% 100%; + + --secondary: 0 0% 17.25%; + --secondary-foreground: 0 0% 89.8%; + + --muted: 0 0% 22.75%; + --muted-foreground: 0 0% 64.71%; + + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + + --destructive: 349 52.24% 60.59%; + --destructive-foreground: 0 0% 100%; + + --border: 0 0% 22.75%; + --input: 0 0% 22.75%; + --ring: 0 0% 89.8%; + + --chart-1: var(--primary); + --chart-2: 220 85% 65%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + + --sidebar-background: 0 0% 11.37%; + --sidebar-foreground: 0 0% 94.51%; + --sidebar-primary: 267 95.16% 75.69%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 0 0% 98%; + --sidebar-border: 0 0% 22.75%; + --sidebar-ring: 0 0% 89.8%; + + --radius: 0.75rem; + } +} +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/platforms/calendar/app/layout.tsx b/platforms/calendar/app/layout.tsx new file mode 100644 index 00000000..f4c04c88 --- /dev/null +++ b/platforms/calendar/app/layout.tsx @@ -0,0 +1,54 @@ +import type { Metadata } from 'next' +import { Geist, Geist_Mono } from 'next/font/google' +import './globals.css' +import { ThemeProvider } from 'next-themes' +import { AuthProvider } from '@/contexts/auth-context' +import Header from '@/components/header/header' + +const geistSans = Geist({ + subsets: ['latin'], + variable: '--font-geist-sans', + display: 'swap', + adjustFontFallback: false, +}) + +const geistMono = Geist_Mono({ + subsets: ['latin'], + variable: '--font-geist-mono', + display: 'swap', + adjustFontFallback: false, +}) + +export const metadata: Metadata = { + title: 'React/Shadcn Calendar', + description: 'By @charlietlamb', +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + + + +
+ {children} + + + + + ) +} diff --git a/platforms/calendar/app/page.tsx b/platforms/calendar/app/page.tsx new file mode 100644 index 00000000..e7ea92fc --- /dev/null +++ b/platforms/calendar/app/page.tsx @@ -0,0 +1,5 @@ +import CalendarDemo from '@/components/calendar-demo' + +export default function Home() { + return +} diff --git a/platforms/calendar/components.json b/platforms/calendar/components.json new file mode 100644 index 00000000..dea737b8 --- /dev/null +++ b/platforms/calendar/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/platforms/calendar/components/auth/login-screen.tsx b/platforms/calendar/components/auth/login-screen.tsx new file mode 100644 index 00000000..f14735b3 --- /dev/null +++ b/platforms/calendar/components/auth/login-screen.tsx @@ -0,0 +1,138 @@ +'use client' + +import { useCallback, useEffect, useState } from 'react' +import { QRCodeSVG } from 'qrcode.react' +import { calendarApi, parseSessionFromUri } from '@/lib/calendar-api' +import { useAuth } from '@/contexts/auth-context' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' + +const API_URL = + process.env.NEXT_PUBLIC_CALENDAR_API_URL || 'http://localhost:4001' + +export default function LoginScreen() { + const { login } = useAuth() + const [uri, setUri] = useState('') + const [sessionId, setSessionId] = useState('') + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [connecting, setConnecting] = useState(false) + + const fetchOffer = useCallback(async () => { + setLoading(true) + setError(null) + try { + const data = await calendarApi.getOffer() + setUri(data.uri) + const sid = data.sessionId ?? parseSessionFromUri(data.uri) + setSessionId(sid ?? '') + } catch (e) { + setError(e instanceof Error ? e.message : 'Failed to load login') + setUri('') + setSessionId('') + } finally { + setLoading(false) + } + }, []) + + useEffect(() => { + fetchOffer() + }, [fetchOffer]) + + useEffect(() => { + if (!sessionId) return + const eventSource = new EventSource( + `${API_URL}/api/auth/sessions/${sessionId}` + ) + eventSource.onmessage = (event) => { + try { + const data = JSON.parse(event.data) as { token?: string } + if (data.token) { + setConnecting(true) + login(data.token) + eventSource.close() + } + } catch { + // ignore parse errors + } + } + eventSource.onerror = () => { + eventSource.close() + } + return () => eventSource.close() + }, [sessionId, login]) + + if (loading) { + return ( +
+

Loading login…

+
+ ) + } + + if (error) { + return ( +
+ + + Login unavailable + + +

{error}

+ +
+
+
+ ) + } + + return ( +
+ + + Sign in with W3DS +

+ Use your eID Wallet to sign in. This app does not store your + password; you keep control of your identity. +

+
+ + {connecting && ( +

+ Signing in… +

+ )} + + {/* Desktop: show QR code */} +
+

+ Scan with your eID Wallet app +

+ {uri && ( +
+ +
+ )} +
+ + {/* Mobile: show button */} +
+

+ Open in your eID Wallet app +

+ +
+
+
+
+ ) +} diff --git a/platforms/calendar/components/calendar-demo.tsx b/platforms/calendar/components/calendar-demo.tsx new file mode 100644 index 00000000..c3aa0109 --- /dev/null +++ b/platforms/calendar/components/calendar-demo.tsx @@ -0,0 +1,88 @@ +'use client' + +import { useCallback, useEffect, useState } from 'react' +import Calendar from './calendar/calendar' +import { CalendarEvent, Mode } from './calendar/calendar-types' +import { useAuth } from '@/contexts/auth-context' +import LoginScreen from '@/components/auth/login-screen' +import { calendarApi } from '@/lib/calendar-api' + +function mapApiToEvent(e: { + id: string + title: string + color: string + start: string + end: string +}): CalendarEvent { + return { + id: e.id, + title: e.title, + color: e.color || 'blue', + start: new Date(e.start), + end: new Date(e.end), + } +} + +export default function CalendarDemo() { + const { isAuthenticated, isReady } = useAuth() + const [events, setEvents] = useState([]) + const [mode, setMode] = useState('month') + const [date, setDate] = useState(new Date()) + const [eventsLoading, setEventsLoading] = useState(false) + const [eventsError, setEventsError] = useState(null) + + const refetchEvents = useCallback(async () => { + if (!isAuthenticated) return + setEventsLoading(true) + setEventsError(null) + try { + const list = await calendarApi.getEvents() + setEvents(list.map(mapApiToEvent)) + } catch (e) { + setEventsError(e instanceof Error ? e.message : 'Failed to load events') + setEvents([]) + } finally { + setEventsLoading(false) + } + }, [isAuthenticated]) + + useEffect(() => { + if (isAuthenticated) refetchEvents() + }, [isAuthenticated, refetchEvents]) + + if (!isReady) { + return ( +
+

Loading…

+
+ ) + } + + if (!isAuthenticated) { + return + } + + return ( + <> + {eventsLoading && events.length === 0 && ( +
+

Loading events…

+
+ )} + {eventsError && ( +
+ {eventsError} +
+ )} + + + ) +} diff --git a/platforms/calendar/components/calendar/body/calendar-body-header.tsx b/platforms/calendar/components/calendar/body/calendar-body-header.tsx new file mode 100644 index 00000000..dbd6ad64 --- /dev/null +++ b/platforms/calendar/components/calendar/body/calendar-body-header.tsx @@ -0,0 +1,35 @@ +import { format, isSameDay } from 'date-fns' +import { cn } from '../../../lib/utils' + +export default function CalendarBodyHeader({ + date, + onlyDay = false, +}: { + date: Date + onlyDay?: boolean +}) { + const isToday = isSameDay(date, new Date()) + + return ( +
+ + {format(date, 'EEE')} + + {!onlyDay && ( + + {format(date, 'dd')} + + )} +
+ ) +} diff --git a/platforms/calendar/components/calendar/body/calendar-body.tsx b/platforms/calendar/components/calendar/body/calendar-body.tsx new file mode 100644 index 00000000..cc383382 --- /dev/null +++ b/platforms/calendar/components/calendar/body/calendar-body.tsx @@ -0,0 +1,16 @@ +import { useCalendarContext } from '../calendar-context' +import CalendarBodyDay from './day/calendar-body-day' +import CalendarBodyWeek from './week/calendar-body-week' +import CalendarBodyMonth from './month/calendar-body-month' + +export default function CalendarBody() { + const { mode } = useCalendarContext() + + return ( + <> + {mode === 'day' && } + {mode === 'week' && } + {mode === 'month' && } + + ) +} diff --git a/platforms/calendar/components/calendar/body/day/calendar-body-day-calendar.tsx b/platforms/calendar/components/calendar/body/day/calendar-body-day-calendar.tsx new file mode 100644 index 00000000..f73d99f2 --- /dev/null +++ b/platforms/calendar/components/calendar/body/day/calendar-body-day-calendar.tsx @@ -0,0 +1,13 @@ +import { useCalendarContext } from '../../calendar-context' +import { Calendar } from '@/components/ui/calendar' + +export default function CalendarBodyDayCalendar() { + const { date, setDate } = useCalendarContext() + return ( + date && setDate(date)} + mode="single" + /> + ) +} diff --git a/platforms/calendar/components/calendar/body/day/calendar-body-day-content.tsx b/platforms/calendar/components/calendar/body/day/calendar-body-day-content.tsx new file mode 100644 index 00000000..272d7347 --- /dev/null +++ b/platforms/calendar/components/calendar/body/day/calendar-body-day-content.tsx @@ -0,0 +1,27 @@ +import { useCalendarContext } from '../../calendar-context' +import { isSameDay } from 'date-fns' +import { hours } from './calendar-body-margin-day-margin' +import CalendarBodyHeader from '../calendar-body-header' +import CalendarEvent from '../../calendar-event' + +export default function CalendarBodyDayContent({ date }: { date: Date }) { + const { events } = useCalendarContext() + + const dayEvents = events.filter((event) => isSameDay(event.start, date)) + + return ( +
+ + +
+ {hours.map((hour) => ( +
+ ))} + + {dayEvents.map((event) => ( + + ))} +
+
+ ) +} diff --git a/platforms/calendar/components/calendar/body/day/calendar-body-day-events.tsx b/platforms/calendar/components/calendar/body/day/calendar-body-day-events.tsx new file mode 100644 index 00000000..b88677dd --- /dev/null +++ b/platforms/calendar/components/calendar/body/day/calendar-body-day-events.tsx @@ -0,0 +1,35 @@ +import { useCalendarContext } from '../../calendar-context' +import { isSameDay } from 'date-fns' + +export default function CalendarBodyDayEvents() { + const { events, date, setManageEventDialogOpen, setSelectedEvent } = + useCalendarContext() + const dayEvents = events.filter((event) => isSameDay(event.start, date)) + + return !!dayEvents.length ? ( +
+

Events

+
+ {dayEvents.map((event) => ( +
{ + setSelectedEvent(event) + setManageEventDialogOpen(true) + }} + > +
+
+

+ {event.title} +

+
+
+ ))} +
+
+ ) : ( +
No events today...
+ ) +} diff --git a/platforms/calendar/components/calendar/body/day/calendar-body-day.tsx b/platforms/calendar/components/calendar/body/day/calendar-body-day.tsx new file mode 100644 index 00000000..bdd4352d --- /dev/null +++ b/platforms/calendar/components/calendar/body/day/calendar-body-day.tsx @@ -0,0 +1,25 @@ +import CalendarBodyDayCalendar from './calendar-body-day-calendar' +import CalendarBodyDayEvents from './calendar-body-day-events' +import { useCalendarContext } from '../../calendar-context' +import CalendarBodyDayContent from './calendar-body-day-content' +import CalendarBodyMarginDayMargin from './calendar-body-margin-day-margin' + +export default function CalendarBodyDay() { + const { date } = useCalendarContext() + return ( +
+
+
+
+ + +
+
+
+
+ + +
+
+ ) +} diff --git a/platforms/calendar/components/calendar/body/day/calendar-body-margin-day-margin.tsx b/platforms/calendar/components/calendar/body/day/calendar-body-margin-day-margin.tsx new file mode 100644 index 00000000..91ce351e --- /dev/null +++ b/platforms/calendar/components/calendar/body/day/calendar-body-margin-day-margin.tsx @@ -0,0 +1,32 @@ +import { format } from 'date-fns' +import { cn } from '@/lib/utils' + +export const hours = Array.from({ length: 24 }, (_, i) => i) + +export default function CalendarBodyMarginDayMargin({ + className, +}: { + className?: string +}) { + return ( +
+
+
+ {hours.map((hour) => ( +
+ {hour !== 0 && ( + + {format(new Date().setHours(hour, 0, 0, 0), 'h a')} + + )} +
+ ))} +
+
+ ) +} diff --git a/platforms/calendar/components/calendar/body/month/calendar-body-month.tsx b/platforms/calendar/components/calendar/body/month/calendar-body-month.tsx new file mode 100644 index 00000000..5bbefacd --- /dev/null +++ b/platforms/calendar/components/calendar/body/month/calendar-body-month.tsx @@ -0,0 +1,139 @@ +import { useCalendarContext } from '../../calendar-context' +import { + startOfMonth, + endOfMonth, + startOfWeek, + endOfWeek, + eachDayOfInterval, + isSameMonth, + isSameDay, + format, + isWithinInterval, +} from 'date-fns' +import { cn } from '@/lib/utils' +import CalendarEvent from '../../calendar-event' +import { AnimatePresence, motion } from 'framer-motion' + +export default function CalendarBodyMonth() { + const { date, events, setDate, setMode } = useCalendarContext() + + // Get the first day of the month + const monthStart = startOfMonth(date) + // Get the last day of the month + const monthEnd = endOfMonth(date) + + // Get the first Monday of the first week (may be in previous month) + const calendarStart = startOfWeek(monthStart, { weekStartsOn: 1 }) + // Get the last Sunday of the last week (may be in next month) + const calendarEnd = endOfWeek(monthEnd, { weekStartsOn: 1 }) + + // Get all days between start and end + const calendarDays = eachDayOfInterval({ + start: calendarStart, + end: calendarEnd, + }) + + const today = new Date() + + // Filter events to only show those within the current month view + const visibleEvents = events.filter( + (event) => + isWithinInterval(event.start, { + start: calendarStart, + end: calendarEnd, + }) || + isWithinInterval(event.end, { start: calendarStart, end: calendarEnd }) + ) + + return ( +
+
+ {['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map((day) => ( +
+ {day} +
+ ))} +
+ + + + {calendarDays.map((day) => { + const dayEvents = visibleEvents.filter((event) => + isSameDay(event.start, day) + ) + const isToday = isSameDay(day, today) + const isCurrentMonth = isSameMonth(day, date) + + return ( + + ) + })} + + +
+ ) +} diff --git a/platforms/calendar/components/calendar/body/week/calendar-body-week.tsx b/platforms/calendar/components/calendar/body/week/calendar-body-week.tsx new file mode 100644 index 00000000..ee9532ed --- /dev/null +++ b/platforms/calendar/components/calendar/body/week/calendar-body-week.tsx @@ -0,0 +1,31 @@ +import { useCalendarContext } from '../../calendar-context' +import { startOfWeek, addDays } from 'date-fns' +import CalendarBodyMarginDayMargin from '../day/calendar-body-margin-day-margin' +import CalendarBodyDayContent from '../day/calendar-body-day-content' +export default function CalendarBodyWeek() { + const { date } = useCalendarContext() + + const weekStart = startOfWeek(date, { weekStartsOn: 1 }) + const weekDays = Array.from({ length: 7 }, (_, i) => addDays(weekStart, i)) + + return ( +
+
+
+
+ + {weekDays.map((day) => ( +
+ + +
+ ))} +
+
+
+
+ ) +} diff --git a/platforms/calendar/components/calendar/calendar-context.tsx b/platforms/calendar/components/calendar/calendar-context.tsx new file mode 100644 index 00000000..981575fa --- /dev/null +++ b/platforms/calendar/components/calendar/calendar-context.tsx @@ -0,0 +1,14 @@ +import { createContext, useContext } from 'react' +import type { CalendarContextType } from './calendar-types' + +export const CalendarContext = createContext( + undefined +) + +export function useCalendarContext() { + const context = useContext(CalendarContext) + if (!context) { + throw new Error('useCalendarContext must be used within a CalendarProvider') + } + return context +} diff --git a/platforms/calendar/components/calendar/calendar-event.tsx b/platforms/calendar/components/calendar/calendar-event.tsx new file mode 100644 index 00000000..1666b99a --- /dev/null +++ b/platforms/calendar/components/calendar/calendar-event.tsx @@ -0,0 +1,151 @@ +import { CalendarEvent as CalendarEventType } from '@/components/calendar/calendar-types' +import { useCalendarContext } from '@/components/calendar/calendar-context' +import { format, isSameDay, isSameMonth } from 'date-fns' +import { cn } from '@/lib/utils' +import { motion, MotionConfig, AnimatePresence } from 'framer-motion' + +interface EventPosition { + left: string + width: string + top: string + height: string +} + +function getOverlappingEvents( + currentEvent: CalendarEventType, + events: CalendarEventType[] +): CalendarEventType[] { + return events.filter((event) => { + if (event.id === currentEvent.id) return false + return ( + currentEvent.start < event.end && + currentEvent.end > event.start && + isSameDay(currentEvent.start, event.start) + ) + }) +} + +function calculateEventPosition( + event: CalendarEventType, + allEvents: CalendarEventType[] +): EventPosition { + const overlappingEvents = getOverlappingEvents(event, allEvents) + const group = [event, ...overlappingEvents].sort( + (a, b) => a.start.getTime() - b.start.getTime() + ) + const position = group.indexOf(event) + const width = `${100 / (overlappingEvents.length + 1)}%` + const left = `${(position * 100) / (overlappingEvents.length + 1)}%` + + const startHour = event.start.getHours() + const startMinutes = event.start.getMinutes() + + let endHour = event.end.getHours() + let endMinutes = event.end.getMinutes() + + if (!isSameDay(event.start, event.end)) { + endHour = 23 + endMinutes = 59 + } + + const topPosition = startHour * 128 + (startMinutes / 60) * 128 + const duration = endHour * 60 + endMinutes - (startHour * 60 + startMinutes) + const height = (duration / 60) * 128 + + return { + left, + width, + top: `${topPosition}px`, + height: `${height}px`, + } +} + +export default function CalendarEvent({ + event, + month = false, + className, +}: { + event: CalendarEventType + month?: boolean + className?: string +}) { + const { events, setSelectedEvent, setManageEventDialogOpen, date } = + useCalendarContext() + const style = month ? {} : calculateEventPosition(event, events) + + // Generate a unique key that includes the current month to prevent animation conflicts + const isEventInCurrentMonth = isSameMonth(event.start, date) + const animationKey = `${event.id}-${ + isEventInCurrentMonth ? 'current' : 'adjacent' + }` + + return ( + + + { + e.stopPropagation() + setSelectedEvent(event) + setManageEventDialogOpen(true) + }} + initial={{ + opacity: 0, + y: -3, + scale: 0.98, + }} + animate={{ + opacity: 1, + y: 0, + scale: 1, + }} + exit={{ + opacity: 0, + scale: 0.98, + transition: { + duration: 0.15, + ease: 'easeOut', + }, + }} + transition={{ + duration: 0.2, + ease: [0.25, 0.1, 0.25, 1], + opacity: { + duration: 0.2, + ease: 'linear', + }, + layout: { + duration: 0.2, + ease: 'easeOut', + }, + }} + layoutId={`event-${animationKey}-${month ? 'month' : 'day'}`} + > + +

+ {event.title} +

+

+ {format(event.start, 'h:mm a')} + - + + {format(event.end, 'h:mm a')} + +

+
+
+
+
+ ) +} diff --git a/platforms/calendar/components/calendar/calendar-mode-icon-map.tsx b/platforms/calendar/components/calendar/calendar-mode-icon-map.tsx new file mode 100644 index 00000000..60e83a6e --- /dev/null +++ b/platforms/calendar/components/calendar/calendar-mode-icon-map.tsx @@ -0,0 +1,8 @@ +import { Columns2, Grid3X3, List } from 'lucide-react' +import { Mode } from './calendar-types' + +export const calendarModeIconMap: Record = { + day: , + week: , + month: , +} diff --git a/platforms/calendar/components/calendar/calendar-provider.tsx b/platforms/calendar/components/calendar/calendar-provider.tsx new file mode 100644 index 00000000..7eed1a79 --- /dev/null +++ b/platforms/calendar/components/calendar/calendar-provider.tsx @@ -0,0 +1,56 @@ +import { CalendarContext } from './calendar-context' +import { CalendarEvent, Mode } from './calendar-types' +import { useState } from 'react' +import CalendarNewEventDialog from './dialog/calendar-new-event-dialog' +import CalendarManageEventDialog from './dialog/calendar-manage-event-dialog' + +export default function CalendarProvider({ + events, + setEvents, + mode, + setMode, + date, + setDate, + calendarIconIsToday = true, + refetchEvents, + children, +}: { + events: CalendarEvent[] + setEvents: (events: CalendarEvent[]) => void + mode: Mode + setMode: (mode: Mode) => void + date: Date + setDate: (date: Date) => void + calendarIconIsToday: boolean + refetchEvents?: () => void | Promise + children: React.ReactNode +}) { + const [newEventDialogOpen, setNewEventDialogOpen] = useState(false) + const [manageEventDialogOpen, setManageEventDialogOpen] = useState(false) + const [selectedEvent, setSelectedEvent] = useState(null) + + return ( + + + + {children} + + ) +} diff --git a/platforms/calendar/components/calendar/calendar-tailwind-classes.ts b/platforms/calendar/components/calendar/calendar-tailwind-classes.ts new file mode 100644 index 00000000..4c4ff01e --- /dev/null +++ b/platforms/calendar/components/calendar/calendar-tailwind-classes.ts @@ -0,0 +1,69 @@ +// this is used to generate all tailwind classes for the calendar +// if you want to use your own colors, you can override the classes here + +export const colorOptions = [ + { + value: 'blue', + label: 'Blue', + class: { + base: 'bg-blue-500 border-blue-500 bg-blue-500/10 hover:bg-blue-500/20 text-blue-500', + light: 'bg-blue-300 border-blue-300 bg-blue-300/10 text-blue-300', + dark: 'dark:bg-blue-700 dark:border-blue-700 bg-blue-700/10 text-blue-700', + }, + }, + { + value: 'indigo', + label: 'Indigo', + class: { + base: 'bg-indigo-500 border-indigo-500 bg-indigo-500/10 hover:bg-indigo-500/20 text-indigo-500', + light: 'bg-indigo-300 border-indigo-300 bg-indigo-300/10 text-indigo-300', + dark: 'dark:bg-indigo-700 dark:border-indigo-700 bg-indigo-700/10 text-indigo-700', + }, + }, + { + value: 'pink', + label: 'Pink', + class: { + base: 'bg-pink-500 border-pink-500 bg-pink-500/10 hover:bg-pink-500/20 text-pink-500', + light: 'bg-pink-300 border-pink-300 bg-pink-300/10 text-pink-300', + dark: 'dark:bg-pink-700 dark:border-pink-700 bg-pink-700/10 text-pink-700', + }, + }, + { + value: 'red', + label: 'Red', + class: { + base: 'bg-red-500 border-red-500 bg-red-500/10 hover:bg-red-500/20 text-red-500', + light: 'bg-red-300 border-red-300 bg-red-300/10 text-red-300', + dark: 'dark:bg-red-700 dark:border-red-700 bg-red-700/10 text-red-700', + }, + }, + { + value: 'orange', + label: 'Orange', + class: { + base: 'bg-orange-500 border-orange-500 bg-orange-500/10 hover:bg-orange-500/20 text-orange-500', + light: 'bg-orange-300 border-orange-300 bg-orange-300/10 text-orange-300', + dark: 'dark:bg-orange-700 dark:border-orange-700 bg-orange-700/10 text-orange-700', + }, + }, + { + value: 'amber', + label: 'Amber', + class: { + base: 'bg-amber-500 border-amber-500 bg-amber-500/10 hover:bg-amber-500/20 text-amber-500', + light: 'bg-amber-300 border-amber-300 bg-amber-300/10 text-amber-300', + dark: 'dark:bg-amber-700 dark:border-amber-700 bg-amber-700/10 text-amber-700', + }, + }, + { + value: 'emerald', + label: 'Emerald', + class: { + base: 'bg-emerald-500 border-emerald-500 bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-500', + light: + 'bg-emerald-300 border-emerald-300 bg-emerald-300/10 text-emerald-300', + dark: 'dark:bg-emerald-700 dark:border-emerald-700 bg-emerald-700/10 text-emerald-700', + }, + }, +] diff --git a/platforms/calendar/components/calendar/calendar-types.ts b/platforms/calendar/components/calendar/calendar-types.ts new file mode 100644 index 00000000..f55d8efb --- /dev/null +++ b/platforms/calendar/components/calendar/calendar-types.ts @@ -0,0 +1,30 @@ +export type CalendarProps = { + events: CalendarEvent[] + setEvents: (events: CalendarEvent[]) => void + mode: Mode + setMode: (mode: Mode) => void + date: Date + setDate: (date: Date) => void + calendarIconIsToday?: boolean + refetchEvents?: () => void | Promise +} + +export type CalendarContextType = CalendarProps & { + newEventDialogOpen: boolean + setNewEventDialogOpen: (open: boolean) => void + manageEventDialogOpen: boolean + setManageEventDialogOpen: (open: boolean) => void + selectedEvent: CalendarEvent | null + setSelectedEvent: (event: CalendarEvent | null) => void + refetchEvents?: () => void | Promise +} +export type CalendarEvent = { + id: string + title: string + color: string + start: Date + end: Date +} + +export const calendarModes = ['day', 'week', 'month'] as const +export type Mode = (typeof calendarModes)[number] diff --git a/platforms/calendar/components/calendar/calendar.tsx b/platforms/calendar/components/calendar/calendar.tsx new file mode 100644 index 00000000..cceb6822 --- /dev/null +++ b/platforms/calendar/components/calendar/calendar.tsx @@ -0,0 +1,41 @@ +import type { CalendarProps } from './calendar-types' +import CalendarHeader from './header/calendar-header' +import CalendarBody from './body/calendar-body' +import CalendarHeaderActions from './header/actions/calendar-header-actions' +import CalendarHeaderDate from './header/date/calendar-header-date' +import CalendarHeaderActionsMode from './header/actions/calendar-header-actions-mode' +import CalendarHeaderActionsAdd from './header/actions/calendar-header-actions-add' +import CalendarProvider from './calendar-provider' + +export default function Calendar({ + events, + setEvents, + mode, + setMode, + date, + setDate, + calendarIconIsToday = true, + refetchEvents, +}: CalendarProps) { + return ( + + + + + + + + + + + ) +} diff --git a/platforms/calendar/components/calendar/dialog/calendar-manage-event-dialog.tsx b/platforms/calendar/components/calendar/dialog/calendar-manage-event-dialog.tsx new file mode 100644 index 00000000..e3af5607 --- /dev/null +++ b/platforms/calendar/components/calendar/dialog/calendar-manage-event-dialog.tsx @@ -0,0 +1,243 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { useForm } from 'react-hook-form' +import { z } from 'zod' +import { useEffect, useState } from 'react' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' +import { useCalendarContext } from '../calendar-context' +import { format } from 'date-fns' +import { DateTimePicker } from '@/components/form/date-time-picker' +import { ColorPicker } from '@/components/form/color-picker' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog' + +const formSchema = z + .object({ + title: z.string().min(1, 'Title is required'), + start: z.string().refine((val) => !isNaN(Date.parse(val)), { + message: 'Invalid start date', + }), + end: z.string().refine((val) => !isNaN(Date.parse(val)), { + message: 'Invalid end date', + }), + color: z.string(), + }) + .refine( + (data) => { + try { + const start = new Date(data.start) + const end = new Date(data.end) + return end >= start + } catch { + return false + } + }, + { + message: 'End time must be after start time', + path: ['end'], + } + ) + +export default function CalendarManageEventDialog() { + const { + manageEventDialogOpen, + setManageEventDialogOpen, + selectedEvent, + setSelectedEvent, + refetchEvents, + } = useCalendarContext() + const [submitting, setSubmitting] = useState(false) + const [deleteLoading, setDeleteLoading] = useState(false) + const [submitError, setSubmitError] = useState(null) + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + title: '', + start: '', + end: '', + color: 'blue', + }, + }) + + useEffect(() => { + if (selectedEvent) { + form.reset({ + title: selectedEvent.title, + start: format(selectedEvent.start, "yyyy-MM-dd'T'HH:mm"), + end: format(selectedEvent.end, "yyyy-MM-dd'T'HH:mm"), + color: selectedEvent.color, + }) + } + }, [selectedEvent, form]) + + async function onSubmit(values: z.infer) { + if (!selectedEvent) return + setSubmitError(null) + setSubmitting(true) + try { + const { calendarApi } = await import('@/lib/calendar-api') + await calendarApi.updateEvent(selectedEvent.id, { + title: values.title, + start: new Date(values.start).toISOString(), + end: new Date(values.end).toISOString(), + color: values.color, + }) + await refetchEvents?.() + handleClose() + } catch (e) { + setSubmitError(e instanceof Error ? e.message : 'Failed to update event') + } finally { + setSubmitting(false) + } + } + + async function handleDelete() { + if (!selectedEvent) return + setDeleteLoading(true) + try { + const { calendarApi } = await import('@/lib/calendar-api') + await calendarApi.deleteEvent(selectedEvent.id) + await refetchEvents?.() + handleClose() + } catch (e) { + setSubmitError(e instanceof Error ? e.message : 'Failed to delete event') + } finally { + setDeleteLoading(false) + } + } + + function handleClose() { + setManageEventDialogOpen(false) + setSelectedEvent(null) + form.reset() + } + + return ( + + + + Manage event + +
+ + {submitError && ( +

{submitError}

+ )} + ( + + Title + + + + + + )} + /> + + ( + + Start + + + + + + )} + /> + + ( + + End + + + + + + )} + /> + + ( + + Color + + + + + + )} + /> + + + + + + + + + Delete event + + Are you sure you want to delete this event? This action + cannot be undone. + + + + Cancel + + Delete + + + + + + + + +
+
+ ) +} diff --git a/platforms/calendar/components/calendar/dialog/calendar-new-event-dialog.tsx b/platforms/calendar/components/calendar/dialog/calendar-new-event-dialog.tsx new file mode 100644 index 00000000..13f90edc --- /dev/null +++ b/platforms/calendar/components/calendar/dialog/calendar-new-event-dialog.tsx @@ -0,0 +1,166 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { useForm } from 'react-hook-form' +import { z } from 'zod' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' +import { useState } from 'react' +import { useCalendarContext } from '../calendar-context' +import { format } from 'date-fns' +import { DateTimePicker } from '@/components/form/date-time-picker' +import { ColorPicker } from '@/components/form/color-picker' + +const formSchema = z + .object({ + title: z.string().min(1, 'Title is required'), + start: z.string().datetime(), + end: z.string().datetime(), + color: z.string(), + }) + .refine( + (data) => { + const start = new Date(data.start) + const end = new Date(data.end) + return end >= start + }, + { + message: 'End time must be after start time', + path: ['end'], + } + ) + +export default function CalendarNewEventDialog() { + const { + newEventDialogOpen, + setNewEventDialogOpen, + date, + setEvents, + refetchEvents, + } = useCalendarContext() + const [submitting, setSubmitting] = useState(false) + const [submitError, setSubmitError] = useState(null) + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + title: '', + start: format(date, "yyyy-MM-dd'T'HH:mm"), + end: format(date, "yyyy-MM-dd'T'HH:mm"), + color: 'blue', + }, + }) + + async function onSubmit(values: z.infer) { + setSubmitError(null) + setSubmitting(true) + try { + const { calendarApi } = await import('@/lib/calendar-api') + const startDate = new Date(values.start) + const endDate = new Date(values.end) + await calendarApi.createEvent({ + title: values.title, + color: values.color, + start: startDate.toISOString(), + end: endDate.toISOString(), + }) + await refetchEvents?.() + setNewEventDialogOpen(false) + form.reset() + } catch (e) { + setSubmitError(e instanceof Error ? e.message : 'Failed to create event') + } finally { + setSubmitting(false) + } + } + + return ( + + + + Create event + +
+ + ( + + Title + + + + + + )} + /> + + ( + + Start + + + + + + )} + /> + + ( + + End + + + + + + )} + /> + + ( + + Color + + + + + + )} + /> + + {submitError && ( +

{submitError}

+ )} +
+ +
+ + +
+
+ ) +} diff --git a/platforms/calendar/components/calendar/header/actions/calendar-header-actions-add.tsx b/platforms/calendar/components/calendar/header/actions/calendar-header-actions-add.tsx new file mode 100644 index 00000000..01c99e09 --- /dev/null +++ b/platforms/calendar/components/calendar/header/actions/calendar-header-actions-add.tsx @@ -0,0 +1,16 @@ +import { Button } from '@/components/ui/button' +import { Plus } from 'lucide-react' +import { useCalendarContext } from '../../calendar-context' + +export default function CalendarHeaderActionsAdd() { + const { setNewEventDialogOpen } = useCalendarContext() + return ( + + ) +} diff --git a/platforms/calendar/components/calendar/header/actions/calendar-header-actions-mode.tsx b/platforms/calendar/components/calendar/header/actions/calendar-header-actions-mode.tsx new file mode 100644 index 00000000..563ab619 --- /dev/null +++ b/platforms/calendar/components/calendar/header/actions/calendar-header-actions-mode.tsx @@ -0,0 +1,129 @@ +'use client' + +import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group' +import { Mode, calendarModes } from '../../calendar-types' +import { useCalendarContext } from '../../calendar-context' +import { calendarModeIconMap } from '../../calendar-mode-icon-map' +import { motion, AnimatePresence, LayoutGroup } from 'framer-motion' +import { cn } from '@/lib/utils' + +export default function CalendarHeaderActionsMode() { + const { mode, setMode } = useCalendarContext() + + return ( + + { + if (value) setMode(value as Mode) + }} + > + {calendarModes.map((modeValue) => { + const isSelected = mode === modeValue + return ( + + + + + {calendarModeIconMap[modeValue]} + + + {isSelected && ( + + {modeValue.charAt(0).toUpperCase() + modeValue.slice(1)} + + )} + + + + + ) + })} + + + ) +} diff --git a/platforms/calendar/components/calendar/header/actions/calendar-header-actions.tsx b/platforms/calendar/components/calendar/header/actions/calendar-header-actions.tsx new file mode 100644 index 00000000..e3b654e7 --- /dev/null +++ b/platforms/calendar/components/calendar/header/actions/calendar-header-actions.tsx @@ -0,0 +1,11 @@ +export default function CalendarHeaderActions({ + children, +}: { + children: React.ReactNode +}) { + return ( +
+ {children} +
+ ) +} diff --git a/platforms/calendar/components/calendar/header/calendar-header.tsx b/platforms/calendar/components/calendar/header/calendar-header.tsx new file mode 100644 index 00000000..628b7eb6 --- /dev/null +++ b/platforms/calendar/components/calendar/header/calendar-header.tsx @@ -0,0 +1,11 @@ +export default function CalendarHeader({ + children, +}: { + children: React.ReactNode +}) { + return ( +
+ {children} +
+ ) +} diff --git a/platforms/calendar/components/calendar/header/date/calendar-header-date-badge.tsx b/platforms/calendar/components/calendar/header/date/calendar-header-date-badge.tsx new file mode 100644 index 00000000..58a105f0 --- /dev/null +++ b/platforms/calendar/components/calendar/header/date/calendar-header-date-badge.tsx @@ -0,0 +1,14 @@ +import { useCalendarContext } from '../../calendar-context' +import { isSameMonth } from 'date-fns' + +export default function CalendarHeaderDateBadge() { + const { events, date } = useCalendarContext() + const monthEvents = events.filter((event) => isSameMonth(event.start, date)) + + if (!monthEvents.length) return null + return ( +
+ {monthEvents.length} events +
+ ) +} diff --git a/platforms/calendar/components/calendar/header/date/calendar-header-date-chevrons.tsx b/platforms/calendar/components/calendar/header/date/calendar-header-date-chevrons.tsx new file mode 100644 index 00000000..6ed6a920 --- /dev/null +++ b/platforms/calendar/components/calendar/header/date/calendar-header-date-chevrons.tsx @@ -0,0 +1,68 @@ +import { Button } from '@/components/ui/button' +import { useCalendarContext } from '../../calendar-context' +import { ChevronLeft, ChevronRight } from 'lucide-react' +import { + format, + addDays, + addMonths, + addWeeks, + subDays, + subMonths, + subWeeks, +} from 'date-fns' + +export default function CalendarHeaderDateChevrons() { + const { mode, date, setDate } = useCalendarContext() + + function handleDateBackward() { + switch (mode) { + case 'month': + setDate(subMonths(date, 1)) + break + case 'week': + setDate(subWeeks(date, 1)) + break + case 'day': + setDate(subDays(date, 1)) + break + } + } + + function handleDateForward() { + switch (mode) { + case 'month': + setDate(addMonths(date, 1)) + break + case 'week': + setDate(addWeeks(date, 1)) + break + case 'day': + setDate(addDays(date, 1)) + break + } + } + + return ( +
+ + + + {format(date, 'MMMM d, yyyy')} + + + +
+ ) +} diff --git a/platforms/calendar/components/calendar/header/date/calendar-header-date-icon.tsx b/platforms/calendar/components/calendar/header/date/calendar-header-date-icon.tsx new file mode 100644 index 00000000..f90376dd --- /dev/null +++ b/platforms/calendar/components/calendar/header/date/calendar-header-date-icon.tsx @@ -0,0 +1,16 @@ +import { format } from 'date-fns' +import { useCalendarContext } from '../../calendar-context' +export default function CalendarHeaderDateIcon() { + const { calendarIconIsToday, date: calendarDate } = useCalendarContext() + const date = calendarIconIsToday ? new Date() : calendarDate + return ( +
+

+ {format(date, 'MMM')} +

+

+ {format(date, 'dd')} +

+
+ ) +} diff --git a/platforms/calendar/components/calendar/header/date/calendar-header-date.tsx b/platforms/calendar/components/calendar/header/date/calendar-header-date.tsx new file mode 100644 index 00000000..349e60f1 --- /dev/null +++ b/platforms/calendar/components/calendar/header/date/calendar-header-date.tsx @@ -0,0 +1,21 @@ +import { useCalendarContext } from '../../calendar-context' +import { format } from 'date-fns' +import CalendarHeaderDateIcon from './calendar-header-date-icon' +import CalendarHeaderDateChevrons from './calendar-header-date-chevrons' +import CalendarHeaderDateBadge from './calendar-header-date-badge' + +export default function CalendarHeaderDate() { + const { date } = useCalendarContext() + return ( +
+ +
+
+

{format(date, 'MMMM yyyy')}

+ +
+ +
+
+ ) +} diff --git a/platforms/calendar/components/form/color-picker.tsx b/platforms/calendar/components/form/color-picker.tsx new file mode 100644 index 00000000..8f0431f2 --- /dev/null +++ b/platforms/calendar/components/form/color-picker.tsx @@ -0,0 +1,34 @@ +import * as React from 'react' +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' +import { cn } from '@/lib/utils' +import { colorOptions } from '../calendar/calendar-tailwind-classes' + +interface ColorPickerProps { + field: { + value: string + onChange: (value: string) => void + } +} + +export function ColorPicker({ field }: ColorPickerProps) { + return ( + + {colorOptions.map((color) => ( + + ))} + + ) +} diff --git a/platforms/calendar/components/form/date-time-picker.tsx b/platforms/calendar/components/form/date-time-picker.tsx new file mode 100644 index 00000000..043c1cb4 --- /dev/null +++ b/platforms/calendar/components/form/date-time-picker.tsx @@ -0,0 +1,159 @@ +'use client' + +import * as React from 'react' +import { CalendarIcon } from 'lucide-react' +import { format } from 'date-fns' +import { cn } from '@/lib/utils' +import { Button } from '@/components/ui/button' +import { Calendar } from '@/components/ui/calendar' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover' +import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' + +interface DateTimePickerProps { + field: { + value: string + onChange: (value: string) => void + } +} + +export function DateTimePicker({ field }: DateTimePickerProps) { + const [date, setDate] = React.useState( + field.value ? new Date(field.value) : new Date() + ) + const [isOpen, setIsOpen] = React.useState(false) + + const hours = Array.from({ length: 12 }, (_, i) => i + 1) + + const handleDateSelect = (selectedDate: Date | undefined) => { + if (selectedDate) { + const newDate = new Date(date) + newDate.setFullYear(selectedDate.getFullYear()) + newDate.setMonth(selectedDate.getMonth()) + newDate.setDate(selectedDate.getDate()) + setDate(newDate) + field.onChange(newDate.toISOString()) + } + } + + const handleTimeChange = ( + type: 'hour' | 'minute' | 'ampm', + value: string + ) => { + const newDate = new Date(date) + if (type === 'hour') { + newDate.setHours( + (parseInt(value) % 12) + (newDate.getHours() >= 12 ? 12 : 0) + ) + } else if (type === 'minute') { + newDate.setMinutes(parseInt(value)) + } else if (type === 'ampm') { + const currentHours = newDate.getHours() + const isPM = value === 'PM' + if (isPM && currentHours < 12) { + newDate.setHours(currentHours + 12) + } else if (!isPM && currentHours >= 12) { + newDate.setHours(currentHours - 12) + } + } + setDate(newDate) + field.onChange(newDate.toISOString()) + } + + return ( + + + + + +
+ +
+ +
+ {hours.map((hour) => ( + + ))} +
+ +
+ +
+ {Array.from({ length: 12 }, (_, i) => i * 5).map((minute) => ( + + ))} +
+ +
+ +
+ {['AM', 'PM'].map((ampm) => ( + + ))} +
+
+
+
+
+
+ ) +} diff --git a/platforms/calendar/components/header/header-github.tsx b/platforms/calendar/components/header/header-github.tsx new file mode 100644 index 00000000..ba63f147 --- /dev/null +++ b/platforms/calendar/components/header/header-github.tsx @@ -0,0 +1,18 @@ +'use client' + +import { Github } from 'lucide-react' +import { Button } from '../ui/button' + +export default function HeaderGithub() { + return ( + + ) +} diff --git a/platforms/calendar/components/header/header-theme-toggle.tsx b/platforms/calendar/components/header/header-theme-toggle.tsx new file mode 100644 index 00000000..d5f22f84 --- /dev/null +++ b/platforms/calendar/components/header/header-theme-toggle.tsx @@ -0,0 +1,56 @@ +'use client' + +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { Monitor, Moon, Sun } from 'lucide-react' +import { useTheme } from 'next-themes' + +export function HeaderThemeToggle() { + const { setTheme } = useTheme() + + return ( + + + + + + setTheme('light')}> + + setTheme('dark')}> + + setTheme('system')}> + + + + ) +} diff --git a/platforms/calendar/components/header/header.tsx b/platforms/calendar/components/header/header.tsx new file mode 100644 index 00000000..091da542 --- /dev/null +++ b/platforms/calendar/components/header/header.tsx @@ -0,0 +1,31 @@ +'use client' + +import HeaderGithub from './header-github' +import { HeaderThemeToggle } from './header-theme-toggle' +import { useAuth } from '@/contexts/auth-context' +import { Button } from '@/components/ui/button' + +export default function Header() { + const { isAuthenticated, logout } = useAuth() + return ( +
+
+

+ React, Tailwind and Shadcn Full Calendar +

+

+ By @charlietlamb +

+
+
+ {isAuthenticated && ( + + )} + + +
+
+ ) +} diff --git a/platforms/calendar/components/ui/accordion.tsx b/platforms/calendar/components/ui/accordion.tsx new file mode 100644 index 00000000..2f55a32f --- /dev/null +++ b/platforms/calendar/components/ui/accordion.tsx @@ -0,0 +1,57 @@ +"use client" + +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/platforms/calendar/components/ui/alert-dialog.tsx b/platforms/calendar/components/ui/alert-dialog.tsx new file mode 100644 index 00000000..57760f2e --- /dev/null +++ b/platforms/calendar/components/ui/alert-dialog.tsx @@ -0,0 +1,141 @@ +"use client" + +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/platforms/calendar/components/ui/alert.tsx b/platforms/calendar/components/ui/alert.tsx new file mode 100644 index 00000000..5afd41d1 --- /dev/null +++ b/platforms/calendar/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/platforms/calendar/components/ui/aspect-ratio.tsx b/platforms/calendar/components/ui/aspect-ratio.tsx new file mode 100644 index 00000000..d6a5226f --- /dev/null +++ b/platforms/calendar/components/ui/aspect-ratio.tsx @@ -0,0 +1,7 @@ +"use client" + +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" + +const AspectRatio = AspectRatioPrimitive.Root + +export { AspectRatio } diff --git a/platforms/calendar/components/ui/avatar.tsx b/platforms/calendar/components/ui/avatar.tsx new file mode 100644 index 00000000..51e507ba --- /dev/null +++ b/platforms/calendar/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/platforms/calendar/components/ui/badge.tsx b/platforms/calendar/components/ui/badge.tsx new file mode 100644 index 00000000..e87d62bf --- /dev/null +++ b/platforms/calendar/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/platforms/calendar/components/ui/breadcrumb.tsx b/platforms/calendar/components/ui/breadcrumb.tsx new file mode 100644 index 00000000..60e6c96f --- /dev/null +++ b/platforms/calendar/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode + } +>(({ ...props }, ref) =>