stammtisch-hersbruck/frontend/src/lib/stores/trails.svelte.ts
Daniel Michelberger f8247ae3e7 feat: Trail-Seite neu geordnet, echte dunkle Karte, sichtbare Flag-Icons
Der Kopf traegt jetzt Name, Zustand und Kennzahlen: Der Status sitzt als
farbiges Dropdown direkt hinter dem Namen und nennt darunter, wann und von
wem er gemeldet wurde. Laenge und Hoehenmeter stehen mit Icons daneben.

Karte und Hoehenprofil links, Marker, Kommentare und Versionen rechts
daneben in einer Spalte. Die Reiter entfallen - alles ist gleichzeitig zu
sehen. Versionen sind Archiv und deshalb eingeklappt; der GPX-Upload sitzt
dort statt im Kopf.

Marker lassen sich jetzt auch loeschen und auch ueber das Hoehenprofil
setzen: Die angeklickte Stelle wird auf dieselbe Linie zurueckgerechnet,
die ein Klick auf der Karte trifft.

Die Flag-Icons wurden bisher gespeichert, aber nirgends gezeichnet. Sie
erscheinen jetzt in der Karte, im Popup, in der Markerliste und in der
Verwaltung. Erlaubt ist jeder Name von lucide.dev - moeglich macht das ein
Glob ueber die Icon-Dateien, der jedes Icon zu einem eigenen, erst bei
Bedarf geladenen Chunk macht. Die Liste im Dialog sind nur Vorschlaege.

Die Karte kommt von CARTO statt von OSM direkt: zurueckhaltend gezeichnet,
sodass die Trail-Linie darueber steht, und mit einer echten dunklen
Fassung. Das vorherige Abdunkeln der OSM-Kacheln ergab nur ein dunkleres
Bild, keine dunkle Karte. In der Kachelvorschau der Liste sind Zoom,
Massstab und Quellenangabe abgeschaltet - dort ist die Karte Bild, nicht
Werkzeug; die grosse Karte traegt die Angabe weiterhin.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
2026-09-02 12:56:17 +02:00

131 lines
4.1 KiB
TypeScript

import { getContext, setContext } from 'svelte'
import { api } from './pocketbase.svelte'
import { auth } from './pocketbase.svelte'
import { getTeamContext } from './teams.svelte'
import type { TrailsResponse } from '$lib/types'
const KEY = Symbol('trail')
export class TrailStore {
records = $state<TrailsResponse[]>([])
loading = $state(false)
error = $state<string | null>(null)
private unsubscribe: (() => void) | null = null
private teams = getTeamContext()
/** Records gefiltert nach aktivem Team */
get scoped(): TrailsResponse[] {
const teamId = this.teams.activeId
if (!teamId) return []
return this.records.filter((r) => r.team === teamId)
}
getById(id: string): TrailsResponse | undefined {
return this.records.find((r) => r.id === id)
}
/**
* Bearbeiten dürfen Trail-Paten sowie Owner und Admins des Teams —
* dieselbe Regel wie in der updateRule der Collection. Die Prüfung hier
* blendet nur UI aus; durchgesetzt wird sie serverseitig.
*/
canEdit(trail: TrailsResponse): boolean {
const uid = auth.user?.id
if (!uid) return false
const team = this.teams.records.find((t) => t.id === trail.team)
const stewards = (trail.stewards ?? []) as string[]
return (
stewards.includes(uid) ||
team?.owner === uid ||
((team?.admins ?? []) as string[]).includes(uid)
)
}
async load() {
this.loading = true
this.error = null
try {
this.records = await api.collection('trails').getFullList({
sort: 'name',
requestKey: null,
})
} catch (e: any) {
this.error = e.message ?? 'Fehler beim Laden der Trails'
console.error(e)
} finally {
this.loading = false
}
}
subscribe() {
if (this.unsubscribe) return
api.collection('trails').subscribe('*', (e) => {
const idx = this.records.findIndex((r) => r.id === e.record.id)
if (e.action === 'create' && idx === -1) this.records = [...this.records, e.record]
else if (e.action === 'update' && idx !== -1) this.records[idx] = e.record
else if (e.action === 'delete' && idx !== -1) this.records = this.records.filter((r) => r.id !== e.record.id)
}).then((unsub) => {
this.unsubscribe = unsub
}).catch((e) => console.warn('trails subscribe failed:', e))
}
async create(data: { name: string; description?: string; status?: string }) {
const teamId = this.teams.activeId
if (!teamId) throw new Error('Kein aktives Team')
const uid = auth.user?.id
// Wer anlegt, wird automatisch erster Pate — sonst könnte niemand
// den frisch angelegten Trail bearbeiten.
return await api.collection('trails').create({
...data,
status: data.status ?? 'offen',
team: teamId,
created_by: uid,
stewards: uid ? [uid] : [],
})
}
async edit(id: string, data: Partial<TrailsResponse>) {
return await api.collection('trails').update(id, data)
}
/**
* Status melden. Wann und von wem kommt gleich mit — ohne das ließe sich
* einer Sperrung nicht ansehen, ob sie von heute oder vom letzten Jahr ist.
*/
async setStatus(id: string, status: string) {
return await api.collection('trails').update(id, {
status,
status_changed: new Date().toISOString(),
status_by: auth.user?.id ?? '',
})
}
async setStewards(id: string, userIds: string[]) {
return await api.collection('trails').update(id, { stewards: userIds })
}
async remove(id: string) {
await api.collection('trails').delete(id)
}
destroy() {
if (this.unsubscribe) {
this.unsubscribe()
this.unsubscribe = null
}
}
}
export function setTrailContext() {
const store = new TrailStore()
setContext(KEY, store)
return store
}
export function getTrailContext(): TrailStore {
return getContext<TrailStore>(KEY)
}