Files
gabrielkaszewski-next/components/markdown-content.tsx

309 lines
7.5 KiB
TypeScript

"use client";
import React, { createContext, useContext } from "react";
import ReactMarkdown from "react-markdown";
import type { Components } from "react-markdown";
import type { Element } from "hast";
// Lets the `code` renderer know it's inside a `pre` block vs. inline.
// React renders parent-to-child, so the Provider in `pre` is active
// when `code` renders inside it.
const InsidePreContext = createContext(false);
function GoldDot() {
return (
<span
style={{
width: 8,
height: 8,
borderRadius: "50%",
background: "linear-gradient(135deg, #facc15, #f97316)",
boxShadow: "0 0 8px rgba(250, 204, 21, 0.7)",
flexShrink: 0,
display: "inline-block",
}}
/>
);
}
function BlueDot() {
return (
<span
style={{
width: 8,
height: 8,
borderRadius: "50%",
background: "linear-gradient(135deg, #60a5fa, #34d399)",
boxShadow: "0 0 8px rgba(96, 165, 250, 0.6)",
flexShrink: 0,
display: "inline-block",
}}
/>
);
}
function KeyBadge({ children }: { children: React.ReactNode }) {
return (
<span
style={{
background: "rgba(250, 204, 21, 0.12)",
border: "1px solid rgba(250, 204, 21, 0.35)",
borderRadius: 6,
padding: "1px 10px",
color: "#fde68a",
fontWeight: 600,
fontSize: "0.875rem",
whiteSpace: "nowrap",
}}
>
{children}
</span>
);
}
// Extracts plain text from a HAST element's text children,
// stripping a trailing colon (e.g. "Backend:" → "Backend").
function extractHastText(node: Element): string {
return (node.children ?? [])
.filter((c): c is { type: "text"; value: string } => c.type === "text")
.map((c) => c.value)
.join("")
.replace(/:$/, "")
.trim();
}
const components: Components = {
h1: ({ children }) => (
<h1
style={{
fontSize: "2.4rem",
fontWeight: 800,
margin: "0 0 1.5rem 0",
background: "linear-gradient(90deg, #facc15, #60a5fa)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
letterSpacing: "-0.02em",
}}
>
{children}
</h1>
),
h2: ({ children }) => (
<div style={{ margin: "1.75rem 0 0.75rem 0" }}>
<h2
style={{
fontSize: "1.25rem",
fontWeight: 700,
margin: 0,
background: "linear-gradient(90deg, #facc15, #60a5fa, #34d399)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
letterSpacing: "0.02em",
}}
>
{children}
</h2>
<span
style={{
display: "block",
height: 2,
marginTop: 6,
background: "linear-gradient(90deg, #facc15, #60a5fa, transparent)",
borderRadius: 2,
}}
/>
</div>
),
h3: ({ children }) => (
<div style={{ margin: "1.25rem 0 0.5rem 0" }}>
<h3
style={{
fontSize: "1.1rem",
fontWeight: 600,
margin: 0,
background: "linear-gradient(90deg, #facc15, #60a5fa)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
}}
>
{children}
</h3>
<span
style={{
display: "block",
height: 1,
marginTop: 4,
background:
"linear-gradient(90deg, rgba(250,204,21,0.5), rgba(96,165,250,0.5), transparent)",
borderRadius: 1,
}}
/>
</div>
),
p: ({ children }) => (
<p
style={{
color: "#cbd5e1",
lineHeight: 1.75,
margin: "0 0 1.25rem 0",
fontSize: "1.05rem",
}}
>
{children}
</p>
),
ul: ({ children }) => (
<ul
style={{
listStyle: "none",
padding: 0,
margin: "0 0 1.25rem 0",
display: "flex",
flexDirection: "column",
gap: 9,
}}
>
{children}
</ul>
),
li: ({ node, children }) => {
// Inspect HAST to detect the `- **Key:** value` pattern.
// HAST tagName is always 'strong' regardless of custom component overrides.
const firstHast = node?.children?.[0];
const isKeyValue =
firstHast?.type === "element" &&
(firstHast as Element).tagName === "strong";
if (isKeyValue) {
const keyText = extractHastText(firstHast as Element);
// Skip the first rendered child (the <strong>) — we replace it with KeyBadge.
const rest = React.Children.toArray(children).slice(1);
return (
<li
style={{
display: "flex",
alignItems: "center",
gap: 10,
fontSize: "1rem",
}}
>
<GoldDot />
<KeyBadge>{keyText}</KeyBadge>
<span style={{ color: "#94a3b8" }}>{rest}</span>
</li>
);
}
return (
<li
style={{
display: "flex",
alignItems: "center",
gap: 10,
fontSize: "1rem",
color: "#e2e8f0",
}}
>
<BlueDot />
{children}
</li>
);
},
a: ({ href, children }) => (
<a
href={href}
target={href?.startsWith("http") ? "_blank" : undefined}
rel={href?.startsWith("http") ? "noopener noreferrer" : undefined}
style={{
background: "linear-gradient(90deg, #facc15, #60a5fa)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
fontWeight: 600,
textDecoration: "underline",
textDecorationColor: "rgba(250, 204, 21, 0.5)",
cursor: "pointer",
}}
>
{children}
</a>
),
strong: ({ children }) => (
<strong style={{ color: "#fde68a", fontWeight: 700 }}>{children}</strong>
),
em: ({ children }) => (
<em style={{ color: "#e2e8f0", fontStyle: "italic" }}>{children}</em>
),
// `pre` wraps `code` for fenced code blocks. The Provider lets the
// nested `code` renderer know it's in a block context (not inline).
pre: ({ children }) => (
<InsidePreContext.Provider value={true}>
<pre
style={{
background: "rgba(255, 255, 255, 0.04)",
border: "1px solid rgba(255, 255, 255, 0.12)",
borderTop: "2px solid rgba(250, 204, 21, 0.6)",
borderRadius: "0 0 10px 10px",
padding: "18px 22px",
overflowX: "auto",
margin: "0 0 1.25rem 0",
}}
>
{children}
</pre>
</InsidePreContext.Provider>
),
code: ({ children }) => {
const insidePre = useContext(InsidePreContext);
if (insidePre) {
return (
<code
style={{
fontFamily: "var(--font-mono, 'Geist Mono', monospace)",
fontSize: "0.9rem",
color: "#e2e8f0",
lineHeight: 1.65,
}}
>
{children}
</code>
);
}
// Inline code
return (
<code
style={{
background: "rgba(250, 204, 21, 0.1)",
border: "1px solid rgba(250, 204, 21, 0.3)",
borderRadius: 5,
padding: "1px 7px",
color: "#fde68a",
fontFamily: "var(--font-mono, 'Geist Mono', monospace)",
fontSize: "0.875em",
}}
>
{children}
</code>
);
},
};
export default function MarkdownContent({ content }: { content: string }) {
return <ReactMarkdown components={components}>{content}</ReactMarkdown>;
}