diff --git a/src/components/report/ReportForm.tsx b/src/components/report/ReportForm.tsx new file mode 100644 index 0000000..a76dad6 --- /dev/null +++ b/src/components/report/ReportForm.tsx @@ -0,0 +1,630 @@ +import { useState } from 'react'; +import { PhotoUpload, type PhotoMetadata } from './PhotoUpload'; +import { LocationSelector } from './LocationSelector'; +import type { LitterType, QuantityLevel, LocationSource, Coordinates } from '@/types'; + +interface EnvironmentalConcern { + type: string; + severity: 'low' | 'medium' | 'high' | 'critical'; + description: string; +} + +interface FormData { + photos: PhotoMetadata[]; + location: Coordinates | null; + locationSource: LocationSource | null; + locationAccuracy?: number; + litterType: LitterType | ''; + quantity: QuantityLevel | ''; + description: string; + environmentalConcerns: EnvironmentalConcern[]; +} + +interface FormErrors { + photos?: string; + location?: string; + litterType?: string; + quantity?: string; +} + +const LITTER_TYPES: { value: LitterType; label: string; description: string }[] = [ + { value: 'plastic', label: 'Plastic', description: 'Bottles, bags, packaging, microplastics' }, + { value: 'metal', label: 'Metal', description: 'Cans, foil, wire, metal fragments' }, + { value: 'glass', label: 'Glass', description: 'Bottles, broken glass, jars' }, + { value: 'organic', label: 'Organic', description: 'Food waste, natural debris' }, + { value: 'hazardous', label: 'Hazardous', description: 'Chemicals, batteries, medical waste' }, + { value: 'other', label: 'Other', description: 'Mixed or unclassified litter' }, +]; + +const QUANTITY_LEVELS: { value: QuantityLevel; label: string; description: string }[] = [ + { value: 'minimal', label: 'Minimal', description: '1-10 items, scattered' }, + { value: 'moderate', label: 'Moderate', description: '11-50 items, noticeable' }, + { value: 'significant', label: 'Significant', description: '51-200 items, concentrated' }, + { value: 'severe', label: 'Severe', description: '200+ items, heavily polluted' }, +]; + +const CONCERN_TYPES = [ + 'Wildlife hazard', + 'Water contamination', + 'Beach access blocked', + 'Sharp objects', + 'Chemical spill', + 'Other', +]; + +export function ReportForm() { + const [currentStep, setCurrentStep] = useState(1); + const [formData, setFormData] = useState({ + photos: [], + location: null, + locationSource: null, + litterType: '', + quantity: '', + description: '', + environmentalConcerns: [], + }); + const [errors, setErrors] = useState({}); + const [isSubmitting, setIsSubmitting] = useState(false); + + const totalSteps = 4; + + const handlePhotosChange = (photos: PhotoMetadata[]) => { + setFormData(prev => ({ ...prev, photos })); + if (photos.length > 0 && errors.photos) { + setErrors(prev => ({ ...prev, photos: undefined })); + } + }; + + const handleLocationChange = (location: Coordinates, source: LocationSource, accuracy?: number) => { + setFormData(prev => ({ + ...prev, + location, + locationSource: source, + locationAccuracy: accuracy, + })); + if (errors.location) { + setErrors(prev => ({ ...prev, location: undefined })); + } + }; + + const validateStep = (step: number): boolean => { + const newErrors: FormErrors = {}; + + if (step === 1) { + if (formData.photos.length === 0) { + newErrors.photos = 'At least one photo is required'; + } + } + + if (step === 2) { + if (!formData.location) { + newErrors.location = 'Location is required'; + } + } + + if (step === 3) { + if (!formData.litterType) { + newErrors.litterType = 'Please select a litter type'; + } + if (!formData.quantity) { + newErrors.quantity = 'Please select a quantity level'; + } + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleNext = () => { + if (validateStep(currentStep)) { + setCurrentStep(prev => Math.min(prev + 1, totalSteps)); + } + }; + + const handleBack = () => { + setCurrentStep(prev => Math.max(prev - 1, 1)); + }; + + const handleSubmit = async () => { + if (!validateStep(currentStep)) { + return; + } + + setIsSubmitting(true); + + try { + // TODO: Implement API submission in future task + console.log('Submitting report:', formData); + + // Simulate API call + await new Promise(resolve => setTimeout(resolve, 1500)); + + alert('Report submitted successfully!'); + + // Reset form + setFormData({ + photos: [], + location: null, + locationSource: null, + litterType: '', + quantity: '', + description: '', + environmentalConcerns: [], + }); + setCurrentStep(1); + } catch (error) { + console.error('Error submitting report:', error); + alert('Failed to submit report. Please try again.'); + } finally { + setIsSubmitting(false); + } + }; + + const addEnvironmentalConcern = () => { + setFormData(prev => ({ + ...prev, + environmentalConcerns: [ + ...prev.environmentalConcerns, + { type: '', severity: 'medium', description: '' }, + ], + })); + }; + + const updateEnvironmentalConcern = (index: number, field: keyof EnvironmentalConcern, value: string) => { + setFormData(prev => ({ + ...prev, + environmentalConcerns: prev.environmentalConcerns.map((concern, i) => + i === index ? { ...concern, [field]: value } : concern + ), + })); + }; + + const removeEnvironmentalConcern = (index: number) => { + setFormData(prev => ({ + ...prev, + environmentalConcerns: prev.environmentalConcerns.filter((_, i) => i !== index), + })); + }; + + return ( +
+ {/* Progress Indicator */} +
+
+ {[1, 2, 3, 4].map((step) => ( +
+
+
+ {step < currentStep ? ( + + + + ) : ( + step + )} +
+
+ {step === 1 && 'Photos'} + {step === 2 && 'Location'} + {step === 3 && 'Details'} + {step === 4 && 'Review'} +
+
+ {step < totalSteps && ( +
+ )} +
+ ))} +
+
+ + {/* Form Content */} +
+ {/* Step 1: Photos */} + {currentStep === 1 && ( +
+
+

Upload Photos

+

+ Take or upload photos of the litter. Photos with GPS data will auto-populate the location. +

+
+ + + + {errors.photos && ( +

{errors.photos}

+ )} +
+ )} + + {/* Step 2: Location */} + {currentStep === 2 && ( +
+
+

Select Location

+

+ Confirm or select the location where the litter was found. +

+
+ + + + {errors.location && ( +

{errors.location}

+ )} +
+ )} + + {/* Step 3: Litter Details */} + {currentStep === 3 && ( +
+
+

Litter Details

+

+ Provide information about the type and quantity of litter. +

+
+ + {/* Litter Type */} +
+ +
+ {LITTER_TYPES.map((type) => ( + + ))} +
+ {errors.litterType && ( +

{errors.litterType}

+ )} +
+ + {/* Quantity Level */} +
+ +
+ {QUANTITY_LEVELS.map((level, index) => ( + + ))} +
+ {errors.quantity && ( +

{errors.quantity}

+ )} +
+ + {/* Description */} +
+ +