Quick Snap: navigate to Add Spot immediately, upload photo on the form (v1.0.76)

Bug: after taking a Quick Snap photo and choosing "keep", the user never
reached the Add Spot form. handleQuickCameraCapture uploaded the photo to
Blossom FIRST (which needs an Amber signature and can be slow/fail) and only
navigated on success — so a stalled upload stranded the user right after the
camera.

Fix: Quick Snap now prioritizes speed —
- MapPage stashes the raw photo as a data URL (quickCapturePendingPhoto),
  grabs location (non-blocking), and navigates to /create-spot immediately.
- CreateSpotPage reads the pending photo on mount, converts it back to a File,
  and uploads it in the background with an "Uploading photo…" tile, adding the
  resulting URL to the images list. A failed upload now shows a toast on the
  form instead of blocking navigation. Old quickCaptureImage (already-uploaded
  URL) path kept for backward-compat.
- Removed the now-unused useUploadFile from MapPage.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-07-24 21:55:15 +07:00
parent 7395346228
commit 7aad972099
3 changed files with 68 additions and 27 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "skate-spots", "name": "skate-spots",
"private": true, "private": true,
"version": "1.0.75", "version": "1.0.76",
"type": "module", "type": "module",
"homepage": "https://skate-spots.com", "homepage": "https://skate-spots.com",
"scripts": { "scripts": {

View file

@ -61,6 +61,9 @@ export default function CreateSpotPage() {
const [images, setImages] = useState<string[]>([]); const [images, setImages] = useState<string[]>([]);
const [isGettingLocation, setIsGettingLocation] = useState(false); const [isGettingLocation, setIsGettingLocation] = useState(false);
const [showCamera, setShowCamera] = useState(false); const [showCamera, setShowCamera] = useState(false);
// True while the Quick Snap photo is uploading in the background after landing
// on this form.
const [isUploadingQuickPhoto, setIsUploadingQuickPhoto] = useState(false);
// Pre-fill coordinates from URL params (from map click) // Pre-fill coordinates from URL params (from map click)
useEffect(() => { useEffect(() => {
@ -74,12 +77,44 @@ export default function CreateSpotPage() {
// Check for quick capture image from map page // Check for quick capture image from map page
const quickCapture = searchParams.get('quickCapture'); const quickCapture = searchParams.get('quickCapture');
if (quickCapture === 'true') { if (quickCapture === 'true') {
// Backward-compat: an already-uploaded URL (old flow).
const capturedImage = sessionStorage.getItem('quickCaptureImage'); const capturedImage = sessionStorage.getItem('quickCaptureImage');
if (capturedImage) { if (capturedImage) {
setImages([capturedImage]); setImages([capturedImage]);
sessionStorage.removeItem('quickCaptureImage'); sessionStorage.removeItem('quickCaptureImage');
} }
// New Quick Snap flow: a raw photo (data URL) stashed by the map page.
// We navigate here immediately for speed, then upload the photo HERE so a
// slow/failed Blossom upload never strands the user before the form.
const pendingPhoto = sessionStorage.getItem('quickCapturePendingPhoto');
if (pendingPhoto) {
sessionStorage.removeItem('quickCapturePendingPhoto');
(async () => {
setIsUploadingQuickPhoto(true);
try {
const res = await fetch(pendingPhoto);
const blob = await res.blob();
const file = new File([blob], `skate-spot-${Date.now()}.jpg`, { type: 'image/jpeg' });
const uploadTags = await uploadFile(file);
const url = uploadTags[0]?.[1];
if (url) {
setImages((prev) => (prev.includes(url) ? prev : [...prev, url]));
}
} catch (error) {
toast({
title: 'Photo upload failed',
description:
error instanceof Error ? error.message : 'Could not upload the photo. You can add one below.',
variant: 'destructive',
});
} finally {
setIsUploadingQuickPhoto(false);
}
})();
}
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams]); }, [searchParams]);
const handleCategoryChange = (category: string) => { const handleCategoryChange = (category: string) => {
@ -600,6 +635,13 @@ export default function CreateSpotPage() {
</button> </button>
</div> </div>
))} ))}
{/* Quick Snap photo uploading in the background */}
{isUploadingQuickPhoto && (
<div className="relative aspect-video rounded-lg overflow-hidden bg-muted flex flex-col items-center justify-center border-2 border-dashed border-[#ccff00]/50">
<Loader2 className="h-6 w-6 animate-spin text-[#ccff00] mb-1" />
<span className="text-xs text-[#ccff00] font-bold">Uploading photo…</span>
</div>
)}
{/* Camera capture button */} {/* Camera capture button */}
<button <button
type="button" type="button"

View file

@ -78,7 +78,6 @@ import {
MessageCircle, MessageCircle,
} from 'lucide-react'; } from 'lucide-react';
import { SpotChat } from '@/components/SpotChat'; import { SpotChat } from '@/components/SpotChat';
import { useUploadFile } from '@/hooks/useUploadFile';
import { useToast } from '@/hooks/useToast'; import { useToast } from '@/hooks/useToast';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { isNativeCapture, captureFromNativeCamera } from '@/lib/quickCapture'; import { isNativeCapture, captureFromNativeCamera } from '@/lib/quickCapture';
@ -611,7 +610,6 @@ export default function MapPage() {
// On web there's no reliable way to force the camera from JS, so we fall back // On web there's no reliable way to force the camera from JS, so we fall back
// to a file input hinting the rear camera. A single photo is all a spot needs. // to a file input hinting the rear camera. A single photo is all a spot needs.
const quickSnapInputRef = useRef<HTMLInputElement>(null); const quickSnapInputRef = useRef<HTMLInputElement>(null);
const { mutateAsync: uploadFile, isPending: isUploadingQuickPhoto } = useUploadFile();
const { toast } = useToast(); const { toast } = useToast();
// BTC Map center for fetching places // BTC Map center for fetching places
@ -799,33 +797,34 @@ export default function MapPage() {
if (file) handleQuickCameraCapture(file); if (file) handleQuickCameraCapture(file);
}; };
// Handle quick camera capture - upload and navigate to create spot // Handle quick camera capture — the whole point of Quick Snap is SPEED, so we
// do NOT block on the Blossom upload here (that needs an Amber signature and
// can be slow or fail — which previously left the user stranded after taking
// the photo). Instead: stash the raw photo, grab location, and navigate to the
// Add Spot form IMMEDIATELY. The form uploads the photo there.
const handleQuickCameraCapture = async (file: File) => { const handleQuickCameraCapture = async (file: File) => {
try { try {
const tags = await uploadFile(file); // Stash the captured photo as a data URL so the Create Spot page can pick
const url = tags[0]?.[1]; // it up and upload it in the right context.
if (url) { const dataUrl = await new Promise<string>((resolve, reject) => {
// Store the captured image URL in sessionStorage for the create spot page const reader = new FileReader();
sessionStorage.setItem('quickCaptureImage', url); reader.onload = () => resolve(reader.result as string);
// Get current location (native-aware) and navigate to create spot. reader.onerror = () => reject(new Error('Could not read photo'));
try { reader.readAsDataURL(file);
const { lat, lng } = await getCurrentPosition();
navigate(`/create-spot?lat=${lat.toFixed(6)}&lng=${lng.toFixed(6)}&quickCapture=true`);
} catch {
// If geolocation fails, still navigate but without coords
navigate('/create-spot?quickCapture=true');
}
toast({
title: 'Photo captured!',
description: 'Now add the spot details.',
});
}
} catch (error) {
toast({
title: 'Upload failed',
description: error instanceof Error ? error.message : 'Failed to upload photo',
variant: 'destructive',
}); });
sessionStorage.setItem('quickCapturePendingPhoto', dataUrl);
// Clear any older uploaded-URL key so the form doesn't use a stale image.
sessionStorage.removeItem('quickCaptureImage');
} catch (err) {
console.warn('[QuickSnap] could not stash photo:', err);
}
// Try to grab location quickly, but never let it block navigation.
try {
const { lat, lng } = await getCurrentPosition();
navigate(`/create-spot?lat=${lat.toFixed(6)}&lng=${lng.toFixed(6)}&quickCapture=true`);
} catch {
navigate('/create-spot?quickCapture=true');
} }
}; };