From d6a61b84990a6ba7afa886c7819b1edc5026a7e8 Mon Sep 17 00:00:00 2001 From: Oscar Gimenez Date: Tue, 18 Nov 2025 08:05:01 +0100 Subject: [PATCH] feat: enhance OGB with components from Daemez MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/Footer/config.ts | 172 +++++++++- src/Header/config.ts | 154 ++++++++- src/collections/Categories.ts | 48 +++ src/collections/Projects.ts | 402 ++++++++++++++++++++++++ src/components/RowLabel.tsx | 11 + src/payload-types.ts | 571 ++++++++++++++++++++++++++++++---- src/payload.config.ts | 21 +- src/plugins/index.ts | 4 +- 8 files changed, 1285 insertions(+), 98 deletions(-) create mode 100644 src/collections/Projects.ts create mode 100644 src/components/RowLabel.tsx diff --git a/src/Footer/config.ts b/src/Footer/config.ts index ca9b54b..5f2ead1 100644 --- a/src/Footer/config.ts +++ b/src/Footer/config.ts @@ -10,20 +10,166 @@ export const Footer: GlobalConfig = { }, fields: [ { - name: 'navItems', - type: 'array', - fields: [ - link({ - appearances: false, - }), - ], - maxRows: 6, - admin: { - initCollapsed: true, - components: { - RowLabel: '@/Footer/RowLabel#RowLabel', + type: 'tabs', + tabs: [ + { + label: 'Navigation', + fields: [ + { + name: 'navItems', + type: 'array', + label: 'Footer Links', + fields: [ + link({ + appearances: false, + }), + ], + maxRows: 12, + admin: { + description: 'Main footer navigation links (Projects, Blog, About, Contact, Privacy, etc.)', + initCollapsed: true, + components: { + 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: { diff --git a/src/Header/config.ts b/src/Header/config.ts index 58fe89c..ecaa5a1 100644 --- a/src/Header/config.ts +++ b/src/Header/config.ts @@ -10,20 +10,148 @@ export const Header: GlobalConfig = { }, fields: [ { - name: 'navItems', - type: 'array', - fields: [ - link({ - appearances: false, - }), - ], - maxRows: 6, - admin: { - initCollapsed: true, - components: { - RowLabel: '@/Header/RowLabel#RowLabel', + type: 'tabs', + tabs: [ + { + label: 'Navigation', + fields: [ + { + name: 'navItems', + type: 'array', + label: 'Main Navigation', + fields: [ + link({ + appearances: false, + }), + ], + maxRows: 6, + admin: { + description: 'Main navigation menu items', + initCollapsed: true, + components: { + 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: { diff --git a/src/collections/Categories.ts b/src/collections/Categories.ts index cca3fc1..12d7cf1 100644 --- a/src/collections/Categories.ts +++ b/src/collections/Categories.ts @@ -14,15 +14,63 @@ export const Categories: CollectionConfig = { }, admin: { useAsTitle: 'title', + defaultColumns: ['title', 'type', 'icon', 'updatedAt'], }, fields: [ { name: 'title', type: 'text', required: true, + localized: true, + }, + { + name: 'description', + type: 'textarea', + localized: true, + admin: { + description: 'Category description for archive pages and SEO', + }, }, slugField({ 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)', + }, + }, ], } diff --git a/src/collections/Projects.ts b/src/collections/Projects.ts new file mode 100644 index 0000000..dbbf535 --- /dev/null +++ b/src/collections/Projects.ts @@ -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, + }, +} \ No newline at end of file diff --git a/src/components/RowLabel.tsx b/src/components/RowLabel.tsx new file mode 100644 index 0000000..3c788b4 --- /dev/null +++ b/src/components/RowLabel.tsx @@ -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')}` +} \ No newline at end of file diff --git a/src/payload-types.ts b/src/payload-types.ts index e34987e..2c49291 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -69,6 +69,7 @@ export interface Config { collections: { pages: Page; posts: Post; + projects: Project; media: Media; categories: Category; users: User; @@ -91,6 +92,7 @@ export interface Config { collectionsSelect: { pages: PagesSelect | PagesSelect; posts: PostsSelect | PostsSelect; + projects: ProjectsSelect | ProjectsSelect; media: MediaSelect | MediaSelect; categories: CategoriesSelect | CategoriesSelect; users: UsersSelect | UsersSelect; @@ -106,7 +108,7 @@ export interface Config { 'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect; }; db: { - defaultIDType: string; + defaultIDType: number; }; globals: { header: Header; @@ -116,7 +118,7 @@ export interface Config { header: HeaderSelect | HeaderSelect; footer: FooterSelect | FooterSelect; }; - locale: null; + locale: 'en' | 'es' | 'ca'; user: User & { collection: 'users'; }; @@ -154,7 +156,7 @@ export interface UserAuthOperations { * via the `definition` "pages". */ export interface Page { - id: string; + id: number; title: string; hero: { type: 'none' | 'highImpact' | 'mediumImpact' | 'lowImpact'; @@ -181,11 +183,11 @@ export interface Page { reference?: | ({ relationTo: 'pages'; - value: string | Page; + value: number | Page; } | null) | ({ relationTo: 'posts'; - value: string | Post; + value: number | Post; } | null); url?: string | null; label: string; @@ -197,7 +199,7 @@ export interface Page { id?: string | null; }[] | null; - media?: (string | null) | Media; + media?: (number | null) | Media; }; layout: (CallToActionBlock | ContentBlock | MediaBlock | ArchiveBlock | FormBlock)[]; meta?: { @@ -205,7 +207,7 @@ export interface Page { /** * Maximum upload file size: 12MB. Recommended file size for images is <500KB. */ - image?: (string | null) | Media; + image?: (number | null) | Media; description?: string | null; }; publishedAt?: string | null; @@ -223,9 +225,9 @@ export interface Page { * via the `definition` "posts". */ export interface Post { - id: string; + id: number; title: string; - heroImage?: (string | null) | Media; + heroImage?: (number | null) | Media; content: { root: { type: string; @@ -241,18 +243,18 @@ export interface Post { }; [k: string]: unknown; }; - relatedPosts?: (string | Post)[] | null; - categories?: (string | Category)[] | null; + relatedPosts?: (number | Post)[] | null; + categories?: (number | Category)[] | null; meta?: { title?: string | null; /** * Maximum upload file size: 12MB. Recommended file size for images is <500KB. */ - image?: (string | null) | Media; + image?: (number | null) | Media; description?: string | null; }; publishedAt?: string | null; - authors?: (string | User)[] | null; + authors?: (number | User)[] | null; populatedAuthors?: | { id?: string | null; @@ -273,7 +275,7 @@ export interface Post { * via the `definition` "media". */ export interface Media { - id: string; + id: number; alt?: string | null; caption?: { root: { @@ -290,7 +292,7 @@ export interface Media { }; [k: string]: unknown; } | null; - folder?: (string | null) | FolderInterface; + folder?: (number | null) | FolderInterface; updatedAt: string; createdAt: string; url?: string | null; @@ -366,18 +368,18 @@ export interface Media { * via the `definition` "payload-folders". */ export interface FolderInterface { - id: string; + id: number; name: string; - folder?: (string | null) | FolderInterface; + folder?: (number | null) | FolderInterface; documentsAndFolders?: { docs?: ( | { relationTo?: 'payload-folders'; - value: string | FolderInterface; + value: number | FolderInterface; } | { relationTo?: 'media'; - value: string | Media; + value: number | Media; } )[]; hasNextPage?: boolean; @@ -392,17 +394,37 @@ export interface FolderInterface { * via the `definition` "categories". */ export interface Category { - id: string; + id: number; 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. */ generateSlug?: boolean | null; 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?: | { - doc?: (string | null) | Category; + doc?: (number | null) | Category; url?: string | null; label?: string | null; id?: string | null; @@ -416,7 +438,7 @@ export interface Category { * via the `definition` "users". */ export interface User { - id: string; + id: number; name?: string | null; updatedAt: string; createdAt: string; @@ -464,11 +486,11 @@ export interface CallToActionBlock { reference?: | ({ relationTo: 'pages'; - value: string | Page; + value: number | Page; } | null) | ({ relationTo: 'posts'; - value: string | Post; + value: number | Post; } | null); url?: string | null; label: string; @@ -514,11 +536,11 @@ export interface ContentBlock { reference?: | ({ relationTo: 'pages'; - value: string | Page; + value: number | Page; } | null) | ({ relationTo: 'posts'; - value: string | Post; + value: number | Post; } | null); url?: string | null; label: string; @@ -539,7 +561,7 @@ export interface ContentBlock { * via the `definition` "MediaBlock". */ export interface MediaBlock { - media: string | Media; + media: number | Media; id?: string | null; blockName?: string | null; blockType: 'mediaBlock'; @@ -566,12 +588,12 @@ export interface ArchiveBlock { } | null; populateBy?: ('collection' | 'selection') | null; relationTo?: 'posts' | null; - categories?: (string | Category)[] | null; + categories?: (number | Category)[] | null; limit?: number | null; selectedDocs?: | { relationTo: 'posts'; - value: string | Post; + value: number | Post; }[] | null; id?: string | null; @@ -583,7 +605,7 @@ export interface ArchiveBlock { * via the `definition` "FormBlock". */ export interface FormBlock { - form: string | Form; + form: number | Form; enableIntro?: boolean | null; introContent?: { root: { @@ -609,7 +631,7 @@ export interface FormBlock { * via the `definition` "forms". */ export interface Form { - id: string; + id: number; title: string; fields?: | ( @@ -778,12 +800,195 @@ export interface Form { updatedAt: 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 * via the `definition` "redirects". */ export interface Redirect { - id: string; + id: number; /** * You will need to rebuild the website when changing this field. */ @@ -793,11 +998,15 @@ export interface Redirect { reference?: | ({ relationTo: 'pages'; - value: string | Page; + value: number | Page; } | null) | ({ relationTo: 'posts'; - value: string | Post; + value: number | Post; + } | null) + | ({ + relationTo: 'projects'; + value: number | Project; } | null); url?: string | null; }; @@ -809,8 +1018,8 @@ export interface Redirect { * via the `definition` "form-submissions". */ export interface FormSubmission { - id: string; - form: string | Form; + id: number; + form: number | Form; submissionData?: | { field: string; @@ -828,18 +1037,23 @@ export interface FormSubmission { * via the `definition` "search". */ export interface Search { - id: string; + id: number; title?: string | null; priority?: number | null; - doc: { - relationTo: 'posts'; - value: string | Post; - }; + doc: + | { + relationTo: 'posts'; + value: number | Post; + } + | { + relationTo: 'projects'; + value: number | Project; + }; slug?: string | null; meta?: { title?: string | null; description?: string | null; - image?: (string | null) | Media; + image?: (number | null) | Media; }; categories?: | { @@ -857,7 +1071,7 @@ export interface Search { * via the `definition` "payload-kv". */ export interface PayloadKv { - id: string; + id: number; key: string; data: | { @@ -874,7 +1088,7 @@ export interface PayloadKv { * via the `definition` "payload-jobs". */ export interface PayloadJob { - id: string; + id: number; /** * Input data provided to the job */ @@ -966,52 +1180,56 @@ export interface PayloadJob { * via the `definition` "payload-locked-documents". */ export interface PayloadLockedDocument { - id: string; + id: number; document?: | ({ relationTo: 'pages'; - value: string | Page; + value: number | Page; } | null) | ({ relationTo: 'posts'; - value: string | Post; + value: number | Post; + } | null) + | ({ + relationTo: 'projects'; + value: number | Project; } | null) | ({ relationTo: 'media'; - value: string | Media; + value: number | Media; } | null) | ({ relationTo: 'categories'; - value: string | Category; + value: number | Category; } | null) | ({ relationTo: 'users'; - value: string | User; + value: number | User; } | null) | ({ relationTo: 'redirects'; - value: string | Redirect; + value: number | Redirect; } | null) | ({ relationTo: 'forms'; - value: string | Form; + value: number | Form; } | null) | ({ relationTo: 'form-submissions'; - value: string | FormSubmission; + value: number | FormSubmission; } | null) | ({ relationTo: 'search'; - value: string | Search; + value: number | Search; } | null) | ({ relationTo: 'payload-folders'; - value: string | FolderInterface; + value: number | FolderInterface; } | null); globalSlug?: string | null; user: { relationTo: 'users'; - value: string | User; + value: number | User; }; updatedAt: string; createdAt: string; @@ -1021,10 +1239,10 @@ export interface PayloadLockedDocument { * via the `definition` "payload-preferences". */ export interface PayloadPreference { - id: string; + id: number; user: { relationTo: 'users'; - value: string | User; + value: number | User; }; key?: string | null; value?: @@ -1044,7 +1262,7 @@ export interface PayloadPreference { * via the `definition` "payload-migrations". */ export interface PayloadMigration { - id: string; + id: number; name?: string | null; batch?: number | null; updatedAt: string; @@ -1216,6 +1434,67 @@ export interface PostsSelect { createdAt?: T; _status?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "projects_select". + */ +export interface ProjectsSelect { + 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 * via the `definition` "media_select". @@ -1316,8 +1595,13 @@ export interface MediaSelect { */ export interface CategoriesSelect { title?: T; + description?: T; generateSlug?: T; slug?: T; + type?: T; + icon?: T; + color?: T; + order?: T; parent?: T; breadcrumbs?: | T @@ -1633,7 +1917,10 @@ export interface PayloadMigrationsSelect { * via the `definition` "header". */ export interface Header { - id: string; + id: number; + /** + * Main navigation menu items + */ navItems?: | { link: { @@ -1642,11 +1929,11 @@ export interface Header { reference?: | ({ relationTo: 'pages'; - value: string | Page; + value: number | Page; } | null) | ({ relationTo: 'posts'; - value: string | Post; + value: number | Post; } | null); url?: string | null; label: string; @@ -1654,6 +1941,50 @@ export interface Header { id?: string | 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; createdAt?: string | null; } @@ -1662,7 +1993,10 @@ export interface Header { * via the `definition` "footer". */ export interface Footer { - id: string; + id: number; + /** + * Main footer navigation links (Projects, Blog, About, Contact, Privacy, etc.) + */ navItems?: | { link: { @@ -1671,11 +2005,11 @@ export interface Footer { reference?: | ({ relationTo: 'pages'; - value: string | Page; + value: number | Page; } | null) | ({ relationTo: 'posts'; - value: string | Post; + value: number | Post; } | null); url?: string | null; label: string; @@ -1683,6 +2017,56 @@ export interface Footer { id?: string | 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; createdAt?: string | null; } @@ -1705,6 +2089,24 @@ export interface HeaderSelect { }; 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; createdAt?: T; globalType?: T; @@ -1728,6 +2130,33 @@ export interface FooterSelect { }; 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; createdAt?: T; globalType?: T; @@ -1743,14 +2172,18 @@ export interface TaskSchedulePublish { doc?: | ({ relationTo: 'pages'; - value: string | Page; + value: number | Page; } | null) | ({ relationTo: 'posts'; - value: string | Post; + value: number | Post; + } | null) + | ({ + relationTo: 'projects'; + value: number | Project; } | null); global?: string | null; - user?: (string | null) | User; + user?: (number | null) | User; }; output?: unknown; } diff --git a/src/payload.config.ts b/src/payload.config.ts index 1e9949c..a2d9ed8 100644 --- a/src/payload.config.ts +++ b/src/payload.config.ts @@ -10,6 +10,7 @@ import { Categories } from './collections/Categories' import { Media } from './collections/Media' import { Pages } from './collections/Pages' import { Posts } from './collections/Posts' +import { Projects } from './collections/Projects' import { Users } from './collections/Users' import { Footer } from './Footer/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 editor: defaultLexical, db: postgresAdapter({ @@ -64,7 +83,7 @@ export default buildConfig({ connectionString: process.env.DATABASE_URI || '', }, }), - collections: [Pages, Posts, Media, Categories, Users], + collections: [Pages, Posts, Projects, Media, Categories, Users], cors: [getServerSideURL()].filter(Boolean), globals: [Header, Footer], plugins: [ diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 23cb672..f210a28 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -25,7 +25,7 @@ const generateURL: GenerateURL = ({ doc }) => { export const plugins: Plugin[] = [ redirectsPlugin({ - collections: ['pages', 'posts'], + collections: ['pages', 'posts', 'projects'], overrides: { // @ts-expect-error - This is a valid override, mapped fields don't resolve to the same type fields: ({ defaultFields }) => { @@ -81,7 +81,7 @@ export const plugins: Plugin[] = [ }, }), searchPlugin({ - collections: ['posts'], + collections: ['posts', 'projects'], beforeSync: beforeSyncWithSearch, searchOverrides: { fields: ({ defaultFields }) => {