This commit is contained in:
Mackie 2026-06-06 08:25:06 +08:00
parent 3149b881ff
commit 9db958743b

View file

@ -1,141 +1 @@
'use client'
import React, { useState } from 'react'
import Image from 'next/image'
import { Container } from '@/components/ui/Container'
import type { Media as MediaType } from '@/payload-types'
// Updated type to include category for the 15-project Lab structure
type ShowcaseItem = {
image?: MediaType | null
imageUrl?: string
title: string
description?: string
category: 'engineering' | 'design'
tags?: { tag: string }[]
links?: { label: string; url: string; newTab?: boolean }[]
}
type ShowcaseBlockProps = {
heading?: string
subheading?: string
items?: ShowcaseItem[]
}
function ShowcaseImage({ item }: { item: ShowcaseItem }): React.ReactElement {
const image = item.image
const imageUrl = image != null && typeof image === 'object' && 'url' in image ? image.url : null
const imageAlt =
image != null && typeof image === 'object' && 'alt' in image ? image.alt : item.title
const imgContent = (
<div className="relative w-full aspect-video overflow-hidden">
{imageUrl ? (
<Image src={imageUrl} alt={imageAlt ?? item.title} fill className="object-cover" />
) : (
<div className="w-full h-full bg-muted/30 flex flex-col items-center justify-center gap-2">
<i className="ti ti-photo-off text-foreground/15 text-2xl" aria-hidden="true" />
<span className="text-xs text-foreground/20">Coming soon</span>
</div>
)}
{imageUrl && (
<>
<div className="absolute inset-0 bg-background/0 group-hover:bg-background/20 transition-colors duration-200" />
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<i className="ti ti-external-link text-foreground/80 text-xl" aria-hidden="true" />
</div>
</>
)}
</div>
)
return item.imageUrl ? (
<a href={item.imageUrl} target="_blank" rel="noopener noreferrer" className="block group">
{imgContent}
</a>
) : (
imgContent
)
}
export function ShowcaseBlock(props: ShowcaseBlockProps): React.ReactElement | null {
const { heading, subheading, items } = props
const [filter, setFilter] = useState<'all' | 'engineering' | 'design'>('all')
if (!Array.isArray(items) || items.length === 0) return null
const filteredItems = filter === 'all' ? items : items.filter((item) => item.category === filter)
return (
<Container className="py-12">
<div className="mb-8 flex flex-col md:flex-row md:items-end justify-between gap-4">
<div>
{heading && <h2 className="text-2xl font-medium text-foreground mb-2">{heading}</h2>}
{subheading && <p className="text-sm text-foreground/40">{subheading}</p>}
</div>
{/* Filter Navigation */}
<div className="flex gap-2">
{(['all', 'engineering', 'design'] as const).map((f) => (
<button
key={f}
onClick={() => setFilter(f)}
className={`px-3 py-1 text-xs rounded-full border transition-colors ${
filter === f
? 'bg-foreground text-background border-foreground'
: 'bg-transparent border-foreground/8 text-foreground/60 hover:border-foreground/30'
}`}
>
{f.charAt(0).toUpperCase() + f.slice(1)}
</button>
))}
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{filteredItems.map((item, i) => (
<div
key={`${item.title}-${i}`}
className="bg-muted/50 border border-foreground/8 rounded-xl overflow-hidden flex flex-col"
>
<ShowcaseImage item={item} />
<div className="p-4 flex flex-col gap-3 flex-1">
<h3 className="text-sm font-medium text-foreground/85">{item.title}</h3>
{item.description && (
<p className="text-xs text-foreground/40 leading-relaxed">{item.description}</p>
)}
{item.tags && item.tags.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{item.tags.map((t, j) => (
<span
key={j}
className="text-xs text-foreground/40 bg-background/50 border border-foreground/8 rounded-full px-2.5 py-0.5"
>
{t.tag}
</span>
))}
</div>
)}
{item.links && item.links.length > 0 && (
<div className="flex flex-wrap gap-3 mt-auto pt-3 border-t border-foreground/8">
{item.links.map((link, k) => (
<a
key={k}
href={link.url}
target={link.newTab ? '_blank' : '_self'}
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-xs text-foreground/40 hover:text-foreground/70 transition-colors"
>
<i className="ti ti-external-link text-[12px]" aria-hidden="true" />
{link.label}
</a>
))}
</div>
)}
</div>
</div>
))}
</div>
</Container>
)
}
2