This commit is contained in:
Mackie 2026-06-03 13:26:38 +08:00
parent 891bcaea93
commit 4197f205f4
5 changed files with 188 additions and 0 deletions

View file

@ -0,0 +1,100 @@
import React from 'react'
import { ImageMedia } from '../ImageMedia' // Adjust this path to wherever your ImageMedia file lives
import type { Media as MediaType } from '@/payload-types'
type ButtonGroup = {
label?: string
url?: string
newTab?: boolean
}
type AboutProfileBlockProps = {
imagePosition?: 'left' | 'right'
image?: MediaType | null
heading?: string
subheading?: string
body?: string
primaryButton?: ButtonGroup
secondaryButton?: ButtonGroup
}
export function AboutProfileBlock({
imagePosition = 'left',
image,
heading,
subheading,
body,
primaryButton,
secondaryButton,
}: AboutProfileBlockProps) {
const isRight = imagePosition === 'right'
// Using your unified ImageMedia component handles S3 URLs, fallback widths, heights, and alt texts seamlessly
const imageEl = image ? (
<div className="w-full sm:w-[340px] shrink-0 rounded-xl overflow-hidden bg-muted/50">
<ImageMedia
resource={image}
imgClassName="w-full h-auto object-cover"
size="(max-width: 640px) 100vw, 340px"
/>
</div>
) : null
const hasPrimary = primaryButton?.label && primaryButton?.url
const hasSecondary = secondaryButton?.label && secondaryButton?.url
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-xs tracking-widest uppercase text-foreground/30">{subheading}</p>
)}
</div>
)}
<div
className={`flex flex-col sm:flex-row gap-10 items-center ${isRight ? 'sm:flex-row-reverse' : ''}`}
>
{imageEl}
<div className="flex flex-col gap-5 flex-1">
{body && (
<p className="text-sm text-foreground/60 leading-relaxed whitespace-pre-line">{body}</p>
)}
{(hasPrimary || hasSecondary) && (
<div className="flex flex-wrap gap-3 mt-2">
{hasPrimary && (
<a // <--- Fixed missing '<' opening bracket here
href={primaryButton.url}
download
className="flex items-center gap-2 text-sm px-5 py-2.5 rounded-lg bg-muted/50 border border-foreground/10 text-foreground/70 hover:text-foreground/90 hover:bg-muted transition-colors"
>
<i className="ti ti-download" style={{ fontSize: 15 }} aria-hidden="true" />
{primaryButton.label}
</a>
)}
{hasSecondary && (
<a // <--- Fixed missing '<' opening bracket here
href={secondaryButton.url}
target={secondaryButton.newTab ? '_blank' : '_self'}
rel="noopener noreferrer"
className="flex items-center gap-2 text-sm px-5 py-2.5 rounded-lg bg-muted/50 border border-foreground/10 text-foreground/70 hover:text-foreground/90 hover:bg-muted transition-colors"
>
<i className="ti ti-external-link" style={{ fontSize: 15 }} aria-hidden="true" />
{secondaryButton.label}
</a>
)}
</div>
)}
</div>
</div>
</section>
)
}

View file

@ -0,0 +1,41 @@
import type { Block } from 'payload'
export const AboutProfileBlock: Block = {
slug: 'aboutProfile',
labels: { singular: 'About Profile', plural: 'About Profiles' },
fields: [
{
name: 'imagePosition',
type: 'select',
label: 'Image position',
defaultValue: 'left',
options: [
{ label: 'Left', value: 'left' },
{ label: 'Right', value: 'right' },
],
},
{ name: 'image', type: 'upload', relationTo: 'media', label: 'Image' },
{ name: 'heading', type: 'text', label: 'Heading' },
{ name: 'subheading', type: 'text', label: 'Subheading' },
{ name: 'body', type: 'textarea', label: 'Body text' },
{
name: 'primaryButton',
type: 'group',
label: 'Primary button (Download CV)',
fields: [
{ name: 'label', type: 'text', label: 'Label', defaultValue: 'Download CV' },
{ name: 'file', type: 'upload', relationTo: 'media', label: 'Upload CV File' },
],
},
{
name: 'secondaryButton',
type: 'group',
label: 'Secondary button (Custom)',
fields: [
{ name: 'label', type: 'text', label: 'Label' },
{ name: 'url', type: 'text', label: 'URL' },
{ name: 'newTab', type: 'checkbox', label: 'Open in new tab', defaultValue: true },
],
},
],
}

View file

@ -12,6 +12,7 @@ import { SkillsMarqueeBlock } from '@/blocks/SkillsMarquee/Component'
import { KanbanColorBlock } from '@/blocks/KanbanColor/Component'
import { KanbanHoriBlock } from '@/blocks/KanbanHori/Component'
import { ShowcaseBlock } from '@/blocks/Showcase/Component'
import { AboutProfileBlock } from '@/blocks/AboutProfile/Component'
const blockComponents = {
archive: ArchiveBlock,
@ -24,6 +25,7 @@ const blockComponents = {
kanbanColor: KanbanColorBlock,
kanbanHori: KanbanHoriBlock,
showcase: ShowcaseBlock,
aboutProfile: AboutProfileBlock,
}
export const RenderBlocks: React.FC<{

View file

@ -12,6 +12,7 @@ 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 { AboutProfileBlock } from '../../blocks/AboutProfile/config'
import { hero } from '@/heros/config'
import { slugField } from 'payload'
import { populatePublishedAt } from '../../hooks/populatePublishedAt'
@ -85,6 +86,7 @@ export const Pages: CollectionConfig<'pages'> = {
KanbanColorBlock,
KanbanHoriBlock,
ShowcaseBlock,
AboutProfileBlock,
],
required: true,
admin: {

View file

@ -329,6 +329,25 @@ export interface Page {
blockName?: string | null;
blockType: 'showcase';
}
| {
imagePosition?: ('left' | 'right') | null;
image?: (string | null) | Media;
heading?: string | null;
subheading?: string | null;
body?: string | null;
primaryButton?: {
label?: string | null;
file?: (string | null) | Media;
};
secondaryButton?: {
label?: string | null;
url?: string | null;
newTab?: boolean | null;
};
id?: string | null;
blockName?: string | null;
blockType: 'aboutProfile';
}
)[];
meta?: {
title?: string | null;
@ -1323,6 +1342,30 @@ export interface PagesSelect<T extends boolean = true> {
id?: T;
blockName?: T;
};
aboutProfile?:
| T
| {
imagePosition?: T;
image?: T;
heading?: T;
subheading?: T;
body?: T;
primaryButton?:
| T
| {
label?: T;
file?: T;
};
secondaryButton?:
| T
| {
label?: T;
url?: T;
newTab?: T;
};
id?: T;
blockName?: T;
};
};
meta?:
| T