mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-25 03:11:13 -05:00
Compare commits
2 Commits
edit/edt-5
...
edit/edt-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8bea4b798 | ||
|
|
250e7c488a |
@@ -1,16 +1,13 @@
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
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;
|
||||
staggerIndex?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,12 +18,8 @@ export function DetailedViewCollapsible({
|
||||
isCollapsed,
|
||||
onToggle,
|
||||
children,
|
||||
fieldCount,
|
||||
className,
|
||||
staggerIndex = 0
|
||||
className
|
||||
}: DetailedViewCollapsibleProps) {
|
||||
// Calculate stagger delay: 50ms per item, max 300ms
|
||||
const staggerDelay = Math.min(staggerIndex * 50, 300);
|
||||
return (
|
||||
<Collapsible open={!isCollapsed} onOpenChange={() => onToggle()}>
|
||||
<div className={cn("mt-6 pt-6 border-t", className)}>
|
||||
@@ -34,42 +27,25 @@ export function DetailedViewCollapsible({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full flex items-center justify-between hover:bg-muted/50 p-2 h-auto transition-colors"
|
||||
className="w-full flex items-center justify-between hover:bg-muted/50 p-2 h-auto"
|
||||
>
|
||||
<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 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>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"h-4 w-4 text-muted-foreground transition-all duration-300 ease-out",
|
||||
!isCollapsed && "rotate-180"
|
||||
)}
|
||||
/>
|
||||
{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"
|
||||
style={{
|
||||
animationDelay: `${staggerDelay}ms`,
|
||||
transitionDelay: `${staggerDelay}ms`
|
||||
}}
|
||||
>
|
||||
<CollapsibleContent className="mt-3">
|
||||
{children}
|
||||
</CollapsibleContent>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { Filter, MessageSquare, FileText, Image, X, ChevronDown, Calendar, Maximize2, Minimize2 } from 'lucide-react';
|
||||
import { Filter, MessageSquare, FileText, Image, X, ChevronDown, Calendar } 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';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
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';
|
||||
|
||||
@@ -57,7 +55,6 @@ export const QueueFilters = ({
|
||||
isRefreshing = false
|
||||
}: QueueFiltersProps) => {
|
||||
const { isCollapsed, toggle } = useFilterPanelState();
|
||||
const { isCollapsed: detailsCollapsed, toggle: toggleDetails } = useDetailedViewState();
|
||||
|
||||
// Count active filters
|
||||
const activeFilterCount = [
|
||||
@@ -79,51 +76,14 @@ export const QueueFilters = ({
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Global toggle for detailed views */}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={toggleDetails}
|
||||
className="h-8 gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{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>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="max-w-xs">
|
||||
<p className="text-xs">
|
||||
{detailsCollapsed
|
||||
? "Show detailed field-by-field view for all items in the queue"
|
||||
: "Hide detailed field-by-field view for all items in the queue"}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
This preference is saved to your account
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
{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>
|
||||
{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>
|
||||
|
||||
<CollapsibleContent className="space-y-4">
|
||||
|
||||
@@ -42,12 +42,6 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
|
||||
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
|
||||
@@ -135,7 +129,7 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
|
||||
}
|
||||
|
||||
// Render item with appropriate display component
|
||||
const renderItem = (item: SubmissionItemData, index: number = 0) => {
|
||||
const renderItem = (item: SubmissionItemData) => {
|
||||
// SubmissionItemData from submissions.ts has item_data property
|
||||
const entityData = item.item_data;
|
||||
const actionType = item.action_type || 'create';
|
||||
@@ -197,12 +191,7 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
|
||||
data={entityData as unknown as ParkSubmissionData}
|
||||
actionType={actionType}
|
||||
/>
|
||||
<DetailedViewCollapsible
|
||||
isCollapsed={isCollapsed}
|
||||
onToggle={toggle}
|
||||
fieldCount={countFields(entityData)}
|
||||
staggerIndex={index}
|
||||
>
|
||||
<DetailedViewCollapsible isCollapsed={isCollapsed} onToggle={toggle}>
|
||||
<SubmissionChangesDisplay
|
||||
item={item}
|
||||
view="detailed"
|
||||
@@ -222,12 +211,7 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
|
||||
data={entityData as unknown as RideSubmissionData}
|
||||
actionType={actionType}
|
||||
/>
|
||||
<DetailedViewCollapsible
|
||||
isCollapsed={isCollapsed}
|
||||
onToggle={toggle}
|
||||
fieldCount={countFields(entityData)}
|
||||
staggerIndex={index}
|
||||
>
|
||||
<DetailedViewCollapsible isCollapsed={isCollapsed} onToggle={toggle}>
|
||||
<SubmissionChangesDisplay
|
||||
item={item}
|
||||
view="detailed"
|
||||
@@ -247,12 +231,7 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
|
||||
data={entityData as unknown as CompanySubmissionData}
|
||||
actionType={actionType}
|
||||
/>
|
||||
<DetailedViewCollapsible
|
||||
isCollapsed={isCollapsed}
|
||||
onToggle={toggle}
|
||||
fieldCount={countFields(entityData)}
|
||||
staggerIndex={index}
|
||||
>
|
||||
<DetailedViewCollapsible isCollapsed={isCollapsed} onToggle={toggle}>
|
||||
<SubmissionChangesDisplay
|
||||
item={item}
|
||||
view="detailed"
|
||||
@@ -272,12 +251,7 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
|
||||
data={entityData as unknown as RideModelSubmissionData}
|
||||
actionType={actionType}
|
||||
/>
|
||||
<DetailedViewCollapsible
|
||||
isCollapsed={isCollapsed}
|
||||
onToggle={toggle}
|
||||
fieldCount={countFields(entityData)}
|
||||
staggerIndex={index}
|
||||
>
|
||||
<DetailedViewCollapsible isCollapsed={isCollapsed} onToggle={toggle}>
|
||||
<SubmissionChangesDisplay
|
||||
item={item}
|
||||
view="detailed"
|
||||
@@ -297,12 +271,7 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
|
||||
data={entityData as unknown as TimelineSubmissionData}
|
||||
actionType={actionType}
|
||||
/>
|
||||
<DetailedViewCollapsible
|
||||
isCollapsed={isCollapsed}
|
||||
onToggle={toggle}
|
||||
fieldCount={countFields(entityData)}
|
||||
staggerIndex={index}
|
||||
>
|
||||
<DetailedViewCollapsible isCollapsed={isCollapsed} onToggle={toggle}>
|
||||
<SubmissionChangesDisplay
|
||||
item={item}
|
||||
view="detailed"
|
||||
@@ -339,9 +308,9 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
|
||||
)}
|
||||
|
||||
{/* Show regular submission items */}
|
||||
{items.map((item, index) => (
|
||||
{items.map((item) => (
|
||||
<div key={item.id} className={view === 'summary' ? 'border-l-2 border-primary/20 pl-3' : ''}>
|
||||
{renderItem(item, index)}
|
||||
{renderItem(item)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
@@ -1,30 +1,9 @@
|
||||
import * as React from "react";
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root;
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
|
||||
|
||||
const CollapsibleContent = React.forwardRef<
|
||||
React.ElementRef<typeof CollapsiblePrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<CollapsiblePrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden transition-all duration-300 ease-out",
|
||||
"data-[state=closed]:animate-accordion-up",
|
||||
"data-[state=open]:animate-accordion-down",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="animate-fade-in">
|
||||
{children}
|
||||
</div>
|
||||
</CollapsiblePrimitive.Content>
|
||||
));
|
||||
CollapsibleContent.displayName = "CollapsibleContent";
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||
|
||||
@@ -1,129 +1,48 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { supabase } from '@/lib/supabaseClient';
|
||||
import { handleNonCriticalError } from '@/lib/errorHandler';
|
||||
import type { Json } from '@/integrations/supabase/types';
|
||||
|
||||
const STORAGE_KEY = 'detailed-view-collapsed';
|
||||
|
||||
interface ModerationPreferences {
|
||||
detailed_view_collapsed: boolean;
|
||||
}
|
||||
|
||||
interface UseDetailedViewStateReturn {
|
||||
isCollapsed: boolean;
|
||||
toggle: () => void;
|
||||
setCollapsed: (value: boolean) => void;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to manage detailed view collapsed/expanded state
|
||||
* Persists to database for authenticated users, localStorage for guests
|
||||
* Syncs with localStorage for persistence across sessions
|
||||
* Defaults to collapsed to reduce visual clutter
|
||||
*/
|
||||
export function useDetailedViewState(): UseDetailedViewStateReturn {
|
||||
const { user } = useAuth();
|
||||
const [isCollapsed, setIsCollapsed] = useState<boolean>(true);
|
||||
const [loading, setLoading] = useState(true);
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
// Load preferences on mount
|
||||
// Sync to localStorage when state changes
|
||||
useEffect(() => {
|
||||
loadPreferences();
|
||||
}, [user]);
|
||||
|
||||
const loadPreferences = async () => {
|
||||
try {
|
||||
if (user) {
|
||||
// Load from database for authenticated users
|
||||
const { data, error } = await supabase
|
||||
.from('user_preferences')
|
||||
.select('moderation_preferences')
|
||||
.eq('user_id', user.id)
|
||||
.maybeSingle();
|
||||
|
||||
if (error && error.code !== 'PGRST116') {
|
||||
handleNonCriticalError(error, {
|
||||
action: 'Load moderation preferences',
|
||||
userId: user.id,
|
||||
});
|
||||
}
|
||||
|
||||
// Type assertion needed until Supabase regenerates types after migration
|
||||
const preferences = (data as any)?.moderation_preferences;
|
||||
if (preferences) {
|
||||
const prefs = preferences as ModerationPreferences;
|
||||
setIsCollapsed(prefs.detailed_view_collapsed ?? true);
|
||||
}
|
||||
} else {
|
||||
// Load from localStorage for guests
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
setIsCollapsed(stored ? JSON.parse(stored) : true);
|
||||
} catch (error) {
|
||||
logger.warn('Error reading detailed view state from localStorage', { error });
|
||||
}
|
||||
}
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(isCollapsed));
|
||||
} catch (error) {
|
||||
logger.warn('Error loading detailed view preferences', { error });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
logger.warn('Error saving detailed view state to localStorage', { error });
|
||||
}
|
||||
};
|
||||
}, [isCollapsed]);
|
||||
|
||||
const savePreferences = async (collapsed: boolean) => {
|
||||
try {
|
||||
if (user) {
|
||||
// Save to database for authenticated users
|
||||
const moderationPrefs: ModerationPreferences = {
|
||||
detailed_view_collapsed: collapsed,
|
||||
};
|
||||
|
||||
const { error } = await supabase
|
||||
.from('user_preferences')
|
||||
.upsert({
|
||||
user_id: user.id,
|
||||
moderation_preferences: moderationPrefs as unknown as Json,
|
||||
updated_at: new Date().toISOString(),
|
||||
}, {
|
||||
onConflict: 'user_id',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
handleNonCriticalError(error, {
|
||||
action: 'Save moderation preferences',
|
||||
userId: user.id,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Save to localStorage for guests
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(collapsed));
|
||||
} catch (error) {
|
||||
logger.warn('Error saving detailed view state to localStorage', { error });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('Error saving detailed view preferences', { error });
|
||||
}
|
||||
};
|
||||
|
||||
const toggle = () => {
|
||||
const newValue = !isCollapsed;
|
||||
setIsCollapsed(newValue);
|
||||
savePreferences(newValue);
|
||||
};
|
||||
const toggle = () => setIsCollapsed(prev => !prev);
|
||||
|
||||
const setCollapsed = (value: boolean) => {
|
||||
setIsCollapsed(value);
|
||||
savePreferences(value);
|
||||
};
|
||||
const setCollapsed = (value: boolean) => setIsCollapsed(value);
|
||||
|
||||
return {
|
||||
isCollapsed,
|
||||
toggle,
|
||||
setCollapsed,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { twMerge } from "tailwind-merge";
|
||||
*
|
||||
* @param inputs - Class values to combine (strings, objects, arrays)
|
||||
* @returns Merged class string with Tailwind conflicts resolved
|
||||
* @example cn('px-2 py-1', 'px-4') // Returns 'py-1 px-4'
|
||||
*/
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
|
||||
@@ -207,8 +207,8 @@ const handler = async (req: Request, context: { supabase: any; user: any; span:
|
||||
p_moderator_id: user.id,
|
||||
p_submitter_id: submission.user_id,
|
||||
p_request_id: requestId,
|
||||
p_approval_mode: 'selective',
|
||||
p_idempotency_key: idempotencyKey
|
||||
p_trace_id: rootSpan.traceId,
|
||||
p_parent_span_id: rpcSpan.spanId
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
-- Add moderation_preferences column to user_preferences table
|
||||
-- This stores moderator UI preferences like detailed view collapsed state
|
||||
|
||||
ALTER TABLE public.user_preferences
|
||||
ADD COLUMN IF NOT EXISTS moderation_preferences JSONB NOT NULL DEFAULT '{}'::jsonb;
|
||||
|
||||
COMMENT ON COLUMN public.user_preferences.moderation_preferences IS
|
||||
'Stores moderator UI preferences like detailed view collapsed state';
|
||||
|
||||
-- Add GIN index for efficient JSONB queries
|
||||
CREATE INDEX IF NOT EXISTS idx_user_preferences_moderation_prefs
|
||||
ON public.user_preferences USING gin(moderation_preferences);
|
||||
Reference in New Issue
Block a user