mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:31:12 -05:00
feat: Add error boundaries to submission queries
This commit is contained in:
43
src/components/error/SubmissionErrorBoundary.tsx
Normal file
43
src/components/error/SubmissionErrorBoundary.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { AlertCircle } from 'lucide-react';
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
import { ModerationErrorBoundary } from './ModerationErrorBoundary';
|
||||||
|
|
||||||
|
interface SubmissionErrorBoundaryProps {
|
||||||
|
children: ReactNode;
|
||||||
|
submissionId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightweight Error Boundary for Submission-Related Components
|
||||||
|
*
|
||||||
|
* Wraps ModerationErrorBoundary with a submission-specific fallback UI.
|
||||||
|
* Use this for any component that displays submission data.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```tsx
|
||||||
|
* <SubmissionErrorBoundary submissionId={id}>
|
||||||
|
* <SubmissionDetails />
|
||||||
|
* </SubmissionErrorBoundary>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function SubmissionErrorBoundary({
|
||||||
|
children,
|
||||||
|
submissionId
|
||||||
|
}: SubmissionErrorBoundaryProps) {
|
||||||
|
return (
|
||||||
|
<ModerationErrorBoundary
|
||||||
|
submissionId={submissionId}
|
||||||
|
fallback={
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
Failed to load submission data. Please try refreshing the page.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ModerationErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,3 +10,4 @@ export { AdminErrorBoundary } from './AdminErrorBoundary';
|
|||||||
export { EntityErrorBoundary } from './EntityErrorBoundary';
|
export { EntityErrorBoundary } from './EntityErrorBoundary';
|
||||||
export { RouteErrorBoundary } from './RouteErrorBoundary';
|
export { RouteErrorBoundary } from './RouteErrorBoundary';
|
||||||
export { ModerationErrorBoundary } from './ModerationErrorBoundary';
|
export { ModerationErrorBoundary } from './ModerationErrorBoundary';
|
||||||
|
export { SubmissionErrorBoundary } from './SubmissionErrorBoundary';
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import { Card, CardContent } from '@/components/ui/card';
|
|||||||
import { supabase } from '@/lib/supabaseClient';
|
import { supabase } from '@/lib/supabaseClient';
|
||||||
import { Image as ImageIcon } from 'lucide-react';
|
import { Image as ImageIcon } from 'lucide-react';
|
||||||
import { PhotoModal } from './PhotoModal';
|
import { PhotoModal } from './PhotoModal';
|
||||||
import { handleError } from '@/lib/errorHandler';
|
import { handleError, getErrorMessage } from '@/lib/errorHandler';
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
import { AlertCircle } from 'lucide-react';
|
||||||
|
|
||||||
interface EntityEditPreviewProps {
|
interface EntityEditPreviewProps {
|
||||||
submissionId: string;
|
submissionId: string;
|
||||||
@@ -68,6 +70,7 @@ interface SubmissionItemData {
|
|||||||
|
|
||||||
export const EntityEditPreview = ({ submissionId, entityType, entityName }: EntityEditPreviewProps) => {
|
export const EntityEditPreview = ({ submissionId, entityType, entityName }: EntityEditPreviewProps) => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [itemData, setItemData] = useState<Record<string, unknown> | null>(null);
|
const [itemData, setItemData] = useState<Record<string, unknown> | null>(null);
|
||||||
const [originalData, setOriginalData] = useState<Record<string, unknown> | null>(null);
|
const [originalData, setOriginalData] = useState<Record<string, unknown> | null>(null);
|
||||||
const [changedFields, setChangedFields] = useState<string[]>([]);
|
const [changedFields, setChangedFields] = useState<string[]>([]);
|
||||||
@@ -196,10 +199,12 @@ export const EntityEditPreview = ({ submissionId, entityType, entityName }: Enti
|
|||||||
setChangedFields(changed);
|
setChangedFields(changed);
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
const errorMsg = getErrorMessage(error);
|
||||||
handleError(error, {
|
handleError(error, {
|
||||||
action: 'Load Submission Preview',
|
action: 'Load Submission Preview',
|
||||||
metadata: { submissionId, entityType }
|
metadata: { submissionId, entityType }
|
||||||
});
|
});
|
||||||
|
setError(errorMsg);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -213,6 +218,17 @@ export const EntityEditPreview = ({ submissionId, entityType, entityName }: Enti
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
{error}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!itemData) {
|
if (!itemData) {
|
||||||
return (
|
return (
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import { ConflictResolutionModal } from './ConflictResolutionModal';
|
|||||||
import { EditHistoryAccordion } from './EditHistoryAccordion';
|
import { EditHistoryAccordion } from './EditHistoryAccordion';
|
||||||
import { validateMultipleItems, ValidationResult } from '@/lib/entityValidationSchemas';
|
import { validateMultipleItems, ValidationResult } from '@/lib/entityValidationSchemas';
|
||||||
import { logger } from '@/lib/logger';
|
import { logger } from '@/lib/logger';
|
||||||
|
import { ModerationErrorBoundary } from '@/components/error';
|
||||||
|
|
||||||
interface SubmissionReviewManagerProps {
|
interface SubmissionReviewManagerProps {
|
||||||
submissionId: string;
|
submissionId: string;
|
||||||
@@ -588,27 +589,29 @@ export function SubmissionReviewManager({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Container open={open} onOpenChange={onOpenChange}>
|
<Container open={open} onOpenChange={onOpenChange}>
|
||||||
{isMobile ? (
|
<ModerationErrorBoundary submissionId={submissionId}>
|
||||||
<SheetContent side="bottom" className="h-[90vh] overflow-y-auto">
|
{isMobile ? (
|
||||||
<SheetHeader>
|
<SheetContent side="bottom" className="h-[90vh] overflow-y-auto">
|
||||||
<SheetTitle>Review Submission</SheetTitle>
|
<SheetHeader>
|
||||||
<SheetDescription>
|
<SheetTitle>Review Submission</SheetTitle>
|
||||||
{pendingCount} pending item(s) • {selectedCount} selected
|
<SheetDescription>
|
||||||
</SheetDescription>
|
{pendingCount} pending item(s) • {selectedCount} selected
|
||||||
</SheetHeader>
|
</SheetDescription>
|
||||||
<ReviewContent />
|
</SheetHeader>
|
||||||
</SheetContent>
|
<ReviewContent />
|
||||||
) : (
|
</SheetContent>
|
||||||
<DialogContent className="max-w-5xl max-h-[90vh] overflow-y-auto">
|
) : (
|
||||||
<DialogHeader>
|
<DialogContent className="max-w-5xl max-h-[90vh] overflow-y-auto">
|
||||||
<DialogTitle>Review Submission</DialogTitle>
|
<DialogHeader>
|
||||||
<DialogDescription>
|
<DialogTitle>Review Submission</DialogTitle>
|
||||||
{pendingCount} pending item(s) • {selectedCount} selected
|
<DialogDescription>
|
||||||
</DialogDescription>
|
{pendingCount} pending item(s) • {selectedCount} selected
|
||||||
</DialogHeader>
|
</DialogDescription>
|
||||||
<ReviewContent />
|
</DialogHeader>
|
||||||
</DialogContent>
|
<ReviewContent />
|
||||||
)}
|
</DialogContent>
|
||||||
|
)}
|
||||||
|
</ModerationErrorBoundary>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
<ConflictResolutionDialog
|
<ConflictResolutionDialog
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { supabase } from '@/lib/supabaseClient';
|
import { supabase } from '@/lib/supabaseClient';
|
||||||
import { getErrorMessage } from '@/lib/errorHandler';
|
import { handleNonCriticalError, getErrorMessage } from '@/lib/errorHandler';
|
||||||
import type { PhotoSubmissionItem } from '@/types/photo-submissions';
|
import type { PhotoSubmissionItem } from '@/types/photo-submissions';
|
||||||
|
|
||||||
interface UsePhotoSubmissionItemsResult {
|
interface UsePhotoSubmissionItemsResult {
|
||||||
@@ -64,6 +64,10 @@ export function usePhotoSubmissionItems(
|
|||||||
setPhotos(data || []);
|
setPhotos(data || []);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMsg = getErrorMessage(error);
|
const errorMsg = getErrorMessage(error);
|
||||||
|
handleNonCriticalError(error, {
|
||||||
|
action: 'Fetch photo submission items',
|
||||||
|
metadata: { submissionId }
|
||||||
|
});
|
||||||
setError(errorMsg);
|
setError(errorMsg);
|
||||||
setPhotos([]);
|
setPhotos([]);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user