about
This commit is contained in:
parent
891bcaea93
commit
4197f205f4
5 changed files with 188 additions and 0 deletions
100
src/blocks/AboutProfile/Component.tsx
Normal file
100
src/blocks/AboutProfile/Component.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
41
src/blocks/AboutProfile/config.ts
Normal file
41
src/blocks/AboutProfile/config.ts
Normal 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 },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
@ -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<{
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue