diff --git a/src/pages/CreateSpotPage.tsx b/src/pages/CreateSpotPage.tsx index 9bbe2cd..8d84630 100644 --- a/src/pages/CreateSpotPage.tsx +++ b/src/pages/CreateSpotPage.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useCallback } from 'react'; import { useSeoMeta } from '@unhead/react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import ngeohash from 'ngeohash'; @@ -101,7 +101,7 @@ export default function CreateSpotPage() { // Duplicate guard: warn when a spot already exists right where you're pinning // one. This is the cheapest fix for clone spam (five near-identical pins for // the same spot) — and it points the contributor at the existing pin instead. - const { data: existingSpots } = useSkateSpots(500); + const { data: existingSpots } = useSkateSpots(2000); const [dupWarning, setDupWarning] = useState | null>(null); const dupConfirmedRef = useRef(false); const [isGettingLocation, setIsGettingLocation] = useState(false); @@ -120,6 +120,42 @@ export default function CreateSpotPage() { const clearError = (field: keyof FieldErrors) => setErrors((prev) => (prev[field] ? { ...prev, [field]: undefined } : prev)); + // Spots within 60 m of a point — used both for the live warning (shown as soon + // as the pin lands) and the final guard on submit. + const findNearby = useCallback( + (la: number, ln: number) => + (existingSpots ?? []) + .map((s) => { + if (!s.geohash) return null; + const { latitude, longitude } = ngeohash.decode(s.geohash); + if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return null; + return { spot: s, km: calculateDistance(la, ln, latitude, longitude) }; + }) + .filter((x): x is { spot: SkateSpot; km: number } => !!x && x.km < 0.06) + .sort((a, b) => a.km - b.km) + .slice(0, 3), + [existingSpots], + ); + + // Warn as soon as the pin lands (debounced), not only when you hit submit. + useEffect(() => { + if (!hasCoords) { + setDupWarning(null); + return; + } + const timer = window.setTimeout(() => { + if (dupConfirmedRef.current) return; + const near = findNearby(lat, lng); + setDupWarning(near.length > 0 ? near : null); + }, 400); + return () => window.clearTimeout(timer); + }, [hasCoords, lat, lng, findNearby]); + + // Moving the pin re-arms the "publish anyway" confirmation. + useEffect(() => { + dupConfirmedRef.current = false; + }, [lat, lng]); + const handlePick = (c: LatLng) => { setLatitude(c.lat.toFixed(6)); setLongitude(c.lng.toFixed(6)); @@ -325,16 +361,7 @@ export default function CreateSpotPage() { // Look for an existing spot within 60 m and ask before creating a clone. if (!dupConfirmedRef.current) { - const nearby = (existingSpots ?? []) - .map((s) => { - if (!s.geohash) return null; - const { latitude, longitude } = ngeohash.decode(s.geohash); - if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return null; - return { spot: s, km: calculateDistance(lat, lng, latitude, longitude) }; - }) - .filter((x): x is { spot: SkateSpot; km: number } => !!x && x.km < 0.06) - .sort((a, b) => a.km - b.km) - .slice(0, 3); + const nearby = findNearby(lat, lng); if (nearby.length > 0) { setDupWarning(nearby); @@ -575,6 +602,54 @@ export default function CreateSpotPage() { + {/* Duplicate warning — appears as soon as the pin lands, right + below the map, instead of only on submit. */} + {dupWarning && ( +
+

+ {t('spotUi.spotMayAlreadyExist')} +

+

+ {dupWarning.length === 1 + ? t('spotUi.duplicateOne') + : t('spotUi.duplicateOther')} +

+ +
+ + +
+
+ )} + {/* ── Title ─────────────────────────────────────────────── */}
@@ -882,53 +957,6 @@ export default function CreateSpotPage() { hasSpotType={!!selectedCategory} /> - {dupWarning && ( -
-

- {t('spotUi.spotMayAlreadyExist')} -

-

- {dupWarning.length === 1 - ? t('spotUi.duplicateOne') - : t('spotUi.duplicateOther')} -

-
    - {dupWarning.map(({ spot, km }) => ( -
  • - - {spot.title || t('spotUi.untitledSpot')} · {t('spotUi.metresAway', { distance: Math.round(km * 1000) })} - -
  • - ))} -
-
- - -
-
- )} - {/* Submit */}