Files
HangmanLab.Frontend/src/components/Markdowns/MarkdownContent.js
hzhang 3ec528701e feat: apikey alias field + markdown/patch authorship display
- ApiKeyCreationModal: required Alias input (reuse alias = renew, with
  banner); align roles to backend allowlist (guest -> user, default
  user); fix copy bug (generatedKey.key).
- MarkdownContent + PatchCards: show author / created / last modified
  (+ by whom); formatDateTime helper (null -> "—").

Branched off fix/buildconfig-cachebust (carries the contenthash +
BuildConfig fix already deployed to prod).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:51:47 +01:00

118 lines
5.2 KiB
JavaScript

import React, { useEffect, useState } from "react";
import {Link, useParams} from "react-router-dom";
import "katex/dist/katex.min.css";
import "./MarkdownContent.css";
import { Settings2, Pencil, User, Clock, History } from "lucide-react";
import { formatDateTime } from "../../lib/utils";
import MarkdownView from "./MarkdownView";
import PatchCards from "./PatchCards";
import PermissionGuard from "../PermissionGuard";
import {useMarkdown} from "../../utils/queries/markdown-queries";
import {usePath} from "../../utils/queries/path-queries";
import {useMarkdownSetting} from "../../utils/queries/markdown-setting-queries";
import {useMarkdownTemplate} from "../../utils/queries/markdown-template-queries";
import {useMarkdownTemplateSetting} from "../../utils/queries/markdown-template-setting-queries";
import MarkdownSettingModal from "../Modals/MarkdownSettingModal";
import { parseMarkdownContent } from "../../utils/safe-json";
import { Button } from "../ui/button";
import { Spinner } from "../ui/misc";
const MarkdownContent = () => {
const { strId } = useParams();
const id = Number(strId);
const [indexTitle, setIndexTitle] = useState(null);
const [isSettingModalOpen, setSettingModalOpen] = useState(false);
const {data: markdown, isLoading, error} = useMarkdown(id);
const {data: path, isFetching: isPathFetching} = usePath(markdown?.path_id);
const {data: setting, isFetching: isSettingFetching} = useMarkdownSetting(markdown?.setting_id);
const {data: templateSetting, isFetching: isTemplateSettingFetching} = useMarkdownTemplateSetting(setting?.template_setting_id);
const {data: template, isFetching: isTemplateFetching} = useMarkdownTemplate(templateSetting?.template_id);
useEffect(() => {
if(markdown && markdown.title === "index" && path){
setIndexTitle(path.id === 1 ? "Home" : path.name);
}
}, [markdown, path]);
const notReady = isLoading || isPathFetching || isSettingFetching || isTemplateSettingFetching || isTemplateFetching;
if (notReady) {
return (
<div className="flex justify-center py-20">
<Spinner label="Loading content" />
</div>
);
}
if (error) {
return (
<div className="rounded-lg border border-destructive/40 bg-destructive/10 px-4 py-3 font-mono text-sm text-destructive">
Error: {error.message || "Failed to load content"}
</div>
);
}
if (markdown.isMessage) {
return (
<div className="rounded-lg border border-primary/30 bg-primary/10 px-5 py-4">
<h4 className="mb-1 font-mono text-base font-semibold text-primary">
{markdown.title}
</h4>
<p className="text-sm text-foreground/80">{markdown.content}</p>
</div>
);
}
return (
<article>
<div className="mb-6 flex items-start justify-between gap-4 border-b border-border pb-4">
<h1 className="font-mono text-3xl font-bold tracking-tight text-foreground">
{markdown.title === "index" ? indexTitle : markdown.title}
</h1>
<PermissionGuard rolesRequired={['admin']}>
<div className="flex shrink-0 items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setSettingModalOpen(true)}
>
<Settings2 className="h-4 w-4" /> Settings
</Button>
<Button asChild size="sm">
<Link to={`/markdown/edit/${id}`}>
<Pencil className="h-4 w-4" /> Edit
</Link>
</Button>
</div>
</PermissionGuard>
</div>
<div className="-mt-3 mb-6 flex flex-wrap items-center gap-x-5 gap-y-1 font-mono text-xs text-muted-foreground">
<span className="inline-flex items-center gap-1.5">
<User className="h-3.5 w-3.5 text-secondary" />
{markdown.author || "—"}
</span>
<span className="inline-flex items-center gap-1.5">
<Clock className="h-3.5 w-3.5" />
created {formatDateTime(markdown.created_at)}
</span>
<span className="inline-flex items-center gap-1.5">
<History className="h-3.5 w-3.5" />
updated {formatDateTime(markdown.updated_at)}
{markdown.last_modified_by
? ` by ${markdown.last_modified_by}`
: ""}
</span>
</div>
<MarkdownView content={parseMarkdownContent(markdown.content)} template={template}/>
<PatchCards markdownId={id} />
<MarkdownSettingModal
isOpen={isSettingModalOpen}
markdown={markdown}
onClose={() => setSettingModalOpen(false)}
/>
</article>
);
};
export default MarkdownContent;