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",
"private": true,
"version": "1.0.75",
"version": "1.0.76",
"type": "module",
"homepage": "https://skate-spots.com",
"scripts": {

View file

@ -61,6 +61,9 @@ export default function CreateSpotPage() {
const [images, setImages] = useState<string[]>([]);
const [isGettingLocation, setIsGettingLocation] = 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)
useEffect(() => {
@ -74,12 +77,44 @@ export default function CreateSpotPage() {
// Check for quick capture image from map page
const quickCapture = searchParams.get('quickCapture');
if (quickCapture === 'true') {
// Backward-compat: an already-uploaded URL (old flow).
const capturedImage = sessionStorage.getItem('quickCaptureImage');
if (capturedImage) {
setImages([capturedImage]);
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]);
const handleCategoryChange = (category: string) => {
@ -600,6 +635,13 @@ export default function CreateSpotPage() {
</button>
</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 */}
<button
type="button"

View file

@ -78,7 +78,6 @@ import {
MessageCircle,
} from 'lucide-react';
import { SpotChat } from '@/components/SpotChat';
import { useUploadFile } from '@/hooks/useUploadFile';
import { useToast } from '@/hooks/useToast';
import { cn } from '@/lib/utils';
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
// to a file input hinting the rear camera. A single photo is all a spot needs.
const quickSnapInputRef = useRef<HTMLInputElement>(null);
const { mutateAsync: uploadFile, isPending: isUploadingQuickPhoto } = useUploadFile();
const { toast } = useToast();
// BTC Map center for fetching places
@ -799,33 +797,34 @@ export default function MapPage() {
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) => {
try {
const tags = await uploadFile(file);
const url = tags[0]?.[1];
if (url) {
// Store the captured image URL in sessionStorage for the create spot page
sessionStorage.setItem('quickCaptureImage', url);
// Get current location (native-aware) and navigate to create spot.
try {
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',
// Stash the captured photo as a data URL so the Create Spot page can pick
// it up and upload it in the right context.
const dataUrl = await new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = () => reject(new Error('Could not read photo'));
reader.readAsDataURL(file);
});
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');
}
};