Interactive preview, sandboxed: nothing you type leaves this page.
import { useMemo, useState } from "react";type ShipmentStatus = "on time" | "delayed" | "held at customs" | "delivered";type Shipment = { id: string; vessel: string; eta: string; weight: string; status: ShipmentStatus; scan: string; port: string };const shipments: Shipment[] = [ { id: "KELU 482190-6", vessel: "Maersk Eland", eta: "18 Jun, 09:20", weight: "24.8 t", status: "on time", scan: "Gate-in confirmed · 17 Jun, 16:42", port: "Rotterdam, NL" }, { id: "KELU 391742-1", vessel: "MSC Algeciras", eta: "18 Jun, 13:00", weight: "18.2 t", status: "delayed", scan: "Berth departure · 17 Jun, 21:08", port: "Algeciras, ES" }, { id: "KELU 740156-3", vessel: "CMA CGM Andromeda", eta: "19 Jun, 06:45", weight: "26.1 t", status: "on time", scan: "Loaded to vessel · 17 Jun, 11:26", port: "Rotterdam, NL" }, { id: "KELU 528304-9", vessel: "Ever Given", eta: "19 Jun, 15:30", weight: "21.7 t", status: "held at customs", scan: "Customs inspection · 17 Jun, 08:15", port: "Singapore, SG" }, { id: "KELU 164980-2", vessel: "ONE Innovation", eta: "20 Jun, 02:10", weight: "19.4 t", status: "on time", scan: "Discharged · 17 Jun, 19:50", port: "Algeciras, ES" }, { id: "KELU 863215-7", vessel: "HMM Rotterdam", eta: "20 Jun, 11:55", weight: "27.3 t", status: "delivered", scan: "Container released · 17 Jun, 14:03", port: "Singapore, SG" }, { id: "KELU 275641-8", vessel: "MSC Oscar", eta: "21 Jun, 04:30", weight: "23.6 t", status: "on time", scan: "Vessel departed · 17 Jun, 22:31", port: "Rotterdam, NL" }, { id: "KELU 609372-4", vessel: "CMA CGM Jacques Saadé", eta: "21 Jun, 17:15", weight: "20.9 t", status: "delayed", scan: "Anchored · 17 Jun, 06:40", port: "Algeciras, ES" }, { id: "KELU 415806-0", vessel: "Ever Ace", eta: "22 Jun, 08:05", weight: "25.2 t", status: "on time", scan: "Transshipment complete · 17 Jun, 18:22", port: "Singapore, SG" }, { id: "KELU 937204-5", vessel: "Maersk Mc-Kinney Møller", eta: "22 Jun, 20:40", weight: "17.8 t", status: "held at customs", scan: "Documents under review · 17 Jun, 12:14", port: "Rotterdam, NL" }, { id: "KELU 351768-2", vessel: "MSC Gülsün", eta: "23 Jun, 07:25", weight: "22.4 t", status: "on time", scan: "Loaded to vessel · 17 Jun, 09:57", port: "Algeciras, ES" }, { id: "KELU 702493-6", vessel: "ONE Apus", eta: "24 Jun, 14:50", weight: "24.1 t", status: "delivered", scan: "Delivery confirmed · 17 Jun, 15:36", port: "Singapore, SG" },];const statusMark: Record<ShipmentStatus, string> = { "on time": "bg-emerald-600 dark:bg-emerald-400", delayed: "bg-amber-600 dark:bg-amber-400", "held at customs": "bg-rose-700 dark:bg-rose-400", delivered: "bg-sky-700 dark:bg-sky-400",};function etaValue(value: string) { const [day, month, time] = value.replace(",", "").split(" "); const months: Record<string, number> = { Jan: 0, Feb: 1, Mar: 2, Apr: 3, May: 4, Jun: 5, Jul: 6, Aug: 7, Sep: 8, Oct: 9, Nov: 10, Dec: 11 }; return new Date(2025, months[month] ?? 0, Number(day), Number(time.slice(0, 2)), Number(time.slice(3))).getTime();}export default function Block() { const [sortAscending, setSortAscending] = useState(true); const [expanded, setExpanded] = useState<Set<string>>(() => new Set()); const orderedShipments = useMemo( () => [...shipments].sort((a, b) => (sortAscending ? 1 : -1) * (etaValue(a.eta) - etaValue(b.eta))), [sortAscending], ); function toggleExpanded(id: string) { setExpanded((current) => { const next = new Set(current); if (next.has(id)) next.delete(id); else next.add(id); return next; }); } return ( <section className="w-full bg-white px-4 py-8 text-neutral-900 dark:bg-neutral-950 dark:text-neutral-100 sm:px-6 sm:py-10"> <div className="mx-auto w-full max-w-7xl"> <header className="mb-5 flex flex-wrap items-end justify-between gap-x-6 gap-y-3 border-b border-neutral-200 pb-4 dark:border-neutral-800 sm:mb-6 sm:pb-5"> <div> <p className="mb-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-neutral-600 dark:text-neutral-400">Kestrel Line <span className="px-1 text-neutral-400">/</span> Freight movements</p> <h1 className="text-[25px] font-semibold leading-tight tracking-[-0.035em] sm:text-[30px]">Shipments</h1> </div> <p className="pb-0.5 text-[13px] font-medium text-neutral-700 dark:text-neutral-300"><span className="font-mono tabular-nums">12</span> consignments <span className="mx-1.5 text-neutral-400" aria-hidden="true">·</span> Rotterdam / Algeciras / Singapore</p> </header> <table className="shipment-table w-full table-fixed border-collapse text-left"> <caption className="sr-only">Kestrel Line shipment schedule with container, vessel, estimated arrival, weight and status.</caption> <thead className="sticky top-0 z-10 bg-neutral-100 text-[11px] font-semibold uppercase tracking-[0.1em] text-neutral-700 dark:bg-neutral-900 dark:text-neutral-300"> <tr className="border-y border-neutral-200 dark:border-neutral-800"> <th scope="col" className="w-[22%] px-3 py-3 sm:px-4">Container</th> <th scope="col" className="w-[25%] px-3 py-3 sm:px-4">Vessel</th> <th scope="col" aria-sort={sortAscending ? "ascending" : "descending"} className="w-[20%] px-3 py-3 sm:px-4"> <button type="button" onClick={() => setSortAscending((value) => !value)} aria-label={`Sort ETA ${sortAscending ? "descending" : "ascending"}`} className="inline-flex min-h-6 items-center gap-1.5 rounded-sm text-left hover:text-neutral-950 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-700 dark:hover:text-white dark:focus-visible:outline-amber-300">ETA <span aria-hidden="true" className="text-[13px] leading-none">{sortAscending ? "↑" : "↓"}</span></button> </th> <th scope="col" className="w-[13%] px-3 py-3 sm:px-4">Weight</th> <th scope="col" className="w-[20%] px-3 py-3 sm:px-4">Status</th> </tr> </thead> <tbody> {orderedShipments.map((shipment) => { const isExpanded = expanded.has(shipment.id); const detailId = `scan-${shipment.id.replace(/[^a-zA-Z0-9]/g, "")}`; return <ShipmentRows key={shipment.id} shipment={shipment} isExpanded={isExpanded} detailId={detailId} onToggle={() => toggleExpanded(shipment.id)} />; })} </tbody> </table> </div> <style>{` .shipment-table th { position: sticky; top: 0; } @media (max-width: 639px) { .shipment-table thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; } .shipment-table, .shipment-table tbody { display: block; width: 100%; } .shipment-table tbody > tr.shipment-record { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; column-gap: 9px; row-gap: 6px; padding: 10px 0 11px; border-bottom: 1px solid rgb(229 229 229); } .dark .shipment-table tbody > tr.shipment-record { border-color: rgb(64 64 64); } .shipment-table tbody > tr.shipment-record > td { display: block; width: auto; padding: 0; } .shipment-table tbody > tr.shipment-record > td:first-child { grid-column: 1 / 3; grid-row: 1; } .shipment-table tbody > tr.shipment-record > td:nth-child(2) { grid-column: 1; grid-row: 2; } .shipment-table tbody > tr.shipment-record > td:nth-child(3) { grid-column: 2; grid-row: 2; } .shipment-table tbody > tr.shipment-record > td:nth-child(4) { grid-column: 3; grid-row: 2; } .shipment-table tbody > tr.shipment-record > td:nth-child(5) { grid-column: 3; grid-row: 1; } .shipment-table tbody > tr.shipment-detail { display: block; border-bottom: 1px solid rgb(229 229 229); } .dark .shipment-table tbody > tr.shipment-detail { border-color: rgb(64 64 64); } .shipment-table tbody > tr.shipment-detail > td { display: block; padding: 10px 0 12px; } } `}</style> </section> );}type ShipmentRowsProps = { shipment: Shipment; isExpanded: boolean; detailId: string; onToggle: () => void };function ShipmentRows({ shipment, isExpanded, detailId, onToggle }: ShipmentRowsProps) { return ( <> <tr className="shipment-record border-b border-neutral-200 text-[13px] dark:border-neutral-800"> <td className="px-3 py-3.5 sm:px-4 sm:py-4"> <button type="button" onClick={onToggle} aria-expanded={isExpanded} aria-controls={detailId} aria-label={`${isExpanded ? "Hide" : "Show"} last scan for ${shipment.id}`} className="group inline-flex min-h-6 max-w-full items-center gap-2 rounded-sm text-left focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-700 dark:focus-visible:outline-amber-300"> <span aria-hidden="true" className={`h-0 w-0 shrink-0 border-y-[4px] border-l-[5px] border-y-transparent border-l-neutral-500 transition-transform dark:border-l-neutral-400 ${isExpanded ? "rotate-90" : ""}`} /> <span className="font-mono text-[12px] font-semibold tabular-nums tracking-[-0.02em] sm:text-[13px]">{shipment.id}</span> </button> </td> <td className="truncate px-3 py-3.5 text-neutral-800 dark:text-neutral-200 sm:px-4 sm:py-4">{shipment.vessel}</td> <td className="whitespace-nowrap px-3 py-3.5 font-mono tabular-nums text-neutral-800 dark:text-neutral-200 sm:px-4 sm:py-4">{shipment.eta}</td> <td className="whitespace-nowrap px-3 py-3.5 font-mono tabular-nums text-neutral-800 dark:text-neutral-200 sm:px-4 sm:py-4">{shipment.weight}</td> <td className="px-3 py-3.5 sm:px-4 sm:py-4"><span className="inline-flex items-center gap-2 whitespace-nowrap text-[12px] font-medium text-neutral-800 dark:text-neutral-200"><span aria-hidden="true" className={`h-1.5 w-1.5 shrink-0 rounded-full ${statusMark[shipment.status]}`} />{shipment.status}</span></td> </tr> {isExpanded && <tr id={detailId} className="shipment-detail bg-neutral-50 text-[12px] text-neutral-700 dark:bg-neutral-900 dark:text-neutral-300"><td colSpan={5} className="px-4 py-3 pl-9"><div className="flex flex-wrap gap-x-8 gap-y-1"><p><span className="mr-2 font-semibold text-neutral-900 dark:text-neutral-100">Last scan</span>{shipment.scan}</p><p><span className="mr-2 font-semibold text-neutral-900 dark:text-neutral-100">Port</span>{shipment.port}</p></div></td></tr>} </> );}