Nicholai 0b8a680dcb Redesign page heroes, add pill buttons, improve HubertChat
Hero sections:
- Redesign blog, dev, and contact page heroes with sleek modern style
- Add gradient text titles, floating accent orbs, stats rows
- Remove heavy terminal-style elements for cleaner aesthetic

Buttons:
- Add rounded-full to all buttons sitewide for consistent pill shape
- Update btn-primary and btn-ghost utilities in global.css

HubertChat:
- Move Hubert to dedicated /hubert page
- Add framer-motion for smooth input transitions
- Support markdown rendering with marked + DOMPurify
- Add multiline textarea with Shift+Enter support
- Fix light mode visibility for input and message bubbles
- Extract useHubertChat hook for cleaner state management

Fonts:
- Update to Sora (sans) and IBM Plex Mono (mono)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 03:49:26 -07:00

237 lines
9.8 KiB
Plaintext

---
import { Image } from 'astro:assets';
import { getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import FormattedDate from '../../components/FormattedDate.astro';
import BlogCard from '../../components/BlogCard.astro';
import BlogFilters from '../../components/BlogFilters.astro';
import { SITE_DESCRIPTION, SITE_TITLE } from '../../consts';
import { calculateReadingTime } from '../../utils/reading-time';
// Fetch all posts sorted by date (newest first)
const allPosts = (await getCollection('blog')).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
// Derive featured post (first post with featured: true, or fallback to latest)
const featuredPost = allPosts.find((post) => post.data.featured) || allPosts[0];
// Editor's picks: next 3 posts after featured (excluding the featured one)
//const editorPicks = allPosts
// .filter((post) => post.id !== featuredPost?.id)
// .slice(0, 3);
// Latest posts: all posts for the filterable grid
const latestPosts = allPosts;
// Extract unique categories for filters
const categories = [...new Set(allPosts.map((post) => post.data.category).filter(Boolean))] as string[];
---
<BaseLayout title={`Blog | ${SITE_TITLE}`} description={SITE_DESCRIPTION}>
<!-- Hero Section -->
<section class="relative pb-16 lg:pb-20 overflow-hidden">
<!-- Floating accent orb -->
<div class="absolute top-0 right-1/4 w-64 h-64 bg-brand-accent/5 rounded-full blur-3xl pointer-events-none"></div>
<div class="container mx-auto px-6 lg:px-12 relative z-10">
<!-- Main Hero Content -->
<div>
<div class="max-w-5xl">
<!-- Small label -->
<div class="flex items-center gap-3 mb-8">
<div class="w-1.5 h-1.5 bg-brand-accent rounded-full"></div>
<span class="text-xs font-medium uppercase tracking-[0.2em] text-[var(--theme-text-muted)]">Writing & Insights</span>
</div>
<!-- Main Title -->
<h1 class="text-6xl md:text-8xl lg:text-9xl font-bold tracking-tighter leading-[0.9] mb-8">
<span class="block text-[var(--theme-text-primary)]">The</span>
<span class="block bg-gradient-to-r from-brand-accent to-brand-accent/70 bg-clip-text text-transparent">Archive</span>
</h1>
<!-- Description -->
<p class="text-xl md:text-2xl text-[var(--theme-text-secondary)] font-light leading-relaxed max-w-2xl">
Thoughts on VFX production, creative workflows, and lessons learned from building visual stories.
</p>
</div>
<!-- Stats Row -->
<div class="flex items-center gap-8 mt-16 pt-8 border-t border-[var(--theme-border-primary)]">
<div class="flex items-center gap-3">
<span class="text-3xl font-bold text-[var(--theme-text-primary)]">{allPosts.length}</span>
<span class="text-xs uppercase tracking-widest text-[var(--theme-text-muted)]">Articles</span>
</div>
<div class="w-px h-8 bg-[var(--theme-border-primary)]"></div>
<div class="flex items-center gap-3">
<span class="text-3xl font-bold text-[var(--theme-text-primary)]">{categories.length}</span>
<span class="text-xs uppercase tracking-widest text-[var(--theme-text-muted)]">Topics</span>
</div>
</div>
</div>
</div>
</section>
<section class="container mx-auto px-6 lg:px-12">
<!-- Featured Hero Section -->
{featuredPost && (
<div class="mb-16 lg:mb-24 animate-on-scroll slide-up stagger-2">
<div class="flex items-center gap-4 mb-8">
<div class="w-2 h-2 bg-brand-accent rounded-full animate-pulse"></div>
<span class="text-[10px] font-mono text-brand-accent uppercase tracking-widest font-bold">
SYS.BLOG /// FEATURED
</span>
<span class="h-px flex-grow bg-[var(--theme-border-secondary)]"></span>
</div>
<article class="group relative border border-[var(--theme-border-primary)] bg-[var(--theme-hover-bg)] hover:border-brand-accent/40 transition-all duration-500 overflow-hidden">
<!-- Accent indicator strip -->
<div class="absolute top-0 left-0 w-1 h-full bg-brand-accent"></div>
<div class="absolute top-0 left-0 w-full h-1 bg-brand-accent opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
<div class="grid grid-cols-1 lg:grid-cols-2">
<!-- Image section -->
<a href={`/blog/${featuredPost.id}/`} class="block relative aspect-[16/10] lg:aspect-auto overflow-hidden">
{featuredPost.data.heroImage && (
<Image
src={featuredPost.data.heroImage}
alt=""
width={900}
height={600}
class="w-full h-full object-cover transition-transform duration-[1.2s] ease-out group-hover:scale-105"
/>
)}
<div class="absolute inset-0 bg-[var(--theme-card-overlay)] group-hover:opacity-50 transition-opacity duration-500"></div>
<div class="absolute inset-0 bg-gradient-to-r from-transparent via-transparent to-[var(--theme-card-gradient)] hidden lg:block"></div>
<div class="absolute inset-0 bg-gradient-to-t from-[var(--theme-card-gradient)] to-transparent lg:hidden"></div>
<!-- Category badge -->
{featuredPost.data.category && (
<div class="absolute top-6 left-6">
<span class="px-4 py-2 text-[10px] font-mono font-bold uppercase tracking-widest bg-[var(--theme-overlay)] border border-brand-accent/50 text-brand-accent backdrop-blur-sm">
{featuredPost.data.category}
</span>
</div>
)}
<!-- Grid overlay effect -->
<div class="absolute inset-0 grid-overlay opacity-30 pointer-events-none"></div>
</a>
<!-- Content section -->
<div class="p-8 lg:p-12 flex flex-col justify-center">
<!-- Technical header -->
<div class="flex items-center gap-3 mb-6">
<span class="text-[10px] font-mono text-brand-accent uppercase tracking-widest">
<FormattedDate date={featuredPost.data.pubDate} />
</span>
<span class="h-px w-8 bg-[var(--theme-border-strong)]"></span>
<span class="text-[10px] font-mono text-[var(--theme-text-muted)] uppercase tracking-widest">
{calculateReadingTime(featuredPost.body)}
</span>
</div>
<!-- Title -->
<a href={`/blog/${featuredPost.id}/`}>
<h2 class="text-3xl lg:text-4xl xl:text-5xl font-bold text-[var(--theme-text-primary)] uppercase tracking-tight mb-6 group-hover:text-brand-accent transition-colors duration-300 leading-tight">
{featuredPost.data.title}
</h2>
</a>
<!-- Description -->
<p class="text-[var(--theme-text-secondary)] text-base lg:text-lg font-light leading-relaxed mb-8 line-clamp-3">
{featuredPost.data.description}
</p>
<!-- Tags -->
{featuredPost.data.tags && featuredPost.data.tags.length > 0 && (
<div class="flex flex-wrap gap-2 mb-8">
{featuredPost.data.tags.slice(0, 5).map((tag: string) => (
<span class="px-3 py-1.5 text-[10px] font-mono uppercase border border-[var(--theme-border-primary)] text-[var(--theme-text-muted)] group-hover:border-[var(--theme-border-strong)] transition-colors">
{tag}
</span>
))}
</div>
)}
<!-- Read link -->
<div class="pt-6 border-t border-[var(--theme-border-primary)]">
<a
href={`/blog/${featuredPost.id}/`}
class="inline-flex items-center gap-4 text-xs font-bold uppercase tracking-widest text-[var(--theme-text-primary)] hover:text-brand-accent transition-all duration-300 group/link"
>
Read Full Article
<span class="block w-8 h-[1px] bg-[var(--theme-border-strong)] group-hover/link:bg-brand-accent group-hover/link:w-12 transition-all duration-300"></span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="group-hover/link:translate-x-1 transition-transform duration-300"
>
<path d="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</svg>
</a>
</div>
</div>
</div>
</article>
</div>
)}
<!-- Latest Section with Filters -->
<div class="mb-16 lg:mb-24">
<div class="flex items-center gap-4 mb-8">
<span class="text-[10px] font-mono text-[var(--theme-text-muted)] uppercase tracking-widest font-bold">
/// LATEST TRANSMISSIONS
</span>
<span class="h-px flex-grow bg-[var(--theme-border-secondary)]"></span>
</div>
<!-- Filters Component -->
<BlogFilters categories={categories} />
<!-- Posts Grid -->
<div data-posts-grid class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-10">
{latestPosts.map((post, index) => (
<div
data-post
data-category={post.data.category || ''}
data-title={post.data.title}
data-description={post.data.description}
class={`animate-on-scroll slide-up stagger-${Math.min((index % 6) + 1, 6)}`}
>
<BlogCard
title={post.data.title}
description={post.data.description}
pubDate={post.data.pubDate}
heroImage={post.data.heroImage}
category={post.data.category}
tags={post.data.tags}
href={`/blog/${post.id}/`}
readTime={calculateReadingTime(post.body)}
/>
</div>
))}
</div>
<!-- Empty state (hidden by default, shown via JS when no results) -->
<div id="no-results" class="hidden text-center py-20">
<div class="text-[var(--theme-text-muted)] font-mono text-sm uppercase tracking-widest mb-4">
/// NO MATCHING ARTICLES FOUND
</div>
<p class="text-[var(--theme-text-secondary)] text-sm">
Try adjusting your search or filter criteria.
</p>
</div>
</div>
</section>
</BaseLayout>