fix: restore orange brand icon and harden OSM tile cache
Some checks are pending
Deploy to GitHub Pages / deploy (push) Waiting to run

This commit is contained in:
SkateSpots 2026-11-01 23:32:29 +07:00
parent f75bad3e8d
commit 579dcddc1d
18 changed files with 79 additions and 39 deletions

View file

@ -44,11 +44,11 @@
<link rel="manifest" href="/manifest.webmanifest"> <link rel="manifest" href="/manifest.webmanifest">
<!-- App Icons --> <!-- App Icons -->
<link rel="icon" type="image/png" href="/images/icon-1024.png"> <link rel="icon" type="image/png" href="/images/brand-192.png">
<link rel="apple-touch-icon" href="/images/icon-180.png"> <link rel="apple-touch-icon" href="/images/brand-180.png">
<link rel="apple-touch-icon" sizes="180x180" href="/images/icon-180.png"> <link rel="apple-touch-icon" sizes="180x180" href="/images/brand-180.png">
<link rel="apple-touch-icon" sizes="180x180" href="/images/icon-180.png"> <link rel="apple-touch-icon" sizes="180x180" href="/images/brand-180.png">
<link rel="apple-touch-startup-image" href="/images/icon-1024.png"> <link rel="apple-touch-startup-image" href="/images/brand-1024.png">
<!-- Theme Color --> <!-- Theme Color -->
<meta name="theme-color" content="#ff0040"> <meta name="theme-color" content="#ff0040">

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

BIN
public/images/brand-180.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/images/brand-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/images/brand-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View file

@ -1,8 +1,8 @@
{ {
"name": "SkateSpots - Decentralized Skate Map", "name": "SkateSpots — Open-sourced Skate Community App",
"short_name": "SkateSpots", "short_name": "SkateSpots",
"id": "/", "id": "/",
"description": "Discover skate spots, find coaches, join events, and connect with the skateboarding community on Nostr. Free, decentralized, censorship-resistant.", "description": "Map, crews, events, gear and more — an open-sourced skate community app.",
"start_url": "/", "start_url": "/",
"scope": "/", "scope": "/",
"display": "standalone", "display": "standalone",
@ -18,25 +18,25 @@
"related_applications": [], "related_applications": [],
"icons": [ "icons": [
{ {
"src": "/images/icon-192.png", "src": "/images/brand-192.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png", "type": "image/png",
"purpose": "any" "purpose": "any"
}, },
{ {
"src": "/images/icon-512.png", "src": "/images/brand-512.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png", "type": "image/png",
"purpose": "any" "purpose": "any"
}, },
{ {
"src": "/images/icon-maskable-512.png", "src": "/images/brand-maskable-512.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png", "type": "image/png",
"purpose": "maskable" "purpose": "maskable"
}, },
{ {
"src": "/images/icon-1024.png", "src": "/images/brand-1024.png",
"sizes": "1024x1024", "sizes": "1024x1024",
"type": "image/png", "type": "image/png",
"purpose": "any" "purpose": "any"
@ -66,7 +66,7 @@
"url": "/map", "url": "/map",
"icons": [ "icons": [
{ {
"src": "/images/icon-1024.png", "src": "/images/brand-180.png",
"sizes": "180x180", "sizes": "180x180",
"type": "image/png" "type": "image/png"
} }
@ -79,7 +79,7 @@
"url": "/events", "url": "/events",
"icons": [ "icons": [
{ {
"src": "/images/icon-1024.png", "src": "/images/brand-180.png",
"sizes": "180x180", "sizes": "180x180",
"type": "image/png" "type": "image/png"
} }
@ -92,7 +92,7 @@
"url": "/coaches", "url": "/coaches",
"icons": [ "icons": [
{ {
"src": "/images/icon-1024.png", "src": "/images/brand-180.png",
"sizes": "180x180", "sizes": "180x180",
"type": "image/png" "type": "image/png"
} }
@ -105,7 +105,7 @@
"url": "/wallet", "url": "/wallet",
"icons": [ "icons": [
{ {
"src": "/images/icon-1024.png", "src": "/images/brand-180.png",
"sizes": "180x180", "sizes": "180x180",
"type": "image/png" "type": "image/png"
} }

View file

@ -630,7 +630,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
title={user ? t('chromeUi.viewMyProfile') : t('nav.map')} title={user ? t('chromeUi.viewMyProfile') : t('nav.map')}
> >
<img <img
src="/images/icon-1024.png" src="/images/logo.png"
alt="SkateSpots" alt="SkateSpots"
className="w-10 h-10 shrink-0 rounded-xl shadow-lg shadow-red-500/30 group-hover:shadow-red-500/50 transition-shadow" className="w-10 h-10 shrink-0 rounded-xl shadow-lg shadow-red-500/30 group-hover:shadow-red-500/50 transition-shadow"
/> />
@ -857,7 +857,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
aria-label={user ? t('chromeUi.viewMyProfile') : t('nav.map')} aria-label={user ? t('chromeUi.viewMyProfile') : t('nav.map')}
> >
<img <img
src="/images/icon-1024.png" src="/images/logo.png"
alt="SkateSpots" alt="SkateSpots"
className="w-9 h-9 rounded-lg flex-shrink-0 shadow-lg shadow-red-500/30" className="w-9 h-9 rounded-lg flex-shrink-0 shadow-lg shadow-red-500/30"
/> />

View file

@ -22,6 +22,12 @@ import {
import { getLastLocation } from '@/lib/lastLocation'; import { getLastLocation } from '@/lib/lastLocation';
import { saveRoute, loadRoute, formatAge } from '@/lib/routeCache'; import { saveRoute, loadRoute, formatAge } from '@/lib/routeCache';
import { createCachedTileLayer } from '@/lib/cachedTileLayer'; import { createCachedTileLayer } from '@/lib/cachedTileLayer';
import {
MAP_TILE_URL,
MAP_TILE_SUBDOMAINS,
MAP_TILE_CACHE_NAMESPACE,
MAP_TILE_ATTRIBUTION,
} from '@/lib/mapTiles';
import { calculateDistance } from '@/lib/sortOptions'; import { calculateDistance } from '@/lib/sortOptions';
import { import {
fetchRoute, fetchRoute,
@ -598,13 +604,13 @@ export function RouteDialog({
minZoom: 2, minZoom: 2,
}); });
createCachedTileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { createCachedTileLayer(MAP_TILE_URL, {
attribution: '', attribution: MAP_TILE_ATTRIBUTION,
subdomains: 'abc', subdomains: MAP_TILE_SUBDOMAINS,
maxZoom: 19, maxZoom: 19,
noWrap: true, noWrap: true,
className: 'skatespots-dark-tiles', className: 'skatespots-dark-tiles',
}).addTo(map); }, MAP_TILE_CACHE_NAMESPACE).addTo(map);
// Metric scale bar — genuinely useful on a route map (how long is that line?). // Metric scale bar — genuinely useful on a route map (how long is that line?).
L.control.scale({ imperial: false, position: 'topleft' }).addTo(map); L.control.scale({ imperial: false, position: 'topleft' }).addTo(map);

View file

@ -258,7 +258,7 @@ export function SplashScreen() {
className="relative" className="relative"
> >
<img <img
src="/images/icon-1024.png" src="/images/logo.png"
alt="SkateSpots" alt="SkateSpots"
draggable={false} draggable={false}
className="relative h-28 w-28 select-none drop-shadow-[0_0_34px_rgba(255,106,0,0.65)]" className="relative h-28 w-28 select-none drop-shadow-[0_0_34px_rgba(255,106,0,0.65)]"

View file

@ -2,6 +2,12 @@ import { useEffect, useRef } from 'react';
import L from 'leaflet'; import L from 'leaflet';
import { createCachedTileLayer } from '@/lib/cachedTileLayer'; import { createCachedTileLayer } from '@/lib/cachedTileLayer';
import {
MAP_TILE_URL,
MAP_TILE_SUBDOMAINS,
MAP_TILE_CACHE_NAMESPACE,
MAP_TILE_ATTRIBUTION,
} from '@/lib/mapTiles';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
export interface LatLng { export interface LatLng {
@ -53,14 +59,14 @@ export function SpotLocationPicker({ value, onChange, className }: SpotLocationP
attributionControl: true, attributionControl: true,
}); });
createCachedTileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { createCachedTileLayer(MAP_TILE_URL, {
attribution: '', attribution: MAP_TILE_ATTRIBUTION,
subdomains: 'abc', subdomains: MAP_TILE_SUBDOMAINS,
minZoom: 2, minZoom: 2,
maxZoom: 19, maxZoom: 19,
noWrap: true, noWrap: true,
className: 'skatespots-dark-tiles', className: 'skatespots-dark-tiles',
}).addTo(map); }, MAP_TILE_CACHE_NAMESPACE).addTo(map);
map.on('click', (e: L.LeafletMouseEvent) => { map.on('click', (e: L.LeafletMouseEvent) => {
onChangeRef.current({ lat: e.latlng.lat, lng: e.latlng.lng }); onChangeRef.current({ lat: e.latlng.lat, lng: e.latlng.lng });

View file

@ -15,6 +15,12 @@ import type { BtcMapPlace } from '@/hooks/useBtcMapPlaces';
import { isRecentlyVerified, isBoosted } from '@/hooks/useBtcMapPlaces'; import { isRecentlyVerified, isBoosted } from '@/hooks/useBtcMapPlaces';
import { sanitizeUrl } from '@/lib/sanitizeUrl'; import { sanitizeUrl } from '@/lib/sanitizeUrl';
import { createCachedTileLayer } from '@/lib/cachedTileLayer'; import { createCachedTileLayer } from '@/lib/cachedTileLayer';
import {
MAP_TILE_URL,
MAP_TILE_SUBDOMAINS,
MAP_TILE_CACHE_NAMESPACE,
MAP_TILE_ATTRIBUTION,
} from '@/lib/mapTiles';
import { FORECAST_DAYS } from '@/lib/weather'; import { FORECAST_DAYS } from '@/lib/weather';
import { useTranslation } from '@/contexts/language'; import { useTranslation } from '@/contexts/language';
@ -955,17 +961,18 @@ export function SpotMap({
applyWorldMinZoom(map); applyWorldMinZoom(map);
// Free, no-API-key basemap: OpenStreetMap standard tiles, darkened via a CSS // OpenStreetMap standard tiles, darkened via a CSS filter (see the
// filter (see the `.skatespots-dark-tiles` rule in index.css) so it matches // `.skatespots-dark-tiles` rule in index.css) so they match the app's dark
// the app's dark theme without depending on a keyed provider like CARTO. // theme. Wrapped in an on-device tile cache so the basemap still draws at
// Wrapped in an on-device tile cache so the basemap still draws at the park. // the park, with a fresh cache namespace so old access-blocked tiles are
createCachedTileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { // never reused.
attribution: '', createCachedTileLayer(MAP_TILE_URL, {
subdomains: 'abc', attribution: MAP_TILE_ATTRIBUTION,
subdomains: MAP_TILE_SUBDOMAINS,
minZoom: 1, minZoom: 1,
maxZoom: 19, maxZoom: 19,
className: 'skatespots-dark-tiles', className: 'skatespots-dark-tiles',
}).addTo(map); }, MAP_TILE_CACHE_NAMESPACE).addTo(map);
// Add click handler if enabled // Add click handler if enabled
if (enableClickToAdd && onMapClick) { if (enableClickToAdd && onMapClick) {

View file

@ -212,7 +212,7 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
setConnectError(''); setConnectError('');
setConnectState('waiting'); setConnectState('waiting');
const session = startNostrConnect(nostr, { const session = startNostrConnect(nostr, {
appImage: 'https://skate-spots.org/images/icon-1024.png', appImage: 'https://skate-spots.org/images/brand-1024.png',
timeoutMs: 180_000, timeoutMs: 180_000,
}); });
connectRef.current = session; connectRef.current = session;

View file

@ -14,12 +14,15 @@ import { getTile, putTile } from '@/lib/tileCache';
* Works on web and in the Capacitor WebView alike, unlike a service worker. * Works on web and in the Capacitor WebView alike, unlike a service worker.
*/ */
export class CachedTileLayer extends L.TileLayer { export class CachedTileLayer extends L.TileLayer {
/** Change this whenever the basemap/provider/cache contract changes. */
cacheNamespace = 'default';
protected createTile(coords: L.Coords, done: L.DoneCallback): HTMLElement { protected createTile(coords: L.Coords, done: L.DoneCallback): HTMLElement {
const tile = document.createElement('img'); const tile = document.createElement('img');
tile.alt = ''; tile.alt = '';
tile.setAttribute('role', 'presentation'); tile.setAttribute('role', 'presentation');
const key = `${coords.z}/${coords.x}/${coords.y}`; const key = `${this.cacheNamespace}:${coords.z}/${coords.x}/${coords.y}`;
const url = this.getTileUrl(coords); const url = this.getTileUrl(coords);
const useSrc = (src: string, revoke: boolean) => { const useSrc = (src: string, revoke: boolean) => {
@ -57,6 +60,9 @@ export class CachedTileLayer extends L.TileLayer {
export function createCachedTileLayer( export function createCachedTileLayer(
urlTemplate: string, urlTemplate: string,
options?: L.TileLayerOptions, options?: L.TileLayerOptions,
cacheNamespace = 'default',
): CachedTileLayer { ): CachedTileLayer {
return new CachedTileLayer(urlTemplate, options); const layer = new CachedTileLayer(urlTemplate, options);
layer.cacheNamespace = cacheNamespace;
return layer;
} }

15
src/lib/mapTiles.ts Normal file
View file

@ -0,0 +1,15 @@
/**
* OpenStreetMap basemap configuration.
*
* Keep the OSM tile provider. `osm-v2` intentionally changes the cache
* namespace so old cached "Access blocked" tiles are never reused; it does not
* change the tile source.
*/
export const MAP_TILE_URL =
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
export const MAP_TILE_SUBDOMAINS = 'abc';
export const MAP_TILE_CACHE_NAMESPACE = 'osm-v2';
export const MAP_TILE_ATTRIBUTION = '&copy; OpenStreetMap contributors';

View file

@ -593,7 +593,7 @@ function Footer() {
<div className="flex flex-col md:flex-row items-center justify-between gap-6"> <div className="flex flex-col md:flex-row items-center justify-between gap-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<img <img
src="/images/icon-1024.png" src="/images/logo.png"
alt="SkateSpots" alt="SkateSpots"
className="w-10 h-10 rounded-lg shadow-[0_0_20px_rgba(255,0,64,0.5)]" className="w-10 h-10 rounded-lg shadow-[0_0_20px_rgba(255,0,64,0.5)]"
/> />

View file

@ -48,7 +48,7 @@ website: https://skate-spots.org
license: AGPL-3.0-only license: AGPL-3.0-only
# Launcher / listing icon. # Launcher / listing icon.
icon: ./public/images/icon-1024.png icon: ./public/images/brand-1024.png
# Listing screenshots (uploaded by zsp to the Zapstore CDN). # Listing screenshots (uploaded by zsp to the Zapstore CDN).
images: images: