diff --git a/src/app/(site)/posts/[slug]/page.tsx b/src/app/(site)/posts/[slug]/page.tsx index c71a6f8..0321bdd 100644 --- a/src/app/(site)/posts/[slug]/page.tsx +++ b/src/app/(site)/posts/[slug]/page.tsx @@ -5,10 +5,11 @@ import { GiscusComments } from "@/components/giscus-comments" import { MainBreadcrumbs } from "@/components/layout" import { MarkdownContent } from "@/components/markdown-content" import { PostMeta } from "@/components/post-meta" +import { PostShare } from "@/components/post-share" import { PostTags } from "@/components/post-tags" import { Lead, PageTitle, typography } from "@/components/typography" import { getPublicIntegrations } from "@/config/integrations" -import { siteConfig } from "@/config/site" +import { absoluteUrl, siteConfig } from "@/config/site" import { shouldRenderTableOfContents } from "@/features/post-toc/toc-policy" import { type RenderedMarkdown, renderMarkdown } from "@/lib/markdown" import { findPostBySlug, getAdjacentPosts, getPostSlugs } from "@/lib/posts" @@ -75,6 +76,7 @@ export default async function PostPage({ params }: PostPageProps) { + diff --git a/src/components/icons.tsx b/src/components/icons.tsx index eb0367e..ec96316 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -1,9 +1,11 @@ import { ChevronDownIcon, ChevronRightIcon, + ChevronUpIcon, FileTextIcon, FolderIcon, HomeIcon, + LinkIcon, ListIcon, MailIcon, MonitorIcon, @@ -15,6 +17,8 @@ import { } from "lucide-react" import type { SVGProps } from "react" +// lucide-react 는 브랜드 로고를 담지 않는다(라이선스 정책). 공유 버튼에 쓰는 +// X·Facebook·Telegram 은 각 서비스가 공개한 로고 마크를 직접 그려 넣는다. function GithubIcon(props: SVGProps) { return (
+
@@ -16,6 +20,8 @@ export function AppShell({ children }: AppShellProps) {
{children}
+ +
) } diff --git a/src/components/layout/back-to-top-button.tsx b/src/components/layout/back-to-top-button.tsx new file mode 100644 index 0000000..3bc8311 --- /dev/null +++ b/src/components/layout/back-to-top-button.tsx @@ -0,0 +1,38 @@ +"use client" + +import { useEffect, useState } from "react" +import { Icons } from "@/components/icons" +import { cn } from "@/lib/utils" + +const VISIBILITY_THRESHOLD_PX = 480 + +export function BackToTopButton() { + const [isVisible, setIsVisible] = useState(false) + + useEffect(() => { + const handleScroll = () => { + setIsVisible(window.scrollY > VISIBILITY_THRESHOLD_PX) + } + + handleScroll() + window.addEventListener("scroll", handleScroll, { passive: true }) + + return () => window.removeEventListener("scroll", handleScroll) + }, []) + + return ( + + ) +} diff --git a/src/components/layout/index.ts b/src/components/layout/index.ts index df6eadb..e49c8c1 100644 --- a/src/components/layout/index.ts +++ b/src/components/layout/index.ts @@ -1,3 +1,4 @@ export { AppShell } from "./app-shell" export { MainBreadcrumbs } from "./main-breadcrumbs" export { MainFrame } from "./main-frame" +export { SiteFooter } from "./site-footer" diff --git a/src/components/layout/reading-progress-bar.tsx b/src/components/layout/reading-progress-bar.tsx new file mode 100644 index 0000000..985e78c --- /dev/null +++ b/src/components/layout/reading-progress-bar.tsx @@ -0,0 +1,33 @@ +"use client" + +import { useEffect, useState } from "react" + +export function ReadingProgressBar() { + const [progress, setProgress] = useState(0) + + useEffect(() => { + const handleScroll = () => { + const scrollable = document.documentElement.scrollHeight - window.innerHeight + + setProgress(scrollable > 0 ? Math.min(100, (window.scrollY / scrollable) * 100) : 0) + } + + handleScroll() + window.addEventListener("scroll", handleScroll, { passive: true }) + window.addEventListener("resize", handleScroll) + + return () => { + window.removeEventListener("scroll", handleScroll) + window.removeEventListener("resize", handleScroll) + } + }, []) + + return ( +