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:
+159
-13
@@ -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: {
|
||||
|
||||
+141
-13
@@ -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: {
|
||||
|
||||
@@ -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)',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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')}`
|
||||
}
|
||||
+502
-69
@@ -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<false> | PagesSelect<true>;
|
||||
posts: PostsSelect<false> | PostsSelect<true>;
|
||||
projects: ProjectsSelect<false> | ProjectsSelect<true>;
|
||||
media: MediaSelect<false> | MediaSelect<true>;
|
||||
categories: CategoriesSelect<false> | CategoriesSelect<true>;
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
@@ -106,7 +108,7 @@ export interface Config {
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: string;
|
||||
defaultIDType: number;
|
||||
};
|
||||
globals: {
|
||||
header: Header;
|
||||
@@ -116,7 +118,7 @@ export interface Config {
|
||||
header: HeaderSelect<false> | HeaderSelect<true>;
|
||||
footer: FooterSelect<false> | FooterSelect<true>;
|
||||
};
|
||||
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<T extends boolean = true> {
|
||||
createdAt?: 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
|
||||
* via the `definition` "media_select".
|
||||
@@ -1316,8 +1595,13 @@ export interface MediaSelect<T extends boolean = true> {
|
||||
*/
|
||||
export interface CategoriesSelect<T extends boolean = true> {
|
||||
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<T extends boolean = true> {
|
||||
* 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<T extends boolean = true> {
|
||||
};
|
||||
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<T extends boolean = true> {
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
+20
-1
@@ -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: [
|
||||
|
||||
@@ -25,7 +25,7 @@ const generateURL: GenerateURL<Post | Page> = ({ 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 }) => {
|
||||
|
||||
Reference in New Issue
Block a user