mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 02:11:14 -05:00
Refactor code structure and remove redundant changes
This commit is contained in:
400
src-old/components/moderation/EntityEditPreview.tsx
Normal file
400
src-old/components/moderation/EntityEditPreview.tsx
Normal file
@@ -0,0 +1,400 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { supabase } from '@/lib/supabaseClient';
|
||||
import { Image as ImageIcon } from 'lucide-react';
|
||||
import { PhotoModal } from './PhotoModal';
|
||||
import { handleError, getErrorMessage } from '@/lib/errorHandler';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
|
||||
interface EntityEditPreviewProps {
|
||||
submissionId: string;
|
||||
entityType: string;
|
||||
entityName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep equality check for detecting changes in nested objects/arrays
|
||||
*/
|
||||
const deepEqual = <T extends Record<string, unknown>>(a: T, b: T): boolean => {
|
||||
// Handle null/undefined cases
|
||||
if (a === b) return true;
|
||||
if (a == null || b == null) return false;
|
||||
if (typeof a !== typeof b) return false;
|
||||
|
||||
// Handle primitives and functions
|
||||
if (typeof a !== 'object') return a === b;
|
||||
|
||||
// Handle arrays
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
if (a.length !== b.length) return false;
|
||||
return a.every((item, index) => deepEqual(item as Record<string, unknown>, b[index] as Record<string, unknown>));
|
||||
}
|
||||
|
||||
// One is array, other is not
|
||||
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
||||
|
||||
// Handle objects
|
||||
const keysA = Object.keys(a);
|
||||
const keysB = Object.keys(b);
|
||||
|
||||
if (keysA.length !== keysB.length) return false;
|
||||
|
||||
return keysA.every(key => {
|
||||
const valueA = a[key];
|
||||
const valueB = b[key];
|
||||
|
||||
if (typeof valueA === 'object' && valueA !== null && typeof valueB === 'object' && valueB !== null) {
|
||||
return deepEqual(valueA as Record<string, unknown>, valueB as Record<string, unknown>);
|
||||
}
|
||||
|
||||
return valueA === valueB;
|
||||
});
|
||||
};
|
||||
|
||||
interface ImageAssignments {
|
||||
uploaded: Array<{
|
||||
url: string;
|
||||
cloudflare_id: string;
|
||||
}>;
|
||||
banner_assignment: number | null;
|
||||
card_assignment: number | null;
|
||||
}
|
||||
|
||||
interface SubmissionItemData {
|
||||
id: string;
|
||||
item_data: Record<string, unknown>;
|
||||
original_data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export const EntityEditPreview = ({ submissionId, entityType, entityName }: EntityEditPreviewProps) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [itemData, setItemData] = useState<Record<string, unknown> | null>(null);
|
||||
const [originalData, setOriginalData] = useState<Record<string, unknown> | null>(null);
|
||||
const [changedFields, setChangedFields] = useState<string[]>([]);
|
||||
const [bannerImageUrl, setBannerImageUrl] = useState<string | null>(null);
|
||||
const [cardImageUrl, setCardImageUrl] = useState<string | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
|
||||
const [isPhotoOperation, setIsPhotoOperation] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSubmissionItems();
|
||||
}, [submissionId]);
|
||||
|
||||
const fetchSubmissionItems = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Fetch items with relational data
|
||||
const { data: items, error } = await supabase
|
||||
.from('submission_items')
|
||||
.select(`
|
||||
*,
|
||||
park_submission:park_submissions!submission_items_park_submission_id_fkey(*),
|
||||
ride_submission:ride_submissions!submission_items_ride_submission_id_fkey(*),
|
||||
photo_submission:photo_submissions!submission_items_photo_submission_id_fkey(
|
||||
*,
|
||||
photo_items:photo_submission_items(*)
|
||||
)
|
||||
`)
|
||||
.eq('submission_id', submissionId)
|
||||
.order('order_index', { ascending: true });
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
if (items && items.length > 0) {
|
||||
const firstItem = items[0];
|
||||
|
||||
// Transform relational data to item_data format
|
||||
let itemDataObj: Record<string, unknown> = {};
|
||||
switch (firstItem.item_type) {
|
||||
case 'park':
|
||||
itemDataObj = (firstItem as any).park_submission || {};
|
||||
break;
|
||||
case 'ride':
|
||||
itemDataObj = (firstItem as any).ride_submission || {};
|
||||
break;
|
||||
case 'photo':
|
||||
itemDataObj = {
|
||||
...(firstItem as any).photo_submission,
|
||||
photos: (firstItem as any).photo_submission?.photo_items || []
|
||||
};
|
||||
break;
|
||||
default:
|
||||
itemDataObj = {};
|
||||
}
|
||||
|
||||
setItemData(itemDataObj);
|
||||
setOriginalData(null); // Original data not used in new relational model
|
||||
|
||||
// Check for photo edit/delete operations
|
||||
if (firstItem.item_type === 'photo_edit' || firstItem.item_type === 'photo_delete') {
|
||||
setIsPhotoOperation(true);
|
||||
if (firstItem.item_type === 'photo_edit') {
|
||||
setChangedFields(['caption']);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse changed fields
|
||||
const changed: string[] = [];
|
||||
const data = itemDataObj as Record<string, unknown>;
|
||||
|
||||
// Check for image changes
|
||||
if (data.images && typeof data.images === 'object') {
|
||||
const images = data.images as {
|
||||
uploaded?: Array<{ url: string; cloudflare_id: string }>;
|
||||
banner_assignment?: number | null;
|
||||
card_assignment?: number | null;
|
||||
};
|
||||
|
||||
// Safety check: verify uploaded array exists and is valid
|
||||
if (!images.uploaded || !Array.isArray(images.uploaded)) {
|
||||
// Invalid images data structure, skip image processing
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract banner image
|
||||
if (images.banner_assignment !== null && images.banner_assignment !== undefined) {
|
||||
// Safety check: verify index is within bounds
|
||||
if (images.banner_assignment >= 0 && images.banner_assignment < images.uploaded.length) {
|
||||
const bannerImg = images.uploaded[images.banner_assignment];
|
||||
// Validate nested image data
|
||||
if (bannerImg && bannerImg.url) {
|
||||
setBannerImageUrl(bannerImg.url);
|
||||
changed.push('banner_image');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract card image
|
||||
if (images.card_assignment !== null && images.card_assignment !== undefined) {
|
||||
// Safety check: verify index is within bounds
|
||||
if (images.card_assignment >= 0 && images.card_assignment < images.uploaded.length) {
|
||||
const cardImg = images.uploaded[images.card_assignment];
|
||||
// Validate nested image data
|
||||
if (cardImg && cardImg.url) {
|
||||
setCardImageUrl(cardImg.url);
|
||||
changed.push('card_image');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for other field changes by comparing with original_data
|
||||
// Note: In new relational model, we don't track original_data at item level
|
||||
// Field changes are determined by comparing current vs approved entity data
|
||||
if (itemDataObj) {
|
||||
const excludeFields = ['images', 'updated_at', 'created_at', 'id'];
|
||||
Object.keys(itemDataObj).forEach(key => {
|
||||
if (!excludeFields.includes(key) && itemDataObj[key] !== null && itemDataObj[key] !== undefined) {
|
||||
changed.push(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setChangedFields(changed);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const errorMsg = getErrorMessage(error);
|
||||
handleError(error, {
|
||||
action: 'Load Submission Preview',
|
||||
metadata: { submissionId, entityType }
|
||||
});
|
||||
setError(errorMsg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Loading preview...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
{error}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (!itemData) {
|
||||
return (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
No preview available
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle photo edit/delete operations
|
||||
if (isPhotoOperation) {
|
||||
const isEdit = changedFields.includes('caption');
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Badge variant="outline">
|
||||
Photo
|
||||
</Badge>
|
||||
<Badge variant={isEdit ? "secondary" : "destructive"}>
|
||||
{isEdit ? 'Edit' : 'Delete'}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{(itemData?.cloudflare_image_url && typeof itemData.cloudflare_image_url === 'string' && (
|
||||
<Card className="overflow-hidden">
|
||||
<CardContent className="p-2">
|
||||
<img
|
||||
src={itemData.cloudflare_image_url}
|
||||
alt="Photo to be modified"
|
||||
className="w-full h-32 object-cover rounded"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)) as React.ReactNode}
|
||||
|
||||
{isEdit && (
|
||||
<div className="space-y-2 text-sm">
|
||||
<div>
|
||||
<span className="font-medium">Old caption: </span>
|
||||
<span className="text-muted-foreground">
|
||||
{(originalData?.caption as string) || <em>No caption</em>}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">New caption: </span>
|
||||
<span className="text-muted-foreground">
|
||||
{(itemData?.new_caption as string) || <em>No caption</em>}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!isEdit && itemData?.reason && typeof itemData.reason === 'string' && (
|
||||
<div className="text-sm">
|
||||
<span className="font-medium">Reason: </span>
|
||||
<span className="text-muted-foreground">{itemData.reason}</span>
|
||||
</div>
|
||||
)) as React.ReactNode}
|
||||
|
||||
<div className="text-xs text-muted-foreground italic">
|
||||
Click "Review Items" for full details
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Build photos array for modal
|
||||
const photos: Array<{ id: string; url: string; caption: string | null }> = [];
|
||||
if (bannerImageUrl) {
|
||||
photos.push({
|
||||
id: 'banner',
|
||||
url: `${bannerImageUrl}`,
|
||||
caption: 'New Banner Image'
|
||||
});
|
||||
}
|
||||
if (cardImageUrl) {
|
||||
photos.push({
|
||||
id: 'card',
|
||||
url: `${cardImageUrl}`,
|
||||
caption: 'New Card Image'
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Badge variant="outline" className="capitalize">
|
||||
{entityType}
|
||||
</Badge>
|
||||
<Badge variant="secondary">
|
||||
Edit
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{entityName && (
|
||||
<div className="font-medium text-base">
|
||||
{entityName}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{changedFields.length > 0 && (
|
||||
<div className="text-sm">
|
||||
<span className="font-medium">Changed fields: </span>
|
||||
<span className="text-muted-foreground">
|
||||
{changedFields.map(field => field.replace(/_/g, ' ')).join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(bannerImageUrl || cardImageUrl) && (
|
||||
<div className="space-y-2">
|
||||
<div className="font-medium text-sm flex items-center gap-2">
|
||||
<ImageIcon className="w-4 h-4" />
|
||||
Image Changes:
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{bannerImageUrl && (
|
||||
<Card className="overflow-hidden cursor-pointer hover:ring-2 hover:ring-primary transition-all" onClick={() => {
|
||||
setSelectedImageIndex(0);
|
||||
setIsModalOpen(true);
|
||||
}}>
|
||||
<CardContent className="p-2">
|
||||
<img
|
||||
src={`${bannerImageUrl}`}
|
||||
alt="New banner"
|
||||
className="w-full h-24 object-cover rounded"
|
||||
/>
|
||||
<div className="text-xs text-center mt-1 text-muted-foreground">
|
||||
Banner
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{cardImageUrl && (
|
||||
<Card className="overflow-hidden cursor-pointer hover:ring-2 hover:ring-primary transition-all" onClick={() => {
|
||||
setSelectedImageIndex(bannerImageUrl ? 1 : 0);
|
||||
setIsModalOpen(true);
|
||||
}}>
|
||||
<CardContent className="p-2">
|
||||
<img
|
||||
src={`${cardImageUrl}`}
|
||||
alt="New card"
|
||||
className="w-full h-24 object-cover rounded"
|
||||
/>
|
||||
<div className="text-xs text-center mt-1 text-muted-foreground">
|
||||
Card
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-xs text-muted-foreground italic">
|
||||
Click "Review Items" for full details
|
||||
</div>
|
||||
|
||||
<PhotoModal
|
||||
photos={photos.map(photo => ({
|
||||
...photo,
|
||||
caption: photo.caption ?? undefined
|
||||
}))}
|
||||
initialIndex={selectedImageIndex}
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user