- 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
146 lines
4.6 KiB
Plaintext
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>
|