showcase 2in 1

This commit is contained in:
Mackie 2026-06-06 07:52:01 +08:00
parent 507a1b6d01
commit 3149b881ff

View file

@ -1,15 +1,17 @@
'use client'
import React from 'react'
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 }[]
}
@ -20,125 +22,119 @@ type ShowcaseBlockProps = {
items?: ShowcaseItem[]
}
function ShowcaseImage(props: { item: ShowcaseItem }): React.ReactElement {
const item = props.item
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
if (imageUrl != null) {
const imgEl = (
<div className="relative w-full aspect-video overflow-hidden">
const imgContent = (
<div className="relative w-full aspect-video overflow-hidden">
{imageUrl ? (
<Image src={imageUrl} alt={imageAlt ?? item.title} fill className="object-cover" />
{item.imageUrl && (
) : (
<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" />
)}
{item.imageUrl && (
<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"
style={{ fontSize: 20 }}
aria-hidden="true"
/>
<i className="ti ti-external-link text-foreground/80 text-xl" aria-hidden="true" />
</div>
)}
</div>
)
if (item.imageUrl) {
return (
<a href={item.imageUrl} target="_blank" rel="noopener noreferrer" className="block group">
{imgEl}
</a>
)
}
return imgEl
}
return (
<div className="relative w-full aspect-video bg-muted/30 flex flex-col items-center justify-center gap-2 border-b border-foreground/8">
<i
className="ti ti-photo-off text-foreground/15"
style={{ fontSize: 28 }}
aria-hidden="true"
/>
<span className="text-xs text-foreground/20">Coming soon</span>
</>
)}
</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 (
// Replaced hardcoded max-w-5xl and section with the unified Container
<Container className="py-12">
{(heading || subheading) && (
<div className="mb-8">
<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>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{items.map(function (item, i) {
const isFeatured = i === 0 && items.length > 1
return (
<div
key={i}
className={
'bg-muted/50 border border-foreground/8 rounded-xl overflow-hidden flex flex-col' +
(isFeatured ? ' sm:col-span-2 lg:col-span-2' : '')
}
{/* 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'
}`}
>
<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>
)}
{Array.isArray(item.tags) && item.tags.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{item.tags.map(function (t, j) {
return (
<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>
)}
{Array.isArray(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(function (link, k) {
return (
<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"
style={{ fontSize: 12 }}
aria-hidden="true"
/>
{link.label}
</a>
)
})}
</div>
)}
</div>
{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>
)