diff --git a/src/components/reservations/reservation-list.tsx b/src/components/reservations/reservation-list.tsx
new file mode 100644
index 0000000..413dd7f
--- /dev/null
+++ b/src/components/reservations/reservation-list.tsx
@@ -0,0 +1,215 @@
+'use client';
+
+import { useParams } from 'next/navigation';
+import Link from 'next/link';
+import { useQuery } from '@tanstack/react-query';
+
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table';
+import { EmptyState } from '@/components/shared/empty-state';
+import { apiFetch } from '@/lib/api/client';
+
+export interface ReservationRow {
+ id: string;
+ berthId: string;
+ portId: string;
+ clientId: string;
+ yachtId: string;
+ status: 'pending' | 'active' | 'ended' | 'cancelled';
+ startDate: string;
+ endDate: string | null;
+ tenureType: string;
+ contractFileId: string | null;
+ notes: string | null;
+ createdAt: string;
+}
+
+export interface ReservationListProps {
+ reservations: ReservationRow[];
+ showBerth?: boolean;
+ portSlug?: string;
+ emptyMessage?: string;
+}
+
+/**
+ * Renders a client's name as a link by fetching the client record.
+ * Uses TanStack Query cache for memoization of repeated clientId queries.
+ */
+function ClientLink({ clientId, portSlug }: { clientId: string; portSlug: string }) {
+ const { data } = useQuery<{ fullName: string }>({
+ queryKey: ['clients', clientId, 'name-only'],
+ queryFn: () =>
+ apiFetch<{ data: { fullName: string } }>(`/api/v1/clients/${clientId}`).then((r) => r.data),
+ });
+
+ return (
+
+ {data?.fullName ?? `Client ${clientId.slice(0, 8)}`}
+
+ );
+}
+
+/**
+ * Renders a yacht's name as a link by fetching the yacht record.
+ */
+function YachtLink({ yachtId, portSlug }: { yachtId: string; portSlug: string }) {
+ const { data } = useQuery<{ name: string }>({
+ queryKey: ['yachts', yachtId, 'name-only'],
+ queryFn: () =>
+ apiFetch<{ data: { name: string } }>(`/api/v1/yachts/${yachtId}`).then((r) => r.data),
+ });
+
+ return (
+
+ {data?.name ?? `Yacht ${yachtId.slice(0, 8)}`}
+
+ );
+}
+
+/**
+ * Renders a berth's mooring number as a link by fetching the berth record.
+ */
+function BerthLink({ berthId, portSlug }: { berthId: string; portSlug: string }) {
+ const { data } = useQuery<{ mooringNumber: string }>({
+ queryKey: ['berths', berthId, 'name-only'],
+ queryFn: () =>
+ apiFetch<{ data: { mooringNumber: string } }>(`/api/v1/berths/${berthId}`).then(
+ (r) => r.data,
+ ),
+ });
+
+ return (
+
+ {data?.mooringNumber ?? `Berth ${berthId.slice(0, 8)}`}
+
+ );
+}
+
+/**
+ * Renders a status badge with appropriate color coding.
+ */
+function StatusBadge({ status }: { status: ReservationRow['status'] }) {
+ const colorMap: Record = {
+ pending: 'bg-gray-100 text-gray-800',
+ active: 'bg-green-100 text-green-800',
+ ended: 'bg-blue-100 text-blue-800',
+ cancelled: 'bg-red-100 text-red-800',
+ };
+
+ const color = colorMap[status];
+ const label = status.charAt(0).toUpperCase() + status.slice(1);
+
+ return (
+
+ {label}
+
+ );
+}
+
+/**
+ * Pretty-prints tenure type for display.
+ */
+function prettyTenure(tenureType: string): string {
+ const tenureMap: Record = {
+ permanent: 'Permanent',
+ fixed_term: 'Fixed term',
+ seasonal: 'Seasonal',
+ };
+ return tenureMap[tenureType] ?? tenureType;
+}
+
+/**
+ * Formats a date range as "{startDate} → {endDate or 'ongoing'}".
+ */
+function formatDateRange(startDate: string, endDate: string | null): string {
+ const start = new Date(startDate).toLocaleDateString();
+ const end = endDate ? new Date(endDate).toLocaleDateString() : 'ongoing';
+ return `${start} → ${end}`;
+}
+
+export function ReservationList({
+ reservations,
+ showBerth = false,
+ portSlug: portSlugProp,
+ emptyMessage,
+}: ReservationListProps) {
+ const routeParams = useParams<{ portSlug: string }>();
+ const portSlug = portSlugProp ?? routeParams?.portSlug ?? '';
+
+ if (reservations.length === 0) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+ {showBerth && Berth}
+ Client
+ Yacht
+ Dates
+ Tenure
+ Status
+ Contract
+
+
+
+ {reservations.map((r) => (
+
+ {showBerth && (
+
+
+
+ )}
+
+
+
+
+
+
+ {formatDateRange(r.startDate, r.endDate)}
+ {prettyTenure(r.tenureType)}
+
+
+
+
+ {r.contractFileId ? (
+ // TODO: Confirm final file-download endpoint URL when available
+
+ View contract
+
+ ) : (
+ '—'
+ )}
+
+
+ ))}
+
+
+
+ );
+}