diff --git a/www/src/modules/create/preview/blocks/index.ts b/www/src/modules/create/preview/blocks/index.ts index 292989d0f..6ed761611 100644 --- a/www/src/modules/create/preview/blocks/index.ts +++ b/www/src/modules/create/preview/blocks/index.ts @@ -9,14 +9,14 @@ export interface BlockMeta { // once its `./.tsx` file lands — entries without a file are simply not // offered yet, so this list can stay ahead of the implementations. // Slugs share a namespace with component and group-example slugs and are looked -// up first — never reuse one of those (e.g. "calendar" → "scheduling" here). +// up first — never reuse one of those (e.g. a calendar block must not be +// slugged "calendar", which is a component). export const BLOCKS: BlockMeta[] = [ { slug: "dashboard", name: "Dashboard" }, { slug: "settings", name: "Settings" }, { slug: "data-table", name: "Data Table" }, { slug: "mail", name: "Mail" }, { slug: "kanban", name: "Kanban Board" }, - { slug: "scheduling", name: "Scheduling" }, { slug: "file-manager", name: "File Manager" }, { slug: "notifications-center", name: "Notifications" }, { slug: "search-results", name: "Search" }, diff --git a/www/src/modules/create/preview/blocks/scheduling.tsx b/www/src/modules/create/preview/blocks/scheduling.tsx deleted file mode 100644 index d76e43a44..000000000 --- a/www/src/modules/create/preview/blocks/scheduling.tsx +++ /dev/null @@ -1,1312 +0,0 @@ -"use client" - -import React from "react" -import type { CalendarDate } from "@internationalized/date" -import { - getLocalTimeZone, - startOfWeek, - Time, - today, -} from "@internationalized/date" - -import { - BellIcon, - Building2Icon, - CalendarIcon, - CheckIcon, - ChevronLeftIcon, - ChevronRightIcon, - ClockIcon, - CopyIcon, - MonitorIcon, - MoreHorizontalIcon, - PencilIcon, - PinIcon, - PlusIcon, - SettingsIcon, - TimerIcon, - TrashIcon, - Users2Icon, -} from "@/registry/icons" -import { Responsive } from "@/registry/lib/responsive" -import { cn } from "@/registry/lib/utils" -import { - Avatar, - AvatarFallback, - AvatarGroup, - AvatarGroupCount, -} from "@/registry/ui/avatar" -import { Badge } from "@/registry/ui/badge" -import { Button } from "@/registry/ui/button" -import { - Calendar, - CalendarCell, - CalendarGrid, - CalendarGridBody, - CalendarGridHeader, - CalendarHeader, - CalendarHeaderCell, -} from "@/registry/ui/calendar" -import { - Card, - CardAction, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/registry/ui/card" -import { Checkbox, CheckboxControl } from "@/registry/ui/checkbox" -import { CheckboxGroup } from "@/registry/ui/checkbox-group" -import { DatePicker } from "@/registry/ui/date-picker" -import { - Dialog, - DialogBody, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/registry/ui/dialog" -import { Drawer } from "@/registry/ui/drawer" -import { - Empty, - EmptyDescription, - EmptyHeader, - EmptyTitle, -} from "@/registry/ui/empty" -import { Description, FieldGroup, Label } from "@/registry/ui/field" -import { - DateInput, - Input, - InputGroup, - InputGroupAddon, - TextArea, -} from "@/registry/ui/input" -import { Menu, MenuContent, MenuItem } from "@/registry/ui/menu" -import { Modal } from "@/registry/ui/modal" -import { Popover } from "@/registry/ui/popover" -import { SearchField } from "@/registry/ui/search-field" -import { - SegmentedControl, - SegmentedControlItem, -} from "@/registry/ui/segmented-control" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, -} from "@/registry/ui/select" -import { Separator } from "@/registry/ui/separator" -import { Switch, SwitchControl } from "@/registry/ui/switch" -import { TextField } from "@/registry/ui/text-field" -import { TimeField } from "@/registry/ui/time-field" -import { ToggleButton } from "@/registry/ui/toggle-button" -import { ToggleButtonGroup } from "@/registry/ui/toggle-button-group" -import { Tooltip, TooltipContent } from "@/registry/ui/tooltip" - -/* --------------------------------- Data ----------------------------------- */ - -type Kind = - | "standup" - | "review" - | "focus" - | "interview" - | "deadline" - | "personal" - -const KINDS: Record< - Kind, - { - label: string - variant: React.ComponentProps["variant"] - dot: string - } -> = { - standup: { label: "Standup", variant: "accent", dot: "bg-accent" }, - review: { label: "Review", variant: "info", dot: "bg-info" }, - focus: { label: "Focus", variant: "success", dot: "bg-success" }, - interview: { label: "Interview", variant: "warning", dot: "bg-warning" }, - deadline: { label: "Deadline", variant: "danger", dot: "bg-danger" }, - personal: { label: "Personal", variant: "neutral", dot: "bg-fg-muted" }, -} - -const TEAM = [ - { id: "nadia", name: "Nadia Okoye", initials: "NO", role: "Design" }, - { id: "marcus", name: "Marcus Field", initials: "MF", role: "Engineering" }, - { id: "priya", name: "Priya Raman", initials: "PR", role: "Product" }, - { id: "tom", name: "Tom Delaney", initials: "TD", role: "Sales" }, - { id: "elena", name: "Elena Vasquez", initials: "EV", role: "Research" }, - { id: "jonas", name: "Jonas Kerr", initials: "JK", role: "Engineering" }, -] - -type Seed = { - title: string - offset: number - start: number - duration: number - kind: Kind - location: string - people: string[] -} - -const SEEDS: Seed[] = [ - { - title: "Engineering standup", - offset: -3, - start: 540, - duration: 15, - kind: "standup", - location: "Zoom", - people: ["MF", "JK"], - }, - { - title: "Q3 roadmap review", - offset: -2, - start: 780, - duration: 60, - kind: "review", - location: "Room 4B", - people: ["PR", "NO", "TD"], - }, - { - title: "Interview — Staff designer", - offset: -1, - start: 600, - duration: 45, - kind: "interview", - location: "Room 2A", - people: ["NO", "PR"], - }, - { - title: "Daily standup", - offset: 0, - start: 540, - duration: 15, - kind: "standup", - location: "Zoom", - people: ["MF", "JK", "PR"], - }, - { - title: "Design review — booking flow", - offset: 0, - start: 630, - duration: 45, - kind: "review", - location: "Room 4B", - people: ["NO", "PR"], - }, - { - title: "Focus — pricing spec", - offset: 0, - start: 750, - duration: 90, - kind: "focus", - location: "Anywhere", - people: [], - }, - { - title: "1:1 with Priya Raman", - offset: 0, - start: 900, - duration: 30, - kind: "standup", - location: "Zoom", - people: ["PR"], - }, - { - title: "Sprint planning", - offset: 1, - start: 555, - duration: 45, - kind: "standup", - location: "Room 4B", - people: ["MF", "JK", "PR", "NO"], - }, - { - title: "Customer call — Halden Group", - offset: 1, - start: 840, - duration: 45, - kind: "review", - location: "Zoom", - people: ["TD", "PR"], - }, - { - title: "Interview — Backend engineer", - offset: 2, - start: 600, - duration: 60, - kind: "interview", - location: "Room 2A", - people: ["MF", "JK"], - }, - { - title: "Dentist", - offset: 2, - start: 810, - duration: 30, - kind: "personal", - location: "Offsite", - people: [], - }, - { - title: "Daily standup", - offset: 3, - start: 540, - duration: 15, - kind: "standup", - location: "Zoom", - people: ["MF", "JK", "PR"], - }, - { - title: "Focus — API cleanup", - offset: 3, - start: 720, - duration: 120, - kind: "focus", - location: "Anywhere", - people: [], - }, - { - title: "Design critique", - offset: 4, - start: 600, - duration: 45, - kind: "review", - location: "Room 4B", - people: ["NO", "EV"], - }, - { - title: "Team dinner", - offset: 4, - start: 1140, - duration: 120, - kind: "personal", - location: "Offsite", - people: ["NO", "MF", "PR", "TD"], - }, - { - title: "Monthly business review", - offset: 7, - start: 570, - duration: 90, - kind: "review", - location: "Room 1", - people: ["PR", "TD", "EV"], - }, - { - title: "Ship 4.2 to production", - offset: 9, - start: 600, - duration: 30, - kind: "deadline", - location: "Anywhere", - people: ["MF"], - }, - { - title: "Partner sync — Volta", - offset: 11, - start: 630, - duration: 45, - kind: "review", - location: "Zoom", - people: ["TD", "PR"], - }, - { - title: "Research offsite — Lisbon", - offset: 14, - start: 540, - duration: 480, - kind: "personal", - location: "Offsite", - people: ["EV", "NO"], - }, -] - -type CalEvent = Omit & { id: string; date: CalendarDate } - -const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] - -const AVAILABILITY_DAYS = [ - { id: "mon", label: "Mon" }, - { id: "tue", label: "Tue" }, - { id: "wed", label: "Wed" }, - { id: "thu", label: "Thu" }, - { id: "fri", label: "Fri" }, - { id: "sat", label: "Sat" }, - { id: "sun", label: "Sun" }, -] - -/* -------------------------------- Helpers --------------------------------- */ - -function formatTime(minutes: number) { - const h = Math.floor(minutes / 60) - const m = minutes % 60 - const suffix = h < 12 ? "AM" : "PM" - return `${h % 12 || 12}:${String(m).padStart(2, "0")} ${suffix}` -} - -function formatDuration(minutes: number) { - if (minutes < 60) return `${minutes}m` - const h = Math.floor(minutes / 60) - const m = minutes % 60 - return m === 0 ? `${h}h` : `${h}h ${m}m` -} - -function formatDay(date: CalendarDate, withYear = false) { - return date.toDate(getLocalTimeZone()).toLocaleDateString(undefined, { - weekday: "long", - month: "long", - day: "numeric", - ...(withYear ? { year: "numeric" } : {}), - }) -} - -function locationIcon(location: string) { - if (location.startsWith("Zoom")) return MonitorIcon - if (location.startsWith("Room")) return Building2Icon - return PinIcon -} - -function compareEvents(a: CalEvent, b: CalEvent) { - return a.date.compare(b.date) || a.start - b.start -} - -/* ------------------------------- Primitives ------------------------------- */ - -function People({ initials }: { initials: string[] }) { - if (initials.length === 0) return null - const visible = initials.slice(0, 3) - const overflow = initials.length - visible.length - return ( - - {visible.map((i) => ( - - {i} - - ))} - {overflow > 0 && +{overflow}} - - ) -} - -function EventRow({ - event, - onRemove, -}: { - event: CalEvent - onRemove: (id: string) => void -}) { - const kind = KINDS[event.kind] - const LocationIcon = locationIcon(event.location) - return ( -
  • - -
    -
    - - {event.title} - - - {kind.label} - -
    -
    - - - {formatTime(event.start)} · {formatDuration(event.duration)} - - - - {event.location} - -
    - -
    - - - - - - - Edit event - - - - Duplicate - - - onRemove(event.id)} - > - - Delete - - - - -
  • - ) -} - -function Stat({ - label, - value, - hint, - icon: Icon, -}: { - label: string - value: string - hint: string - icon: typeof CalendarIcon -}) { - return ( - - - - - -
    - {label} - - {value} - -
    - - {hint} - -
    -
    - ) -} - -/* --------------------------------- Views ---------------------------------- */ - -function MonthView({ - selected, - onSelect, - eventsOn, -}: { - selected: CalendarDate - onSelect: (date: CalendarDate) => void - eventsOn: (date: CalendarDate) => CalEvent[] -}) { - return ( - date && onSelect(date)} - className="w-full" - > - - - - {(day) => {day}} - - - {(date) => ( - - {({ formattedDate, isOutsideMonth }) => { - const dayEvents = eventsOn(date as CalendarDate) - const visible = dayEvents.slice(0, 2) - const overflow = dayEvents.length - visible.length - return ( - <> - - {formattedDate} - - {!isOutsideMonth && dayEvents.length > 0 && ( - <> -
    - {dayEvents.slice(0, 3).map((e) => ( - - ))} -
    -
    - {visible.map((e) => ( - - - {e.title} - - - ))} - {overflow > 0 && ( - - +{overflow} more - - )} -
    - - )} - - ) - }} -
    - )} -
    -
    -
    - ) -} - -function WeekView({ - selected, - onSelect, - now, - eventsOn, -}: { - selected: CalendarDate - onSelect: (date: CalendarDate) => void - now: CalendarDate - eventsOn: (date: CalendarDate) => CalEvent[] -}) { - const start = startOfWeek(selected, "en-US") - const days = Array.from({ length: 7 }, (_, i) => start.add({ days: i })) - const shortDate = (date: CalendarDate) => - date - .toDate(getLocalTimeZone()) - .toLocaleDateString(undefined, { month: "short", day: "numeric" }) - const rangeLabel = `${shortDate(start)} – ${shortDate(start.add({ days: 6 }))}` - - return ( -
    -
    - - {rangeLabel} - - -
    -
    -
    - {days.map((day, i) => { - const dayEvents = eventsOn(day) - const isToday = day.compare(now) === 0 - const isSelected = day.compare(selected) === 0 - return ( -
    - -
    - {dayEvents.map((e) => ( -
    - - - {formatTime(e.start)} - - - {e.title} - -
    - ))} -
    -
    - ) - })} -
    -
    -
    - ) -} - -function AgendaView({ - events, - now, - onRemove, -}: { - events: CalEvent[] - now: CalendarDate - onRemove: (id: string) => void -}) { - const upcoming = events - .filter((e) => e.date.compare(now) >= 0) - .sort(compareEvents) - .slice(0, 14) - - const groups: { date: CalendarDate; items: CalEvent[] }[] = [] - for (const event of upcoming) { - const last = groups.at(-1) - if (last && last.date.compare(event.date) === 0) last.items.push(event) - else groups.push({ date: event.date, items: [event] }) - } - - return ( -
    - {groups.map((group) => ( -
    -
    - - {group.date.compare(now) === 0 ? "Today" : formatDay(group.date)} - - - {group.items.length}{" "} - {group.items.length === 1 ? "event" : "events"} - -
    -
      - {group.items.map((event) => ( - - ))} -
    -
    - ))} -
    - ) -} - -/* ------------------------------- New event -------------------------------- */ - -function NewEventDialog({ - defaultDate, - onCreate, -}: { - defaultDate: CalendarDate - onCreate: (event: { - title: string - date: CalendarDate - start: number - kind: Kind - location: string - people: string[] - }) => void -}) { - const [isOpen, setOpen] = React.useState(false) - const [title, setTitle] = React.useState("Roadmap sync — Q4 planning") - const [date, setDate] = React.useState(defaultDate) - const [start, setStart] = React.useState