Files
thrilltrack-explorer/src-old/components/moderation/ItemEditDialog.tsx

386 lines
13 KiB
TypeScript

import { useState } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from '@/components/ui/sheet';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { toast } from '@/hooks/use-toast';
import { useIsMobile } from '@/hooks/use-mobile';
import { logger } from '@/lib/logger';
import { useUserRole } from '@/hooks/useUserRole';
import { getErrorMessage } from '@/lib/errorHandler';
import { useAuth } from '@/hooks/useAuth';
import { editSubmissionItem, type SubmissionItemWithDeps } from '@/lib/submissionItemsService';
import { ParkForm } from '@/components/admin/ParkForm';
import { RideForm } from '@/components/admin/RideForm';
import { ManufacturerForm } from '@/components/admin/ManufacturerForm';
import { DesignerForm } from '@/components/admin/DesignerForm';
import { OperatorForm } from '@/components/admin/OperatorForm';
import { jsonToFormData } from '@/lib/typeConversions';
import { PropertyOwnerForm } from '@/components/admin/PropertyOwnerForm';
import { RideModelForm } from '@/components/admin/RideModelForm';
import { Save, X, Edit } from 'lucide-react';
import { SubmissionErrorBoundary } from '@/components/error/SubmissionErrorBoundary';
interface ItemEditDialogProps {
item?: SubmissionItemWithDeps | null;
items?: SubmissionItemWithDeps[];
open: boolean;
onOpenChange: (open: boolean) => void;
onComplete: () => void;
}
export function ItemEditDialog({ item, items, open, onOpenChange, onComplete }: ItemEditDialogProps) {
const [submitting, setSubmitting] = useState(false);
const [activeTab, setActiveTab] = useState<string>(items?.[0]?.id || '');
const isMobile = useIsMobile();
const { isModerator } = useUserRole();
const { user } = useAuth();
const Container = isMobile ? Sheet : Dialog;
// Phase 5: Bulk edit mode
const bulkEditMode = items && items.length > 1;
const currentItem = bulkEditMode ? items.find(i => i.id === activeTab) : item;
if (!currentItem && !bulkEditMode) return null;
const handleSubmit = async (data: Record<string, unknown>, itemId?: string) => {
if (!user?.id) {
toast({
title: 'Authentication Required',
description: 'You must be logged in to edit items',
variant: 'destructive',
});
return;
}
const targetItemId = itemId || currentItem?.id;
if (!targetItemId) return;
setSubmitting(true);
try {
await editSubmissionItem(targetItemId, data, user.id);
toast({
title: isModerator() ? 'Item Updated' : 'Edit Submitted',
description: isModerator()
? 'The item has been updated successfully.'
: 'Your edit has been submitted and will be reviewed by a moderator.',
});
if (bulkEditMode && items) {
// Move to next tab or complete
const currentIndex = items.findIndex(i => i.id === activeTab);
if (currentIndex < items.length - 1) {
setActiveTab(items[currentIndex + 1].id);
} else {
onComplete();
onOpenChange(false);
}
} else {
onComplete();
onOpenChange(false);
}
} catch (error: unknown) {
const errorMsg = getErrorMessage(error);
toast({
title: 'Error',
description: errorMsg,
variant: 'destructive',
});
} finally {
setSubmitting(false);
}
};
const handlePhotoSubmit = async (caption: string, credit: string) => {
if (!item?.item_data) {
toast({
title: 'Error',
description: 'No photo data available',
variant: 'destructive',
});
return;
}
const itemData = typeof item.item_data === 'object' && item.item_data !== null && !Array.isArray(item.item_data)
? item.item_data as Record<string, unknown>
: {};
const photos = 'photos' in itemData && Array.isArray(itemData.photos)
? itemData.photos
: [];
const photoData = {
...itemData,
photos: photos.map((photo: unknown) => ({
...(typeof photo === 'object' && photo !== null ? photo as Record<string, unknown> : {}),
caption,
credit,
})),
};
await handleSubmit(photoData);
};
const renderEditForm = (editItem: SubmissionItemWithDeps) => {
const itemData = typeof editItem.item_data === 'object' && editItem.item_data !== null && !Array.isArray(editItem.item_data)
? editItem.item_data as Record<string, unknown>
: {};
switch (editItem.item_type) {
case 'park':
return (
<SubmissionErrorBoundary submissionId={editItem.id}>
<ParkForm
onSubmit={handleSubmit}
onCancel={() => onOpenChange(false)}
initialData={jsonToFormData(editItem.item_data) as any}
isEditing
/>
</SubmissionErrorBoundary>
);
case 'ride':
return (
<SubmissionErrorBoundary submissionId={editItem.id}>
<RideForm
onSubmit={handleSubmit}
onCancel={() => onOpenChange(false)}
initialData={jsonToFormData(editItem.item_data) as any}
isEditing
/>
</SubmissionErrorBoundary>
);
case 'manufacturer':
return (
<SubmissionErrorBoundary submissionId={editItem.id}>
<ManufacturerForm
onSubmit={handleSubmit}
onCancel={() => onOpenChange(false)}
initialData={jsonToFormData(editItem.item_data) as any}
/>
</SubmissionErrorBoundary>
);
case 'designer':
return (
<SubmissionErrorBoundary submissionId={editItem.id}>
<DesignerForm
onSubmit={handleSubmit}
onCancel={() => onOpenChange(false)}
initialData={jsonToFormData(editItem.item_data) as any}
/>
</SubmissionErrorBoundary>
);
case 'operator':
return (
<SubmissionErrorBoundary submissionId={editItem.id}>
<OperatorForm
onSubmit={handleSubmit}
onCancel={() => onOpenChange(false)}
initialData={jsonToFormData(editItem.item_data) as any}
/>
</SubmissionErrorBoundary>
);
case 'property_owner':
return (
<SubmissionErrorBoundary submissionId={editItem.id}>
<PropertyOwnerForm
onSubmit={handleSubmit}
onCancel={() => onOpenChange(false)}
initialData={jsonToFormData(editItem.item_data) as any}
/>
</SubmissionErrorBoundary>
);
case 'ride_model':
const manufacturerName = 'manufacturer_name' in itemData && typeof itemData.manufacturer_name === 'string'
? itemData.manufacturer_name
: 'Unknown';
const manufacturerId = 'manufacturer_id' in itemData && typeof itemData.manufacturer_id === 'string'
? itemData.manufacturer_id
: '';
return (
<SubmissionErrorBoundary submissionId={editItem.id}>
<RideModelForm
manufacturerName={manufacturerName}
manufacturerId={manufacturerId}
onSubmit={handleSubmit}
onCancel={() => onOpenChange(false)}
initialData={itemData as any}
/>
</SubmissionErrorBoundary>
);
case 'photo':
const photos = 'photos' in itemData && Array.isArray(itemData.photos)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
? itemData.photos as any
: [];
return (
<PhotoEditForm
photos={photos}
onSubmit={handlePhotoSubmit}
onCancel={() => onOpenChange(false)}
submitting={submitting}
/>
);
default:
return (
<div className="text-center py-8 text-muted-foreground">
No edit form available for this item type
</div>
);
}
};
return (
<Container open={open} onOpenChange={onOpenChange}>
{isMobile ? (
<SheetContent side="bottom" className="h-[90vh] overflow-y-auto">
<SheetHeader>
<SheetTitle className="flex items-center gap-2">
<Edit className="w-5 h-5" />
{bulkEditMode ? `Edit ${items.length} Items` : `Edit ${currentItem?.item_type.replace('_', ' ')}`}
</SheetTitle>
<SheetDescription>
{bulkEditMode ? 'Edit multiple submission items using tabs' : 'Make changes to this submission item'}
</SheetDescription>
</SheetHeader>
<div className="mt-6">
{bulkEditMode && items ? (
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full" style={{ gridTemplateColumns: `repeat(${items.length}, 1fr)` }}>
{items.map((tabItem, index) => (
<TabsTrigger key={tabItem.id} value={tabItem.id}>
{index + 1}. {tabItem.item_type.replace('_', ' ')}
</TabsTrigger>
))}
</TabsList>
{items.map(tabItem => (
<TabsContent key={tabItem.id} value={tabItem.id} className="mt-4">
{renderEditForm(tabItem)}
</TabsContent>
))}
</Tabs>
) : currentItem ? (
renderEditForm(currentItem)
) : null}
</div>
</SheetContent>
) : (
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Edit className="w-5 h-5" />
{bulkEditMode ? `Edit ${items.length} Items` : `Edit ${currentItem?.item_type.replace('_', ' ')}`}
</DialogTitle>
<DialogDescription>
{bulkEditMode ? 'Edit multiple submission items using tabs' : 'Make changes to this submission item'}
</DialogDescription>
</DialogHeader>
<div className="mt-4">
{bulkEditMode && items ? (
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full" style={{ gridTemplateColumns: `repeat(${items.length}, 1fr)` }}>
{items.map((tabItem, index) => (
<TabsTrigger key={tabItem.id} value={tabItem.id}>
{index + 1}. {tabItem.item_type.replace('_', ' ')}
</TabsTrigger>
))}
</TabsList>
{items.map(tabItem => (
<TabsContent key={tabItem.id} value={tabItem.id} className="mt-4">
{renderEditForm(tabItem)}
</TabsContent>
))}
</Tabs>
) : currentItem ? (
renderEditForm(currentItem)
) : null}
</div>
</DialogContent>
)}
</Container>
);
}
// Simple photo editing form for caption and credit
interface PhotoItem {
url: string;
caption?: string | null;
credit?: string | null;
}
function PhotoEditForm({
photos,
onSubmit,
onCancel,
submitting
}: {
photos: PhotoItem[];
onSubmit: (caption: string, credit: string) => void;
onCancel: () => void;
submitting: boolean;
}) {
const [caption, setCaption] = useState(photos[0]?.caption || '');
const [credit, setCredit] = useState(photos[0]?.credit || '');
return (
<div className="space-y-6">
{/* Photo Preview */}
<div className="grid grid-cols-3 gap-2">
{photos.slice(0, 3).map((photo, idx) => (
<img
key={idx}
src={photo.url}
alt={photo.caption || 'Photo'}
className="w-full h-32 object-cover rounded"
/>
))}
</div>
{/* Caption */}
<div className="space-y-2">
<Label htmlFor="caption">Caption</Label>
<Textarea
id="caption"
value={caption}
onChange={(e) => setCaption(e.target.value)}
placeholder="Describe the photo..."
rows={3}
/>
</div>
{/* Credit */}
<div className="space-y-2">
<Label htmlFor="credit">Photo Credit</Label>
<Input
id="credit"
value={credit}
onChange={(e) => setCredit(e.target.value)}
placeholder="Photographer name"
/>
</div>
{/* Actions */}
<div className="flex gap-3 justify-end">
<Button type="button" variant="outline" onClick={onCancel} disabled={submitting}>
<X className="w-4 h-4 mr-2" />
Cancel
</Button>
<Button onClick={() => onSubmit(caption, credit)} disabled={submitting}>
<Save className="w-4 h-4 mr-2" />
{submitting ? 'Saving...' : 'Save Changes'}
</Button>
</div>
</div>
);
}