feat: enhance OGB with components from Daemez

Day 1 Phase 1 Complete: Core Infrastructure

 Enhanced Header:
- Added CTA button (enabled by default, personal CTA text)
- Added Announcement Bar (dismissible, with type variants)
- Organized in tabs for better UX
- Localized fields for multilingual support

 Enhanced Footer:
- Added Social Links (LinkedIn, Twitter, GitHub, YouTube, etc.)
- Added EU Funding Logos (MANDATORY for Kit Digital grant)
- Added Personal Info (name, professional title, location, email, phone)
- Added Newsletter section (optional)
- Organized in tabs for better UX

 Enhanced Categories:
- Added description field (localized, for SEO)
- Added color field (hex color for UI badges)
- Added order field (custom sort)
- Added type field (project/post/skill categories)
- Added icon field for visual identification

 Localization:
- Configured EN/ES/CA (English, Spanish, Catalan)
- Default locale: English
- Fallback enabled

 Projects Collection:
- Portfolio showcase collection
- Content: heroImage, short/full description, problem/solution/results
- Technical: techStack, highlights
- Media: gallery, liveUrl, githubUrl, videoUrl, caseStudyUrl
- Metadata: client, year, duration, role, teamSize
- Categories relationship (filtered to project categories)
- Related projects relationship
- SEO plugin integration
- Draft/publish workflow with schedule
- Featured flag for homepage display

 Plugins Updated:
- Search plugin: now includes 'projects' collection
- Redirects plugin: now includes 'projects' collection

 Shared Components:
- Created /components/RowLabel.tsx (generic, reusable)

 TypeScript Types:
- Generated payload-types.ts for all new fields

All changes adapted for personal branding (not B2B corporate).
Ready for testing and content creation.
This commit is contained in:
2025-11-18 08:05:01 +01:00
parent 9842a7a0e3
commit d6a61b8499
8 changed files with 1285 additions and 98 deletions
+147 -1
View File
@@ -8,23 +8,169 @@ export const Footer: GlobalConfig = {
access: { access: {
read: () => true, read: () => true,
}, },
fields: [
{
type: 'tabs',
tabs: [
{
label: 'Navigation',
fields: [ fields: [
{ {
name: 'navItems', name: 'navItems',
type: 'array', type: 'array',
label: 'Footer Links',
fields: [ fields: [
link({ link({
appearances: false, appearances: false,
}), }),
], ],
maxRows: 6, maxRows: 12,
admin: { admin: {
description: 'Main footer navigation links (Projects, Blog, About, Contact, Privacy, etc.)',
initCollapsed: true, initCollapsed: true,
components: { components: {
RowLabel: '@/Footer/RowLabel#RowLabel', RowLabel: '@/Footer/RowLabel#RowLabel',
}, },
}, },
}, },
{
name: 'socialLinks',
type: 'array',
label: 'Social Links',
fields: [
{
name: 'platform',
type: 'select',
required: true,
options: [
{ label: 'LinkedIn', value: 'linkedin' },
{ label: 'Twitter / X', value: 'twitter' },
{ label: 'GitHub', value: 'github' },
{ label: 'YouTube', value: 'youtube' },
{ label: 'Facebook', value: 'facebook' },
{ label: 'Instagram', value: 'instagram' },
],
},
{
name: 'url',
type: 'text',
required: true,
},
],
admin: {
description: 'Social media links',
initCollapsed: true,
},
},
],
},
{
label: 'Branding & Compliance',
fields: [
{
name: 'euFundingLogos',
type: 'array',
label: 'EU Funding Logos',
required: true,
fields: [
{
name: 'logo',
type: 'upload',
relationTo: 'media',
required: true,
},
{
name: 'alt',
type: 'text',
required: true,
admin: {
description: 'Alt text (e.g., "Financiado por la Unión Europea - NextGenerationEU")',
},
},
{
name: 'link',
type: 'text',
admin: {
description: 'Optional link URL',
},
},
],
admin: {
description: 'MANDATORY for Kit Digital grant: EU, Next Generation EU, Plan de Recuperación logos',
initCollapsed: false,
},
},
{
name: 'personalInfo',
type: 'group',
label: 'Personal Information',
fields: [
{
name: 'name',
type: 'text',
defaultValue: 'Oscar Gimenez',
},
{
name: 'professionalTitle',
type: 'text',
localized: true,
admin: {
description: 'Professional title or tagline (e.g., "Full Stack Developer", "Tech Consultant")',
},
},
{
name: 'location',
type: 'text',
localized: true,
admin: {
description: 'Location (e.g., "Barcelona, Spain")',
},
},
{
name: 'email',
type: 'email',
},
{
name: 'phone',
type: 'text',
},
],
},
],
},
{
label: 'Newsletter',
fields: [
{
name: 'newsletterEnabled',
type: 'checkbox',
defaultValue: false,
admin: {
description: 'Enable newsletter signup in footer',
},
},
{
name: 'newsletterHeading',
type: 'text',
localized: true,
admin: {
description: 'Newsletter section heading',
condition: (data) => data.newsletterEnabled === true,
},
},
{
name: 'newsletterDescription',
type: 'textarea',
localized: true,
admin: {
description: 'Newsletter description text',
condition: (data) => data.newsletterEnabled === true,
},
},
],
},
],
},
], ],
hooks: { hooks: {
afterChange: [revalidateFooter], afterChange: [revalidateFooter],
+128
View File
@@ -8,10 +8,17 @@ export const Header: GlobalConfig = {
access: { access: {
read: () => true, read: () => true,
}, },
fields: [
{
type: 'tabs',
tabs: [
{
label: 'Navigation',
fields: [ fields: [
{ {
name: 'navItems', name: 'navItems',
type: 'array', type: 'array',
label: 'Main Navigation',
fields: [ fields: [
link({ link({
appearances: false, appearances: false,
@@ -19,12 +26,133 @@ export const Header: GlobalConfig = {
], ],
maxRows: 6, maxRows: 6,
admin: { admin: {
description: 'Main navigation menu items',
initCollapsed: true, initCollapsed: true,
components: { components: {
RowLabel: '@/Header/RowLabel#RowLabel', RowLabel: '@/Header/RowLabel#RowLabel',
}, },
}, },
}, },
{
name: 'ctaButton',
type: 'group',
label: 'CTA Button',
fields: [
{
name: 'enabled',
type: 'checkbox',
defaultValue: true,
admin: {
description: 'Show CTA button in header',
},
},
{
name: 'text',
type: 'text',
localized: true,
admin: {
description: 'Button text (e.g., "Get in Touch", "Hire Me", "Let\'s Talk")',
condition: (data) => data.ctaButton?.enabled === true,
},
},
{
name: 'link',
type: 'text',
admin: {
description: 'Button URL or page slug',
condition: (data) => data.ctaButton?.enabled === true,
},
},
{
name: 'style',
type: 'select',
options: [
{ label: 'Primary', value: 'primary' },
{ label: 'Secondary', value: 'secondary' },
{ label: 'Outline', value: 'outline' },
],
defaultValue: 'primary',
admin: {
description: 'Button visual style',
condition: (data) => data.ctaButton?.enabled === true,
},
},
],
},
],
},
{
label: 'Announcement Bar',
fields: [
{
name: 'announcementBar',
type: 'group',
label: 'Announcement Bar',
fields: [
{
name: 'enabled',
type: 'checkbox',
defaultValue: false,
admin: {
description: 'Show announcement bar above header',
},
},
{
name: 'message',
type: 'text',
localized: true,
admin: {
description: 'Announcement text',
condition: (data) => data.announcementBar?.enabled === true,
},
},
{
name: 'link',
type: 'text',
admin: {
description: 'Optional link URL',
condition: (data) => data.announcementBar?.enabled === true,
},
},
{
name: 'linkText',
type: 'text',
localized: true,
admin: {
description: 'Link text (e.g., "Learn more")',
condition: (data) => data.announcementBar?.enabled === true,
},
},
{
name: 'type',
type: 'select',
options: [
{ label: 'Info', value: 'info' },
{ label: 'Success', value: 'success' },
{ label: 'Warning', value: 'warning' },
{ label: 'Promotion', value: 'promotion' },
],
defaultValue: 'info',
admin: {
description: 'Bar style/color',
condition: (data) => data.announcementBar?.enabled === true,
},
},
{
name: 'dismissible',
type: 'checkbox',
defaultValue: true,
admin: {
description: 'Allow users to close the announcement',
condition: (data) => data.announcementBar?.enabled === true,
},
},
],
},
],
},
],
},
], ],
hooks: { hooks: {
afterChange: [revalidateHeader], afterChange: [revalidateHeader],
+48
View File
@@ -14,15 +14,63 @@ export const Categories: CollectionConfig = {
}, },
admin: { admin: {
useAsTitle: 'title', useAsTitle: 'title',
defaultColumns: ['title', 'type', 'icon', 'updatedAt'],
}, },
fields: [ fields: [
{ {
name: 'title', name: 'title',
type: 'text', type: 'text',
required: true, required: true,
localized: true,
},
{
name: 'description',
type: 'textarea',
localized: true,
admin: {
description: 'Category description for archive pages and SEO',
},
}, },
slugField({ slugField({
position: undefined, position: undefined,
}), }),
{
name: 'type',
type: 'select',
required: true,
options: [
{ label: 'Project Category', value: 'project' },
{ label: 'Post Category', value: 'post' },
{ label: 'Skill Category', value: 'skill' },
],
admin: {
position: 'sidebar',
description: 'What type of content this category is for',
},
},
{
name: 'icon',
type: 'text',
admin: {
position: 'sidebar',
description: 'Icon name (e.g., code, shield, cloud)',
},
},
{
name: 'color',
type: 'text',
admin: {
position: 'sidebar',
description: 'Hex color for UI badges/tags (e.g., #3B82F6)',
},
},
{
name: 'order',
type: 'number',
admin: {
position: 'sidebar',
description: 'Custom sort order (lower numbers appear first)',
},
},
], ],
} }
+402
View File
@@ -0,0 +1,402 @@
import type { CollectionConfig } from 'payload'
import { authenticated } from '../access/authenticated'
import { authenticatedOrPublished } from '../access/authenticatedOrPublished'
import { slugField } from 'payload'
import { populatePublishedAt } from '../hooks/populatePublishedAt'
import { generatePreviewPath } from '../utilities/generatePreviewPath'
import {
MetaDescriptionField,
MetaImageField,
MetaTitleField,
OverviewField,
PreviewField,
} from '@payloadcms/plugin-seo/fields'
export const Projects: CollectionConfig = {
slug: 'projects',
labels: {
singular: 'Project',
plural: 'Projects',
},
access: {
create: authenticated,
delete: authenticated,
read: authenticatedOrPublished,
update: authenticated,
},
defaultPopulate: {
name: true,
slug: true,
categories: true,
meta: {
image: true,
description: true,
},
},
admin: {
useAsTitle: 'name',
defaultColumns: ['name', 'client', 'year', 'featured', 'updatedAt'],
livePreview: {
url: ({ data, req }) =>
generatePreviewPath({
slug: data?.slug,
collection: 'projects',
req,
}),
},
preview: (data, { req }) =>
generatePreviewPath({
slug: data?.slug as string,
collection: 'projects',
req,
}),
},
fields: [
{
name: 'name',
type: 'text',
required: true,
localized: true,
},
{
type: 'tabs',
tabs: [
{
label: 'Content',
fields: [
{
name: 'heroImage',
type: 'upload',
relationTo: 'media',
required: true,
admin: {
description: 'Main project image/screenshot',
},
},
{
name: 'shortDescription',
type: 'textarea',
required: true,
localized: true,
admin: {
description: 'Brief description for cards and previews (max 200 chars)',
},
validate: (val: string | null | undefined) => {
if (val && val.length > 200) {
return 'Short description must be 200 characters or less'
}
return true
},
},
{
name: 'description',
type: 'richText',
required: true,
localized: true,
admin: {
description: 'Full project description and details',
},
},
{
name: 'problem',
type: 'richText',
localized: true,
admin: {
description: 'Problem statement: What challenge did this project solve?',
},
},
{
name: 'solution',
type: 'richText',
localized: true,
admin: {
description: 'Solution: How did you solve it?',
},
},
{
name: 'results',
type: 'richText',
localized: true,
admin: {
description: 'Results: What were the outcomes?',
},
},
{
name: 'client',
type: 'text',
localized: true,
admin: {
description: 'Client name (optional, can anonymize if needed)',
},
},
{
name: 'year',
type: 'number',
admin: {
description: 'Year of completion',
},
},
{
name: 'duration',
type: 'text',
localized: true,
admin: {
description: 'Project duration (e.g., "3 months", "6 weeks")',
},
},
{
name: 'role',
type: 'text',
localized: true,
admin: {
description: 'Your role in the project (e.g., "Lead Developer", "Full Stack Developer")',
},
},
{
name: 'teamSize',
type: 'text',
localized: true,
admin: {
description: 'Team size (e.g., "Solo project", "Team of 5")',
},
},
],
},
{
label: 'Technical',
fields: [
{
name: 'techStack',
type: 'array',
label: 'Tech Stack',
required: true,
fields: [
{
name: 'name',
type: 'text',
required: true,
admin: {
description: 'Technology name (e.g., "React", "Node.js", "PostgreSQL")',
},
},
{
name: 'category',
type: 'select',
options: [
{ label: 'Frontend', value: 'frontend' },
{ label: 'Backend', value: 'backend' },
{ label: 'Database', value: 'database' },
{ label: 'DevOps', value: 'devops' },
{ label: 'Tool', value: 'tool' },
{ label: 'Other', value: 'other' },
],
},
],
admin: {
initCollapsed: true,
components: {
RowLabel: '@/components/RowLabel#RowLabel',
},
},
},
{
name: 'highlights',
type: 'array',
label: 'Technical Highlights',
fields: [
{
name: 'title',
type: 'text',
required: true,
localized: true,
},
{
name: 'description',
type: 'textarea',
required: true,
localized: true,
},
],
admin: {
description: 'Key technical achievements or interesting challenges solved',
initCollapsed: true,
components: {
RowLabel: '@/components/RowLabel#RowLabel',
},
},
},
],
},
{
label: 'Media & Links',
fields: [
{
name: 'gallery',
type: 'array',
fields: [
{
name: 'image',
type: 'upload',
relationTo: 'media',
required: true,
},
{
name: 'caption',
type: 'text',
localized: true,
},
{
name: 'alt',
type: 'text',
required: true,
localized: true,
},
],
admin: {
description: 'Additional screenshots, diagrams, or images',
},
},
{
name: 'liveUrl',
type: 'text',
admin: {
description: 'Link to live project (if public)',
},
},
{
name: 'githubUrl',
type: 'text',
admin: {
description: 'GitHub repository URL (if public)',
},
},
{
name: 'videoUrl',
type: 'text',
admin: {
description: 'Project demo video URL (YouTube, Vimeo, etc.)',
},
},
{
name: 'caseStudyUrl',
type: 'text',
admin: {
description: 'Link to external case study or blog post',
},
},
],
},
{
label: 'Relationships',
fields: [
{
name: 'categories',
type: 'relationship',
relationTo: 'categories',
hasMany: true,
required: true,
filterOptions: {
type: { equals: 'project' },
},
admin: {
description: 'Project categories (can select multiple)',
},
},
{
name: 'relatedProjects',
type: 'relationship',
relationTo: 'projects',
hasMany: true,
filterOptions: ({ id }) => ({
id: { not_in: [id] },
}),
admin: {
description: 'Similar or related projects',
},
},
],
},
{
name: 'meta',
label: 'SEO',
fields: [
OverviewField({
titlePath: 'meta.title',
descriptionPath: 'meta.description',
imagePath: 'meta.image',
}),
MetaTitleField({
hasGenerateFn: true,
}),
MetaImageField({
relationTo: 'media',
}),
MetaDescriptionField({}),
PreviewField({
hasGenerateFn: true,
titlePath: 'meta.title',
descriptionPath: 'meta.description',
}),
],
},
],
},
{
name: 'featured',
type: 'checkbox',
defaultValue: false,
admin: {
position: 'sidebar',
description: 'Display this project prominently on the homepage',
},
},
{
name: 'status',
type: 'select',
required: true,
defaultValue: 'completed',
options: [
{ label: 'Completed', value: 'completed' },
{ label: 'In Progress', value: 'in-progress' },
{ label: 'Archived', value: 'archived' },
],
admin: {
position: 'sidebar',
},
},
{
name: 'publishedAt',
type: 'date',
admin: {
position: 'sidebar',
date: {
pickerAppearance: 'dayAndTime',
},
},
hooks: {
beforeChange: [
({ siblingData, value }) => {
if (siblingData._status === 'published' && !value) {
return new Date()
}
return value
},
],
},
},
slugField(),
],
hooks: {
beforeChange: [populatePublishedAt],
},
versions: {
drafts: {
autosave: {
interval: 100,
},
schedulePublish: true,
},
maxPerDoc: 50,
},
}
+11
View File
@@ -0,0 +1,11 @@
'use client'
import { useRowLabel } from '@payloadcms/ui'
export const RowLabel = () => {
const { data, rowNumber } = useRowLabel()
// Try common field names for labels
const label = data?.title || data?.name || data?.clientName || data?.label
return label || `Item ${String(rowNumber).padStart(2, '0')}`
}
+500 -67
View File
@@ -69,6 +69,7 @@ export interface Config {
collections: { collections: {
pages: Page; pages: Page;
posts: Post; posts: Post;
projects: Project;
media: Media; media: Media;
categories: Category; categories: Category;
users: User; users: User;
@@ -91,6 +92,7 @@ export interface Config {
collectionsSelect: { collectionsSelect: {
pages: PagesSelect<false> | PagesSelect<true>; pages: PagesSelect<false> | PagesSelect<true>;
posts: PostsSelect<false> | PostsSelect<true>; posts: PostsSelect<false> | PostsSelect<true>;
projects: ProjectsSelect<false> | ProjectsSelect<true>;
media: MediaSelect<false> | MediaSelect<true>; media: MediaSelect<false> | MediaSelect<true>;
categories: CategoriesSelect<false> | CategoriesSelect<true>; categories: CategoriesSelect<false> | CategoriesSelect<true>;
users: UsersSelect<false> | UsersSelect<true>; users: UsersSelect<false> | UsersSelect<true>;
@@ -106,7 +108,7 @@ export interface Config {
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>; 'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
}; };
db: { db: {
defaultIDType: string; defaultIDType: number;
}; };
globals: { globals: {
header: Header; header: Header;
@@ -116,7 +118,7 @@ export interface Config {
header: HeaderSelect<false> | HeaderSelect<true>; header: HeaderSelect<false> | HeaderSelect<true>;
footer: FooterSelect<false> | FooterSelect<true>; footer: FooterSelect<false> | FooterSelect<true>;
}; };
locale: null; locale: 'en' | 'es' | 'ca';
user: User & { user: User & {
collection: 'users'; collection: 'users';
}; };
@@ -154,7 +156,7 @@ export interface UserAuthOperations {
* via the `definition` "pages". * via the `definition` "pages".
*/ */
export interface Page { export interface Page {
id: string; id: number;
title: string; title: string;
hero: { hero: {
type: 'none' | 'highImpact' | 'mediumImpact' | 'lowImpact'; type: 'none' | 'highImpact' | 'mediumImpact' | 'lowImpact';
@@ -181,11 +183,11 @@ export interface Page {
reference?: reference?:
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: string | Page; value: number | Page;
} | null) } | null)
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: string | Post; value: number | Post;
} | null); } | null);
url?: string | null; url?: string | null;
label: string; label: string;
@@ -197,7 +199,7 @@ export interface Page {
id?: string | null; id?: string | null;
}[] }[]
| null; | null;
media?: (string | null) | Media; media?: (number | null) | Media;
}; };
layout: (CallToActionBlock | ContentBlock | MediaBlock | ArchiveBlock | FormBlock)[]; layout: (CallToActionBlock | ContentBlock | MediaBlock | ArchiveBlock | FormBlock)[];
meta?: { meta?: {
@@ -205,7 +207,7 @@ export interface Page {
/** /**
* Maximum upload file size: 12MB. Recommended file size for images is <500KB. * Maximum upload file size: 12MB. Recommended file size for images is <500KB.
*/ */
image?: (string | null) | Media; image?: (number | null) | Media;
description?: string | null; description?: string | null;
}; };
publishedAt?: string | null; publishedAt?: string | null;
@@ -223,9 +225,9 @@ export interface Page {
* via the `definition` "posts". * via the `definition` "posts".
*/ */
export interface Post { export interface Post {
id: string; id: number;
title: string; title: string;
heroImage?: (string | null) | Media; heroImage?: (number | null) | Media;
content: { content: {
root: { root: {
type: string; type: string;
@@ -241,18 +243,18 @@ export interface Post {
}; };
[k: string]: unknown; [k: string]: unknown;
}; };
relatedPosts?: (string | Post)[] | null; relatedPosts?: (number | Post)[] | null;
categories?: (string | Category)[] | null; categories?: (number | Category)[] | null;
meta?: { meta?: {
title?: string | null; title?: string | null;
/** /**
* Maximum upload file size: 12MB. Recommended file size for images is <500KB. * Maximum upload file size: 12MB. Recommended file size for images is <500KB.
*/ */
image?: (string | null) | Media; image?: (number | null) | Media;
description?: string | null; description?: string | null;
}; };
publishedAt?: string | null; publishedAt?: string | null;
authors?: (string | User)[] | null; authors?: (number | User)[] | null;
populatedAuthors?: populatedAuthors?:
| { | {
id?: string | null; id?: string | null;
@@ -273,7 +275,7 @@ export interface Post {
* via the `definition` "media". * via the `definition` "media".
*/ */
export interface Media { export interface Media {
id: string; id: number;
alt?: string | null; alt?: string | null;
caption?: { caption?: {
root: { root: {
@@ -290,7 +292,7 @@ export interface Media {
}; };
[k: string]: unknown; [k: string]: unknown;
} | null; } | null;
folder?: (string | null) | FolderInterface; folder?: (number | null) | FolderInterface;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
url?: string | null; url?: string | null;
@@ -366,18 +368,18 @@ export interface Media {
* via the `definition` "payload-folders". * via the `definition` "payload-folders".
*/ */
export interface FolderInterface { export interface FolderInterface {
id: string; id: number;
name: string; name: string;
folder?: (string | null) | FolderInterface; folder?: (number | null) | FolderInterface;
documentsAndFolders?: { documentsAndFolders?: {
docs?: ( docs?: (
| { | {
relationTo?: 'payload-folders'; relationTo?: 'payload-folders';
value: string | FolderInterface; value: number | FolderInterface;
} }
| { | {
relationTo?: 'media'; relationTo?: 'media';
value: string | Media; value: number | Media;
} }
)[]; )[];
hasNextPage?: boolean; hasNextPage?: boolean;
@@ -392,17 +394,37 @@ export interface FolderInterface {
* via the `definition` "categories". * via the `definition` "categories".
*/ */
export interface Category { export interface Category {
id: string; id: number;
title: string; title: string;
/**
* Category description for archive pages and SEO
*/
description?: string | null;
/** /**
* When enabled, the slug will auto-generate from the title field on save and autosave. * When enabled, the slug will auto-generate from the title field on save and autosave.
*/ */
generateSlug?: boolean | null; generateSlug?: boolean | null;
slug: string; slug: string;
parent?: (string | null) | Category; /**
* What type of content this category is for
*/
type: 'project' | 'post' | 'skill';
/**
* Icon name (e.g., code, shield, cloud)
*/
icon?: string | null;
/**
* Hex color for UI badges/tags (e.g., #3B82F6)
*/
color?: string | null;
/**
* Custom sort order (lower numbers appear first)
*/
order?: number | null;
parent?: (number | null) | Category;
breadcrumbs?: breadcrumbs?:
| { | {
doc?: (string | null) | Category; doc?: (number | null) | Category;
url?: string | null; url?: string | null;
label?: string | null; label?: string | null;
id?: string | null; id?: string | null;
@@ -416,7 +438,7 @@ export interface Category {
* via the `definition` "users". * via the `definition` "users".
*/ */
export interface User { export interface User {
id: string; id: number;
name?: string | null; name?: string | null;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
@@ -464,11 +486,11 @@ export interface CallToActionBlock {
reference?: reference?:
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: string | Page; value: number | Page;
} | null) } | null)
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: string | Post; value: number | Post;
} | null); } | null);
url?: string | null; url?: string | null;
label: string; label: string;
@@ -514,11 +536,11 @@ export interface ContentBlock {
reference?: reference?:
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: string | Page; value: number | Page;
} | null) } | null)
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: string | Post; value: number | Post;
} | null); } | null);
url?: string | null; url?: string | null;
label: string; label: string;
@@ -539,7 +561,7 @@ export interface ContentBlock {
* via the `definition` "MediaBlock". * via the `definition` "MediaBlock".
*/ */
export interface MediaBlock { export interface MediaBlock {
media: string | Media; media: number | Media;
id?: string | null; id?: string | null;
blockName?: string | null; blockName?: string | null;
blockType: 'mediaBlock'; blockType: 'mediaBlock';
@@ -566,12 +588,12 @@ export interface ArchiveBlock {
} | null; } | null;
populateBy?: ('collection' | 'selection') | null; populateBy?: ('collection' | 'selection') | null;
relationTo?: 'posts' | null; relationTo?: 'posts' | null;
categories?: (string | Category)[] | null; categories?: (number | Category)[] | null;
limit?: number | null; limit?: number | null;
selectedDocs?: selectedDocs?:
| { | {
relationTo: 'posts'; relationTo: 'posts';
value: string | Post; value: number | Post;
}[] }[]
| null; | null;
id?: string | null; id?: string | null;
@@ -583,7 +605,7 @@ export interface ArchiveBlock {
* via the `definition` "FormBlock". * via the `definition` "FormBlock".
*/ */
export interface FormBlock { export interface FormBlock {
form: string | Form; form: number | Form;
enableIntro?: boolean | null; enableIntro?: boolean | null;
introContent?: { introContent?: {
root: { root: {
@@ -609,7 +631,7 @@ export interface FormBlock {
* via the `definition` "forms". * via the `definition` "forms".
*/ */
export interface Form { export interface Form {
id: string; id: number;
title: string; title: string;
fields?: fields?:
| ( | (
@@ -778,12 +800,195 @@ export interface Form {
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "projects".
*/
export interface Project {
id: number;
name: string;
/**
* Main project image/screenshot
*/
heroImage: number | Media;
/**
* Brief description for cards and previews (max 200 chars)
*/
shortDescription: string;
/**
* Full project description and details
*/
description: {
root: {
type: string;
children: {
type: any;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
};
/**
* Problem statement: What challenge did this project solve?
*/
problem?: {
root: {
type: string;
children: {
type: any;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
/**
* Solution: How did you solve it?
*/
solution?: {
root: {
type: string;
children: {
type: any;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
/**
* Results: What were the outcomes?
*/
results?: {
root: {
type: string;
children: {
type: any;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
/**
* Client name (optional, can anonymize if needed)
*/
client?: string | null;
/**
* Year of completion
*/
year?: number | null;
/**
* Project duration (e.g., "3 months", "6 weeks")
*/
duration?: string | null;
/**
* Your role in the project (e.g., "Lead Developer", "Full Stack Developer")
*/
role?: string | null;
/**
* Team size (e.g., "Solo project", "Team of 5")
*/
teamSize?: string | null;
techStack: {
/**
* Technology name (e.g., "React", "Node.js", "PostgreSQL")
*/
name: string;
category?: ('frontend' | 'backend' | 'database' | 'devops' | 'tool' | 'other') | null;
id?: string | null;
}[];
/**
* Key technical achievements or interesting challenges solved
*/
highlights?:
| {
title: string;
description: string;
id?: string | null;
}[]
| null;
/**
* Additional screenshots, diagrams, or images
*/
gallery?:
| {
image: number | Media;
caption?: string | null;
alt: string;
id?: string | null;
}[]
| null;
/**
* Link to live project (if public)
*/
liveUrl?: string | null;
/**
* GitHub repository URL (if public)
*/
githubUrl?: string | null;
/**
* Project demo video URL (YouTube, Vimeo, etc.)
*/
videoUrl?: string | null;
/**
* Link to external case study or blog post
*/
caseStudyUrl?: string | null;
/**
* Project categories (can select multiple)
*/
categories: (number | Category)[];
/**
* Similar or related projects
*/
relatedProjects?: (number | Project)[] | null;
meta?: {
title?: string | null;
/**
* Maximum upload file size: 12MB. Recommended file size for images is <500KB.
*/
image?: (number | null) | Media;
description?: string | null;
};
/**
* Display this project prominently on the homepage
*/
featured?: boolean | null;
status: 'completed' | 'in-progress' | 'archived';
publishedAt?: string | null;
/**
* When enabled, the slug will auto-generate from the title field on save and autosave.
*/
generateSlug?: boolean | null;
slug: string;
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "redirects". * via the `definition` "redirects".
*/ */
export interface Redirect { export interface Redirect {
id: string; id: number;
/** /**
* You will need to rebuild the website when changing this field. * You will need to rebuild the website when changing this field.
*/ */
@@ -793,11 +998,15 @@ export interface Redirect {
reference?: reference?:
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: string | Page; value: number | Page;
} | null) } | null)
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: string | Post; value: number | Post;
} | null)
| ({
relationTo: 'projects';
value: number | Project;
} | null); } | null);
url?: string | null; url?: string | null;
}; };
@@ -809,8 +1018,8 @@ export interface Redirect {
* via the `definition` "form-submissions". * via the `definition` "form-submissions".
*/ */
export interface FormSubmission { export interface FormSubmission {
id: string; id: number;
form: string | Form; form: number | Form;
submissionData?: submissionData?:
| { | {
field: string; field: string;
@@ -828,18 +1037,23 @@ export interface FormSubmission {
* via the `definition` "search". * via the `definition` "search".
*/ */
export interface Search { export interface Search {
id: string; id: number;
title?: string | null; title?: string | null;
priority?: number | null; priority?: number | null;
doc: { doc:
| {
relationTo: 'posts'; relationTo: 'posts';
value: string | Post; value: number | Post;
}
| {
relationTo: 'projects';
value: number | Project;
}; };
slug?: string | null; slug?: string | null;
meta?: { meta?: {
title?: string | null; title?: string | null;
description?: string | null; description?: string | null;
image?: (string | null) | Media; image?: (number | null) | Media;
}; };
categories?: categories?:
| { | {
@@ -857,7 +1071,7 @@ export interface Search {
* via the `definition` "payload-kv". * via the `definition` "payload-kv".
*/ */
export interface PayloadKv { export interface PayloadKv {
id: string; id: number;
key: string; key: string;
data: data:
| { | {
@@ -874,7 +1088,7 @@ export interface PayloadKv {
* via the `definition` "payload-jobs". * via the `definition` "payload-jobs".
*/ */
export interface PayloadJob { export interface PayloadJob {
id: string; id: number;
/** /**
* Input data provided to the job * Input data provided to the job
*/ */
@@ -966,52 +1180,56 @@ export interface PayloadJob {
* via the `definition` "payload-locked-documents". * via the `definition` "payload-locked-documents".
*/ */
export interface PayloadLockedDocument { export interface PayloadLockedDocument {
id: string; id: number;
document?: document?:
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: string | Page; value: number | Page;
} | null) } | null)
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: string | Post; value: number | Post;
} | null)
| ({
relationTo: 'projects';
value: number | Project;
} | null) } | null)
| ({ | ({
relationTo: 'media'; relationTo: 'media';
value: string | Media; value: number | Media;
} | null) } | null)
| ({ | ({
relationTo: 'categories'; relationTo: 'categories';
value: string | Category; value: number | Category;
} | null) } | null)
| ({ | ({
relationTo: 'users'; relationTo: 'users';
value: string | User; value: number | User;
} | null) } | null)
| ({ | ({
relationTo: 'redirects'; relationTo: 'redirects';
value: string | Redirect; value: number | Redirect;
} | null) } | null)
| ({ | ({
relationTo: 'forms'; relationTo: 'forms';
value: string | Form; value: number | Form;
} | null) } | null)
| ({ | ({
relationTo: 'form-submissions'; relationTo: 'form-submissions';
value: string | FormSubmission; value: number | FormSubmission;
} | null) } | null)
| ({ | ({
relationTo: 'search'; relationTo: 'search';
value: string | Search; value: number | Search;
} | null) } | null)
| ({ | ({
relationTo: 'payload-folders'; relationTo: 'payload-folders';
value: string | FolderInterface; value: number | FolderInterface;
} | null); } | null);
globalSlug?: string | null; globalSlug?: string | null;
user: { user: {
relationTo: 'users'; relationTo: 'users';
value: string | User; value: number | User;
}; };
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
@@ -1021,10 +1239,10 @@ export interface PayloadLockedDocument {
* via the `definition` "payload-preferences". * via the `definition` "payload-preferences".
*/ */
export interface PayloadPreference { export interface PayloadPreference {
id: string; id: number;
user: { user: {
relationTo: 'users'; relationTo: 'users';
value: string | User; value: number | User;
}; };
key?: string | null; key?: string | null;
value?: value?:
@@ -1044,7 +1262,7 @@ export interface PayloadPreference {
* via the `definition` "payload-migrations". * via the `definition` "payload-migrations".
*/ */
export interface PayloadMigration { export interface PayloadMigration {
id: string; id: number;
name?: string | null; name?: string | null;
batch?: number | null; batch?: number | null;
updatedAt: string; updatedAt: string;
@@ -1216,6 +1434,67 @@ export interface PostsSelect<T extends boolean = true> {
createdAt?: T; createdAt?: T;
_status?: T; _status?: T;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "projects_select".
*/
export interface ProjectsSelect<T extends boolean = true> {
name?: T;
heroImage?: T;
shortDescription?: T;
description?: T;
problem?: T;
solution?: T;
results?: T;
client?: T;
year?: T;
duration?: T;
role?: T;
teamSize?: T;
techStack?:
| T
| {
name?: T;
category?: T;
id?: T;
};
highlights?:
| T
| {
title?: T;
description?: T;
id?: T;
};
gallery?:
| T
| {
image?: T;
caption?: T;
alt?: T;
id?: T;
};
liveUrl?: T;
githubUrl?: T;
videoUrl?: T;
caseStudyUrl?: T;
categories?: T;
relatedProjects?: T;
meta?:
| T
| {
title?: T;
image?: T;
description?: T;
};
featured?: T;
status?: T;
publishedAt?: T;
generateSlug?: T;
slug?: T;
updatedAt?: T;
createdAt?: T;
_status?: T;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "media_select". * via the `definition` "media_select".
@@ -1316,8 +1595,13 @@ export interface MediaSelect<T extends boolean = true> {
*/ */
export interface CategoriesSelect<T extends boolean = true> { export interface CategoriesSelect<T extends boolean = true> {
title?: T; title?: T;
description?: T;
generateSlug?: T; generateSlug?: T;
slug?: T; slug?: T;
type?: T;
icon?: T;
color?: T;
order?: T;
parent?: T; parent?: T;
breadcrumbs?: breadcrumbs?:
| T | T
@@ -1633,7 +1917,10 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
* via the `definition` "header". * via the `definition` "header".
*/ */
export interface Header { export interface Header {
id: string; id: number;
/**
* Main navigation menu items
*/
navItems?: navItems?:
| { | {
link: { link: {
@@ -1642,11 +1929,11 @@ export interface Header {
reference?: reference?:
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: string | Page; value: number | Page;
} | null) } | null)
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: string | Post; value: number | Post;
} | null); } | null);
url?: string | null; url?: string | null;
label: string; label: string;
@@ -1654,6 +1941,50 @@ export interface Header {
id?: string | null; id?: string | null;
}[] }[]
| null; | null;
ctaButton?: {
/**
* Show CTA button in header
*/
enabled?: boolean | null;
/**
* Button text (e.g., "Get in Touch", "Hire Me", "Let's Talk")
*/
text?: string | null;
/**
* Button URL or page slug
*/
link?: string | null;
/**
* Button visual style
*/
style?: ('primary' | 'secondary' | 'outline') | null;
};
announcementBar?: {
/**
* Show announcement bar above header
*/
enabled?: boolean | null;
/**
* Announcement text
*/
message?: string | null;
/**
* Optional link URL
*/
link?: string | null;
/**
* Link text (e.g., "Learn more")
*/
linkText?: string | null;
/**
* Bar style/color
*/
type?: ('info' | 'success' | 'warning' | 'promotion') | null;
/**
* Allow users to close the announcement
*/
dismissible?: boolean | null;
};
updatedAt?: string | null; updatedAt?: string | null;
createdAt?: string | null; createdAt?: string | null;
} }
@@ -1662,7 +1993,10 @@ export interface Header {
* via the `definition` "footer". * via the `definition` "footer".
*/ */
export interface Footer { export interface Footer {
id: string; id: number;
/**
* Main footer navigation links (Projects, Blog, About, Contact, Privacy, etc.)
*/
navItems?: navItems?:
| { | {
link: { link: {
@@ -1671,11 +2005,11 @@ export interface Footer {
reference?: reference?:
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: string | Page; value: number | Page;
} | null) } | null)
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: string | Post; value: number | Post;
} | null); } | null);
url?: string | null; url?: string | null;
label: string; label: string;
@@ -1683,6 +2017,56 @@ export interface Footer {
id?: string | null; id?: string | null;
}[] }[]
| null; | null;
/**
* Social media links
*/
socialLinks?:
| {
platform: 'linkedin' | 'twitter' | 'github' | 'youtube' | 'facebook' | 'instagram';
url: string;
id?: string | null;
}[]
| null;
/**
* MANDATORY for Kit Digital grant: EU, Next Generation EU, Plan de Recuperación logos
*/
euFundingLogos: {
logo: number | Media;
/**
* Alt text (e.g., "Financiado por la Unión Europea - NextGenerationEU")
*/
alt: string;
/**
* Optional link URL
*/
link?: string | null;
id?: string | null;
}[];
personalInfo?: {
name?: string | null;
/**
* Professional title or tagline (e.g., "Full Stack Developer", "Tech Consultant")
*/
professionalTitle?: string | null;
/**
* Location (e.g., "Barcelona, Spain")
*/
location?: string | null;
email?: string | null;
phone?: string | null;
};
/**
* Enable newsletter signup in footer
*/
newsletterEnabled?: boolean | null;
/**
* Newsletter section heading
*/
newsletterHeading?: string | null;
/**
* Newsletter description text
*/
newsletterDescription?: string | null;
updatedAt?: string | null; updatedAt?: string | null;
createdAt?: string | null; createdAt?: string | null;
} }
@@ -1705,6 +2089,24 @@ export interface HeaderSelect<T extends boolean = true> {
}; };
id?: T; id?: T;
}; };
ctaButton?:
| T
| {
enabled?: T;
text?: T;
link?: T;
style?: T;
};
announcementBar?:
| T
| {
enabled?: T;
message?: T;
link?: T;
linkText?: T;
type?: T;
dismissible?: T;
};
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
globalType?: T; globalType?: T;
@@ -1728,6 +2130,33 @@ export interface FooterSelect<T extends boolean = true> {
}; };
id?: T; id?: T;
}; };
socialLinks?:
| T
| {
platform?: T;
url?: T;
id?: T;
};
euFundingLogos?:
| T
| {
logo?: T;
alt?: T;
link?: T;
id?: T;
};
personalInfo?:
| T
| {
name?: T;
professionalTitle?: T;
location?: T;
email?: T;
phone?: T;
};
newsletterEnabled?: T;
newsletterHeading?: T;
newsletterDescription?: T;
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
globalType?: T; globalType?: T;
@@ -1743,14 +2172,18 @@ export interface TaskSchedulePublish {
doc?: doc?:
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: string | Page; value: number | Page;
} | null) } | null)
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: string | Post; value: number | Post;
} | null)
| ({
relationTo: 'projects';
value: number | Project;
} | null); } | null);
global?: string | null; global?: string | null;
user?: (string | null) | User; user?: (number | null) | User;
}; };
output?: unknown; output?: unknown;
} }
+20 -1
View File
@@ -10,6 +10,7 @@ import { Categories } from './collections/Categories'
import { Media } from './collections/Media' import { Media } from './collections/Media'
import { Pages } from './collections/Pages' import { Pages } from './collections/Pages'
import { Posts } from './collections/Posts' import { Posts } from './collections/Posts'
import { Projects } from './collections/Projects'
import { Users } from './collections/Users' import { Users } from './collections/Users'
import { Footer } from './Footer/config' import { Footer } from './Footer/config'
import { Header } from './Header/config' import { Header } from './Header/config'
@@ -57,6 +58,24 @@ export default buildConfig({
], ],
}, },
}, },
localization: {
locales: [
{
label: 'English',
code: 'en',
},
{
label: 'Español',
code: 'es',
},
{
label: 'Català',
code: 'ca',
},
],
defaultLocale: 'en',
fallback: true,
},
// This config helps us configure global or default features that the other editors can inherit // This config helps us configure global or default features that the other editors can inherit
editor: defaultLexical, editor: defaultLexical,
db: postgresAdapter({ db: postgresAdapter({
@@ -64,7 +83,7 @@ export default buildConfig({
connectionString: process.env.DATABASE_URI || '', connectionString: process.env.DATABASE_URI || '',
}, },
}), }),
collections: [Pages, Posts, Media, Categories, Users], collections: [Pages, Posts, Projects, Media, Categories, Users],
cors: [getServerSideURL()].filter(Boolean), cors: [getServerSideURL()].filter(Boolean),
globals: [Header, Footer], globals: [Header, Footer],
plugins: [ plugins: [
+2 -2
View File
@@ -25,7 +25,7 @@ const generateURL: GenerateURL<Post | Page> = ({ doc }) => {
export const plugins: Plugin[] = [ export const plugins: Plugin[] = [
redirectsPlugin({ redirectsPlugin({
collections: ['pages', 'posts'], collections: ['pages', 'posts', 'projects'],
overrides: { overrides: {
// @ts-expect-error - This is a valid override, mapped fields don't resolve to the same type // @ts-expect-error - This is a valid override, mapped fields don't resolve to the same type
fields: ({ defaultFields }) => { fields: ({ defaultFields }) => {
@@ -81,7 +81,7 @@ export const plugins: Plugin[] = [
}, },
}), }),
searchPlugin({ searchPlugin({
collections: ['posts'], collections: ['posts', 'projects'],
beforeSync: beforeSyncWithSearch, beforeSync: beforeSyncWithSearch,
searchOverrides: { searchOverrides: {
fields: ({ defaultFields }) => { fields: ({ defaultFields }) => {