Compare commits

...

5 Commits

Author SHA1 Message Date
gpt-engineer-app[bot]
bf4b05bb18 Changes 2025-11-12 14:42:20 +00:00
gpt-engineer-app[bot]
6bd7d24a1b Add item-level history badge and animations
- Show a dynamic field-count badge next to All Fields (Detailed View) in the moderation queue
- Animate collapsible sections with smooth transitions for expand/collapse
- Pass fieldCount to DetailedViewCollapsible and render count alongside header; add animation utility in DetailedViewCollapsible.tsx
- Ensure SubmissionItemsList passes item data to calculate field counts and display badges accordingly

X-Lovable-Edit-ID: edt-ffd226b0-af99-491b-b6b8-3fe0063e0082
2025-11-12 14:40:52 +00:00
gpt-engineer-app[bot]
72e76e86af testing changes with virtual file cleanup 2025-11-12 14:40:51 +00:00
gpt-engineer-app[bot]
a35486fb11 Add collapsible detailed view
Implements expand/collapse for the All Fields (Detailed View) sections in the moderation queue:
- Adds useDetailedViewState hook to persist collapse state in localStorage (default collapsed)
- Adds DetailedViewCollapsible wrapper component using Radix Collapsible
- Updates SubmissionItemsList to wrap each detailed view block with the new collapsible, and imports the new hook and component

X-Lovable-Edit-ID: edt-a95a840d-e7e7-4f9e-aa25-03bb68194aee
2025-11-12 14:36:50 +00:00
gpt-engineer-app[bot]
3d3ae57ee3 testing changes with virtual file cleanup 2025-11-12 14:36:49 +00:00
4 changed files with 187 additions and 34 deletions

View File

@@ -0,0 +1,67 @@
import { ChevronDown, ChevronUp } from 'lucide-react';
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@/components/ui/collapsible';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils';
interface DetailedViewCollapsibleProps {
isCollapsed: boolean;
onToggle: () => void;
children: React.ReactNode;
fieldCount?: number;
className?: string;
}
/**
* Collapsible wrapper for detailed field-by-field view sections
* Provides expand/collapse functionality with visual indicators
*/
export function DetailedViewCollapsible({
isCollapsed,
onToggle,
children,
fieldCount,
className
}: DetailedViewCollapsibleProps) {
return (
<Collapsible open={!isCollapsed} onOpenChange={() => onToggle()}>
<div className={cn("mt-6 pt-6 border-t", className)}>
<CollapsibleTrigger asChild>
<Button
variant="ghost"
size="sm"
className="w-full flex items-center justify-between hover:bg-muted/50 p-2 h-auto transition-colors"
>
<div className="flex items-center gap-2">
<span className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
All Fields (Detailed View)
</span>
{fieldCount !== undefined && fieldCount > 0 && (
<Badge
variant="secondary"
className="h-5 px-1.5 text-xs font-normal transition-transform duration-200 hover:scale-105"
>
{fieldCount}
</Badge>
)}
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground normal-case font-normal">
{isCollapsed ? 'Show' : 'Hide'}
</span>
{isCollapsed ? (
<ChevronDown className="h-4 w-4 text-muted-foreground transition-transform duration-200" />
) : (
<ChevronUp className="h-4 w-4 text-muted-foreground transition-transform duration-200" />
)}
</div>
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="mt-3 data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down">
{children}
</CollapsibleContent>
</div>
</Collapsible>
);
}

View File

@@ -1,4 +1,4 @@
import { Filter, MessageSquare, FileText, Image, X, ChevronDown, Calendar } from 'lucide-react';
import { Filter, MessageSquare, FileText, Image, X, ChevronDown, Calendar, Maximize2, Minimize2 } from 'lucide-react';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Button } from '@/components/ui/button';
@@ -7,6 +7,7 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/component
import { RefreshButton } from '@/components/ui/refresh-button';
import { QueueSortControls } from './QueueSortControls';
import { useFilterPanelState } from '@/hooks/useFilterPanelState';
import { useDetailedViewState } from '@/hooks/useDetailedViewState';
import { FilterDateRangePicker } from '@/components/filters/FilterDateRangePicker';
import type { EntityFilter, StatusFilter, SortConfig, QueueTab, ApprovalDateRangeFilter } from '@/types/moderation';
@@ -55,6 +56,7 @@ export const QueueFilters = ({
isRefreshing = false
}: QueueFiltersProps) => {
const { isCollapsed, toggle } = useFilterPanelState();
const { isCollapsed: detailsCollapsed, toggle: toggleDetails } = useDetailedViewState();
// Count active filters
const activeFilterCount = [
@@ -76,14 +78,36 @@ export const QueueFilters = ({
</Badge>
)}
</div>
{isMobile && (
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<ChevronDown className={`h-4 w-4 transition-transform duration-250 ${isCollapsed ? '' : 'rotate-180'}`} />
<span className="sr-only">{isCollapsed ? 'Expand filters' : 'Collapse filters'}</span>
</Button>
</CollapsibleTrigger>
)}
<div className="flex items-center gap-2">
{/* Global toggle for detailed views */}
<Button
variant="ghost"
size="sm"
onClick={toggleDetails}
className="h-8 gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors"
title={detailsCollapsed ? "Expand all detailed views" : "Collapse all detailed views"}
>
{detailsCollapsed ? (
<>
<Maximize2 className="h-3.5 w-3.5" />
{!isMobile && <span>Expand All</span>}
</>
) : (
<>
<Minimize2 className="h-3.5 w-3.5" />
{!isMobile && <span>Collapse All</span>}
</>
)}
</Button>
{isMobile && (
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<ChevronDown className={`h-4 w-4 transition-transform duration-250 ${isCollapsed ? '' : 'rotate-180'}`} />
<span className="sr-only">{isCollapsed ? 'Expand filters' : 'Collapse filters'}</span>
</Button>
</CollapsibleTrigger>
)}
</div>
</div>
<CollapsibleContent className="space-y-4">

View File

@@ -7,6 +7,7 @@ import { RichRideDisplay } from './displays/RichRideDisplay';
import { RichCompanyDisplay } from './displays/RichCompanyDisplay';
import { RichRideModelDisplay } from './displays/RichRideModelDisplay';
import { RichTimelineEventDisplay } from './displays/RichTimelineEventDisplay';
import { DetailedViewCollapsible } from './DetailedViewCollapsible';
import { Skeleton } from '@/components/ui/skeleton';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
@@ -17,6 +18,7 @@ import type { ParkSubmissionData, RideSubmissionData, CompanySubmissionData, Rid
import type { TimelineSubmissionData } from '@/types/timeline';
import { getErrorMessage, handleNonCriticalError } from '@/lib/errorHandler';
import { ModerationErrorBoundary } from '@/components/error/ModerationErrorBoundary';
import { useDetailedViewState } from '@/hooks/useDetailedViewState';
interface SubmissionItemsListProps {
submissionId: string;
@@ -34,11 +36,18 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
const { isCollapsed, toggle } = useDetailedViewState();
useEffect(() => {
fetchSubmissionItems();
}, [submissionId]);
// Helper function to count non-null fields in entity data
const countFields = (data: any): number => {
if (!data || typeof data !== 'object') return 0;
return Object.values(data).filter(value => value !== null && value !== undefined).length;
};
const fetchSubmissionItems = async () => {
try {
// Only show skeleton on initial load, show refreshing indicator on refresh
@@ -188,17 +197,18 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
data={entityData as unknown as ParkSubmissionData}
actionType={actionType}
/>
<div className="mt-6 pt-6 border-t">
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-3">
All Fields (Detailed View)
</div>
<DetailedViewCollapsible
isCollapsed={isCollapsed}
onToggle={toggle}
fieldCount={countFields(entityData)}
>
<SubmissionChangesDisplay
item={item}
view="detailed"
showImages={showImages}
submissionId={submissionId}
/>
</div>
</DetailedViewCollapsible>
</>
);
}
@@ -211,17 +221,18 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
data={entityData as unknown as RideSubmissionData}
actionType={actionType}
/>
<div className="mt-6 pt-6 border-t">
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-3">
All Fields (Detailed View)
</div>
<DetailedViewCollapsible
isCollapsed={isCollapsed}
onToggle={toggle}
fieldCount={countFields(entityData)}
>
<SubmissionChangesDisplay
item={item}
view="detailed"
showImages={showImages}
submissionId={submissionId}
/>
</div>
</DetailedViewCollapsible>
</>
);
}
@@ -234,17 +245,18 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
data={entityData as unknown as CompanySubmissionData}
actionType={actionType}
/>
<div className="mt-6 pt-6 border-t">
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-3">
All Fields (Detailed View)
</div>
<DetailedViewCollapsible
isCollapsed={isCollapsed}
onToggle={toggle}
fieldCount={countFields(entityData)}
>
<SubmissionChangesDisplay
item={item}
view="detailed"
showImages={showImages}
submissionId={submissionId}
/>
</div>
</DetailedViewCollapsible>
</>
);
}
@@ -257,17 +269,18 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
data={entityData as unknown as RideModelSubmissionData}
actionType={actionType}
/>
<div className="mt-6 pt-6 border-t">
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-3">
All Fields (Detailed View)
</div>
<DetailedViewCollapsible
isCollapsed={isCollapsed}
onToggle={toggle}
fieldCount={countFields(entityData)}
>
<SubmissionChangesDisplay
item={item}
view="detailed"
showImages={showImages}
submissionId={submissionId}
/>
</div>
</DetailedViewCollapsible>
</>
);
}
@@ -280,17 +293,18 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
data={entityData as unknown as TimelineSubmissionData}
actionType={actionType}
/>
<div className="mt-6 pt-6 border-t">
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-3">
All Fields (Detailed View)
</div>
<DetailedViewCollapsible
isCollapsed={isCollapsed}
onToggle={toggle}
fieldCount={countFields(entityData)}
>
<SubmissionChangesDisplay
item={item}
view="detailed"
showImages={showImages}
submissionId={submissionId}
/>
</div>
</DetailedViewCollapsible>
</>
);
}

View File

@@ -0,0 +1,48 @@
import { useState, useEffect } from 'react';
import { logger } from '@/lib/logger';
const STORAGE_KEY = 'detailed-view-collapsed';
interface UseDetailedViewStateReturn {
isCollapsed: boolean;
toggle: () => void;
setCollapsed: (value: boolean) => void;
}
/**
* Hook to manage detailed view collapsed/expanded state
* Syncs with localStorage for persistence across sessions
* Defaults to collapsed to reduce visual clutter
*/
export function useDetailedViewState(): UseDetailedViewStateReturn {
const [isCollapsed, setIsCollapsed] = useState<boolean>(() => {
// Initialize from localStorage on mount
try {
const stored = localStorage.getItem(STORAGE_KEY);
// Default to collapsed (true) to reduce visual clutter
return stored ? JSON.parse(stored) : true;
} catch (error) {
logger.warn('Error reading detailed view state from localStorage', { error });
return true;
}
});
// Sync to localStorage when state changes
useEffect(() => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(isCollapsed));
} catch (error) {
logger.warn('Error saving detailed view state to localStorage', { error });
}
}, [isCollapsed]);
const toggle = () => setIsCollapsed(prev => !prev);
const setCollapsed = (value: boolean) => setIsCollapsed(value);
return {
isCollapsed,
toggle,
setCollapsed,
};
}