diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d4d14b7..a7a7895 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,8 @@ jobs: - name: Build run: npm run build + env: + NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} - name: Check bundle size run: npm run bundle:check diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 1b6e9a8..f68a0a2 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -23,6 +23,8 @@ jobs: - name: Build run: npm run build + env: + NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} - name: Run Lighthouse CI run: | @@ -50,6 +52,8 @@ jobs: - name: Build run: npm run build + env: + NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} - name: Start server run: npm run start & @@ -79,6 +83,8 @@ jobs: - name: Build run: npm run build + env: + NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} - name: Run E2E tests run: npm run test:e2e diff --git a/app/blogs/[slug]/page.tsx b/app/blogs/[slug]/page.tsx index 3175ca7..f7130cf 100644 --- a/app/blogs/[slug]/page.tsx +++ b/app/blogs/[slug]/page.tsx @@ -37,10 +37,19 @@ interface BlogPostPageProps { }>; } -// Generate static params for all blog posts from Notion export async function generateStaticParams() { - const slugs = await getNotionBlogSlugs(); - return slugs.map((slug) => ({ slug })); + try { + const slugs = await getNotionBlogSlugs(); + // Ensure we always return an array, even if empty + if (!Array.isArray(slugs)) { + return []; + } + return slugs.map((slug) => ({ slug })); + } catch (error) { + console.error("Error generating static params for blog posts:", error); + // Return empty array to allow build to continue + return []; + } } // Generate metadata for SEO diff --git a/app/favicon.ico b/app/favicon.ico index 09aca3b..b7e46a0 100644 Binary files a/app/favicon.ico and b/app/favicon.ico differ diff --git a/app/globals.css b/app/globals.css index f3ec305..c6e4165 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1345,6 +1345,37 @@ html:not(.dark) .text-amber-400 { border-color: var(--color-callout-tip-border) !important; } +/* Global custom scrollbar styling */ +/* Firefox */ +* { + scrollbar-width: thin; + scrollbar-color: var(--color-border) transparent; +} + +/* WebKit browsers (Chrome, Safari, Edge) */ +*::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +*::-webkit-scrollbar-track { + background: transparent; +} + +*::-webkit-scrollbar-thumb { + background-color: var(--color-border); + border-radius: 4px; +} + +*::-webkit-scrollbar-thumb:hover { + background-color: var(--color-border-light); +} + +/* Horizontal scrollbars */ +*::-webkit-scrollbar:horizontal { + height: 8px; +} + /* ============================================================================= PERFORMANCE OPTIMIZATIONS diff --git a/components/FooterReveal.tsx b/components/FooterReveal.tsx index ee5695a..5c395c6 100644 --- a/components/FooterReveal.tsx +++ b/components/FooterReveal.tsx @@ -16,15 +16,6 @@ export function FooterReveal() { className="w-[120%] max-w-none h-auto opacity-70" /> - - {/* Bottom copyright bar */} -
-
-

- © {new Date().getFullYear()} Procedure. All rights reserved. -

-
-
{/* Spacer to push content up and allow scrolling to reveal */} diff --git a/components/blog/BlogTableOfContents.tsx b/components/blog/BlogTableOfContents.tsx index 01a8adc..9dd59c2 100644 --- a/components/blog/BlogTableOfContents.tsx +++ b/components/blog/BlogTableOfContents.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; export interface TOCHeading { id: string; @@ -15,6 +15,62 @@ interface BlogTableOfContentsProps { export function BlogTableOfContents({ headings }: BlogTableOfContentsProps) { const [activeId, setActiveId] = useState(""); const [readProgress, setReadProgress] = useState(0); + const scrollContainerRef = useRef(null); + const activeButtonRef = useRef(null); + const isUserScrollingRef = useRef(false); + + // Auto-scroll active item into view + useEffect(() => { + if (!activeId || !activeButtonRef.current || !scrollContainerRef.current) { + return; + } + + // Don't auto-scroll if user is manually scrolling + if (isUserScrollingRef.current) { + return; + } + + const button = activeButtonRef.current; + const container = scrollContainerRef.current; + + // Check if button is visible in container + const buttonRect = button.getBoundingClientRect(); + const containerRect = container.getBoundingClientRect(); + + const isAboveViewport = buttonRect.top < containerRect.top; + const isBelowViewport = buttonRect.bottom > containerRect.bottom; + + if (isAboveViewport || isBelowViewport) { + // Scroll the button into view with some padding + button.scrollIntoView({ + behavior: "smooth", + block: "nearest", + }); + } + }, [activeId]); + + // Handle user scrolling in TOC container + useEffect(() => { + const container = scrollContainerRef.current; + if (!container) return; + + let scrollTimeout: NodeJS.Timeout; + + const handleTOCScroll = () => { + isUserScrollingRef.current = true; + // Reset flag after scrolling stops + clearTimeout(scrollTimeout); + scrollTimeout = setTimeout(() => { + isUserScrollingRef.current = false; + }, 500); + }; + + container.addEventListener("scroll", handleTOCScroll, { passive: true }); + return () => { + container.removeEventListener("scroll", handleTOCScroll); + clearTimeout(scrollTimeout); + }; + }, []); // Scroll spy and progress tracking useEffect(() => { @@ -53,6 +109,12 @@ export function BlogTableOfContents({ headings }: BlogTableOfContentsProps) { } const handleClick = (id: string) => { + // Prevent auto-scroll when user clicks + isUserScrollingRef.current = true; + setTimeout(() => { + isUserScrollingRef.current = false; + }, 1000); + const element = document.getElementById(id); if (element) { const offset = 100; @@ -68,9 +130,9 @@ export function BlogTableOfContents({ headings }: BlogTableOfContentsProps) { return (