diff --git a/.gitignore b/.gitignore
index 537a84e..f650315 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,36 +1,27 @@
-# --- Backend (Python) ---
-.env
-__pycache__/
-*.py[cod]
-database/calendar.db
-database/calendar.db-journal
-.vscode
-.venv/
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-# --- Frontend (Next.js / v0) ---
-node_modules/
-.next/
-out/
-.vercel/
-*.tsbuildinfo
-next-env.d.ts
+# dependencies
+/node_modules
-# leftover v0/dev artifacts
-dump.txt
-*.old-mock
-*.old-mock.tsx
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
# env files
-.env*.local
-.env.production
-.env.development
+.env*
-# OS / editor
-.DS_Store
-Thumbs.db
+# vercel
+.vercel
-# logs
-npm-debug.log*
-pnpm-debug.log*
-yarn-debug.log*
-yarn-error.log*
\ No newline at end of file
+# typescript
+*.tsbuildinfo
+next-env.d.ts
\ No newline at end of file
diff --git a/backend/__pycache__/auth.cpython-313.pyc b/backend/__pycache__/auth.cpython-313.pyc
new file mode 100644
index 0000000..e006d8e
Binary files /dev/null and b/backend/__pycache__/auth.cpython-313.pyc differ
diff --git a/backend/__pycache__/database.cpython-313.pyc b/backend/__pycache__/database.cpython-313.pyc
new file mode 100644
index 0000000..f00343f
Binary files /dev/null and b/backend/__pycache__/database.cpython-313.pyc differ
diff --git a/backend/__pycache__/email_service.cpython-313.pyc b/backend/__pycache__/email_service.cpython-313.pyc
new file mode 100644
index 0000000..d7a6301
Binary files /dev/null and b/backend/__pycache__/email_service.cpython-313.pyc differ
diff --git a/backend/__pycache__/event_signups.cpython-313.pyc b/backend/__pycache__/event_signups.cpython-313.pyc
new file mode 100644
index 0000000..a3301d1
Binary files /dev/null and b/backend/__pycache__/event_signups.cpython-313.pyc differ
diff --git a/backend/__pycache__/events.cpython-313.pyc b/backend/__pycache__/events.cpython-313.pyc
new file mode 100644
index 0000000..0b6861b
Binary files /dev/null and b/backend/__pycache__/events.cpython-313.pyc differ
diff --git a/backend/__pycache__/main.cpython-313.pyc b/backend/__pycache__/main.cpython-313.pyc
new file mode 100644
index 0000000..53c32cf
Binary files /dev/null and b/backend/__pycache__/main.cpython-313.pyc differ
diff --git a/backend/__pycache__/permissions.cpython-313.pyc b/backend/__pycache__/permissions.cpython-313.pyc
new file mode 100644
index 0000000..939d082
Binary files /dev/null and b/backend/__pycache__/permissions.cpython-313.pyc differ
diff --git a/backend/__pycache__/rooms.cpython-313.pyc b/backend/__pycache__/rooms.cpython-313.pyc
new file mode 100644
index 0000000..ce2403f
Binary files /dev/null and b/backend/__pycache__/rooms.cpython-313.pyc differ
diff --git a/backend/email_service.py b/backend/email_service.py
index 307f85b..50ada61 100644
--- a/backend/email_service.py
+++ b/backend/email_service.py
@@ -1,17 +1,19 @@
import hashlib
-import secrets
-import sqlite3
-from datetime import datetime, timedelta, timezone
-from pathlib import Path
-
import os
+import secrets
import smtplib
+import sqlite3
+
+from datetime import datetime, timedelta, timezone
from email.message import EmailMessage
+from pathlib import Path
from dotenv import load_dotenv
+
load_dotenv()
+
DB_NAME = Path(__file__).parent.parent / "database" / "calendar.db"
@@ -28,7 +30,207 @@ def hash_token(token: str) -> str:
).hexdigest()
-def update_email(user_id: int, new_email: str):
+# ============================================================
+# EMAIL
+# ============================================================
+
+def send_email(
+ email: str,
+ subject: str,
+ plain_text: str,
+ html: str,
+):
+ smtp_host = os.getenv("SMTP_HOST")
+ smtp_port = int(os.getenv("SMTP_PORT", "587"))
+ smtp_username = os.getenv("SMTP_USERNAME")
+ smtp_password = os.getenv("SMTP_PASSWORD")
+ smtp_from = os.getenv("SMTP_FROM")
+
+ if not all([
+ smtp_host,
+ smtp_username,
+ smtp_password,
+ smtp_from,
+ ]):
+ raise RuntimeError("SMTP configuration is incomplete")
+
+ message = EmailMessage()
+
+ message["From"] = smtp_from
+ message["To"] = email
+ message["Subject"] = subject
+
+ # Plain-text fallback
+ message.set_content(plain_text)
+
+ # HTML version
+ message.add_alternative(
+ html,
+ subtype="html",
+ )
+
+ print(f"Sending email to {email}")
+
+ with smtplib.SMTP(
+ smtp_host,
+ smtp_port,
+ ) as smtp:
+
+ print("SMTP connected")
+
+ smtp.starttls()
+
+ print("TLS started")
+
+ smtp.login(
+ smtp_username,
+ smtp_password,
+ )
+
+ print("SMTP login successful")
+
+ smtp.send_message(message)
+
+ print("Email sent")
+
+
+def send_verification_email(
+ email: str,
+ token: str,
+):
+ verification_url = (
+ "https://ben.de-roo.org"
+ f"/api/verify-email?token={token}"
+ )
+
+ plain_text = f"""Hello,
+
+You requested to use this email address for your Calendar account.
+
+Verify your email address using this link:
+
+{verification_url}
+
+This link expires after 24 hours.
+
+If you did not request this, you can ignore this email.
+"""
+
+ html = f"""\
+
+
+
+
+
+
+
+
+ Hello,
+
+
+ You requested to use this email address
+ for your Calendar account.
+
+
+
+
+ Verify your email address
+
+
+
+
+ This link expires after 24 hours.
+
+
+
+ If you did not request this,
+ you can ignore this email.
+
+
+
+
+"""
+
+ send_email(
+ email=email,
+ subject="Verify your Calendar email address",
+ plain_text=plain_text,
+ html=html,
+ )
+
+
+def send_password_reset_email(
+ email: str,
+ token: str,
+):
+ password_reset_url = (
+ "https://ben.de-roo.org"
+ f"/api/update-password_page?token={token}"
+ )
+
+ plain_text = f"""Hello,
+
+You requested to reset your Calendar password.
+
+Reset your password using this link:
+
+{password_reset_url}
+
+This link expires after 1 hour.
+
+If you did not request this, you can ignore this email.
+"""
+
+ html = f"""\
+
+
+
+
+
+
+
+
+ Hello,
+
+
+ You requested to reset your Calendar password.
+
+
+
+
+ Reset your password
+
+
+
+
+ This link expires after 1 hour.
+
+
+
+ If you did not request this,
+ you can ignore this email.
+
+
+
+
+"""
+
+ send_email(
+ email=email,
+ subject="Reset your password",
+ plain_text=plain_text,
+ html=html,
+ )
+
+
+# ============================================================
+# EMAIL ADDRESS VERIFICATION
+# ============================================================
+
+def update_email(
+ user_id: int,
+ new_email: str,
+):
new_email = new_email.strip().lower()
conn = get_connection()
@@ -57,7 +259,10 @@ def update_email(user_id: int, new_email: str):
WHERE email = ?
AND id != ?
""",
- (new_email, user_id),
+ (
+ new_email,
+ user_id,
+ ),
)
if cur.fetchone() is not None:
@@ -73,7 +278,10 @@ def update_email(user_id: int, new_email: str):
email_verified = 0
WHERE id = ?
""",
- (new_email, user_id),
+ (
+ new_email,
+ user_id,
+ ),
)
cur.execute(
@@ -104,7 +312,9 @@ def update_email(user_id: int, new_email: str):
conn.close()
-def create_verification(user_id: int):
+def create_verification(
+ user_id: int,
+):
token = secrets.token_urlsafe(32)
token_hash = hash_token(token)
@@ -149,7 +359,9 @@ def create_verification(user_id: int):
conn.close()
-def verify_email(token: str):
+def verify_email(
+ token: str,
+):
token_hash = hash_token(token)
conn = get_connection()
@@ -181,6 +393,7 @@ def verify_email(token: str):
)
if datetime.now(timezone.utc) >= expires_at:
+
cur.execute(
"""
DELETE FROM email_verifications
@@ -222,121 +435,14 @@ def verify_email(token: str):
finally:
conn.close()
-def send_verification_email(email: str, token: str):
- smtp_host = os.getenv("SMTP_HOST")
- smtp_port = int(os.getenv("SMTP_PORT", "587"))
- smtp_username = os.getenv("SMTP_USERNAME")
- smtp_password = os.getenv("SMTP_PASSWORD")
- smtp_from = os.getenv("SMTP_FROM")
- if not all([
- smtp_host,
- smtp_username,
- smtp_password,
- smtp_from,
- ]):
- raise RuntimeError("SMTP configuration is incomplete")
+# ============================================================
+# PASSWORD RESET
+# ============================================================
- verification_url = (
- f"https://ben.de-roo.org/api/verify-email?token={token}"
- )
-
- message = EmailMessage()
-
- message["From"] = smtp_from
- message["To"] = email
- message["Subject"] = "Verify your Calendar email address"
-
- message.set_content(
- f"""Hello,
-
-You requested to use this email address for your Calendar account.
-
-Verify your email address using this link:
-
-{verification_url}
-
-This link expires after 24 hours.
-
-If you did not request this, you can ignore this email.
-"""
- )
-
- print(f"Sending verification email to {email}")
-
- with smtplib.SMTP(smtp_host, smtp_port) as smtp:
- print("SMTP connected")
-
- smtp.starttls()
- print("TLS started")
-
- smtp.login(
- smtp_username,
- smtp_password,
- )
- print("SMTP login successful")
-
- smtp.send_message(message)
- print("Verification email sent")
-
-def send_password_reset_email(email: str, token: str):
- smtp_host = os.getenv("SMTP_HOST")
- smtp_port = int(os.getenv("SMTP_PORT", "587"))
- smtp_username = os.getenv("SMTP_USERNAME")
- smtp_password = os.getenv("SMTP_PASSWORD")
- smtp_from = os.getenv("SMTP_FROM")
-
- if not all([
- smtp_host,
- smtp_username,
- smtp_password,
- smtp_from,
- ]):
- raise RuntimeError("SMTP configuration is incomplete")
-
- password_reset_url = (
- f"https://ben.de-roo.org/api/update-password_page?token={token}"
- )
-
- message = EmailMessage()
-
- message["From"] = smtp_from
- message["To"] = email
- message["Subject"] = "Reset your password"
-
- message.set_content(
- f"""Hello,
-
-You requested to use this email address for reseting your Calendar password.
-
-Reset your password using this link:
-
-{password_reset_url}
-
-This link expires after 24 hours.
-
-If you did not request this, you can ignore this email.
-"""
- )
-
- print(f"Sending password reset email to {email}")
-
- with smtplib.SMTP(smtp_host, smtp_port) as smtp:
- print("SMTP connected")
-
- smtp.starttls()
- print("TLS started")
-
- smtp.login(
- smtp_username,
- smtp_password,
- )
- print("SMTP login successful")
-
- smtp.send_message(message)
- print("Password reset email sent")
-
-def create_password_reset(user_id: int):
+def create_password_reset(
+ user_id: int,
+):
token = secrets.token_urlsafe(32)
token_hash = hash_token(token)
@@ -378,4 +484,4 @@ def create_password_reset(user_id: int):
return token
finally:
- conn.close()
\ No newline at end of file
+ conn.close()
diff --git a/frontend(experimental)/newchat/.gitignore b/frontend(experimental)/newchat/.gitignore
deleted file mode 100644
index f650315..0000000
--- a/frontend(experimental)/newchat/.gitignore
+++ /dev/null
@@ -1,27 +0,0 @@
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-
-# dependencies
-/node_modules
-
-# next.js
-/.next/
-/out/
-
-# production
-/build
-
-# debug
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-.pnpm-debug.log*
-
-# env files
-.env*
-
-# vercel
-.vercel
-
-# typescript
-*.tsbuildinfo
-next-env.d.ts
\ No newline at end of file
diff --git a/frontend(experimental)/newchat/README.md b/frontend(experimental)/newchat/README.md
deleted file mode 100644
index 14a4cd3..0000000
--- a/frontend(experimental)/newchat/README.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# calendar-app
-
-This is a [Next.js](https://nextjs.org) project bootstrapped with [v0](https://v0.app).
-
-## Built with v0
-
-This repository is linked to a [v0](https://v0.app) project. You can continue developing by visiting the link below -- start new chats to make changes, and v0 will push commits directly to this repo. Every merge to `main` will automatically deploy.
-
-[Continue working on v0 →](https://v0.app/chat/projects/prj_JicVuGp27VUV82GWT4csmGqXBtSV)
-
-## Getting Started
-
-First, run the development server:
-
-```bash
-npm run dev
-# or
-yarn dev
-# or
-pnpm dev
-```
-
-Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
-
-You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
-
-## Learn More
-
-To learn more, take a look at the following resources:
-
-- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
-- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
-- [v0 Documentation](https://v0.app/docs) - learn about v0 and how to use it.
diff --git a/frontend(experimental)/newchat/app/globals.css b/frontend(experimental)/newchat/app/globals.css
deleted file mode 100644
index 5791b0a..0000000
--- a/frontend(experimental)/newchat/app/globals.css
+++ /dev/null
@@ -1,62 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-
-:root {
- --background: 0 0% 100%;
- --foreground: 222.2 84% 4.9%;
- --card: 0 0% 100%;
- --card-foreground: 222.2 84% 4.9%;
- --popover: 0 0% 100%;
- --popover-foreground: 222.2 84% 4.9%;
- --primary: 221.2 83.2% 53.3%;
- --primary-foreground: 210 40% 98%;
- --secondary: 210 40% 96.1%;
- --secondary-foreground: 222.2 47.4% 11.2%;
- --muted: 210 40% 96.1%;
- --muted-foreground: 215.4 16.3% 46.9%;
- --accent: 210 40% 96.1%;
- --accent-foreground: 222.2 47.4% 11.2%;
- --destructive: 0 84.2% 60.2%;
- --destructive-foreground: 210 40% 98%;
- --border: 214.3 31.8% 91.4%;
- --input: 214.3 31.8% 91.4%;
- --ring: 221.2 83.2% 53.3%;
- --radius: 0.5rem;
-}
-
-.dark {
- --background: 222.2 84% 4.9%;
- --foreground: 210 40% 98%;
- --card: 222.2 84% 4.9%;
- --card-foreground: 210 40% 98%;
- --popover: 222.2 84% 4.9%;
- --popover-foreground: 210 40% 98%;
- --primary: 217.2 91.2% 59.8%;
- --primary-foreground: 222.2 47.4% 11.2%;
- --secondary: 217.2 32.6% 17.5%;
- --secondary-foreground: 210 40% 98%;
- --muted: 217.2 32.6% 17.5%;
- --muted-foreground: 215 20.2% 65.1%;
- --accent: 217.2 32.6% 17.5%;
- --accent-foreground: 210 40% 98%;
- --destructive: 0 62.8% 30.6%;
- --destructive-foreground: 210 40% 98%;
- --border: 217.2 32.6% 17.5%;
- --input: 217.2 32.6% 17.5%;
- --ring: 224.3 76.3% 48%;
-}
-
-@layer utilities {
- .bg-grid-slate-200\/50 {
- background-image: linear-gradient(currentColor 1px, transparent 1px),
- linear-gradient(to right, currentColor 1px, transparent 1px);
- background-size: 20px 20px;
- }
-}
-
-.react-draggable {
- backdrop-filter: blur(16px);
- -webkit-backdrop-filter: blur(16px);
-}
-
diff --git a/frontend(experimental)/newchat/app/layout.tsx b/frontend(experimental)/newchat/app/layout.tsx
deleted file mode 100644
index 95ec418..0000000
--- a/frontend(experimental)/newchat/app/layout.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import type React from "react"
-import type { Metadata } from "next"
-import { Inter } from "next/font/google"
-import "./globals.css"
-import { AuthProvider } from "@/lib/auth-context"
-
-const inter = Inter({ subsets: ["latin"] })
-
-export const metadata: Metadata = {
- title: "Calendar | Rooms & Events",
- description: "A shared, room-based calendar for planning events together.",
- generator: "v0.app",
-}
-
-export default function RootLayout({
- children,
-}: Readonly<{
- children: React.ReactNode
-}>) {
- return (
-
-
- {children}
-
-
- )
-}
diff --git a/frontend(experimental)/newchat/app/loading.tsx b/frontend(experimental)/newchat/app/loading.tsx
deleted file mode 100644
index cec067b..0000000
--- a/frontend(experimental)/newchat/app/loading.tsx
+++ /dev/null
@@ -1,4 +0,0 @@
-export default function Loading() {
- return null
-}
-
diff --git a/frontend(experimental)/newchat/app/page.tsx b/frontend(experimental)/newchat/app/page.tsx
deleted file mode 100644
index 9549c74..0000000
--- a/frontend(experimental)/newchat/app/page.tsx
+++ /dev/null
@@ -1,239 +0,0 @@
-"use client"
-
-import { useMemo, useState } from "react"
-import { Menu, Plus, Loader2 } from "lucide-react"
-
-import { useAuth } from "@/lib/auth-context"
-import { useRooms, useEvents, useMyResponses } from "@/lib/hooks"
-import { AuthScreen } from "@/components/auth-screen"
-import { Sidebar } from "@/components/sidebar"
-import { WeekView } from "@/components/week-view"
-import { CreateEventDialog } from "@/components/create-event-dialog"
-import { EventDetailDialog } from "@/components/event-detail-dialog"
-import { CreateRoomDialog, JoinRoomDialog } from "@/components/room-dialogs"
-import { RoomSettingsDialog } from "@/components/room-settings-dialog"
-import { AccountSettingsDialog } from "@/components/account-settings-dialog"
-import { RoomBackground } from "@/components/room-background"
-import { getRoomBackground } from "@/lib/room-theme"
-import { addDays, startOfWeek, toDateParam, formatMonthYear } from "@/lib/date-utils"
-import type { CalendarEvent, User } from "@/lib/types"
-
-type DialogState =
- | { type: "none" }
- | { type: "create-event" }
- | { type: "edit-event"; event: CalendarEvent }
- | { type: "event-detail"; event: CalendarEvent }
- | { type: "create-room" }
- | { type: "join-room" }
- | { type: "room-settings" }
- | { type: "account-settings" }
-
-export default function Home() {
- const { user, loading, logout } = useAuth()
-
- if (loading) {
- return (
-
-
-
- )
- }
-
- if (!user) {
- return
- }
-
- return
-}
-
-function CalendarApp({ user, onLogout }: { user: User; onLogout: () => void }) {
- const [activeRoomId, setActiveRoomId] = useState(null)
- const [selectedDate, setSelectedDate] = useState(() => new Date())
- const [sidebarOpen, setSidebarOpen] = useState(false)
- const [dialog, setDialog] = useState({ type: "none" })
-
- const { rooms, mutate: mutateRooms } = useRooms()
-
- const weekStart = useMemo(() => startOfWeek(selectedDate), [selectedDate])
- const weekEnd = useMemo(() => addDays(weekStart, 7), [weekStart])
- const from = useMemo(() => toDateParam(weekStart), [weekStart])
- const to = useMemo(() => toDateParam(weekEnd), [weekEnd])
-
- const {
- events,
- isLoading: eventsLoading,
- mutate: mutateEvents,
- } = useEvents(activeRoomId, from, to)
-
- const { myResponses } = useMyResponses(events, user.id)
-
- const activeRoom = rooms.find((r) => r.id === activeRoomId)
- const background = getRoomBackground(activeRoomId)
-
- const refreshAll = () => {
- mutateEvents()
- mutateRooms()
- }
-
- return (
-
-
-
-
-
- {/* Mobile sidebar backdrop */}
- {sidebarOpen && (
-
setSidebarOpen(false)}
- />
- )}
-
-
- {
- setActiveRoomId(id)
- setSidebarOpen(false)
- }}
- selectedDate={selectedDate}
- onSelectDate={setSelectedDate}
- onCreateEvent={() => setDialog({ type: "create-event" })}
- onCreateRoom={() => setDialog({ type: "create-room" })}
- onJoinRoom={() => setDialog({ type: "join-room" })}
- user={user}
- onLogout={onLogout}
- onOpenAccountSettings={() => setDialog({ type: "account-settings" })}
- />
-
-
-
-
-
-
-
-
- {activeRoom ? activeRoom.name : "All Rooms"}
-
-
{formatMonthYear(weekStart)}
-
-
-
-
- {activeRoom && (
-
- )}
-
-
-
-
-
- {eventsLoading ? (
-
-
-
- ) : (
-
setDialog({ type: "event-detail", event })}
- />
- )}
-
-
-
-
- {(dialog.type === "create-event" || dialog.type === "edit-event") && (
-
setDialog({ type: "none" })}
- onCreated={() => {
- setDialog({ type: "none" })
- refreshAll()
- }}
- />
- )}
-
- {dialog.type === "event-detail" && (
- r.id === dialog.event.room_id)}
- currentUserId={user.id}
- onClose={() => setDialog({ type: "none" })}
- onChanged={refreshAll}
- onEdit={() => setDialog({ type: "edit-event", event: dialog.event })}
- />
- )}
-
- {dialog.type === "create-room" && (
- setDialog({ type: "none" })}
- onCreated={(roomId: number) => {
- setDialog({ type: "none" })
- mutateRooms()
- setActiveRoomId(roomId)
- }}
- />
- )}
-
- {dialog.type === "join-room" && (
- setDialog({ type: "none" })}
- onJoined={(roomId: number) => {
- setDialog({ type: "none" })
- mutateRooms()
- setActiveRoomId(roomId)
- }}
- />
- )}
-
- {dialog.type === "room-settings" && activeRoom && (
- setDialog({ type: "none" })}
- onChanged={mutateRooms}
- onLeftOrDeleted={() => {
- setDialog({ type: "none" })
- setActiveRoomId(null)
- mutateRooms()
- }}
- />
- )}
-
- {dialog.type === "account-settings" && (
- setDialog({ type: "none" })}
- onLogout={onLogout}
- />
- )}
-
- )
-}
\ No newline at end of file
diff --git a/frontend(experimental)/newchat/components.json b/frontend(experimental)/newchat/components.json
deleted file mode 100644
index 4ee62ee..0000000
--- a/frontend(experimental)/newchat/components.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "$schema": "https://ui.shadcn.com/schema.json",
- "style": "new-york",
- "rsc": true,
- "tsx": true,
- "tailwind": {
- "config": "",
- "css": "app/globals.css",
- "baseColor": "neutral",
- "cssVariables": true,
- "prefix": ""
- },
- "aliases": {
- "components": "@/components",
- "utils": "@/lib/utils",
- "ui": "@/components/ui",
- "lib": "@/lib",
- "hooks": "@/hooks"
- },
- "iconLibrary": "lucide"
-}
diff --git a/frontend(experimental)/newchat/components/account-settings-dialog.tsx b/frontend(experimental)/newchat/components/account-settings-dialog.tsx
deleted file mode 100644
index 053309c..0000000
--- a/frontend(experimental)/newchat/components/account-settings-dialog.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-"use client"
-
-import { LogOut, Mail, MailCheck, MailWarning, UserRound } from "lucide-react"
-import { GlassModal, fieldClass, labelClass } from "./glass-modal"
-import type { User } from "@/lib/types"
-
-interface AccountSettingsDialogProps {
- user: User
- onClose: () => void
- onLogout: () => void
-}
-
-export function AccountSettingsDialog({ user, onClose, onLogout }: AccountSettingsDialogProps) {
- return (
-
-
-
-
- {user.username.charAt(0).toUpperCase()}
-
-
-
{user.username}
-
{user.email ?? "No email on file"}
-
-
-
-
-
-
-
- {user.username}
-
-
-
-
-
-
-
- {user.email ?? "—"}
-
- {user.email && (
-
- {user.email_verified ? (
- <>
- Verified
- >
- ) : (
- <>
- Not verified
- >
- )}
-
- )}
-
-
-
-
-
-
-
-
-
- )
-}
diff --git a/frontend(experimental)/newchat/components/auth-screen.tsx b/frontend(experimental)/newchat/components/auth-screen.tsx
deleted file mode 100644
index 3bcc92c..0000000
--- a/frontend(experimental)/newchat/components/auth-screen.tsx
+++ /dev/null
@@ -1,161 +0,0 @@
-"use client"
-
-import type React from "react"
-import { useState } from "react"
-import Image from "next/image"
-import { Calendar, Loader2 } from "lucide-react"
-import { useAuth } from "@/lib/auth-context"
-import { ApiError } from "@/lib/api"
-import { ALL_ROOMS_BACKGROUND } from "@/lib/room-theme"
-
-type Mode = "login" | "register"
-
-export function AuthScreen() {
- const { login, register } = useAuth()
- const [mode, setMode] = useState
("login")
- const [username, setUsername] = useState("")
- const [email, setEmail] = useState("")
- const [password, setPassword] = useState("")
- const [error, setError] = useState(null)
- const [notice, setNotice] = useState(null)
- const [submitting, setSubmitting] = useState(false)
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault()
- setError(null)
- setNotice(null)
- setSubmitting(true)
- try {
- if (mode === "login") {
- await login(username.trim(), password)
- } else {
- await register(username.trim(), password, email.trim())
- }
- } catch (err) {
- const message =
- err instanceof ApiError ? err.message : "Something went wrong. Please try again."
- setError(message)
- } finally {
- setSubmitting(false)
- }
- }
-
- const switchMode = (next: Mode) => {
- setMode(next)
- setError(null)
- setNotice(null)
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
- {mode === "login" ? "Welcome back" : "Create your account"}
-
-
- {mode === "login"
- ? "Sign in to your shared calendar"
- : "Join rooms and plan events together"}
-
-
-
-
-
-
- {mode === "login" ? "Don't have an account? " : "Already have an account? "}
-
-
-
-
- )
-}
diff --git a/frontend(experimental)/newchat/components/create-event-dialog.tsx b/frontend(experimental)/newchat/components/create-event-dialog.tsx
deleted file mode 100644
index 214f9ee..0000000
--- a/frontend(experimental)/newchat/components/create-event-dialog.tsx
+++ /dev/null
@@ -1,354 +0,0 @@
-"use client"
-
-import type React from "react"
-import { useState } from "react"
-import { Loader2 } from "lucide-react"
-import { GlassModal, fieldClass, labelClass } from "./glass-modal"
-import * as api from "@/lib/api"
-import { ApiError } from "@/lib/api"
-import type { CalendarEvent, EventType, EventVisibility, Room } from "@/lib/types"
-import { parseDateTime, toBackendDateTime, toDateParam, toInputTime } from "@/lib/date-utils"
-
-interface CreateEventDialogProps {
- rooms: Room[]
- defaultRoomId: number | null
- defaultDate: Date
- /** When set, the dialog edits this event instead of creating a new one. */
- editingEvent?: CalendarEvent
- onClose: () => void
- onCreated: () => void
-}
-
-const TYPE_OPTIONS: { value: EventType; label: string }[] = [
- { value: "proposal", label: "Proposal (members vote to confirm)" },
- { value: "busy", label: "Busy — room sees it as busy, no details" },
- { value: "private", label: "Private — hidden from the room" },
-]
-
-const VISIBILITY_OPTIONS: { value: EventVisibility; label: string }[] = [
- { value: "room", label: "Room — visible to all members" },
- { value: "busy_only", label: "Busy only — hide details" },
- { value: "private", label: "Private — only me" },
-]
-
-const optionClass = "bg-slate-800 text-white"
-
-export function CreateEventDialog({
- rooms,
- defaultRoomId,
- defaultDate,
- editingEvent,
- onClose,
- onCreated,
-}: CreateEventDialogProps) {
- const isEditing = !!editingEvent
- const initialRoom = editingEvent?.room_id ?? defaultRoomId ?? rooms[0]?.id ?? 0
-
- // Start time defaults to "now", rounded up to the next quarter hour.
- const now = new Date()
- const defaultStartTime = new Date(now)
- defaultStartTime.setMinutes(Math.ceil(now.getMinutes() / 15) * 15, 0, 0)
- const defaultEndTime = new Date(defaultStartTime)
- defaultEndTime.setHours(defaultEndTime.getHours() + 1)
-
- const editStart = editingEvent ? parseDateTime(editingEvent.start_time) : null
- const editEnd = editingEvent ? parseDateTime(editingEvent.end_time) : null
-
- const [roomId, setRoomId] = useState(initialRoom)
- const [title, setTitle] = useState(editingEvent?.title ?? "")
- const [description, setDescription] = useState(editingEvent?.description ?? "")
- const [type, setType] = useState(editingEvent?.type ?? "proposal")
- const [visibility, setVisibility] = useState(editingEvent?.visibility ?? "room")
- const [date, setDate] = useState(toDateParam(editStart ?? defaultDate))
- const [multiDay, setMultiDay] = useState(
- editStart && editEnd ? toDateParam(editStart) !== toDateParam(editEnd) : false,
- )
- const [endDate, setEndDate] = useState(toDateParam(editEnd ?? editStart ?? defaultDate))
- const [startTime, setStartTime] = useState(toInputTime(editStart ?? defaultStartTime))
- const [endTime, setEndTime] = useState(toInputTime(editEnd ?? defaultEndTime))
- const [minPeople, setMinPeople] = useState(editingEvent?.min_people ?? 1)
- const [error, setError] = useState(null)
- const [submitting, setSubmitting] = useState(false)
-
- // Busy/private events are always visible to the room as just "busy" (no
- // details) or not visible at all — that's implied by the type, so the
- // visibility choice only makes sense for proposals.
- const handleTypeChange = (value: EventType) => {
- setType(value)
- if (value === "busy") setVisibility("busy_only")
- else if (value === "private") setVisibility("private")
- else setVisibility("room")
- }
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault()
- setError(null)
-
- if (!roomId) {
- setError("Please select a room.")
- return
- }
- const startDate = new Date(`${date}T${startTime}`)
- const endDate2 = new Date(`${multiDay ? endDate : date}T${endTime}`)
- if (endDate2 <= startDate) {
- setError("End time must be after the start time.")
- return
- }
-
- const payload = {
- room_id: roomId,
- type,
- title: title.trim(),
- description: description.trim(),
- start_time: toBackendDateTime(startDate),
- end_time: toBackendDateTime(endDate2),
- visibility: type === "proposal" ? visibility : type === "busy" ? "busy_only" : "private",
- min_people: type === "proposal" ? minPeople : 0,
- } as const
-
- setSubmitting(true)
- try {
- if (isEditing && editingEvent) {
- await api.updateEvent(editingEvent.id, payload)
- } else {
- await api.createEvent({ ...payload, status: type === "proposal" ? "proposed" : "confirmed" })
- }
- onCreated()
- onClose()
- } catch (err) {
- setError(
- err instanceof ApiError
- ? err.message
- : isEditing
- ? "Could not save the changes."
- : "Could not create the event.",
- )
- } finally {
- setSubmitting(false)
- }
- }
-
- const dialogTitle = isEditing ? "Edit event" : "Create event"
-
- if (rooms.length === 0) {
- return (
-
-
- You need to be in a room before creating an event. Create or join a room first.
-
-
- )
- }
-
- return (
-
-
-
- )
-}
diff --git a/frontend(experimental)/newchat/components/event-detail-dialog.tsx b/frontend(experimental)/newchat/components/event-detail-dialog.tsx
deleted file mode 100644
index 9b800c2..0000000
--- a/frontend(experimental)/newchat/components/event-detail-dialog.tsx
+++ /dev/null
@@ -1,294 +0,0 @@
-"use client"
-
-import { useState } from "react"
-import useSWR from "swr"
-import {
- Clock,
- Calendar as CalendarIcon,
- DoorClosed,
- Users,
- Check,
- X,
- HelpCircle,
- Trash2,
- CheckCircle2,
- Ban,
- Loader2,
- Pencil,
- UserRound,
-} from "lucide-react"
-import { GlassModal } from "./glass-modal"
-import * as api from "@/lib/api"
-import { ApiError } from "@/lib/api"
-import { useRoomMembers } from "@/lib/hooks"
-import type { CalendarEvent, Room, SignupStatus } from "@/lib/types"
-import { getRoomColor } from "@/lib/room-theme"
-import { parseDateTime, formatDayLabel, formatTime } from "@/lib/date-utils"
-
-interface EventDetailDialogProps {
- event: CalendarEvent
- room?: Room
- currentUserId: number
- onClose: () => void
- onChanged: () => void
- onEdit: () => void
-}
-
-const STATUS_STYLES: Record = {
- confirmed: "bg-emerald-500/30 text-emerald-100 border-emerald-400/40",
- proposed: "bg-amber-500/30 text-amber-100 border-amber-400/40",
- draft: "bg-white/20 text-white border-white/30",
- cancelled: "bg-red-500/30 text-red-100 border-red-400/40",
-}
-
-export function EventDetailDialog({
- event,
- room,
- currentUserId,
- onClose,
- onChanged,
- onEdit,
-}: EventDetailDialogProps) {
- const [busy, setBusy] = useState(null)
- const [error, setError] = useState(null)
- const [deleteConfirming, setDeleteConfirming] = useState(false)
-
- const { data: responses, mutate: mutateResponses } = useSWR(
- ["responses", event.id],
- () => api.getEventResponses(event.id),
- )
- const { members } = useRoomMembers(event.room_id)
-
- const start = parseDateTime(event.start_time)
- const end = parseDateTime(event.end_time)
- const color = getRoomColor(event.room_id)
-
- const creator = members.find((m) => m.id === event.creator_id)
- const creatorLabel =
- event.creator_id === currentUserId ? "you" : (creator?.username ?? `member #${event.creator_id}`)
-
- const canManage =
- event.creator_id === currentUserId || room?.role === "admin" || room?.role === "owner"
-
- const myResponse = responses?.find((r) => r.user_id === currentUserId)?.status
- const goingCount = responses?.filter((r) => r.status === "going").length ?? 0
- const maybeCount = responses?.filter((r) => r.status === "maybe").length ?? 0
- const declinedCount = responses?.filter((r) => r.status === "declined").length ?? 0
-
- const run = async (key: string, fn: () => Promise, refetchResponses = false) => {
- setBusy(key)
- setError(null)
- try {
- await fn()
- if (refetchResponses) await mutateResponses()
- onChanged()
- } catch (err) {
- setError(err instanceof ApiError ? err.message : "Action failed.")
- } finally {
- setBusy(null)
- }
- }
-
- const handleDelete = async () => {
- setBusy("delete")
- setError(null)
- try {
- await api.deleteEvent(event.id)
- onChanged()
- onClose()
- } catch (err) {
- setError(err instanceof ApiError ? err.message : "Action failed.")
- } finally {
- setBusy(null)
- }
- }
-
- const respond = (status: SignupStatus) =>
- run(`respond-${status}`, () => api.respondToEvent(event.id, status), true)
-
- const rsvpButtons: { status: SignupStatus; label: string; icon: typeof Check }[] = [
- { status: "going", label: "Going", icon: Check },
- { status: "maybe", label: "Maybe", icon: HelpCircle },
- { status: "declined", label: "Can't go", icon: X },
- ]
-
- return (
-
-
-
-
- {room?.name ?? `Room #${event.room_id}`}
- {event.type === "proposal" && (
-
- {event.status}
-
- )}
-
-
-
-
-
- {formatDayLabel(start)}
-
-
-
- {formatTime(start)} – {formatTime(end)}
-
-
-
- {event.type} · {event.visibility.replace("_", " ")}
-
-
-
- Created by {creatorLabel}
-
- {event.type === "proposal" && (
-
-
- {goingCount} going · needs {event.min_people}
-
- )}
-
-
- {event.description && (
-
- {event.description}
-
- )}
-
- {/* RSVP — only relevant for proposals, busy/private have no attendees to respond */}
- {event.type === "proposal" && event.status !== "cancelled" && (
-
-
Your response
-
- {rsvpButtons.map(({ status, label, icon: Icon }) => {
- const active = myResponse === status
- return (
-
- )
- })}
-
-
- {goingCount} going · {maybeCount} maybe · {declinedCount} declined
-
-
- )}
-
- {error && (
-
- {error}
-
- )}
-
- {/* Management actions */}
- {canManage && (
-
-
-
- {/* Confirm/cancel only make sense for proposals — busy/private
- blocks are already "confirmed" the moment they're created. */}
- {event.type === "proposal" && event.status !== "confirmed" && event.status !== "cancelled" && (
-
- )}
- {event.type === "proposal" && event.status !== "cancelled" && (
-
- )}
-
-
- {/* Delete lives in its own fixed spot, separate from Confirm/Cancel,
- so its position never shifts depending on which of those show. */}
-
- {!deleteConfirming ? (
-
- ) : (
-
-
- Weet je zeker dat je dit evenement wilt verwijderen? Dit kan niet ongedaan
- worden gemaakt.
-
-
- {/* Destructive action sits on top, off the "usual" spot. */}
-
- {/* Safe action sits below, in the spot people click fast out of habit. */}
-
-
-
- )}
-
-
- )}
-
- {/* Always-available close action, bottom right. */}
-
-
-
-
-
- )
-}
diff --git a/frontend(experimental)/newchat/components/glass-modal.tsx b/frontend(experimental)/newchat/components/glass-modal.tsx
deleted file mode 100644
index 0e59775..0000000
--- a/frontend(experimental)/newchat/components/glass-modal.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-"use client"
-
-import { useEffect, type ReactNode } from "react"
-import { X } from "lucide-react"
-
-interface GlassModalProps {
- title: string
- onClose: () => void
- children: ReactNode
- maxWidth?: string
-}
-
-export function GlassModal({ title, onClose, children, maxWidth = "max-w-md" }: GlassModalProps) {
- useEffect(() => {
- const onKey = (e: KeyboardEvent) => {
- if (e.key === "Escape") onClose()
- }
- window.addEventListener("keydown", onKey)
- document.body.style.overflow = "hidden"
- return () => {
- window.removeEventListener("keydown", onKey)
- document.body.style.overflow = ""
- }
- }, [onClose])
-
- return (
-
-
e.stopPropagation()}
- role="dialog"
- aria-modal="true"
- aria-label={title}
- >
-
-
{title}
-
-
- {children}
-
-
- )
-}
-
-// Shared input styling used across dialogs.
-export const fieldClass =
- "w-full rounded-xl border border-white/20 bg-white/10 px-4 py-2.5 text-white placeholder:text-white/50 focus:outline-none focus:ring-2 focus:ring-white/40"
-
-export const labelClass = "mb-1.5 block text-sm font-medium text-white/90"
diff --git a/frontend(experimental)/newchat/components/room-background.tsx b/frontend(experimental)/newchat/components/room-background.tsx
deleted file mode 100644
index 296e070..0000000
--- a/frontend(experimental)/newchat/components/room-background.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-"use client"
-
-import { useEffect, useRef, useState } from "react"
-import Image from "next/image"
-
-interface RoomBackgroundProps {
- src: string
- durationMs?: number
-}
-
-interface Layer {
- id: number
- src: string
-}
-
-// Stacks background images and crossfades between them whenever `src`
-// changes. Each new layer only starts fading in once it has actually
-// finished loading, and layers below a fully-faded-in layer are dropped
-// on transitionend rather than a guessed timeout — this keeps rapid
-// room switches (and slow/cold-cache images) visually consistent.
-export function RoomBackground({ src, durationMs = 700 }: RoomBackgroundProps) {
- const idRef = useRef(0)
- const [layers, setLayers] = useState(() => [{ id: idRef.current, src }])
- const [revealed, setRevealed] = useState>(() => new Set([idRef.current]))
-
- useEffect(() => {
- setLayers((prev) => {
- if (prev[prev.length - 1]?.src === src) return prev
- idRef.current += 1
- return [...prev, { id: idRef.current, src }]
- })
- }, [src])
-
- const reveal = (id: number) => {
- setRevealed((prev) => {
- if (prev.has(id)) return prev
- const next = new Set(prev)
- next.add(id)
- return next
- })
- }
-
- const handleTopLayerTransitionEnd = (id: number) => {
- // Once the newest layer has finished fading in, everything below it
- // is fully hidden and can be dropped.
- setLayers((prev) => {
- const idx = prev.findIndex((l) => l.id === id)
- if (idx <= 0) return prev
- return prev.slice(idx)
- })
- }
-
- const topId = layers[layers.length - 1]?.id
-
- return (
-
- {layers.map((layer, i) => {
- const isTop = i === layers.length - 1
- const isRevealed = revealed.has(layer.id)
- return (
- reveal(layer.id)}
- onTransitionEnd={() => {
- if (isTop && layer.id === topId) handleTopLayerTransitionEnd(layer.id)
- }}
- />
- )
- })}
-
- )
-}
\ No newline at end of file
diff --git a/frontend(experimental)/newchat/components/room-dialogs.tsx b/frontend(experimental)/newchat/components/room-dialogs.tsx
deleted file mode 100644
index befb5bb..0000000
--- a/frontend(experimental)/newchat/components/room-dialogs.tsx
+++ /dev/null
@@ -1,224 +0,0 @@
-"use client"
-
-import type React from "react"
-import { useState } from "react"
-import { Loader2, Search } from "lucide-react"
-import { GlassModal, fieldClass, labelClass } from "./glass-modal"
-import * as api from "@/lib/api"
-import { ApiError } from "@/lib/api"
-
-function randomCode() {
- const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
- return Array.from({ length: 8 }, () => chars[Math.floor(Math.random() * chars.length)]).join("")
-}
-
-interface CreateRoomDialogProps {
- onClose: () => void
- onCreated: (roomId: number) => void
-}
-
-export function CreateRoomDialog({ onClose, onCreated }: CreateRoomDialogProps) {
- const [name, setName] = useState("")
- const [code, setCode] = useState(randomCode())
- const [error, setError] = useState(null)
- const [submitting, setSubmitting] = useState(false)
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault()
- setError(null)
- if (!/^[A-Za-z0-9]{6,32}$/.test(code)) {
- setError("Invite code must be 6–32 letters or numbers.")
- return
- }
- setSubmitting(true)
- try {
- const res = await api.createRoom(name.trim(), code)
- onCreated(res.room_id)
- onClose()
- } catch (err) {
- setError(err instanceof ApiError ? err.message : "Could not create the room.")
- } finally {
- setSubmitting(false)
- }
- }
-
- return (
-
-
-
- )
-}
-
-interface JoinRoomDialogProps {
- onClose: () => void
- onJoined: (roomId: number) => void
-}
-
-export function JoinRoomDialog({ onClose, onJoined }: JoinRoomDialogProps) {
- const [code, setCode] = useState("")
- const [preview, setPreview] = useState(null)
- const [error, setError] = useState(null)
- const [searching, setSearching] = useState(false)
- const [submitting, setSubmitting] = useState(false)
-
- const handleSearch = async () => {
- setError(null)
- setPreview(null)
- if (!code.trim()) return
- setSearching(true)
- try {
- const res = await api.searchRoom(code.trim())
- if (res.status === "found" && res.room_name) {
- setPreview(res.room_name)
- } else {
- setError("No room found with that invite code.")
- }
- } catch (err) {
- setError(err instanceof ApiError ? err.message : "Search failed.")
- } finally {
- setSearching(false)
- }
- }
-
- const handleJoin = async (e: React.FormEvent) => {
- e.preventDefault()
- setError(null)
- setSubmitting(true)
- try {
- const res = await api.joinRoom(code.trim())
- if (res.status === "joined" && res.room_id) {
- onJoined(res.room_id)
- onClose()
- } else {
- setError("Could not join — the room was not found or you're already a member.")
- }
- } catch (err) {
- setError(err instanceof ApiError ? err.message : "Could not join the room.")
- } finally {
- setSubmitting(false)
- }
- }
-
- return (
-
-
-
- )
-}
diff --git a/frontend(experimental)/newchat/components/room-settings-dialog.tsx b/frontend(experimental)/newchat/components/room-settings-dialog.tsx
deleted file mode 100644
index b7fe84e..0000000
--- a/frontend(experimental)/newchat/components/room-settings-dialog.tsx
+++ /dev/null
@@ -1,302 +0,0 @@
-"use client"
-
-import { useState } from "react"
-import {
- Copy,
- Check,
- RefreshCw,
- Loader2,
- Crown,
- Shield,
- UserMinus,
- ArrowUp,
- ArrowDown,
- LogOut,
- Trash2,
-} from "lucide-react"
-import { GlassModal, fieldClass, labelClass } from "./glass-modal"
-import * as api from "@/lib/api"
-import { ApiError } from "@/lib/api"
-import type { Room } from "@/lib/types"
-import { useRoomMembers } from "@/lib/hooks"
-
-const ROLE_LEVEL = { owner: 3, admin: 2, member: 1 } as const
-
-interface RoomSettingsDialogProps {
- room: Room
- currentUserId: number
- onClose: () => void
- onChanged: () => void
- onLeftOrDeleted: () => void
-}
-
-export function RoomSettingsDialog({
- room,
- currentUserId,
- onClose,
- onChanged,
- onLeftOrDeleted,
-}: RoomSettingsDialogProps) {
- const { members, mutate: mutateMembers } = useRoomMembers(room.id)
- const [name, setName] = useState(room.name)
- const [inviteCode, setInviteCode] = useState(room.invite_code)
- const [copied, setCopied] = useState(false)
- const [busy, setBusy] = useState(null)
- const [error, setError] = useState(null)
- const [deleteConfirming, setDeleteConfirming] = useState(false)
-
- const isOwner = room.role === "owner"
- const isAdmin = room.role === "admin" || isOwner
- const myLevel = ROLE_LEVEL[room.role]
-
- const run = async (key: string, fn: () => Promise, opts?: { refetchMembers?: boolean }) => {
- setBusy(key)
- setError(null)
- try {
- await fn()
- if (opts?.refetchMembers) await mutateMembers()
- onChanged()
- } catch (err) {
- setError(err instanceof ApiError ? err.message : "Action failed.")
- } finally {
- setBusy(null)
- }
- }
-
- const copyCode = async () => {
- try {
- await navigator.clipboard.writeText(inviteCode)
- setCopied(true)
- setTimeout(() => setCopied(false), 1500)
- } catch {
- /* clipboard unavailable */
- }
- }
-
- const regenerate = () =>
- run("regen", async () => {
- const res = await api.regenerateInviteCode(room.id)
- setInviteCode(res.invite_code)
- })
-
- const rename = () => run("rename", () => api.renameRoom(room.id, name.trim()))
-
- const changeRole = (userId: number, role: "member" | "admin") =>
- run(`role-${userId}`, () => api.updateMemberRole(room.id, userId, role), {
- refetchMembers: true,
- })
-
- const remove = (userId: number) =>
- run(`remove-${userId}`, () => api.removeMember(room.id, userId), { refetchMembers: true })
-
- const leave = () =>
- run("leave", async () => {
- await api.leaveRoom(room.id)
- onLeftOrDeleted()
- onClose()
- })
-
- const destroy = () =>
- run("delete", async () => {
- await api.deleteRoom(room.id)
- onLeftOrDeleted()
- onClose()
- })
-
- return (
-
-
- {/* Name */}
-
-
-
- setName(e.target.value)}
- disabled={!isAdmin}
- className={fieldClass}
- />
- {isAdmin && (
-
- )}
-
-
-
- {/* Invite code */}
-
-
-
-
- {inviteCode}
-
-
- {isAdmin && (
-
- )}
-
-
-
- {/* Members */}
-
-
-
- {members.map((m) => {
- const targetLevel = ROLE_LEVEL[m.role]
- const isSelf = m.id === currentUserId
- const canManageTarget = isAdmin && !isSelf && myLevel > targetLevel
- return (
-
-
- {m.username.charAt(0).toUpperCase()}
-
-
- {m.username}
- {isSelf && (you)}
-
-
- {m.role === "owner" && }
- {m.role === "admin" && }
- {m.role}
-
-
- {/* Owner can promote/demote between member and admin */}
- {isOwner && !isSelf && m.role === "member" && (
-
- )}
- {isOwner && !isSelf && m.role === "admin" && (
-
- )}
- {canManageTarget && (
-
- )}
-
- )
- })}
-
-
-
- {error && (
-
- {error}
-
- )}
-
- {/* Danger zone */}
-
-
-
- {isOwner && (
-
- {!deleteConfirming ? (
-
- ) : (
-
-
- Weet je zeker dat je deze room wilt verwijderen? Dit kan niet ongedaan worden
- gemaakt.
-
-
- {/* Destructive action sits on top, off the "usual" spot. */}
-
- {/* Safe action sits below, in the spot people click fast out of habit. */}
-
-
-
- )}
-
- )}
-
-
-
- )
-}
diff --git a/frontend(experimental)/newchat/components/sidebar.tsx b/frontend(experimental)/newchat/components/sidebar.tsx
deleted file mode 100644
index 962ce7d..0000000
--- a/frontend(experimental)/newchat/components/sidebar.tsx
+++ /dev/null
@@ -1,227 +0,0 @@
-"use client"
-
-import { useState } from "react"
-import {
- ChevronLeft,
- ChevronRight,
- Plus,
- LogOut,
- Users,
- DoorOpen,
- Layers,
- Crown,
- Shield,
-} from "lucide-react"
-import type { Room } from "@/lib/types"
-import type { User } from "@/lib/types"
-import { getRoomColor } from "@/lib/room-theme"
-import { addDays, isSameDay, startOfWeek, formatMonthYear } from "@/lib/date-utils"
-
-interface SidebarProps {
- rooms: Room[]
- activeRoomId: number | null
- onSelectRoom: (id: number | null) => void
- selectedDate: Date
- onSelectDate: (d: Date) => void
- onCreateEvent: () => void
- onCreateRoom: () => void
- onJoinRoom: () => void
- user: User | null
- onLogout: () => void
- onOpenAccountSettings: () => void
-}
-
-const WEEKDAY_MINI = ["S", "M", "T", "W", "T", "F", "S"]
-
-function RoleBadge({ role }: { role: Room["role"] }) {
- if (role === "owner")
- return
- if (role === "admin")
- return
- return null
-}
-
-export function Sidebar({
- rooms,
- activeRoomId,
- onSelectRoom,
- selectedDate,
- onSelectDate,
- onCreateEvent,
- onCreateRoom,
- onJoinRoom,
- user,
- onLogout,
- onOpenAccountSettings,
-}: SidebarProps) {
- const [miniMonth, setMiniMonth] = useState(() => {
- const d = new Date(selectedDate)
- d.setDate(1)
- return d
- })
-
- const weekStart = startOfWeek(selectedDate)
- const weekEnd = addDays(weekStart, 6)
-
- // Build mini-calendar grid for miniMonth.
- const firstDay = new Date(miniMonth.getFullYear(), miniMonth.getMonth(), 1)
- const offset = firstDay.getDay()
- const daysInMonth = new Date(miniMonth.getFullYear(), miniMonth.getMonth() + 1, 0).getDate()
- const cells: (Date | null)[] = []
- for (let i = 0; i < offset; i++) cells.push(null)
- for (let d = 1; d <= daysInMonth; d++) {
- cells.push(new Date(miniMonth.getFullYear(), miniMonth.getMonth(), d))
- }
-
- const today = new Date()
-
- const shiftMonth = (delta: number) => {
- setMiniMonth((m) => new Date(m.getFullYear(), m.getMonth() + delta, 1))
- }
-
- const inSelectedWeek = (d: Date) => d >= weekStart && d <= weekEnd
-
- return (
-
-
-
-
- {/* Mini Calendar */}
-
-
-
{formatMonthYear(miniMonth)}
-
-
-
-
-
-
-
- {WEEKDAY_MINI.map((day, i) => (
-
- {day}
-
- ))}
- {cells.map((date, i) => {
- if (!date) return
- const selected = isSameDay(date, selectedDate)
- const isToday = isSameDay(date, today)
- const highlightWeek = inSelectedWeek(date) && !selected
- return (
-
- )
- })}
-
-
-
- {/* Rooms */}
-
-
Rooms
-
-
-
-
- {rooms.map((room) => {
- const color = getRoomColor(room.id)
- const active = activeRoomId === room.id
- return (
-
- )
- })}
-
- {rooms.length === 0 && (
-
- No rooms yet. Create or join one to get started.
-
- )}
-
-
-
-
-
- {/* User footer */}
-
-
-
-
-
- )
-}
diff --git a/frontend(experimental)/newchat/components/theme-provider.tsx b/frontend(experimental)/newchat/components/theme-provider.tsx
deleted file mode 100644
index 55c2f6e..0000000
--- a/frontend(experimental)/newchat/components/theme-provider.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-'use client'
-
-import * as React from 'react'
-import {
- ThemeProvider as NextThemesProvider,
- type ThemeProviderProps,
-} from 'next-themes'
-
-export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
- return {children}
-}
diff --git a/frontend(experimental)/newchat/components/ui/accordion.tsx b/frontend(experimental)/newchat/components/ui/accordion.tsx
deleted file mode 100644
index b69428b..0000000
--- a/frontend(experimental)/newchat/components/ui/accordion.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-'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/frontend(experimental)/newchat/components/ui/alert-dialog.tsx b/frontend(experimental)/newchat/components/ui/alert-dialog.tsx
deleted file mode 100644
index e58f90a..0000000
--- a/frontend(experimental)/newchat/components/ui/alert-dialog.tsx
+++ /dev/null
@@ -1,141 +0,0 @@
-'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/frontend(experimental)/newchat/components/ui/alert.tsx b/frontend(experimental)/newchat/components/ui/alert.tsx
deleted file mode 100644
index 2b2ced8..0000000
--- a/frontend(experimental)/newchat/components/ui/alert.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-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 p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
- {
- 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/frontend(experimental)/newchat/components/ui/aspect-ratio.tsx b/frontend(experimental)/newchat/components/ui/aspect-ratio.tsx
deleted file mode 100644
index 794c6f4..0000000
--- a/frontend(experimental)/newchat/components/ui/aspect-ratio.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-'use client'
-
-import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio'
-
-const AspectRatio = AspectRatioPrimitive.Root
-
-export { AspectRatio }
diff --git a/frontend(experimental)/newchat/components/ui/avatar.tsx b/frontend(experimental)/newchat/components/ui/avatar.tsx
deleted file mode 100644
index 77fde46..0000000
--- a/frontend(experimental)/newchat/components/ui/avatar.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-'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/frontend(experimental)/newchat/components/ui/badge.tsx b/frontend(experimental)/newchat/components/ui/badge.tsx
deleted file mode 100644
index 1238b27..0000000
--- a/frontend(experimental)/newchat/components/ui/badge.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-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-full 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 hover:bg-primary/80',
- secondary:
- 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
- destructive:
- 'border-transparent bg-destructive text-destructive-foreground 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/frontend(experimental)/newchat/components/ui/breadcrumb.tsx b/frontend(experimental)/newchat/components/ui/breadcrumb.tsx
deleted file mode 100644
index d731202..0000000
--- a/frontend(experimental)/newchat/components/ui/breadcrumb.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-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) => )
-Breadcrumb.displayName = 'Breadcrumb'
-
-const BreadcrumbList = React.forwardRef<
- HTMLOListElement,
- React.ComponentPropsWithoutRef<'ol'>
->(({ className, ...props }, ref) => (
-
-))
-BreadcrumbList.displayName = 'BreadcrumbList'
-
-const BreadcrumbItem = React.forwardRef<
- HTMLLIElement,
- React.ComponentPropsWithoutRef<'li'>
->(({ className, ...props }, ref) => (
-
-))
-BreadcrumbItem.displayName = 'BreadcrumbItem'
-
-const BreadcrumbLink = React.forwardRef<
- HTMLAnchorElement,
- React.ComponentPropsWithoutRef<'a'> & {
- asChild?: boolean
- }
->(({ asChild, className, ...props }, ref) => {
- const Comp = asChild ? Slot : 'a'
-
- return (
-
- )
-})
-BreadcrumbLink.displayName = 'BreadcrumbLink'
-
-const BreadcrumbPage = React.forwardRef<
- HTMLSpanElement,
- React.ComponentPropsWithoutRef<'span'>
->(({ className, ...props }, ref) => (
-
-))
-BreadcrumbPage.displayName = 'BreadcrumbPage'
-
-const BreadcrumbSeparator = ({
- children,
- className,
- ...props
-}: React.ComponentProps<'li'>) => (
- svg]:w-3.5 [&>svg]:h-3.5', className)}
- {...props}
- >
- {children ?? }
-
-)
-BreadcrumbSeparator.displayName = 'BreadcrumbSeparator'
-
-const BreadcrumbEllipsis = ({
- className,
- ...props
-}: React.ComponentProps<'span'>) => (
-
-
- More
-
-)
-BreadcrumbEllipsis.displayName = 'BreadcrumbElipssis'
-
-export {
- Breadcrumb,
- BreadcrumbList,
- BreadcrumbItem,
- BreadcrumbLink,
- BreadcrumbPage,
- BreadcrumbSeparator,
- BreadcrumbEllipsis,
-}
diff --git a/frontend(experimental)/newchat/components/ui/button-group.tsx b/frontend(experimental)/newchat/components/ui/button-group.tsx
deleted file mode 100644
index 72e6a61..0000000
--- a/frontend(experimental)/newchat/components/ui/button-group.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-import { Slot } from '@radix-ui/react-slot'
-import { cva, type VariantProps } from 'class-variance-authority'
-
-import { cn } from '@/lib/utils'
-import { Separator } from '@/components/ui/separator'
-
-const buttonGroupVariants = cva(
- "flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
- {
- variants: {
- orientation: {
- horizontal:
- '[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none',
- vertical:
- 'flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none',
- },
- },
- defaultVariants: {
- orientation: 'horizontal',
- },
- },
-)
-
-function ButtonGroup({
- className,
- orientation,
- ...props
-}: React.ComponentProps<'div'> & VariantProps) {
- return (
-
- )
-}
-
-function ButtonGroupText({
- className,
- asChild = false,
- ...props
-}: React.ComponentProps<'div'> & {
- asChild?: boolean
-}) {
- const Comp = asChild ? Slot : 'div'
-
- return (
-
- )
-}
-
-function ButtonGroupSeparator({
- className,
- orientation = 'vertical',
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-export {
- ButtonGroup,
- ButtonGroupSeparator,
- ButtonGroupText,
- buttonGroupVariants,
-}
diff --git a/frontend(experimental)/newchat/components/ui/button.tsx b/frontend(experimental)/newchat/components/ui/button.tsx
deleted file mode 100644
index ee95f41..0000000
--- a/frontend(experimental)/newchat/components/ui/button.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import * as React from 'react'
-import { Slot } from '@radix-ui/react-slot'
-import { cva, type VariantProps } from 'class-variance-authority'
-
-import { cn } from '@/lib/utils'
-
-const buttonVariants = cva(
- 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
- {
- variants: {
- variant: {
- default: 'bg-primary text-primary-foreground hover:bg-primary/90',
- destructive:
- 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
- outline:
- 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
- secondary:
- 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
- ghost: 'hover:bg-accent hover:text-accent-foreground',
- link: 'text-primary underline-offset-4 hover:underline',
- },
- size: {
- default: 'h-10 px-4 py-2',
- sm: 'h-9 rounded-md px-3',
- lg: 'h-11 rounded-md px-8',
- icon: 'h-10 w-10',
- },
- },
- defaultVariants: {
- variant: 'default',
- size: 'default',
- },
- },
-)
-
-export interface ButtonProps
- extends React.ButtonHTMLAttributes,
- VariantProps {
- asChild?: boolean
-}
-
-const Button = React.forwardRef(
- ({ className, variant, size, asChild = false, ...props }, ref) => {
- const Comp = asChild ? Slot : 'button'
- return (
-
- )
- },
-)
-Button.displayName = 'Button'
-
-export { Button, buttonVariants }
diff --git a/frontend(experimental)/newchat/components/ui/calendar.tsx b/frontend(experimental)/newchat/components/ui/calendar.tsx
deleted file mode 100644
index ee03b65..0000000
--- a/frontend(experimental)/newchat/components/ui/calendar.tsx
+++ /dev/null
@@ -1,213 +0,0 @@
-'use client'
-
-import * as React from 'react'
-import {
- ChevronDownIcon,
- ChevronLeftIcon,
- ChevronRightIcon,
-} from 'lucide-react'
-import { DayButton, DayPicker, getDefaultClassNames } from 'react-day-picker'
-
-import { cn } from '@/lib/utils'
-import { Button, buttonVariants } from '@/components/ui/button'
-
-function Calendar({
- className,
- classNames,
- showOutsideDays = true,
- captionLayout = 'label',
- buttonVariant = 'ghost',
- formatters,
- components,
- ...props
-}: React.ComponentProps & {
- buttonVariant?: React.ComponentProps['variant']
-}) {
- const defaultClassNames = getDefaultClassNames()
-
- return (
- svg]:rotate-180`,
- String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
- className,
- )}
- captionLayout={captionLayout}
- formatters={{
- formatMonthDropdown: (date) =>
- date.toLocaleString('default', { month: 'short' }),
- ...formatters,
- }}
- classNames={{
- root: cn('w-fit', defaultClassNames.root),
- months: cn(
- 'flex gap-4 flex-col md:flex-row relative',
- defaultClassNames.months,
- ),
- month: cn('flex flex-col w-full gap-4', defaultClassNames.month),
- nav: cn(
- 'flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between',
- defaultClassNames.nav,
- ),
- button_previous: cn(
- buttonVariants({ variant: buttonVariant }),
- 'size-[--cell-size] aria-disabled:opacity-50 p-0 select-none',
- defaultClassNames.button_previous,
- ),
- button_next: cn(
- buttonVariants({ variant: buttonVariant }),
- 'size-[--cell-size] aria-disabled:opacity-50 p-0 select-none',
- defaultClassNames.button_next,
- ),
- month_caption: cn(
- 'flex items-center justify-center h-[--cell-size] w-full px-[--cell-size]',
- defaultClassNames.month_caption,
- ),
- dropdowns: cn(
- 'w-full flex items-center text-sm font-medium justify-center h-[--cell-size] gap-1.5',
- defaultClassNames.dropdowns,
- ),
- dropdown_root: cn(
- 'relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md',
- defaultClassNames.dropdown_root,
- ),
- dropdown: cn(
- 'absolute bg-popover inset-0 opacity-0',
- defaultClassNames.dropdown,
- ),
- caption_label: cn(
- 'select-none font-medium',
- captionLayout === 'label'
- ? 'text-sm'
- : 'rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5',
- defaultClassNames.caption_label,
- ),
- table: 'w-full border-collapse',
- weekdays: cn('flex', defaultClassNames.weekdays),
- weekday: cn(
- 'text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none',
- defaultClassNames.weekday,
- ),
- week: cn('flex w-full mt-2', defaultClassNames.week),
- week_number_header: cn(
- 'select-none w-[--cell-size]',
- defaultClassNames.week_number_header,
- ),
- week_number: cn(
- 'text-[0.8rem] select-none text-muted-foreground',
- defaultClassNames.week_number,
- ),
- day: cn(
- 'relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none',
- defaultClassNames.day,
- ),
- range_start: cn(
- 'rounded-l-md bg-accent',
- defaultClassNames.range_start,
- ),
- range_middle: cn('rounded-none', defaultClassNames.range_middle),
- range_end: cn('rounded-r-md bg-accent', defaultClassNames.range_end),
- today: cn(
- 'bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none',
- defaultClassNames.today,
- ),
- outside: cn(
- 'text-muted-foreground aria-selected:text-muted-foreground',
- defaultClassNames.outside,
- ),
- disabled: cn(
- 'text-muted-foreground opacity-50',
- defaultClassNames.disabled,
- ),
- hidden: cn('invisible', defaultClassNames.hidden),
- ...classNames,
- }}
- components={{
- Root: ({ className, rootRef, ...props }) => {
- return (
-
- )
- },
- Chevron: ({ className, orientation, ...props }) => {
- if (orientation === 'left') {
- return (
-
- )
- }
-
- if (orientation === 'right') {
- return (
-
- )
- }
-
- return (
-
- )
- },
- DayButton: CalendarDayButton,
- WeekNumber: ({ children, ...props }) => {
- return (
-
-
- {children}
-
- |
- )
- },
- ...components,
- }}
- {...props}
- />
- )
-}
-
-function CalendarDayButton({
- className,
- day,
- modifiers,
- ...props
-}: React.ComponentProps) {
- const defaultClassNames = getDefaultClassNames()
-
- const ref = React.useRef(null)
- React.useEffect(() => {
- if (modifiers.focused) ref.current?.focus()
- }, [modifiers.focused])
-
- return (
-