Files
pargar/frontend/src/components/safe-markdown.tsx
T
ahpandClaude Sonnet 4.6 40fa0b8581 fix: quality audit — memory leaks, dead prose classes, stale state scope
- safe-markdown: add `cancelled` flag to async useEffect; no more
  setState after unmount when component is destroyed mid-import
- rich-editor: same cleanup for preview tab effect; also remove dead
  `prose prose-sm max-w-none` classes (Tailwind Typography not installed)
  and replace with explicit arbitrary-selector markdown styling that
  actually renders using existing design tokens
- profile-certificate-slot: drop dead `ORDER_LABEL`, `order` variable,
  and `physicalOrder` from `MineCertificateResponse`; physical orders
  are disabled — these were leftover references causing lint noise
- learning-panel: change `issuingCert: boolean` → `issuingCertFor:
  number | null` so only the targeted user's button shows a spinner;
  a boolean shared across all users caused the wrong row to animate
  when the admin switched selected users mid-request

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-18 10:29:58 +03:30

63 lines
1.6 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";
export function SafeMarkdown({
text,
className,
lines,
}: {
text: string;
className?: string;
lines?: 2 | 3;
}) {
const [html, setHtml] = useState("");
useEffect(() => {
if (!text) { setHtml(""); return; }
let cancelled = false;
void (async () => {
const { marked } = await import("marked");
const DOMPurify = (await import("dompurify")).default;
const raw = await marked(text);
if (!cancelled) setHtml(DOMPurify.sanitize(raw));
})();
return () => { cancelled = true; };
}, [text]);
const mdClasses =
"[&_strong]:font-bold [&_em]:italic [&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:text-[0.85em] [&_h2]:text-base [&_h2]:font-bold [&_h2]:mt-3 [&_h3]:text-sm [&_h3]:font-bold [&_h3]:mt-2 [&_ul]:list-disc [&_ul]:pr-5 [&_ol]:list-decimal [&_ol]:pr-5 [&_li]:mt-0.5 [&_p]:leading-relaxed [&_p+p]:mt-2 [&_a]:underline [&_a]:text-primary";
if (!html) {
return (
<div
dir="auto"
className={cn(
"break-words text-sm [unicode-bidi:plaintext]",
lines === 2 && "line-clamp-2",
lines === 3 && "line-clamp-3",
!lines && "whitespace-pre-wrap",
className,
)}
>
{text}
</div>
);
}
return (
<div
dir="auto"
className={cn(
"break-words text-sm [unicode-bidi:plaintext]",
mdClasses,
lines === 2 && "line-clamp-2",
lines === 3 && "line-clamp-3",
className,
)}
dangerouslySetInnerHTML={{ __html: html }}
/>
);
}