Add mobile card view to EntityTable with renderers for People and Locations
- EntityTable: add useMediaQuery hook, mobileCardRender prop, and mobile card path that replaces the table on screens <768px when a renderer is provided - PeoplePage: add mobileCardRender showing name, category, email, phone - LocationsPage: add mobileCardRender showing name, category, address Note: TodosPage and RemindersPage use custom list components (TodoList, ReminderList), not EntityTable directly — no changes needed there. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d0477b1c13
commit
09c35752c6
@ -356,6 +356,17 @@ export default function LocationsPage() {
|
||||
sortDir={sortDir}
|
||||
onSort={handleSort}
|
||||
visibilityMode={visibilityMode}
|
||||
mobileCardRender={(location) => (
|
||||
<div className={`rounded-lg border p-3 transition-colors ${selectedLocationId === location.id ? 'border-accent/40 bg-accent/5' : 'border-border bg-card hover:bg-card-elevated'}`}>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="font-medium text-sm truncate flex-1">{location.name}</span>
|
||||
{location.category && <span className="text-[10px] text-muted-foreground">{location.category}</span>}
|
||||
</div>
|
||||
{location.address && (
|
||||
<p className="text-xs text-muted-foreground truncate">{location.address}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -744,6 +744,18 @@ export default function PeoplePage() {
|
||||
sortDir={sortDir}
|
||||
onSort={handleSort}
|
||||
visibilityMode={visibilityMode}
|
||||
mobileCardRender={(person) => (
|
||||
<div className={`rounded-lg border p-3 transition-colors ${selectedPersonId === person.id ? 'border-accent/40 bg-accent/5' : 'border-border bg-card hover:bg-card-elevated'}`}>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="font-medium text-sm truncate flex-1">{person.name}</span>
|
||||
{person.category && <span className="text-[10px] text-muted-foreground">{person.category}</span>}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
{person.email && <span className="truncate">{person.email}</span>}
|
||||
{person.phone && <span>{person.phone}</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react';
|
||||
import type { VisibilityMode } from '@/hooks/useTableVisibility';
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
|
||||
export interface ColumnDef<T> {
|
||||
key: string;
|
||||
@ -28,6 +29,7 @@ interface EntityTableProps<T extends { id: number }> {
|
||||
onSort: (key: string) => void;
|
||||
visibilityMode: VisibilityMode;
|
||||
loading?: boolean;
|
||||
mobileCardRender?: (item: T) => React.ReactNode;
|
||||
}
|
||||
|
||||
const LEVEL_ORDER: VisibilityMode[] = ['essential', 'filtered', 'all'];
|
||||
@ -127,10 +129,51 @@ export function EntityTable<T extends { id: number }>({
|
||||
onSort,
|
||||
visibilityMode,
|
||||
loading = false,
|
||||
mobileCardRender,
|
||||
}: EntityTableProps<T>) {
|
||||
const visibleColumns = columns.filter((col) => isVisible(col.visibilityLevel, visibilityMode));
|
||||
const colCount = visibleColumns.length;
|
||||
const showPinnedSection = showPinned && pinnedRows.length > 0;
|
||||
const isMobile = useMediaQuery('(max-width: 767px)');
|
||||
|
||||
if (isMobile && mobileCardRender) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{loading ? (
|
||||
Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="animate-pulse rounded-lg bg-card border border-border p-4 h-20" />
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
{showPinnedSection && (
|
||||
<>
|
||||
<p className="text-[11px] uppercase tracking-wider text-muted-foreground font-medium pt-2">{pinnedLabel}</p>
|
||||
{pinnedRows.map((item) => (
|
||||
<div key={item.id} onClick={() => onRowClick(item.id)} className="cursor-pointer">
|
||||
{mobileCardRender(item)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{groups.map((group) => (
|
||||
<React.Fragment key={group.label}>
|
||||
{group.rows.length > 0 && (
|
||||
<>
|
||||
<p className="text-[11px] uppercase tracking-wider text-muted-foreground font-medium pt-2">{group.label}</p>
|
||||
{group.rows.map((item) => (
|
||||
<div key={item.id} onClick={() => onRowClick(item.id)} className="cursor-pointer">
|
||||
{mobileCardRender(item)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user