mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 13:51:12 -05:00
feat: Implement comprehensive change display
This commit is contained in:
@@ -75,35 +75,45 @@ export function ImageDiff({ change, compact = false }: ImageDiffProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine scenario
|
||||||
|
const isAddition = !oldUrl && newUrl;
|
||||||
|
const isRemoval = oldUrl && !newUrl;
|
||||||
|
const isReplacement = oldUrl && newUrl;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 p-2 rounded-md bg-muted/50">
|
<div className="flex flex-col gap-2 p-3 rounded-md bg-muted/50">
|
||||||
<div className="text-sm font-medium">
|
<div className="text-sm font-medium">
|
||||||
{type === 'banner' ? 'Banner' : 'Card'} Image
|
{type === 'banner' ? 'Banner' : 'Card'} Image
|
||||||
|
{isAddition && <span className="text-green-600 dark:text-green-400 ml-2">(New)</span>}
|
||||||
|
{isRemoval && <span className="text-red-600 dark:text-red-400 ml-2">(Removed)</span>}
|
||||||
|
{isReplacement && <span className="text-amber-600 dark:text-amber-400 ml-2">(Changed)</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-3">
|
||||||
{oldUrl && (
|
{oldUrl && (
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="text-xs text-muted-foreground mb-1">Before</div>
|
<div className="text-xs text-muted-foreground mb-1">Before</div>
|
||||||
<img
|
<img
|
||||||
src={oldUrl}
|
src={oldUrl}
|
||||||
alt="Previous"
|
alt="Previous"
|
||||||
className="w-full h-20 object-cover rounded border-2 border-red-500/50"
|
className="w-full h-32 object-cover rounded border-2 border-red-500/50"
|
||||||
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{oldUrl && newUrl && (
|
{oldUrl && newUrl && (
|
||||||
<ArrowRight className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
<ArrowRight className="h-5 w-5 text-muted-foreground flex-shrink-0" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{newUrl && (
|
{newUrl && (
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="text-xs text-muted-foreground mb-1">After</div>
|
<div className="text-xs text-muted-foreground mb-1">{isAddition ? 'New Image' : 'After'}</div>
|
||||||
<img
|
<img
|
||||||
src={newUrl}
|
src={newUrl}
|
||||||
alt="New"
|
alt="New"
|
||||||
className="w-full h-20 object-cover rounded border-2 border-green-500/50"
|
className="w-full h-32 object-cover rounded border-2 border-green-500/50"
|
||||||
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { SubmissionReviewManager } from './SubmissionReviewManager';
|
|||||||
import { useRealtimeSubmissions } from '@/hooks/useRealtimeSubmissions';
|
import { useRealtimeSubmissions } from '@/hooks/useRealtimeSubmissions';
|
||||||
import { useIsMobile } from '@/hooks/use-mobile';
|
import { useIsMobile } from '@/hooks/use-mobile';
|
||||||
import { SubmissionChangesDisplay } from './SubmissionChangesDisplay';
|
import { SubmissionChangesDisplay } from './SubmissionChangesDisplay';
|
||||||
|
import { SubmissionItemsList } from './SubmissionItemsList';
|
||||||
import { RealtimeConnectionStatus } from './RealtimeConnectionStatus';
|
import { RealtimeConnectionStatus } from './RealtimeConnectionStatus';
|
||||||
import { MeasurementDisplay } from '@/components/ui/measurement-display';
|
import { MeasurementDisplay } from '@/components/ui/measurement-display';
|
||||||
|
|
||||||
@@ -1436,9 +1437,11 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
|||||||
item.submission_type === 'property_owner' ||
|
item.submission_type === 'property_owner' ||
|
||||||
item.submission_type === 'park' ||
|
item.submission_type === 'park' ||
|
||||||
item.submission_type === 'ride') ? (
|
item.submission_type === 'ride') ? (
|
||||||
<div className="text-sm text-muted-foreground">
|
<SubmissionItemsList
|
||||||
Standard entity submission - open review manager to see details
|
submissionId={item.id}
|
||||||
</div>
|
view="summary"
|
||||||
|
showImages={true}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-muted-foreground mb-2">
|
<div className="text-sm text-muted-foreground mb-2">
|
||||||
|
|||||||
159
src/components/moderation/PhotoComparison.tsx
Normal file
159
src/components/moderation/PhotoComparison.tsx
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { ImageIcon, Trash2, Edit } from 'lucide-react';
|
||||||
|
|
||||||
|
interface PhotoAdditionPreviewProps {
|
||||||
|
photos: Array<{
|
||||||
|
url: string;
|
||||||
|
title?: string;
|
||||||
|
caption?: string;
|
||||||
|
}>;
|
||||||
|
compact?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PhotoAdditionPreview({ photos, compact = false }: PhotoAdditionPreviewProps) {
|
||||||
|
if (compact) {
|
||||||
|
return (
|
||||||
|
<Badge variant="outline" className="text-green-600 dark:text-green-400">
|
||||||
|
<ImageIcon className="h-3 w-3 mr-1" />
|
||||||
|
+{photos.length} Photo{photos.length > 1 ? 's' : ''}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2 p-3 rounded-md bg-muted/50">
|
||||||
|
<div className="text-sm font-medium text-green-600 dark:text-green-400">
|
||||||
|
<ImageIcon className="h-4 w-4 inline mr-1" />
|
||||||
|
Adding {photos.length} Photo{photos.length > 1 ? 's' : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||||
|
{photos.slice(0, 6).map((photo, idx) => (
|
||||||
|
<div key={idx} className="flex flex-col gap-1">
|
||||||
|
<img
|
||||||
|
src={photo.url}
|
||||||
|
alt={photo.title || photo.caption || `Photo ${idx + 1}`}
|
||||||
|
className="w-full h-24 object-cover rounded border-2 border-green-500/50"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
{(photo.title || photo.caption) && (
|
||||||
|
<div className="text-xs text-muted-foreground truncate">
|
||||||
|
{photo.title || photo.caption}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{photos.length > 6 && (
|
||||||
|
<div className="flex items-center justify-center h-24 bg-muted rounded border-2 border-dashed">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
+{photos.length - 6} more
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PhotoEditPreviewProps {
|
||||||
|
photo: {
|
||||||
|
url: string;
|
||||||
|
oldCaption?: string;
|
||||||
|
newCaption?: string;
|
||||||
|
oldTitle?: string;
|
||||||
|
newTitle?: string;
|
||||||
|
};
|
||||||
|
compact?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PhotoEditPreview({ photo, compact = false }: PhotoEditPreviewProps) {
|
||||||
|
if (compact) {
|
||||||
|
return (
|
||||||
|
<Badge variant="outline" className="text-amber-600 dark:text-amber-400">
|
||||||
|
<Edit className="h-3 w-3 mr-1" />
|
||||||
|
Photo Edit
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2 p-3 rounded-md bg-muted/50">
|
||||||
|
<div className="text-sm font-medium text-amber-600 dark:text-amber-400">
|
||||||
|
<Edit className="h-4 w-4 inline mr-1" />
|
||||||
|
Photo Metadata Edit
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<img
|
||||||
|
src={photo.url}
|
||||||
|
alt="Photo being edited"
|
||||||
|
className="w-32 h-32 object-cover rounded"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1 flex flex-col gap-2 text-sm">
|
||||||
|
{photo.oldTitle !== photo.newTitle && (
|
||||||
|
<div>
|
||||||
|
<div className="font-medium mb-1">Title:</div>
|
||||||
|
<div className="text-red-600 dark:text-red-400 line-through">{photo.oldTitle || 'None'}</div>
|
||||||
|
<div className="text-green-600 dark:text-green-400">{photo.newTitle || 'None'}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{photo.oldCaption !== photo.newCaption && (
|
||||||
|
<div>
|
||||||
|
<div className="font-medium mb-1">Caption:</div>
|
||||||
|
<div className="text-red-600 dark:text-red-400 line-through">{photo.oldCaption || 'None'}</div>
|
||||||
|
<div className="text-green-600 dark:text-green-400">{photo.newCaption || 'None'}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PhotoDeletionPreviewProps {
|
||||||
|
photo: {
|
||||||
|
url: string;
|
||||||
|
title?: string;
|
||||||
|
caption?: string;
|
||||||
|
};
|
||||||
|
compact?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PhotoDeletionPreview({ photo, compact = false }: PhotoDeletionPreviewProps) {
|
||||||
|
if (compact) {
|
||||||
|
return (
|
||||||
|
<Badge variant="outline" className="text-red-600 dark:text-red-400">
|
||||||
|
<Trash2 className="h-3 w-3 mr-1" />
|
||||||
|
Delete Photo
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2 p-3 rounded-md bg-destructive/10">
|
||||||
|
<div className="text-sm font-medium text-red-600 dark:text-red-400">
|
||||||
|
<Trash2 className="h-4 w-4 inline mr-1" />
|
||||||
|
Deleting Photo
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<img
|
||||||
|
src={photo.url}
|
||||||
|
alt={photo.title || photo.caption || 'Photo to be deleted'}
|
||||||
|
className="w-32 h-32 object-cover rounded opacity-75"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{(photo.title || photo.caption) && (
|
||||||
|
<div className="flex-1 text-sm">
|
||||||
|
{photo.title && <div className="font-medium">{photo.title}</div>}
|
||||||
|
{photo.caption && <div className="text-muted-foreground">{photo.caption}</div>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import { FieldDiff, ImageDiff, LocationDiff } from './FieldComparison';
|
|||||||
import { detectChanges } from '@/lib/submissionChangeDetection';
|
import { detectChanges } from '@/lib/submissionChangeDetection';
|
||||||
import type { SubmissionItemData } from '@/types/submissions';
|
import type { SubmissionItemData } from '@/types/submissions';
|
||||||
import type { SubmissionItemWithDeps } from '@/lib/submissionItemsService';
|
import type { SubmissionItemWithDeps } from '@/lib/submissionItemsService';
|
||||||
import { Building2, Train, MapPin, Building, User, ImageIcon, Trash2, Edit, Plus } from 'lucide-react';
|
import { Building2, Train, MapPin, Building, User, ImageIcon, Trash2, Edit, Plus, AlertTriangle } from 'lucide-react';
|
||||||
|
|
||||||
interface SubmissionChangesDisplayProps {
|
interface SubmissionChangesDisplayProps {
|
||||||
item: SubmissionItemData | SubmissionItemWithDeps;
|
item: SubmissionItemData | SubmissionItemWithDeps;
|
||||||
@@ -11,6 +11,16 @@ interface SubmissionChangesDisplayProps {
|
|||||||
showImages?: boolean;
|
showImages?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to determine change magnitude
|
||||||
|
function getChangeMagnitude(totalChanges: number, hasImages: boolean, action: string) {
|
||||||
|
if (action === 'delete') return { label: 'Deletion', variant: 'destructive' as const, icon: AlertTriangle };
|
||||||
|
if (action === 'create') return { label: 'New', variant: 'default' as const, icon: Plus };
|
||||||
|
if (hasImages) return { label: 'Major', variant: 'default' as const, icon: Edit };
|
||||||
|
if (totalChanges >= 5) return { label: 'Major', variant: 'default' as const, icon: Edit };
|
||||||
|
if (totalChanges >= 3) return { label: 'Moderate', variant: 'secondary' as const, icon: Edit };
|
||||||
|
return { label: 'Minor', variant: 'outline' as const, icon: Edit };
|
||||||
|
}
|
||||||
|
|
||||||
export function SubmissionChangesDisplay({
|
export function SubmissionChangesDisplay({
|
||||||
item,
|
item,
|
||||||
view = 'summary',
|
view = 'summary',
|
||||||
@@ -45,13 +55,24 @@ export function SubmissionChangesDisplay({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const magnitude = getChangeMagnitude(
|
||||||
|
changes.totalChanges,
|
||||||
|
changes.imageChanges.length > 0,
|
||||||
|
changes.action
|
||||||
|
);
|
||||||
|
|
||||||
if (view === 'summary') {
|
if (view === 'summary') {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
{getEntityIcon()}
|
{getEntityIcon()}
|
||||||
<span className="font-medium">{changes.entityName}</span>
|
<span className="font-medium">{changes.entityName}</span>
|
||||||
{getActionBadge()}
|
{getActionBadge()}
|
||||||
|
{changes.action === 'edit' && (
|
||||||
|
<Badge variant={magnitude.variant} className="text-xs">
|
||||||
|
{magnitude.label} Change
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{changes.action === 'edit' && changes.totalChanges > 0 && (
|
{changes.action === 'edit' && changes.totalChanges > 0 && (
|
||||||
@@ -75,9 +96,9 @@ export function SubmissionChangesDisplay({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{changes.action === 'create' && (
|
{changes.action === 'create' && item.item_data?.description && (
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground line-clamp-2">
|
||||||
New {item.item_type}
|
{item.item_data.description}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
112
src/components/moderation/SubmissionItemsList.tsx
Normal file
112
src/components/moderation/SubmissionItemsList.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
|
import { SubmissionChangesDisplay } from './SubmissionChangesDisplay';
|
||||||
|
import { PhotoSubmissionDisplay } from './PhotoSubmissionDisplay';
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
import { AlertCircle } from 'lucide-react';
|
||||||
|
import type { SubmissionItemData } from '@/types/submissions';
|
||||||
|
|
||||||
|
interface SubmissionItemsListProps {
|
||||||
|
submissionId: string;
|
||||||
|
view?: 'summary' | 'detailed';
|
||||||
|
showImages?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SubmissionItemsList({
|
||||||
|
submissionId,
|
||||||
|
view = 'summary',
|
||||||
|
showImages = true
|
||||||
|
}: SubmissionItemsListProps) {
|
||||||
|
const [items, setItems] = useState<SubmissionItemData[]>([]);
|
||||||
|
const [hasPhotos, setHasPhotos] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchSubmissionItems();
|
||||||
|
}, [submissionId]);
|
||||||
|
|
||||||
|
const fetchSubmissionItems = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// Fetch submission items
|
||||||
|
const { data: itemsData, error: itemsError } = await supabase
|
||||||
|
.from('submission_items')
|
||||||
|
.select('*')
|
||||||
|
.eq('submission_id', submissionId)
|
||||||
|
.order('order_index');
|
||||||
|
|
||||||
|
if (itemsError) throw itemsError;
|
||||||
|
|
||||||
|
// Check for photo submissions
|
||||||
|
const { data: photoData, error: photoError } = await supabase
|
||||||
|
.from('photo_submissions')
|
||||||
|
.select('id')
|
||||||
|
.eq('submission_id', submissionId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (photoError && photoError.code !== 'PGRST116') {
|
||||||
|
console.warn('Error checking photo submissions:', photoError);
|
||||||
|
}
|
||||||
|
|
||||||
|
setItems((itemsData || []) as SubmissionItemData[]);
|
||||||
|
setHasPhotos(!!photoData);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching submission items:', err);
|
||||||
|
setError('Failed to load submission details');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Skeleton className="h-16 w-full" />
|
||||||
|
{view === 'detailed' && <Skeleton className="h-32 w-full" />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.length === 0 && !hasPhotos) {
|
||||||
|
return (
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
No items found for this submission
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{/* Show regular submission items */}
|
||||||
|
{items.map((item) => (
|
||||||
|
<div key={item.id} className={view === 'summary' ? 'border-l-2 border-primary/20 pl-3' : ''}>
|
||||||
|
<SubmissionChangesDisplay
|
||||||
|
item={item}
|
||||||
|
view={view}
|
||||||
|
showImages={showImages}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Show photo submission if exists */}
|
||||||
|
{hasPhotos && (
|
||||||
|
<div className={view === 'summary' ? 'border-l-2 border-primary/20 pl-3' : ''}>
|
||||||
|
<PhotoSubmissionDisplay submissionId={submissionId} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -15,12 +15,22 @@ export interface ImageChange {
|
|||||||
newId?: string;
|
newId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PhotoChange {
|
||||||
|
type: 'added' | 'edited' | 'deleted';
|
||||||
|
photoUrl: string;
|
||||||
|
title?: string;
|
||||||
|
caption?: string;
|
||||||
|
oldCaption?: string;
|
||||||
|
newCaption?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ChangesSummary {
|
export interface ChangesSummary {
|
||||||
action: 'create' | 'edit' | 'delete';
|
action: 'create' | 'edit' | 'delete';
|
||||||
entityType: string;
|
entityType: string;
|
||||||
entityName?: string;
|
entityName?: string;
|
||||||
fieldChanges: FieldChange[];
|
fieldChanges: FieldChange[];
|
||||||
imageChanges: ImageChange[];
|
imageChanges: ImageChange[];
|
||||||
|
photoChanges: PhotoChange[];
|
||||||
hasLocationChange: boolean;
|
hasLocationChange: boolean;
|
||||||
totalChanges: number;
|
totalChanges: number;
|
||||||
}
|
}
|
||||||
@@ -120,8 +130,9 @@ export function detectChanges(item: { item_data?: any; original_data?: any; item
|
|||||||
entityName,
|
entityName,
|
||||||
fieldChanges,
|
fieldChanges,
|
||||||
imageChanges,
|
imageChanges,
|
||||||
|
photoChanges: [], // Will be populated by component with submissionId
|
||||||
hasLocationChange,
|
hasLocationChange,
|
||||||
totalChanges: fieldChanges.length + imageChanges.length,
|
totalChanges: fieldChanges.length + imageChanges.length + (hasLocationChange ? 1 : 0)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,10 +228,48 @@ export function formatFieldName(field: string): string {
|
|||||||
export function formatFieldValue(value: any): string {
|
export function formatFieldValue(value: any): string {
|
||||||
if (value === null || value === undefined) return 'None';
|
if (value === null || value === undefined) return 'None';
|
||||||
if (typeof value === 'boolean') return value ? 'Yes' : 'No';
|
if (typeof value === 'boolean') return value ? 'Yes' : 'No';
|
||||||
if (typeof value === 'object') {
|
|
||||||
if (Array.isArray(value)) return `${value.length} items`;
|
// Handle dates
|
||||||
return JSON.stringify(value, null, 2);
|
if (value instanceof Date || (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}/.test(value))) {
|
||||||
|
try {
|
||||||
|
const date = new Date(value);
|
||||||
|
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
|
||||||
|
} catch {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle arrays - show actual items
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (value.length === 0) return 'None';
|
||||||
|
if (value.length <= 3) return value.map(v => String(v)).join(', ');
|
||||||
|
return `${value.slice(0, 3).map(v => String(v)).join(', ')}... +${value.length - 3} more`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle objects - create readable summary
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
// Location object
|
||||||
|
if (value.city || value.state_province || value.country) {
|
||||||
|
const parts = [value.city, value.state_province, value.country].filter(Boolean);
|
||||||
|
return parts.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic object - show key-value pairs
|
||||||
|
const entries = Object.entries(value).slice(0, 3);
|
||||||
|
if (entries.length === 0) return 'Empty';
|
||||||
|
return entries.map(([k, v]) => `${k}: ${v}`).join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle URLs
|
||||||
|
if (typeof value === 'string' && value.startsWith('http')) {
|
||||||
|
try {
|
||||||
|
const url = new URL(value);
|
||||||
|
return url.hostname + (url.pathname !== '/' ? url.pathname.slice(0, 30) : '');
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof value === 'number') return value.toLocaleString();
|
if (typeof value === 'number') return value.toLocaleString();
|
||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user