release: 1.7.86
Some checks are pending
Deploy to GitHub Pages / deploy (push) Waiting to run

This commit is contained in:
SkateSpots 2026-11-02 16:19:19 +07:00
parent c30ac8376b
commit a696ba59a4
8 changed files with 83 additions and 57 deletions

View file

@ -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

View file

@ -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
View file

@ -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",

View file

@ -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",

View file

@ -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>

View file

@ -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.

View file

@ -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,36 +719,36 @@ 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">
{/* 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>
</CollapsibleContent>
</Collapsible>
</>
)}
</div>
{errors.location && <p className="text-xs font-medium text-red-400">{errors.location}</p>}
</section>

View file

@ -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>
<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)}
onChange={(e) => { setLatitude(e.target.value); setCoordError(null); }}
inputMode="decimal"
required
aria-invalid={!!coordError}
className="font-mono text-base"
/>
</div>
<div>
<Input
aria-label={t('createSpot.longitudePlaceholder')}
placeholder={t('createSpot.longitudePlaceholder')}
value={longitude}
onChange={(e) => setLongitude(e.target.value)}
onChange={(e) => { setLongitude(e.target.value); setCoordError(null); }}
inputMode="decimal"
required
aria-invalid={!!coordError}
className="font-mono text-base"
/>
</div>
</div>
{coordError && <p className="text-xs font-medium text-red-400">{coordError}</p>}
<Button
type="button"
variant="outline"