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