"use client" /** * MeetingsList — table-style meeting list with status badges, join links, * and an optional delete action. * * Distinct from UpcomingMeetings (sidebar card-stack); this is the * full-width table-row layout used by /calendar's "All Meetings" section * and /fundraises/[id]/outreach's MeetingsTab (Upcoming + Past lists). * * Pure presentational. The consumer owns sort, filter, fetch, and delete * confirmation. * * lifecycle:calendar-ui (raise-simpli-9cp) */ import * as React from "react" import { CalendarDays, ExternalLink, Plus, Trash2 } from "lucide-react" import { Button } from "../ui/button" import { cn } from "../../utils" export type MeetingsListStatus = "pending" | "confirmed" | "cancelled" | "completed" export interface MeetingsListItem { id: string | number title: string /** ISO 8601 timestamp string */ scheduled_at: string status: MeetingsListStatus duration_minutes?: number investor_name?: string investor_email?: string meeting_link?: string } export interface MeetingsListProps { meetings: T[] /** Whether the list is still loading. */ loading?: boolean /** Number of skeleton rows to show while loading. Defaults to 4. */ loadingRowCount?: number /** If set, each row gets a delete (trash) button that calls this with the id. */ onDelete?: (id: string | number) => void /** If set, the whole row becomes clickable. */ onRowClick?: (meeting: T) => void /** If set, the empty state shows a "Schedule First Meeting" CTA wired to this. */ onSchedule?: () => void /** Empty-state copy. Defaults to "No meetings scheduled yet." */ emptyMessage?: string /** className passed to the wrapping div. */ className?: string } const STATUS_BADGE: Record = { pending: { label: "Pending", className: "bg-yellow-100 text-yellow-800" }, confirmed: { label: "Confirmed", className: "bg-green-100 text-green-800" }, cancelled: { label: "Cancelled", className: "bg-red-100 text-red-800" }, completed: { label: "Completed", className: "bg-gray-100 text-gray-800" }, } function formatDate(isoStr: string): string { return new Date(isoStr).toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric", year: "numeric", }) } function formatTime(isoStr: string): string { return new Date(isoStr).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" }) } /** * @example * deleteMeeting(id)} * onSchedule={() => setShowSchedule(true)} * /> */ export function MeetingsList({ meetings, loading = false, loadingRowCount = 4, onDelete, onRowClick, onSchedule, emptyMessage = "No meetings scheduled yet.", className, }: MeetingsListProps) { if (loading) { return (
{Array.from({ length: loadingRowCount }).map((_, i) => (
))}
) } if (meetings.length === 0) { return (

{emptyMessage}

{onSchedule && ( )}
) } return (
{meetings.map(m => { const badge = STATUS_BADGE[m.status] const interactive = !!onRowClick return (
onRowClick(m) : undefined} onKeyDown={ interactive ? e => { if (e.key === "Enter" || e.key === " ") { e.preventDefault() onRowClick(m) } } : undefined } className={cn( "flex items-center justify-between px-4 py-3 transition-colors", interactive && "hover:bg-muted/50 cursor-pointer", !interactive && "hover:bg-muted/50", )} >

{m.title}

{(m.investor_name || m.investor_email) && (

{m.investor_name || m.investor_email}

)}

{formatDate(m.scheduled_at)}

{formatTime(m.scheduled_at)} {m.duration_minutes ? ` · ${m.duration_minutes}min` : ""}

{badge.label} {m.meeting_link && ( e.stopPropagation()} className="text-primary" title="Join meeting" > )} {onDelete && ( )}
) })}
) }