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}
|
sortDir={sortDir}
|
||||||
onSort={handleSort}
|
onSort={handleSort}
|
||||||
visibilityMode={visibilityMode}
|
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>
|
</div>
|
||||||
|
|||||||
@ -744,6 +744,18 @@ export default function PeoplePage() {
|
|||||||
sortDir={sortDir}
|
sortDir={sortDir}
|
||||||
onSort={handleSort}
|
onSort={handleSort}
|
||||||
visibilityMode={visibilityMode}
|
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>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react';
|
import { ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react';
|
||||||
import type { VisibilityMode } from '@/hooks/useTableVisibility';
|
import type { VisibilityMode } from '@/hooks/useTableVisibility';
|
||||||
|
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||||
|
|
||||||
export interface ColumnDef<T> {
|
export interface ColumnDef<T> {
|
||||||
key: string;
|
key: string;
|
||||||
@ -28,6 +29,7 @@ interface EntityTableProps<T extends { id: number }> {
|
|||||||
onSort: (key: string) => void;
|
onSort: (key: string) => void;
|
||||||
visibilityMode: VisibilityMode;
|
visibilityMode: VisibilityMode;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
mobileCardRender?: (item: T) => React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LEVEL_ORDER: VisibilityMode[] = ['essential', 'filtered', 'all'];
|
const LEVEL_ORDER: VisibilityMode[] = ['essential', 'filtered', 'all'];
|
||||||
@ -127,10 +129,51 @@ export function EntityTable<T extends { id: number }>({
|
|||||||
onSort,
|
onSort,
|
||||||
visibilityMode,
|
visibilityMode,
|
||||||
loading = false,
|
loading = false,
|
||||||
|
mobileCardRender,
|
||||||
}: EntityTableProps<T>) {
|
}: EntityTableProps<T>) {
|
||||||
const visibleColumns = columns.filter((col) => isVisible(col.visibilityLevel, visibilityMode));
|
const visibleColumns = columns.filter((col) => isVisible(col.visibilityLevel, visibilityMode));
|
||||||
const colCount = visibleColumns.length;
|
const colCount = visibleColumns.length;
|
||||||
const showPinnedSection = showPinned && pinnedRows.length > 0;
|
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 (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user