fix(residential): mobile card list alongside the desktop table
Both the residential-clients and residential-interests pages rendered
plain HTML <table>s with 5–6 columns directly. At 390px viewport the
header columns clipped at the right edge — "Sour..." for the clients
page, no header for the interests page either.
Adds a parallel mobile card list:
- <table> stays inside `hidden lg:block` (unchanged at lg+)
- new card list inside `lg:hidden` mirrors the row data:
- Clients: name + status pill on top, then email · phone ·
residence · source as a wrap-friendly meta row.
- Interests: stage label as headline, updated-at on the right,
preferences (line-clamp-2) and notes (line-clamp-1) below,
source small at the bottom.
- Each card is a Link to the detail page (matching the row click
target on desktop).
- Empty + loading states render as a centered card on mobile.
This is the same `hidden lg:block` / `lg:hidden` pattern used for the
main /clients and /interests pages. Doesn't refactor to the full
DataView primitive (would mean rebuilding the residential data layer
on TanStack Table) — keeps the change tightly scoped to the visible
output.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ import { SubdivisionCombobox } from '@/components/shared/subdivision-combobox';
|
||||
import { PhoneInput, type PhoneInputValue } from '@/components/shared/phone-input';
|
||||
import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation';
|
||||
import { apiFetch } from '@/lib/api/client';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { CountryCode } from '@/lib/i18n/countries';
|
||||
|
||||
interface ResidentialClientRow {
|
||||
@@ -85,7 +86,9 @@ export function ResidentialClientsList() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border bg-card overflow-hidden">
|
||||
{/* Desktop: table layout. Hidden below lg because the 6 columns clip
|
||||
off the viewport at phone widths. */}
|
||||
<div className="hidden lg:block rounded-lg border bg-card overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-muted/40 text-xs text-muted-foreground">
|
||||
<tr>
|
||||
@@ -137,6 +140,51 @@ export function ResidentialClientsList() {
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Mobile: card list. Each card mirrors the table row data with
|
||||
name + status pill on top, then meta line(s) below. */}
|
||||
<div className="lg:hidden space-y-2">
|
||||
{isLoading && (
|
||||
<div className="rounded-lg border bg-card px-3 py-8 text-center text-sm text-muted-foreground">
|
||||
Loading…
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && data?.data.length === 0 && (
|
||||
<div className="rounded-lg border bg-card px-3 py-8 text-center text-sm text-muted-foreground">
|
||||
No residential clients yet.
|
||||
</div>
|
||||
)}
|
||||
{data?.data.map((c) => (
|
||||
<Link
|
||||
key={c.id}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
href={`/${portSlug}/residential/clients/${c.id}` as any}
|
||||
className="block rounded-lg border bg-card p-3 transition-colors hover:bg-muted/30"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className="font-medium text-sm truncate">{c.fullName}</p>
|
||||
<span
|
||||
className={cn(
|
||||
'shrink-0 inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide',
|
||||
c.status === 'active'
|
||||
? 'bg-emerald-100 text-emerald-800'
|
||||
: c.status === 'inactive'
|
||||
? 'bg-muted text-muted-foreground'
|
||||
: 'bg-blue-100 text-blue-800',
|
||||
)}
|
||||
>
|
||||
{STATUS_LABELS[c.status] ?? c.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-muted-foreground">
|
||||
{c.email ? <span className="truncate">{c.email}</span> : null}
|
||||
{c.phone ? <span>{c.phone}</span> : null}
|
||||
{c.placeOfResidence ? <span>{c.placeOfResidence}</span> : null}
|
||||
{c.source ? <span className="capitalize">· {c.source}</span> : null}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<NewResidentialClientSheet open={createOpen} onOpenChange={setCreateOpen} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -94,7 +94,8 @@ export function ResidentialInterestsList() {
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border bg-card overflow-hidden">
|
||||
{/* Desktop: table layout. Hidden below lg; mobile renders cards. */}
|
||||
<div className="hidden lg:block rounded-lg border bg-card overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-muted/40 text-xs text-muted-foreground">
|
||||
<tr>
|
||||
@@ -149,6 +150,47 @@ export function ResidentialInterestsList() {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Mobile: card list. Stage as the headline (it's the most actionable
|
||||
field for triage), preferences/notes truncated below. */}
|
||||
<div className="lg:hidden space-y-2">
|
||||
{isLoading && (
|
||||
<div className="rounded-lg border bg-card px-3 py-8 text-center text-sm text-muted-foreground">
|
||||
Loading…
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && data?.data.length === 0 && (
|
||||
<div className="rounded-lg border bg-card px-3 py-8 text-center text-sm text-muted-foreground">
|
||||
No interests match.
|
||||
</div>
|
||||
)}
|
||||
{data?.data.map((i) => (
|
||||
<Link
|
||||
key={i.id}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
href={`/${portSlug}/residential/interests/${i.id}` as any}
|
||||
className="block rounded-lg border bg-card p-3 transition-colors hover:bg-muted/30"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className="font-medium text-sm">
|
||||
{STAGE_LABELS[i.pipelineStage] ?? i.pipelineStage}
|
||||
</p>
|
||||
<span className="shrink-0 text-[11px] text-muted-foreground">
|
||||
{new Date(i.updatedAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
{i.preferences ? (
|
||||
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">{i.preferences}</p>
|
||||
) : null}
|
||||
{i.notes ? (
|
||||
<p className="mt-1 line-clamp-1 text-xs text-muted-foreground/80">{i.notes}</p>
|
||||
) : null}
|
||||
{i.source ? (
|
||||
<p className="mt-1 text-[11px] capitalize text-muted-foreground">{i.source}</p>
|
||||
) : null}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user