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">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<span className={`w-2 h-2 rounded-full shrink-0 ${dot}`} />
|
<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="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>
|
</div>
|
||||||
{Array.isArray(col.cards) &&
|
{Array.isArray(col.cards) &&
|
||||||
col.cards.map((card, j) => (
|
col.cards.map((card, j) => (
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { SkillsBlock } from '@/blocks/Skills/Component'
|
||||||
import { SkillsMarqueeBlock } from '@/blocks/SkillsMarquee/Component'
|
import { SkillsMarqueeBlock } from '@/blocks/SkillsMarquee/Component'
|
||||||
import { KanbanColorBlock } from '@/blocks/KanbanColor/Component'
|
import { KanbanColorBlock } from '@/blocks/KanbanColor/Component'
|
||||||
import { KanbanHoriBlock } from '@/blocks/KanbanHori/Component'
|
import { KanbanHoriBlock } from '@/blocks/KanbanHori/Component'
|
||||||
|
import { ShowcaseBlock } from '@/blocks/Showcase/Component'
|
||||||
|
|
||||||
const blockComponents = {
|
const blockComponents = {
|
||||||
archive: ArchiveBlock,
|
archive: ArchiveBlock,
|
||||||
|
|
@ -22,6 +23,7 @@ const blockComponents = {
|
||||||
skillsMarquee: SkillsMarqueeBlock,
|
skillsMarquee: SkillsMarqueeBlock,
|
||||||
kanbanColor: KanbanColorBlock,
|
kanbanColor: KanbanColorBlock,
|
||||||
kanbanHori: KanbanHoriBlock,
|
kanbanHori: KanbanHoriBlock,
|
||||||
|
showcase: ShowcaseBlock,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RenderBlocks: React.FC<{
|
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 { SkillsMarqueeBlock } from '../../blocks/SkillsMarquee/config'
|
||||||
import { KanbanColorBlock } from '../../blocks/KanbanColor/config'
|
import { KanbanColorBlock } from '../../blocks/KanbanColor/config'
|
||||||
import { KanbanHoriBlock } from '../../blocks/KanbanHori/config'
|
import { KanbanHoriBlock } from '../../blocks/KanbanHori/config'
|
||||||
|
import { ShowcaseBlock } from '../../blocks/Showcase/config'
|
||||||
import { hero } from '@/heros/config'
|
import { hero } from '@/heros/config'
|
||||||
import { slugField } from 'payload'
|
import { slugField } from 'payload'
|
||||||
import { populatePublishedAt } from '../../hooks/populatePublishedAt'
|
import { populatePublishedAt } from '../../hooks/populatePublishedAt'
|
||||||
|
|
@ -83,6 +84,7 @@ export const Pages: CollectionConfig<'pages'> = {
|
||||||
SkillsMarqueeBlock,
|
SkillsMarqueeBlock,
|
||||||
KanbanColorBlock,
|
KanbanColorBlock,
|
||||||
KanbanHoriBlock,
|
KanbanHoriBlock,
|
||||||
|
ShowcaseBlock,
|
||||||
],
|
],
|
||||||
required: true,
|
required: true,
|
||||||
admin: {
|
admin: {
|
||||||
|
|
|
||||||
|
|
@ -299,6 +299,36 @@ export interface Page {
|
||||||
blockName?: string | null;
|
blockName?: string | null;
|
||||||
blockType: 'kanbanHori';
|
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?: {
|
meta?: {
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
|
|
@ -1262,6 +1292,37 @@ export interface PagesSelect<T extends boolean = true> {
|
||||||
id?: T;
|
id?: T;
|
||||||
blockName?: 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?:
|
meta?:
|
||||||
| T
|
| T
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue