mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 04:31:13 -05:00
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
This commit is contained in:
54
src/components/moderation/DetailedViewCollapsible.tsx
Normal file
54
src/components/moderation/DetailedViewCollapsible.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
|
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@/components/ui/collapsible';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface DetailedViewCollapsibleProps {
|
||||||
|
isCollapsed: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collapsible wrapper for detailed field-by-field view sections
|
||||||
|
* Provides expand/collapse functionality with visual indicators
|
||||||
|
*/
|
||||||
|
export function DetailedViewCollapsible({
|
||||||
|
isCollapsed,
|
||||||
|
onToggle,
|
||||||
|
children,
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
||||||
|
All Fields (Detailed View)
|
||||||
|
</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" />
|
||||||
|
) : (
|
||||||
|
<ChevronUp className="h-4 w-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
|
||||||
|
<CollapsibleContent className="mt-3">
|
||||||
|
{children}
|
||||||
|
</CollapsibleContent>
|
||||||
|
</div>
|
||||||
|
</Collapsible>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { RichRideDisplay } from './displays/RichRideDisplay';
|
|||||||
import { RichCompanyDisplay } from './displays/RichCompanyDisplay';
|
import { RichCompanyDisplay } from './displays/RichCompanyDisplay';
|
||||||
import { RichRideModelDisplay } from './displays/RichRideModelDisplay';
|
import { RichRideModelDisplay } from './displays/RichRideModelDisplay';
|
||||||
import { RichTimelineEventDisplay } from './displays/RichTimelineEventDisplay';
|
import { RichTimelineEventDisplay } from './displays/RichTimelineEventDisplay';
|
||||||
|
import { DetailedViewCollapsible } from './DetailedViewCollapsible';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
@@ -17,6 +18,7 @@ import type { ParkSubmissionData, RideSubmissionData, CompanySubmissionData, Rid
|
|||||||
import type { TimelineSubmissionData } from '@/types/timeline';
|
import type { TimelineSubmissionData } from '@/types/timeline';
|
||||||
import { getErrorMessage, handleNonCriticalError } from '@/lib/errorHandler';
|
import { getErrorMessage, handleNonCriticalError } from '@/lib/errorHandler';
|
||||||
import { ModerationErrorBoundary } from '@/components/error/ModerationErrorBoundary';
|
import { ModerationErrorBoundary } from '@/components/error/ModerationErrorBoundary';
|
||||||
|
import { useDetailedViewState } from '@/hooks/useDetailedViewState';
|
||||||
|
|
||||||
interface SubmissionItemsListProps {
|
interface SubmissionItemsListProps {
|
||||||
submissionId: string;
|
submissionId: string;
|
||||||
@@ -34,6 +36,7 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const { isCollapsed, toggle } = useDetailedViewState();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchSubmissionItems();
|
fetchSubmissionItems();
|
||||||
@@ -188,17 +191,14 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
|
|||||||
data={entityData as unknown as ParkSubmissionData}
|
data={entityData as unknown as ParkSubmissionData}
|
||||||
actionType={actionType}
|
actionType={actionType}
|
||||||
/>
|
/>
|
||||||
<div className="mt-6 pt-6 border-t">
|
<DetailedViewCollapsible isCollapsed={isCollapsed} onToggle={toggle}>
|
||||||
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-3">
|
|
||||||
All Fields (Detailed View)
|
|
||||||
</div>
|
|
||||||
<SubmissionChangesDisplay
|
<SubmissionChangesDisplay
|
||||||
item={item}
|
item={item}
|
||||||
view="detailed"
|
view="detailed"
|
||||||
showImages={showImages}
|
showImages={showImages}
|
||||||
submissionId={submissionId}
|
submissionId={submissionId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</DetailedViewCollapsible>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -211,17 +211,14 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
|
|||||||
data={entityData as unknown as RideSubmissionData}
|
data={entityData as unknown as RideSubmissionData}
|
||||||
actionType={actionType}
|
actionType={actionType}
|
||||||
/>
|
/>
|
||||||
<div className="mt-6 pt-6 border-t">
|
<DetailedViewCollapsible isCollapsed={isCollapsed} onToggle={toggle}>
|
||||||
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-3">
|
|
||||||
All Fields (Detailed View)
|
|
||||||
</div>
|
|
||||||
<SubmissionChangesDisplay
|
<SubmissionChangesDisplay
|
||||||
item={item}
|
item={item}
|
||||||
view="detailed"
|
view="detailed"
|
||||||
showImages={showImages}
|
showImages={showImages}
|
||||||
submissionId={submissionId}
|
submissionId={submissionId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</DetailedViewCollapsible>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -234,17 +231,14 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
|
|||||||
data={entityData as unknown as CompanySubmissionData}
|
data={entityData as unknown as CompanySubmissionData}
|
||||||
actionType={actionType}
|
actionType={actionType}
|
||||||
/>
|
/>
|
||||||
<div className="mt-6 pt-6 border-t">
|
<DetailedViewCollapsible isCollapsed={isCollapsed} onToggle={toggle}>
|
||||||
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-3">
|
|
||||||
All Fields (Detailed View)
|
|
||||||
</div>
|
|
||||||
<SubmissionChangesDisplay
|
<SubmissionChangesDisplay
|
||||||
item={item}
|
item={item}
|
||||||
view="detailed"
|
view="detailed"
|
||||||
showImages={showImages}
|
showImages={showImages}
|
||||||
submissionId={submissionId}
|
submissionId={submissionId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</DetailedViewCollapsible>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -257,17 +251,14 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
|
|||||||
data={entityData as unknown as RideModelSubmissionData}
|
data={entityData as unknown as RideModelSubmissionData}
|
||||||
actionType={actionType}
|
actionType={actionType}
|
||||||
/>
|
/>
|
||||||
<div className="mt-6 pt-6 border-t">
|
<DetailedViewCollapsible isCollapsed={isCollapsed} onToggle={toggle}>
|
||||||
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-3">
|
|
||||||
All Fields (Detailed View)
|
|
||||||
</div>
|
|
||||||
<SubmissionChangesDisplay
|
<SubmissionChangesDisplay
|
||||||
item={item}
|
item={item}
|
||||||
view="detailed"
|
view="detailed"
|
||||||
showImages={showImages}
|
showImages={showImages}
|
||||||
submissionId={submissionId}
|
submissionId={submissionId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</DetailedViewCollapsible>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -280,17 +271,14 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
|
|||||||
data={entityData as unknown as TimelineSubmissionData}
|
data={entityData as unknown as TimelineSubmissionData}
|
||||||
actionType={actionType}
|
actionType={actionType}
|
||||||
/>
|
/>
|
||||||
<div className="mt-6 pt-6 border-t">
|
<DetailedViewCollapsible isCollapsed={isCollapsed} onToggle={toggle}>
|
||||||
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-3">
|
|
||||||
All Fields (Detailed View)
|
|
||||||
</div>
|
|
||||||
<SubmissionChangesDisplay
|
<SubmissionChangesDisplay
|
||||||
item={item}
|
item={item}
|
||||||
view="detailed"
|
view="detailed"
|
||||||
showImages={showImages}
|
showImages={showImages}
|
||||||
submissionId={submissionId}
|
submissionId={submissionId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</DetailedViewCollapsible>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
48
src/hooks/useDetailedViewState.ts
Normal file
48
src/hooks/useDetailedViewState.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user