update admin dashboard & landing page editor
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -281,58 +281,77 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
|
|||||||
*/
|
*/
|
||||||
export interface LandingPage {
|
export interface LandingPage {
|
||||||
id: number;
|
id: number;
|
||||||
hero?: {
|
/**
|
||||||
badgeEmoji?: string | null;
|
* Add, remove, and reorder landing page sections while keeping live preview and autosave.
|
||||||
badgeText?: string | null;
|
*/
|
||||||
headingPrefix?: string | null;
|
layout?:
|
||||||
headingHighlight?: string | null;
|
| (
|
||||||
description?: string | null;
|
| {
|
||||||
primaryCta?: {
|
badgeEmoji?: string | null;
|
||||||
label?: string | null;
|
badgeText?: string | null;
|
||||||
url?: string | null;
|
headingPrefix?: string | null;
|
||||||
};
|
headingHighlight?: string | null;
|
||||||
highlights?:
|
description?: string | null;
|
||||||
| {
|
primaryCta?: {
|
||||||
label?: string | null;
|
label?: string | null;
|
||||||
id?: string | null;
|
url?: string | null;
|
||||||
}[]
|
};
|
||||||
| null;
|
highlights?:
|
||||||
};
|
| {
|
||||||
features?: {
|
label?: string | null;
|
||||||
heading?: string | null;
|
id?: string | null;
|
||||||
description?: string | null;
|
}[]
|
||||||
items?:
|
| null;
|
||||||
| {
|
id?: string | null;
|
||||||
icon?: string | null;
|
blockName?: string | null;
|
||||||
title?: string | null;
|
blockType: 'hero';
|
||||||
description?: string | null;
|
}
|
||||||
id?: string | null;
|
| {
|
||||||
}[]
|
heading?: string | null;
|
||||||
| null;
|
description?: string | null;
|
||||||
};
|
items?:
|
||||||
techStack?: {
|
| {
|
||||||
heading?: string | null;
|
icon?: string | null;
|
||||||
description?: string | null;
|
title?: string | null;
|
||||||
categories?:
|
description?: string | null;
|
||||||
| {
|
id?: string | null;
|
||||||
category?: string | null;
|
}[]
|
||||||
technologies?:
|
| null;
|
||||||
| {
|
id?: string | null;
|
||||||
name?: string | null;
|
blockName?: string | null;
|
||||||
description?: string | null;
|
blockType: 'features';
|
||||||
id?: string | null;
|
}
|
||||||
}[]
|
| {
|
||||||
| null;
|
heading?: string | null;
|
||||||
id?: string | null;
|
description?: string | null;
|
||||||
}[]
|
categories?:
|
||||||
| null;
|
| {
|
||||||
};
|
category?: string | null;
|
||||||
cta?: {
|
technologies?:
|
||||||
heading?: string | null;
|
| {
|
||||||
description?: string | null;
|
name?: string | null;
|
||||||
commandLabel?: string | null;
|
description?: string | null;
|
||||||
command?: string | null;
|
id?: string | null;
|
||||||
};
|
}[]
|
||||||
|
| null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'techStack';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
heading?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
commandLabel?: string | null;
|
||||||
|
command?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'cta';
|
||||||
|
}
|
||||||
|
)[]
|
||||||
|
| null;
|
||||||
_status?: ('draft' | 'published') | null;
|
_status?: ('draft' | 'published') | null;
|
||||||
updatedAt?: string | null;
|
updatedAt?: string | null;
|
||||||
createdAt?: string | null;
|
createdAt?: string | null;
|
||||||
@@ -342,67 +361,79 @@ export interface LandingPage {
|
|||||||
* via the `definition` "landing-page_select".
|
* via the `definition` "landing-page_select".
|
||||||
*/
|
*/
|
||||||
export interface LandingPageSelect<T extends boolean = true> {
|
export interface LandingPageSelect<T extends boolean = true> {
|
||||||
hero?:
|
layout?:
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
badgeEmoji?: T;
|
hero?:
|
||||||
badgeText?: T;
|
|
||||||
headingPrefix?: T;
|
|
||||||
headingHighlight?: T;
|
|
||||||
description?: T;
|
|
||||||
primaryCta?:
|
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
label?: T;
|
badgeEmoji?: T;
|
||||||
url?: T;
|
badgeText?: T;
|
||||||
};
|
headingPrefix?: T;
|
||||||
highlights?:
|
headingHighlight?: T;
|
||||||
| T
|
|
||||||
| {
|
|
||||||
label?: T;
|
|
||||||
id?: T;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
features?:
|
|
||||||
| T
|
|
||||||
| {
|
|
||||||
heading?: T;
|
|
||||||
description?: T;
|
|
||||||
items?:
|
|
||||||
| T
|
|
||||||
| {
|
|
||||||
icon?: T;
|
|
||||||
title?: T;
|
|
||||||
description?: T;
|
description?: T;
|
||||||
id?: T;
|
primaryCta?:
|
||||||
};
|
|
||||||
};
|
|
||||||
techStack?:
|
|
||||||
| T
|
|
||||||
| {
|
|
||||||
heading?: T;
|
|
||||||
description?: T;
|
|
||||||
categories?:
|
|
||||||
| T
|
|
||||||
| {
|
|
||||||
category?: T;
|
|
||||||
technologies?:
|
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
name?: T;
|
label?: T;
|
||||||
|
url?: T;
|
||||||
|
};
|
||||||
|
highlights?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
label?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
};
|
||||||
|
features?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
heading?: T;
|
||||||
|
description?: T;
|
||||||
|
items?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
icon?: T;
|
||||||
|
title?: T;
|
||||||
description?: T;
|
description?: T;
|
||||||
id?: T;
|
id?: T;
|
||||||
};
|
};
|
||||||
id?: T;
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
};
|
||||||
|
techStack?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
heading?: T;
|
||||||
|
description?: T;
|
||||||
|
categories?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
category?: T;
|
||||||
|
technologies?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
name?: T;
|
||||||
|
description?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
};
|
||||||
|
cta?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
heading?: T;
|
||||||
|
description?: T;
|
||||||
|
commandLabel?: T;
|
||||||
|
command?: T;
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
};
|
};
|
||||||
};
|
|
||||||
cta?:
|
|
||||||
| T
|
|
||||||
| {
|
|
||||||
heading?: T;
|
|
||||||
description?: T;
|
|
||||||
commandLabel?: T;
|
|
||||||
command?: T;
|
|
||||||
};
|
};
|
||||||
_status?: T;
|
_status?: T;
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CTA, Features, Hero, TechStack } from '@/components/landing';
|
import { LandingPageBuilder } from '@/components/landing';
|
||||||
import { RefreshRouteOnSave } from '@/components/payload/refresh-route-on-save';
|
import { RefreshRouteOnSave } from '@/components/payload/refresh-route-on-save';
|
||||||
import { getLandingPageContent } from '@/lib/payload/get-landing-page-content';
|
import { getLandingPageContent } from '@/lib/payload/get-landing-page-content';
|
||||||
|
|
||||||
@@ -19,10 +19,7 @@ const Home = async ({ searchParams }: HomeProps) => {
|
|||||||
return (
|
return (
|
||||||
<main className='flex min-h-screen flex-col'>
|
<main className='flex min-h-screen flex-col'>
|
||||||
{isPreview ? <RefreshRouteOnSave /> : null}
|
{isPreview ? <RefreshRouteOnSave /> : null}
|
||||||
<Hero content={content.hero} />
|
<LandingPageBuilder blocks={content.layout} />
|
||||||
<Features content={content.features} />
|
|
||||||
<TechStack content={content.techStack} />
|
|
||||||
<CTA content={content.cta} />
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -46,150 +46,231 @@ export interface LandingCtaContent {
|
|||||||
command: string;
|
command: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LandingPageContent {
|
export interface LandingBlockMetadata {
|
||||||
hero: LandingHeroContent;
|
id?: string;
|
||||||
features: LandingFeaturesContent;
|
blockName?: string;
|
||||||
techStack: LandingTechStackContent;
|
|
||||||
cta: LandingCtaContent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultLandingPageContent: LandingPageContent = {
|
export interface LandingHeroBlock
|
||||||
hero: {
|
extends LandingHeroContent, LandingBlockMetadata {
|
||||||
badgeEmoji: '🚀',
|
blockType: 'hero';
|
||||||
badgeText: 'Production-ready monorepo template',
|
}
|
||||||
headingPrefix: 'Build Full-Stack Apps with',
|
|
||||||
headingHighlight: 'convex-monorepo',
|
export interface LandingFeaturesBlock
|
||||||
description:
|
extends LandingFeaturesContent, LandingBlockMetadata {
|
||||||
'A Turborepo starter with Next.js, Expo, and self-hosted Convex. Ship web and mobile apps faster with shared code, type-safe backend, and complete control over your infrastructure.',
|
blockType: 'features';
|
||||||
primaryCta: {
|
}
|
||||||
label: 'View Source Code',
|
|
||||||
url: 'https://git.gbrown.org/gib/convex-monorepo',
|
export interface LandingTechStackBlock
|
||||||
|
extends LandingTechStackContent, LandingBlockMetadata {
|
||||||
|
blockType: 'techStack';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LandingCtaBlock
|
||||||
|
extends LandingCtaContent, LandingBlockMetadata {
|
||||||
|
blockType: 'cta';
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LandingPageBlock =
|
||||||
|
| LandingHeroBlock
|
||||||
|
| LandingFeaturesBlock
|
||||||
|
| LandingTechStackBlock
|
||||||
|
| LandingCtaBlock;
|
||||||
|
|
||||||
|
export interface LandingPageContent {
|
||||||
|
layout: LandingPageBlock[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PayloadLandingHeroRow {
|
||||||
|
blockType: 'hero';
|
||||||
|
badgeEmoji: string;
|
||||||
|
badgeText: string;
|
||||||
|
headingPrefix: string;
|
||||||
|
headingHighlight: string;
|
||||||
|
description: string;
|
||||||
|
primaryCta: {
|
||||||
|
label: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
highlights: Array<{
|
||||||
|
label: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PayloadLandingFeaturesRow {
|
||||||
|
blockType: 'features';
|
||||||
|
heading: string;
|
||||||
|
description: string;
|
||||||
|
items: LandingFeatureItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PayloadLandingTechStackRow {
|
||||||
|
blockType: 'techStack';
|
||||||
|
heading: string;
|
||||||
|
description: string;
|
||||||
|
categories: LandingTechCategory[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PayloadLandingCtaRow {
|
||||||
|
blockType: 'cta';
|
||||||
|
heading: string;
|
||||||
|
description: string;
|
||||||
|
commandLabel: string;
|
||||||
|
command: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PayloadLandingPageRow =
|
||||||
|
| PayloadLandingHeroRow
|
||||||
|
| PayloadLandingFeaturesRow
|
||||||
|
| PayloadLandingTechStackRow
|
||||||
|
| PayloadLandingCtaRow;
|
||||||
|
|
||||||
|
export const defaultLandingHeroContent: LandingHeroContent = {
|
||||||
|
badgeEmoji: '🚀',
|
||||||
|
badgeText: 'Production-ready monorepo template',
|
||||||
|
headingPrefix: 'Build Full-Stack Apps with',
|
||||||
|
headingHighlight: 'convex-monorepo',
|
||||||
|
description:
|
||||||
|
'A Turborepo starter with Next.js, Expo, and self-hosted Convex. Ship web and mobile apps faster with shared code, type-safe backend, and complete control over your infrastructure.',
|
||||||
|
primaryCta: {
|
||||||
|
label: 'View Source Code',
|
||||||
|
url: 'https://git.gbrown.org/gib/convex-monorepo',
|
||||||
|
},
|
||||||
|
highlights: ['TypeScript', 'Self-Hosted', 'Real-time', 'Auth Included'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultLandingFeaturesContent: LandingFeaturesContent = {
|
||||||
|
heading: 'Everything You Need to Ship Fast',
|
||||||
|
description:
|
||||||
|
'A complete monorepo template with all the tools and patterns you need for production-ready applications.',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: 'Turborepo',
|
||||||
|
description:
|
||||||
|
'Efficient build system with intelligent caching. Share code between web and mobile apps seamlessly.',
|
||||||
|
icon: '⚡',
|
||||||
},
|
},
|
||||||
highlights: ['TypeScript', 'Self-Hosted', 'Real-time', 'Auth Included'],
|
{
|
||||||
},
|
title: 'Self-Hosted Convex',
|
||||||
features: {
|
description:
|
||||||
heading: 'Everything You Need to Ship Fast',
|
'Complete control over your data with self-hosted Convex backend. No vendor lock-in, deploy anywhere.',
|
||||||
description:
|
icon: '🏠',
|
||||||
'A complete monorepo template with all the tools and patterns you need for production-ready applications.',
|
},
|
||||||
items: [
|
{
|
||||||
{
|
title: 'Next.js 16 + Expo',
|
||||||
title: 'Turborepo',
|
description:
|
||||||
description:
|
'Modern Next.js 16 with App Router for web, Expo 54 for mobile. One codebase, multiple platforms.',
|
||||||
'Efficient build system with intelligent caching. Share code between web and mobile apps seamlessly.',
|
icon: '📱',
|
||||||
icon: '⚡',
|
},
|
||||||
},
|
{
|
||||||
{
|
title: 'Type-Safe Backend',
|
||||||
title: 'Self-Hosted Convex',
|
description:
|
||||||
description:
|
'Fully type-safe queries and mutations with Convex. Auto-generated TypeScript types for the entire API.',
|
||||||
'Complete control over your data with self-hosted Convex backend. No vendor lock-in, deploy anywhere.',
|
icon: '🔒',
|
||||||
icon: '🏠',
|
},
|
||||||
},
|
{
|
||||||
{
|
title: 'Authentication Included',
|
||||||
title: 'Next.js 16 + Expo',
|
description:
|
||||||
description:
|
'OAuth with Authentik + custom password auth with email verification. Production-ready auth out of the box.',
|
||||||
'Modern Next.js 16 with App Router for web, Expo 54 for mobile. One codebase, multiple platforms.',
|
icon: '🔐',
|
||||||
icon: '📱',
|
},
|
||||||
},
|
{
|
||||||
{
|
title: 'Real-time Updates',
|
||||||
title: 'Type-Safe Backend',
|
description:
|
||||||
description:
|
'Built-in real-time subscriptions with Convex reactive queries. No WebSocket configuration needed.',
|
||||||
'Fully type-safe queries and mutations with Convex. Auto-generated TypeScript types for the entire API.',
|
icon: '⚡',
|
||||||
icon: '🔒',
|
},
|
||||||
},
|
{
|
||||||
{
|
title: 'shadcn/ui Components',
|
||||||
title: 'Authentication Included',
|
description:
|
||||||
description:
|
'Beautiful, accessible components from shadcn/ui. Customizable with Tailwind CSS v4.',
|
||||||
'OAuth with Authentik + custom password auth with email verification. Production-ready auth out of the box.',
|
icon: '🎨',
|
||||||
icon: '🔐',
|
},
|
||||||
},
|
{
|
||||||
{
|
title: 'Docker Ready',
|
||||||
title: 'Real-time Updates',
|
description:
|
||||||
description:
|
'Production Docker setup included. Deploy to any server with docker-compose up.',
|
||||||
'Built-in real-time subscriptions with Convex reactive queries. No WebSocket configuration needed.',
|
icon: '🐳',
|
||||||
icon: '⚡',
|
},
|
||||||
},
|
{
|
||||||
{
|
title: 'Developer Experience',
|
||||||
title: 'shadcn/ui Components',
|
description:
|
||||||
description:
|
'Hot reload, TypeScript strict mode, ESLint, Prettier, and Bun for blazing fast installs.',
|
||||||
'Beautiful, accessible components from shadcn/ui. Customizable with Tailwind CSS v4.',
|
icon: '⚙️',
|
||||||
icon: '🎨',
|
},
|
||||||
},
|
],
|
||||||
{
|
};
|
||||||
title: 'Docker Ready',
|
|
||||||
description:
|
export const defaultLandingTechStackContent: LandingTechStackContent = {
|
||||||
'Production Docker setup included. Deploy to any server with docker-compose up.',
|
heading: 'Modern Tech Stack',
|
||||||
icon: '🐳',
|
description:
|
||||||
},
|
'Built with the latest and greatest tools for maximum productivity and performance.',
|
||||||
{
|
categories: [
|
||||||
title: 'Developer Experience',
|
{
|
||||||
description:
|
category: 'Frontend',
|
||||||
'Hot reload, TypeScript strict mode, ESLint, Prettier, and Bun for blazing fast installs.',
|
technologies: [
|
||||||
icon: '⚙️',
|
{
|
||||||
},
|
name: 'Next.js 16',
|
||||||
],
|
description: 'React framework with App Router',
|
||||||
},
|
},
|
||||||
techStack: {
|
{ name: 'Expo 54', description: 'React Native framework' },
|
||||||
heading: 'Modern Tech Stack',
|
{
|
||||||
description:
|
name: 'React 19',
|
||||||
'Built with the latest and greatest tools for maximum productivity and performance.',
|
description: 'Latest React with Server Components',
|
||||||
categories: [
|
},
|
||||||
{
|
{
|
||||||
category: 'Frontend',
|
name: 'Tailwind CSS v4',
|
||||||
technologies: [
|
description: 'Utility-first CSS framework',
|
||||||
{
|
},
|
||||||
name: 'Next.js 16',
|
{ name: 'shadcn/ui', description: 'Beautiful component library' },
|
||||||
description: 'React framework with App Router',
|
],
|
||||||
},
|
},
|
||||||
{ name: 'Expo 54', description: 'React Native framework' },
|
{
|
||||||
{
|
category: 'Backend',
|
||||||
name: 'React 19',
|
technologies: [
|
||||||
description: 'Latest React with Server Components',
|
{ name: 'Convex', description: 'Self-hosted reactive backend' },
|
||||||
},
|
{
|
||||||
{
|
name: '@convex-dev/auth',
|
||||||
name: 'Tailwind CSS v4',
|
description: 'Multi-provider authentication',
|
||||||
description: 'Utility-first CSS framework',
|
},
|
||||||
},
|
{ name: 'UseSend', description: 'Self-hosted email service' },
|
||||||
{ name: 'shadcn/ui', description: 'Beautiful component library' },
|
{ name: 'File Storage', description: 'Built-in file uploads' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: 'Backend',
|
category: 'Developer Tools',
|
||||||
technologies: [
|
technologies: [
|
||||||
{ name: 'Convex', description: 'Self-hosted reactive backend' },
|
{ name: 'Turborepo', description: 'High-performance build system' },
|
||||||
{
|
{ name: 'TypeScript', description: 'Type-safe development' },
|
||||||
name: '@convex-dev/auth',
|
{ name: 'Bun', description: 'Fast package manager & runtime' },
|
||||||
description: 'Multi-provider authentication',
|
{ name: 'ESLint + Prettier', description: 'Code quality tools' },
|
||||||
},
|
{ name: 'Docker', description: 'Containerized deployment' },
|
||||||
{ name: 'UseSend', description: 'Self-hosted email service' },
|
],
|
||||||
{ name: 'File Storage', description: 'Built-in file uploads' },
|
},
|
||||||
],
|
],
|
||||||
},
|
};
|
||||||
{
|
|
||||||
category: 'Developer Tools',
|
export const defaultLandingCtaContent: LandingCtaContent = {
|
||||||
technologies: [
|
heading: 'Ready to Build Something Amazing?',
|
||||||
{ name: 'Turborepo', description: 'High-performance build system' },
|
description:
|
||||||
{ name: 'TypeScript', description: 'Type-safe development' },
|
'Clone the repository and start building your next project with everything pre-configured.',
|
||||||
{ name: 'Bun', description: 'Fast package manager & runtime' },
|
commandLabel: 'Quick Start',
|
||||||
{ name: 'ESLint + Prettier', description: 'Code quality tools' },
|
command:
|
||||||
{ name: 'Docker', description: 'Containerized deployment' },
|
'git clone https://git.gbrown.org/gib/convex-monorepo.git\ncd convex-monorepo\nbun i',
|
||||||
],
|
};
|
||||||
},
|
|
||||||
],
|
const isRecord = (value: unknown): value is Record<string, unknown> => {
|
||||||
},
|
return typeof value === 'object' && value !== null;
|
||||||
cta: {
|
|
||||||
heading: 'Ready to Build Something Amazing?',
|
|
||||||
description:
|
|
||||||
'Clone the repository and start building your next project with everything pre-configured.',
|
|
||||||
commandLabel: 'Quick Start',
|
|
||||||
command:
|
|
||||||
'git clone https://git.gbrown.org/gib/convex-monorepo.git\ncd convex-monorepo\nbun i',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanitizeString = (value: unknown, fallback: string) => {
|
const sanitizeString = (value: unknown, fallback: string) => {
|
||||||
return typeof value === 'string' && value.length > 0 ? value : fallback;
|
return typeof value === 'string' && value.length > 0 ? value : fallback;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sanitizeOptionalString = (value: unknown) => {
|
||||||
|
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
const sanitizeStringArray = (value: unknown, fallback: string[]) => {
|
const sanitizeStringArray = (value: unknown, fallback: string[]) => {
|
||||||
if (!Array.isArray(value)) return fallback;
|
if (!Array.isArray(value)) return fallback;
|
||||||
|
|
||||||
@@ -197,9 +278,7 @@ const sanitizeStringArray = (value: unknown, fallback: string[]) => {
|
|||||||
.map((item) => {
|
.map((item) => {
|
||||||
if (typeof item === 'string' && item.length > 0) return item;
|
if (typeof item === 'string' && item.length > 0) return item;
|
||||||
if (
|
if (
|
||||||
typeof item === 'object' &&
|
isRecord(item) &&
|
||||||
item !== null &&
|
|
||||||
'label' in item &&
|
|
||||||
typeof item.label === 'string' &&
|
typeof item.label === 'string' &&
|
||||||
item.label.length > 0
|
item.label.length > 0
|
||||||
) {
|
) {
|
||||||
@@ -213,154 +292,283 @@ const sanitizeStringArray = (value: unknown, fallback: string[]) => {
|
|||||||
return items.length > 0 ? items : fallback;
|
return items.length > 0 ? items : fallback;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mergeLandingPageContent = (
|
const sanitizeBlockMetadata = (value: unknown): LandingBlockMetadata => {
|
||||||
value: Partial<LandingPageContent> | null | undefined,
|
if (!isRecord(value)) return {};
|
||||||
): LandingPageContent => {
|
|
||||||
const defaultFeatureFallback = {
|
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
icon: '',
|
|
||||||
};
|
|
||||||
const defaultTechCategoryFallback = {
|
|
||||||
category: '',
|
|
||||||
technologies: [{ name: '', description: '' }],
|
|
||||||
};
|
|
||||||
const defaultTechItemFallback = { name: '', description: '' };
|
|
||||||
const heroValue = value?.hero;
|
|
||||||
const featuresValue = value?.features;
|
|
||||||
const techStackValue = value?.techStack;
|
|
||||||
const ctaValue = value?.cta;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hero: {
|
id: sanitizeOptionalString(value.id),
|
||||||
badgeEmoji: sanitizeString(
|
blockName: sanitizeOptionalString(value.blockName),
|
||||||
heroValue?.badgeEmoji,
|
};
|
||||||
defaultLandingPageContent.hero.badgeEmoji,
|
};
|
||||||
),
|
|
||||||
badgeText: sanitizeString(
|
const sanitizeFeatureItems = (
|
||||||
heroValue?.badgeText,
|
value: unknown,
|
||||||
defaultLandingPageContent.hero.badgeText,
|
fallback: LandingFeatureItem[],
|
||||||
),
|
): LandingFeatureItem[] => {
|
||||||
headingPrefix: sanitizeString(
|
if (!Array.isArray(value)) return fallback;
|
||||||
heroValue?.headingPrefix,
|
|
||||||
defaultLandingPageContent.hero.headingPrefix,
|
const items = value
|
||||||
),
|
.map((item, index) => {
|
||||||
headingHighlight: sanitizeString(
|
if (!isRecord(item)) return null;
|
||||||
heroValue?.headingHighlight,
|
|
||||||
defaultLandingPageContent.hero.headingHighlight,
|
const fallbackItem = fallback[index] ?? {
|
||||||
),
|
title: '',
|
||||||
description: sanitizeString(
|
description: '',
|
||||||
heroValue?.description,
|
icon: '',
|
||||||
defaultLandingPageContent.hero.description,
|
};
|
||||||
),
|
|
||||||
primaryCta: {
|
return {
|
||||||
label: sanitizeString(
|
title: sanitizeString(item.title, fallbackItem.title),
|
||||||
heroValue?.primaryCta?.label,
|
description: sanitizeString(item.description, fallbackItem.description),
|
||||||
defaultLandingPageContent.hero.primaryCta.label,
|
icon: sanitizeString(item.icon, fallbackItem.icon),
|
||||||
),
|
};
|
||||||
url: sanitizeString(
|
})
|
||||||
heroValue?.primaryCta?.url,
|
.filter((item): item is LandingFeatureItem => item !== null);
|
||||||
defaultLandingPageContent.hero.primaryCta.url,
|
|
||||||
),
|
return items.length > 0 ? items : fallback;
|
||||||
},
|
};
|
||||||
highlights: sanitizeStringArray(
|
|
||||||
heroValue?.highlights,
|
const sanitizeTechCategories = (
|
||||||
defaultLandingPageContent.hero.highlights,
|
value: unknown,
|
||||||
),
|
fallback: LandingTechCategory[],
|
||||||
},
|
): LandingTechCategory[] => {
|
||||||
features: {
|
if (!Array.isArray(value)) return fallback;
|
||||||
heading: sanitizeString(
|
|
||||||
featuresValue?.heading,
|
const categories = value
|
||||||
defaultLandingPageContent.features.heading,
|
.map((category, categoryIndex) => {
|
||||||
),
|
if (!isRecord(category)) return null;
|
||||||
description: sanitizeString(
|
|
||||||
featuresValue?.description,
|
const fallbackCategory = fallback[categoryIndex] ?? {
|
||||||
defaultLandingPageContent.features.description,
|
category: '',
|
||||||
),
|
technologies: [{ name: '', description: '' }],
|
||||||
items:
|
};
|
||||||
Array.isArray(featuresValue?.items) && featuresValue.items.length > 0
|
|
||||||
? featuresValue.items.map((item, index) => ({
|
const technologies = Array.isArray(category.technologies)
|
||||||
title: sanitizeString(
|
? category.technologies
|
||||||
item?.title,
|
.map((technology, technologyIndex) => {
|
||||||
defaultLandingPageContent.features.items[index]?.title ??
|
if (!isRecord(technology)) return null;
|
||||||
defaultFeatureFallback.title,
|
|
||||||
),
|
const fallbackTechnology = fallbackCategory.technologies[
|
||||||
description: sanitizeString(
|
technologyIndex
|
||||||
item?.description,
|
] ?? {
|
||||||
defaultLandingPageContent.features.items[index]?.description ??
|
name: '',
|
||||||
defaultFeatureFallback.description,
|
description: '',
|
||||||
),
|
};
|
||||||
icon: sanitizeString(
|
|
||||||
item?.icon,
|
return {
|
||||||
defaultLandingPageContent.features.items[index]?.icon ??
|
name: sanitizeString(technology.name, fallbackTechnology.name),
|
||||||
defaultFeatureFallback.icon,
|
description: sanitizeString(
|
||||||
),
|
technology.description,
|
||||||
}))
|
fallbackTechnology.description,
|
||||||
: defaultLandingPageContent.features.items,
|
),
|
||||||
},
|
};
|
||||||
techStack: {
|
})
|
||||||
heading: sanitizeString(
|
.filter((item): item is LandingTechItem => item !== null)
|
||||||
techStackValue?.heading,
|
: fallbackCategory.technologies;
|
||||||
defaultLandingPageContent.techStack.heading,
|
|
||||||
),
|
return {
|
||||||
description: sanitizeString(
|
category: sanitizeString(category.category, fallbackCategory.category),
|
||||||
techStackValue?.description,
|
technologies:
|
||||||
defaultLandingPageContent.techStack.description,
|
technologies.length > 0
|
||||||
),
|
? technologies
|
||||||
categories:
|
: fallbackCategory.technologies,
|
||||||
Array.isArray(techStackValue?.categories) &&
|
};
|
||||||
techStackValue.categories.length > 0
|
})
|
||||||
? techStackValue.categories.map((category, categoryIndex) => ({
|
.filter((item): item is LandingTechCategory => item !== null);
|
||||||
category: sanitizeString(
|
|
||||||
category?.category,
|
return categories.length > 0 ? categories : fallback;
|
||||||
defaultLandingPageContent.techStack.categories[categoryIndex]
|
};
|
||||||
?.category ?? defaultTechCategoryFallback.category,
|
|
||||||
),
|
const sanitizeHeroBlock = (value: unknown): LandingHeroBlock => {
|
||||||
technologies:
|
const metadata = sanitizeBlockMetadata(value);
|
||||||
Array.isArray(category?.technologies) &&
|
const source = isRecord(value) ? value : {};
|
||||||
category.technologies.length > 0
|
|
||||||
? category.technologies.map(
|
return {
|
||||||
(technology, technologyIndex) => ({
|
blockType: 'hero',
|
||||||
name: sanitizeString(
|
...metadata,
|
||||||
technology?.name,
|
badgeEmoji: sanitizeString(
|
||||||
defaultLandingPageContent.techStack.categories[
|
source.badgeEmoji,
|
||||||
categoryIndex
|
defaultLandingHeroContent.badgeEmoji,
|
||||||
]?.technologies[technologyIndex]?.name ??
|
),
|
||||||
defaultTechItemFallback.name,
|
badgeText: sanitizeString(
|
||||||
),
|
source.badgeText,
|
||||||
description: sanitizeString(
|
defaultLandingHeroContent.badgeText,
|
||||||
technology?.description,
|
),
|
||||||
defaultLandingPageContent.techStack.categories[
|
headingPrefix: sanitizeString(
|
||||||
categoryIndex
|
source.headingPrefix,
|
||||||
]?.technologies[technologyIndex]?.description ??
|
defaultLandingHeroContent.headingPrefix,
|
||||||
defaultTechItemFallback.description,
|
),
|
||||||
),
|
headingHighlight: sanitizeString(
|
||||||
}),
|
source.headingHighlight,
|
||||||
)
|
defaultLandingHeroContent.headingHighlight,
|
||||||
: (defaultLandingPageContent.techStack.categories[
|
),
|
||||||
categoryIndex
|
description: sanitizeString(
|
||||||
]?.technologies ??
|
source.description,
|
||||||
defaultTechCategoryFallback.technologies),
|
defaultLandingHeroContent.description,
|
||||||
}))
|
),
|
||||||
: defaultLandingPageContent.techStack.categories,
|
primaryCta: {
|
||||||
},
|
label: sanitizeString(
|
||||||
cta: {
|
isRecord(source.primaryCta) ? source.primaryCta.label : undefined,
|
||||||
heading: sanitizeString(
|
defaultLandingHeroContent.primaryCta.label,
|
||||||
ctaValue?.heading,
|
),
|
||||||
defaultLandingPageContent.cta.heading,
|
url: sanitizeString(
|
||||||
),
|
isRecord(source.primaryCta) ? source.primaryCta.url : undefined,
|
||||||
description: sanitizeString(
|
defaultLandingHeroContent.primaryCta.url,
|
||||||
ctaValue?.description,
|
),
|
||||||
defaultLandingPageContent.cta.description,
|
},
|
||||||
),
|
highlights: sanitizeStringArray(
|
||||||
commandLabel: sanitizeString(
|
source.highlights,
|
||||||
ctaValue?.commandLabel,
|
defaultLandingHeroContent.highlights,
|
||||||
defaultLandingPageContent.cta.commandLabel,
|
),
|
||||||
),
|
};
|
||||||
command: sanitizeString(
|
};
|
||||||
ctaValue?.command,
|
|
||||||
defaultLandingPageContent.cta.command,
|
const sanitizeFeaturesBlock = (value: unknown): LandingFeaturesBlock => {
|
||||||
),
|
const metadata = sanitizeBlockMetadata(value);
|
||||||
},
|
const source = isRecord(value) ? value : {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
blockType: 'features',
|
||||||
|
...metadata,
|
||||||
|
heading: sanitizeString(
|
||||||
|
source.heading,
|
||||||
|
defaultLandingFeaturesContent.heading,
|
||||||
|
),
|
||||||
|
description: sanitizeString(
|
||||||
|
source.description,
|
||||||
|
defaultLandingFeaturesContent.description,
|
||||||
|
),
|
||||||
|
items: sanitizeFeatureItems(
|
||||||
|
source.items,
|
||||||
|
defaultLandingFeaturesContent.items,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const sanitizeTechStackBlock = (value: unknown): LandingTechStackBlock => {
|
||||||
|
const metadata = sanitizeBlockMetadata(value);
|
||||||
|
const source = isRecord(value) ? value : {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
blockType: 'techStack',
|
||||||
|
...metadata,
|
||||||
|
heading: sanitizeString(
|
||||||
|
source.heading,
|
||||||
|
defaultLandingTechStackContent.heading,
|
||||||
|
),
|
||||||
|
description: sanitizeString(
|
||||||
|
source.description,
|
||||||
|
defaultLandingTechStackContent.description,
|
||||||
|
),
|
||||||
|
categories: sanitizeTechCategories(
|
||||||
|
source.categories,
|
||||||
|
defaultLandingTechStackContent.categories,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const sanitizeCtaBlock = (value: unknown): LandingCtaBlock => {
|
||||||
|
const metadata = sanitizeBlockMetadata(value);
|
||||||
|
const source = isRecord(value) ? value : {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
blockType: 'cta',
|
||||||
|
...metadata,
|
||||||
|
heading: sanitizeString(source.heading, defaultLandingCtaContent.heading),
|
||||||
|
description: sanitizeString(
|
||||||
|
source.description,
|
||||||
|
defaultLandingCtaContent.description,
|
||||||
|
),
|
||||||
|
commandLabel: sanitizeString(
|
||||||
|
source.commandLabel,
|
||||||
|
defaultLandingCtaContent.commandLabel,
|
||||||
|
),
|
||||||
|
command: sanitizeString(source.command, defaultLandingCtaContent.command),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const sanitizeLayout = (value: unknown): LandingPageBlock[] => {
|
||||||
|
if (!Array.isArray(value)) return [];
|
||||||
|
|
||||||
|
return value
|
||||||
|
.map((block) => {
|
||||||
|
if (!isRecord(block) || typeof block.blockType !== 'string') return null;
|
||||||
|
|
||||||
|
switch (block.blockType) {
|
||||||
|
case 'hero': {
|
||||||
|
return sanitizeHeroBlock(block);
|
||||||
|
}
|
||||||
|
case 'features': {
|
||||||
|
return sanitizeFeaturesBlock(block);
|
||||||
|
}
|
||||||
|
case 'techStack': {
|
||||||
|
return sanitizeTechStackBlock(block);
|
||||||
|
}
|
||||||
|
case 'cta': {
|
||||||
|
return sanitizeCtaBlock(block);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((block): block is LandingPageBlock => block !== null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildLegacyLandingPageLayout = (value: unknown): LandingPageBlock[] => {
|
||||||
|
const source = isRecord(value) ? value : {};
|
||||||
|
|
||||||
|
return [
|
||||||
|
sanitizeHeroBlock(source.hero),
|
||||||
|
sanitizeFeaturesBlock(source.features),
|
||||||
|
sanitizeTechStackBlock(source.techStack),
|
||||||
|
sanitizeCtaBlock(source.cta),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createDefaultLandingPageLayout = (): LandingPageBlock[] => {
|
||||||
|
return buildLegacyLandingPageLayout({
|
||||||
|
hero: defaultLandingHeroContent,
|
||||||
|
features: defaultLandingFeaturesContent,
|
||||||
|
techStack: defaultLandingTechStackContent,
|
||||||
|
cta: defaultLandingCtaContent,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createDefaultLandingPageLayoutForPayload =
|
||||||
|
(): PayloadLandingPageRow[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
blockType: 'hero',
|
||||||
|
...defaultLandingHeroContent,
|
||||||
|
highlights: defaultLandingHeroContent.highlights.map((label) => ({
|
||||||
|
label,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'features',
|
||||||
|
...defaultLandingFeaturesContent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'techStack',
|
||||||
|
...defaultLandingTechStackContent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'cta',
|
||||||
|
...defaultLandingCtaContent,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultLandingPageContent: LandingPageContent = {
|
||||||
|
layout: createDefaultLandingPageLayout(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mergeLandingPageContent = (value: unknown): LandingPageContent => {
|
||||||
|
const source = isRecord(value) ? value : {};
|
||||||
|
const layout = sanitizeLayout(source.layout);
|
||||||
|
|
||||||
|
return {
|
||||||
|
layout: layout.length > 0 ? layout : buildLegacyLandingPageLayout(source),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ export { Hero } from './hero';
|
|||||||
export { Features } from './features';
|
export { Features } from './features';
|
||||||
export { TechStack } from './tech-stack';
|
export { TechStack } from './tech-stack';
|
||||||
export { CTA } from './cta';
|
export { CTA } from './cta';
|
||||||
|
export { LandingPageBuilder } from './page-builder';
|
||||||
|
|||||||
33
apps/next/src/components/landing/page-builder.tsx
Normal file
33
apps/next/src/components/landing/page-builder.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import type { LandingPageBlock } from './content';
|
||||||
|
import { CTA } from './cta';
|
||||||
|
import { Features } from './features';
|
||||||
|
import { Hero } from './hero';
|
||||||
|
import { TechStack } from './tech-stack';
|
||||||
|
|
||||||
|
interface LandingPageBuilderProps {
|
||||||
|
blocks: LandingPageBlock[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LandingPageBuilder = ({ blocks }: LandingPageBuilderProps) => {
|
||||||
|
return blocks.map((block, index) => {
|
||||||
|
const key = block.id ?? `${block.blockType}-${index}`;
|
||||||
|
|
||||||
|
switch (block.blockType) {
|
||||||
|
case 'hero': {
|
||||||
|
return <Hero key={key} content={block} />;
|
||||||
|
}
|
||||||
|
case 'features': {
|
||||||
|
return <Features key={key} content={block} />;
|
||||||
|
}
|
||||||
|
case 'techStack': {
|
||||||
|
return <TechStack key={key} content={block} />;
|
||||||
|
}
|
||||||
|
case 'cta': {
|
||||||
|
return <CTA key={key} content={block} />;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
import type { LandingPageContent } from '@/components/landing/content';
|
import type { LandingPageContent } from '@/components/landing/content';
|
||||||
import { cache } from 'react';
|
import { cache } from 'react';
|
||||||
import {
|
import { mergeLandingPageContent } from '@/components/landing/content';
|
||||||
defaultLandingPageContent,
|
|
||||||
mergeLandingPageContent,
|
|
||||||
} from '@/components/landing/content';
|
|
||||||
|
|
||||||
import { getPayloadClient } from './get-payload';
|
import { getPayloadClient } from './get-payload';
|
||||||
|
|
||||||
@@ -19,9 +16,6 @@ export const getLandingPageContent = cache(
|
|||||||
}
|
}
|
||||||
).findGlobal({ slug: 'landing-page', draft: isPreview });
|
).findGlobal({ slug: 'landing-page', draft: isPreview });
|
||||||
|
|
||||||
return mergeLandingPageContent(
|
return mergeLandingPageContent(landingPage as Partial<LandingPageContent>);
|
||||||
(landingPage as Partial<LandingPageContent> | null | undefined) ??
|
|
||||||
defaultLandingPageContent,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
1783
apps/next/src/payload-generated-schema.ts
Normal file
1783
apps/next/src/payload-generated-schema.ts
Normal file
File diff suppressed because it is too large
Load Diff
186
apps/next/src/payload/globals/landing-page-blocks.ts
Normal file
186
apps/next/src/payload/globals/landing-page-blocks.ts
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import type { Block } from 'payload';
|
||||||
|
|
||||||
|
import {
|
||||||
|
defaultLandingCtaContent,
|
||||||
|
defaultLandingFeaturesContent,
|
||||||
|
defaultLandingHeroContent,
|
||||||
|
defaultLandingTechStackContent,
|
||||||
|
} from '../../components/landing/content';
|
||||||
|
|
||||||
|
export const landingPageBlocks: Block[] = [
|
||||||
|
{
|
||||||
|
slug: 'hero',
|
||||||
|
labels: {
|
||||||
|
singular: 'Hero Section',
|
||||||
|
plural: 'Hero Sections',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'badgeEmoji',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: defaultLandingHeroContent.badgeEmoji,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'badgeText',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: defaultLandingHeroContent.badgeText,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'headingPrefix',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: defaultLandingHeroContent.headingPrefix,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'headingHighlight',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: defaultLandingHeroContent.headingHighlight,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
type: 'textarea',
|
||||||
|
defaultValue: defaultLandingHeroContent.description,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'primaryCta',
|
||||||
|
label: 'Primary CTA',
|
||||||
|
type: 'group',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'label',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: defaultLandingHeroContent.primaryCta.label,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'url',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: defaultLandingHeroContent.primaryCta.url,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'highlights',
|
||||||
|
type: 'array',
|
||||||
|
defaultValue: defaultLandingHeroContent.highlights.map((label) => ({
|
||||||
|
label,
|
||||||
|
})),
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'label',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'features',
|
||||||
|
labels: {
|
||||||
|
singular: 'Features Section',
|
||||||
|
plural: 'Features Sections',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'heading',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: defaultLandingFeaturesContent.heading,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
type: 'textarea',
|
||||||
|
defaultValue: defaultLandingFeaturesContent.description,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'items',
|
||||||
|
type: 'array',
|
||||||
|
defaultValue: defaultLandingFeaturesContent.items,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'icon',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
type: 'textarea',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'techStack',
|
||||||
|
labels: {
|
||||||
|
singular: 'Tech Stack Section',
|
||||||
|
plural: 'Tech Stack Sections',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'heading',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: defaultLandingTechStackContent.heading,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
type: 'textarea',
|
||||||
|
defaultValue: defaultLandingTechStackContent.description,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'categories',
|
||||||
|
type: 'array',
|
||||||
|
defaultValue: defaultLandingTechStackContent.categories,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'category',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'technologies',
|
||||||
|
type: 'array',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'cta',
|
||||||
|
labels: {
|
||||||
|
singular: 'CTA Section',
|
||||||
|
plural: 'CTA Sections',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'heading',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: defaultLandingCtaContent.heading,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
type: 'textarea',
|
||||||
|
defaultValue: defaultLandingCtaContent.description,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'commandLabel',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: defaultLandingCtaContent.commandLabel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'command',
|
||||||
|
type: 'textarea',
|
||||||
|
defaultValue: defaultLandingCtaContent.command,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { GlobalConfig } from 'payload';
|
import type { GlobalConfig } from 'payload';
|
||||||
|
|
||||||
import { defaultLandingPageContent } from '../../components/landing/content';
|
import { createDefaultLandingPageLayoutForPayload } from '../../components/landing/content';
|
||||||
|
import { landingPageBlocks } from './landing-page-blocks';
|
||||||
|
|
||||||
export const LandingPage: GlobalConfig = {
|
export const LandingPage: GlobalConfig = {
|
||||||
slug: 'landing-page',
|
slug: 'landing-page',
|
||||||
@@ -42,194 +43,17 @@ export const LandingPage: GlobalConfig = {
|
|||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
type: 'tabs',
|
name: 'layout',
|
||||||
tabs: [
|
label: 'Layout Builder',
|
||||||
{
|
type: 'blocks',
|
||||||
label: 'Hero',
|
minRows: 1,
|
||||||
fields: [
|
defaultValue: () => createDefaultLandingPageLayoutForPayload(),
|
||||||
{
|
blocks: landingPageBlocks,
|
||||||
name: 'hero',
|
admin: {
|
||||||
type: 'group',
|
description:
|
||||||
fields: [
|
'Add, remove, and reorder landing page sections while keeping live preview and autosave.',
|
||||||
{
|
initCollapsed: true,
|
||||||
name: 'badgeEmoji',
|
},
|
||||||
type: 'text',
|
|
||||||
defaultValue: defaultLandingPageContent.hero.badgeEmoji,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'badgeText',
|
|
||||||
type: 'text',
|
|
||||||
defaultValue: defaultLandingPageContent.hero.badgeText,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'headingPrefix',
|
|
||||||
type: 'text',
|
|
||||||
defaultValue: defaultLandingPageContent.hero.headingPrefix,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'headingHighlight',
|
|
||||||
type: 'text',
|
|
||||||
defaultValue: defaultLandingPageContent.hero.headingHighlight,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'description',
|
|
||||||
type: 'textarea',
|
|
||||||
defaultValue: defaultLandingPageContent.hero.description,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'primaryCta',
|
|
||||||
label: 'Primary CTA',
|
|
||||||
type: 'group',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'label',
|
|
||||||
type: 'text',
|
|
||||||
defaultValue:
|
|
||||||
defaultLandingPageContent.hero.primaryCta.label,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'url',
|
|
||||||
type: 'text',
|
|
||||||
defaultValue:
|
|
||||||
defaultLandingPageContent.hero.primaryCta.url,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'highlights',
|
|
||||||
type: 'array',
|
|
||||||
defaultValue: defaultLandingPageContent.hero.highlights.map(
|
|
||||||
(label) => ({ label }),
|
|
||||||
),
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'label',
|
|
||||||
type: 'text',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Features',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'features',
|
|
||||||
type: 'group',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'heading',
|
|
||||||
type: 'text',
|
|
||||||
defaultValue: defaultLandingPageContent.features.heading,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'description',
|
|
||||||
type: 'textarea',
|
|
||||||
defaultValue: defaultLandingPageContent.features.description,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'items',
|
|
||||||
type: 'array',
|
|
||||||
defaultValue: defaultLandingPageContent.features.items,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'icon',
|
|
||||||
type: 'text',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'title',
|
|
||||||
type: 'text',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'description',
|
|
||||||
type: 'textarea',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Tech Stack',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'techStack',
|
|
||||||
type: 'group',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'heading',
|
|
||||||
type: 'text',
|
|
||||||
defaultValue: defaultLandingPageContent.techStack.heading,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'description',
|
|
||||||
type: 'textarea',
|
|
||||||
defaultValue: defaultLandingPageContent.techStack.description,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'categories',
|
|
||||||
type: 'array',
|
|
||||||
defaultValue: defaultLandingPageContent.techStack.categories,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'category',
|
|
||||||
type: 'text',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'technologies',
|
|
||||||
type: 'array',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
type: 'text',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'description',
|
|
||||||
type: 'text',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'CTA',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'cta',
|
|
||||||
type: 'group',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'heading',
|
|
||||||
type: 'text',
|
|
||||||
defaultValue: defaultLandingPageContent.cta.heading,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'description',
|
|
||||||
type: 'textarea',
|
|
||||||
defaultValue: defaultLandingPageContent.cta.description,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'commandLabel',
|
|
||||||
type: 'text',
|
|
||||||
defaultValue: defaultLandingPageContent.cta.commandLabel,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'command',
|
|
||||||
type: 'textarea',
|
|
||||||
defaultValue: defaultLandingPageContent.cta.command,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user