feat(help): in-app help center with per-screen guides and pictures

A help icon on every screen opens a panel with the screen's overview, its
sub-screens, task guides with a framed picture per step, search across guides
and the wiki, and a switch to any other screen's help. Guides exist for the
dashboard, Vacay, Atlas, Collections, Journey (list, journal, studio), Settings
(all tabs), Admin (all tabs) and the trip frame; the trip's own screens are
in progress. Pictures are produced by Playwright (client/e2e/help), texts in
all 23 languages.
This commit is contained in:
Maurice
2026-09-21 23:20:27 +02:00
parent 151e32107f
commit 4346d7f2c1
483 changed files with 47654 additions and 33 deletions
+26
View File
@@ -0,0 +1,26 @@
import { describe, it, expect } from 'vitest'
import { http, HttpResponse } from 'msw'
import { server } from '../../tests/helpers/msw/server'
import { searchHelp } from './help'
describe('searchHelp', () => {
it('asks /api/help/search with the query and limit and unwraps the hits', async () => {
let url = ''
server.use(
http.get('/api/help/search', ({ request }) => {
url = request.url
return HttpResponse.json({ hits: [{ slug: 'Home', title: 'Home', section: '', anchor: null, heading: null, snippet: 'x', score: 1 }] })
}),
)
const hits = await searchHelp('cover image', undefined, 3)
expect(hits).toHaveLength(1)
const params = new URL(url).searchParams
expect(params.get('q')).toBe('cover image')
expect(params.get('limit')).toBe('3')
})
it('rejects with the server error', async () => {
server.use(http.get('/api/help/search', () => HttpResponse.json({ error: 'A search query is required' }, { status: 400 })))
await expect(searchHelp('')).rejects.toBeTruthy()
})
})
+23
View File
@@ -0,0 +1,23 @@
import { apiClient } from './client'
/** One hit of the full-text search over the bundled wiki (`GET /api/help/search`). */
export interface HelpSearchHit {
slug: string
title: string
section: string
anchor: string | null
heading: string | null
snippet: string
score: number
}
/**
* Search the docs. Online-only by nature: the pages live on the server, and a
* help lookup is never queued for later. Pass a signal so a superseded query
* can be dropped instead of racing the newer one.
*/
export function searchHelp(query: string, signal?: AbortSignal, limit = 8): Promise<HelpSearchHit[]> {
return apiClient
.get<{ hits: HelpSearchHit[] }>('/help/search', { params: { q: query, limit }, signal })
.then(r => r.data.hits)
}