Add central Markdown component with spoiler support

This commit is contained in:
Jared Schoeny
2025-12-27 16:10:11 -10:00
parent 23297cb4d9
commit ace34c4408
13 changed files with 186 additions and 86 deletions

View File

@@ -1,9 +1,7 @@
"use client";
import React from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeSlug from "rehype-slug";
import Markdown from "@/components/Markdown/Markdown";
import TagSelector from "@/components/Submit/TagSelector";
import { baseRoms } from "@/data/baseRoms";
import Image from "next/image";
@@ -346,7 +344,7 @@ export default function HackEditForm({ slug, initial }: HackEditFormProps) {
/>
) : (
<div className={`prose max-w-none rounded-md min-h-[14rem] px-3 py-2 ring-1 ring-inset ${descriptionChanged ? 'ring-[var(--ring)] bg-[var(--surface-2)]' : 'bg-[var(--surface-2)] ring-[var(--border)]'} ${description ? "" : "text-foreground/60 text-sm"}`}>
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeSlug]}>{description || "Nothing to preview yet."}</ReactMarkdown>
<Markdown headingLevelOffset={1}>{description || "Nothing to preview yet."}</Markdown>
</div>
)}
</div>

View File

@@ -10,9 +10,7 @@ import { presignCoverUpload } from "@/app/hack/actions";
import { DndContext, PointerSensor, closestCenter, useSensor, useSensors } from "@dnd-kit/core";
import { SortableContext, arrayMove, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeSlug from "rehype-slug";
import Markdown from "@/components/Markdown/Markdown";
import { RxDragHandleDots2 } from "react-icons/rx";
import { FaDiscord, FaTwitter, FaGithub } from "react-icons/fa6";
import PokeCommunityIcon from "@/components/Icons/PokeCommunityIcon";
@@ -900,7 +898,7 @@ export default function HackSubmitForm({
</div>
{isDummy ? (
<div className="prose max-w-none h-36 rounded-md bg-[var(--surface-2)] px-3 py-2 ring-1 ring-inset ring-[var(--border)] text-foreground/60 select-none">
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeSlug]}>{description || "Write a longer markdown description here."}</ReactMarkdown>
<Markdown headingLevelOffset={1}>{description || "Write a longer markdown description here."}</Markdown>
</div>
) : !showMdPreview ? (
<textarea
@@ -912,7 +910,7 @@ export default function HackSubmitForm({
/>
) : (
<div className={`prose max-w-none rounded-md bg-[var(--surface-2)] min-h-[14rem] px-3 py-2 ring-1 ring-inset ring-[var(--border)] ${description ? "" : "text-foreground/60 text-sm"}`}>
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeSlug]}>{description || "Nothing to preview yet."}</ReactMarkdown>
<Markdown headingLevelOffset={1}>{description || "Nothing to preview yet."}</Markdown>
</div>
)}
</div>

View File

@@ -1,9 +1,7 @@
"use client";
import React, { useState, useEffect, useRef } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeSlug from "rehype-slug";
import Markdown from "@/components/Markdown/Markdown";
import { FaChevronDown, FaChevronUp, FaStar, FaDownload, FaTrash, FaRotateLeft, FaUpload, FaCheck, FaPlus } from "react-icons/fa6";
import { FiEdit2, FiEdit, FiX } from "react-icons/fi";
import VersionActions from "@/components/Hack/VersionActions";
@@ -262,20 +260,7 @@ export default function VersionList({ patches, currentPatchId, canEdit, hackSlug
/>
) : (
<div className="prose prose-sm max-w-none text-foreground/80">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeSlug]}
components={{
h1: 'h2',
h2: 'h3',
h3: 'h4',
h4: 'h5',
h5: 'h6',
h6: 'h6',
}}
>
{patch.changelog || ""}
</ReactMarkdown>
<Markdown headingLevelOffset={1}>{patch.changelog || ""}</Markdown>
</div>
)}
</div>

View File

@@ -0,0 +1,50 @@
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeSlug from "rehype-slug";
import remarkSpoiler from "@/utils/markdown/remark-spoiler";
import Spoiler from "./Spoiler";
interface MarkdownProps {
children: string;
className?: string;
headingLevelOffset?: number;
}
export default function Markdown({
children,
headingLevelOffset = 0,
}: MarkdownProps) {
// Build heading demotion components if offset is provided
const headingComponents: Record<string, string> = {};
if (headingLevelOffset > 0) {
for (let i = 1; i <= 6; i++) {
const targetLevel = Math.min(i + headingLevelOffset, 6);
headingComponents[`h${i}`] = `h${targetLevel}`;
}
}
return (
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkSpoiler]}
rehypePlugins={[rehypeSlug]}
components={{
...headingComponents,
span: ({ node, children, className, ...props }: any) => {
// Check if this span has the markdown-spoiler class (from our remark plugin)
if (
className === "markdown-spoiler" ||
(Array.isArray(node?.properties?.className) &&
node.properties.className.includes("markdown-spoiler"))
) {
return <Spoiler>{children}</Spoiler>;
}
// Default span rendering
return <span className={className} {...props}>{children}</span>;
},
}}
>
{children}
</ReactMarkdown>
);
}

View File

@@ -0,0 +1,25 @@
"use client";
import { useState } from "react";
interface SpoilerProps {
children: React.ReactNode;
}
export default function Spoiler({ children }: SpoilerProps) {
const [revealed, setRevealed] = useState(false);
return (
<span
onClick={() => setRevealed(true)}
className={`inline rounded px-1 transition-colors duration-150 ${
revealed
? "bg-foreground/20 text-foreground cursor-text"
: "bg-foreground/50 text-transparent cursor-pointer hover:bg-foreground/60"
}`}
>
{children}
</span>
);
}