Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ jobs:

- name: Build
run: npm run build
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}

- name: Run Lighthouse CI
run: |
Expand Down Expand Up @@ -50,6 +52,8 @@ jobs:

- name: Build
run: npm run build
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}

- name: Start server
run: npm run start &
Expand Down Expand Up @@ -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
Expand Down
15 changes: 12 additions & 3 deletions app/blogs/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Binary file modified app/favicon.ico
Binary file not shown.
31 changes: 31 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 0 additions & 9 deletions components/FooterReveal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,6 @@ export function FooterReveal() {
className="w-[120%] max-w-none h-auto opacity-70"
/>
</div>

{/* Bottom copyright bar */}
<div className="absolute bottom-0 left-0 right-0 py-6 px-6">
<div className="max-w-7xl mx-auto flex flex-col sm:flex-row justify-between items-center gap-4">
<p className="text-sm text-text-muted">
&copy; {new Date().getFullYear()} Procedure. All rights reserved.
</p>
</div>
</div>
</div>

{/* Spacer to push content up and allow scrolling to reveal */}
Expand Down
78 changes: 72 additions & 6 deletions components/blog/BlogTableOfContents.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";

export interface TOCHeading {
id: string;
Expand All @@ -15,6 +15,62 @@ interface BlogTableOfContentsProps {
export function BlogTableOfContents({ headings }: BlogTableOfContentsProps) {
const [activeId, setActiveId] = useState<string>("");
const [readProgress, setReadProgress] = useState(0);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const activeButtonRef = useRef<HTMLButtonElement>(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(() => {
Expand Down Expand Up @@ -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;
Expand All @@ -68,25 +130,29 @@ export function BlogTableOfContents({ headings }: BlogTableOfContentsProps) {

return (
<aside>
<div className="bg-surface-elevated rounded-xl border border-border p-6">
<div className="bg-surface-elevated rounded-xl border border-border p-6 max-h-[calc(100vh-8rem)] flex flex-col">
{/* Progress bar */}
<div className="h-1 bg-surface rounded-full mb-6 overflow-hidden">
<div className="h-1 bg-surface rounded-full mb-6 overflow-hidden shrink-0">
<div
className="h-full bg-accent rounded-full transition-all duration-150"
style={{ width: `${readProgress}%` }}
/>
</div>

{/* Title */}
<h4 className="text-sm font-semibold text-text-primary uppercase tracking-wider mb-4">
<h4 className="text-sm font-semibold text-text-primary uppercase tracking-wider mb-4 shrink-0">
On This Page
</h4>

{/* TOC Items */}
<div className="space-y-1">
{/* TOC Items - Scrollable */}
<div
ref={scrollContainerRef}
className="space-y-1 overflow-y-auto flex-1 min-h-0 pr-2 -mr-2"
>
{headings.map((heading) => (
<button
key={heading.id}
ref={activeId === heading.id ? activeButtonRef : null}
onClick={() => handleClick(heading.id)}
className={`block w-full text-left text-sm py-1.5 border-l-2 transition-colors ${
heading.type === "heading_1"
Expand Down
10 changes: 7 additions & 3 deletions components/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export function Footer() {
d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z"
/>
</svg>
San Francisco (USA) | Mumbai (IND)
San Francisco (US) | Mumbai (IN)
</div>
</div>

Expand Down Expand Up @@ -224,11 +224,11 @@ export function Footer() {
</div>

{/* Certification Badges */}
<div className="pt-8 border-t border-border mb-8">
<div className="pt-12 border-t border-border">
<p className="text-xs text-text-muted uppercase tracking-widest mb-6 text-center">
Security & Best Practices
</p>
<div className="flex flex-wrap items-center justify-center gap-4">
<div className="flex flex-wrap items-center justify-center gap-4 mb-6">
<ComplianceBadge
icon="shield"
title="Secure SDLC"
Expand All @@ -241,7 +241,11 @@ export function Footer() {
subtitle="Practices"
iconColor="blue"
/>
{/* Bottom copyright bar */}
</div>
<p className="text-sm text-text-muted text-center">
&copy; {new Date().getFullYear()} Procedure. All rights reserved.
</p>
</div>
</div>
</footer>
Expand Down
Loading
Loading