diff --git a/src/app/projects/[slug]/page.tsx b/src/app/projects/[slug]/page.tsx index 8449526..61bb394 100644 --- a/src/app/projects/[slug]/page.tsx +++ b/src/app/projects/[slug]/page.tsx @@ -1,5 +1,12 @@ import { notFound } from 'next/navigation'; -import { getAllSlugs, getContentBySlug } from '@/lib/content'; +import Link from 'next/link'; +import { + getAllSlugs, + getContentBySlug, + getAdjacentContent, + getAllContent, +} from '@/lib/content'; +import ProjectNavigation from '@/components/ProjectNavigation'; export function generateStaticParams() { return getAllSlugs('projects').map((slug) => ({ slug })); @@ -26,39 +33,132 @@ export default async function ProjectPage({ const item = getContentBySlug('projects', params.slug); if (!item) notFound(); + const all = getAllContent('projects'); + const idx = all.findIndex((p) => p.slug === params.slug); + const num = String(idx + 1).padStart(2, '0'); + const { prev, next } = getAdjacentContent('projects', params.slug); + const { default: MDXContent } = await import( `../../../../content/projects/${params.slug}.mdx` ); + const date = new Date(item.frontmatter.date).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }); + return ( -
-
-
-

- {item.frontmatter.title} -

-

- {new Date(item.frontmatter.date).toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - })} -

- {item.frontmatter.tags && item.frontmatter.tags.length > 0 && ( -
    - {item.frontmatter.tags.map((tag) => ( -
  • - {tag} -
  • - ))} -
- )} -
- -
-
+
+
+ {/* Back link */} + + ← back to projects + + + {/* Section head */} +
+ {num} {'//'} + {params.slug}.mdx + + {date} +
+ + {/* Hero */} +
+

+ {item.frontmatter.title} +

+

+ {item.frontmatter.description} +

+ {item.frontmatter.tags && item.frontmatter.tags.length > 0 && ( +
    + {item.frontmatter.tags.map((tag) => ( +
  • + {tag} +
  • + ))} +
+ )} +
+ + {/* Gradient separator */} +
+ + {/* MDX body */} +
+ +
+ + {/* Prev/Next navigation */} + + + {/* All projects link */} +
+ + all projects → + +
+
+
); } diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx index 8c4c37d..8ed02d4 100644 --- a/src/app/projects/page.tsx +++ b/src/app/projects/page.tsx @@ -1,6 +1,6 @@ -import MotionSection from '@/components/MotionSection'; -import ContentProjectCard from '@/components/ContentProjectCard'; import { getAllContent } from '@/lib/content'; +import MdxProjectCard from '@/components/MdxProjectCard'; +import RevealOnScroll from '@/components/RevealOnScroll'; export const metadata = { title: 'Projects — Altan Esmer', @@ -11,27 +11,64 @@ export default function ProjectsPage() { const projects = getAllContent('projects'); return ( -
- -

- Projects -

- {projects.length > 0 ? ( -
- {projects.map((item) => ( - - ))} +
+
+ {/* Section head */} +
+ 02 // + all_projects.json + + {projects.length} entries
- ) : ( -

- No projects yet. Check back soon. -

- )} - -
+ + {/* Hero title */} +

+ All the work I've{' '} + shipped. +

+ + + {projects.length > 0 ? ( +
+ {projects.map((item, i) => ( + + ))} +
+ ) : ( +

No projects yet. Check back soon.

+ )} +
+ + {/* Responsive: collapse to 1 col on mobile */} + +
+ ); } diff --git a/src/components/MdxProjectCard.tsx b/src/components/MdxProjectCard.tsx new file mode 100644 index 0000000..e54d658 --- /dev/null +++ b/src/components/MdxProjectCard.tsx @@ -0,0 +1,134 @@ +import Link from 'next/link'; +import type { ContentItem } from '@/lib/content'; + +type Props = Pick & { index: number }; + +const PV_GRADS = [ + 'pv-grad-1', + 'pv-grad-2', + 'pv-grad-3', + 'pv-grad-4', + 'pv-grad-5', + 'pv-grad-6', + 'pv-grad-7', +] as const; + +export default function MdxProjectCard({ slug, frontmatter, index }: Props) { + const preview = PV_GRADS[index % PV_GRADS.length]; + const date = new Date(frontmatter.date).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + }); + + return ( + + {/* Background gradient */} +
+
+
+ + {/* Scrim */} +
+ + {/* Content */} +
+ {/* Top row: date + arrow */} +
+ {date} + +
+ + {/* Bottom block */} +
+

+ {frontmatter.title} +

+

+ {frontmatter.description} +

+ {frontmatter.tags && frontmatter.tags.length > 0 && ( +
+ {frontmatter.tags.map((tag) => ( + + {tag} + + ))} +
+ )} +
+
+ + ); +} diff --git a/src/components/ProjectCard.tsx b/src/components/ProjectCard.tsx index 645a57a..b5df29a 100644 --- a/src/components/ProjectCard.tsx +++ b/src/components/ProjectCard.tsx @@ -33,7 +33,7 @@ export default function ProjectCard({ overflow: 'hidden', background: 'rgba(26,27,58,.6)', border: '1px solid var(--line)', - padding: '22px', + padding: '28px', display: 'flex', flexDirection: 'column', justifyContent: 'space-between', @@ -135,10 +135,10 @@ export default function ProjectCard({ borderRadius: '12px', background: 'rgba(10,14,39,.6)', border: '1px solid var(--line)', - padding: '14px', + padding: '16px', display: 'flex', flexDirection: 'column', - gap: '10px', + gap: '12px', backdropFilter: 'blur(6px)', overflow: 'hidden', }} @@ -259,7 +259,7 @@ export default function ProjectCard({
{project.stack.map((s) => ( diff --git a/src/components/ProjectNavigation.tsx b/src/components/ProjectNavigation.tsx new file mode 100644 index 0000000..6b71637 --- /dev/null +++ b/src/components/ProjectNavigation.tsx @@ -0,0 +1,118 @@ +import Link from 'next/link'; +import type { ContentItem } from '@/lib/content'; + +type Props = { prev: ContentItem | null; next: ContentItem | null }; + +export default function ProjectNavigation({ prev, next }: Props) { + return ( + <> + + + + ); +} diff --git a/src/components/ProjectsSection.tsx b/src/components/ProjectsSection.tsx index 58423f9..b96bde9 100644 --- a/src/components/ProjectsSection.tsx +++ b/src/components/ProjectsSection.tsx @@ -95,8 +95,8 @@ export default function ProjectsSection() { #projects { padding: 72px 0 !important; } #projects .bento { grid-template-columns: repeat(2, 1fr) !important; - grid-auto-rows: 180px !important; - gap: 14px !important; + grid-auto-rows: 220px !important; + gap: 18px !important; } #projects .c-2x2, #projects .c-2x1, #projects .c-1x1, #projects .c-1x2 { grid-column: span 2 !important; @@ -247,8 +247,8 @@ export default function ProjectsSection() { style={{ display: 'grid', gridTemplateColumns: 'repeat(6, 1fr)', - gap: '20px', - gridAutoRows: '220px', + gap: '24px', + gridAutoRows: '260px', }} > {filtered.map((project) => ( diff --git a/src/components/RevealOnScroll.tsx b/src/components/RevealOnScroll.tsx new file mode 100644 index 0000000..52a3797 --- /dev/null +++ b/src/components/RevealOnScroll.tsx @@ -0,0 +1,33 @@ +'use client'; + +import { useEffect, useRef } from 'react'; + +export default function RevealOnScroll({ children }: { children: React.ReactNode }) { + const wrapperRef = useRef(null); + + useEffect(() => { + const wrapper = wrapperRef.current; + if (!wrapper) return; + + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + (entry.target as HTMLElement).classList.add('in'); + } + }); + }, + { threshold: 0.1 }, + ); + + const revealEls = wrapper.querySelectorAll('.reveal'); + revealEls.forEach((el, i) => { + el.style.transitionDelay = `${i * 0.05}s`; + observer.observe(el); + }); + + return () => observer.disconnect(); + }, []); + + return
{children}
; +} diff --git a/src/lib/content.ts b/src/lib/content.ts index 8343d6e..ab5e182 100644 --- a/src/lib/content.ts +++ b/src/lib/content.ts @@ -71,3 +71,17 @@ export function getAllSlugs(type: 'projects' | 'posts'): string[] { .filter((f) => f.endsWith('.mdx')) .map((f) => f.replace(/\.mdx$/, '')); } + +export function getAdjacentContent( + type: 'projects' | 'posts', + slug: string, +): { prev: ContentItem | null; next: ContentItem | null } { + const items = getAllContent(type); // already date-desc + const idx = items.findIndex((x) => x.slug === slug); + if (idx === -1) return { prev: null, next: null }; + // prev = older entry → comes after in date-desc array + // next = newer entry → comes before in date-desc array + const prev = idx < items.length - 1 ? items[idx + 1] : null; + const next = idx > 0 ? items[idx - 1] : null; + return { prev, next }; +}