mirror of
https://github.com/Hackdex-App/hackdex-website.git
synced 2026-09-09 09:26:35 -05:00
Add central Markdown component with spoiler support
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
50
src/components/Markdown/Markdown.tsx
Normal file
50
src/components/Markdown/Markdown.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
25
src/components/Markdown/Spoiler.tsx
Normal file
25
src/components/Markdown/Spoiler.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user