Nicholai 06e1cdb58e Add search with lunr and read time to site
- Add lunr dependency to package.json and pnpm-lock.yaml.
- Implement SearchDialog component for full‑text search using lunr.
- Add readTime prop to BlogCard with default 5 min read.
- Add reading‑time utility for estimated read duration.
- Update layout and page components to include new props.
- Generate search.json data for indexing.

Hubert The Eunuch
2025-12-24 03:48:51 -07:00

146 lines
4.6 KiB
Plaintext

---
import { Image } from 'astro:assets';
import type { ImageMetadata } from 'astro';
import FormattedDate from './FormattedDate.astro';
interface Props {
title: string;
description: string;
pubDate: Date;
heroImage?: ImageMetadata;
category?: string;
tags?: string[];
href: string;
readTime?: string;
variant?: 'default' | 'compact' | 'featured';
class?: string;
}
const {
title,
description,
pubDate,
heroImage,
category,
tags,
href,
readTime = '5 min read',
variant = 'default',
class: className = '',
} = Astro.props;
const isCompact = variant === 'compact';
const isFeatured = variant === 'featured';
---
<article class:list={[
'group relative border border-[var(--theme-border-primary)] bg-[var(--theme-hover-bg)] hover:border-brand-accent/40 transition-all duration-500 overflow-hidden',
isFeatured ? 'lg:grid lg:grid-cols-2' : '',
className
]}>
<!-- Accent indicator strip -->
<div class="absolute top-0 left-0 w-1 h-full bg-[var(--theme-text-subtle)] opacity-50 group-hover:bg-brand-accent group-hover:opacity-100 transition-all duration-500"></div>
<!-- Image section -->
<a href={href} class:list={[
'block relative overflow-hidden',
isFeatured ? 'aspect-[16/10] lg:aspect-auto lg:h-full' : isCompact ? 'aspect-[16/9]' : 'aspect-[16/9]'
]}>
{heroImage && (
<Image
src={heroImage}
alt=""
width={isFeatured ? 800 : 720}
height={isFeatured ? 500 : 360}
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-t from-[var(--theme-card-gradient)] to-transparent"></div>
<!-- Category badge overlay -->
{category && (
<div class="absolute top-4 left-4">
<span class="px-3 py-1.5 text-[10px] font-mono font-bold uppercase tracking-widest bg-[var(--theme-bg-primary)]/80 border border-[var(--theme-border-strong)] text-[var(--theme-text-primary)] backdrop-blur-sm">
{category}
</span>
</div>
)}
</a>
<!-- Content section -->
<div class:list={[
'flex flex-col',
isFeatured ? 'p-8 lg:p-12 justify-center' : isCompact ? 'p-5' : 'p-6 lg:p-8'
]}>
<!-- Technical header with metadata -->
<div class="flex items-center gap-3 mb-4">
<span class="text-[10px] font-mono text-brand-accent uppercase tracking-widest">
<FormattedDate date={pubDate} />
</span>
<span class="h-px flex-grow max-w-8 bg-[var(--theme-border-strong)]"></span>
<span class="text-[10px] font-mono text-[var(--theme-text-muted)] uppercase tracking-widest">
{readTime}
</span>
</div>
<!-- Title -->
<a href={href}>
<h3 class:list={[
'font-bold text-[var(--theme-text-primary)] uppercase tracking-tight mb-3 group-hover:text-brand-accent transition-colors duration-300 leading-tight',
isFeatured ? 'text-3xl lg:text-4xl' : isCompact ? 'text-lg' : 'text-xl lg:text-2xl'
]}>
{title}
</h3>
</a>
<!-- Description -->
<p class:list={[
'text-[var(--theme-text-secondary)] font-light leading-relaxed',
isFeatured ? 'text-base lg:text-lg line-clamp-3 mb-8' : isCompact ? 'text-sm line-clamp-2 mb-4' : 'text-sm line-clamp-2 mb-6'
]}>
{description}
</p>
<!-- Tags (only for featured and default variants) -->
{tags && tags.length > 0 && !isCompact && (
<div class="flex flex-wrap gap-2 mb-6">
{tags.slice(0, 4).map((tag) => (
<span class="px-2 py-1 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:list={[
'flex items-center',
isFeatured ? 'mt-auto pt-6 border-t border-[var(--theme-border-primary)]' : 'mt-auto'
]}>
<a
href={href}
class="inline-flex items-center gap-3 text-xs font-bold uppercase tracking-widest text-[var(--theme-text-muted)] group-hover:text-[var(--theme-text-primary)] transition-all duration-300"
>
Read Article
<span class="block w-6 h-[1px] bg-[var(--theme-text-subtle)] group-hover:bg-brand-accent group-hover:w-10 transition-all duration-300"></span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-300"
>
<path d="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</svg>
</a>
</div>
</div>
</article>