Update to use payload for landing page
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -67,16 +67,16 @@ export interface Config {
|
|||||||
};
|
};
|
||||||
blocks: {};
|
blocks: {};
|
||||||
collections: {
|
collections: {
|
||||||
'payload-kv': PayloadKv;
|
|
||||||
users: User;
|
users: User;
|
||||||
|
'payload-kv': PayloadKv;
|
||||||
'payload-locked-documents': PayloadLockedDocument;
|
'payload-locked-documents': PayloadLockedDocument;
|
||||||
'payload-preferences': PayloadPreference;
|
'payload-preferences': PayloadPreference;
|
||||||
'payload-migrations': PayloadMigration;
|
'payload-migrations': PayloadMigration;
|
||||||
};
|
};
|
||||||
collectionsJoins: {};
|
collectionsJoins: {};
|
||||||
collectionsSelect: {
|
collectionsSelect: {
|
||||||
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
|
|
||||||
users: UsersSelect<false> | UsersSelect<true>;
|
users: UsersSelect<false> | UsersSelect<true>;
|
||||||
|
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
|
||||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||||
@@ -85,8 +85,12 @@ export interface Config {
|
|||||||
defaultIDType: number;
|
defaultIDType: number;
|
||||||
};
|
};
|
||||||
fallbackLocale: null;
|
fallbackLocale: null;
|
||||||
globals: {};
|
globals: {
|
||||||
globalsSelect: {};
|
'landing-page': LandingPage;
|
||||||
|
};
|
||||||
|
globalsSelect: {
|
||||||
|
'landing-page': LandingPageSelect<false> | LandingPageSelect<true>;
|
||||||
|
};
|
||||||
locale: null;
|
locale: null;
|
||||||
widgets: {
|
widgets: {
|
||||||
collections: CollectionsWidget;
|
collections: CollectionsWidget;
|
||||||
@@ -115,23 +119,6 @@ export interface UserAuthOperations {
|
|||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
|
||||||
* via the `definition` "payload-kv".
|
|
||||||
*/
|
|
||||||
export interface PayloadKv {
|
|
||||||
id: number;
|
|
||||||
key: string;
|
|
||||||
data:
|
|
||||||
| {
|
|
||||||
[k: string]: unknown;
|
|
||||||
}
|
|
||||||
| unknown[]
|
|
||||||
| string
|
|
||||||
| number
|
|
||||||
| boolean
|
|
||||||
| null;
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "users".
|
* via the `definition` "users".
|
||||||
@@ -157,6 +144,23 @@ export interface User {
|
|||||||
password?: string | null;
|
password?: string | null;
|
||||||
collection: 'users';
|
collection: 'users';
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-kv".
|
||||||
|
*/
|
||||||
|
export interface PayloadKv {
|
||||||
|
id: number;
|
||||||
|
key: string;
|
||||||
|
data:
|
||||||
|
| {
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
| unknown[]
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "payload-locked-documents".
|
* via the `definition` "payload-locked-documents".
|
||||||
@@ -209,14 +213,6 @@ export interface PayloadMigration {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
|
||||||
* via the `definition` "payload-kv_select".
|
|
||||||
*/
|
|
||||||
export interface PayloadKvSelect<T extends boolean = true> {
|
|
||||||
key?: T;
|
|
||||||
data?: T;
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "users_select".
|
* via the `definition` "users_select".
|
||||||
@@ -239,6 +235,14 @@ export interface UsersSelect<T extends boolean = true> {
|
|||||||
expiresAt?: T;
|
expiresAt?: T;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-kv_select".
|
||||||
|
*/
|
||||||
|
export interface PayloadKvSelect<T extends boolean = true> {
|
||||||
|
key?: T;
|
||||||
|
data?: T;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "payload-locked-documents_select".
|
* via the `definition` "payload-locked-documents_select".
|
||||||
@@ -271,6 +275,138 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
|
|||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "landing-page".
|
||||||
|
*/
|
||||||
|
export interface LandingPage {
|
||||||
|
id: number;
|
||||||
|
hero?: {
|
||||||
|
badgeEmoji?: string | null;
|
||||||
|
badgeText?: string | null;
|
||||||
|
headingPrefix?: string | null;
|
||||||
|
headingHighlight?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
primaryCta?: {
|
||||||
|
label?: string | null;
|
||||||
|
url?: string | null;
|
||||||
|
};
|
||||||
|
highlights?:
|
||||||
|
| {
|
||||||
|
label?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
};
|
||||||
|
features?: {
|
||||||
|
heading?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
items?:
|
||||||
|
| {
|
||||||
|
icon?: string | null;
|
||||||
|
title?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
};
|
||||||
|
techStack?: {
|
||||||
|
heading?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
categories?:
|
||||||
|
| {
|
||||||
|
category?: string | null;
|
||||||
|
technologies?:
|
||||||
|
| {
|
||||||
|
name?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
};
|
||||||
|
cta?: {
|
||||||
|
heading?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
commandLabel?: string | null;
|
||||||
|
command?: string | null;
|
||||||
|
};
|
||||||
|
updatedAt?: string | null;
|
||||||
|
createdAt?: string | null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "landing-page_select".
|
||||||
|
*/
|
||||||
|
export interface LandingPageSelect<T extends boolean = true> {
|
||||||
|
hero?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
badgeEmoji?: T;
|
||||||
|
badgeText?: T;
|
||||||
|
headingPrefix?: T;
|
||||||
|
headingHighlight?: T;
|
||||||
|
description?: T;
|
||||||
|
primaryCta?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
label?: T;
|
||||||
|
url?: T;
|
||||||
|
};
|
||||||
|
highlights?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
label?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
features?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
heading?: T;
|
||||||
|
description?: T;
|
||||||
|
items?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
icon?: T;
|
||||||
|
title?: T;
|
||||||
|
description?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
techStack?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
heading?: T;
|
||||||
|
description?: T;
|
||||||
|
categories?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
category?: T;
|
||||||
|
technologies?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
name?: T;
|
||||||
|
description?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
cta?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
heading?: T;
|
||||||
|
description?: T;
|
||||||
|
commandLabel?: T;
|
||||||
|
command?: T;
|
||||||
|
};
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
globalType?: T;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "collections_widget".
|
* via the `definition` "collections_widget".
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import { CTA, Features, Hero, TechStack } from '@/components/landing';
|
import { CTA, Features, Hero, TechStack } from '@/components/landing';
|
||||||
|
import { getLandingPageContent } from '@/lib/payload/get-landing-page-content';
|
||||||
|
|
||||||
|
const Home = async () => {
|
||||||
|
const content = await getLandingPageContent();
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
return (
|
||||||
<main className='flex min-h-screen flex-col'>
|
<main className='flex min-h-screen flex-col'>
|
||||||
<Hero />
|
<Hero content={content.hero} />
|
||||||
<Features />
|
<Features content={content.features} />
|
||||||
<TechStack />
|
<TechStack content={content.techStack} />
|
||||||
<CTA />
|
<CTA content={content.cta} />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default Home;
|
||||||
|
|||||||
366
apps/next/src/components/landing/content.ts
Normal file
366
apps/next/src/components/landing/content.ts
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
export interface LandingHeroContent {
|
||||||
|
badgeEmoji: string;
|
||||||
|
badgeText: string;
|
||||||
|
headingPrefix: string;
|
||||||
|
headingHighlight: string;
|
||||||
|
description: string;
|
||||||
|
primaryCta: {
|
||||||
|
label: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
highlights: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LandingFeatureItem {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LandingFeaturesContent {
|
||||||
|
heading: string;
|
||||||
|
description: string;
|
||||||
|
items: LandingFeatureItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LandingTechItem {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LandingTechCategory {
|
||||||
|
category: string;
|
||||||
|
technologies: LandingTechItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LandingTechStackContent {
|
||||||
|
heading: string;
|
||||||
|
description: string;
|
||||||
|
categories: LandingTechCategory[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LandingCtaContent {
|
||||||
|
heading: string;
|
||||||
|
description: string;
|
||||||
|
commandLabel: string;
|
||||||
|
command: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LandingPageContent {
|
||||||
|
hero: LandingHeroContent;
|
||||||
|
features: LandingFeaturesContent;
|
||||||
|
techStack: LandingTechStackContent;
|
||||||
|
cta: LandingCtaContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultLandingPageContent: LandingPageContent = {
|
||||||
|
hero: {
|
||||||
|
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'],
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
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: '⚡',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Self-Hosted Convex',
|
||||||
|
description:
|
||||||
|
'Complete control over your data with self-hosted Convex backend. No vendor lock-in, deploy anywhere.',
|
||||||
|
icon: '🏠',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Next.js 16 + Expo',
|
||||||
|
description:
|
||||||
|
'Modern Next.js 16 with App Router for web, Expo 54 for mobile. One codebase, multiple platforms.',
|
||||||
|
icon: '📱',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Type-Safe Backend',
|
||||||
|
description:
|
||||||
|
'Fully type-safe queries and mutations with Convex. Auto-generated TypeScript types for the entire API.',
|
||||||
|
icon: '🔒',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Authentication Included',
|
||||||
|
description:
|
||||||
|
'OAuth with Authentik + custom password auth with email verification. Production-ready auth out of the box.',
|
||||||
|
icon: '🔐',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Real-time Updates',
|
||||||
|
description:
|
||||||
|
'Built-in real-time subscriptions with Convex reactive queries. No WebSocket configuration needed.',
|
||||||
|
icon: '⚡',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'shadcn/ui Components',
|
||||||
|
description:
|
||||||
|
'Beautiful, accessible components from shadcn/ui. Customizable with Tailwind CSS v4.',
|
||||||
|
icon: '🎨',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Docker Ready',
|
||||||
|
description:
|
||||||
|
'Production Docker setup included. Deploy to any server with docker-compose up.',
|
||||||
|
icon: '🐳',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Developer Experience',
|
||||||
|
description:
|
||||||
|
'Hot reload, TypeScript strict mode, ESLint, Prettier, and Bun for blazing fast installs.',
|
||||||
|
icon: '⚙️',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
techStack: {
|
||||||
|
heading: 'Modern Tech Stack',
|
||||||
|
description:
|
||||||
|
'Built with the latest and greatest tools for maximum productivity and performance.',
|
||||||
|
categories: [
|
||||||
|
{
|
||||||
|
category: 'Frontend',
|
||||||
|
technologies: [
|
||||||
|
{
|
||||||
|
name: 'Next.js 16',
|
||||||
|
description: 'React framework with App Router',
|
||||||
|
},
|
||||||
|
{ name: 'Expo 54', description: 'React Native framework' },
|
||||||
|
{
|
||||||
|
name: 'React 19',
|
||||||
|
description: 'Latest React with Server Components',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Tailwind CSS v4',
|
||||||
|
description: 'Utility-first CSS framework',
|
||||||
|
},
|
||||||
|
{ name: 'shadcn/ui', description: 'Beautiful component library' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'Backend',
|
||||||
|
technologies: [
|
||||||
|
{ name: 'Convex', description: 'Self-hosted reactive backend' },
|
||||||
|
{
|
||||||
|
name: '@convex-dev/auth',
|
||||||
|
description: 'Multi-provider authentication',
|
||||||
|
},
|
||||||
|
{ name: 'UseSend', description: 'Self-hosted email service' },
|
||||||
|
{ name: 'File Storage', description: 'Built-in file uploads' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'Developer Tools',
|
||||||
|
technologies: [
|
||||||
|
{ name: 'Turborepo', description: 'High-performance build system' },
|
||||||
|
{ name: 'TypeScript', description: 'Type-safe development' },
|
||||||
|
{ name: 'Bun', description: 'Fast package manager & runtime' },
|
||||||
|
{ name: 'ESLint + Prettier', description: 'Code quality tools' },
|
||||||
|
{ name: 'Docker', description: 'Containerized deployment' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
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) => {
|
||||||
|
return typeof value === 'string' && value.length > 0 ? value : fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sanitizeStringArray = (value: unknown, fallback: string[]) => {
|
||||||
|
if (!Array.isArray(value)) return fallback;
|
||||||
|
|
||||||
|
const items = value
|
||||||
|
.map((item) => {
|
||||||
|
if (typeof item === 'string' && item.length > 0) return item;
|
||||||
|
if (
|
||||||
|
typeof item === 'object' &&
|
||||||
|
item !== null &&
|
||||||
|
'label' in item &&
|
||||||
|
typeof item.label === 'string' &&
|
||||||
|
item.label.length > 0
|
||||||
|
) {
|
||||||
|
return item.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter((item): item is string => item !== null);
|
||||||
|
|
||||||
|
return items.length > 0 ? items : fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mergeLandingPageContent = (
|
||||||
|
value: Partial<LandingPageContent> | null | undefined,
|
||||||
|
): 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 {
|
||||||
|
hero: {
|
||||||
|
badgeEmoji: sanitizeString(
|
||||||
|
heroValue?.badgeEmoji,
|
||||||
|
defaultLandingPageContent.hero.badgeEmoji,
|
||||||
|
),
|
||||||
|
badgeText: sanitizeString(
|
||||||
|
heroValue?.badgeText,
|
||||||
|
defaultLandingPageContent.hero.badgeText,
|
||||||
|
),
|
||||||
|
headingPrefix: sanitizeString(
|
||||||
|
heroValue?.headingPrefix,
|
||||||
|
defaultLandingPageContent.hero.headingPrefix,
|
||||||
|
),
|
||||||
|
headingHighlight: sanitizeString(
|
||||||
|
heroValue?.headingHighlight,
|
||||||
|
defaultLandingPageContent.hero.headingHighlight,
|
||||||
|
),
|
||||||
|
description: sanitizeString(
|
||||||
|
heroValue?.description,
|
||||||
|
defaultLandingPageContent.hero.description,
|
||||||
|
),
|
||||||
|
primaryCta: {
|
||||||
|
label: sanitizeString(
|
||||||
|
heroValue?.primaryCta?.label,
|
||||||
|
defaultLandingPageContent.hero.primaryCta.label,
|
||||||
|
),
|
||||||
|
url: sanitizeString(
|
||||||
|
heroValue?.primaryCta?.url,
|
||||||
|
defaultLandingPageContent.hero.primaryCta.url,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
highlights: sanitizeStringArray(
|
||||||
|
heroValue?.highlights,
|
||||||
|
defaultLandingPageContent.hero.highlights,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
heading: sanitizeString(
|
||||||
|
featuresValue?.heading,
|
||||||
|
defaultLandingPageContent.features.heading,
|
||||||
|
),
|
||||||
|
description: sanitizeString(
|
||||||
|
featuresValue?.description,
|
||||||
|
defaultLandingPageContent.features.description,
|
||||||
|
),
|
||||||
|
items:
|
||||||
|
Array.isArray(featuresValue?.items) && featuresValue.items.length > 0
|
||||||
|
? featuresValue.items.map((item, index) => ({
|
||||||
|
title: sanitizeString(
|
||||||
|
item?.title,
|
||||||
|
defaultLandingPageContent.features.items[index]?.title ??
|
||||||
|
defaultFeatureFallback.title,
|
||||||
|
),
|
||||||
|
description: sanitizeString(
|
||||||
|
item?.description,
|
||||||
|
defaultLandingPageContent.features.items[index]?.description ??
|
||||||
|
defaultFeatureFallback.description,
|
||||||
|
),
|
||||||
|
icon: sanitizeString(
|
||||||
|
item?.icon,
|
||||||
|
defaultLandingPageContent.features.items[index]?.icon ??
|
||||||
|
defaultFeatureFallback.icon,
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
: defaultLandingPageContent.features.items,
|
||||||
|
},
|
||||||
|
techStack: {
|
||||||
|
heading: sanitizeString(
|
||||||
|
techStackValue?.heading,
|
||||||
|
defaultLandingPageContent.techStack.heading,
|
||||||
|
),
|
||||||
|
description: sanitizeString(
|
||||||
|
techStackValue?.description,
|
||||||
|
defaultLandingPageContent.techStack.description,
|
||||||
|
),
|
||||||
|
categories:
|
||||||
|
Array.isArray(techStackValue?.categories) &&
|
||||||
|
techStackValue.categories.length > 0
|
||||||
|
? techStackValue.categories.map((category, categoryIndex) => ({
|
||||||
|
category: sanitizeString(
|
||||||
|
category?.category,
|
||||||
|
defaultLandingPageContent.techStack.categories[categoryIndex]
|
||||||
|
?.category ?? defaultTechCategoryFallback.category,
|
||||||
|
),
|
||||||
|
technologies:
|
||||||
|
Array.isArray(category?.technologies) &&
|
||||||
|
category.technologies.length > 0
|
||||||
|
? category.technologies.map(
|
||||||
|
(technology, technologyIndex) => ({
|
||||||
|
name: sanitizeString(
|
||||||
|
technology?.name,
|
||||||
|
defaultLandingPageContent.techStack.categories[
|
||||||
|
categoryIndex
|
||||||
|
]?.technologies[technologyIndex]?.name ??
|
||||||
|
defaultTechItemFallback.name,
|
||||||
|
),
|
||||||
|
description: sanitizeString(
|
||||||
|
technology?.description,
|
||||||
|
defaultLandingPageContent.techStack.categories[
|
||||||
|
categoryIndex
|
||||||
|
]?.technologies[technologyIndex]?.description ??
|
||||||
|
defaultTechItemFallback.description,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
: (defaultLandingPageContent.techStack.categories[
|
||||||
|
categoryIndex
|
||||||
|
]?.technologies ??
|
||||||
|
defaultTechCategoryFallback.technologies),
|
||||||
|
}))
|
||||||
|
: defaultLandingPageContent.techStack.categories,
|
||||||
|
},
|
||||||
|
cta: {
|
||||||
|
heading: sanitizeString(
|
||||||
|
ctaValue?.heading,
|
||||||
|
defaultLandingPageContent.cta.heading,
|
||||||
|
),
|
||||||
|
description: sanitizeString(
|
||||||
|
ctaValue?.description,
|
||||||
|
defaultLandingPageContent.cta.description,
|
||||||
|
),
|
||||||
|
commandLabel: sanitizeString(
|
||||||
|
ctaValue?.commandLabel,
|
||||||
|
defaultLandingPageContent.cta.commandLabel,
|
||||||
|
),
|
||||||
|
command: sanitizeString(
|
||||||
|
ctaValue?.command,
|
||||||
|
defaultLandingPageContent.cta.command,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,27 +1,27 @@
|
|||||||
export const CTA = () => (
|
import type { LandingCtaContent } from './content';
|
||||||
|
|
||||||
|
interface CTAProps {
|
||||||
|
content: LandingCtaContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CTA = ({ content }: CTAProps) => (
|
||||||
<section className='container mx-auto px-4 py-24'>
|
<section className='container mx-auto px-4 py-24'>
|
||||||
<div className='mx-auto max-w-4xl'>
|
<div className='mx-auto max-w-4xl'>
|
||||||
<div className='border-border/40 from-muted/50 to-muted/30 rounded-2xl border bg-linear-to-br p-8 text-center md:p-12'>
|
<div className='border-border/40 from-muted/50 to-muted/30 rounded-2xl border bg-linear-to-br p-8 text-center md:p-12'>
|
||||||
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>
|
||||||
Ready to Build Something Amazing?
|
{content.heading}
|
||||||
</h2>
|
</h2>
|
||||||
<p className='text-muted-foreground mb-8 text-lg'>
|
<p className='text-muted-foreground mb-8 text-lg'>
|
||||||
Clone the repository and start building your next project with
|
{content.description}
|
||||||
everything pre-configured.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Quick Start Command */}
|
|
||||||
<div className='mt-12'>
|
<div className='mt-12'>
|
||||||
<p className='text-muted-foreground mb-3 text-sm font-medium'>
|
<p className='text-muted-foreground mb-3 text-sm font-medium'>
|
||||||
Quick Start
|
{content.commandLabel}
|
||||||
</p>
|
</p>
|
||||||
<div className='border-border/40 bg-background mx-auto max-w-2xl rounded-lg border p-4'>
|
<div className='border-border/40 bg-background mx-auto max-w-2xl rounded-lg border p-4'>
|
||||||
<code className='text-sm'>
|
<code className='block text-left text-sm whitespace-pre-wrap'>
|
||||||
git clone https://git.gbrown.org/gib/convex-monorepo.git
|
{content.command}
|
||||||
<br />
|
|
||||||
cd convex-monorepo
|
|
||||||
<br />
|
|
||||||
bun i
|
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,80 +1,29 @@
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from '@gib/ui';
|
import { Card, CardContent, CardHeader, CardTitle } from '@gib/ui';
|
||||||
|
|
||||||
const features = [
|
import type { LandingFeaturesContent } from './content';
|
||||||
{
|
|
||||||
title: 'Turborepo',
|
|
||||||
description:
|
|
||||||
'Efficient build system with intelligent caching. Share code between web and mobile apps seamlessly.',
|
|
||||||
icon: '⚡',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Self-Hosted Convex',
|
|
||||||
description:
|
|
||||||
'Complete control over your data with self-hosted Convex backend. No vendor lock-in, deploy anywhere.',
|
|
||||||
icon: '🏠',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Next.js 16 + Expo',
|
|
||||||
description:
|
|
||||||
'Modern Next.js 16 with App Router for web, Expo 54 for mobile. One codebase, multiple platforms.',
|
|
||||||
icon: '📱',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Type-Safe Backend',
|
|
||||||
description:
|
|
||||||
'Fully type-safe queries and mutations with Convex. Auto-generated TypeScript types for the entire API.',
|
|
||||||
icon: '🔒',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Authentication Included',
|
|
||||||
description:
|
|
||||||
'OAuth with Authentik + custom password auth with email verification. Production-ready auth out of the box.',
|
|
||||||
icon: '🔐',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Real-time Updates',
|
|
||||||
description:
|
|
||||||
'Built-in real-time subscriptions with Convex reactive queries. No WebSocket configuration needed.',
|
|
||||||
icon: '⚡',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'shadcn/ui Components',
|
|
||||||
description:
|
|
||||||
'Beautiful, accessible components from shadcn/ui. Customizable with Tailwind CSS v4.',
|
|
||||||
icon: '🎨',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Docker Ready',
|
|
||||||
description:
|
|
||||||
'Production Docker setup included. Deploy to any server with docker-compose up.',
|
|
||||||
icon: '🐳',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Developer Experience',
|
|
||||||
description:
|
|
||||||
'Hot reload, TypeScript strict mode, ESLint, Prettier, and Bun for blazing fast installs.',
|
|
||||||
icon: '⚙️',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const Features = () => (
|
interface FeaturesProps {
|
||||||
|
content: LandingFeaturesContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Features = ({ content }: FeaturesProps) => (
|
||||||
<section id='features' className='container mx-auto px-4 py-24'>
|
<section id='features' className='container mx-auto px-4 py-24'>
|
||||||
<div className='mx-auto max-w-6xl'>
|
<div className='mx-auto max-w-6xl'>
|
||||||
{/* Section Header */}
|
|
||||||
<div className='mb-16 text-center'>
|
<div className='mb-16 text-center'>
|
||||||
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl'>
|
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl'>
|
||||||
Everything You Need to Ship Fast
|
{content.heading}
|
||||||
</h2>
|
</h2>
|
||||||
<p className='text-muted-foreground mx-auto max-w-2xl text-lg'>
|
<p className='text-muted-foreground mx-auto max-w-2xl text-lg'>
|
||||||
A complete monorepo template with all the tools and patterns you need
|
{content.description}
|
||||||
for production-ready applications.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Features Grid */}
|
|
||||||
<div className='grid gap-6 md:grid-cols-2 lg:grid-cols-3'>
|
<div className='grid gap-6 md:grid-cols-2 lg:grid-cols-3'>
|
||||||
{features.map((feature) => (
|
{content.items.map((feature) => (
|
||||||
<Card key={feature.title} className='border-border/40'>
|
<Card
|
||||||
|
key={`${feature.title}-${feature.icon}`}
|
||||||
|
className='border-border/40'
|
||||||
|
>
|
||||||
<CardHeader className='flex items-center gap-2'>
|
<CardHeader className='flex items-center gap-2'>
|
||||||
<div className='mb-2 text-3xl'>{feature.icon}</div>
|
<div className='mb-2 text-3xl'>{feature.icon}</div>
|
||||||
<CardTitle className='text-xl'>{feature.title}</CardTitle>
|
<CardTitle className='text-xl'>{feature.title}</CardTitle>
|
||||||
|
|||||||
@@ -4,42 +4,42 @@ import Link from 'next/link';
|
|||||||
|
|
||||||
import { Button } from '@gib/ui';
|
import { Button } from '@gib/ui';
|
||||||
|
|
||||||
|
import type { LandingHeroContent } from './content';
|
||||||
|
|
||||||
const kanitSans = Kanit({
|
const kanitSans = Kanit({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
weight: ['400', '500', '600', '700'],
|
weight: ['400', '500', '600', '700'],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Hero = () => (
|
interface HeroProps {
|
||||||
|
content: LandingHeroContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Hero = ({ content }: HeroProps) => (
|
||||||
<section className='container mx-auto px-4 py-24 md:py-32 lg:py-40'>
|
<section className='container mx-auto px-4 py-24 md:py-32 lg:py-40'>
|
||||||
<div className='mx-auto flex max-w-5xl flex-col items-center gap-8 text-center'>
|
<div className='mx-auto flex max-w-5xl flex-col items-center gap-8 text-center'>
|
||||||
{/* Badge */}
|
|
||||||
<div className='border-border/40 bg-muted/50 inline-flex items-center rounded-full border px-3 py-1 text-sm font-medium'>
|
<div className='border-border/40 bg-muted/50 inline-flex items-center rounded-full border px-3 py-1 text-sm font-medium'>
|
||||||
<span className='mr-2'>🚀</span>
|
<span className='mr-2'>{content.badgeEmoji}</span>
|
||||||
<span>Production-ready monorepo template</span>
|
<span>{content.badgeText}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Heading */}
|
|
||||||
<h1 className='from-foreground to-foreground/70 bg-linear-to-br bg-clip-text text-4xl font-bold tracking-tight text-transparent sm:text-5xl md:text-6xl lg:text-7xl'>
|
<h1 className='from-foreground to-foreground/70 bg-linear-to-br bg-clip-text text-4xl font-bold tracking-tight text-transparent sm:text-5xl md:text-6xl lg:text-7xl'>
|
||||||
Build Full-Stack Apps with{' '}
|
{content.headingPrefix}{' '}
|
||||||
<span
|
<span
|
||||||
className={`${kanitSans.className} to-accent-foreground bg-linear-to-r from-[#281A65] via-[#363354] bg-clip-text text-transparent sm:text-6xl lg:text-7xl xl:text-8xl dark:from-[#bec8e6] dark:via-[#F0EEE4] dark:to-[#FFF8E7]`}
|
className={`${kanitSans.className} to-accent-foreground bg-linear-to-r from-[#281A65] via-[#363354] bg-clip-text text-transparent sm:text-6xl lg:text-7xl xl:text-8xl dark:from-[#bec8e6] dark:via-[#F0EEE4] dark:to-[#FFF8E7]`}
|
||||||
>
|
>
|
||||||
convex monorepo
|
{content.headingHighlight}
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
<p className='text-muted-foreground max-w-2xl text-lg md:text-xl'>
|
<p className='text-muted-foreground max-w-2xl text-lg md:text-xl'>
|
||||||
A Turborepo starter with Next.js, Expo, and self-hosted Convex. Ship web
|
{content.description}
|
||||||
and mobile apps faster with shared code, type-safe backend, and complete
|
|
||||||
control over your infrastructure.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* CTA Buttons */}
|
|
||||||
<div className='flex flex-col gap-3 sm:flex-row'>
|
<div className='flex flex-col gap-3 sm:flex-row'>
|
||||||
<Button size='lg' variant='outline' asChild>
|
<Button size='lg' variant='outline' asChild>
|
||||||
<Link
|
<Link
|
||||||
href='https://git.gbrown.org/gib/convex-monorepo'
|
href={content.primaryCta.url}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
>
|
>
|
||||||
@@ -49,77 +49,30 @@ export const Hero = () => (
|
|||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
/>
|
/>
|
||||||
View Source Code
|
{content.primaryCta.label}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Features Quick List */}
|
|
||||||
<div className='text-muted-foreground mt-8 flex flex-wrap items-center justify-center gap-6 text-sm'>
|
<div className='text-muted-foreground mt-8 flex flex-wrap items-center justify-center gap-6 text-sm'>
|
||||||
<div className='flex items-center gap-2'>
|
{content.highlights.map((highlight) => (
|
||||||
<svg
|
<div key={highlight} className='flex items-center gap-2'>
|
||||||
className='h-5 w-5 text-green-500'
|
<svg
|
||||||
fill='none'
|
className='h-5 w-5 text-green-500'
|
||||||
viewBox='0 0 24 24'
|
fill='none'
|
||||||
stroke='currentColor'
|
viewBox='0 0 24 24'
|
||||||
>
|
stroke='currentColor'
|
||||||
<path
|
>
|
||||||
strokeLinecap='round'
|
<path
|
||||||
strokeLinejoin='round'
|
strokeLinecap='round'
|
||||||
strokeWidth={2}
|
strokeLinejoin='round'
|
||||||
d='M5 13l4 4L19 7'
|
strokeWidth={2}
|
||||||
/>
|
d='M5 13l4 4L19 7'
|
||||||
</svg>
|
/>
|
||||||
<span>TypeScript</span>
|
</svg>
|
||||||
</div>
|
<span>{highlight}</span>
|
||||||
<div className='flex items-center gap-2'>
|
</div>
|
||||||
<svg
|
))}
|
||||||
className='h-5 w-5 text-green-500'
|
|
||||||
fill='none'
|
|
||||||
viewBox='0 0 24 24'
|
|
||||||
stroke='currentColor'
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap='round'
|
|
||||||
strokeLinejoin='round'
|
|
||||||
strokeWidth={2}
|
|
||||||
d='M5 13l4 4L19 7'
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>Self-Hosted</span>
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center gap-2'>
|
|
||||||
<svg
|
|
||||||
className='h-5 w-5 text-green-500'
|
|
||||||
fill='none'
|
|
||||||
viewBox='0 0 24 24'
|
|
||||||
stroke='currentColor'
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap='round'
|
|
||||||
strokeLinejoin='round'
|
|
||||||
strokeWidth={2}
|
|
||||||
d='M5 13l4 4L19 7'
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>Real-time</span>
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center gap-2'>
|
|
||||||
<svg
|
|
||||||
className='h-5 w-5 text-green-500'
|
|
||||||
fill='none'
|
|
||||||
viewBox='0 0 24 24'
|
|
||||||
stroke='currentColor'
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap='round'
|
|
||||||
strokeLinejoin='round'
|
|
||||||
strokeWidth={2}
|
|
||||||
d='M5 13l4 4L19 7'
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>Auth Included</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,59 +1,24 @@
|
|||||||
const techStack = [
|
import type { LandingTechStackContent } from './content';
|
||||||
{
|
|
||||||
category: 'Frontend',
|
|
||||||
technologies: [
|
|
||||||
{ name: 'Next.js 16', description: 'React framework with App Router' },
|
|
||||||
{ name: 'Expo 54', description: 'React Native framework' },
|
|
||||||
{ name: 'React 19', description: 'Latest React with Server Components' },
|
|
||||||
{
|
|
||||||
name: 'Tailwind CSS v4',
|
|
||||||
description: 'Utility-first CSS framework',
|
|
||||||
},
|
|
||||||
{ name: 'shadcn/ui', description: 'Beautiful component library' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: 'Backend',
|
|
||||||
technologies: [
|
|
||||||
{ name: 'Convex', description: 'Self-hosted reactive backend' },
|
|
||||||
{
|
|
||||||
name: '@convex-dev/auth',
|
|
||||||
description: 'Multi-provider authentication',
|
|
||||||
},
|
|
||||||
{ name: 'UseSend', description: 'Self-hosted email service' },
|
|
||||||
{ name: 'File Storage', description: 'Built-in file uploads' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: 'Developer Tools',
|
|
||||||
technologies: [
|
|
||||||
{ name: 'Turborepo', description: 'High-performance build system' },
|
|
||||||
{ name: 'TypeScript', description: 'Type-safe development' },
|
|
||||||
{ name: 'Bun', description: 'Fast package manager & runtime' },
|
|
||||||
{ name: 'ESLint + Prettier', description: 'Code quality tools' },
|
|
||||||
{ name: 'Docker', description: 'Containerized deployment' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const TechStack = () => (
|
interface TechStackProps {
|
||||||
|
content: LandingTechStackContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TechStack = ({ content }: TechStackProps) => (
|
||||||
<section id='tech-stack' className='border-border/40 bg-muted/30 border-t'>
|
<section id='tech-stack' className='border-border/40 bg-muted/30 border-t'>
|
||||||
<div className='container mx-auto px-4 py-24'>
|
<div className='container mx-auto px-4 py-24'>
|
||||||
<div className='mx-auto max-w-6xl'>
|
<div className='mx-auto max-w-6xl'>
|
||||||
{/* Section Header */}
|
|
||||||
<div className='mb-16 text-center'>
|
<div className='mb-16 text-center'>
|
||||||
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl'>
|
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl'>
|
||||||
Modern Tech Stack
|
{content.heading}
|
||||||
</h2>
|
</h2>
|
||||||
<p className='text-muted-foreground mx-auto max-w-2xl text-lg'>
|
<p className='text-muted-foreground mx-auto max-w-2xl text-lg'>
|
||||||
Built with the latest and greatest tools for maximum productivity
|
{content.description}
|
||||||
and performance.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tech Stack Grid */}
|
|
||||||
<div className='grid gap-12 md:grid-cols-3'>
|
<div className='grid gap-12 md:grid-cols-3'>
|
||||||
{techStack.map((stack) => (
|
{content.categories.map((stack) => (
|
||||||
<div key={stack.category}>
|
<div key={stack.category}>
|
||||||
<h3 className='mb-6 text-xl font-semibold'>{stack.category}</h3>
|
<h3 className='mb-6 text-xl font-semibold'>{stack.category}</h3>
|
||||||
<ul className='space-y-4'>
|
<ul className='space-y-4'>
|
||||||
|
|||||||
24
apps/next/src/lib/payload/get-landing-page-content.ts
Normal file
24
apps/next/src/lib/payload/get-landing-page-content.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type { LandingPageContent } from '@/components/landing/content';
|
||||||
|
import { cache } from 'react';
|
||||||
|
import {
|
||||||
|
defaultLandingPageContent,
|
||||||
|
mergeLandingPageContent,
|
||||||
|
} from '@/components/landing/content';
|
||||||
|
|
||||||
|
import { getPayloadClient } from './get-payload';
|
||||||
|
|
||||||
|
export const getLandingPageContent = cache(
|
||||||
|
async (): Promise<LandingPageContent> => {
|
||||||
|
const payload = await getPayloadClient();
|
||||||
|
const landingPage = await (
|
||||||
|
payload as {
|
||||||
|
findGlobal: (args: { slug: string }) => Promise<unknown>;
|
||||||
|
}
|
||||||
|
).findGlobal({ slug: 'landing-page' });
|
||||||
|
|
||||||
|
return mergeLandingPageContent(
|
||||||
|
(landingPage as Partial<LandingPageContent> | null | undefined) ??
|
||||||
|
defaultLandingPageContent,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
7
apps/next/src/lib/payload/get-payload.ts
Normal file
7
apps/next/src/lib/payload/get-payload.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { cache } from 'react';
|
||||||
|
import config from '@payload-config';
|
||||||
|
import { getPayload } from 'payload';
|
||||||
|
|
||||||
|
export const getPayloadClient = cache(async () => {
|
||||||
|
return await getPayload({ config });
|
||||||
|
});
|
||||||
@@ -4,10 +4,13 @@ import { buildConfig } from 'payload';
|
|||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
|
|
||||||
import { env } from './env';
|
import { env } from './env';
|
||||||
|
import { Users } from './payload/collections/users';
|
||||||
|
import { LandingPage } from './payload/globals/landing-page';
|
||||||
|
|
||||||
export default buildConfig({
|
export default buildConfig({
|
||||||
editor: lexicalEditor(),
|
editor: lexicalEditor(),
|
||||||
collections: [],
|
collections: [Users],
|
||||||
|
globals: [LandingPage],
|
||||||
secret: env.PAYLOAD_SECRET ?? '',
|
secret: env.PAYLOAD_SECRET ?? '',
|
||||||
db: postgresAdapter({
|
db: postgresAdapter({
|
||||||
pool: {
|
pool: {
|
||||||
|
|||||||
10
apps/next/src/payload/collections/users.ts
Normal file
10
apps/next/src/payload/collections/users.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import type { CollectionConfig } from 'payload';
|
||||||
|
|
||||||
|
export const Users: CollectionConfig = {
|
||||||
|
slug: 'users',
|
||||||
|
auth: true,
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'email',
|
||||||
|
},
|
||||||
|
fields: [],
|
||||||
|
};
|
||||||
203
apps/next/src/payload/globals/landing-page.ts
Normal file
203
apps/next/src/payload/globals/landing-page.ts
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import type { GlobalConfig } from 'payload';
|
||||||
|
|
||||||
|
import { defaultLandingPageContent } from '../../components/landing/content';
|
||||||
|
|
||||||
|
export const LandingPage: GlobalConfig = {
|
||||||
|
slug: 'landing-page',
|
||||||
|
label: 'Landing Page',
|
||||||
|
access: {
|
||||||
|
read: () => true,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'tabs',
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
label: 'Hero',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'hero',
|
||||||
|
type: 'group',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
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