Laenge und Hoehenmeter tragen dieselben Icons wie der Kopf der Trail-Seite. Dazu kommen Anzahl der Marker und Kommentare - und ein Punkt daneben, wenn davon etwas aus den letzten 14 Tagen stammt. Die blosse Zahl sagt nichts darueber, ob sich gerade etwas tut. Kommentare bekommen dafuer einen eigenen Store. Sie lagen bisher direkt in der Trail-Seite, was richtig war, solange es sie nur dort gab; fuer die Liste waere ein zweiter Ladeweg auf dieselben Daten einer zu viel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
104 lines
3.1 KiB
TypeScript
104 lines
3.1 KiB
TypeScript
import { getContext, setContext } from 'svelte'
|
|
import { api, auth } from './pocketbase.svelte'
|
|
import { getTeamContext } from './teams.svelte'
|
|
import { isRecent } from '$lib/time'
|
|
import type { TrailCommentsResponse, UsersResponse } from '$lib/types'
|
|
|
|
const KEY = Symbol('trailComment')
|
|
|
|
/** Kommentar samt Verfasser, sofern dessen Datensatz sichtbar ist. */
|
|
export type TrailComment = Omit<TrailCommentsResponse, 'expand'> & {
|
|
expand?: { created_by?: UsersResponse }
|
|
}
|
|
|
|
/**
|
|
* Kommentare lagen bisher direkt in der Trail-Seite — als es sie nur dort
|
|
* gab, war das die einfachere Lösung. Die Trail-Liste zeigt jetzt ebenfalls
|
|
* ihre Anzahl, und zwei Ladewege auf dieselben Daten wären einer zu viel.
|
|
*/
|
|
export class TrailCommentStore {
|
|
records = $state<TrailComment[]>([])
|
|
loading = $state(false)
|
|
error = $state<string | null>(null)
|
|
|
|
private unsubscribe: (() => void) | null = null
|
|
private teams = getTeamContext()
|
|
|
|
get scoped(): TrailComment[] {
|
|
const teamId = this.teams.activeId
|
|
if (!teamId) return []
|
|
return this.records.filter((r) => r.team === teamId)
|
|
}
|
|
|
|
/** Kommentare eines Trails, jüngste zuerst */
|
|
byTrail(trailId: string): TrailComment[] {
|
|
return this.scoped
|
|
.filter((r) => r.trail === trailId)
|
|
.sort((a, b) => (a.created < b.created ? 1 : -1))
|
|
}
|
|
|
|
/** Gab es zuletzt Bewegung? Für die Hervorhebung in der Liste. */
|
|
hasRecent(trailId: string, days?: number): boolean {
|
|
return this.byTrail(trailId).some((r) => isRecent(r.created, days))
|
|
}
|
|
|
|
async load() {
|
|
this.loading = true
|
|
this.error = null
|
|
try {
|
|
this.records = (await api.collection('trail_comments').getFullList({
|
|
sort: '-created',
|
|
expand: 'created_by',
|
|
requestKey: null,
|
|
})) as TrailComment[]
|
|
} catch (e: any) {
|
|
this.error = e.message ?? 'Fehler beim Laden der Kommentare'
|
|
console.error(e)
|
|
} finally {
|
|
this.loading = false
|
|
}
|
|
}
|
|
|
|
subscribe() {
|
|
if (this.unsubscribe) return
|
|
api.collection('trail_comments').subscribe('*', (e) => {
|
|
const record = e.record as TrailComment
|
|
const idx = this.records.findIndex((r) => r.id === record.id)
|
|
if (e.action === 'create' && idx === -1) this.records = [record, ...this.records]
|
|
else if (e.action === 'update' && idx !== -1) this.records[idx] = record
|
|
else if (e.action === 'delete' && idx !== -1) this.records = this.records.filter((r) => r.id !== record.id)
|
|
}, { expand: 'created_by' }).then((unsub) => {
|
|
this.unsubscribe = unsub
|
|
}).catch((e) => console.warn('trail_comments subscribe failed:', e))
|
|
}
|
|
|
|
async create(trailId: string, teamId: string, text: string) {
|
|
return await api.collection('trail_comments').create({
|
|
trail: trailId,
|
|
team: teamId,
|
|
text: text.trim(),
|
|
created_by: auth.user?.id,
|
|
})
|
|
}
|
|
|
|
async remove(id: string) {
|
|
await api.collection('trail_comments').delete(id)
|
|
}
|
|
|
|
destroy() {
|
|
if (this.unsubscribe) {
|
|
this.unsubscribe()
|
|
this.unsubscribe = null
|
|
}
|
|
}
|
|
}
|
|
|
|
export function setTrailCommentContext() {
|
|
const store = new TrailCommentStore()
|
|
setContext(KEY, store)
|
|
return store
|
|
}
|
|
|
|
export function getTrailCommentContext(): TrailCommentStore {
|
|
return getContext<TrailCommentStore>(KEY)
|
|
}
|