Update EntityDetailPanel to use 2-column grid layout

Add fullWidth field option to PanelField interface. Short fields
render in a grid grid-cols-2 layout; fullWidth fields (address, notes)
render below at full width. Add icons to People and Locations fields
for consistency with other detail panels.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-02-25 23:59:30 +08:00
parent 87a7a4ae32
commit 6fc134d113
3 changed files with 56 additions and 30 deletions

View File

@ -1,5 +1,5 @@
import { useState, useMemo, useRef, useEffect } from 'react';
import { Plus, MapPin, Phone, Mail } from 'lucide-react';
import { Plus, MapPin, Phone, Mail, Tag, AlignLeft } from 'lucide-react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import api, { getErrorMessage } from '@/lib/api';
@ -240,11 +240,11 @@ export default function LocationsPage() {
];
const panelFields: PanelField[] = [
{ label: 'Address', key: 'address', copyable: true, icon: MapPin },
{ label: 'Contact Number', key: 'contact_number', copyable: true, icon: Phone },
{ label: 'Email', key: 'email', copyable: true, icon: Mail },
{ label: 'Category', key: 'category' },
{ label: 'Notes', key: 'notes', multiline: true },
{ label: 'Category', key: 'category', icon: Tag },
{ label: 'Address', key: 'address', copyable: true, icon: MapPin, fullWidth: true },
{ label: 'Notes', key: 'notes', multiline: true, icon: AlignLeft, fullWidth: true },
];
const renderPanel = () => (

View File

@ -1,5 +1,5 @@
import { useState, useMemo, useRef, useEffect } from 'react';
import { Plus, Users, Star, Cake, Phone, Mail, MapPin } from 'lucide-react';
import { Plus, Users, Star, Cake, Phone, Mail, MapPin, Tag, Building2, Briefcase, AlignLeft } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { format, parseISO, differenceInYears } from 'date-fns';
@ -173,12 +173,12 @@ const panelFields: PanelField[] = [
{ label: 'Mobile', key: 'mobile', copyable: true, icon: Phone },
{ label: 'Phone', key: 'phone', copyable: true, icon: Phone },
{ label: 'Email', key: 'email', copyable: true, icon: Mail },
{ label: 'Address', key: 'address', copyable: true, icon: MapPin },
{ label: 'Birthday', key: 'birthday_display' },
{ label: 'Category', key: 'category' },
{ label: 'Company', key: 'company' },
{ label: 'Job Title', key: 'job_title' },
{ label: 'Notes', key: 'notes', multiline: true },
{ label: 'Birthday', key: 'birthday_display', icon: Cake },
{ label: 'Category', key: 'category', icon: Tag },
{ label: 'Company', key: 'company', icon: Building2 },
{ label: 'Job Title', key: 'job_title', icon: Briefcase },
{ label: 'Address', key: 'address', copyable: true, icon: MapPin, fullWidth: true },
{ label: 'Notes', key: 'notes', multiline: true, icon: AlignLeft, fullWidth: true },
];
// ---------------------------------------------------------------------------

View File

@ -11,6 +11,7 @@ export interface PanelField {
copyable?: boolean;
icon?: LucideIcon;
multiline?: boolean;
fullWidth?: boolean;
}
interface EntityDetailPanelProps<T> {
@ -81,29 +82,54 @@ export function EntityDetailPanel<T>({
{/* Body */}
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-3">
{fields.map((field) => {
const value = getValue(item, field.key);
if (!value) return null;
{(() => {
const gridFields = fields.filter((f) => !f.fullWidth && getValue(item, f.key));
const fullWidthFields = fields.filter((f) => f.fullWidth && getValue(item, f.key));
return (
<div key={field.key}>
{field.copyable ? (
<div className="space-y-0.5">
<p className="text-[11px] uppercase tracking-wider text-muted-foreground">
{field.label}
</p>
<CopyableField value={value} icon={field.icon} label={field.label} />
</div>
) : (
<div className="space-y-0.5">
<p className="text-[11px] uppercase tracking-wider text-muted-foreground">
{field.label}
</p>
<p className={`text-sm ${field.multiline ? 'whitespace-pre-wrap' : ''}`}>{value}</p>
<>
{gridFields.length > 0 && (
<div className="grid grid-cols-2 gap-3">
{gridFields.map((field) => {
const value = getValue(item, field.key)!;
return (
<div key={field.key} className="space-y-1">
<div className="flex items-center gap-1.5 text-[11px] text-muted-foreground uppercase tracking-wider">
{field.icon && <field.icon className="h-3 w-3" />}
{field.label}
</div>
{field.copyable ? (
<CopyableField value={value} icon={field.icon} label={field.label} />
) : (
<p className="text-sm">{value}</p>
)}
</div>
);
})}
</div>
)}
</div>
{fullWidthFields.map((field) => {
const value = getValue(item, field.key)!;
return (
<div key={field.key} className="space-y-1">
<div className="flex items-center gap-1.5 text-[11px] text-muted-foreground uppercase tracking-wider">
{field.icon && <field.icon className="h-3 w-3" />}
{field.label}
</div>
{field.copyable ? (
<CopyableField value={value} icon={field.icon} label={field.label} />
) : (
<p className={`text-sm ${field.multiline ? 'whitespace-pre-wrap text-muted-foreground leading-relaxed' : ''}`}>
{value}
</p>
)}
</div>
);
})}
</>
);
})}
})()}
</div>
{/* Footer */}