mirror of
https://github.com/Dokploy/website
synced 2025-06-26 18:16:01 +00:00
feat: add clickable heading links with copy-to-clipboard functionality
This commit is contained in:
100
apps/website/app/[locale]/blog/[slug]/components/Headings.tsx
Normal file
100
apps/website/app/[locale]/blog/[slug]/components/Headings.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import type { DetailedHTMLProps, HTMLAttributes } from "react";
|
||||||
|
import slugify from "slugify";
|
||||||
|
|
||||||
|
type HeadingProps = DetailedHTMLProps<
|
||||||
|
HTMLAttributes<HTMLHeadingElement>,
|
||||||
|
HTMLHeadingElement
|
||||||
|
>;
|
||||||
|
|
||||||
|
function LinkIcon() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className="inline-block w-4 h-4 ml-2 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function H1({ children, ...props }: HeadingProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const id = slugify(children?.toString() || "", {
|
||||||
|
lower: true,
|
||||||
|
strict: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
router.push(`#${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<h1
|
||||||
|
id={id}
|
||||||
|
onClick={handleClick}
|
||||||
|
className="group text-xl md:text-2xl xl:text-3xl text-primary font-bold mt-8 mb-4 cursor-pointer hover:text-primary/80 transition-colors"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<LinkIcon />
|
||||||
|
</h1>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function H2({ children, ...props }: HeadingProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const id = slugify(children?.toString() || "", {
|
||||||
|
lower: true,
|
||||||
|
strict: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
router.push(`#${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<h2
|
||||||
|
id={id}
|
||||||
|
onClick={handleClick}
|
||||||
|
className="group text-2xl text-primary/90 font-semibold mt-6 mb-3 cursor-pointer hover:text-primary/80 transition-colors"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<LinkIcon />
|
||||||
|
</h2>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function H3({ children, ...props }: HeadingProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const id = slugify(children?.toString() || "", {
|
||||||
|
lower: true,
|
||||||
|
strict: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
router.push(`#${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<h3
|
||||||
|
id={id}
|
||||||
|
onClick={handleClick}
|
||||||
|
className="group text-xl text-primary/90 font-semibold mt-4 mb-2 cursor-pointer hover:text-primary/80 transition-colors"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<LinkIcon />
|
||||||
|
</h3>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import slugify from "slugify";
|
|||||||
import TurndownService from "turndown";
|
import TurndownService from "turndown";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import * as turndownPluginGfm from "turndown-plugin-gfm";
|
import * as turndownPluginGfm from "turndown-plugin-gfm";
|
||||||
|
import { H1, H2, H3 } from "./components/Headings";
|
||||||
import { TableOfContents } from "./components/TableOfContents";
|
import { TableOfContents } from "./components/TableOfContents";
|
||||||
import { ZoomableImage } from "./components/ZoomableImage";
|
import { ZoomableImage } from "./components/ZoomableImage";
|
||||||
|
|
||||||
@@ -172,55 +173,17 @@ export default async function BlogPostPage({ params }: Props) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const components: Partial<Components> = {
|
const components: Partial<Components> = {
|
||||||
h1: ({ node, ...props }) => {
|
h1: H1,
|
||||||
const id = slugify(props.children?.toString() || "", {
|
h2: H2,
|
||||||
lower: true,
|
h3: H3,
|
||||||
strict: true,
|
p: ({ node, children, ...props }) => (
|
||||||
});
|
<p
|
||||||
return (
|
className="text-base text-muted-foreground leading-relaxed mb-4"
|
||||||
<h1
|
{...props}
|
||||||
id={id}
|
>
|
||||||
className="text-xl md:text-2xl xl:text-3xl text-primary font-bold mt-8 mb-4"
|
{children}
|
||||||
{...props}
|
</p>
|
||||||
/>
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
h2: ({ node, ...props }) => {
|
|
||||||
const id = slugify(props.children?.toString() || "", {
|
|
||||||
lower: true,
|
|
||||||
strict: true,
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<h2
|
|
||||||
id={id}
|
|
||||||
className="text-2xl text-primary/90 font-semibold mt-6 mb-3"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
h3: ({ node, ...props }) => {
|
|
||||||
const id = slugify(props.children?.toString() || "", {
|
|
||||||
lower: true,
|
|
||||||
strict: true,
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<h3
|
|
||||||
id={id}
|
|
||||||
className="text-xl text-primary/90 font-semibold mt-4 mb-2"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
p: ({ node, children, ...props }) => {
|
|
||||||
return (
|
|
||||||
<p
|
|
||||||
className="text-base text-muted-foreground leading-relaxed mb-4"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
a: ({ node, href, ...props }) => (
|
a: ({ node, href, ...props }) => (
|
||||||
<a
|
<a
|
||||||
href={href}
|
href={href}
|
||||||
|
|||||||
Reference in New Issue
Block a user