diff --git a/.kiro/specs/beach-litter-mapping/tasks.md b/.kiro/specs/beach-litter-mapping/tasks.md index 2a9d1cb..b8a4828 100644 --- a/.kiro/specs/beach-litter-mapping/tasks.md +++ b/.kiro/specs/beach-litter-mapping/tasks.md @@ -66,7 +66,7 @@ - Create environmental concerns section - _Requirements: 1.6, 1.7, 9.1, 9.2_ -- [ ] 9. Implement report submission and display +- [x] 9. Implement report submission and display - Connect form to API endpoint for report creation - Handle submission success and error states - Create report detail modal/popup component diff --git a/src/App.tsx b/src/App.tsx index 900ec9c..96f33bc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -19,7 +19,8 @@ import { EventsPage, AnalyticsPage, PhotoUploadDemo, - LocationSelectorDemo + LocationSelectorDemo, + ReportDetailDemo } from './pages'; const queryClient = new QueryClient({ @@ -114,6 +115,14 @@ function App() { } /> + + + + } + /> diff --git a/src/components/report/ReportDetailModal.tsx b/src/components/report/ReportDetailModal.tsx new file mode 100644 index 0000000..f75e334 --- /dev/null +++ b/src/components/report/ReportDetailModal.tsx @@ -0,0 +1,386 @@ +// Modal component for displaying detailed report information +import { useState } from 'react'; +import type { ReportResponseDTO } from '@/types'; +import { format } from 'date-fns'; + +interface ReportDetailModalProps { + report: ReportResponseDTO; + isOpen: boolean; + onClose: () => void; + onVerify?: (reportId: string) => void; + onDispute?: (reportId: string) => void; +} + +export function ReportDetailModal({ report, isOpen, onClose, onVerify, onDispute }: ReportDetailModalProps) { + const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0); + const [showVerifyComment, setShowVerifyComment] = useState(false); + const [showDisputeComment, setShowDisputeComment] = useState(false); + const [comment, setComment] = useState(''); + + if (!isOpen) return null; + + const handleVerify = () => { + if (onVerify) { + onVerify(report.id); + setShowVerifyComment(false); + setComment(''); + } + }; + + const handleDispute = () => { + if (onDispute) { + onDispute(report.id); + setShowDisputeComment(false); + setComment(''); + } + }; + + const nextPhoto = () => { + setCurrentPhotoIndex((prev) => (prev + 1) % report.photoUrls.length); + }; + + const prevPhoto = () => { + setCurrentPhotoIndex((prev) => (prev - 1 + report.photoUrls.length) % report.photoUrls.length); + }; + + const getVerificationStatusColor = (status: string) => { + switch (status) { + case 'verified': + return 'bg-green-100 text-green-800 border-green-200'; + case 'disputed': + return 'bg-red-100 text-red-800 border-red-200'; + default: + return 'bg-yellow-100 text-yellow-800 border-yellow-200'; + } + }; + + const getVerificationStatusIcon = (status: string) => { + switch (status) { + case 'verified': + return ( + + + + ); + case 'disputed': + return ( + + + + ); + default: + return ( + + + + ); + } + }; + + const getLitterTypeLabel = (type: string) => { + const labels: Record = { + plastic: 'Plastic', + metal: 'Metal', + glass: 'Glass', + organic: 'Organic', + hazardous: 'Hazardous', + other: 'Other', + }; + return labels[type] || type; + }; + + const getQuantityLabel = (quantity: string) => { + const labels: Record = { + minimal: 'Minimal (1-10 items)', + moderate: 'Moderate (11-50 items)', + significant: 'Significant (51-200 items)', + severe: 'Severe (200+ items)', + }; + return labels[quantity] || quantity; + }; + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal */} +
+
+ {/* Close Button */} + + + {/* Photo Gallery */} +
+ {`Report + + {/* Photo Navigation */} + {report.photoUrls.length > 1 && ( + <> + + + + {/* Photo Counter */} +
+ {currentPhotoIndex + 1} / {report.photoUrls.length} +
+ + )} +
+ + {/* Content */} +
+ {/* Header with Verification Status */} +
+
+

Litter Report

+

+ Reported {format(new Date(report.createdAt), 'PPP')} at {format(new Date(report.createdAt), 'p')} +

+
+
+ {getVerificationStatusIcon(report.verificationStatus)} + {report.verificationStatus} +
+
+ + {/* Reporter Info */} +
+ {report.user.avatarUrl ? ( + {report.user.username} + ) : ( +
+ {report.user.username.charAt(0).toUpperCase()} +
+ )} +
+

{report.user.username}

+

Reporter

+
+
+ + {/* Litter Details */} +
+
+

Litter Type

+

{getLitterTypeLabel(report.litterType)}

+
+
+

Quantity

+

{getQuantityLabel(report.quantity)}

+
+
+ + {/* Location */} +
+

Location

+
+

+ {report.latitude.toFixed(6)}, {report.longitude.toFixed(6)} +

+

+ Source: {report.locationSource === 'exif' ? 'Photo EXIF data' : + report.locationSource === 'gps' ? 'Device GPS' : 'Manual selection'} +

+
+
+ + {/* Description */} + {report.description && ( +
+

Description

+

{report.description}

+
+ )} + + {/* Photo Timestamp */} + {report.photoTimestamp && ( +
+

Photo Taken

+

+ {format(new Date(report.photoTimestamp), 'PPP')} at {format(new Date(report.photoTimestamp), 'p')} +

+
+ )} + + {/* Environmental Concerns */} + {report.environmentalConcerns && report.environmentalConcerns.length > 0 && ( +
+

Environmental Concerns

+
+ {report.environmentalConcerns.map((concern) => ( +
+
+ {concern.concernType} + + {concern.severity} + +
+

{concern.description}

+
+ ))} +
+
+ )} + + {/* Verification Stats */} +
+
+ + + + + {report.verificationCount} verifications + +
+
+ + + + + {report.disputeCount} disputes + +
+
+ + {/* Action Buttons */} + {(onVerify || onDispute) && ( +
+

+ Help maintain data quality by verifying or disputing this report. +

+ + {!showVerifyComment && !showDisputeComment && ( +
+ {onVerify && ( + + )} + {onDispute && ( + + )} +
+ )} + + {/* Verify Comment Form */} + {showVerifyComment && ( +
+