diff --git a/app/(pages)/blog/[slug]/page.tsx b/app/(pages)/blog/[slug]/page.tsx index 617317e..431b958 100644 --- a/app/(pages)/blog/[slug]/page.tsx +++ b/app/(pages)/blog/[slug]/page.tsx @@ -12,6 +12,7 @@ import { cn, getBackgroundImage } from "@/lib/utils" import { Metadata } from "next" import Link from "next/link" import { notFound } from "next/navigation" +import { BlogHeader } from "../sections/BlogHeader" export const dynamic = "force-dynamic" @@ -101,79 +102,7 @@ export default function BlogArticle({ params }: any) { : undefined, }} /> -
- - - {post?.date || post?.tldr ? ( -
- {post?.date && ( -
- {new Date(post?.date).toLocaleDateString("en-US", { - month: "long", - day: "numeric", - year: "numeric", - })} -
- )} - {post?.canonical && ( -
- This post was originally posted in{" "} - - {new URL(post.canonical).hostname.replace(/^www\./, "")} - -
- )} - {post?.tldr && ( - {post?.tldr} - )} -
- ) : null} - {(post?.tags ?? [])?.length > 0 && ( -
- - Tags: - -
- {post?.tags?.map((tag) => ( - - - - ))} -
-
- )} -
-
+
diff --git a/app/(pages)/blog/sections/BlogHeader.tsx b/app/(pages)/blog/sections/BlogHeader.tsx new file mode 100644 index 0000000..dfb0868 --- /dev/null +++ b/app/(pages)/blog/sections/BlogHeader.tsx @@ -0,0 +1,114 @@ +"use client" + +import { blogArticleCardTagCardVariants } from "@/components/blog/blog-article-card" +import { AppContent } from "@/components/ui/app-content" +import { Button } from "@/components/ui/button" +import { Label } from "@/components/ui/label" +import { Markdown } from "@/components/ui/markdown" +import { cn } from "@/lib/utils" +import Link from "next/link" + +interface BlogHeaderProps { + post: any + imageAsCover: boolean +} + +export const BlogHeader = ({ post, imageAsCover }: BlogHeaderProps) => { + const authors = + (post?.authors ?? [])?.length > 0 ? post.authors?.join(", ") : null + + return ( +
+ +
+ + {authors && ( + + {authors} + + )} +
+ + {post?.date || post?.tldr ? ( +
+ {post?.date && ( +
+ {new Date(post?.date).toLocaleDateString("en-US", { + month: "long", + day: "numeric", + year: "numeric", + })} +
+ )} + {post?.canonical && ( +
+ This post was originally posted in{" "} + + {new URL(post.canonical).hostname.replace(/^www\./, "")} + +
+ )} + {post?.tldr && ( + ( +

+ {children} +

+ ), + }} + > + {post?.tldr} +
+ )} +
+ ) : null} + {(post?.tags ?? [])?.length > 0 && ( +
+ + Tags: + +
+ {post?.tags?.map((tag: any) => ( + + + + ))} +
+
+ )} +
+
+ ) +} diff --git a/components/icons.tsx b/components/icons.tsx index 4d540b1..eda7567 100644 --- a/components/icons.tsx +++ b/components/icons.tsx @@ -542,6 +542,39 @@ export const Icons = { /> ), + copied: (props: LucideProps) => ( + + + + ), + copy: (props: LucideProps) => ( + + + + + ), mirror: (props: LucideProps) => ( { + const [copied, setCopied] = React.useState(false) + const [showLink, setShowLink] = React.useState(false) + + const id = React.useMemo(() => { + if (typeof children === "string") { + return generateSectionId(children) + } + const text = React.Children.toArray(children) + .map((child) => { + if (typeof child === "string") return child + if ( + React.isValidElement(child) && + typeof child.props?.children === "string" + ) { + return child.props.children + } + return "" + }) + .join("") + return generateSectionId(text) + }, [children]) + + const copyToClipboard = React.useCallback(() => { + const url = new URL(window.location.href) + url.hash = id + navigator.clipboard.writeText(url.toString()) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + }, [id]) + + const headingClasses = React.useMemo(() => { + switch (level) { + case 1: + return "text-4xl md:text-5xl font-bold" + case 2: + return "text-4xl" + case 3: + return "text-3xl" + case 4: + return "text-xl" + case 5: + return "text-lg font-bold" + default: + return "text-md font-bold" + } + }, [level]) + + const Component = React.createElement( + `h${level}` as keyof JSX.IntrinsicElements, + { + id, + className: `group relative flex items-center gap-2 text-primary ${headingClasses}`, + onMouseEnter: () => setShowLink(true), + onMouseLeave: () => setShowLink(false), + }, + <> + {children} + + + ) + + return Component +} + const REACT_MARKDOWN_CONFIG = (darkMode: boolean): CustomComponents => ({ a: ({ href, children }) => { if (href?.startsWith("#")) { @@ -485,36 +565,12 @@ const REACT_MARKDOWN_CONFIG = (darkMode: boolean): CustomComponents => ({ ) }, - h1: ({ ...props }) => - createMarkdownElement("h1", { - className: "text-primary text-4xl md:text-5xl font-bold", - ...props, - }), - h2: ({ ...props }) => - createMarkdownElement("h2", { - className: "text-primary text-4xl", - ...props, - }), - h3: ({ ...props }) => - createMarkdownElement("h3", { - className: "text-primary text-3xl", - ...props, - }), - h4: ({ ...props }) => - createMarkdownElement("h4", { - className: "text-primary text-xl", - ...props, - }), - h5: ({ ...props }) => - createMarkdownElement("h5", { - className: "text-primary text-lg font-bold", - ...props, - }), - h6: ({ ...props }) => - createMarkdownElement("h6", { - className: "text-primary text-md font-bold", - ...props, - }), + h1: ({ children }) => {children}, + h2: ({ children }) => {children}, + h3: ({ children }) => {children}, + h4: ({ children }) => {children}, + h5: ({ children }) => {children}, + h6: ({ children }) => {children}, code: ({ node, inline, className, children, ...props }) => { const match = /language-(\w+)/.exec(className || "") return !inline ? ( @@ -841,6 +897,21 @@ export const Markdown = ({ }: MarkdownProps) => { const [content, setContent] = React.useState([]) + React.useEffect(() => { + const timer = setTimeout(() => { + const hash = window.location.hash + if (hash) { + const id = hash.slice(1) // Remove the # from the hash + const element = document.getElementById(id) + if (element) { + scrollToElementWithOffset(element) + } + } + }, 100) + + return () => clearTimeout(timer) + }, [content]) + React.useEffect(() => { if (!children) { setContent([])