feat(docs): standalone docs page, Hilfe nav entry, rewritten user guide
- docs route renders without the app shell (own header, back-to-app link, theme toggle) - nav entry renamed to Hilfe (de) / Help (en) - reference links now case-insensitive (schema/tag slugs) - handbook rewritten as 17-page user guide with links into the API reference - release notes v0.8.1 added
This commit is contained in:
@@ -37,31 +37,6 @@ function buildCrumbs(location: string, t: TFunction): Crumb[] {
|
||||
crumbs.push({ label: t("compare.title") });
|
||||
} else if (location.startsWith("/analytics")) {
|
||||
crumbs.push({ label: t("nav.analytics") });
|
||||
} else if (location.startsWith("/docs")) {
|
||||
crumbs.push({ href: "/docs", label: t("docs.title") });
|
||||
const m = location.replace(/^\/docs\/?/, "");
|
||||
if (m.startsWith("handbook/")) {
|
||||
crumbs.push({ href: "/docs/handbook", label: t("docs.guides") });
|
||||
const slug = m.replace(/^handbook\//, "").split("#")[0];
|
||||
if (slug) crumbs.push({ label: slug });
|
||||
} else if (m.startsWith("reference/")) {
|
||||
crumbs.push({ href: "/docs/reference/endpoints", label: t("docs.reference") });
|
||||
const rest = m.replace(/^reference\//, "").split("#")[0];
|
||||
if (rest.startsWith("schemas/")) {
|
||||
crumbs.push({ label: t("docs.schemas") });
|
||||
const name = rest.replace(/^schemas\//, "");
|
||||
if (name) crumbs.push({ label: name });
|
||||
} else {
|
||||
const tag = rest.replace(/^endpoints\//, "");
|
||||
if (tag) crumbs.push({ label: tag });
|
||||
}
|
||||
} else if (m.startsWith("releases/")) {
|
||||
crumbs.push({ href: "/docs/releases", label: t("docs.releases") });
|
||||
const version = m.replace(/^releases\//, "").split("#")[0];
|
||||
if (version) crumbs.push({ label: version });
|
||||
} else if (m) {
|
||||
crumbs.push({ label: m });
|
||||
}
|
||||
} else if (location.startsWith("/login")) {
|
||||
crumbs.push({ label: t("auth.signIn") });
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"trash": "Papierkorb",
|
||||
"admin": "Admin",
|
||||
"redundancy": "Redundanz",
|
||||
"docs": "Doku",
|
||||
"docs": "Hilfe",
|
||||
"search": "Tools suchen…"
|
||||
},
|
||||
"auth": {
|
||||
@@ -198,7 +198,18 @@
|
||||
"reference": "API-Referenz",
|
||||
"referenceIntro": "Automatisch aus der OpenAPI-Spezifikation generiert — alle Endpunkte und Datenfelder der aktuellen Version.",
|
||||
"onThisPage": "Auf dieser Seite",
|
||||
"searchPlaceholder": "Doku durchsuchen…"
|
||||
"searchPlaceholder": "Doku durchsuchen…",
|
||||
"backToApp": "Zur App",
|
||||
"field": "Feld",
|
||||
"type": "Typ",
|
||||
"required": "Pflicht",
|
||||
"description": "Beschreibung",
|
||||
"status": "Status",
|
||||
"schema": "Schema",
|
||||
"parameter": "Parameter",
|
||||
"noResults": "Keine Treffer",
|
||||
"fields": "Felder",
|
||||
"fieldHelpHint": "Hinweis: Formular-Felder verlinken per ?-Icon direkt zu den jeweiligen Zeilen dieser Tabelle."
|
||||
},
|
||||
"command": {
|
||||
"navigate": "Navigation",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"trash": "Trash",
|
||||
"admin": "Admin",
|
||||
"redundancy": "Redundancy",
|
||||
"docs": "Docs",
|
||||
"docs": "Help",
|
||||
"search": "Search tools…"
|
||||
},
|
||||
"auth": {
|
||||
@@ -198,7 +198,18 @@
|
||||
"reference": "API reference",
|
||||
"referenceIntro": "Generated automatically from the OpenAPI spec — all endpoints and data fields of the current version.",
|
||||
"onThisPage": "On this page",
|
||||
"searchPlaceholder": "Search docs…"
|
||||
"searchPlaceholder": "Search docs…",
|
||||
"backToApp": "Back to app",
|
||||
"field": "Field",
|
||||
"type": "Type",
|
||||
"required": "Required",
|
||||
"description": "Description",
|
||||
"status": "Status",
|
||||
"schema": "Schema",
|
||||
"parameter": "Parameter",
|
||||
"noResults": "No results",
|
||||
"fields": "Fields",
|
||||
"fieldHelpHint": "Note: form fields link via the ? icon directly to the respective rows of this table."
|
||||
},
|
||||
"command": {
|
||||
"navigate": "Navigate",
|
||||
|
||||
@@ -3,11 +3,11 @@ import { Link, useLocation } from "wouter";
|
||||
import { Marked } from "marked";
|
||||
import DOMPurify from "dompurify";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Layout } from "@/components/layout";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { useGetVersion, getGetVersionQueryKey } from "@workspace/api-client-react";
|
||||
import {
|
||||
ArrowLeft,
|
||||
BookOpen,
|
||||
CalendarDays,
|
||||
ExternalLink,
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
Search,
|
||||
Server,
|
||||
Tag,
|
||||
Wrench,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
@@ -485,15 +487,16 @@ function FieldTypeChip({ type }: { type: FieldType }) {
|
||||
}
|
||||
|
||||
function FieldTable({ fields }: { fields: Field[] }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="overflow-x-auto rounded-lg border">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50 text-left text-xs uppercase tracking-wider text-muted-foreground">
|
||||
<th className="px-3 py-2 font-semibold">Feld</th>
|
||||
<th className="px-3 py-2 font-semibold">Typ</th>
|
||||
<th className="px-3 py-2 font-semibold">Pflicht</th>
|
||||
<th className="px-3 py-2 font-semibold">Beschreibung</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("docs.field")}</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("docs.type")}</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("docs.required")}</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("docs.description")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -529,6 +532,7 @@ function FieldTable({ fields }: { fields: Field[] }) {
|
||||
}
|
||||
|
||||
function SchemaView({ schema }: { schema: SchemaModel }) {
|
||||
const { t } = useTranslation();
|
||||
const headings: Heading[] = schema.fields.map((f) => ({ id: f.name, text: f.name, level: 2 }));
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row gap-8">
|
||||
@@ -540,15 +544,16 @@ function SchemaView({ schema }: { schema: SchemaModel }) {
|
||||
<FieldTable fields={schema.fields} />
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<HelpCircle className="h-3.5 w-3.5 inline mr-1" />
|
||||
Hinweis: Formular-Felder verlinken per ?-Icon direkt zu den jeweiligen Zeilen dieser Tabelle.
|
||||
{t("docs.fieldHelpHint")}
|
||||
</p>
|
||||
</div>
|
||||
<Toc headings={headings} title="Felder" />
|
||||
<Toc headings={headings} title={t("docs.fields")} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EndpointTagView({ tag }: { tag: TagGroup }) {
|
||||
const { t } = useTranslation();
|
||||
const headings: Heading[] = tag.endpoints.map((e) => ({
|
||||
id: e.operationId,
|
||||
text: `${e.method} ${e.path}`,
|
||||
@@ -577,16 +582,16 @@ function EndpointTagView({ tag }: { tag: TagGroup }) {
|
||||
|
||||
{ep.parameters.length > 0 && (
|
||||
<div className="mb-3">
|
||||
<p className="mb-1 text-xs font-semibold uppercase tracking-wider text-muted-foreground">Parameter</p>
|
||||
<p className="mb-1 text-xs font-semibold uppercase tracking-wider text-muted-foreground">{t("docs.parameter")}</p>
|
||||
<div className="overflow-x-auto rounded-lg border">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50 text-left text-xs uppercase tracking-wider text-muted-foreground">
|
||||
<th className="px-3 py-2 font-semibold">Name</th>
|
||||
<th className="px-3 py-2 font-semibold">In</th>
|
||||
<th className="px-3 py-2 font-semibold">Typ</th>
|
||||
<th className="px-3 py-2 font-semibold">Pflicht</th>
|
||||
<th className="px-3 py-2 font-semibold">Beschreibung</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("docs.type")}</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("docs.required")}</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("docs.description")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -623,9 +628,9 @@ function EndpointTagView({ tag }: { tag: TagGroup }) {
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50 text-left text-xs uppercase tracking-wider text-muted-foreground">
|
||||
<th className="px-3 py-2 font-semibold">Status</th>
|
||||
<th className="px-3 py-2 font-semibold">Beschreibung</th>
|
||||
<th className="px-3 py-2 font-semibold">Schema</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("docs.status")}</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("docs.description")}</th>
|
||||
<th className="px-3 py-2 font-semibold">{t("docs.schema")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -642,7 +647,7 @@ function EndpointTagView({ tag }: { tag: TagGroup }) {
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
<Toc headings={headings} title="Endpunkte" />
|
||||
<Toc headings={headings} title={t("docs.endpoints")} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -737,12 +742,13 @@ function useDocsSearch(query: string) {
|
||||
}
|
||||
|
||||
function SearchOverlay({ query, onClose }: { query: string; onClose: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const { results } = useDocsSearch(query);
|
||||
if (!query.trim()) return null;
|
||||
return (
|
||||
<div className="mt-3 rounded-lg border bg-card p-2 shadow-md max-h-96 overflow-auto">
|
||||
{results.length === 0 ? (
|
||||
<p className="px-3 py-2 text-sm text-muted-foreground">Keine Treffer</p>
|
||||
<p className="px-3 py-2 text-sm text-muted-foreground">{t("docs.noResults")}</p>
|
||||
) : (
|
||||
results.map((r) => (
|
||||
<Link
|
||||
@@ -849,7 +855,9 @@ export default function Docs() {
|
||||
} else if (section === "handbook" && param) {
|
||||
content = <HandbookView slug={param} />;
|
||||
} else if (section === "reference" && param === "endpoints" && path[2]) {
|
||||
const tag = reference?.tags.find((tg) => tg.name === path[2]);
|
||||
const tag = reference?.tags.find(
|
||||
(tg) => tg.name.toLowerCase() === path[2].toLowerCase(),
|
||||
);
|
||||
content = tag ? (
|
||||
<EndpointTagView tag={tag} />
|
||||
) : refError || (reference && !tag) ? (
|
||||
@@ -858,7 +866,9 @@ export default function Docs() {
|
||||
<Skeleton className="h-64 w-full" />
|
||||
);
|
||||
} else if (section === "reference" && param === "schemas" && path[2]) {
|
||||
const schema = reference?.schemas.find((s) => s.name === path[2]);
|
||||
const schema = reference?.schemas.find(
|
||||
(s) => s.name.toLowerCase() === path[2].toLowerCase(),
|
||||
);
|
||||
content = schema ? (
|
||||
<SchemaView schema={schema} />
|
||||
) : refError || (reference && !schema) ? (
|
||||
@@ -923,8 +933,28 @@ export default function Docs() {
|
||||
const showSearch = version === null && section !== "releases";
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="mx-auto max-w-6xl space-y-6 pb-10">
|
||||
<div className="min-h-screen bg-background flex flex-col">
|
||||
<header className="border-b bg-card shrink-0">
|
||||
<div className="mx-auto max-w-6xl px-4 md:px-6 h-14 flex items-center justify-between gap-3">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-2 text-primary font-bold text-lg min-w-0"
|
||||
data-testid="link-docs-back"
|
||||
>
|
||||
<Wrench className="w-5 h-5 shrink-0" />
|
||||
<span className="truncate">toolr</span>
|
||||
<span className="hidden md:inline-flex items-center gap-1 text-xs font-normal text-muted-foreground border-l pl-2 ml-1">
|
||||
<ArrowLeft className="w-3.5 h-3.5" />
|
||||
{t("docs.backToApp")}
|
||||
</span>
|
||||
</Link>
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="mx-auto max-w-6xl w-full flex-1 space-y-6 px-4 md:px-6 py-6 pb-12">
|
||||
<DocsHeader
|
||||
versions={releases ?? []}
|
||||
activeVersion={version}
|
||||
@@ -948,6 +978,6 @@ export default function Docs() {
|
||||
<div className="min-w-0">{content}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user