// Deterministic per-room theming: background image + accent color. import type { CSSProperties } from "react" import type { CalendarEvent, SignupStatus } from "./types" const ROOM_BACKGROUNDS = [ "/backgrounds/room-1.png", "/backgrounds/room-2.png", "/backgrounds/room-3.png", "/backgrounds/room-4.png", "/backgrounds/room-5.png", ] export const ALL_ROOMS_BACKGROUND = "/backgrounds/all-rooms.png" // Tailwind background classes used to color-code events per room. const ROOM_COLORS = [ { bg: "bg-blue-500", dot: "bg-blue-500", ring: "ring-blue-400" }, { bg: "bg-emerald-500", dot: "bg-emerald-500", ring: "ring-emerald-400" }, { bg: "bg-purple-500", dot: "bg-purple-500", ring: "ring-purple-400" }, { bg: "bg-orange-500", dot: "bg-orange-500", ring: "ring-orange-400" }, { bg: "bg-pink-500", dot: "bg-pink-500", ring: "ring-pink-400" }, { bg: "bg-cyan-500", dot: "bg-cyan-500", ring: "ring-cyan-400" }, { bg: "bg-amber-500", dot: "bg-amber-500", ring: "ring-amber-400" }, { bg: "bg-teal-500", dot: "bg-teal-500", ring: "ring-teal-400" }, ] export function getRoomBackground(roomId: number | null): string { if (roomId === null) return ALL_ROOMS_BACKGROUND return ROOM_BACKGROUNDS[roomId % ROOM_BACKGROUNDS.length] } // Guarded against null/undefined/NaN room ids (e.g. personal blocks that // arrive without a valid room_id) so we never hand back an undefined color, // which previously rendered as no background color at all. export function getRoomColor(roomId: number | null | undefined) { const id = typeof roomId === "number" && Number.isFinite(roomId) ? roomId : 0 const idx = ((id % ROOM_COLORS.length) + ROOM_COLORS.length) % ROOM_COLORS.length return ROOM_COLORS[idx] } // ---------- Event coloring by type / status / RSVP ---------- // // Confirmed and busy events each pick a color from a small themed pool, // deterministically by event id — so color varies event-to-event rather // than clustering by room, but a given event still looks the same on // every render/refresh. Private, proposed, declined and cancelled get // one fixed look each so they're recognizable at a glance. export interface EventStyle { bg: string ring: string /** Dashed border — signals "not yet settled" (pending proposal). */ dashed?: boolean /** Diagonal hatch pattern — signals "you've declined this". */ striped?: boolean /** Reduced opacity — signals "you're not going". */ muted?: boolean /** Outline only, transparent fill — signals "private, details hidden". */ outline?: boolean } const CONFIRMED_PALETTE = [ { bg: "bg-emerald-500", ring: "ring-emerald-400" }, { bg: "bg-blue-500", ring: "ring-blue-400" }, { bg: "bg-purple-500", ring: "ring-purple-400" }, ] const BUSY_PALETTE = [ { bg: "bg-orange-500", ring: "ring-orange-400" }, { bg: "bg-yellow-400", ring: "ring-yellow-300" }, { bg: "bg-red-500", ring: "ring-red-400" }, ] function pickFromPalette( palette: { bg: string; ring: string }[], seed: number | null | undefined, ) { const id = typeof seed === "number" && Number.isFinite(seed) ? seed : 0 const idx = ((id % palette.length) + palette.length) % palette.length return palette[idx] } const PRIVATE_STYLE: EventStyle = { bg: "bg-white/10", ring: "ring-white/60", outline: true } const PROPOSED_STYLE: EventStyle = { bg: "bg-white/25", ring: "ring-white/50", dashed: true } const CANCELLED_STYLE: EventStyle = { bg: "bg-red-500", ring: "ring-red-400" } const DECLINED_STYLE: EventStyle = { bg: "bg-neutral-500", ring: "ring-neutral-400", striped: true, muted: true, } // Inline CSS for diagonal hatch patterns — Tailwind has no built-in // striped-background utility, so this is applied via style prop. export const STRIPE_STYLE: CSSProperties = { backgroundImage: "repeating-linear-gradient(135deg, rgba(255,255,255,0.18) 0px, rgba(255,255,255,0.18) 6px, transparent 6px, transparent 12px)", } export function getEventStyle( event: Pick, myResponse: SignupStatus | undefined, ): EventStyle { if (event.type === "private") return PRIVATE_STYLE if (event.type === "busy") return pickFromPalette(BUSY_PALETTE, event.id) // Proposal-type events: colored by where they stand. if (event.status === "cancelled") return CANCELLED_STYLE if (myResponse === "declined") return DECLINED_STYLE if (event.status === "proposed" || event.status === "draft") return PROPOSED_STYLE // Confirmed proposal: pick from the confirmed palette, per event. return pickFromPalette(CONFIRMED_PALETTE, event.id) }