"use client" /** * UpcomingMeetings — sidebar/widget listing the next N meetings with status, * date, time, and an optional join-link. * * Pure presentational. The consumer owns the data fetch + filtering — pass * the already-filtered, already-sorted meetings via the `meetings` prop. This * keeps the component decoupled from any specific API client or data shape * beyond the small interface declared below. * * lifecycle:calendar-ui (raise-simpli-azc) */ import * as React from "react" import { CalendarDays, Clock, ExternalLink, Plus } from "lucide-react" import { Card, CardContent, CardHeader, CardTitle } from "../ui/card" import { Button } from "../ui/button" import { cn } from "../../utils" export type UpcomingMeetingStatus = "pending" | "confirmed" | "cancelled" | "completed" export interface UpcomingMeeting { id: string | number title: string /** ISO 8601 timestamp string */ scheduled_at: string status: UpcomingMeetingStatus investor_name?: string investor_email?: string meeting_link?: string } export interface UpcomingMeetingsProps { meetings: T[] /** Whether the list is still loading. Renders skeletons. */ loading?: boolean /** Optional click handler when a meeting row is clicked (whole row). */ onMeetingClick?: (meeting: T) => void /** Optional URL for the "View all" header link. Hidden if absent. */ viewAllHref?: string /** Optional click handler for the "View all" header link. Used instead of href if provided. */ onViewAll?: () => void /** Optional click handler for the empty-state "Schedule one" CTA. Hidden if absent. */ onSchedule?: () => void /** Custom class name for the outer Card. */ className?: string /** Title shown in the card header. Defaults to "Upcoming Meetings". */ title?: string /** Number of skeleton rows to show while loading. Defaults to 3. */ loadingRowCount?: number } 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", }) } function formatTime(isoStr: string): string { return new Date(isoStr).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", }) } /** * @example * setShowSchedule(true)} * /> */ export function UpcomingMeetings({ meetings, loading = false, onMeetingClick, viewAllHref, onViewAll, onSchedule, className, title = "Upcoming Meetings", loadingRowCount = 3, }: UpcomingMeetingsProps) { return (
{title} {(viewAllHref || onViewAll) && ( )}
{loading ? (
{Array.from({ length: loadingRowCount }).map((_, i) => (
))}
) : meetings.length === 0 ? (

No upcoming meetings

{onSchedule && ( )}
) : (
{meetings.map(m => { const badge = STATUS_BADGE[m.status] const interactive = !!onMeetingClick return (
onMeetingClick(m) : undefined} onKeyDown={ interactive ? e => { if (e.key === "Enter" || e.key === " ") { e.preventDefault() onMeetingClick(m) } } : undefined } className={cn( "border rounded-lg p-2.5 space-y-1 transition-colors", interactive && "hover:bg-muted/40 cursor-pointer", )} >

{m.title}

{badge.label}
{(m.investor_name || m.investor_email) && (

{m.investor_name || m.investor_email}

)}
{formatDate(m.scheduled_at)} {formatTime(m.scheduled_at)}
{m.meeting_link && ( e.stopPropagation()} className="flex items-center gap-1 text-[11px] text-primary hover:underline" > Join )}
) })}
)} ) }