From d9f75d3e10c16d9c2ba21f94e4e9d02fccb9e43a Mon Sep 17 00:00:00 2001 From: AltanEsmer Date: Sat, 9 May 2026 19:12:50 +0300 Subject: [PATCH 1/3] feat(projects): increase homepage bento card size MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump grid auto-rows 200px→260px (220px on mobile), gap 18px→24px, card padding 22px→28px, title 22px→26px, desc 14px, spark bars 40px. Cards now have more breathing room and present larger preview surface without changing the bento span system. Co-Authored-By: Claude Opus 4.7 --- src/components/ProjectCard.tsx | 18 +++++++++--------- src/components/ProjectsSection.tsx | 8 ++++---- 2 files changed, 13 insertions(+), 13 deletions(-) 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/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) => ( From 686df2d2ea77a823b5169bf0b9b3d246cfac39bb Mon Sep 17 00:00:00 2001 From: AltanEsmer Date: Sat, 9 May 2026 19:12:59 +0300 Subject: [PATCH 2/3] feat(projects): redesign /projects index to match dark theme Replace the light-mode max-w-3xl stack of ContentProjectCard with the AlesSystems aesthetic: section-head strip, Space Grotesk hero, and a 2-col responsive grid of MdxProjectCard tiles using the existing .bento-card / .pv-grad / .chip primitives. Adds a small RevealOnScroll client wrapper that reuses the IntersectionObserver pattern from ProjectsSection so cards stagger-in as they enter the viewport. Co-Authored-By: Claude Opus 4.7 --- src/app/projects/page.tsx | 83 +++++++++++++----- src/components/MdxProjectCard.tsx | 134 ++++++++++++++++++++++++++++++ src/components/RevealOnScroll.tsx | 33 ++++++++ 3 files changed, 227 insertions(+), 23 deletions(-) create mode 100644 src/components/MdxProjectCard.tsx create mode 100644 src/components/RevealOnScroll.tsx 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/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}
; +} From 3bf012819ae13cc508bec92a730c3375536cf1fb Mon Sep 17 00:00:00 2001 From: AltanEsmer Date: Sat, 9 May 2026 19:13:07 +0300 Subject: [PATCH 3/3] feat(projects): redesign case-study page and add prev/next nav Rewrite /projects/[slug] in the AlesSystems dark theme: JetBrains Mono back-link, section-head with slug + date, Space Grotesk hero, .chip tags, gradient hairline separator, and the existing .prose-mdx body styles. Add a prev/next ProjectNavigation block at the foot of the page so readers can walk the case-study list without hitting the browser back button. Powered by a new getAdjacentContent helper in src/lib/content.ts that traverses the date-sorted content array. Co-Authored-By: Claude Opus 4.7 --- src/app/projects/[slug]/page.tsx | 160 ++++++++++++++++++++++----- src/components/ProjectNavigation.tsx | 118 ++++++++++++++++++++ src/lib/content.ts | 14 +++ 3 files changed, 262 insertions(+), 30 deletions(-) create mode 100644 src/components/ProjectNavigation.tsx 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/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/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 }; +}