feat: implement report submission with photo upload and map display

- Fix Prisma client generation and database schema sync issues
- Implement user auto-creation from Clerk authentication
- Add raw SQL queries to handle MySQL POINT spatial fields
- Make password_hash optional for Clerk-only authentication
- Update MapPage to fetch real reports from API
- Add photo display in map popups with full-size modal view
- Configure CORS and Helmet for cross-origin image loading
- Add cleanup tracking task to implementation plan

Fixes report submission 500 errors and enables end-to-end workflow from submission to map display.
This commit is contained in:
2025-11-16 20:01:26 +00:00
parent a58e8da0e1
commit 64fe312f01
7 changed files with 180 additions and 39 deletions

View File

@@ -185,8 +185,22 @@ export function MapView({
const marker = L.marker([report.latitude, report.longitude], { icon });
// Add popup with report details
const photoUrl = report.photoUrls && report.photoUrls.length > 0 ? report.photoUrls[0] : null;
// Remove /api from the base URL for uploads since they're served at the root level
const serverBaseUrl = (import.meta.env.VITE_API_BASE_URL || 'http://localhost:3005/api').replace('/api', '');
const fullPhotoUrl = photoUrl ? (photoUrl.startsWith('http') ? photoUrl : `${serverBaseUrl}${photoUrl}`) : null;
marker.bindPopup(`
<div class="p-2">
<div class="p-2 max-w-xs">
${fullPhotoUrl ? `
<img
src="${fullPhotoUrl}"
alt="Litter photo"
class="w-full h-32 object-cover rounded mb-2 cursor-pointer hover:opacity-80 transition-opacity"
onclick="window.openImageModal('${fullPhotoUrl}')"
onerror="this.style.display='none'"
/>
` : ''}
<h3 class="font-bold text-sm mb-1">${getLitterTypeLabel(report.litterType)}</h3>
<p class="text-xs text-gray-600">Quantity: ${report.quantity}</p>
<p class="text-xs text-gray-600">Status: ${report.verificationStatus}</p>

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react';
import { MapView, MapFilters } from '@/components/map';
import { analyticsService } from '@/services/analyticsService';
import { reportService } from '@/services/reportService';
import type { Report, FilterState, HotspotWithSeverity } from '@/types';
/**
@@ -14,8 +15,40 @@ export function MapPage() {
const [showHotspots, setShowHotspots] = useState(false);
const [hotspots, setHotspots] = useState<HotspotWithSeverity[]>([]);
const [isLoadingHotspots, setIsLoadingHotspots] = useState(false);
const [reports, setReports] = useState<Report[]>([]);
const [isLoadingReports, setIsLoadingReports] = useState(true);
const [modalImageUrl, setModalImageUrl] = useState<string | null>(null);
// Sample data for demonstration (will be replaced with API data in later tasks)
// Fetch reports from API
useEffect(() => {
const fetchReports = async () => {
try {
setIsLoadingReports(true);
const data = await reportService.getReports(filters);
setReports(data);
} catch (error) {
console.error('Failed to fetch reports:', error);
// Fall back to sample data on error
setReports(sampleReports);
} finally {
setIsLoadingReports(false);
}
};
fetchReports();
}, [filters]);
// Set up global function for opening image modal
useEffect(() => {
(window as any).openImageModal = (url: string) => {
setModalImageUrl(url);
};
return () => {
delete (window as any).openImageModal;
};
}, []);
// Sample data for fallback
const sampleReports: Report[] = [
{
id: '1',
@@ -196,16 +229,25 @@ export function MapPage() {
<div className="flex-1 p-4">
<div className="h-full bg-white rounded-lg shadow-lg overflow-hidden relative">
<MapView
reports={sampleReports}
hotspots={hotspots}
filters={filters}
showClusters={showClusters}
showHeatmap={showHeatmap}
showHotspots={showHotspots}
onMarkerClick={handleMarkerClick}
onMapClick={handleMapClick}
/>
{isLoadingReports ? (
<div className="absolute inset-0 flex items-center justify-center bg-gray-50">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading reports...</p>
</div>
</div>
) : (
<MapView
reports={reports}
hotspots={hotspots}
filters={filters}
showClusters={showClusters}
showHeatmap={showHeatmap}
showHotspots={showHotspots}
onMarkerClick={handleMarkerClick}
onMapClick={handleMapClick}
/>
)}
<MapFilters
filters={filters}
@@ -234,6 +276,29 @@ export function MapPage() {
</button>
</div>
)}
{/* Image Modal */}
{modalImageUrl && (
<div
className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-[2000] p-4"
onClick={() => setModalImageUrl(null)}
>
<div className="relative max-w-4xl max-h-full">
<button
onClick={() => setModalImageUrl(null)}
className="absolute -top-10 right-0 text-white hover:text-gray-300 text-2xl font-bold"
>
✕
</button>
<img
src={modalImageUrl}
alt="Full size litter photo"
className="max-w-full max-h-[90vh] object-contain rounded-lg"
onClick={(e) => e.stopPropagation()}
/>
</div>
</div>
)}
</div>
);
}