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
|
removes both `skatespots:last-location` and `skatespots:map-view` on
|
||||||
account switch/logout and is exposed in Settings → Appearance; drafts are
|
account switch/logout and is exposed in Settings → Appearance; drafts are
|
||||||
scoped by pubkey and `useFormDraft` debounces localStorage writes (400 ms).
|
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
|
⚠️ **Remaining parity gap:** `EditSpotPage` still uses raw lat/lng + toast
|
||||||
validation and does not yet share the map picker/draft/duplicate guard.
|
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
|
- **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>
|
- Forge: <https://git.skate-spots.org/skatespots/SkateSpots>
|
||||||
- Latest release: <https://git.skate-spots.org/skatespots/SkateSpots/releases/latest>
|
- Latest release: <https://git.skate-spots.org/skatespots/SkateSpots/releases/latest>
|
||||||
- Signed APK (1.7.85):
|
- Signed APK (1.7.86):
|
||||||
<https://git.skate-spots.org/skatespots/SkateSpots/releases/download/v1.7.85/SkateSpots-1.7.85.apk>
|
<https://git.skate-spots.org/skatespots/SkateSpots/releases/download/v1.7.86/SkateSpots-1.7.86.apk>
|
||||||
- SHA-256:
|
- SHA-256:
|
||||||
`58d642b792e6382b2a01159d66155c9cf60970b1cd9c5710200685e40b2a0c95`
|
`7890f29072f7df39241d99df94e9d17050752b22045579dee4190bbe005f0f0d`
|
||||||
|
|
||||||
The APK is signed with the same key as the Zapstore build. Installing an
|
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
|
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",
|
"name": "skate-spots",
|
||||||
"version": "1.7.85",
|
"version": "1.7.86",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "skate-spots",
|
"name": "skate-spots",
|
||||||
"version": "1.7.85",
|
"version": "1.7.86",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bitcoinerlab/secp256k1": "^1.2.0",
|
"@bitcoinerlab/secp256k1": "^1.2.0",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "skate-spots",
|
"name": "skate-spots",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.7.85",
|
"version": "1.7.86",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"homepage": "https://skate-spots.org",
|
"homepage": "https://skate-spots.org",
|
||||||
|
|
|
||||||
|
|
@ -98,10 +98,10 @@
|
||||||
|
|
||||||
<a
|
<a
|
||||||
class="download"
|
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"
|
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>
|
<span aria-hidden="true">↓</span>
|
||||||
</a>
|
</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
|
🛹 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.
|
- 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.
|
- 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 [errors, setErrors] = useState<FieldErrors>({});
|
||||||
const [showMore, setShowMore] = useState(false);
|
const [showMore, setShowMore] = useState(false);
|
||||||
const [showLocationMap, setShowLocationMap] = useState(!initial.pin);
|
const [showLocationMap, setShowLocationMap] = useState(!initial.pin);
|
||||||
const [showManualCoords, setShowManualCoords] = useState(false);
|
|
||||||
// Duplicate guard: warn when a spot already exists right where you're pinning
|
// 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
|
// one. This is the cheapest fix for clone spam and it points the contributor
|
||||||
// at the existing pin instead.
|
// at the existing pin instead.
|
||||||
|
|
@ -663,6 +662,9 @@ export default function CreateSpotPage() {
|
||||||
<p className="text-sm font-bold text-white">
|
<p className="text-sm font-bold text-white">
|
||||||
{location.trim() || `${lat.toFixed(5)}, ${lng.toFixed(5)}`}
|
{location.trim() || `${lat.toFixed(5)}, ${lng.toFixed(5)}`}
|
||||||
</p>
|
</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">
|
<p className="text-[11px] text-gray-400">
|
||||||
{t('spotUi.exactPinPublic')}
|
{t('spotUi.exactPinPublic')}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -717,36 +719,36 @@ export default function CreateSpotPage() {
|
||||||
<p className="text-[11px] leading-snug text-amber-300/90">
|
<p className="text-[11px] leading-snug text-amber-300/90">
|
||||||
{t('spotUi.exactPinPublic')}
|
{t('spotUi.exactPinPublic')}
|
||||||
</p>
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<Collapsible open={showManualCoords} onOpenChange={setShowManualCoords}>
|
{/* Coordinates are deliberately visible and editable on BOTH
|
||||||
<CollapsibleTrigger asChild>
|
add and edit. A spot cannot be published without a valid pair. */}
|
||||||
<button
|
<div className="space-y-2">
|
||||||
type="button"
|
<Label className="text-base font-bold">{t('createSpot.coordinates')} *</Label>
|
||||||
className="flex items-center gap-1 text-xs font-bold text-gray-400 hover:text-white transition-colors"
|
<div className="grid grid-cols-2 gap-3">
|
||||||
>
|
|
||||||
<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
|
<Input
|
||||||
|
aria-label={t('createSpot.latitudePlaceholder')}
|
||||||
placeholder={t('createSpot.latitudePlaceholder')}
|
placeholder={t('createSpot.latitudePlaceholder')}
|
||||||
value={latitude}
|
value={latitude}
|
||||||
onChange={(e) => { setLatitude(e.target.value); clearError('location'); }}
|
onChange={(e) => { setLatitude(e.target.value); clearError('location'); }}
|
||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
|
required
|
||||||
|
aria-invalid={!hasCoords}
|
||||||
|
className="font-mono text-base"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
|
aria-label={t('createSpot.longitudePlaceholder')}
|
||||||
placeholder={t('createSpot.longitudePlaceholder')}
|
placeholder={t('createSpot.longitudePlaceholder')}
|
||||||
value={longitude}
|
value={longitude}
|
||||||
onChange={(e) => { setLongitude(e.target.value); clearError('location'); }}
|
onChange={(e) => { setLongitude(e.target.value); clearError('location'); }}
|
||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
|
required
|
||||||
|
aria-invalid={!hasCoords}
|
||||||
|
className="font-mono text-base"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleContent>
|
</div>
|
||||||
</Collapsible>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{errors.location && <p className="text-xs font-medium text-red-400">{errors.location}</p>}
|
{errors.location && <p className="text-xs font-medium text-red-400">{errors.location}</p>}
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ export default function EditSpotPage() {
|
||||||
const [shelter, setShelter] = useState<ShelterType>(DEFAULT_SHELTER);
|
const [shelter, setShelter] = useState<ShelterType>(DEFAULT_SHELTER);
|
||||||
const [contactInfo, setContactInfo] = useState('');
|
const [contactInfo, setContactInfo] = useState('');
|
||||||
const [images, setImages] = useState<string[]>([]);
|
const [images, setImages] = useState<string[]>([]);
|
||||||
|
const [coordError, setCoordError] = useState<string | null>(null);
|
||||||
const [isGettingLocation, setIsGettingLocation] = useState(false);
|
const [isGettingLocation, setIsGettingLocation] = useState(false);
|
||||||
const [initialized, setInitialized] = useState(false);
|
const [initialized, setInitialized] = useState(false);
|
||||||
|
|
||||||
|
|
@ -115,6 +116,7 @@ export default function EditSpotPage() {
|
||||||
setLongitude(lng.toFixed(6));
|
setLongitude(lng.toFixed(6));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCoordError(null);
|
||||||
setInitialized(true);
|
setInitialized(true);
|
||||||
}
|
}
|
||||||
}, [spot, initialized]);
|
}, [spot, initialized]);
|
||||||
|
|
@ -138,6 +140,7 @@ export default function EditSpotPage() {
|
||||||
(position) => {
|
(position) => {
|
||||||
setLatitude(position.coords.latitude.toFixed(6));
|
setLatitude(position.coords.latitude.toFixed(6));
|
||||||
setLongitude(position.coords.longitude.toFixed(6));
|
setLongitude(position.coords.longitude.toFixed(6));
|
||||||
|
setCoordError(null);
|
||||||
setIsGettingLocation(false);
|
setIsGettingLocation(false);
|
||||||
toast({
|
toast({
|
||||||
title: t('spotUi.locationFound'),
|
title: t('spotUi.locationFound'),
|
||||||
|
|
@ -202,7 +205,8 @@ export default function EditSpotPage() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!latitude || !longitude) {
|
if (!latitude.trim() || !longitude.trim()) {
|
||||||
|
setCoordError(t('spotUi.locationRequiredDesc'));
|
||||||
toast({
|
toast({
|
||||||
title: t('spotUi.locationRequired'),
|
title: t('spotUi.locationRequired'),
|
||||||
description: t('spotUi.locationRequiredDesc'),
|
description: t('spotUi.locationRequiredDesc'),
|
||||||
|
|
@ -215,6 +219,7 @@ export default function EditSpotPage() {
|
||||||
const lng = parseFloat(longitude);
|
const lng = parseFloat(longitude);
|
||||||
|
|
||||||
if (isNaN(lat) || isNaN(lng) || lat < -90 || lat > 90 || lng < -180 || lng > 180) {
|
if (isNaN(lat) || isNaN(lng) || lat < -90 || lat > 90 || lng < -180 || lng > 180) {
|
||||||
|
setCoordError(t('spotUi.invalidCoordinatesDesc'));
|
||||||
toast({
|
toast({
|
||||||
title: t('spotUi.invalidCoordinates'),
|
title: t('spotUi.invalidCoordinates'),
|
||||||
description: t('spotUi.invalidCoordinatesDesc'),
|
description: t('spotUi.invalidCoordinatesDesc'),
|
||||||
|
|
@ -222,6 +227,7 @@ export default function EditSpotPage() {
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setCoordError(null);
|
||||||
|
|
||||||
if (!selectedCategory) {
|
if (!selectedCategory) {
|
||||||
toast({
|
toast({
|
||||||
|
|
@ -417,27 +423,32 @@ export default function EditSpotPage() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Coordinates */}
|
{/* Coordinates — mandatory, visible and large enough to verify on a phone. */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>{t('createSpot.coordinates')} *</Label>
|
<Label className="text-base font-bold">{t('createSpot.coordinates')} *</Label>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div>
|
|
||||||
<Input
|
<Input
|
||||||
|
aria-label={t('createSpot.latitudePlaceholder')}
|
||||||
placeholder={t('createSpot.latitudePlaceholder')}
|
placeholder={t('createSpot.latitudePlaceholder')}
|
||||||
value={latitude}
|
value={latitude}
|
||||||
onChange={(e) => setLatitude(e.target.value)}
|
onChange={(e) => { setLatitude(e.target.value); setCoordError(null); }}
|
||||||
|
inputMode="decimal"
|
||||||
required
|
required
|
||||||
|
aria-invalid={!!coordError}
|
||||||
|
className="font-mono text-base"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Input
|
<Input
|
||||||
|
aria-label={t('createSpot.longitudePlaceholder')}
|
||||||
placeholder={t('createSpot.longitudePlaceholder')}
|
placeholder={t('createSpot.longitudePlaceholder')}
|
||||||
value={longitude}
|
value={longitude}
|
||||||
onChange={(e) => setLongitude(e.target.value)}
|
onChange={(e) => { setLongitude(e.target.value); setCoordError(null); }}
|
||||||
|
inputMode="decimal"
|
||||||
required
|
required
|
||||||
|
aria-invalid={!!coordError}
|
||||||
|
className="font-mono text-base"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{coordError && <p className="text-xs font-medium text-red-400">{coordError}</p>}
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue