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 -1
View File
@@ -1,4 +1,4 @@
import { Controller, Get, Param, Req, Res } from '@nestjs/common';
import { Controller, Get, HttpException, Param, Query, Req, Res } from '@nestjs/common';
import type { Request, Response } from 'express';
import { Public } from '../auth/public.decorator';
import {
@@ -6,11 +6,16 @@ import {
getWikiPage,
getWikiAsset,
isLocalWiki,
searchWiki,
WikiNotFound,
type WikiPage,
type WikiNavSection,
type WikiSearchHit,
} from './wiki';
/** Longest query the search accepts; anything past it is noise, not a question. */
const SEARCH_MAX_QUERY = 120;
/**
* /api/help — embedded TREK wiki, served from the `wiki/` directory that ships
* with the app (see wiki.ts for the GitHub fallback). Content is public docs,
@@ -25,6 +30,26 @@ export class HelpController {
return getWikiIndex();
}
/**
* Full-text search over the bundled pages, for the help panel's search box.
* `q` is required; `limit` is optional and clamped by the search itself.
*/
@Get('search')
async search(@Query('q') q?: string, @Query('limit') limit?: string): Promise<{ hits: WikiSearchHit[] }> {
const query = (q ?? '').trim();
if (!query) throw new HttpException({ error: 'A search query is required' }, 400);
if (query.length > SEARCH_MAX_QUERY) {
throw new HttpException({ error: `Search query too long (max ${SEARCH_MAX_QUERY} chars)` }, 400);
}
const n = limit === undefined ? undefined : Number(limit);
if (n !== undefined && !Number.isInteger(n)) throw new HttpException({ error: 'limit must be an integer' }, 400);
try {
return { hits: await searchWiki(query, n) };
} catch {
throw new HttpException({ error: 'Help search unavailable' }, 502);
}
}
@Get('page/:slug')
async page(@Param('slug') slug: string, @Res() res: Response): Promise<void> {
try {