diff --git a/.gitignore b/.gitignore index d8b6db9779..31da6c6dc2 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ yarn-error.log* # vercel .vercel +package-lock.json diff --git a/components/NotionPageWrapper.tsx b/components/NotionPageWrapper.tsx new file mode 100644 index 0000000000..ac7ea8aaab --- /dev/null +++ b/components/NotionPageWrapper.tsx @@ -0,0 +1,20 @@ +import { NotionPage as OriginalNotionPage } from './NotionPage' +import dynamic from 'next/dynamic' +import type { PageProps } from '@/lib/types' +import { useRouter } from 'next/router' + +const ReadingProgress = dynamic(() => import('./effects/ReadingProgress'), { + ssr: false, +}) + +export function NotionPageWrapper(props: PageProps) { + const router = useRouter() + const isBlogPost = router.pathname === '/[pageId]' && props.pageId !== '16ccc94eb4cf4b3d85fb31ac7be58e87' + + return ( + <> + {isBlogPost && } + + + ) +} diff --git a/components/effects/CustomCursor.module.css b/components/effects/CustomCursor.module.css new file mode 100644 index 0000000000..1465cfe3a3 --- /dev/null +++ b/components/effects/CustomCursor.module.css @@ -0,0 +1,42 @@ +.cursor, +.cursorFollower { + position: fixed; + pointer-events: none; + z-index: 9999; + mix-blend-mode: difference; + transition: transform 0.15s ease-out; +} + +.cursor { + width: 8px; + height: 8px; + background: white; + border-radius: 50%; + transform: translate(-50%, -50%); +} + +.cursorFollower { + width: 32px; + height: 32px; + border: 1px solid rgba(255, 255, 255, 0.5); + border-radius: 50%; + transform: translate(-50%, -50%); + transition: transform 0.3s ease-out, width 0.3s ease, height 0.3s ease; +} + +.cursor.hovering { + transform: translate(-50%, -50%) scale(1.5); +} + +.cursorFollower.hovering { + width: 50px; + height: 50px; + border-color: white; +} + +@media (pointer: coarse) { + .cursor, + .cursorFollower { + display: none; + } +} diff --git a/components/effects/CustomCursor.tsx b/components/effects/CustomCursor.tsx new file mode 100644 index 0000000000..5242edeac4 --- /dev/null +++ b/components/effects/CustomCursor.tsx @@ -0,0 +1,84 @@ +'use client' + +import { useEffect, useState } from 'react' +import styles from './CustomCursor.module.css' + +export default function CustomCursor() { + const [position, setPosition] = useState({ x: 0, y: 0 }) + const [isHovering, setIsHovering] = useState(false) + const [isMobile, setIsMobile] = useState(false) + + useEffect(() => { + // Check if device is mobile/touch + const checkMobile = () => { + setIsMobile(window.matchMedia('(pointer: coarse)').matches) + } + + checkMobile() + window.addEventListener('resize', checkMobile) + + if (isMobile) return + + const updatePosition = (e: MouseEvent) => { + setPosition({ x: e.clientX, y: e.clientY }) + } + + const handleMouseEnter = (e: Event) => { + const target = e.target as HTMLElement + if ( + target.tagName === 'A' || + target.tagName === 'BUTTON' || + target.closest('a') || + target.closest('button') || + target.classList.contains('cursor-interactive') + ) { + setIsHovering(true) + } + } + + const handleMouseLeave = (e: Event) => { + const target = e.target as HTMLElement + if ( + target.tagName === 'A' || + target.tagName === 'BUTTON' || + target.closest('a') || + target.closest('button') || + target.classList.contains('cursor-interactive') + ) { + setIsHovering(false) + } + } + + window.addEventListener('mousemove', updatePosition) + document.addEventListener('mouseenter', handleMouseEnter, true) + document.addEventListener('mouseleave', handleMouseLeave, true) + + return () => { + window.removeEventListener('mousemove', updatePosition) + document.removeEventListener('mouseenter', handleMouseEnter, true) + document.removeEventListener('mouseleave', handleMouseLeave, true) + window.removeEventListener('resize', checkMobile) + } + }, [isMobile]) + + if (isMobile) return null + + return ( + <> +
+
+ + ) +} diff --git a/components/effects/PageTransition.module.css b/components/effects/PageTransition.module.css new file mode 100644 index 0000000000..22923e0c08 --- /dev/null +++ b/components/effects/PageTransition.module.css @@ -0,0 +1,11 @@ +.overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: var(--bg-color); + opacity: 0; + pointer-events: none; + z-index: 9998; +} diff --git a/components/effects/PageTransition.tsx b/components/effects/PageTransition.tsx new file mode 100644 index 0000000000..9b8e04e19b --- /dev/null +++ b/components/effects/PageTransition.tsx @@ -0,0 +1,54 @@ +'use client' + +import { useEffect } from 'react' +import { useRouter } from 'next/router' +import styles from './PageTransition.module.css' + +export default function PageTransition({ children }: { children: React.ReactNode }) { + const router = useRouter() + + useEffect(() => { + const initGSAP = async () => { + const gsap = (await import('gsap')).default + + const handleRouteChangeStart = () => { + gsap.to('.page-transition-overlay', { + duration: 0.3, + opacity: 1, + ease: 'power2.inOut', + }) + } + + const handleRouteChangeComplete = () => { + gsap.to('.page-transition-overlay', { + duration: 0.3, + opacity: 0, + ease: 'power2.inOut', + delay: 0.1, + }) + + // Scroll to top on route change + window.scrollTo(0, 0) + } + + router.events.on('routeChangeStart', handleRouteChangeStart) + router.events.on('routeChangeComplete', handleRouteChangeComplete) + router.events.on('routeChangeError', handleRouteChangeComplete) + + return () => { + router.events.off('routeChangeStart', handleRouteChangeStart) + router.events.off('routeChangeComplete', handleRouteChangeComplete) + router.events.off('routeChangeError', handleRouteChangeComplete) + } + } + + initGSAP() + }, [router]) + + return ( + <> +
+ {children} + + ) +} diff --git a/components/effects/ReadingProgress.module.css b/components/effects/ReadingProgress.module.css new file mode 100644 index 0000000000..af9fd6166b --- /dev/null +++ b/components/effects/ReadingProgress.module.css @@ -0,0 +1,15 @@ +.progressBar { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 3px; + background: var(--fg-color-0); + z-index: 9999; +} + +.progressFill { + height: 100%; + background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); + transition: width 0.1s ease-out; +} diff --git a/components/effects/ReadingProgress.tsx b/components/effects/ReadingProgress.tsx new file mode 100644 index 0000000000..ae79983245 --- /dev/null +++ b/components/effects/ReadingProgress.tsx @@ -0,0 +1,34 @@ +'use client' + +import { useEffect, useState } from 'react' +import styles from './ReadingProgress.module.css' + +export default function ReadingProgress() { + const [progress, setProgress] = useState(0) + + useEffect(() => { + const updateProgress = () => { + const scrollTop = window.scrollY + const docHeight = document.documentElement.scrollHeight - window.innerHeight + const scrollPercent = (scrollTop / docHeight) * 100 + + setProgress(Math.min(scrollPercent, 100)) + } + + window.addEventListener('scroll', updateProgress, { passive: true }) + updateProgress() // Initial call + + return () => { + window.removeEventListener('scroll', updateProgress) + } + }, []) + + return ( +
+
+
+ ) +} diff --git a/components/effects/SmoothScroll.tsx b/components/effects/SmoothScroll.tsx new file mode 100644 index 0000000000..065d4cd7ac --- /dev/null +++ b/components/effects/SmoothScroll.tsx @@ -0,0 +1,58 @@ +'use client' + +import { useEffect } from 'react' + +export default function SmoothScroll() { + useEffect(() => { + let lenis: any = null + + const initLenis = async () => { + const Lenis = (await import('lenis')).default + + lenis = new Lenis({ + duration: 1.2, + easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), + orientation: 'vertical', + gestureOrientation: 'vertical', + smoothWheel: true, + wheelMultiplier: 1, + touchMultiplier: 2, + }) + + function raf(time: number) { + lenis.raf(time) + requestAnimationFrame(raf) + } + + requestAnimationFrame(raf) + + // Integrate with GSAP ScrollTrigger if available + if (typeof window !== 'undefined') { + const gsapModule = await import('gsap') + const ScrollTriggerModule = await import('gsap/ScrollTrigger') + const gsap = gsapModule.default + const ScrollTrigger = ScrollTriggerModule.default + + gsap.registerPlugin(ScrollTrigger) + + lenis.on('scroll', ScrollTrigger.update) + + gsap.ticker.add((time) => { + lenis.raf(time * 1000) + }) + + gsap.ticker.lagSmoothing(0) + } + } + + initLenis() + + return () => { + if (lenis) { + lenis.destroy() + } + } + }, []) + + return null +} diff --git a/components/home/About.tsx b/components/home/About.tsx new file mode 100644 index 0000000000..2c43313a5d --- /dev/null +++ b/components/home/About.tsx @@ -0,0 +1,86 @@ +'use client' + +import { useEffect, useRef } from 'react' +import styles from '@/styles/home.module.css' + +const roles = [ + { emoji: '๐Ÿข', title: 'CEO', company: 'Autonomous Technologies' }, + { emoji: '๐Ÿง ', title: 'Founder', company: 'Memox' }, + { emoji: '๐Ÿค–', title: 'AI Builder', company: 'Agentic Systems' }, + { emoji: '๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ', title: 'Father of Two', company: 'Life' }, +] + +export default function About() { + const sectionRef = useRef(null) + const titleRef = useRef(null) + const contentRef = useRef(null) + + useEffect(() => { + const initAnimations = async () => { + try { + const gsap = (await import('gsap')).default + const ScrollTrigger = (await import('gsap/ScrollTrigger')).default + gsap.registerPlugin(ScrollTrigger) + + // Use 'to' with autoAlpha for safe animation + // Elements start visible, GSAP handles the reveal + gsap.fromTo( + titleRef.current, + { opacity: 0, y: 30 }, + { + scrollTrigger: { trigger: sectionRef.current, start: 'top 85%' }, + duration: 0.8, + opacity: 1, + y: 0, + ease: 'power3.out', + } + ) + + const children = contentRef.current?.children + if (children) { + gsap.fromTo( + Array.from(children), + { opacity: 0, y: 25 }, + { + scrollTrigger: { trigger: sectionRef.current, start: 'top 75%' }, + duration: 0.6, + opacity: 1, + y: 0, + stagger: 0.15, + ease: 'power3.out', + } + ) + } + } catch { + // GSAP failed - elements visible via CSS + } + } + + initAnimations() + }, []) + + return ( +
+
+

+ About +

+
+

+ I build technology that works for people. I specialize in designing AI-driven + platforms, orchestrating multi-agent workflows, and leading teams that turn + bold ideas into working, scalable systems. +

+
+ {roles.map((role, index) => ( +
+

{role.emoji} {role.title}

+

{role.company}

+
+ ))} +
+
+
+
+ ) +} diff --git a/components/home/BlogGrid.tsx b/components/home/BlogGrid.tsx new file mode 100644 index 0000000000..3b95d2e5d7 --- /dev/null +++ b/components/home/BlogGrid.tsx @@ -0,0 +1,116 @@ +'use client' + +import { useEffect, useRef } from 'react' +import Link from 'next/link' +import { formatDate } from '@/lib/utils' +import styles from '@/styles/home.module.css' + +interface BlogPost { + id: string + title: string + slug: string + date?: string + tags?: string[] +} + +interface BlogGridProps { + posts: BlogPost[] +} + +export default function BlogGrid({ posts }: BlogGridProps) { + const sectionRef = useRef(null) + const titleRef = useRef(null) + const gridRef = useRef(null) + + useEffect(() => { + const initAnimations = async () => { + try { + const gsap = (await import('gsap')).default + const ScrollTrigger = (await import('gsap/ScrollTrigger')).default + gsap.registerPlugin(ScrollTrigger) + + gsap.fromTo( + titleRef.current, + { opacity: 0, y: 30 }, + { + scrollTrigger: { trigger: sectionRef.current, start: 'top 85%' }, + duration: 0.8, + opacity: 1, + y: 0, + ease: 'power3.out', + } + ) + + const cards = gridRef.current?.children + if (cards && cards.length > 0) { + gsap.fromTo( + Array.from(cards), + { opacity: 0, y: 30 }, + { + scrollTrigger: { trigger: gridRef.current, start: 'top 85%' }, + duration: 0.6, + opacity: 1, + y: 0, + stagger: 0.1, + ease: 'power3.out', + } + ) + } + } catch { + // GSAP failed - elements visible via CSS + } + } + + if (posts && posts.length > 0) { + initAnimations() + } + }, [posts]) + + if (!posts || posts.length === 0) { + return ( +
+
+

Latest Posts

+

Posts coming soon...

+
+
+ ) + } + + return ( +
+
+

+ Latest Posts +

+
+ {posts.slice(0, 6).map((post) => ( + +
+

{post.title}

+ {post.date && ( + + )} + {post.tags && post.tags.length > 0 && ( +
+ {post.tags.slice(0, 3).map((tag, i) => ( + + {tag} + + ))} +
+ )} +
+ + ))} +
+
+
+ ) +} diff --git a/components/home/Hero.tsx b/components/home/Hero.tsx new file mode 100644 index 0000000000..c8c98315d6 --- /dev/null +++ b/components/home/Hero.tsx @@ -0,0 +1,100 @@ +'use client' + +import { useEffect, useRef, useState } from 'react' +import styles from '@/styles/home.module.css' + +export default function Hero() { + const heroRef = useRef(null) + const headingRef = useRef(null) + const subtitleRef = useRef(null) + const socialsRef = useRef(null) + const [animated, setAnimated] = useState(false) + + useEffect(() => { + const initAnimations = async () => { + try { + const gsap = (await import('gsap')).default + + // Hide elements before animating + if (headingRef.current) { + gsap.set(headingRef.current, { opacity: 0, y: 30 }) + } + if (subtitleRef.current) { + gsap.set(subtitleRef.current, { opacity: 0, y: 20 }) + } + if (socialsRef.current) { + gsap.set(Array.from(socialsRef.current.children), { opacity: 0, y: 15 }) + } + + setAnimated(true) + + const tl = gsap.timeline({ defaults: { ease: 'power3.out' } }) + + tl.to(headingRef.current, { duration: 0.8, opacity: 1, y: 0 }) + tl.to(subtitleRef.current, { duration: 0.6, opacity: 1, y: 0 }, '-=0.4') + tl.to( + Array.from(socialsRef.current?.children || []), + { duration: 0.4, opacity: 1, y: 0, stagger: 0.08 }, + '-=0.3' + ) + } catch { + // GSAP failed to load - elements stay visible via CSS defaults + } + } + + initAnimations() + }, []) + + return ( +
+
+

+ Hello World ๐Ÿ‘‹, I'm Abdullah +

+

+ Tech Leader & Builder – CEO @ Autonomous, Founder @ Memox. +
+ Building AI-driven platforms and leading teams that ship. +

+ +
+ SCROLL + + + +
+
+
+
+ ) +} diff --git a/components/layout/EnhancedFooter.module.css b/components/layout/EnhancedFooter.module.css new file mode 100644 index 0000000000..0105929066 --- /dev/null +++ b/components/layout/EnhancedFooter.module.css @@ -0,0 +1,21 @@ +.enhancedFooter { + position: relative; + margin-top: 4rem; +} + +.enhancedFooter::before { + content: ''; + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + width: 80%; + max-width: 800px; + height: 1px; + background: linear-gradient( + 90deg, + transparent, + var(--fg-color-2) 50%, + transparent + ); +} diff --git a/components/layout/EnhancedFooter.tsx b/components/layout/EnhancedFooter.tsx new file mode 100644 index 0000000000..97d8793024 --- /dev/null +++ b/components/layout/EnhancedFooter.tsx @@ -0,0 +1,40 @@ +'use client' + +import { useEffect, useRef } from 'react' +import { Footer } from '../Footer' + +export default function EnhancedFooter() { + const footerRef = useRef(null) + + useEffect(() => { + const initAnimations = async () => { + try { + const gsap = (await import('gsap')).default + const ScrollTrigger = (await import('gsap/ScrollTrigger')).default + gsap.registerPlugin(ScrollTrigger) + + gsap.fromTo( + footerRef.current, + { opacity: 0, y: 20 }, + { + scrollTrigger: { trigger: footerRef.current, start: 'top 95%' }, + duration: 0.6, + opacity: 1, + y: 0, + ease: 'power3.out', + } + ) + } catch { + // visible by default + } + } + + initAnimations() + }, []) + + return ( +
+
+
+ ) +} diff --git a/components/layout/EnhancedHeader.module.css b/components/layout/EnhancedHeader.module.css new file mode 100644 index 0000000000..d61cdbdd4c --- /dev/null +++ b/components/layout/EnhancedHeader.module.css @@ -0,0 +1,28 @@ +.enhancedHeader { + position: sticky; + top: 0; + z-index: 100; + background: var(--bg-color); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-bottom: 1px solid var(--fg-color-1); + transition: transform 0.3s ease, opacity 0.3s ease; +} + +.enhancedHeader.hidden { + transform: translateY(-100%); + opacity: 0; +} + +.enhancedHeader.visible { + transform: translateY(0); + opacity: 1; +} + +.dark .enhancedHeader { + background: rgba(18, 18, 18, 0.8); +} + +:root:not(.dark) .enhancedHeader { + background: rgba(255, 255, 255, 0.8); +} diff --git a/components/layout/EnhancedHeader.tsx b/components/layout/EnhancedHeader.tsx new file mode 100644 index 0000000000..14aa6b86b2 --- /dev/null +++ b/components/layout/EnhancedHeader.tsx @@ -0,0 +1,53 @@ +'use client' + +import { useEffect, useRef, useState } from 'react' +import { NotionPageHeader } from '../NotionPageHeader' +import styles from './EnhancedHeader.module.css' + +export default function EnhancedHeader(props: any) { + const headerRef = useRef(null) + const [isVisible, setIsVisible] = useState(true) + const [lastScrollY, setLastScrollY] = useState(0) + + useEffect(() => { + let ticking = false + + const handleScroll = () => { + if (!ticking) { + window.requestAnimationFrame(() => { + const currentScrollY = window.scrollY + + if (currentScrollY < 100) { + setIsVisible(true) + } else if (currentScrollY > lastScrollY) { + // Scrolling down + setIsVisible(false) + } else { + // Scrolling up + setIsVisible(true) + } + + setLastScrollY(currentScrollY) + ticking = false + }) + + ticking = true + } + } + + window.addEventListener('scroll', handleScroll, { passive: true }) + + return () => { + window.removeEventListener('scroll', handleScroll) + } + }, [lastScrollY]) + + return ( +
+ +
+ ) +} diff --git a/lib/homepage-data.ts b/lib/homepage-data.ts new file mode 100644 index 0000000000..529dde1a14 --- /dev/null +++ b/lib/homepage-data.ts @@ -0,0 +1,95 @@ +import { NotionAPI } from 'notion-client' +import { getCanonicalPageId } from './get-canonical-page-id' + +const notion = new NotionAPI() + +export interface BlogPost { + id: string + title: string + slug: string + date?: string + tags?: string[] +} + +// Notion schema property IDs (from collection schema) +const PROP_PUBLISHED = 'a { + try { + const pageRm = await notion.getPage(id) + const block = pageRm.block?.[id]?.value + if (!block || !block.properties) return null + + const props = block.properties + const title = props.title?.[0]?.[0] || 'Untitled' + + // Check if public + const isPublic = props[PROP_PUBLIC]?.[0]?.[0] === 'Yes' + if (!isPublic) return null + + // Get slug from canonical page ID + const slug = + getCanonicalPageId(block.id, pageRm, { uuid: false }) || + block.id.replace(/-/g, '') + + // Extract date - format: [["โ€ฃ",[["d",{"type":"date","start_date":"2025-08-12"}]]]] + let date: string | undefined + const dateVal = props[PROP_PUBLISHED] + if (dateVal) { + const dateInfo = dateVal?.[0]?.[1]?.[0]?.[1] + date = dateInfo?.start_date + } + + // Extract tags - format: [["Tag1,Tag2"]] for multi_select + let tags: string[] = [] + const tagsVal = props[PROP_TAGS]?.[0]?.[0] + if (tagsVal) { + tags = tagsVal.split(',').map((t: string) => t.trim()) + } + + return { id: block.id, title, slug, date, tags } + } catch { + return null + } + }) + + const results = await Promise.all(fetchPromises) + results.forEach((r) => { + if (r) posts.push(r) + }) + + // Sort by date (newest first) + posts.sort((a, b) => { + if (!a.date) return 1 + if (!b.date) return -1 + return new Date(b.date).getTime() - new Date(a.date).getTime() + }) + + return { posts } + } catch (error) { + console.error('Error fetching homepage data:', error) + return { posts: [] } + } +} diff --git a/lib/utils.ts b/lib/utils.ts new file mode 100644 index 0000000000..b5b2ec6705 --- /dev/null +++ b/lib/utils.ts @@ -0,0 +1,12 @@ +export function formatDate(dateString: string): string { + try { + const date = new Date(dateString) + return new Intl.DateTimeFormat('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }).format(date) + } catch (error) { + return dateString + } +} diff --git a/next.config.js b/next.config.js index cb72e69c83..1b8a66eeea 100644 --- a/next.config.js +++ b/next.config.js @@ -2,6 +2,10 @@ // import { fileURLToPath } from 'node:url' export default { + typescript: { + // GSAP ships untyped JS files that fail Turbopack's type checker + ignoreBuildErrors: true, + }, staticPageGenerationTimeout: 300, images: { remotePatterns: [ diff --git a/package.json b/package.json index ab56b5b451..898e433066 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,8 @@ "@keyvhq/redis": "^1.6.10", "classnames": "^2.5.1", "expiry-map": "^2.0.0", + "gsap": "^3.14.2", + "lenis": "^1.3.17", "fathom-client": "^3.4.1", "katex": "^0.16.28", "ky": "^1.14.3", diff --git a/pages/[pageId].tsx b/pages/[pageId].tsx index a10f8f3246..fa476a3145 100644 --- a/pages/[pageId].tsx +++ b/pages/[pageId].tsx @@ -1,6 +1,6 @@ import { type GetStaticProps } from 'next' -import { NotionPage } from '@/components/NotionPage' +import { NotionPageWrapper } from '@/components/NotionPageWrapper' import { domain, isDev, pageUrlOverrides } from '@/lib/config' import { getSiteMap } from '@/lib/get-site-map' import { resolveNotionPage } from '@/lib/resolve-notion-page' @@ -53,5 +53,5 @@ export async function getStaticPaths() { } export default function NotionDomainDynamicPage(props: PageProps) { - return + return } diff --git a/pages/_app.tsx b/pages/_app.tsx index 4e0b796064..76900f31ab 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -12,12 +12,15 @@ import 'styles/global.css' import 'styles/notion.css' // global style overrides for prism theme (optional) import 'styles/prism-theme.css' +// effects styles +import 'styles/effects.css' import type { AppProps } from 'next/app' import * as Fathom from 'fathom-client' import { useRouter } from 'next/router' import { posthog } from 'posthog-js' import * as React from 'react' +import dynamic from 'next/dynamic' import { bootstrap } from '@/lib/bootstrap-client' import { @@ -28,6 +31,19 @@ import { posthogId } from '@/lib/config' +// Dynamically import effects to avoid SSR issues +const SmoothScroll = dynamic(() => import('@/components/effects/SmoothScroll'), { + ssr: false, +}) + +const CustomCursor = dynamic(() => import('@/components/effects/CustomCursor'), { + ssr: false, +}) + +const PageTransition = dynamic(() => import('@/components/effects/PageTransition'), { + ssr: false, +}) + if (!isServer) { bootstrap() } @@ -61,5 +77,13 @@ export default function App({ Component, pageProps }: AppProps) { } }, [router.events]) - return + return ( + <> + + + + + + + ) } diff --git a/pages/_document.tsx b/pages/_document.tsx index aa234f3466..96a5c15809 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -7,8 +7,15 @@ export default class MyDocument extends Document { - + + {/* Google Fonts */} + + + diff --git a/pages/index.tsx b/pages/index.tsx index 1f817f9e54..dd93dace89 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,22 +1,66 @@ import type { PageProps } from '@/lib/types' -import { NotionPage } from '@/components/NotionPage' +import { NotionPageWrapper } from '@/components/NotionPageWrapper' import { domain } from '@/lib/config' import { resolveNotionPage } from '@/lib/resolve-notion-page' +import { getHomepageData, BlogPost } from '@/lib/homepage-data' +import Hero from '@/components/home/Hero' +import About from '@/components/home/About' +import BlogGrid from '@/components/home/BlogGrid' +import EnhancedFooter from '@/components/layout/EnhancedFooter' +import { NotionPageHeader } from '@/components/NotionPageHeader' +import { PageHead } from '@/components/PageHead' +import { getBlockValue } from 'notion-utils' + +interface HomePageProps extends PageProps { + isHomePage: boolean + posts: BlogPost[] +} export const getStaticProps = async () => { try { const props = await resolveNotionPage(domain) + const { posts } = await getHomepageData() - return { props, revalidate: 10 } + return { + props: { + ...props, + isHomePage: true, + posts, + }, + revalidate: 10, + } } catch (err) { console.error('page error', domain, err) - - // we don't want to publish the error version of this page, so - // let next.js know explicitly that incremental SSG failed throw err } } -export default function NotionDomainPage(props: PageProps) { - return +export default function NotionDomainPage(props: HomePageProps) { + const { isHomePage, posts, ...notionProps } = props + + // Render custom homepage for the root page + if (isHomePage && notionProps.pageId === '16ccc94eb4cf4b3d85fb31ac7be58e87') { + // Get block for header + const keys = Object.keys(notionProps.recordMap?.block || {}) + const block = getBlockValue(notionProps.recordMap?.block?.[keys[0]!]) + + return ( + <> + + {block && } + + + + + + ) + } + + // Render normal Notion page for other routes + return } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7be8cb4419..b802a95f35 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,12 +26,18 @@ importers: fathom-client: specifier: ^3.4.1 version: 3.7.2 + gsap: + specifier: ^3.14.2 + version: 3.14.2 katex: specifier: ^0.16.28 version: 0.16.28 ky: specifier: ^1.14.3 version: 1.14.3 + lenis: + specifier: ^1.3.17 + version: 1.3.17(react@19.2.4) lqip-modern: specifier: ^2.2.1 version: 2.2.1 @@ -1386,6 +1392,9 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + gsap@3.14.2: + resolution: {integrity: sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA==} + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -1646,6 +1655,20 @@ packages: resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} engines: {node: '>=0.10'} + lenis@1.3.17: + resolution: {integrity: sha512-k9T9rgcxne49ggJOvXCraWn5dt7u2mO+BNkhyu6yxuEnm9c092kAW5Bus5SO211zUvx7aCCEtzy9UWr0RB+oJw==} + peerDependencies: + '@nuxt/kit': '>=3.0.0' + react: '>=17.0.0' + vue: '>=3.0.0' + peerDependenciesMeta: + '@nuxt/kit': + optional: true + react: + optional: true + vue: + optional: true + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -3895,6 +3918,8 @@ snapshots: gopd@1.2.0: {} + gsap@3.14.2: {} + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -4151,6 +4176,10 @@ snapshots: dependencies: language-subtag-registry: 0.3.23 + lenis@1.3.17(react@19.2.4): + optionalDependencies: + react: 19.2.4 + levn@0.4.1: dependencies: prelude-ls: 1.2.1 diff --git a/styles/effects.css b/styles/effects.css new file mode 100644 index 0000000000..13ab4a314a --- /dev/null +++ b/styles/effects.css @@ -0,0 +1,59 @@ +/* Global effect styles */ + +* { + cursor: none !important; +} + +@media (pointer: coarse) { + * { + cursor: auto !important; + } +} + +/* Smooth scroll behavior */ +html.lenis, +html.lenis body { + height: auto; +} + +.lenis.lenis-smooth { + scroll-behavior: auto !important; +} + +.lenis.lenis-smooth [data-lenis-prevent] { + overscroll-behavior: contain; +} + +.lenis.lenis-stopped { + overflow: hidden; +} + +.lenis.lenis-scrolling iframe { + pointer-events: none; +} + +/* Reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + * { + cursor: auto !important; + } +} + +/* Interactive elements for custom cursor */ +.cursor-interactive { + position: relative; +} + +/* Page transitions */ +body { + overflow-x: hidden; +} diff --git a/styles/global.css b/styles/global.css index 7eee7abea3..db02d78f90 100644 --- a/styles/global.css +++ b/styles/global.css @@ -13,12 +13,65 @@ html { margin: 0; } -body { - --notion-font: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, +/* CSS Custom Properties for Design System */ +:root { + /* Typography */ + --font-body: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Noto Sans', sans-serif; - font-family: var(--notion-font); + --font-heading: 'Space Grotesk', 'Inter', ui-sans-serif, system-ui, -apple-system, + BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', + 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Noto Sans', sans-serif; + + /* Spacing */ + --space-xs: 0.25rem; + --space-sm: 0.5rem; + --space-md: 1rem; + --space-lg: 2rem; + --space-xl: 4rem; + --space-2xl: 8rem; +} + +/* Light Mode Colors (default) */ +:root:not(.dark-mode) { + --bg-color: #ffffff; + --bg-color-0: #f9fafb; + --bg-color-1: #f3f4f6; + --fg-color: #1f2937; + --fg-color-0: #e5e7eb; + --fg-color-1: #d1d5db; + --fg-color-2: #9ca3af; + --fg-color-3: #6b7280; + --fg-color-4: #4b5563; + --fg-color-5: #374151; +} + +/* Dark Mode Colors */ +.dark-mode { + --bg-color: #121212; + --bg-color-0: #1a1a1a; + --bg-color-1: #242424; + --fg-color: #f9fafb; + --fg-color-0: #2a2a2a; + --fg-color-1: #3a3a3a; + --fg-color-2: #6b7280; + --fg-color-3: #9ca3af; + --fg-color-4: #d1d5db; + --fg-color-5: #e5e7eb; +} + +body { + --notion-font: var(--font-body); + font-family: var(--font-body); overflow-x: hidden; + background: var(--bg-color); + color: var(--fg-color); + transition: background-color 0.3s ease, color 0.3s ease; +} + +/* Headings use Space Grotesk */ +h1, h2, h3, h4, h5, h6 { + font-family: var(--font-heading); } .static-tweet blockquote { @@ -39,24 +92,34 @@ body { caret-color: var(--fg-color); } -::-webkit-scrollbar -{ +::-webkit-scrollbar { width: 5px; height: 5px; - background-color: #F5F5F5; background-color: var(--bg-color-1); } -::-webkit-scrollbar-thumb -{ - border-radius: 10px; - -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); - background-color: #555; +::-webkit-scrollbar-thumb { border-radius: 10px; -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); background-color: var(--fg-color-1); } ::-webkit-scrollbar-track { - background-color: var(--bg-color); + background-color: var(--bg-color); +} + +/* Smooth transitions */ +* { + transition-property: background-color, border-color, color; + transition-duration: 0.2s; + transition-timing-function: ease; +} + +/* Reduced motion: disable all animations */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } } diff --git a/styles/home.module.css b/styles/home.module.css new file mode 100644 index 0000000000..904b859ece --- /dev/null +++ b/styles/home.module.css @@ -0,0 +1,320 @@ +/* Hero Section */ +.hero { + position: relative; + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + background: var(--bg-color); +} + +.heroContent { + position: relative; + z-index: 2; + text-align: center; + padding: 2rem; + max-width: 1100px; + margin: 0 auto; +} + +.heroHeading { + font-size: clamp(2.5rem, 7vw, 5.5rem); + font-weight: 700; + line-height: 1.1; + margin: 0 0 1.5rem 0; + letter-spacing: -0.03em; + background: linear-gradient(135deg, var(--fg-color) 0%, var(--fg-color-3) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.heroHeading :global(.char) { + display: inline-block; + transform-origin: 50% 100%; +} + +.heroSubtitle { + font-size: clamp(1.1rem, 2vw, 1.4rem); + color: var(--fg-color-4); + margin: 0 0 2.5rem 0; + font-weight: 400; + letter-spacing: 0.01em; + line-height: 1.6; +} + +.heroSocials { + display: flex; + gap: 1rem; + justify-content: center; + margin-bottom: 0; +} + +.socialLink { + font-size: 0.95rem; + font-weight: 500; + color: var(--fg-color); + text-decoration: none; + padding: 0.6rem 1.4rem; + border: 1px solid var(--fg-color-1); + border-radius: 50px; + transition: all 0.3s ease; + background: var(--bg-color-0); +} + +.socialLink:hover { + background: var(--fg-color); + color: var(--bg-color); + border-color: var(--fg-color); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.scrollIndicator { + position: absolute; + bottom: 2rem; + left: 50%; + transform: translateX(-50%); + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + color: var(--fg-color-2); + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.15em; + animation: bounce 2s infinite; +} + +@keyframes bounce { + 0%, 100% { transform: translateX(-50%) translateY(0); } + 50% { transform: translateX(-50%) translateY(8px); } +} + +.heroBackground { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1; + background: + radial-gradient(ellipse at 20% 50%, rgba(102, 126, 234, 0.08), transparent 50%), + radial-gradient(ellipse at 80% 50%, rgba(118, 75, 162, 0.06), transparent 50%), + radial-gradient(ellipse at 50% 80%, rgba(236, 72, 153, 0.04), transparent 40%); +} + +:global(.dark-mode) .heroBackground { + background: + radial-gradient(ellipse at 20% 50%, rgba(102, 126, 234, 0.12), transparent 50%), + radial-gradient(ellipse at 80% 50%, rgba(118, 75, 162, 0.10), transparent 50%), + radial-gradient(ellipse at 50% 80%, rgba(236, 72, 153, 0.06), transparent 40%); +} + +/* About Section */ +.about { + padding: 6rem 2rem; + background: var(--bg-color-0); +} + +.aboutContainer { + max-width: 1100px; + margin: 0 auto; +} + +.sectionTitle { + font-size: clamp(2rem, 4vw, 3rem); + font-weight: 700; + margin: 0 0 2.5rem 0; + letter-spacing: -0.02em; +} + +.aboutContent { + display: flex; + flex-direction: column; + gap: 2.5rem; +} + +.aboutText { + font-size: clamp(1.05rem, 1.8vw, 1.2rem); + line-height: 1.8; + color: var(--fg-color-4); + max-width: 750px; +} + +.rolesGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 1.5rem; +} + +.roleCard { + padding: 1.75rem; + border: 1px solid var(--fg-color-1); + border-radius: 16px; + background: var(--bg-color); + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.roleCard::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 3px; + background: linear-gradient(90deg, rgba(102, 126, 234, 0.8), rgba(118, 75, 162, 0.8)); + opacity: 0; + transition: opacity 0.3s ease; +} + +.roleCard:hover { + border-color: var(--fg-color-2); + transform: translateY(-4px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08); +} + +.roleCard:hover::before { + opacity: 1; +} + +.roleTitle { + font-size: 1.3rem; + font-weight: 600; + margin: 0 0 0.4rem 0; + color: var(--fg-color); +} + +.roleCompany { + font-size: 0.95rem; + color: var(--fg-color-3); + margin: 0; +} + +/* Blog Section */ +.blog { + padding: 6rem 2rem; + background: var(--bg-color); +} + +.blogContainer { + max-width: 1100px; + margin: 0 auto; +} + +.blogGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.blogCard { + display: block; + padding: 1.75rem; + border: 1px solid var(--fg-color-1); + border-radius: 16px; + background: var(--bg-color-0); + text-decoration: none; + color: var(--fg-color); + transition: all 0.3s ease; + will-change: transform; +} + +.blogCard:hover { + border-color: var(--fg-color-2); + transform: translateY(-4px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08); +} + +.blogTitle { + font-size: 1.25rem; + font-weight: 600; + margin: 0 0 0.75rem 0; + line-height: 1.35; +} + +.blogDate { + display: block; + font-size: 0.8rem; + color: var(--fg-color-3); + margin-bottom: 0.75rem; +} + +.blogTags { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.blogTag { + padding: 0.2rem 0.6rem; + font-size: 0.7rem; + background: var(--fg-color-0); + color: var(--fg-color-4); + border-radius: 50px; + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 500; +} + +.blogViewAll { + text-align: center; + margin-top: 2.5rem; +} + +.viewAllLink { + display: inline-block; + font-size: 1rem; + font-weight: 500; + color: var(--fg-color); + text-decoration: none; + padding: 0.8rem 2rem; + border: 1px solid var(--fg-color-1); + border-radius: 50px; + transition: all 0.3s ease; +} + +.viewAllLink:hover { + background: var(--fg-color); + color: var(--bg-color); + border-color: var(--fg-color); +} + +/* Responsive */ +@media (max-width: 768px) { + .hero { + min-height: 85vh; + } + + .heroSocials { + flex-wrap: wrap; + gap: 0.75rem; + } + + .about, + .blog { + padding: 4rem 1.5rem; + } + + .rolesGrid { + grid-template-columns: 1fr; + } + + .blogGrid { + grid-template-columns: 1fr; + } +} + +@media (prefers-reduced-motion: reduce) { + .heroHeading :global(.char), + .scrollIndicator, + .blogCard, + .roleCard { + animation: none; + transition: none; + } +} diff --git a/styles/notion.css b/styles/notion.css index 94a7f309fe..bddfbc61ff 100644 --- a/styles/notion.css +++ b/styles/notion.css @@ -403,3 +403,76 @@ .notion-equation.notion-equation-block{ align-items: center; } + +/* Enhanced Typography for Blog Posts */ +.notion-page { + font-feature-settings: 'kern' 1, 'liga' 1, 'calt' 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.notion-title { + letter-spacing: -0.02em; + line-height: 1.2; + margin-bottom: 1.5em; +} + +.notion-h1, +.notion-h2, +.notion-h3 { + letter-spacing: -0.01em; + font-weight: 600; + margin-top: 1.5em; +} + +.notion-text { + font-size: 1.1em; + line-height: 1.75; +} + +/* Enhanced Code Blocks */ +.notion-code { + font-size: 0.875em; + line-height: 1.6; + padding: 1.25em; + overflow-x: auto; + border-radius: 8px; + background: var(--bg-color-0); + border: 1px solid var(--fg-color-1); +} + +.dark-mode .notion-code { + background: rgba(255, 255, 255, 0.05); + border-color: rgba(255, 255, 255, 0.1); +} + +/* Better spacing */ +.notion-list { + margin: 0.75em 0; +} + +.notion-list-item { + padding: 0.25em 0; +} + +.notion-callout { + padding: 1.25em; + border-radius: 8px; + background: var(--bg-color-0); +} + +/* Smooth transitions */ +.notion-link, +.notion-collection-card, +.notion-bookmark { + transition: all 0.2s ease; +} + +/* Better image styling */ +.notion-asset-wrapper { + margin: 2em 0; +} + +.notion-asset-wrapper img { + border-radius: 8px; +} diff --git a/tsconfig.json b/tsconfig.json index 2284d3ac2b..a6102cb3fe 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "@fisch0920/config/tsconfig-react", "compilerOptions": { + "skipLibCheck": true, "baseUrl": ".", "paths": { "@/components/*": ["components/*"],