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:
parent
7395346228
commit
7aad972099
3 changed files with 68 additions and 27 deletions
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue