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",
|
"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": {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue