showcase
This commit is contained in:
parent
a30184d5a9
commit
7108096ad7
6 changed files with 270 additions and 3 deletions
|
|
@ -31,9 +31,6 @@ export function KanbanColorBlock({ columns }: Props) {
|
|||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className={`w-2 h-2 rounded-full shrink-0 ${dot}`} />
|
||||
<span className="text-sm font-medium text-foreground/80">{col.title}</span>
|
||||
<span className="ml-auto text-xs text-foreground/30 border border-foreground/10 rounded-full px-2 py-0.5">
|
||||
{col.cards?.length ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
{Array.isArray(col.cards) &&
|
||||
col.cards.map((card, j) => (
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { SkillsBlock } from '@/blocks/Skills/Component'
|
|||
import { SkillsMarqueeBlock } from '@/blocks/SkillsMarquee/Component'
|
||||
import { KanbanColorBlock } from '@/blocks/KanbanColor/Component'
|
||||
import { KanbanHoriBlock } from '@/blocks/KanbanHori/Component'
|
||||
import { ShowcaseBlock } from '@/blocks/Showcase/Component'
|
||||
|
||||
const blockComponents = {
|
||||
archive: ArchiveBlock,
|
||||
|
|
@ -22,6 +23,7 @@ const blockComponents = {
|
|||
skillsMarquee: SkillsMarqueeBlock,
|
||||
kanbanColor: KanbanColorBlock,
|
||||
kanbanHori: KanbanHoriBlock,
|
||||
showcase: ShowcaseBlock,
|
||||
}
|
||||
|
||||
export const RenderBlocks: React.FC<{
|
||||
|
|
|
|||
136
src/blocks/Showcase/Component.tsx
Normal file
136
src/blocks/Showcase/Component.tsx
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import React from 'react'
|
||||
import Image from 'next/image'
|
||||
|
||||
type MediaObject = {
|
||||
url?: string
|
||||
alt?: string
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
type ShowcaseItem = {
|
||||
image?: MediaObject | null
|
||||
imageUrl?: string
|
||||
title: string
|
||||
description?: string
|
||||
tags?: { tag: string }[]
|
||||
links?: { label: string; url: string; newTab?: boolean }[]
|
||||
}
|
||||
|
||||
type ShowcaseBlockProps = {
|
||||
heading?: string
|
||||
subheading?: string
|
||||
items?: ShowcaseItem[]
|
||||
}
|
||||
|
||||
const COMING_SOON_BG = 'bg-muted/50'
|
||||
|
||||
export function ShowcaseBlock({ heading, subheading, items }: ShowcaseBlockProps) {
|
||||
if (!Array.isArray(items) || items.length === 0) return null
|
||||
|
||||
return (
|
||||
<section className="w-full max-w-5xl mx-auto px-6 py-16">
|
||||
{(heading || subheading) && (
|
||||
<div className="text-center mb-12">
|
||||
{heading && (
|
||||
<h2 className="text-3xl font-semibold tracking-tight text-foreground mb-3">
|
||||
{heading}
|
||||
</h2>
|
||||
)}
|
||||
{subheading && (
|
||||
<p className="text-sm text-foreground/50 max-w-xl mx-auto">{subheading}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{items.map((item, i) => {
|
||||
const imageUrl =
|
||||
item.image && typeof item.image === 'object' ? item.image.url : null
|
||||
const imageAlt =
|
||||
item.image && typeof item.image === 'object' ? item.image.alt : item.title
|
||||
|
||||
const imageContent = imageUrl ? (
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt={imageAlt ?? item.title}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-2">
|
||||
<i className="ti ti-photo-off text-foreground/15" style={{ fontSize: 32 }} aria-hidden="true" />
|
||||
<span className="text-xs text-foreground/20">Coming soon</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
const imageWrapper = item.imageUrl ? (
|
||||
|
||||
href={item.imageUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block relative w-full aspect-video overflow-hidden rounded-t-xl group"
|
||||
>
|
||||
{imageContent}
|
||||
<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" style={{ fontSize: 20 }} aria-hidden="true" />
|
||||
</div>
|
||||
</a>
|
||||
) : (
|
||||
<div className={`relative w-full aspect-video overflow-hidden rounded-t-xl ${!imageUrl ? COMING_SOON_BG : ''}`}>
|
||||
{imageContent}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="bg-muted/50 border-0 rounded-xl overflow-hidden flex flex-col"
|
||||
>
|
||||
{imageWrapper}
|
||||
|
||||
<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/50 leading-relaxed">{item.description}</p>
|
||||
)}
|
||||
|
||||
{Array.isArray(item.tags) && item.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{item.tags.map(({ tag }, j) => (
|
||||
<span
|
||||
key={j}
|
||||
className="text-xs text-foreground/50 bg-background/50 border border-foreground/10 rounded-full px-2.5 py-0.5"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Array.isArray(item.links) && item.links.length > 0 && (
|
||||
<div className="flex flex-wrap gap-3 mt-auto pt-2 border-t border-foreground/8">
|
||||
{item.links.map((link, k) => (
|
||||
|
||||
key={k}
|
||||
href={link.url}
|
||||
target={link.newTab ? '_blank' : '_self'}
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1.5 text-xs text-foreground/50 hover:text-foreground/80 transition-colors"
|
||||
>
|
||||
<i className="ti ti-external-link" style={{ fontSize: 13 }} aria-hidden="true" />
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
69
src/blocks/Showcase/config.ts
Normal file
69
src/blocks/Showcase/config.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import type { Block } from 'payload'
|
||||
|
||||
export const ShowcaseBlock: Block = {
|
||||
slug: 'showcase',
|
||||
labels: { singular: 'Showcase', plural: 'Showcases' },
|
||||
fields: [
|
||||
{
|
||||
name: 'heading',
|
||||
type: 'text',
|
||||
label: 'Heading',
|
||||
},
|
||||
{
|
||||
name: 'subheading',
|
||||
type: 'text',
|
||||
label: 'Subheading',
|
||||
},
|
||||
{
|
||||
name: 'items',
|
||||
type: 'array',
|
||||
label: 'Items',
|
||||
minRows: 1,
|
||||
fields: [
|
||||
{
|
||||
name: 'image',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
label: 'Image (optional)',
|
||||
},
|
||||
{
|
||||
name: 'imageUrl',
|
||||
type: 'text',
|
||||
label: 'Image click URL (optional)',
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
label: 'Title',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
label: 'Description',
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
type: 'array',
|
||||
label: 'Tags',
|
||||
fields: [{ name: 'tag', type: 'text', label: 'Tag', required: true }],
|
||||
},
|
||||
{
|
||||
name: 'links',
|
||||
type: 'array',
|
||||
label: 'Links',
|
||||
fields: [
|
||||
{ name: 'label', type: 'text', label: 'Label', required: true },
|
||||
{ name: 'url', type: 'text', label: 'URL', required: true },
|
||||
{
|
||||
name: 'newTab',
|
||||
type: 'checkbox',
|
||||
label: 'Open in new tab',
|
||||
defaultValue: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import { SkillsBlock } from '../../blocks/Skills/config'
|
|||
import { SkillsMarqueeBlock } from '../../blocks/SkillsMarquee/config'
|
||||
import { KanbanColorBlock } from '../../blocks/KanbanColor/config'
|
||||
import { KanbanHoriBlock } from '../../blocks/KanbanHori/config'
|
||||
import { ShowcaseBlock } from '../../blocks/Showcase/config'
|
||||
import { hero } from '@/heros/config'
|
||||
import { slugField } from 'payload'
|
||||
import { populatePublishedAt } from '../../hooks/populatePublishedAt'
|
||||
|
|
@ -83,6 +84,7 @@ export const Pages: CollectionConfig<'pages'> = {
|
|||
SkillsMarqueeBlock,
|
||||
KanbanColorBlock,
|
||||
KanbanHoriBlock,
|
||||
ShowcaseBlock,
|
||||
],
|
||||
required: true,
|
||||
admin: {
|
||||
|
|
|
|||
|
|
@ -299,6 +299,36 @@ export interface Page {
|
|||
blockName?: string | null;
|
||||
blockType: 'kanbanHori';
|
||||
}
|
||||
| {
|
||||
heading?: string | null;
|
||||
subheading?: string | null;
|
||||
items?:
|
||||
| {
|
||||
image?: (string | null) | Media;
|
||||
imageUrl?: string | null;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
tags?:
|
||||
| {
|
||||
tag: string;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
links?:
|
||||
| {
|
||||
label: string;
|
||||
url: string;
|
||||
newTab?: boolean | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'showcase';
|
||||
}
|
||||
)[];
|
||||
meta?: {
|
||||
title?: string | null;
|
||||
|
|
@ -1262,6 +1292,37 @@ export interface PagesSelect<T extends boolean = true> {
|
|||
id?: T;
|
||||
blockName?: T;
|
||||
};
|
||||
showcase?:
|
||||
| T
|
||||
| {
|
||||
heading?: T;
|
||||
subheading?: T;
|
||||
items?:
|
||||
| T
|
||||
| {
|
||||
image?: T;
|
||||
imageUrl?: T;
|
||||
title?: T;
|
||||
description?: T;
|
||||
tags?:
|
||||
| T
|
||||
| {
|
||||
tag?: T;
|
||||
id?: T;
|
||||
};
|
||||
links?:
|
||||
| T
|
||||
| {
|
||||
label?: T;
|
||||
url?: T;
|
||||
newTab?: T;
|
||||
id?: T;
|
||||
};
|
||||
id?: T;
|
||||
};
|
||||
id?: T;
|
||||
blockName?: T;
|
||||
};
|
||||
};
|
||||
meta?:
|
||||
| T
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue