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

View File

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

View File

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