228 lines
7.9 KiB
TypeScript
228 lines
7.9 KiB
TypeScript
"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 <Crown className="h-3.5 w-3.5 text-amber-300" aria-label="Owner" />
|
|
if (role === "admin")
|
|
return <Shield className="h-3.5 w-3.5 text-blue-200" aria-label="Admin" />
|
|
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 (
|
|
<div className="flex h-full w-64 flex-col justify-between rounded-tr-3xl border-r border-white/20 bg-white/10 p-4 shadow-xl backdrop-blur-lg">
|
|
<div className="flex min-h-0 flex-1 flex-col">
|
|
<button
|
|
onClick={onCreateEvent}
|
|
className="mb-6 flex items-center justify-center gap-2 rounded-full bg-blue-500 px-4 py-3 text-white transition-colors hover:bg-blue-600"
|
|
>
|
|
<Plus className="h-5 w-5" />
|
|
<span>Create event</span>
|
|
</button>
|
|
|
|
{/* Mini Calendar */}
|
|
<div className="mb-6">
|
|
<div className="mb-3 flex items-center justify-between">
|
|
<h3 className="font-medium text-white">{formatMonthYear(miniMonth)}</h3>
|
|
<div className="flex gap-1">
|
|
<button
|
|
onClick={() => shiftMonth(-1)}
|
|
className="rounded-full p-1 hover:bg-white/20"
|
|
aria-label="Previous month"
|
|
>
|
|
<ChevronLeft className="h-4 w-4 text-white" />
|
|
</button>
|
|
<button
|
|
onClick={() => shiftMonth(1)}
|
|
className="rounded-full p-1 hover:bg-white/20"
|
|
aria-label="Next month"
|
|
>
|
|
<ChevronRight className="h-4 w-4 text-white" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-7 gap-1 text-center">
|
|
{WEEKDAY_MINI.map((day, i) => (
|
|
<div key={i} className="py-1 text-xs font-medium text-white/70">
|
|
{day}
|
|
</div>
|
|
))}
|
|
{cells.map((date, i) => {
|
|
if (!date) return <div key={i} />
|
|
const selected = isSameDay(date, selectedDate)
|
|
const isToday = isSameDay(date, today)
|
|
const highlightWeek = inSelectedWeek(date) && !selected
|
|
return (
|
|
<button
|
|
key={i}
|
|
onClick={() => onSelectDate(date)}
|
|
className={`flex h-7 w-7 items-center justify-center rounded-full text-xs transition-colors ${
|
|
selected
|
|
? "bg-blue-500 font-semibold text-white"
|
|
: highlightWeek
|
|
? "bg-white/15 text-white"
|
|
: "text-white hover:bg-white/20"
|
|
} ${isToday && !selected ? "ring-1 ring-white/60" : ""}`}
|
|
>
|
|
{date.getDate()}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Rooms */}
|
|
<div className="mb-2 flex items-center justify-between">
|
|
<h3 className="font-medium text-white">Rooms</h3>
|
|
</div>
|
|
<div className="-mr-1 flex-1 space-y-1 overflow-y-auto pr-1">
|
|
<button
|
|
onClick={() => onSelectRoom(null)}
|
|
className={`flex w-full items-center gap-3 rounded-lg px-2 py-2 text-left text-sm transition-colors ${
|
|
activeRoomId === null ? "bg-white/25 text-white" : "text-white/90 hover:bg-white/15"
|
|
}`}
|
|
>
|
|
<Layers className="h-4 w-4 shrink-0" />
|
|
<span className="truncate">All rooms</span>
|
|
</button>
|
|
|
|
{rooms.map((room) => {
|
|
const color = getRoomColor(room.id)
|
|
const active = activeRoomId === room.id
|
|
return (
|
|
<button
|
|
key={room.id}
|
|
onClick={() => onSelectRoom(room.id)}
|
|
className={`flex w-full items-center gap-3 rounded-lg px-2 py-2 text-left text-sm transition-colors ${
|
|
active ? "bg-white/25 text-white" : "text-white/90 hover:bg-white/15"
|
|
}`}
|
|
>
|
|
<span className={`h-3 w-3 shrink-0 rounded-sm ${color.dot}`} />
|
|
<span className="flex-1 truncate">{room.name}</span>
|
|
<RoleBadge role={room.role} />
|
|
</button>
|
|
)
|
|
})}
|
|
|
|
{rooms.length === 0 && (
|
|
<p className="px-2 py-2 text-xs text-white/60">
|
|
No rooms yet. Create or join one to get started.
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="mt-3 flex gap-2">
|
|
<button
|
|
onClick={onCreateRoom}
|
|
className="flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-white/20 bg-white/10 px-2 py-2 text-xs text-white transition-colors hover:bg-white/20"
|
|
>
|
|
<Plus className="h-3.5 w-3.5" /> New
|
|
</button>
|
|
<button
|
|
onClick={onJoinRoom}
|
|
className="flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-white/20 bg-white/10 px-2 py-2 text-xs text-white transition-colors hover:bg-white/20"
|
|
>
|
|
<DoorOpen className="h-3.5 w-3.5" /> Join
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* User footer */}
|
|
<div className="mt-4 flex items-center gap-3 border-t border-white/20 pt-4">
|
|
<button
|
|
onClick={onOpenAccountSettings}
|
|
className="flex min-w-0 flex-1 items-center gap-3 rounded-lg p-1 text-left transition-colors hover:bg-white/10"
|
|
>
|
|
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-blue-500 font-bold text-white">
|
|
{user?.username?.charAt(0).toUpperCase() ?? "U"}
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="truncate text-sm font-medium text-white">{user?.username}</p>
|
|
<p className="truncate text-xs text-white/60">{user?.email}</p>
|
|
</div>
|
|
</button>
|
|
<button
|
|
onClick={onLogout}
|
|
className="rounded-lg p-2 text-white/80 transition-colors hover:bg-white/20 hover:text-white"
|
|
aria-label="Sign out"
|
|
>
|
|
<LogOut className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|