This commit is contained in:
parent
c30ac8376b
commit
a696ba59a4
8 changed files with 83 additions and 57 deletions
|
|
@ -1288,6 +1288,11 @@ session with **no other memory of this project**. So:
|
|||
removes both `skatespots:last-location` and `skatespots:map-view` on
|
||||
account switch/logout and is exposed in Settings → Appearance; drafts are
|
||||
scoped by pubkey and `useFormDraft` debounces localStorage writes (400 ms).
|
||||
**Coordinates are mandatory and explicit (2026-11-02):** Add Spot now always
|
||||
shows `Coordinates *` with latitude/longitude inputs (`font-mono text-base`,
|
||||
`required`, `aria-invalid`), below the pin summary/map; Edit Spot shows the same
|
||||
larger fields and an inline `coordError`. Both still hard-block publish when
|
||||
the pair is missing or out of range and always write `lat`/`lon`/`g` tags.
|
||||
⚠️ **Remaining parity gap:** `EditSpotPage` still uses raw lat/lng + toast
|
||||
validation and does not yet share the map picker/draft/duplicate guard.
|
||||
- **Map Add entry points (2026-11-02):** every map Add Spot link now carries the
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ account is required.
|
|||
|
||||
- Forge: <https://git.skate-spots.org/skatespots/SkateSpots>
|
||||
- Latest release: <https://git.skate-spots.org/skatespots/SkateSpots/releases/latest>
|
||||
- Signed APK (1.7.85):
|
||||
<https://git.skate-spots.org/skatespots/SkateSpots/releases/download/v1.7.85/SkateSpots-1.7.85.apk>
|
||||
- Signed APK (1.7.86):
|
||||
<https://git.skate-spots.org/skatespots/SkateSpots/releases/download/v1.7.86/SkateSpots-1.7.86.apk>
|
||||
- SHA-256:
|
||||
`58d642b792e6382b2a01159d66155c9cf60970b1cd9c5710200685e40b2a0c95`
|
||||
`7890f29072f7df39241d99df94e9d17050752b22045579dee4190bbe005f0f0d`
|
||||
|
||||
The APK is signed with the same key as the Zapstore build. Installing an
|
||||
F-Droid-signed build over it would require uninstalling SkateSpots and losing
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "skate-spots",
|
||||
"version": "1.7.85",
|
||||
"version": "1.7.86",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "skate-spots",
|
||||
"version": "1.7.85",
|
||||
"version": "1.7.86",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@bitcoinerlab/secp256k1": "^1.2.0",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "skate-spots",
|
||||
"private": true,
|
||||
"version": "1.7.85",
|
||||
"version": "1.7.86",
|
||||
"license": "AGPL-3.0-only",
|
||||
"type": "module",
|
||||
"homepage": "https://skate-spots.org",
|
||||
|
|
|
|||
|
|
@ -98,10 +98,10 @@
|
|||
|
||||
<a
|
||||
class="download"
|
||||
href="https://git.skate-spots.org/skatespots/SkateSpots/releases/download/v1.7.85/SkateSpots-1.7.85.apk"
|
||||
href="https://git.skate-spots.org/skatespots/SkateSpots/releases/download/v1.7.86/SkateSpots-1.7.86.apk"
|
||||
rel="noopener"
|
||||
>
|
||||
<span>Download SkateSpots-1.7.85.apk · 60 MB</span>
|
||||
<span>Download SkateSpots-1.7.86.apk · 60 MB</span>
|
||||
<span aria-hidden="true">↓</span>
|
||||
</a>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,11 @@
|
|||
🛹 Spot coordinates are explicit and mandatory
|
||||
- Add Spot now shows a permanent "Coordinates *" row with latitude and longitude fields, even when the map is collapsed.
|
||||
- Edit Spot uses the same larger monospace coordinate fields, with an inline error if the pair is missing or invalid.
|
||||
- Neither form can publish or save without a valid latitude/longitude pair; the event always carries lat, lon and a geohash.
|
||||
- The pin summary shows the exact public coordinates in a larger, easier-to-check font.
|
||||
|
||||
---
|
||||
|
||||
🛹 Navigation: map first, voice in your language
|
||||
- The Directions map now takes most of the phone screen: 56vh normally and 60vh while following. Route details and transit itineraries stay in the scrollable section below.
|
||||
- Voice guidance follows the app language instead of the phone/OS language, and prefers an installed local voice for that language. This covers all 24 app locales; the system default voice is used only when a matching one is not installed.
|
||||
|
|
|
|||
|
|
@ -136,7 +136,6 @@ export default function CreateSpotPage() {
|
|||
const [errors, setErrors] = useState<FieldErrors>({});
|
||||
const [showMore, setShowMore] = useState(false);
|
||||
const [showLocationMap, setShowLocationMap] = useState(!initial.pin);
|
||||
const [showManualCoords, setShowManualCoords] = useState(false);
|
||||
// Duplicate guard: warn when a spot already exists right where you're pinning
|
||||
// one. This is the cheapest fix for clone spam and it points the contributor
|
||||
// at the existing pin instead.
|
||||
|
|
@ -663,6 +662,9 @@ export default function CreateSpotPage() {
|
|||
<p className="text-sm font-bold text-white">
|
||||
{location.trim() || `${lat.toFixed(5)}, ${lng.toFixed(5)}`}
|
||||
</p>
|
||||
<p className="font-mono text-base font-black tabular-nums text-white">
|
||||
{lat.toFixed(6)}, {lng.toFixed(6)}
|
||||
</p>
|
||||
<p className="text-[11px] text-gray-400">
|
||||
{t('spotUi.exactPinPublic')}
|
||||
</p>
|
||||
|
|
@ -717,37 +719,37 @@ export default function CreateSpotPage() {
|
|||
<p className="text-[11px] leading-snug text-amber-300/90">
|
||||
{t('spotUi.exactPinPublic')}
|
||||
</p>
|
||||
|
||||
<Collapsible open={showManualCoords} onOpenChange={setShowManualCoords}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 text-xs font-bold text-gray-400 hover:text-white transition-colors"
|
||||
>
|
||||
<ChevronDown className={cn('h-3.5 w-3.5 transition-transform', showManualCoords && 'rotate-180')} />
|
||||
{t('createSpot.manualCoords')}
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="pt-3">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input
|
||||
placeholder={t('createSpot.latitudePlaceholder')}
|
||||
value={latitude}
|
||||
onChange={(e) => { setLatitude(e.target.value); clearError('location'); }}
|
||||
inputMode="decimal"
|
||||
/>
|
||||
<Input
|
||||
placeholder={t('createSpot.longitudePlaceholder')}
|
||||
value={longitude}
|
||||
onChange={(e) => { setLongitude(e.target.value); clearError('location'); }}
|
||||
inputMode="decimal"
|
||||
/>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Coordinates are deliberately visible and editable on BOTH
|
||||
add and edit. A spot cannot be published without a valid pair. */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-base font-bold">{t('createSpot.coordinates')} *</Label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Input
|
||||
aria-label={t('createSpot.latitudePlaceholder')}
|
||||
placeholder={t('createSpot.latitudePlaceholder')}
|
||||
value={latitude}
|
||||
onChange={(e) => { setLatitude(e.target.value); clearError('location'); }}
|
||||
inputMode="decimal"
|
||||
required
|
||||
aria-invalid={!hasCoords}
|
||||
className="font-mono text-base"
|
||||
/>
|
||||
<Input
|
||||
aria-label={t('createSpot.longitudePlaceholder')}
|
||||
placeholder={t('createSpot.longitudePlaceholder')}
|
||||
value={longitude}
|
||||
onChange={(e) => { setLongitude(e.target.value); clearError('location'); }}
|
||||
inputMode="decimal"
|
||||
required
|
||||
aria-invalid={!hasCoords}
|
||||
className="font-mono text-base"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{errors.location && <p className="text-xs font-medium text-red-400">{errors.location}</p>}
|
||||
</section>
|
||||
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ export default function EditSpotPage() {
|
|||
const [shelter, setShelter] = useState<ShelterType>(DEFAULT_SHELTER);
|
||||
const [contactInfo, setContactInfo] = useState('');
|
||||
const [images, setImages] = useState<string[]>([]);
|
||||
const [coordError, setCoordError] = useState<string | null>(null);
|
||||
const [isGettingLocation, setIsGettingLocation] = useState(false);
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
|
|
@ -115,6 +116,7 @@ export default function EditSpotPage() {
|
|||
setLongitude(lng.toFixed(6));
|
||||
}
|
||||
|
||||
setCoordError(null);
|
||||
setInitialized(true);
|
||||
}
|
||||
}, [spot, initialized]);
|
||||
|
|
@ -138,6 +140,7 @@ export default function EditSpotPage() {
|
|||
(position) => {
|
||||
setLatitude(position.coords.latitude.toFixed(6));
|
||||
setLongitude(position.coords.longitude.toFixed(6));
|
||||
setCoordError(null);
|
||||
setIsGettingLocation(false);
|
||||
toast({
|
||||
title: t('spotUi.locationFound'),
|
||||
|
|
@ -202,7 +205,8 @@ export default function EditSpotPage() {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!latitude || !longitude) {
|
||||
if (!latitude.trim() || !longitude.trim()) {
|
||||
setCoordError(t('spotUi.locationRequiredDesc'));
|
||||
toast({
|
||||
title: t('spotUi.locationRequired'),
|
||||
description: t('spotUi.locationRequiredDesc'),
|
||||
|
|
@ -215,6 +219,7 @@ export default function EditSpotPage() {
|
|||
const lng = parseFloat(longitude);
|
||||
|
||||
if (isNaN(lat) || isNaN(lng) || lat < -90 || lat > 90 || lng < -180 || lng > 180) {
|
||||
setCoordError(t('spotUi.invalidCoordinatesDesc'));
|
||||
toast({
|
||||
title: t('spotUi.invalidCoordinates'),
|
||||
description: t('spotUi.invalidCoordinatesDesc'),
|
||||
|
|
@ -222,6 +227,7 @@ export default function EditSpotPage() {
|
|||
});
|
||||
return;
|
||||
}
|
||||
setCoordError(null);
|
||||
|
||||
if (!selectedCategory) {
|
||||
toast({
|
||||
|
|
@ -417,27 +423,32 @@ export default function EditSpotPage() {
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* Coordinates */}
|
||||
{/* Coordinates — mandatory, visible and large enough to verify on a phone. */}
|
||||
<div className="space-y-2">
|
||||
<Label>{t('createSpot.coordinates')} *</Label>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Input
|
||||
placeholder={t('createSpot.latitudePlaceholder')}
|
||||
value={latitude}
|
||||
onChange={(e) => setLatitude(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
placeholder={t('createSpot.longitudePlaceholder')}
|
||||
value={longitude}
|
||||
onChange={(e) => setLongitude(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Label className="text-base font-bold">{t('createSpot.coordinates')} *</Label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Input
|
||||
aria-label={t('createSpot.latitudePlaceholder')}
|
||||
placeholder={t('createSpot.latitudePlaceholder')}
|
||||
value={latitude}
|
||||
onChange={(e) => { setLatitude(e.target.value); setCoordError(null); }}
|
||||
inputMode="decimal"
|
||||
required
|
||||
aria-invalid={!!coordError}
|
||||
className="font-mono text-base"
|
||||
/>
|
||||
<Input
|
||||
aria-label={t('createSpot.longitudePlaceholder')}
|
||||
placeholder={t('createSpot.longitudePlaceholder')}
|
||||
value={longitude}
|
||||
onChange={(e) => { setLongitude(e.target.value); setCoordError(null); }}
|
||||
inputMode="decimal"
|
||||
required
|
||||
aria-invalid={!!coordError}
|
||||
className="font-mono text-base"
|
||||
/>
|
||||
</div>
|
||||
{coordError && <p className="text-xs font-medium text-red-400">{coordError}</p>}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
|
|
|||
Loading…
Reference in a new issue