From 7732a56c4c8cfbe104e7b83f38b34fc4bcc401f8 Mon Sep 17 00:00:00 2001 From: roy-bme Date: Thu, 14 May 2026 18:13:25 +0100 Subject: [PATCH] Add business photo upload and map thumbnail support --- app/indaba/(ops)/map/page.tsx | 3 +- components/ops/Map/BulawayoMap.tsx | 2 + components/ops/Map/MapView.tsx | 72 ++++++++++++++++++++++++++++++ components/ops/Map/types.ts | 1 + 4 files changed, 77 insertions(+), 1 deletion(-) diff --git a/app/indaba/(ops)/map/page.tsx b/app/indaba/(ops)/map/page.tsx index 123d37c..b6bcbfe 100644 --- a/app/indaba/(ops)/map/page.tsx +++ b/app/indaba/(ops)/map/page.tsx @@ -64,7 +64,7 @@ export default async function MapPage() { supabase .from("businesses") .select( - "id, name, sector, zone_id, lat, lng, est_monthly_volume, notes, decision_maker_name, decision_maker_title, phone, email, linkedin, key_suppliers, key_customers, pain_points, zimx_fit_score, active, mapped_by, created_at", + "id, name, sector, zone_id, lat, lng, est_monthly_volume, notes, decision_maker_name, decision_maker_title, phone, email, linkedin, key_suppliers, key_customers, pain_points, zimx_fit_score, active, mapped_by, created_at, photos", ), supabase .from("supply_chain_links") @@ -137,6 +137,7 @@ export default async function MapPage() { active: Boolean(b.active ?? true), mapped_by: b.mapped_by ?? null, created_at: String(b.created_at ?? new Date().toISOString()), + photos: b.photos ?? [], })); const links: MapLink[] = linkRows.map((link) => { diff --git a/components/ops/Map/BulawayoMap.tsx b/components/ops/Map/BulawayoMap.tsx index 115bfac..3008236 100644 --- a/components/ops/Map/BulawayoMap.tsx +++ b/components/ops/Map/BulawayoMap.tsx @@ -255,11 +255,13 @@ export default function BulawayoMap({ b.notes && b.notes.length > 120 ? `${b.notes.slice(0, 120)}…` : b.notes ?? ""; + const firstPhoto = b.photos?.[0] ?? null; const popupHtml = `
${escapeHtml(b.name)}
${escapeHtml(b.sector)} · ${escapeHtml(zoneName)}
+ ${firstPhoto ? `Business photo` : `
📷 Tap to add photo
`}
$${volume.toLocaleString()}/mo
${notes ? `
${escapeHtml(notes)}
` : ""}
diff --git a/components/ops/Map/MapView.tsx b/components/ops/Map/MapView.tsx index 6afcd70..29d33ed 100644 --- a/components/ops/Map/MapView.tsx +++ b/components/ops/Map/MapView.tsx @@ -12,6 +12,7 @@ import Textarea from "@/components/ops/ui/Textarea"; import Button from "@/components/ops/ui/Button"; import { useToast } from "@/components/ui/Toast"; import { opsApiPost } from "@/lib/ops/api-client"; +import { createSupabaseBrowserClient } from "@/lib/supabase/client"; import AddBusinessDialog from "./AddBusinessDialog"; import LogInteractionModal from "@/components/ops/Interactions/LogInteractionModal"; @@ -66,6 +67,8 @@ export default function MapView({ const [selected, setSelected] = useState(null); const [movingPinId, setMovingPinId] = useState(null); const [editing, setEditing] = useState(false); + const [isUploadingPhoto, setIsUploadingPhoto] = useState(false); + const [uploadSuccess, setUploadSuccess] = useState(false); const [form, setForm] = useState>({}); const [interactionBusinessId, setInteractionBusinessId] = useState(null); const [showCandidates, setShowCandidates] = useState(true); @@ -207,6 +210,50 @@ export default function MapView({ }); } + async function handlePhotoUpload(file: File) { + if (!selected || isUploadingPhoto) return; + setIsUploadingPhoto(true); + const supabase = createSupabaseBrowserClient(); + const filePath = `${selected.id}/${Date.now()}_${file.name.replace(/\s+/g, "_")}`; + const { error: uploadError } = await supabase.storage + .from("business-photos") + .upload(filePath, file, { upsert: false }); + if (uploadError) { + toast.error("Upload failed. Please try again."); + setIsUploadingPhoto(false); + return; + } + const { data } = supabase.storage.from("business-photos").getPublicUrl(filePath); + const nextPhotos = [...(selected.photos ?? []), data.publicUrl]; + const { error: updateError } = await supabase + .from("businesses") + .update({ photos: nextPhotos }) + .eq("id", selected.id); + if (updateError) { + toast.error("Photo uploaded, but saving failed."); + setIsUploadingPhoto(false); + return; + } + setItems((prev) => prev.map((b) => (b.id === selected.id ? { ...b, photos: nextPhotos } : b))); + setSelected((prev) => (prev ? { ...prev, photos: nextPhotos } : prev)); + setUploadSuccess(true); + window.setTimeout(() => setUploadSuccess(false), 1200); + setIsUploadingPhoto(false); + } + + async function handleDeletePhoto(photoUrl: string) { + if (!selected) return; + const nextPhotos = (selected.photos ?? []).filter((url) => url !== photoUrl); + const supabase = createSupabaseBrowserClient(); + const { error } = await supabase.from("businesses").update({ photos: nextPhotos }).eq("id", selected.id); + if (error) { + toast.error("Could not delete photo."); + return; + } + setItems((prev) => prev.map((b) => (b.id === selected.id ? { ...b, photos: nextPhotos } : b))); + setSelected((prev) => (prev ? { ...prev, photos: nextPhotos } : prev)); + } + return (
@@ -373,6 +420,31 @@ export default function MapView({ ))}