SPB Git
5.3 KB · 44 lines tsx
1"use client";2import { useState } from "react";3import { ChevronLeft, ChevronRight, Plus } from "lucide-react";4import { Empty } from "@/components/ui/primitives";5import { cn } from "@/lib/utils";6import type { RowOut } from "@/lib/databases/types";7import { dateOf } from "@/lib/databases/values";8import { useDb } from "../hooks";9import { CellDisplay } from "../cells/Cell";1011const iso = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;12const DOW = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];13export function CalendarView() {14  const ctx = useDb(); const dp = ctx.props.find((p) => p.id === ctx.view.config.dateProperty); const [cur, setCur] = useState(() => { const d = new Date(); d.setDate(1); d.setHours(0, 0, 0, 0); return d; });15  if (!dp) return <Empty title="Choose a date property" hint="Open the view settings (click the active tab) and pick the date property to place rows on the calendar." />;16  const byDay = new Map<string, RowOut[]>(); for (const r of ctx.rows) { const d = dateOf(r.values[dp.id]); if (!d) continue; const k = iso(d); byDay.set(k, [...(byDay.get(k) ?? []), r]); }17  const start = new Date(cur); start.setDate(1 - ((cur.getDay() + 6) % 7)); const days: Date[] = []; for (let i = 0; i < 42; i++) { const d = new Date(start); d.setDate(start.getDate() + i); days.push(d); }18  const today = iso(new Date()); const month = cur.getMonth(); const label = cur.toLocaleDateString("en-CA", { month: "long", year: "numeric" });19  const nav = (n: number) => setCur((c) => { const d = new Date(c); d.setMonth(d.getMonth() + n); return d; });20  const add = (d: Date) => ctx.addRow({ [dp.id]: dp.type === "date" ? iso(d) : undefined });21  const canAdd = dp.type === "date"; const undated = ctx.rows.filter((r) => !dateOf(r.values[dp.id]));22  const header = <div className="mb-2 flex items-center gap-1"><div className="text-sm font-medium capitalize">{label}</div><div className="ml-auto flex items-center gap-0.5"><button type="button" onClick={() => nav(-1)} className="grid size-8 place-items-center rounded-md hover:bg-bg-sunk"><ChevronLeft className="size-4" /></button><button type="button" onClick={() => { const d = new Date(); d.setDate(1); d.setHours(0, 0, 0, 0); setCur(d); }} className="h-8 rounded-md px-2 text-[12px] hover:bg-bg-sunk">Today</button><button type="button" onClick={() => nav(1)} className="grid size-8 place-items-center rounded-md hover:bg-bg-sunk"><ChevronRight className="size-4" /></button></div></div>;23  if (ctx.isMobile) {24    const inMonth = days.filter((d) => d.getMonth() === month && byDay.has(iso(d)));25    return <div>{header}{inMonth.length === 0 && <div className="py-8 text-center text-[13px] text-fg-muted">Nothing this month.</div>}<div className="space-y-3">{inMonth.map((d) => <div key={iso(d)}><div className={cn("mb-1 text-[12px] font-medium", iso(d) === today ? "text-accent" : "text-fg-muted")}>{d.toLocaleDateString("en-CA", { weekday: "short", day: "numeric", month: "short" })}</div><div className="space-y-1">{byDay.get(iso(d))!.map((r) => <Chip key={r.id} row={r} big />)}</div></div>)}</div>{canAdd && <button type="button" onClick={() => add(new Date())} className="mt-3 flex h-9 items-center gap-1 rounded-md px-2 text-[13px] text-fg-muted hover:bg-bg-sunk"><Plus className="size-4" /> New today</button>}</div>;26  }27  return (28    <div>{header}29      <div className="grid grid-cols-7 border-l border-t border-line text-[11px]">{DOW.map((d) => <div key={d} className="border-b border-r border-line px-1.5 py-1 font-medium text-fg-muted">{d}</div>)}30        {days.map((d) => { const k = iso(d); const evs = byDay.get(k) ?? []; return (31          <div key={k} onDoubleClick={() => canAdd && add(d)} className={cn("group/day relative min-h-[6rem] border-b border-r border-line p-1", d.getMonth() !== month && "bg-bg-sunk/40 text-fg-faint")}>32            <div className="flex items-center"><span className={cn("grid size-5 place-items-center rounded-full text-[11px]", k === today && "bg-accent font-medium text-accent-fg")}>{d.getDate()}</span>{canAdd && <button type="button" onClick={() => add(d)} className="ml-auto hidden size-5 place-items-center rounded text-fg-faint hover:bg-bg-sunk hover:text-fg group-hover/day:grid"><Plus className="size-3" /></button>}</div>33            <div className="mt-0.5 space-y-0.5">{evs.slice(0, 4).map((r) => <Chip key={r.id} row={r} />)}{evs.length > 4 && <div className="px-1 text-[10px] text-fg-faint">+{evs.length - 4} more</div>}</div>34          </div>); })}35      </div>36      {undated.length > 0 && <div className="mt-3 text-[12px] text-fg-faint">{undated.length} row{undated.length > 1 ? "s" : ""} without a date (visible in the table view).</div>}37    </div>38  );39}40function Chip({ row, big }: { row: RowOut; big?: boolean }) {41  const ctx = useDb(); const st = ctx.props.find((p) => p.type === "status" || p.type === "select");42  return <button type="button" onClick={() => ctx.open(row.id)} className={cn("flex w-full items-center gap-1 truncate rounded border border-line bg-bg-elev text-left hover:bg-bg-sunk", big ? "h-10 px-2 text-[13px]" : "h-5 px-1 text-[11px]")}><span className="truncate">{row.icon ? `${row.icon} ` : ""}{row.title || "Untitled"}</span>{big && st && <span className="ml-auto shrink-0"><CellDisplay p={st} value={row.values[st.id]} compact /></span>}</button>;43}44
Files · main
Menu

Theme