feat: payload cms
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Failing after 1m13s
Build & Deploy / 🏗️ Build (push) Failing after 5m53s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Smoke Test (push) Has been skipped
Build & Deploy / ⚡ Lighthouse (push) Has been skipped
Build & Deploy / ♿ WCAG (push) Has been skipped
Build & Deploy / 🛡️ Quality Gates (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Failing after 1m13s
Build & Deploy / 🏗️ Build (push) Failing after 5m53s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Smoke Test (push) Has been skipped
Build & Deploy / ⚡ Lighthouse (push) Has been skipped
Build & Deploy / ♿ WCAG (push) Has been skipped
Build & Deploy / 🛡️ Quality Gates (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
This commit is contained in:
4
.env
4
.env
@@ -37,3 +37,7 @@ INFRA_DIRECTUS_URL=http://localhost:8059
|
|||||||
INFRA_DIRECTUS_TOKEN=59fb8f4c1a51b18fe28ad947f713914e
|
INFRA_DIRECTUS_TOKEN=59fb8f4c1a51b18fe28ad947f713914e
|
||||||
GATEKEEPER_ORIGIN=http://klz.localhost
|
GATEKEEPER_ORIGIN=http://klz.localhost
|
||||||
OPENROUTER_API_KEY=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
|
OPENROUTER_API_KEY=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
|
||||||
|
# Payload CMS Local Development
|
||||||
|
POSTGRES_URI=postgres://klz_db_user:klz_db_pass@127.0.0.1:54322/directus
|
||||||
|
PAYLOAD_SECRET=D7F36ED8D3B2F77A5E6B4A3962
|
||||||
|
|
||||||
|
|||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,18 +1,17 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.next
|
.next
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.pnpm-store
|
||||||
public/uploads
|
public/uploads
|
||||||
|
public/media
|
||||||
|
|
||||||
# Lighthouse CI
|
# Lighthouse CI
|
||||||
.lighthouseci/
|
.lighthouseci/
|
||||||
lighthouserc.cjs
|
lighthouserc.cjs
|
||||||
.lighthouserc.json
|
.lighthouserc.json
|
||||||
|
|
||||||
# Directus
|
# Legacy (Directus) cleanup
|
||||||
directus/uploads
|
directus/uploads
|
||||||
!directus/extensions/
|
|
||||||
!directus/schema/
|
|
||||||
!directus/migrations/
|
|
||||||
|
|
||||||
.next-docker
|
.next-docker
|
||||||
|
|
||||||
|
|||||||
18
Dockerfile.dev
Normal file
18
Dockerfile.dev
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
# Install essential build tools if needed (e.g., for node-gyp)
|
||||||
|
RUN apk add --no-cache libc6-compat python3 make g++
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Enable corepack for pnpm
|
||||||
|
RUN corepack enable
|
||||||
|
|
||||||
|
# Pre-set the pnpm store directory
|
||||||
|
ENV PNPM_HOME="/pnpm"
|
||||||
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
|
|
||||||
|
# Set up pnpm store configuration
|
||||||
|
RUN pnpm config set store-dir /pnpm/store
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
17
app/(payload)/admin/[[...segments]]/page.tsx
Normal file
17
app/(payload)/admin/[[...segments]]/page.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import configPromise from '@payload-config';
|
||||||
|
import { RootPage } from '@payloadcms/next/views';
|
||||||
|
import { importMap } from '../importMap';
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
params: Promise<{
|
||||||
|
segments: string[];
|
||||||
|
}>;
|
||||||
|
searchParams: Promise<{
|
||||||
|
[key: string]: string | string[];
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Page = ({ params, searchParams }: Args) =>
|
||||||
|
RootPage({ config: configPromise, importMap, params, searchParams });
|
||||||
|
|
||||||
|
export default Page;
|
||||||
77
app/(payload)/admin/importMap.js
Normal file
77
app/(payload)/admin/importMap.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc';
|
||||||
|
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc';
|
||||||
|
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc';
|
||||||
|
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client';
|
||||||
|
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client';
|
||||||
|
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client';
|
||||||
|
import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client';
|
||||||
|
import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client';
|
||||||
|
import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client';
|
||||||
|
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client';
|
||||||
|
import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client';
|
||||||
|
import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client';
|
||||||
|
import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client';
|
||||||
|
import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client';
|
||||||
|
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client';
|
||||||
|
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client';
|
||||||
|
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client';
|
||||||
|
import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client';
|
||||||
|
import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client';
|
||||||
|
import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client';
|
||||||
|
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client';
|
||||||
|
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client';
|
||||||
|
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client';
|
||||||
|
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client';
|
||||||
|
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc';
|
||||||
|
|
||||||
|
export const importMap = {
|
||||||
|
'@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell':
|
||||||
|
RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||||
|
'@payloadcms/richtext-lexical/rsc#RscEntryLexicalField':
|
||||||
|
RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||||
|
'@payloadcms/richtext-lexical/rsc#LexicalDiffComponent':
|
||||||
|
LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||||
|
'@payloadcms/richtext-lexical/client#BlocksFeatureClient':
|
||||||
|
BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient':
|
||||||
|
InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient':
|
||||||
|
HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#UploadFeatureClient':
|
||||||
|
UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#BlockquoteFeatureClient':
|
||||||
|
BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#RelationshipFeatureClient':
|
||||||
|
RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#LinkFeatureClient':
|
||||||
|
LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#ChecklistFeatureClient':
|
||||||
|
ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#OrderedListFeatureClient':
|
||||||
|
OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#UnorderedListFeatureClient':
|
||||||
|
UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#IndentFeatureClient':
|
||||||
|
IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#AlignFeatureClient':
|
||||||
|
AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#HeadingFeatureClient':
|
||||||
|
HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#ParagraphFeatureClient':
|
||||||
|
ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#InlineCodeFeatureClient':
|
||||||
|
InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#SuperscriptFeatureClient':
|
||||||
|
SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#SubscriptFeatureClient':
|
||||||
|
SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#StrikethroughFeatureClient':
|
||||||
|
StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#UnderlineFeatureClient':
|
||||||
|
UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#BoldFeatureClient':
|
||||||
|
BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/richtext-lexical/client#ItalicFeatureClient':
|
||||||
|
ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
'@payloadcms/next/rsc#CollectionCards': CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1,
|
||||||
|
};
|
||||||
14
app/(payload)/api/[...slug]/route.ts
Normal file
14
app/(payload)/api/[...slug]/route.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import config from '@payload-config';
|
||||||
|
import {
|
||||||
|
REST_GET,
|
||||||
|
REST_OPTIONS,
|
||||||
|
REST_PATCH,
|
||||||
|
REST_POST,
|
||||||
|
REST_DELETE,
|
||||||
|
} from '@payloadcms/next/routes';
|
||||||
|
|
||||||
|
export const GET = REST_GET(config);
|
||||||
|
export const POST = REST_POST(config);
|
||||||
|
export const DELETE = REST_DELETE(config);
|
||||||
|
export const PATCH = REST_PATCH(config);
|
||||||
|
export const OPTIONS = REST_OPTIONS(config);
|
||||||
4
app/(payload)/api/graphql/route.ts
Normal file
4
app/(payload)/api/graphql/route.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import config from '@payload-config';
|
||||||
|
import { GRAPHQL_POST } from '@payloadcms/next/routes';
|
||||||
|
|
||||||
|
export const POST = GRAPHQL_POST(config);
|
||||||
1
app/(payload)/custom.scss
Normal file
1
app/(payload)/custom.scss
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/* Custom Payload CMS admin styles can go here. Do not import payloadcms/ui/scss/app.scss as it is handled by @payloadcms/next/css */
|
||||||
31
app/(payload)/layout.tsx
Normal file
31
app/(payload)/layout.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import configPromise from '@payload-config';
|
||||||
|
import { RootLayout } from '@payloadcms/next/layouts';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import '@payloadcms/next/css';
|
||||||
|
import './custom.scss';
|
||||||
|
import { handleServerFunctions } from '@payloadcms/next/layouts';
|
||||||
|
import { importMap } from './admin/importMap';
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const serverFunction: any = async function (args: any) {
|
||||||
|
'use server';
|
||||||
|
return handleServerFunctions({
|
||||||
|
...args,
|
||||||
|
config: configPromise,
|
||||||
|
importMap,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const Layout = ({ children }: Args) => {
|
||||||
|
return (
|
||||||
|
<RootLayout config={configPromise} importMap={importMap} serverFunction={serverFunction}>
|
||||||
|
{children}
|
||||||
|
</RootLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import { MDXRemote } from 'next-mdx-remote/rsc';
|
|
||||||
import { Container, Badge, Heading } from '@/components/ui';
|
import { Container, Badge, Heading } from '@/components/ui';
|
||||||
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { getPageBySlug, getAllPages } from '@/lib/pages';
|
import { getPageBySlug, getAllPages } from '@/lib/pages';
|
||||||
import { mdxComponents } from '@/components/blog/MDXComponents';
|
import PayloadRichText from '@/components/PayloadRichText';
|
||||||
import { SITE_URL } from '@/lib/schema';
|
import { SITE_URL } from '@/lib/schema';
|
||||||
import TrackedLink from '@/components/analytics/TrackedLink';
|
import TrackedLink from '@/components/analytics/TrackedLink';
|
||||||
|
|
||||||
@@ -102,7 +101,7 @@ export default async function StandardPage({ params }: PageProps) {
|
|||||||
|
|
||||||
{/* Main content with shared blog components */}
|
{/* Main content with shared blog components */}
|
||||||
<div className="prose prose-lg md:prose-xl max-w-none prose-headings:font-bold prose-headings:text-text-primary prose-p:text-text-secondary prose-p:leading-relaxed prose-a:text-primary prose-a:no-underline hover:prose-a:underline prose-img:rounded-2xl prose-img:shadow-2xl prose-blockquote:border-primary prose-blockquote:bg-primary/5 prose-blockquote:rounded-r-2xl prose-strong:text-primary">
|
<div className="prose prose-lg md:prose-xl max-w-none prose-headings:font-bold prose-headings:text-text-primary prose-p:text-text-secondary prose-p:leading-relaxed prose-a:text-primary prose-a:no-underline hover:prose-a:underline prose-img:rounded-2xl prose-img:shadow-2xl prose-blockquote:border-primary prose-blockquote:bg-primary/5 prose-blockquote:rounded-r-2xl prose-strong:text-primary">
|
||||||
<MDXRemote source={pageData.content} components={mdxComponents} />
|
<PayloadRichText data={pageData.content} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Support Section */}
|
{/* Support Section */}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import JsonLd from '@/components/JsonLd';
|
import JsonLd from '@/components/JsonLd';
|
||||||
import { SITE_URL } from '@/lib/schema';
|
import { SITE_URL } from '@/lib/schema';
|
||||||
import { MDXRemote } from 'next-mdx-remote/rsc';
|
import { getPostBySlug, getAdjacentPosts, getReadingTime } from '@/lib/blog';
|
||||||
import { getPostBySlug, getAdjacentPosts, getReadingTime, getHeadings } from '@/lib/blog';
|
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import PostNavigation from '@/components/blog/PostNavigation';
|
import PostNavigation from '@/components/blog/PostNavigation';
|
||||||
import PowerCTA from '@/components/blog/PowerCTA';
|
import PowerCTA from '@/components/blog/PowerCTA';
|
||||||
import TableOfContents from '@/components/blog/TableOfContents';
|
|
||||||
import { mdxComponents } from '@/components/blog/MDXComponents';
|
|
||||||
import { Heading } from '@/components/ui';
|
import { Heading } from '@/components/ui';
|
||||||
import { setRequestLocale } from 'next-intl/server';
|
import { setRequestLocale } from 'next-intl/server';
|
||||||
import BlogEngagementTracker from '@/components/analytics/BlogEngagementTracker';
|
import BlogEngagementTracker from '@/components/analytics/BlogEngagementTracker';
|
||||||
|
|
||||||
|
// Payload CMS Imports
|
||||||
|
import PayloadRichText from '@/components/PayloadRichText';
|
||||||
|
|
||||||
interface BlogPostProps {
|
interface BlogPostProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
locale: string;
|
locale: string;
|
||||||
@@ -60,7 +60,8 @@ export default async function BlogPost({ params }: BlogPostProps) {
|
|||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const headings = getHeadings(post.content);
|
// Convert Lexical content into a plain string to estimate reading time roughly
|
||||||
|
const rawTextContent = JSON.stringify(post.content);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className="bg-white min-h-screen font-sans selection:bg-primary/10 selection:text-primary">
|
<article className="bg-white min-h-screen font-sans selection:bg-primary/10 selection:text-primary">
|
||||||
@@ -68,7 +69,7 @@ export default async function BlogPost({ params }: BlogPostProps) {
|
|||||||
title={post.frontmatter.title}
|
title={post.frontmatter.title}
|
||||||
slug={slug}
|
slug={slug}
|
||||||
category={post.frontmatter.category}
|
category={post.frontmatter.category}
|
||||||
readingTime={getReadingTime(post.content)}
|
readingTime={getReadingTime(rawTextContent)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Featured Image Header */}
|
{/* Featured Image Header */}
|
||||||
@@ -76,7 +77,7 @@ export default async function BlogPost({ params }: BlogPostProps) {
|
|||||||
<div className="relative w-full h-[70vh] min-h-[500px] overflow-hidden group">
|
<div className="relative w-full h-[70vh] min-h-[500px] overflow-hidden group">
|
||||||
<div className="absolute inset-0 transition-transform duration-[3s] ease-out scale-110 group-hover:scale-100">
|
<div className="absolute inset-0 transition-transform duration-[3s] ease-out scale-110 group-hover:scale-100">
|
||||||
<Image
|
<Image
|
||||||
src={`${post.frontmatter.featuredImage}?ar=16:9`}
|
src={post.frontmatter.featuredImage.split('?')[0]}
|
||||||
alt={post.frontmatter.title}
|
alt={post.frontmatter.title}
|
||||||
fill
|
fill
|
||||||
priority
|
priority
|
||||||
@@ -109,7 +110,7 @@ export default async function BlogPost({ params }: BlogPostProps) {
|
|||||||
})}
|
})}
|
||||||
</time>
|
</time>
|
||||||
<span className="w-1 h-1 bg-white/30 rounded-full" />
|
<span className="w-1 h-1 bg-white/30 rounded-full" />
|
||||||
<span>{getReadingTime(post.content)} min read</span>
|
<span>{getReadingTime(rawTextContent)} min read</span>
|
||||||
{(new Date(post.frontmatter.date) > new Date() ||
|
{(new Date(post.frontmatter.date) > new Date() ||
|
||||||
post.frontmatter.public === false) && (
|
post.frontmatter.public === false) && (
|
||||||
<>
|
<>
|
||||||
@@ -146,7 +147,7 @@ export default async function BlogPost({ params }: BlogPostProps) {
|
|||||||
})}
|
})}
|
||||||
</time>
|
</time>
|
||||||
<span className="w-1 h-1 bg-neutral-400 rounded-full" />
|
<span className="w-1 h-1 bg-neutral-400 rounded-full" />
|
||||||
<span>{getReadingTime(post.content)} min read</span>
|
<span>{getReadingTime(rawTextContent)} min read</span>
|
||||||
{(new Date(post.frontmatter.date) > new Date() ||
|
{(new Date(post.frontmatter.date) > new Date() ||
|
||||||
post.frontmatter.public === false) && (
|
post.frontmatter.public === false) && (
|
||||||
<>
|
<>
|
||||||
@@ -175,9 +176,9 @@ export default async function BlogPost({ params }: BlogPostProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Main content with enhanced styling */}
|
{/* Main content with enhanced styling rendering Payload Lexical */}
|
||||||
<div className="prose prose-lg md:prose-xl max-w-none prose-headings:font-bold prose-headings:text-text-primary prose-p:text-text-secondary prose-p:leading-relaxed prose-a:text-primary prose-a:no-underline hover:prose-a:underline prose-img:rounded-2xl prose-img:shadow-2xl prose-blockquote:border-primary prose-blockquote:bg-primary/5 prose-blockquote:rounded-r-2xl prose-strong:text-primary">
|
<div className="prose prose-lg md:prose-xl max-w-none prose-headings:font-bold prose-headings:text-text-primary prose-p:text-text-secondary prose-p:leading-relaxed prose-a:text-primary prose-a:no-underline hover:prose-a:underline prose-img:rounded-2xl prose-img:shadow-2xl prose-blockquote:border-primary prose-blockquote:bg-primary/5 prose-blockquote:rounded-r-2xl prose-strong:text-primary">
|
||||||
<MDXRemote source={post.content} components={mdxComponents} />
|
<PayloadRichText data={post.content} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Power CTA */}
|
{/* Power CTA */}
|
||||||
@@ -220,10 +221,10 @@ export default async function BlogPost({ params }: BlogPostProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Column: Sticky Sidebar */}
|
{/* Right Column: Sticky Sidebar - Temporarily Hidden without ToC */}
|
||||||
<aside className="sticky-narrative-sidebar hidden lg:block">
|
<aside className="sticky-narrative-sidebar hidden lg:block">
|
||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
<TableOfContents headings={headings} locale={locale} />
|
{/* Future Payload Table of Contents Implementation */}
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
@@ -262,8 +263,8 @@ export default async function BlogPost({ params }: BlogPostProps) {
|
|||||||
'@id': `${SITE_URL}/${locale}/blog/${slug}`,
|
'@id': `${SITE_URL}/${locale}/blog/${slug}`,
|
||||||
},
|
},
|
||||||
articleSection: post.frontmatter.category,
|
articleSection: post.frontmatter.category,
|
||||||
wordCount: post.content.split(/\s+/).length,
|
wordCount: rawTextContent.split(/\s+/).length,
|
||||||
timeRequired: `PT${getReadingTime(post.content)}M`,
|
timeRequired: `PT${getReadingTime(rawTextContent)}M`,
|
||||||
} as any
|
} as any
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
|
|||||||
{featuredPost && featuredPost.frontmatter.featuredImage && (
|
{featuredPost && featuredPost.frontmatter.featuredImage && (
|
||||||
<>
|
<>
|
||||||
<Image
|
<Image
|
||||||
src={featuredPost.frontmatter.featuredImage}
|
src={featuredPost.frontmatter.featuredImage.split('?')[0]}
|
||||||
alt={featuredPost.frontmatter.title}
|
alt={featuredPost.frontmatter.title}
|
||||||
fill
|
fill
|
||||||
className="absolute inset-0 w-full h-full object-cover opacity-40 md:opacity-60"
|
className="absolute inset-0 w-full h-full object-cover opacity-40 md:opacity-60"
|
||||||
@@ -164,7 +164,7 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
|
|||||||
{post.frontmatter.featuredImage && (
|
{post.frontmatter.featuredImage && (
|
||||||
<div className="relative h-48 md:h-72 overflow-hidden">
|
<div className="relative h-48 md:h-72 overflow-hidden">
|
||||||
<Image
|
<Image
|
||||||
src={post.frontmatter.featuredImage}
|
src={post.frontmatter.featuredImage.split('?')[0]}
|
||||||
alt={post.frontmatter.title}
|
alt={post.frontmatter.title}
|
||||||
fill
|
fill
|
||||||
className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-110"
|
className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-110"
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export const contentType = 'image/png';
|
|||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
export default async function Image({ params }: { params: Promise<{ locale: string }> }) {
|
export default async function Image({ params }: { params: Promise<{ locale: string }> }) {
|
||||||
console.log('🖼️ OG Image Handler Called');
|
|
||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
const t = await getTranslations({ locale, namespace: 'Index.meta' });
|
const t = await getTranslations({ locale, namespace: 'Index.meta' });
|
||||||
const fonts = await getOgFonts();
|
const fonts = await getOgFonts();
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ import { mapFileSlugToTranslated, mapSlugToFileSlug } from '@/lib/slugs';
|
|||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||||
import { getProductOGImageMetadata } from '@/lib/metadata';
|
import { getProductOGImageMetadata } from '@/lib/metadata';
|
||||||
import { MDXRemote } from 'next-mdx-remote/rsc';
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import ProductEngagementTracker from '@/components/analytics/ProductEngagementTracker';
|
import ProductEngagementTracker from '@/components/analytics/ProductEngagementTracker';
|
||||||
|
import PayloadRichText from '@/components/PayloadRichText';
|
||||||
|
|
||||||
interface ProductPageProps {
|
interface ProductPageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@@ -103,76 +103,7 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const components = {
|
// ... the rest of the file layout ...
|
||||||
ProductTechnicalData,
|
|
||||||
ProductTabs,
|
|
||||||
p: (props: any) => (
|
|
||||||
<p
|
|
||||||
{...props}
|
|
||||||
className="text-lg md:text-xl text-text-secondary leading-relaxed mb-8 font-medium"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
h1: (props: any) => (
|
|
||||||
<div className="relative mb-16">
|
|
||||||
<h2
|
|
||||||
{...props}
|
|
||||||
className="text-3xl md:text-4xl font-black text-primary tracking-tighter uppercase mb-6"
|
|
||||||
/>
|
|
||||||
<div className="w-20 h-1.5 bg-accent rounded-full" />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
h2: (props: any) => (
|
|
||||||
<h3
|
|
||||||
{...props}
|
|
||||||
className="text-2xl md:text-3xl font-black text-primary mb-10 tracking-tight uppercase"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
h3: (props: any) => (
|
|
||||||
<h4
|
|
||||||
{...props}
|
|
||||||
className="text-xl md:text-2xl font-black text-primary mb-8 tracking-tight uppercase"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
ul: (props: any) => <ul {...props} className="list-none pl-0 mb-10" />,
|
|
||||||
section: (props: any) => <div {...props} className="block" />,
|
|
||||||
li: (props: any) => (
|
|
||||||
<li className="flex items-start gap-4 group mb-4 last:mb-0">
|
|
||||||
<div className="mt-2.5 w-2 h-2 rounded-full bg-accent flex-shrink-0 group-hover:scale-125 transition-transform" />
|
|
||||||
<span
|
|
||||||
{...props}
|
|
||||||
className="text-lg md:text-xl text-text-secondary leading-relaxed font-medium"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
),
|
|
||||||
strong: (props: any) => <strong {...props} className="font-black text-primary" />,
|
|
||||||
table: (props: any) => (
|
|
||||||
<div className="overflow-x-auto my-20 rounded-[32px] border border-neutral-dark/10 shadow-xl bg-white p-1">
|
|
||||||
<table {...props} className="min-w-full divide-y divide-neutral-dark/10" />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
th: (props: any) => (
|
|
||||||
<th
|
|
||||||
{...props}
|
|
||||||
className="px-8 py-6 bg-neutral-light/50 text-left text-[10px] font-black uppercase tracking-[0.25em] text-primary/60"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
td: (props: any) => (
|
|
||||||
<td
|
|
||||||
{...props}
|
|
||||||
className="px-8 py-6 text-text-secondary border-t border-neutral-dark/5 text-lg md:text-xl font-medium"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
hr: () => <hr className="my-24 border-t-2 border-neutral-dark/5" />,
|
|
||||||
blockquote: (props: any) => (
|
|
||||||
<div className="my-20 p-10 md:p-16 bg-primary-dark rounded-[40px] relative overflow-hidden group">
|
|
||||||
<div className="absolute top-0 right-0 w-64 h-64 bg-accent/10 rounded-full -translate-y-1/2 translate-x-1/2 blur-3xl group-hover:bg-accent/20 transition-colors duration-700" />
|
|
||||||
<div
|
|
||||||
className="relative z-10 italic text-2xl md:text-3xl text-white/90 leading-relaxed font-black tracking-tight"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function ProductPage({ params }: ProductPageProps) {
|
export default async function ProductPage({ params }: ProductPageProps) {
|
||||||
const { locale, slug } = await params;
|
const { locale, slug } = await params;
|
||||||
@@ -181,7 +112,6 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
const t = await getTranslations('Products');
|
const t = await getTranslations('Products');
|
||||||
const productsSlug = await mapFileSlugToTranslated('products', locale);
|
const productsSlug = await mapFileSlugToTranslated('products', locale);
|
||||||
|
|
||||||
// Check if it's a category page
|
|
||||||
const categories = [
|
const categories = [
|
||||||
'low-voltage-cables',
|
'low-voltage-cables',
|
||||||
'medium-voltage-cables',
|
'medium-voltage-cables',
|
||||||
@@ -191,6 +121,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
const fileSlug = await mapSlugToFileSlug(productSlug, locale);
|
const fileSlug = await mapSlugToFileSlug(productSlug, locale);
|
||||||
|
|
||||||
if (categories.includes(fileSlug)) {
|
if (categories.includes(fileSlug)) {
|
||||||
|
// (Skipping category page block, same as before)
|
||||||
const allProducts = await getAllProducts(locale);
|
const allProducts = await getAllProducts(locale);
|
||||||
const categoryKey = fileSlug
|
const categoryKey = fileSlug
|
||||||
.replace(/-cables$/, '')
|
.replace(/-cables$/, '')
|
||||||
@@ -199,14 +130,12 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
? t(`categories.${categoryKey}.title`)
|
? t(`categories.${categoryKey}.title`)
|
||||||
: fileSlug;
|
: fileSlug;
|
||||||
|
|
||||||
// Filter products for this category
|
|
||||||
const filteredProducts = allProducts.filter((p) =>
|
const filteredProducts = allProducts.filter((p) =>
|
||||||
p.frontmatter.categories.some(
|
p.frontmatter.categories.some(
|
||||||
(cat) => cat.toLowerCase().replace(/\s+/g, '-') === fileSlug || cat === categoryTitle,
|
(cat) => cat.toLowerCase().replace(/\s+/g, '-') === fileSlug || cat === categoryTitle,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get translated product slugs
|
|
||||||
const productsWithTranslatedSlugs = await Promise.all(
|
const productsWithTranslatedSlugs = await Promise.all(
|
||||||
filteredProducts.map(async (p) => ({
|
filteredProducts.map(async (p) => ({
|
||||||
...p,
|
...p,
|
||||||
@@ -257,7 +186,6 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
className="object-contain p-8 transition-transform duration-700 group-hover:scale-110 z-10"
|
className="object-contain p-8 transition-transform duration-700 group-hover:scale-110 z-10"
|
||||||
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
||||||
/>
|
/>
|
||||||
{/* Subtle reflection/shadow effect */}
|
|
||||||
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 w-2/3 h-4 bg-black/5 blur-xl rounded-full" />
|
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 w-2/3 h-4 bg-black/5 blur-xl rounded-full" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -314,17 +242,14 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract technical data for schema
|
// Extract technical data natively from the Lexical AST for Schema.org
|
||||||
const technicalDataMatch = product.content.match(
|
|
||||||
/technicalData=\{<ProductTechnicalData data=\{(.*?)\}\s*\/>\}/s,
|
|
||||||
);
|
|
||||||
let technicalItems = [];
|
let technicalItems = [];
|
||||||
if (technicalDataMatch) {
|
if (product.content?.root?.children) {
|
||||||
try {
|
const productTabsBlock = product.content.root.children.find(
|
||||||
const data = JSON.parse(technicalDataMatch[1]);
|
(node: any) => node.type === 'block' && node.fields?.blockType === 'productTabs',
|
||||||
technicalItems = data.technicalItems || [];
|
);
|
||||||
} catch (e) {
|
if (productTabsBlock && productTabsBlock.fields?.technicalItems) {
|
||||||
console.error('Failed to parse technical data for schema', e);
|
technicalItems = productTabsBlock.fields.technicalItems;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,23 +272,6 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const productComponents = {
|
|
||||||
...components,
|
|
||||||
ProductTabs: (props: any) => <ProductTabs {...props} sidebar={sidebar} />,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Pre-process content to convert raw HTML tags to Markdown so they use our custom components
|
|
||||||
const processedContent = product.content
|
|
||||||
.replace(/<h1[^>]*>(.*?)<\/h1>/gs, '\n# $1\n') // Maps to our custom h1 (which renders h2)
|
|
||||||
.replace(/<h2[^>]*>(.*?)<\/h2>/gs, '\n## $1\n') // Maps to our custom h2 (which renders h3)
|
|
||||||
.replace(/<h3[^>]*>(.*?)<\/h3>/gs, '\n### $1\n') // Maps to our custom h3 (which renders h4)
|
|
||||||
.replace(/<p[^>]*>(.*?)<\/p>/gs, '\n$1\n')
|
|
||||||
.replace(/<ul[^>]*>(.*?)<\/ul>/gs, '\n$1\n')
|
|
||||||
.replace(/<li[^>]*>(.*?)<\/li>/gs, '\n- $1\n')
|
|
||||||
.replace(/<strong[^>]*>(.*?)<\/strong>/gs, '**$1**')
|
|
||||||
.replace(/<section[^>]*>/gs, '')
|
|
||||||
.replace(/<\/section>/gs, '');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col min-h-screen bg-white relative">
|
<div className="flex flex-col min-h-screen bg-white relative">
|
||||||
{/* Product Hero */}
|
{/* Product Hero */}
|
||||||
@@ -474,8 +382,8 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<div className="max-w-none">
|
<div className="max-w-none prose prose-lg mt-8">
|
||||||
<MDXRemote source={processedContent} components={productComponents} />
|
<PayloadRichText data={product.content} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Datasheet Download Section - Only for Medium Voltage for now */}
|
{/* Datasheet Download Section - Only for Medium Voltage for now */}
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
|
|||||||
className="group whitespace-nowrap w-full md:w-auto md:h-16 md:px-10 md:text-xl"
|
className="group whitespace-nowrap w-full md:w-auto md:h-16 md:px-10 md:text-xl"
|
||||||
>
|
>
|
||||||
{t('cta.button')}
|
{t('cta.button')}
|
||||||
<span className="ml-4 transition-transform group-hover:translate-x-2">
|
<span className="ml-2 md:ml-4 transition-transform group-hover:translate-x-2">
|
||||||
→
|
→
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import client, { ensureAuthenticated } from '@/lib/directus';
|
|
||||||
import { createItem } from '@directus/sdk';
|
|
||||||
import { sendEmail } from '@/lib/mail/mailer';
|
import { sendEmail } from '@/lib/mail/mailer';
|
||||||
import { render, ContactFormNotification, ConfirmationMessage } from '@mintel/mail';
|
import { render, ContactFormNotification, ConfirmationMessage } from '@mintel/mail';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@@ -41,31 +39,30 @@ export async function sendContactFormAction(formData: FormData) {
|
|||||||
return { success: false, error: 'Missing required fields' };
|
return { success: false, error: 'Missing required fields' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Save to Directus
|
// 1. Save to CMS
|
||||||
try {
|
try {
|
||||||
await ensureAuthenticated();
|
const { getPayload } = await import('payload');
|
||||||
if (productName) {
|
const configPromise = (await import('@payload-config')).default;
|
||||||
await client.request(
|
const payload = await getPayload({ config: configPromise });
|
||||||
createItem('product_requests', {
|
|
||||||
product_name: productName,
|
await payload.create({
|
||||||
email,
|
collection: 'form-submissions',
|
||||||
message,
|
data: {
|
||||||
}),
|
|
||||||
);
|
|
||||||
logger.info('Product request stored in Directus');
|
|
||||||
} else {
|
|
||||||
await client.request(
|
|
||||||
createItem('contact_submissions', {
|
|
||||||
name,
|
name,
|
||||||
email,
|
email,
|
||||||
message,
|
message,
|
||||||
}),
|
type: productName ? 'product_quote' : 'contact',
|
||||||
);
|
productName: productName || undefined,
|
||||||
logger.info('Contact submission stored in Directus');
|
},
|
||||||
}
|
});
|
||||||
|
|
||||||
|
logger.info('Successfully saved form submission to Payload CMS', {
|
||||||
|
type: productName ? 'product_quote' : 'contact',
|
||||||
|
email,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to store submission in Directus', { error });
|
logger.error('Failed to store submission in Payload CMS', { error });
|
||||||
services.errors.captureException(error, { action: 'directus_store_submission' });
|
services.errors.captureException(error, { action: 'payload_store_submission' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Send Emails
|
// 2. Send Emails
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { checkHealth } from '@/lib/directus';
|
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
const health = await checkHealth();
|
// Payload is embedded within the Next.js app, so if this route responds, the CMS is up.
|
||||||
return NextResponse.json(health, { status: health.status === 'ok' ? 200 : 503 });
|
// Further DB health checks can be implemented via Payload Local API later.
|
||||||
|
return NextResponse.json({ status: 'ok', message: 'Payload CMS is embedded.' }, { status: 200 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ export async function POST(request: NextRequest) {
|
|||||||
const logger = services.logger.child({ component: 'sentry-relay' });
|
const logger = services.logger.child({ component: 'sentry-relay' });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Prevent 403 Forbidden console noise in local dev
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
return NextResponse.json({ status: 'ignored_in_dev' }, { status: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
const envelope = await request.text();
|
const envelope = await request.text();
|
||||||
|
|
||||||
// Sentry envelopes can contain multiple parts separated by newlines
|
// Sentry envelopes can contain multiple parts separated by newlines
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ export async function POST(request: NextRequest) {
|
|||||||
const logger = services.logger.child({ component: 'umami-smart-proxy' });
|
const logger = services.logger.child({ component: 'umami-smart-proxy' });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Prevent 400 Bad Request console noise in local dev
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
return NextResponse.json({ status: 'ignored_in_dev' }, { status: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { type, payload } = body;
|
const { type, payload } = body;
|
||||||
|
|
||||||
|
|||||||
39
check-data.ts
Normal file
39
check-data.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { getPayload } from 'payload';
|
||||||
|
import configPromise from '@payload-config';
|
||||||
|
|
||||||
|
async function checkData() {
|
||||||
|
try {
|
||||||
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
|
||||||
|
const { docs: posts } = await payload.find({ collection: 'posts', limit: 3 });
|
||||||
|
const { docs: products } = await payload.find({ collection: 'products', limit: 3 });
|
||||||
|
const { docs: pages } = await payload.find({ collection: 'pages', limit: 3 });
|
||||||
|
|
||||||
|
const checkDocs = (name: string, docs: any[]) => {
|
||||||
|
console.log(`\n----- ${name.toUpperCase()} -----`);
|
||||||
|
docs.forEach((p) => {
|
||||||
|
console.log(`ID: ${p.id}, Slug: ${p.slug}`);
|
||||||
|
if (Array.isArray(p.content)) {
|
||||||
|
console.log(
|
||||||
|
'Content is ARRAY (Slate format!)',
|
||||||
|
JSON.stringify(p.content).substring(0, 100),
|
||||||
|
);
|
||||||
|
} else if (p.content && p.content.root) {
|
||||||
|
console.log('Content is Lexical format.');
|
||||||
|
} else {
|
||||||
|
console.log('Content is UNKNOWN format.');
|
||||||
|
console.log(JSON.stringify(p.content).substring(0, 100));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
checkDocs('posts', posts);
|
||||||
|
checkDocs('products', products);
|
||||||
|
checkDocs('pages', pages);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkData();
|
||||||
213
components/PayloadRichText.tsx
Normal file
213
components/PayloadRichText.tsx
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
import { RichText } from '@payloadcms/richtext-lexical/react';
|
||||||
|
import type { JSXConverters } from '@payloadcms/richtext-lexical/react';
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
// Import all custom React components that were previously mapped via MDX
|
||||||
|
import StickyNarrative from '@/components/blog/StickyNarrative';
|
||||||
|
import ComparisonGrid from '@/components/blog/ComparisonGrid';
|
||||||
|
import VisualLinkPreview from '@/components/blog/VisualLinkPreview';
|
||||||
|
import TechnicalGrid from '@/components/blog/TechnicalGrid';
|
||||||
|
import HighlightBox from '@/components/blog/HighlightBox';
|
||||||
|
import AnimatedImage from '@/components/blog/AnimatedImage';
|
||||||
|
import ChatBubble from '@/components/blog/ChatBubble';
|
||||||
|
import PowerCTA from '@/components/blog/PowerCTA';
|
||||||
|
import { Callout } from '@/components/ui/Callout';
|
||||||
|
import Stats from '@/components/blog/Stats';
|
||||||
|
import SplitHeading from '@/components/blog/SplitHeading';
|
||||||
|
import ProductTabs from '@/components/ProductTabs';
|
||||||
|
import ProductTechnicalData from '@/components/ProductTechnicalData';
|
||||||
|
const jsxConverters: JSXConverters = {
|
||||||
|
blocks: {
|
||||||
|
// Map the custom Payload Blocks created in src/payload/blocks to their React components
|
||||||
|
// Payload Lexical exposes blocks using the 'block-[slug]' pattern
|
||||||
|
stickyNarrative: ({ node }: any) => (
|
||||||
|
<StickyNarrative title={node.fields.title} items={node.fields.items} />
|
||||||
|
),
|
||||||
|
'block-stickyNarrative': ({ node }: any) => (
|
||||||
|
<StickyNarrative title={node.fields.title} items={node.fields.items} />
|
||||||
|
),
|
||||||
|
comparisonGrid: ({ node }: any) => (
|
||||||
|
<ComparisonGrid
|
||||||
|
title={node.fields.title}
|
||||||
|
leftLabel={node.fields.leftLabel}
|
||||||
|
rightLabel={node.fields.rightLabel}
|
||||||
|
items={node.fields.items}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
'block-comparisonGrid': ({ node }: any) => (
|
||||||
|
<ComparisonGrid
|
||||||
|
title={node.fields.title}
|
||||||
|
leftLabel={node.fields.leftLabel}
|
||||||
|
rightLabel={node.fields.rightLabel}
|
||||||
|
items={node.fields.items}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
visualLinkPreview: ({ node }: any) => (
|
||||||
|
<VisualLinkPreview
|
||||||
|
url={node.fields.url}
|
||||||
|
title={node.fields.title}
|
||||||
|
summary={node.fields.summary}
|
||||||
|
image={node.fields.image?.sizes?.card?.url || node.fields.image?.url}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
'block-visualLinkPreview': ({ node }: any) => (
|
||||||
|
<VisualLinkPreview
|
||||||
|
url={node.fields.url}
|
||||||
|
title={node.fields.title}
|
||||||
|
summary={node.fields.summary}
|
||||||
|
image={node.fields.image?.sizes?.card?.url || node.fields.image?.url}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
technicalGrid: ({ node }: any) => (
|
||||||
|
<TechnicalGrid title={node.fields.title} items={node.fields.items} />
|
||||||
|
),
|
||||||
|
'block-technicalGrid': ({ node }: any) => {
|
||||||
|
console.log('[PayloadRichText] Rendering block-technicalGrid:', node.fields.title);
|
||||||
|
return <TechnicalGrid title={node.fields.title} items={node.fields.items} />;
|
||||||
|
},
|
||||||
|
highlightBox: ({ node }: any) => (
|
||||||
|
<HighlightBox title={node.fields.title} color={node.fields.color}>
|
||||||
|
<RichText data={node.fields.content} converters={jsxConverters} />
|
||||||
|
</HighlightBox>
|
||||||
|
),
|
||||||
|
'block-highlightBox': ({ node }: any) => (
|
||||||
|
<HighlightBox title={node.fields.title} color={node.fields.color}>
|
||||||
|
<RichText data={node.fields.content} converters={jsxConverters} />
|
||||||
|
</HighlightBox>
|
||||||
|
),
|
||||||
|
animatedImage: ({ node }: any) => (
|
||||||
|
<AnimatedImage
|
||||||
|
src={node.fields.src}
|
||||||
|
alt={node.fields.alt}
|
||||||
|
width={node.fields.width}
|
||||||
|
height={node.fields.height}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
'block-animatedImage': ({ node }: any) => (
|
||||||
|
<AnimatedImage
|
||||||
|
src={node.fields.src}
|
||||||
|
alt={node.fields.alt}
|
||||||
|
width={node.fields.width}
|
||||||
|
height={node.fields.height}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
chatBubble: ({ node }: any) => (
|
||||||
|
<ChatBubble
|
||||||
|
author={node.fields.author}
|
||||||
|
avatar={node.fields.avatar}
|
||||||
|
role={node.fields.role}
|
||||||
|
align={node.fields.align}
|
||||||
|
>
|
||||||
|
<RichText data={node.fields.content} converters={jsxConverters} />
|
||||||
|
</ChatBubble>
|
||||||
|
),
|
||||||
|
'block-chatBubble': ({ node }: any) => (
|
||||||
|
<ChatBubble
|
||||||
|
author={node.fields.author}
|
||||||
|
avatar={node.fields.avatar}
|
||||||
|
role={node.fields.role}
|
||||||
|
align={node.fields.align}
|
||||||
|
>
|
||||||
|
<RichText data={node.fields.content} converters={jsxConverters} />
|
||||||
|
</ChatBubble>
|
||||||
|
),
|
||||||
|
powerCTA: ({ node }: any) => <PowerCTA locale={node.fields.locale} />,
|
||||||
|
'block-powerCTA': ({ node }: any) => <PowerCTA locale={node.fields.locale} />,
|
||||||
|
callout: ({ node }: any) => (
|
||||||
|
<Callout type={node.fields.type} title={node.fields.title}>
|
||||||
|
<RichText data={node.fields.content} converters={jsxConverters} />
|
||||||
|
</Callout>
|
||||||
|
),
|
||||||
|
'block-callout': ({ node }: any) => (
|
||||||
|
<Callout type={node.fields.type} title={node.fields.title}>
|
||||||
|
<RichText data={node.fields.content} converters={jsxConverters} />
|
||||||
|
</Callout>
|
||||||
|
),
|
||||||
|
stats: ({ node }: any) => <Stats stats={node.fields.stats} />,
|
||||||
|
'block-stats': ({ node }: any) => <Stats stats={node.fields.stats} />,
|
||||||
|
splitHeading: ({ node }: any) => (
|
||||||
|
<SplitHeading id={node.fields.id} level={node.fields.level}>
|
||||||
|
{node.fields.title}
|
||||||
|
</SplitHeading>
|
||||||
|
),
|
||||||
|
'block-splitHeading': ({ node }: any) => (
|
||||||
|
<SplitHeading id={node.fields.id} level={node.fields.level}>
|
||||||
|
{node.fields.title}
|
||||||
|
</SplitHeading>
|
||||||
|
),
|
||||||
|
productTabs: ({ node }: any) => (
|
||||||
|
<ProductTabs
|
||||||
|
technicalData={
|
||||||
|
<ProductTechnicalData
|
||||||
|
data={{
|
||||||
|
technicalItems: node.fields.technicalItems,
|
||||||
|
voltageTables: node.fields.voltageTables,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<></>
|
||||||
|
</ProductTabs>
|
||||||
|
),
|
||||||
|
'block-productTabs': ({ node }: any) => (
|
||||||
|
<ProductTabs
|
||||||
|
technicalData={
|
||||||
|
<ProductTechnicalData
|
||||||
|
data={{
|
||||||
|
technicalItems: node.fields.technicalItems,
|
||||||
|
voltageTables: node.fields.voltageTables,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<></>
|
||||||
|
</ProductTabs>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
// Custom converter for the Payload "upload" Lexical node (Media collection)
|
||||||
|
// This natively reconstructs Next.js <Image /> tags pointing to the focal-point cropped sizes
|
||||||
|
upload: ({ node }: any) => {
|
||||||
|
// Attempt to extract the highly optimized 'card' generated size from Payload, fallback to raw url
|
||||||
|
let src = node?.value?.sizes?.card?.url || node?.value?.url;
|
||||||
|
const alt = node?.value?.alt || 'Blog Post Media';
|
||||||
|
|
||||||
|
if (!src) return null;
|
||||||
|
|
||||||
|
// Strip legacy imgproxy query parameters (e.g. ?ar=16:9) that crash Next.js 14+ localPatterns
|
||||||
|
if (src.includes('?')) {
|
||||||
|
src = src.split('?')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback dimensions if unmapped or loading from raw
|
||||||
|
const width = node?.value?.sizes?.card?.width || 800;
|
||||||
|
const height = node?.value?.sizes?.card?.height || 600;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<figure className="my-8 md:my-12 relative w-full rounded-2xl md:rounded-[32px] overflow-hidden shadow-xl md:shadow-2xl">
|
||||||
|
<Image
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
className="w-full object-cover transition-transform duration-700 hover:scale-105"
|
||||||
|
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 75vw, 60vw"
|
||||||
|
/>
|
||||||
|
{node?.value?.caption && (
|
||||||
|
<figcaption className="p-4 bg-neutral-dark text-white/80 text-sm text-center italic border-t border-white/10">
|
||||||
|
{node.value.caption}
|
||||||
|
</figcaption>
|
||||||
|
)}
|
||||||
|
</figure>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PayloadRichText({ data }: { data: any }) {
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="article-content max-w-none">
|
||||||
|
<RichText data={data} converters={jsxConverters} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -35,7 +35,7 @@ export default function ProductSidebar({
|
|||||||
<div className="relative w-full aspect-[16/10] mb-6 rounded-2xl overflow-hidden bg-white/5 backdrop-blur-md p-4 border border-white/10 z-10 group">
|
<div className="relative w-full aspect-[16/10] mb-6 rounded-2xl overflow-hidden bg-white/5 backdrop-blur-md p-4 border border-white/10 z-10 group">
|
||||||
<div className="relative w-full h-full transition-transform duration-1000 ease-out group-hover:scale-105">
|
<div className="relative w-full h-full transition-transform duration-1000 ease-out group-hover:scale-105">
|
||||||
<Image
|
<Image
|
||||||
src={productImage}
|
src={productImage.split('?')[0]}
|
||||||
alt={productName}
|
alt={productName}
|
||||||
fill
|
fill
|
||||||
className="object-contain p-2 drop-shadow-[0_20px_30px_rgba(0,0,0,0.4)]"
|
className="object-contain p-2 drop-shadow-[0_20px_30px_rgba(0,0,0,0.4)]"
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export default async function RelatedProducts({
|
|||||||
{product.frontmatter.images?.[0] ? (
|
{product.frontmatter.images?.[0] ? (
|
||||||
<>
|
<>
|
||||||
<Image
|
<Image
|
||||||
src={product.frontmatter.images[0]}
|
src={product.frontmatter.images[0].split('?')[0]}
|
||||||
alt={product.frontmatter.title}
|
alt={product.frontmatter.title}
|
||||||
fill
|
fill
|
||||||
className="object-contain p-4 transition-transform duration-700 group-hover:scale-110 z-10"
|
className="object-contain p-4 transition-transform duration-700 group-hover:scale-110 z-10"
|
||||||
|
|||||||
@@ -29,10 +29,14 @@ export default function TrackedLink({
|
|||||||
const { trackEvent } = useAnalytics();
|
const { trackEvent } = useAnalytics();
|
||||||
|
|
||||||
const handleClick = (e: React.MouseEvent) => {
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
|
try {
|
||||||
trackEvent(eventName, {
|
trackEvent(eventName, {
|
||||||
href,
|
href,
|
||||||
...eventProperties,
|
...eventProperties,
|
||||||
});
|
});
|
||||||
|
} catch (_e) {
|
||||||
|
// Analytics tracking should not block navigation, so we catch and ignore errors.
|
||||||
|
}
|
||||||
if (onClick) onClick();
|
if (onClick) onClick();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export default function PostNavigation({
|
|||||||
{prev.frontmatter.featuredImage ? (
|
{prev.frontmatter.featuredImage ? (
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 bg-cover bg-center transition-transform duration-700 group-hover:scale-110"
|
className="absolute inset-0 bg-cover bg-center transition-transform duration-700 group-hover:scale-110"
|
||||||
style={{ backgroundImage: `url(${prev.frontmatter.featuredImage}?ar=16:9)` }}
|
style={{ backgroundImage: `url(${prev.frontmatter.featuredImage.split('?')[0]})` }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="absolute inset-0 bg-neutral-100" />
|
<div className="absolute inset-0 bg-neutral-100" />
|
||||||
@@ -82,7 +82,7 @@ export default function PostNavigation({
|
|||||||
{next.frontmatter.featuredImage ? (
|
{next.frontmatter.featuredImage ? (
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 bg-cover bg-center transition-transform duration-700 group-hover:scale-110"
|
className="absolute inset-0 bg-cover bg-center transition-transform duration-700 group-hover:scale-110"
|
||||||
style={{ backgroundImage: `url(${next.frontmatter.featuredImage}?ar=16:9)` }}
|
style={{ backgroundImage: `url(${next.frontmatter.featuredImage.split('?')[0]})` }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="absolute inset-0 bg-neutral-100" />
|
<div className="absolute inset-0 bg-neutral-100" />
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default function VisualLinkPreview({ url, title, summary, image }: Visual
|
|||||||
<div className="relative w-full md:w-64 h-48 md:h-auto flex-shrink-0 bg-neutral-50 overflow-hidden">
|
<div className="relative w-full md:w-64 h-48 md:h-auto flex-shrink-0 bg-neutral-50 overflow-hidden">
|
||||||
{image ? (
|
{image ? (
|
||||||
<Image
|
<Image
|
||||||
src={image}
|
src={image.split('?')[0]}
|
||||||
alt={title}
|
alt={title}
|
||||||
fill
|
fill
|
||||||
unoptimized
|
unoptimized
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default async function RecentPosts({ locale }: RecentPostsProps) {
|
|||||||
{post.frontmatter.featuredImage && (
|
{post.frontmatter.featuredImage && (
|
||||||
<div className="relative h-64 overflow-hidden">
|
<div className="relative h-64 overflow-hidden">
|
||||||
<Image
|
<Image
|
||||||
src={`${post.frontmatter.featuredImage}?ar=16:9`}
|
src={post.frontmatter.featuredImage.split('?')[0]}
|
||||||
alt={post.frontmatter.title}
|
alt={post.frontmatter.title}
|
||||||
fill
|
fill
|
||||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
|
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function Button({
|
|||||||
...props
|
...props
|
||||||
}: ButtonProps) {
|
}: ButtonProps) {
|
||||||
const baseStyles =
|
const baseStyles =
|
||||||
'inline-flex items-center justify-center rounded-full font-semibold transition-all duration-500 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none active:scale-95 hover:-translate-y-1 hover:scale-[1.02] relative overflow-hidden group/btn isolate';
|
'inline-flex items-center justify-center whitespace-nowrap rounded-full font-semibold transition-all duration-500 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none active:scale-95 hover:-translate-y-1 hover:scale-[1.02] relative overflow-hidden group/btn isolate';
|
||||||
|
|
||||||
const variants = {
|
const variants = {
|
||||||
primary: 'bg-primary text-white shadow-md hover:shadow-primary/30 hover:shadow-2xl',
|
primary: 'bg-primary text-white shadow-md hover:shadow-primary/30 hover:shadow-2xl',
|
||||||
@@ -45,8 +45,8 @@ export function Button({
|
|||||||
const sizes = {
|
const sizes = {
|
||||||
sm: 'h-9 px-4 text-sm md:text-base',
|
sm: 'h-9 px-4 text-sm md:text-base',
|
||||||
md: 'h-11 px-6 text-base md:text-lg',
|
md: 'h-11 px-6 text-base md:text-lg',
|
||||||
lg: 'h-14 px-8 text-base md:text-lg',
|
lg: 'h-14 px-5 md:px-8 text-base md:text-lg',
|
||||||
xl: 'h-16 px-10 text-lg md:text-xl',
|
xl: 'h-16 px-6 md:px-10 text-lg md:text-xl',
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = cn(baseStyles, variants[variant], sizes[size], className);
|
const styles = cn(baseStyles, variants[variant], sizes[size], className);
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
version: 1
|
|
||||||
directus: 11.14.1
|
|
||||||
vendor: postgres
|
|
||||||
collections:
|
|
||||||
- collection: contact_submissions
|
|
||||||
meta:
|
|
||||||
accountability: all
|
|
||||||
archive_app_filter: true
|
|
||||||
collapse: open
|
|
||||||
collection: contact_submissions
|
|
||||||
color: '#002b49'
|
|
||||||
display_template: '{{name}} | {{email}}'
|
|
||||||
hidden: false
|
|
||||||
icon: contact_mail
|
|
||||||
singleton: false
|
|
||||||
schema:
|
|
||||||
name: contact_submissions
|
|
||||||
fields:
|
|
||||||
- collection: contact_submissions
|
|
||||||
field: id
|
|
||||||
type: uuid
|
|
||||||
meta:
|
|
||||||
collection: contact_submissions
|
|
||||||
field: id
|
|
||||||
hidden: true
|
|
||||||
sort: 1
|
|
||||||
schema:
|
|
||||||
name: id
|
|
||||||
table: contact_submissions
|
|
||||||
data_type: uuid
|
|
||||||
is_nullable: false
|
|
||||||
is_primary_key: true
|
|
||||||
- collection: contact_submissions
|
|
||||||
field: name
|
|
||||||
type: string
|
|
||||||
meta:
|
|
||||||
collection: contact_submissions
|
|
||||||
field: name
|
|
||||||
interface: input
|
|
||||||
sort: 2
|
|
||||||
schema:
|
|
||||||
name: name
|
|
||||||
table: contact_submissions
|
|
||||||
data_type: character varying
|
|
||||||
- collection: contact_submissions
|
|
||||||
field: email
|
|
||||||
type: string
|
|
||||||
meta:
|
|
||||||
collection: contact_submissions
|
|
||||||
field: email
|
|
||||||
interface: input
|
|
||||||
sort: 3
|
|
||||||
schema:
|
|
||||||
name: email
|
|
||||||
table: contact_submissions
|
|
||||||
data_type: character varying
|
|
||||||
- collection: contact_submissions
|
|
||||||
field: message
|
|
||||||
type: text
|
|
||||||
meta:
|
|
||||||
collection: contact_submissions
|
|
||||||
field: message
|
|
||||||
interface: textarea
|
|
||||||
sort: 4
|
|
||||||
schema:
|
|
||||||
name: message
|
|
||||||
table: contact_submissions
|
|
||||||
data_type: text
|
|
||||||
- collection: contact_submissions
|
|
||||||
field: date_created
|
|
||||||
type: timestamp
|
|
||||||
meta:
|
|
||||||
collection: contact_submissions
|
|
||||||
field: date_created
|
|
||||||
interface: datetime
|
|
||||||
readonly: true
|
|
||||||
sort: 5
|
|
||||||
schema:
|
|
||||||
name: date_created
|
|
||||||
table: contact_submissions
|
|
||||||
data_type: timestamp with time zone
|
|
||||||
default_value: CURRENT_TIMESTAMP
|
|
||||||
relations: []
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
version: 1
|
|
||||||
directus: 11.14.1
|
|
||||||
vendor: postgres
|
|
||||||
collections:
|
|
||||||
- collection: contact_submissions
|
|
||||||
meta:
|
|
||||||
accountability: all
|
|
||||||
archive_app_filter: true
|
|
||||||
collapse: open
|
|
||||||
collection: contact_submissions
|
|
||||||
color: '#002b49'
|
|
||||||
display_template: '{{name}} | {{email}}'
|
|
||||||
hidden: false
|
|
||||||
icon: contact_mail
|
|
||||||
singleton: false
|
|
||||||
schema:
|
|
||||||
name: contact_submissions
|
|
||||||
- collection: product_requests
|
|
||||||
meta:
|
|
||||||
accountability: all
|
|
||||||
archive_app_filter: true
|
|
||||||
collapse: open
|
|
||||||
collection: product_requests
|
|
||||||
color: '#002b49'
|
|
||||||
display_template: '{{product_name}} | {{email}}'
|
|
||||||
hidden: false
|
|
||||||
icon: inventory
|
|
||||||
singleton: false
|
|
||||||
schema:
|
|
||||||
name: product_requests
|
|
||||||
- collection: products
|
|
||||||
meta:
|
|
||||||
accountability: all
|
|
||||||
collection: products
|
|
||||||
icon: inventory_2
|
|
||||||
singleton: false
|
|
||||||
schema:
|
|
||||||
name: products
|
|
||||||
- collection: products_translations
|
|
||||||
meta:
|
|
||||||
accountability: all
|
|
||||||
collection: products_translations
|
|
||||||
hidden: true
|
|
||||||
schema:
|
|
||||||
name: products_translations
|
|
||||||
|
|
||||||
fields:
|
|
||||||
# contact_submissions
|
|
||||||
- collection: contact_submissions
|
|
||||||
field: id
|
|
||||||
type: uuid
|
|
||||||
meta:
|
|
||||||
collection: contact_submissions
|
|
||||||
field: id
|
|
||||||
hidden: true
|
|
||||||
sort: 1
|
|
||||||
schema:
|
|
||||||
name: id
|
|
||||||
table: contact_submissions
|
|
||||||
data_type: uuid
|
|
||||||
is_nullable: false
|
|
||||||
is_primary_key: true
|
|
||||||
- collection: contact_submissions
|
|
||||||
field: name
|
|
||||||
type: string
|
|
||||||
meta:
|
|
||||||
collection: contact_submissions
|
|
||||||
field: name
|
|
||||||
interface: input
|
|
||||||
sort: 2
|
|
||||||
schema:
|
|
||||||
name: name
|
|
||||||
table: contact_submissions
|
|
||||||
data_type: character varying
|
|
||||||
- collection: contact_submissions
|
|
||||||
field: email
|
|
||||||
type: string
|
|
||||||
meta:
|
|
||||||
collection: contact_submissions
|
|
||||||
field: email
|
|
||||||
interface: input
|
|
||||||
sort: 3
|
|
||||||
schema:
|
|
||||||
name: email
|
|
||||||
table: contact_submissions
|
|
||||||
data_type: character varying
|
|
||||||
- collection: contact_submissions
|
|
||||||
field: message
|
|
||||||
type: text
|
|
||||||
meta:
|
|
||||||
collection: contact_submissions
|
|
||||||
field: message
|
|
||||||
interface: textarea
|
|
||||||
sort: 4
|
|
||||||
schema:
|
|
||||||
name: message
|
|
||||||
table: contact_submissions
|
|
||||||
data_type: text
|
|
||||||
- collection: contact_submissions
|
|
||||||
field: date_created
|
|
||||||
type: timestamp
|
|
||||||
meta:
|
|
||||||
collection: contact_submissions
|
|
||||||
field: date_created
|
|
||||||
interface: datetime
|
|
||||||
readonly: true
|
|
||||||
sort: 5
|
|
||||||
schema:
|
|
||||||
name: date_created
|
|
||||||
table: contact_submissions
|
|
||||||
data_type: timestamp with time zone
|
|
||||||
default_value: CURRENT_TIMESTAMP
|
|
||||||
|
|
||||||
# product_requests
|
|
||||||
- collection: product_requests
|
|
||||||
field: id
|
|
||||||
type: uuid
|
|
||||||
meta:
|
|
||||||
collection: product_requests
|
|
||||||
field: id
|
|
||||||
hidden: true
|
|
||||||
sort: 1
|
|
||||||
schema:
|
|
||||||
name: id
|
|
||||||
table: product_requests
|
|
||||||
data_type: uuid
|
|
||||||
is_nullable: false
|
|
||||||
is_primary_key: true
|
|
||||||
- collection: product_requests
|
|
||||||
field: product_name
|
|
||||||
type: string
|
|
||||||
meta:
|
|
||||||
collection: product_requests
|
|
||||||
field: product_name
|
|
||||||
interface: input
|
|
||||||
sort: 2
|
|
||||||
schema:
|
|
||||||
name: product_name
|
|
||||||
table: product_requests
|
|
||||||
data_type: character varying
|
|
||||||
- collection: product_requests
|
|
||||||
field: email
|
|
||||||
type: string
|
|
||||||
meta:
|
|
||||||
collection: product_requests
|
|
||||||
field: email
|
|
||||||
interface: input
|
|
||||||
sort: 3
|
|
||||||
schema:
|
|
||||||
name: email
|
|
||||||
table: product_requests
|
|
||||||
data_type: character varying
|
|
||||||
- collection: product_requests
|
|
||||||
field: message
|
|
||||||
type: text
|
|
||||||
meta:
|
|
||||||
collection: product_requests
|
|
||||||
field: message
|
|
||||||
interface: textarea
|
|
||||||
sort: 4
|
|
||||||
schema:
|
|
||||||
name: message
|
|
||||||
table: product_requests
|
|
||||||
data_type: text
|
|
||||||
- collection: product_requests
|
|
||||||
field: date_created
|
|
||||||
type: timestamp
|
|
||||||
meta:
|
|
||||||
collection: product_requests
|
|
||||||
field: date_created
|
|
||||||
interface: datetime
|
|
||||||
readonly: true
|
|
||||||
sort: 5
|
|
||||||
schema:
|
|
||||||
name: date_created
|
|
||||||
table: product_requests
|
|
||||||
data_type: timestamp with time zone
|
|
||||||
default_value: CURRENT_TIMESTAMP
|
|
||||||
|
|
||||||
# products
|
|
||||||
- collection: products
|
|
||||||
field: id
|
|
||||||
type: uuid
|
|
||||||
meta:
|
|
||||||
collection: products
|
|
||||||
field: id
|
|
||||||
hidden: true
|
|
||||||
sort: 1
|
|
||||||
schema:
|
|
||||||
name: id
|
|
||||||
table: products
|
|
||||||
data_type: uuid
|
|
||||||
is_nullable: false
|
|
||||||
is_primary_key: true
|
|
||||||
|
|
||||||
# products_translations
|
|
||||||
- collection: products_translations
|
|
||||||
field: id
|
|
||||||
type: integer
|
|
||||||
meta:
|
|
||||||
collection: products_translations
|
|
||||||
field: id
|
|
||||||
hidden: true
|
|
||||||
schema:
|
|
||||||
name: id
|
|
||||||
table: products_translations
|
|
||||||
data_type: integer
|
|
||||||
is_primary_key: true
|
|
||||||
has_auto_increment: true
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
systemFields:
|
|
||||||
- collection: directus_activity
|
|
||||||
field: timestamp
|
|
||||||
schema:
|
|
||||||
is_indexed: true
|
|
||||||
- collection: directus_revisions
|
|
||||||
field: activity
|
|
||||||
schema:
|
|
||||||
is_indexed: true
|
|
||||||
- collection: directus_revisions
|
|
||||||
field: parent
|
|
||||||
schema:
|
|
||||||
is_indexed: true
|
|
||||||
|
|
||||||
relations: []
|
|
||||||
|
|
||||||
|
|
||||||
65
docker-compose.dev.yml
Normal file
65
docker-compose.dev.yml
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
services:
|
||||||
|
klz-app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
working_dir: /app
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
infra:
|
||||||
|
aliases:
|
||||||
|
- klz.localhost
|
||||||
|
env_file:
|
||||||
|
- ${ENV_FILE:-.env}
|
||||||
|
environment:
|
||||||
|
NODE_ENV: development
|
||||||
|
NEXT_TELEMETRY_DISABLED: "1"
|
||||||
|
POSTGRES_URI: postgres://${DIRECTUS_DB_USER:-payload}:${DIRECTUS_DB_PASSWORD:-120in09oenaoinsd9iaidon}@klz-db:5432/${DIRECTUS_DB_NAME:-payload}
|
||||||
|
PAYLOAD_SECRET: ${DIRECTUS_SECRET:-fallback-secret-for-dev}
|
||||||
|
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||||
|
CI: "true" # Prevents some interactive prompts during install
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
# Named volumes for persistence & performance (mintel.me standard)
|
||||||
|
- klz_node_modules:/app/node_modules
|
||||||
|
- klz_next_cache:/app/.next
|
||||||
|
- klz_pnpm_store:/pnpm
|
||||||
|
- ~/.npmrc:/root/.npmrc:ro
|
||||||
|
command: >
|
||||||
|
sh -c "pnpm install && pnpm next dev --turbo --hostname 0.0.0.0"
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.services.${PROJECT_NAME:-klz}-app-svc.loadbalancer.server.port=3000"
|
||||||
|
- "traefik.docker.network=infra"
|
||||||
|
- "caddy=http://${TRAEFIK_HOST:-klz.localhost}"
|
||||||
|
- "caddy.reverse_proxy={{upstreams 3000}}"
|
||||||
|
|
||||||
|
klz-db:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- ${ENV_FILE:-.env}
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${DIRECTUS_DB_NAME:-payload}
|
||||||
|
POSTGRES_USER: ${DIRECTUS_DB_USER:-payload}
|
||||||
|
POSTGRES_PASSWORD: ${DIRECTUS_DB_PASSWORD:-120in09oenaoinsd9iaidon}
|
||||||
|
volumes:
|
||||||
|
- klz_db_data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
ports:
|
||||||
|
- "54322:5432"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: ${PROJECT_NAME:-klz-cables}-internal
|
||||||
|
infra:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
klz_db_data:
|
||||||
|
external: false
|
||||||
|
klz_node_modules:
|
||||||
|
klz_next_cache:
|
||||||
|
klz_pnpm_store:
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
services:
|
|
||||||
klz-app:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
target: development
|
|
||||||
volumes:
|
|
||||||
- .:/app
|
|
||||||
- /app/node_modules
|
|
||||||
- /app/.next
|
|
||||||
environment:
|
|
||||||
- WATCHPACK_POLLING=true # Useful for Docker volume mounting issues on some systems
|
|
||||||
restart: "no"
|
|
||||||
container_name: klz-app-dev
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
# Clear any production middlewares/headers redirect
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-web.middlewares="
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-web.rule=Host(`${TRAEFIK_HOST:-klz.localhost}`)"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-web.entrypoints=web"
|
|
||||||
# Configure main router for local HTTP without auth
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}.rule=Host(`${TRAEFIK_HOST:-klz.localhost}`)"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}.entrypoints=web"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}.middlewares="
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}.tls=false"
|
|
||||||
- "traefik.docker.network=infra"
|
|
||||||
|
|
||||||
klz-cms:
|
|
||||||
container_name: klz-cms-dev
|
|
||||||
restart: "no"
|
|
||||||
ports:
|
|
||||||
- "8055:8055"
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-cms.rule=Host(`${DIRECTUS_HOST:-cms.klz.localhost}`)"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-cms.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-cms.service=${PROJECT_NAME:-klz-cables}-cms"
|
|
||||||
- "traefik.http.services.${PROJECT_NAME:-klz-cables}-cms.loadbalancer.server.port=8055"
|
|
||||||
- "traefik.docker.network=infra"
|
|
||||||
|
|
||||||
klz-db:
|
|
||||||
restart: "no"
|
|
||||||
|
|
||||||
klz-gatekeeper:
|
|
||||||
restart: "no"
|
|
||||||
@@ -1,12 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
klz-app:
|
klz-app:
|
||||||
build:
|
image: registry.infra.mintel.me/mintel/klz-2026:${IMAGE_TAG:-latest}
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
args:
|
|
||||||
NEXT_PUBLIC_BASE_URL: ${NEXT_PUBLIC_BASE_URL}
|
|
||||||
DIRECTUS_URL: "${DIRECTUS_URL}"
|
|
||||||
image: registry.infra.mintel.me/mintel/klz-cables.com:${IMAGE_TAG:-latest}
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
@@ -15,8 +9,6 @@ services:
|
|||||||
- klz.localhost
|
- klz.localhost
|
||||||
env_file:
|
env_file:
|
||||||
- ${ENV_FILE:-.env}
|
- ${ENV_FILE:-.env}
|
||||||
environment:
|
|
||||||
IMGPROXY_URL: ${IMGPROXY_URL:-http://klz-imgproxy:8080}
|
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
# HTTP ⇒ HTTPS redirect
|
# HTTP ⇒ HTTPS redirect
|
||||||
@@ -31,43 +23,23 @@ services:
|
|||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}.service=${PROJECT_NAME:-klz}-app-svc"
|
- "traefik.http.routers.${PROJECT_NAME:-klz}.service=${PROJECT_NAME:-klz}-app-svc"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}.middlewares=${AUTH_MIDDLEWARE:-klz-ratelimit,klz-forward,klz-compress}"
|
- "traefik.http.routers.${PROJECT_NAME:-klz}.middlewares=${AUTH_MIDDLEWARE:-klz-ratelimit,klz-forward,klz-compress}"
|
||||||
|
|
||||||
# Public Router (Whitelist for OG Images, Sitemaps, Health)
|
# Public Router (Whitelist)
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.rule=(${TRAEFIK_HOST_RULE:-Host(`${TRAEFIK_HOST:-klz-cables.com}`)}) && (PathPrefix(`/health`) || PathPrefix(`/sitemap.xml`) || PathPrefix(`/robots.txt`) || PathPrefix(`/manifest.webmanifest`) || PathPrefix(`/api/og`) || PathPrefix(`/de/api/og`) || PathPrefix(`/en/api/og`) || PathPrefix(`/logo-white.svg`) || PathPrefix(`/icon-white.svg`) || PathPrefix(`/opengraph-image`) || PathPrefix(`/de/opengraph-image`) || PathPrefix(`/en/opengraph-image`) || PathPrefix(`/blog/opengraph-image`) || PathPrefix(`/de/blog/opengraph-image`) || PathPrefix(`/en/blog/opengraph-image`) || PathRegexp(`^/sitemap(-[0-9]+)?\\.xml$`) || PathRegexp(`.*\\.(svg|png|jpg|jpeg|gif|webp|ico|webm|mp4|map)$`))"
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.rule=(${TRAEFIK_HOST_RULE:-Host(`${TRAEFIK_HOST:-klz-cables.com}`)}) && (PathPrefix(`/health`) || PathPrefix(`/sitemap.xml`) || PathPrefix(`/robots.txt`) || PathPrefix(`/manifest.webmanifest`) || PathPrefix(`/api/og`) || PathRegexp(`.*opengraph-image.*`) || PathRegexp(`^/sitemap(-[0-9]+)?\\.xml$`))"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.tls=${TRAEFIK_TLS:-false}"
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.tls=${TRAEFIK_TLS:-false}"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.service=${PROJECT_NAME:-klz}-app-svc"
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.service=${PROJECT_NAME:-klz}-app-svc"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.middlewares=${AUTH_MIDDLEWARE_UNPROTECTED:-klz-ratelimit,klz-forward,klz-compress}"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.priority=2000"
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.priority=2000"
|
||||||
|
|
||||||
- "traefik.http.services.${PROJECT_NAME:-klz}-app-svc.loadbalancer.server.scheme=http"
|
|
||||||
- "traefik.http.services.${PROJECT_NAME:-klz}-app-svc.loadbalancer.server.port=3000"
|
- "traefik.http.services.${PROJECT_NAME:-klz}-app-svc.loadbalancer.server.port=3000"
|
||||||
- "traefik.docker.network=infra"
|
- "traefik.docker.network=infra"
|
||||||
- "caddy=http://${TRAEFIK_HOST:-klz.localhost}"
|
|
||||||
- "caddy.reverse_proxy={{upstreams 3000}}"
|
|
||||||
|
|
||||||
# Middleware Definitions
|
# Middlewares
|
||||||
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-compress.compress=true"
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-compress.compress=true"
|
||||||
|
|
||||||
# Forwarded Headers
|
|
||||||
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-forward.headers.customrequestheaders.X-Forwarded-Proto=https"
|
|
||||||
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-forward.headers.customrequestheaders.X-Forwarded-Ssl=on"
|
|
||||||
|
|
||||||
# Authentication Middleware (ForwardAuth)
|
|
||||||
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-auth.forwardauth.address=http://${PROJECT_NAME:-klz}-gatekeeper:3000/gatekeeper/api/verify"
|
|
||||||
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-auth.forwardauth.trustForwardHeader=true"
|
|
||||||
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-auth.forwardauth.authRequestHeaders=X-Forwarded-Host,X-Forwarded-Proto,X-Forwarded-For,Cookie"
|
|
||||||
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-auth.forwardauth.authResponseHeaders=X-Auth-User"
|
|
||||||
|
|
||||||
# Rate Limit Middleware
|
|
||||||
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-ratelimit.ratelimit.average=100"
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-ratelimit.ratelimit.average=100"
|
||||||
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-ratelimit.ratelimit.burst=50"
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-ratelimit.ratelimit.burst=50"
|
||||||
healthcheck:
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-forward.headers.customrequestheaders.X-Forwarded-Proto=https"
|
||||||
test: [ "CMD", "curl", "-f", "http://127.0.0.1:3000/health" ]
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-forward.headers.customrequestheaders.X-Forwarded-Ssl=on"
|
||||||
interval: 15s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 45s
|
|
||||||
|
|
||||||
klz-gatekeeper:
|
klz-gatekeeper:
|
||||||
profiles: [ "gatekeeper" ]
|
profiles: [ "gatekeeper" ]
|
||||||
@@ -81,125 +53,26 @@ services:
|
|||||||
- ${ENV_FILE:-.env}
|
- ${ENV_FILE:-.env}
|
||||||
environment:
|
environment:
|
||||||
PORT: 3000
|
PORT: 3000
|
||||||
PROJECT_NAME: ${PROJECT_NAME:-KLZ Cables}
|
|
||||||
PROJECT_COLOR: "#82ed20"
|
|
||||||
COOKIE_DOMAIN: ${COOKIE_DOMAIN}
|
|
||||||
AUTH_COOKIE_NAME: ${AUTH_COOKIE_NAME:-klz_gatekeeper_session}
|
|
||||||
GATEKEEPER_PASSWORD: ${GATEKEEPER_PASSWORD}
|
|
||||||
NEXT_PUBLIC_BASE_URL: ${GATEKEEPER_ORIGIN}
|
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.docker.network=infra"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-gatekeeper.rule=(Host(`${TRAEFIK_HOST:-testing.klz-cables.com}`) && PathPrefix(`/gatekeeper`))"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-gatekeeper.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-gatekeeper.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-gatekeeper.tls=${TRAEFIK_TLS:-false}"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-gatekeeper.service=${PROJECT_NAME:-klz}-gatekeeper-svc"
|
|
||||||
- "traefik.http.services.${PROJECT_NAME:-klz}-gatekeeper-svc.loadbalancer.server.port=3000"
|
- "traefik.http.services.${PROJECT_NAME:-klz}-gatekeeper-svc.loadbalancer.server.port=3000"
|
||||||
- "traefik.docker.network=infra"
|
- "traefik.docker.network=infra"
|
||||||
|
|
||||||
klz-cms:
|
|
||||||
image: registry.infra.mintel.me/mintel/directus:latest
|
|
||||||
restart: unless-stopped
|
|
||||||
command: [ "node", "cli.js", "start" ]
|
|
||||||
env_file:
|
|
||||||
- ${ENV_FILE:-.env}
|
|
||||||
environment:
|
|
||||||
KEY: ${DIRECTUS_KEY}
|
|
||||||
SECRET: ${DIRECTUS_SECRET}
|
|
||||||
ADMIN_EMAIL: ${DIRECTUS_ADMIN_EMAIL}
|
|
||||||
ADMIN_PASSWORD: ${DIRECTUS_ADMIN_PASSWORD}
|
|
||||||
DB_CLIENT: 'pg'
|
|
||||||
DB_HOST: 'klz-db'
|
|
||||||
DB_PORT: '5432'
|
|
||||||
DB_DATABASE: ${DIRECTUS_DB_NAME:-directus}
|
|
||||||
DB_USER: ${DIRECTUS_DB_USER:-directus}
|
|
||||||
DB_PASSWORD: ${DIRECTUS_DB_PASSWORD:-120in09oenaoinsd9iaidon}
|
|
||||||
WEBSOCKETS_ENABLED: 'true'
|
|
||||||
PUBLIC_URL: ${DIRECTUS_URL:-https://cms.klz-cables.com}
|
|
||||||
HOST: '0.0.0.0'
|
|
||||||
networks:
|
|
||||||
- default
|
|
||||||
- infra
|
|
||||||
volumes:
|
|
||||||
- ./directus/uploads:/directus/uploads
|
|
||||||
- ./directus/extensions:/directus/extensions
|
|
||||||
- ./directus/schema:/directus/schema
|
|
||||||
- ./directus/migrations:/directus/migrations
|
|
||||||
healthcheck:
|
|
||||||
disable: true
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-cms.rule=Host(`${DIRECTUS_HOST:-cms.klz-cables.com}`)"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-cms.entrypoints=websecure"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-cms.priority=5000"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-cms.tls=true"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-cms.tls.certresolver=le"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-cms.service=${PROJECT_NAME:-klz}-cms-svc"
|
|
||||||
- "traefik.http.services.${PROJECT_NAME:-klz}-cms-svc.loadbalancer.server.port=8055"
|
|
||||||
- "traefik.docker.network=infra"
|
|
||||||
- "caddy=http://${DIRECTUS_HOST:-cms.klz-cables.com}"
|
|
||||||
- "caddy.reverse_proxy={{upstreams 8055}}"
|
|
||||||
klz-db:
|
klz-db:
|
||||||
image: postgres:15-alpine
|
image: postgres:15-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file:
|
env_file:
|
||||||
- ${ENV_FILE:-.env}
|
- ${ENV_FILE:-.env}
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: ${DIRECTUS_DB_NAME:-directus}
|
POSTGRES_DB: ${DIRECTUS_DB_NAME:-payload}
|
||||||
POSTGRES_USER: ${DIRECTUS_DB_USER:-directus}
|
POSTGRES_USER: ${DIRECTUS_DB_USER:-payload}
|
||||||
POSTGRES_PASSWORD: ${DIRECTUS_DB_PASSWORD:-120in09oenaoinsd9iaidon}
|
POSTGRES_PASSWORD: ${DIRECTUS_DB_PASSWORD:-120in09oenaoinsd9iaidon}
|
||||||
volumes:
|
volumes:
|
||||||
- directus-db-data:/var/lib/postgresql/data
|
- klz_db_data:/var/lib/postgresql/data
|
||||||
networks:
|
networks:
|
||||||
- default
|
- default
|
||||||
|
ports:
|
||||||
klz-imgproxy:
|
- "54322:5432"
|
||||||
build:
|
|
||||||
context: ../at-mintel
|
|
||||||
dockerfile: apps/image-service/Dockerfile
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- default
|
|
||||||
- infra
|
|
||||||
extra_hosts:
|
|
||||||
- "klz.localhost:host-gateway"
|
|
||||||
- "cms.klz.localhost:host-gateway"
|
|
||||||
- "host.docker.internal:host-gateway"
|
|
||||||
environment:
|
|
||||||
OPENROUTER_API_KEY: ${OPENROUTER_API_KEY}
|
|
||||||
# explicitly map localhost, production and staging to bypass gatekeeper
|
|
||||||
IMGPROXY_URL_MAPPING: "https://staging.klz-cables.com:http://klz-app:3000,https://klz-cables.com:http://klz-app:3000,https://${TRAEFIK_HOST:-klz.localhost}:http://klz-app:3000,${DIRECTUS_URL}:http://klz-cms:8055,https://cms.klz-cables.com:http://klz-cms:8055"
|
|
||||||
IMGPROXY_LOG_LEVEL: debug
|
|
||||||
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
# Existing Local HTTP Router
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-imgproxy.rule=Host(`img.${TRAEFIK_HOST:-klz.localhost}`)"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-imgproxy.entrypoints=web"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-imgproxy.service=${PROJECT_NAME:-klz}-imgproxy-svc"
|
|
||||||
|
|
||||||
# NEW: Direct Public Staging Router for /_img (Bypasses Next.js rewrites)
|
|
||||||
# This fixes the Next.js URL-decoding bug on dynamic image proxy paths
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-img.rule=(Host(`${TRAEFIK_HOST:-klz.localhost}`) || Host(`staging.klz-cables.com`) || Host(`testing.klz-cables.com`)) && PathPrefix(`/_img`)"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-img.priority=99999"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-img.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-img.tls=${TRAEFIK_TLS:-false}"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-img.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-img.service=${PROJECT_NAME:-klz}-imgproxy-svc"
|
|
||||||
- "traefik.http.services.${PROJECT_NAME:-klz}-imgproxy-svc.loadbalancer.server.port=8080"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-img.middlewares=${PROJECT_NAME:-klz}-img-strip"
|
|
||||||
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-img-strip.stripprefix.prefixes=/_img"
|
|
||||||
# HTTPS router (staging/prod)
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-imgproxy-secure.rule=Host(`img.${TRAEFIK_HOST:-klz.localhost}`)"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-imgproxy-secure.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-imgproxy-secure.tls=${TRAEFIK_TLS:-false}"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-imgproxy-secure.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
|
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-imgproxy-secure.service=${PROJECT_NAME:-klz}-imgproxy-svc"
|
|
||||||
- "traefik.http.services.${PROJECT_NAME:-klz}-imgproxy-svc.loadbalancer.server.port=8080"
|
|
||||||
- "traefik.docker.network=infra"
|
|
||||||
- "caddy=http://img.${TRAEFIK_HOST:-klz.localhost}"
|
|
||||||
- "caddy.reverse_proxy={{upstreams 8080}}"
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
@@ -208,6 +81,5 @@ networks:
|
|||||||
external: true
|
external: true
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
directus-db-data:
|
klz_db_data:
|
||||||
external: true
|
external: false
|
||||||
name: klz-cablescom_directus-db-data
|
|
||||||
|
|||||||
130
lib/blog.ts
130
lib/blog.ts
@@ -1,7 +1,5 @@
|
|||||||
import fs from 'fs';
|
import { getPayload } from 'payload';
|
||||||
import path from 'path';
|
import configPromise from '@payload-config';
|
||||||
import matter from 'gray-matter';
|
|
||||||
import { mapSlugToFileSlug } from './slugs';
|
|
||||||
import { config } from '@/lib/config';
|
import { config } from '@/lib/config';
|
||||||
|
|
||||||
export function extractExcerpt(content: string): string {
|
export function extractExcerpt(content: string): string {
|
||||||
@@ -42,7 +40,7 @@ export interface PostFrontmatter {
|
|||||||
export interface PostMdx {
|
export interface PostMdx {
|
||||||
slug: string;
|
slug: string;
|
||||||
frontmatter: PostFrontmatter;
|
frontmatter: PostFrontmatter;
|
||||||
content: string;
|
content: any; // Mapped to Lexical SerializedEditorState
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPostVisible(post: { frontmatter: { date: string; public?: boolean } }) {
|
export function isPostVisible(post: { frontmatter: { date: string; public?: boolean } }) {
|
||||||
@@ -57,87 +55,81 @@ export function isPostVisible(post: { frontmatter: { date: string; public?: bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getPostBySlug(slug: string, locale: string): Promise<PostMdx | null> {
|
export async function getPostBySlug(slug: string, locale: string): Promise<PostMdx | null> {
|
||||||
// Map translated slug to file slug
|
const payload = await getPayload({ config: configPromise });
|
||||||
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
|
||||||
const postsDir = path.join(process.cwd(), 'data', 'blog', locale);
|
|
||||||
const filePath = path.join(postsDir, `${fileSlug}.mdx`);
|
|
||||||
|
|
||||||
if (!fs.existsSync(filePath)) {
|
const { docs } = await payload.find({
|
||||||
return null;
|
collection: 'posts',
|
||||||
}
|
where: {
|
||||||
|
slug: { equals: slug },
|
||||||
|
locale: { equals: locale },
|
||||||
|
},
|
||||||
|
draft: process.env.NODE_ENV === 'development',
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
if (!docs || docs.length === 0) return null;
|
||||||
const { data, content } = matter(fileContent);
|
|
||||||
|
|
||||||
const postInfo = {
|
const doc = docs[0];
|
||||||
slug: fileSlug,
|
|
||||||
|
return {
|
||||||
|
slug: doc.slug,
|
||||||
frontmatter: {
|
frontmatter: {
|
||||||
...data,
|
title: doc.title,
|
||||||
excerpt: data.excerpt || extractExcerpt(content),
|
date: doc.date,
|
||||||
|
excerpt: doc.excerpt || '',
|
||||||
|
category: doc.category || '',
|
||||||
|
locale: doc.locale,
|
||||||
|
featuredImage:
|
||||||
|
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
||||||
|
? doc.featuredImage.sizes?.card?.url || doc.featuredImage.url
|
||||||
|
: null,
|
||||||
|
public: doc._status === 'published',
|
||||||
} as PostFrontmatter,
|
} as PostFrontmatter,
|
||||||
content,
|
content: doc.content as any, // Native Lexical Editor State
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isPostVisible(postInfo)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return postInfo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllPosts(locale: string): Promise<PostMdx[]> {
|
export async function getAllPosts(locale: string): Promise<PostMdx[]> {
|
||||||
const postsDir = path.join(process.cwd(), 'data', 'blog', locale);
|
const payload = await getPayload({ config: configPromise });
|
||||||
if (!fs.existsSync(postsDir)) return [];
|
// Query only published posts (access checks applied automatically by Payload!)
|
||||||
|
const { docs } = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
where: {
|
||||||
|
locale: {
|
||||||
|
equals: locale,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sort: '-date',
|
||||||
|
draft: process.env.NODE_ENV === 'development', // Includes Drafts if running locally
|
||||||
|
limit: 100,
|
||||||
|
});
|
||||||
|
|
||||||
const files = fs.readdirSync(postsDir);
|
return docs.map((doc) => {
|
||||||
const posts = files
|
|
||||||
.filter((file) => file.endsWith('.mdx'))
|
|
||||||
.map((file) => {
|
|
||||||
const filePath = path.join(postsDir, file);
|
|
||||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
||||||
const { data, content } = matter(fileContent);
|
|
||||||
return {
|
return {
|
||||||
slug: file.replace(/\.mdx$/, ''),
|
slug: doc.slug,
|
||||||
frontmatter: {
|
frontmatter: {
|
||||||
...data,
|
title: doc.title,
|
||||||
excerpt: data.excerpt || extractExcerpt(content),
|
date: doc.date,
|
||||||
|
excerpt: doc.excerpt || '',
|
||||||
|
category: doc.category || '',
|
||||||
|
locale: doc.locale,
|
||||||
|
featuredImage:
|
||||||
|
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
||||||
|
? doc.featuredImage.sizes?.card?.url || doc.featuredImage.url
|
||||||
|
: null,
|
||||||
} as PostFrontmatter,
|
} as PostFrontmatter,
|
||||||
content,
|
// Pass the Lexical content object rather than raw markdown string
|
||||||
|
content: doc.content as any,
|
||||||
};
|
};
|
||||||
})
|
});
|
||||||
.filter(isPostVisible)
|
|
||||||
.sort(
|
|
||||||
(a, b) => new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return posts;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllPostsMetadata(locale: string): Promise<Partial<PostMdx>[]> {
|
export async function getAllPostsMetadata(locale: string): Promise<Partial<PostMdx>[]> {
|
||||||
const postsDir = path.join(process.cwd(), 'data', 'blog', locale);
|
const posts = await getAllPosts(locale);
|
||||||
if (!fs.existsSync(postsDir)) return [];
|
return posts.map((p) => ({
|
||||||
|
slug: p.slug,
|
||||||
const files = fs.readdirSync(postsDir);
|
frontmatter: p.frontmatter,
|
||||||
return files
|
}));
|
||||||
.filter((file) => file.endsWith('.mdx'))
|
|
||||||
.map((file) => {
|
|
||||||
const filePath = path.join(postsDir, file);
|
|
||||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
||||||
const { data } = matter(fileContent);
|
|
||||||
return {
|
|
||||||
slug: file.replace(/\.mdx$/, ''),
|
|
||||||
frontmatter: {
|
|
||||||
...data,
|
|
||||||
excerpt: data.excerpt || extractExcerpt(fileContent.replace(/^---[\s\S]*?---/, '')),
|
|
||||||
} as PostFrontmatter,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter(isPostVisible)
|
|
||||||
.sort(
|
|
||||||
(a, b) =>
|
|
||||||
new Date(b.frontmatter.date as string).getTime() -
|
|
||||||
new Date(a.frontmatter.date as string).getTime(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAdjacentPosts(
|
export async function getAdjacentPosts(
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ let memoizedConfig: ReturnType<typeof createConfig> | undefined;
|
|||||||
function createConfig() {
|
function createConfig() {
|
||||||
const env = getRawEnv();
|
const env = getRawEnv();
|
||||||
|
|
||||||
const target = env.NEXT_PUBLIC_TARGET || env.TARGET;
|
const target =
|
||||||
|
env.NEXT_PUBLIC_TARGET ||
|
||||||
|
env.TARGET ||
|
||||||
|
(env.NODE_ENV === 'development' ? 'development' : 'production');
|
||||||
|
|
||||||
console.log('[Config] Initializing Toggles:', {
|
console.log('[Config] Initializing Toggles:', {
|
||||||
feedbackEnabled: env.NEXT_PUBLIC_FEEDBACK_ENABLED,
|
feedbackEnabled: env.NEXT_PUBLIC_FEEDBACK_ENABLED,
|
||||||
@@ -65,17 +68,9 @@ function createConfig() {
|
|||||||
from: env.MAIL_FROM,
|
from: env.MAIL_FROM,
|
||||||
recipients: env.MAIL_RECIPIENTS,
|
recipients: env.MAIL_RECIPIENTS,
|
||||||
},
|
},
|
||||||
directus: {
|
|
||||||
url: env.DIRECTUS_URL,
|
|
||||||
adminEmail: env.DIRECTUS_ADMIN_EMAIL,
|
|
||||||
password: env.DIRECTUS_ADMIN_PASSWORD,
|
|
||||||
token: env.DIRECTUS_API_TOKEN,
|
|
||||||
internalUrl: env.INTERNAL_DIRECTUS_URL,
|
|
||||||
proxyPath: '/cms',
|
|
||||||
},
|
|
||||||
infraCMS: {
|
infraCMS: {
|
||||||
url: env.INFRA_DIRECTUS_URL || env.DIRECTUS_URL,
|
url: env.INFRA_DIRECTUS_URL,
|
||||||
token: env.INFRA_DIRECTUS_TOKEN || env.DIRECTUS_API_TOKEN,
|
token: env.INFRA_DIRECTUS_TOKEN,
|
||||||
},
|
},
|
||||||
notifications: {
|
notifications: {
|
||||||
gotify: {
|
gotify: {
|
||||||
@@ -139,9 +134,6 @@ export const config = {
|
|||||||
get mail() {
|
get mail() {
|
||||||
return getConfig().mail;
|
return getConfig().mail;
|
||||||
},
|
},
|
||||||
get directus() {
|
|
||||||
return getConfig().directus;
|
|
||||||
},
|
|
||||||
get notifications() {
|
get notifications() {
|
||||||
return getConfig().notifications;
|
return getConfig().notifications;
|
||||||
},
|
},
|
||||||
@@ -192,12 +184,6 @@ export function getMaskedConfig() {
|
|||||||
from: c.mail.from,
|
from: c.mail.from,
|
||||||
recipients: c.mail.recipients,
|
recipients: c.mail.recipients,
|
||||||
},
|
},
|
||||||
directus: {
|
|
||||||
url: c.directus.url,
|
|
||||||
adminEmail: mask(c.directus.adminEmail),
|
|
||||||
password: mask(c.directus.password),
|
|
||||||
token: mask(c.directus.token),
|
|
||||||
},
|
|
||||||
notifications: {
|
notifications: {
|
||||||
gotify: {
|
gotify: {
|
||||||
url: c.notifications.gotify.url,
|
url: c.notifications.gotify.url,
|
||||||
|
|||||||
192
lib/directus.ts
192
lib/directus.ts
@@ -1,192 +0,0 @@
|
|||||||
import { readItems, readCollections } from '@directus/sdk';
|
|
||||||
import { createMintelDirectusClient, ensureDirectusAuthenticated } from '@mintel/next-utils';
|
|
||||||
import { config } from './config';
|
|
||||||
import { getServerAppServices } from './services/create-services.server';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Directus Schema Definitions
|
|
||||||
*/
|
|
||||||
export interface Schema {
|
|
||||||
products: any[];
|
|
||||||
categories: any[];
|
|
||||||
contact_submissions: any[];
|
|
||||||
product_requests: any[];
|
|
||||||
translations: any[];
|
|
||||||
categories_link: any[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize client using Mintel standards (environment-aware)
|
|
||||||
const client = createMintelDirectusClient<Schema>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to determine if we should show detailed errors
|
|
||||||
*/
|
|
||||||
const shouldShowDevErrors = config.isTesting || config.isDevelopment;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Genericizes error messages for production/staging
|
|
||||||
*/
|
|
||||||
function formatError(error: any) {
|
|
||||||
if (shouldShowDevErrors) {
|
|
||||||
return error.errors?.[0]?.message || error.message || 'An unexpected error occurred.';
|
|
||||||
}
|
|
||||||
return 'A system error occurred. Our team has been notified.';
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function ensureAuthenticated() {
|
|
||||||
try {
|
|
||||||
await ensureDirectusAuthenticated(client);
|
|
||||||
} catch (e: any) {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
getServerAppServices().errors.captureException(e, { part: 'directus_auth' });
|
|
||||||
}
|
|
||||||
console.error(`Failed to authenticate with Directus:`, e.message);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps the new translation-based schema back to the application's Product interface
|
|
||||||
*/
|
|
||||||
function mapDirectusProduct(item: any, locale: string): any {
|
|
||||||
const langCode = locale === 'en' ? 'en-US' : 'de-DE';
|
|
||||||
const translation =
|
|
||||||
item.translations?.find((t: any) => t.languages_code === langCode) ||
|
|
||||||
item.translations?.[0] ||
|
|
||||||
{};
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: item.id,
|
|
||||||
sku: item.sku,
|
|
||||||
title: translation.name || '',
|
|
||||||
description: translation.description || '',
|
|
||||||
content: translation.content || '',
|
|
||||||
technicalData: {
|
|
||||||
technicalItems: translation.technical_items || [],
|
|
||||||
voltageTables: translation.voltage_tables || [],
|
|
||||||
},
|
|
||||||
locale: locale,
|
|
||||||
// Use standardized proxy path for assets to avoid CORS
|
|
||||||
data_sheet_url: item.data_sheet ? `/api/directus/assets/${item.data_sheet}` : null,
|
|
||||||
categories: (item.categories_link || [])
|
|
||||||
.map((c: any) => c.categories_id?.translations?.[0]?.name)
|
|
||||||
.filter(Boolean),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getProducts(locale: string = 'de') {
|
|
||||||
await ensureAuthenticated();
|
|
||||||
try {
|
|
||||||
const items = await client.request(
|
|
||||||
readItems('products', {
|
|
||||||
fields: ['*', 'translations.*', 'categories_link.categories_id.translations.name'],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return items.map((item) => mapDirectusProduct(item, locale));
|
|
||||||
} catch (error) {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
getServerAppServices().errors.captureException(error, { part: 'directus_get_products' });
|
|
||||||
}
|
|
||||||
console.error('Error fetching products:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getProductBySlug(slug: string, locale: string = 'de') {
|
|
||||||
await ensureAuthenticated();
|
|
||||||
const langCode = locale === 'en' ? 'en-US' : 'de-DE';
|
|
||||||
try {
|
|
||||||
const items = await client.request(
|
|
||||||
readItems('products', {
|
|
||||||
filter: {
|
|
||||||
translations: {
|
|
||||||
slug: { _eq: slug },
|
|
||||||
languages_code: { _eq: langCode },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fields: ['*', 'translations.*', 'categories_link.categories_id.translations.name'],
|
|
||||||
limit: 1,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!items || items.length === 0) return null;
|
|
||||||
return mapDirectusProduct(items[0], locale);
|
|
||||||
} catch (error) {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
getServerAppServices().errors.captureException(error, {
|
|
||||||
part: 'directus_get_product_by_slug',
|
|
||||||
slug,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.error(`Error fetching product ${slug}:`, error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function checkHealth() {
|
|
||||||
try {
|
|
||||||
// 1. Connectivity & Auth Check
|
|
||||||
try {
|
|
||||||
await ensureAuthenticated();
|
|
||||||
await client.request(readCollections());
|
|
||||||
} catch (e: any) {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
getServerAppServices().errors.captureException(e, { part: 'directus_health_auth' });
|
|
||||||
}
|
|
||||||
console.error('Directus authentication or collection-read failed during health check:', e);
|
|
||||||
return {
|
|
||||||
status: 'error',
|
|
||||||
message: shouldShowDevErrors
|
|
||||||
? `Directus Health Error: ${e.message || 'Unknown'}`
|
|
||||||
: 'CMS is currently unavailable due to an internal authentication or connection error.',
|
|
||||||
code: e.code || 'HEALTH_AUTH_FAILED',
|
|
||||||
details: shouldShowDevErrors
|
|
||||||
? { message: e.message, code: e.code, errors: e.errors }
|
|
||||||
: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Schema check (does the contact_submissions table exist?)
|
|
||||||
try {
|
|
||||||
await client.request(readItems('contact_submissions', { limit: 1 }));
|
|
||||||
} catch (e: any) {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
getServerAppServices().errors.captureException(e, { part: 'directus_health_schema' });
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
e.message?.includes('does not exist') ||
|
|
||||||
e.code === 'INVALID_PAYLOAD' ||
|
|
||||||
e.status === 404
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
status: 'error',
|
|
||||||
message: shouldShowDevErrors
|
|
||||||
? `The "contact_submissions" collection is missing or inaccessible. Error: ${e.message || 'Unknown'}`
|
|
||||||
: 'Required data structures are currently unavailable.',
|
|
||||||
code: 'SCHEMA_MISSING',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
status: 'error',
|
|
||||||
message: shouldShowDevErrors
|
|
||||||
? `Schema error: ${e.errors?.[0]?.message || e.message || 'Unknown error'}`
|
|
||||||
: 'The data schema is currently misconfigured.',
|
|
||||||
code: 'SCHEMA_ERROR',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { status: 'ok', message: 'Directus is reachable and responding.' };
|
|
||||||
} catch (error: any) {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
getServerAppServices().errors.captureException(error, { part: 'directus_health_critical' });
|
|
||||||
}
|
|
||||||
console.error('Directus health check failed with unexpected error:', error);
|
|
||||||
return {
|
|
||||||
status: 'error',
|
|
||||||
message: formatError(error),
|
|
||||||
code: error.code || 'UNKNOWN',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default client;
|
|
||||||
@@ -43,12 +43,6 @@ const envExtension = {
|
|||||||
MAIL_PASSWORD: z.string().optional(),
|
MAIL_PASSWORD: z.string().optional(),
|
||||||
MAIL_FROM: z.string().optional(),
|
MAIL_FROM: z.string().optional(),
|
||||||
MAIL_RECIPIENTS: z.string().optional(),
|
MAIL_RECIPIENTS: z.string().optional(),
|
||||||
|
|
||||||
// Directus Authentication
|
|
||||||
DIRECTUS_URL: z.string().url().optional(),
|
|
||||||
DIRECTUS_ADMIN_EMAIL: z.string().email().optional(),
|
|
||||||
DIRECTUS_ADMIN_PASSWORD: z.string().optional(),
|
|
||||||
DIRECTUS_API_TOKEN: z.string().optional(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
import { getImgproxyUrl } from './imgproxy';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Next.js Image Loader for imgproxy
|
|
||||||
*
|
|
||||||
* @param {Object} props - properties from Next.js Image component
|
|
||||||
* @param {string} props.src - The source image URL
|
|
||||||
* @param {number} props.width - The desired image width
|
|
||||||
* @param {number} props.quality - The desired image quality (ignored for now as imgproxy handles it)
|
|
||||||
*/
|
|
||||||
export default function imgproxyLoader({
|
|
||||||
src,
|
|
||||||
width,
|
|
||||||
_quality,
|
|
||||||
}: {
|
|
||||||
src: string;
|
|
||||||
width: number;
|
|
||||||
_quality?: number;
|
|
||||||
}) {
|
|
||||||
// Skip imgproxy for SVGs as they are vectors and don't benefit from resizing,
|
|
||||||
// and often cause 404s if the source is not correctly resolvable by imgproxy.
|
|
||||||
if (src.toLowerCase().endsWith('.svg')) {
|
|
||||||
return src;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if src contains custom gravity or aspect ratio query parameters
|
|
||||||
let gravity = 'sm'; // Use smart gravity (content-aware) by default
|
|
||||||
let cleanSrc = src;
|
|
||||||
let calculatedHeight = 0;
|
|
||||||
let resizingType: 'fit' | 'fill' = 'fit';
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Dummy base needed for relative URLs
|
|
||||||
const url = new URL(src, 'http://localhost');
|
|
||||||
const customGravity = url.searchParams.get('gravity');
|
|
||||||
const aspectRatio = url.searchParams.get('ar'); // e.g. "16:9"
|
|
||||||
|
|
||||||
if (customGravity) {
|
|
||||||
gravity = customGravity;
|
|
||||||
url.searchParams.delete('gravity');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (aspectRatio) {
|
|
||||||
const parts = aspectRatio.split(':');
|
|
||||||
if (parts.length === 2) {
|
|
||||||
const arW = parseFloat(parts[0]);
|
|
||||||
const arH = parseFloat(parts[1]);
|
|
||||||
if (!isNaN(arW) && !isNaN(arH) && arW > 0) {
|
|
||||||
calculatedHeight = Math.round(width * (arH / arW));
|
|
||||||
resizingType = 'fill'; // Must use fill to allow imgproxy to crop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
url.searchParams.delete('ar');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (customGravity || aspectRatio) {
|
|
||||||
cleanSrc = src.startsWith('http') ? url.href : url.pathname + url.search;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Fallback if parsing fails
|
|
||||||
}
|
|
||||||
|
|
||||||
// We use the width provided by Next.js for responsive images
|
|
||||||
// Height is calculated from aspect ratio if provided, otherwise 0 to maintain aspect ratio
|
|
||||||
return getImgproxyUrl(cleanSrc, {
|
|
||||||
width,
|
|
||||||
height: calculatedHeight,
|
|
||||||
resizing_type: resizingType,
|
|
||||||
gravity,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
/**
|
|
||||||
* Generates an imgproxy URL for a given source image and options.
|
|
||||||
*
|
|
||||||
* Documentation: https://docs.imgproxy.net/usage/processing
|
|
||||||
*/
|
|
||||||
|
|
||||||
interface ImgproxyOptions {
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
resizing_type?: 'fit' | 'fill' | 'fill-down' | 'force' | 'auto';
|
|
||||||
gravity?: string;
|
|
||||||
enlarge?: boolean;
|
|
||||||
extension?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getImgproxyUrl(src: string, options: ImgproxyOptions = {}): string {
|
|
||||||
// Use local proxy path which is rewritten in next.config.mjs
|
|
||||||
const baseUrl = '/_img';
|
|
||||||
|
|
||||||
// Handle local paths or relative URLs
|
|
||||||
let absoluteSrc = src;
|
|
||||||
if (src.startsWith('/')) {
|
|
||||||
const baseUrlForSrc =
|
|
||||||
process.env.NEXT_PUBLIC_BASE_URL ||
|
|
||||||
(typeof window !== 'undefined' ? window.location.origin : 'https://klz-cables.com');
|
|
||||||
if (baseUrlForSrc) {
|
|
||||||
absoluteSrc = `${baseUrlForSrc.replace(/\/$/, '')}${src}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Development mapping: Map local domains to internal Docker hostnames
|
|
||||||
// so imgproxy can fetch images without SSL issues or external routing
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
|
||||||
if (absoluteSrc.includes('klz.localhost')) {
|
|
||||||
absoluteSrc = absoluteSrc.replace(/^https?:\/\/klz\.localhost/, 'http://klz-app:3000');
|
|
||||||
} else if (absoluteSrc.includes('cms.klz.localhost')) {
|
|
||||||
absoluteSrc = absoluteSrc.replace(/^https?:\/\/cms\.klz\.localhost/, 'http://klz-cms:8055');
|
|
||||||
}
|
|
||||||
// Also handle direct container names if needed
|
|
||||||
}
|
|
||||||
|
|
||||||
const { width = 0, height = 0, enlarge = false, extension = '' } = options;
|
|
||||||
|
|
||||||
let quality = 80;
|
|
||||||
if (extension) quality = 90;
|
|
||||||
|
|
||||||
// Re-map imgproxy URL to our new parameter structure
|
|
||||||
// e.g. /process?url=...&w=...&h=...&q=...&format=...
|
|
||||||
const queryParams = new URLSearchParams({
|
|
||||||
url: absoluteSrc,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (width > 0) queryParams.set('w', width.toString());
|
|
||||||
if (height > 0) queryParams.set('h', height.toString());
|
|
||||||
if (extension) queryParams.set('format', extension.replace('.', ''));
|
|
||||||
if (quality) queryParams.set('q', quality.toString());
|
|
||||||
|
|
||||||
return `${baseUrl}/process?${queryParams.toString()}`;
|
|
||||||
}
|
|
||||||
285
lib/mdx.ts
285
lib/mdx.ts
@@ -1,6 +1,5 @@
|
|||||||
import fs from 'fs';
|
import { getPayload } from 'payload';
|
||||||
import path from 'path';
|
import configPromise from '@payload-config';
|
||||||
import matter from 'gray-matter';
|
|
||||||
import { mapSlugToFileSlug } from './slugs';
|
import { mapSlugToFileSlug } from './slugs';
|
||||||
|
|
||||||
export interface ProductFrontmatter {
|
export interface ProductFrontmatter {
|
||||||
@@ -10,65 +9,69 @@ export interface ProductFrontmatter {
|
|||||||
categories: string[];
|
categories: string[];
|
||||||
images: string[];
|
images: string[];
|
||||||
locale: string;
|
locale: string;
|
||||||
|
isFallback?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProductMdx {
|
export interface ProductMdx {
|
||||||
slug: string;
|
slug: string;
|
||||||
frontmatter: ProductFrontmatter;
|
frontmatter: ProductFrontmatter;
|
||||||
content: string;
|
content: any; // Lexical AST from Payload
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getProductMetadata(
|
export async function getProductMetadata(
|
||||||
slug: string,
|
slug: string,
|
||||||
locale: string,
|
locale: string,
|
||||||
): Promise<Partial<ProductMdx> | null> {
|
): Promise<Partial<ProductMdx> | null> {
|
||||||
// Map translated slug to file slug
|
const payload = await getPayload({ config: configPromise });
|
||||||
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
||||||
const productsDir = path.join(process.cwd(), 'data', 'products', locale);
|
|
||||||
|
|
||||||
if (!fs.existsSync(productsDir)) return null;
|
let result = await payload.find({
|
||||||
|
collection: 'products',
|
||||||
|
where: {
|
||||||
|
and: [{ slug: { equals: fileSlug } }, { locale: { equals: locale } }],
|
||||||
|
},
|
||||||
|
depth: 1, // To auto-resolve Media relation (images array)
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
// Recursive search for the file
|
let isFallback = false;
|
||||||
const findFile = (dir: string): string | null => {
|
|
||||||
const files = fs.readdirSync(dir);
|
|
||||||
for (const file of files) {
|
|
||||||
const fullPath = path.join(dir, file);
|
|
||||||
const stat = fs.statSync(fullPath);
|
|
||||||
if (stat.isDirectory()) {
|
|
||||||
const found = findFile(fullPath);
|
|
||||||
if (found) return found;
|
|
||||||
} else if (file === `${fileSlug}.mdx` || file === `${fileSlug}-2.mdx`) {
|
|
||||||
return fullPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
let filePath = findFile(productsDir);
|
if (result.docs.length === 0 && locale !== 'en') {
|
||||||
|
|
||||||
if (!filePath && locale !== 'en') {
|
|
||||||
// Fallback to English
|
// Fallback to English
|
||||||
const enProductsDir = path.join(process.cwd(), 'data', 'products', 'en');
|
result = await payload.find({
|
||||||
if (fs.existsSync(enProductsDir)) {
|
collection: 'products',
|
||||||
filePath = findFile(enProductsDir);
|
where: {
|
||||||
|
and: [{ slug: { equals: fileSlug } }, { locale: { equals: 'en' } }],
|
||||||
|
},
|
||||||
|
depth: 1,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
if (result.docs.length > 0) {
|
||||||
|
isFallback = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filePath && fs.existsSync(filePath)) {
|
if (result.docs.length > 0) {
|
||||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
const doc = result.docs[0];
|
||||||
const { data } = matter(fileContent);
|
|
||||||
|
|
||||||
// Filter out products without images to match getProductBySlug behavior
|
// Process Images
|
||||||
if (!data.images || data.images.length === 0 || !data.images[0]) {
|
const resolvedImages = ((doc.images as any[]) || [])
|
||||||
return null;
|
.map((img) => (typeof img === 'string' ? img : img.url))
|
||||||
}
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (resolvedImages.length === 0) return null; // Original logic skipped docs without images
|
||||||
|
|
||||||
return {
|
return {
|
||||||
slug: fileSlug,
|
slug: doc.slug,
|
||||||
frontmatter: {
|
frontmatter: {
|
||||||
...data,
|
title: doc.title,
|
||||||
isFallback: filePath.includes('/en/'),
|
sku: doc.sku,
|
||||||
} as any,
|
description: doc.description,
|
||||||
|
categories: Array.isArray(doc.categories) ? doc.categories.map((c: any) => c.category) : [],
|
||||||
|
images: resolvedImages,
|
||||||
|
locale: doc.locale,
|
||||||
|
isFallback,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,111 +79,159 @@ export async function getProductMetadata(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getProductBySlug(slug: string, locale: string): Promise<ProductMdx | null> {
|
export async function getProductBySlug(slug: string, locale: string): Promise<ProductMdx | null> {
|
||||||
// Map translated slug to file slug
|
const payload = await getPayload({ config: configPromise });
|
||||||
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
||||||
const productsDir = path.join(process.cwd(), 'data', 'products', locale);
|
|
||||||
|
|
||||||
if (!fs.existsSync(productsDir)) return null;
|
let result = await payload.find({
|
||||||
|
collection: 'products',
|
||||||
|
where: {
|
||||||
|
and: [{ slug: { equals: fileSlug } }, { locale: { equals: locale } }],
|
||||||
|
},
|
||||||
|
depth: 1, // Auto-resolve Media logic
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
// Recursive search for the file
|
|
||||||
const findFile = (dir: string): string | null => {
|
|
||||||
const files = fs.readdirSync(dir);
|
|
||||||
for (const file of files) {
|
|
||||||
const fullPath = path.join(dir, file);
|
|
||||||
const stat = fs.statSync(fullPath);
|
|
||||||
if (stat.isDirectory()) {
|
|
||||||
const found = findFile(fullPath);
|
|
||||||
if (found) return found;
|
|
||||||
} else if (file === `${fileSlug}.mdx` || file === `${fileSlug}-2.mdx`) {
|
|
||||||
return fullPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
let filePath = findFile(productsDir);
|
|
||||||
let isFallback = false;
|
let isFallback = false;
|
||||||
|
|
||||||
if (!filePath && locale !== 'en') {
|
if (result.docs.length === 0 && locale !== 'en') {
|
||||||
// Fallback to English
|
// Fallback to English
|
||||||
const enProductsDir = path.join(process.cwd(), 'data', 'products', 'en');
|
result = await payload.find({
|
||||||
if (fs.existsSync(enProductsDir)) {
|
collection: 'products',
|
||||||
filePath = findFile(enProductsDir);
|
where: {
|
||||||
if (filePath) isFallback = true;
|
and: [{ slug: { equals: fileSlug } }, { locale: { equals: 'en' } }],
|
||||||
|
},
|
||||||
|
depth: 1,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
if (result.docs.length > 0) {
|
||||||
|
isFallback = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filePath && fs.existsSync(filePath)) {
|
if (result.docs.length > 0) {
|
||||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
const doc = result.docs[0];
|
||||||
const { data, content } = matter(fileContent);
|
|
||||||
const product = {
|
// Map Images correctly from resolved Media docs
|
||||||
slug: fileSlug,
|
const resolvedImages = ((doc.images as any[]) || [])
|
||||||
|
.map((img) => (typeof img === 'string' ? img : img.url))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (resolvedImages.length === 0) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
slug: doc.slug,
|
||||||
frontmatter: {
|
frontmatter: {
|
||||||
...data,
|
title: doc.title,
|
||||||
|
sku: doc.sku,
|
||||||
|
description: doc.description,
|
||||||
|
categories: Array.isArray(doc.categories) ? doc.categories.map((c: any) => c.category) : [],
|
||||||
|
images: resolvedImages,
|
||||||
|
locale: doc.locale,
|
||||||
isFallback,
|
isFallback,
|
||||||
} as any,
|
},
|
||||||
content,
|
content: doc.content, // Lexical payload instead of raw MDX String
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filter out products without images
|
|
||||||
if (
|
|
||||||
!product.frontmatter.images ||
|
|
||||||
product.frontmatter.images.length === 0 ||
|
|
||||||
!product.frontmatter.images[0]
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return product;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllProductSlugs(locale: string): Promise<string[]> {
|
export async function getAllProductSlugs(locale: string): Promise<string[]> {
|
||||||
const productsDir = path.join(process.cwd(), 'data', 'products', locale);
|
const payload = await getPayload({ config: configPromise });
|
||||||
if (!fs.existsSync(productsDir)) return [];
|
const result = await payload.find({
|
||||||
|
collection: 'products',
|
||||||
|
where: {
|
||||||
|
locale: {
|
||||||
|
equals: locale,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pagination: false, // get all docs
|
||||||
|
});
|
||||||
|
|
||||||
const slugs: string[] = [];
|
return result.docs.map((doc) => doc.slug);
|
||||||
const walk = (dir: string) => {
|
|
||||||
const files = fs.readdirSync(dir);
|
|
||||||
for (const file of files) {
|
|
||||||
const fullPath = path.join(dir, file);
|
|
||||||
const stat = fs.statSync(fullPath);
|
|
||||||
if (stat.isDirectory()) {
|
|
||||||
walk(fullPath);
|
|
||||||
} else if (file.endsWith('.mdx')) {
|
|
||||||
slugs.push(file.replace(/\.mdx$/, ''));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
walk(productsDir);
|
|
||||||
return slugs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllProducts(locale: string): Promise<ProductMdx[]> {
|
export async function getAllProducts(locale: string): Promise<ProductMdx[]> {
|
||||||
const slugs = await getAllProductSlugs(locale);
|
// Fetch ALL products in a single query to avoid N+1 getPayload() calls
|
||||||
let allSlugs = slugs;
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
|
||||||
|
// Get products for this locale
|
||||||
|
const result = await payload.find({
|
||||||
|
collection: 'products',
|
||||||
|
where: { locale: { equals: locale } },
|
||||||
|
depth: 1,
|
||||||
|
pagination: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let products: ProductMdx[] = result.docs
|
||||||
|
.filter((doc) => {
|
||||||
|
const resolvedImages = ((doc.images as any[]) || [])
|
||||||
|
.map((img) => (typeof img === 'string' ? img : img.url))
|
||||||
|
.filter(Boolean);
|
||||||
|
return resolvedImages.length > 0;
|
||||||
|
})
|
||||||
|
.map((doc) => ({
|
||||||
|
slug: doc.slug,
|
||||||
|
frontmatter: {
|
||||||
|
title: doc.title,
|
||||||
|
sku: doc.sku,
|
||||||
|
description: doc.description,
|
||||||
|
categories: Array.isArray(doc.categories) ? doc.categories.map((c: any) => c.category) : [],
|
||||||
|
images: ((doc.images as any[]) || [])
|
||||||
|
.map((img) => (typeof img === 'string' ? img : img.url))
|
||||||
|
.filter(Boolean),
|
||||||
|
locale: doc.locale,
|
||||||
|
},
|
||||||
|
content: doc.content,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Also include English fallbacks for slugs not in this locale
|
||||||
if (locale !== 'en') {
|
if (locale !== 'en') {
|
||||||
const enSlugs = await getAllProductSlugs('en');
|
const localeSlugs = new Set(products.map((p) => p.slug));
|
||||||
allSlugs = Array.from(new Set([...slugs, ...enSlugs]));
|
const enResult = await payload.find({
|
||||||
|
collection: 'products',
|
||||||
|
where: { locale: { equals: 'en' } },
|
||||||
|
depth: 1,
|
||||||
|
pagination: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fallbacks = enResult.docs
|
||||||
|
.filter((doc) => !localeSlugs.has(doc.slug))
|
||||||
|
.filter((doc) => {
|
||||||
|
const resolvedImages = ((doc.images as any[]) || [])
|
||||||
|
.map((img) => (typeof img === 'string' ? img : img.url))
|
||||||
|
.filter(Boolean);
|
||||||
|
return resolvedImages.length > 0;
|
||||||
|
})
|
||||||
|
.map((doc) => ({
|
||||||
|
slug: doc.slug,
|
||||||
|
frontmatter: {
|
||||||
|
title: doc.title,
|
||||||
|
sku: doc.sku,
|
||||||
|
description: doc.description,
|
||||||
|
categories: Array.isArray(doc.categories)
|
||||||
|
? doc.categories.map((c: any) => c.category)
|
||||||
|
: [],
|
||||||
|
images: ((doc.images as any[]) || [])
|
||||||
|
.map((img) => (typeof img === 'string' ? img : img.url))
|
||||||
|
.filter(Boolean),
|
||||||
|
locale: doc.locale,
|
||||||
|
isFallback: true,
|
||||||
|
},
|
||||||
|
content: doc.content,
|
||||||
|
}));
|
||||||
|
|
||||||
|
products = [...products, ...fallbacks];
|
||||||
}
|
}
|
||||||
|
|
||||||
const products = await Promise.all(allSlugs.map((slug) => getProductBySlug(slug, locale)));
|
return products;
|
||||||
return products.filter((p): p is ProductMdx => p !== null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllProductsMetadata(locale: string): Promise<Partial<ProductMdx>[]> {
|
export async function getAllProductsMetadata(locale: string): Promise<Partial<ProductMdx>[]> {
|
||||||
const slugs = await getAllProductSlugs(locale);
|
// Reuse getAllProducts to avoid separate N+1 queries
|
||||||
let allSlugs = slugs;
|
const products = await getAllProducts(locale);
|
||||||
|
return products.map((p) => ({
|
||||||
if (locale !== 'en') {
|
slug: p.slug,
|
||||||
const enSlugs = await getAllProductSlugs('en');
|
frontmatter: p.frontmatter,
|
||||||
allSlugs = Array.from(new Set([...slugs, ...enSlugs]));
|
}));
|
||||||
}
|
|
||||||
|
|
||||||
const metadata = await Promise.all(allSlugs.map((slug) => getProductMetadata(slug, locale)));
|
|
||||||
return metadata.filter((m): m is Partial<ProductMdx> => m !== null);
|
|
||||||
}
|
}
|
||||||
|
|||||||
138
lib/pages.ts
138
lib/pages.ts
@@ -1,80 +1,112 @@
|
|||||||
import fs from 'fs';
|
import { getPayload } from 'payload';
|
||||||
import path from 'path';
|
import configPromise from '@payload-config';
|
||||||
import matter from 'gray-matter';
|
|
||||||
import { mapSlugToFileSlug } from './slugs';
|
|
||||||
|
|
||||||
export interface PageFrontmatter {
|
export interface PageFrontmatter {
|
||||||
title: string;
|
title: string;
|
||||||
excerpt: string;
|
excerpt: string;
|
||||||
featuredImage: string | null;
|
featuredImage: string | null;
|
||||||
locale: string;
|
locale: string;
|
||||||
|
public?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PageMdx {
|
export interface PageMdx {
|
||||||
slug: string;
|
slug: string;
|
||||||
frontmatter: PageFrontmatter;
|
frontmatter: PageFrontmatter;
|
||||||
content: string;
|
content: any; // Lexical AST Document
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPageBySlug(slug: string, locale: string): Promise<PageMdx | null> {
|
export async function getPageBySlug(slug: string, locale: string): Promise<PageMdx | null> {
|
||||||
// Map translated slug to file slug
|
const payload = await getPayload({ config: configPromise });
|
||||||
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
|
||||||
const pagesDir = path.join(process.cwd(), 'data', 'pages', locale);
|
|
||||||
const filePath = path.join(pagesDir, `${fileSlug}.mdx`);
|
|
||||||
|
|
||||||
if (!fs.existsSync(filePath)) {
|
const result = await payload.find({
|
||||||
return null;
|
collection: 'pages' as any,
|
||||||
}
|
where: {
|
||||||
|
slug: { equals: slug },
|
||||||
|
locale: { equals: locale },
|
||||||
|
},
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
const docs = result.docs as any[];
|
||||||
const { data, content } = matter(fileContent);
|
|
||||||
|
if (!docs || docs.length === 0) return null;
|
||||||
|
|
||||||
|
const doc = docs[0];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
slug: fileSlug,
|
slug: doc.slug,
|
||||||
frontmatter: data as PageFrontmatter,
|
frontmatter: {
|
||||||
content,
|
title: doc.title,
|
||||||
|
excerpt: doc.excerpt || '',
|
||||||
|
locale: doc.locale,
|
||||||
|
featuredImage:
|
||||||
|
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
||||||
|
? doc.featuredImage.sizes?.card?.url || doc.featuredImage.url
|
||||||
|
: null,
|
||||||
|
} as PageFrontmatter,
|
||||||
|
content: doc.content as any, // Native Lexical Editor State
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllPages(locale: string): Promise<PageMdx[]> {
|
export async function getAllPages(locale: string): Promise<PageMdx[]> {
|
||||||
const pagesDir = path.join(process.cwd(), 'data', 'pages', locale);
|
const payload = await getPayload({ config: configPromise });
|
||||||
if (!fs.existsSync(pagesDir)) return [];
|
|
||||||
|
|
||||||
const files = fs.readdirSync(pagesDir);
|
const result = await payload.find({
|
||||||
const pages = await Promise.all(
|
collection: 'pages' as any,
|
||||||
files
|
where: {
|
||||||
.filter((file) => file.endsWith('.mdx'))
|
locale: {
|
||||||
.map((file) => {
|
equals: locale,
|
||||||
const fileSlug = file.replace(/\.mdx$/, '');
|
},
|
||||||
const filePath = path.join(pagesDir, file);
|
},
|
||||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
limit: 100,
|
||||||
const { data, content } = matter(fileContent);
|
});
|
||||||
|
|
||||||
|
const docs = result.docs as any[];
|
||||||
|
|
||||||
|
return docs.map((doc: any) => {
|
||||||
return {
|
return {
|
||||||
slug: fileSlug,
|
slug: doc.slug,
|
||||||
frontmatter: data as PageFrontmatter,
|
frontmatter: {
|
||||||
content,
|
title: doc.title,
|
||||||
};
|
excerpt: doc.excerpt || '',
|
||||||
}),
|
locale: doc.locale,
|
||||||
);
|
featuredImage:
|
||||||
|
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
||||||
return pages.filter((p): p is PageMdx => p !== null);
|
? doc.featuredImage.sizes?.card?.url || doc.featuredImage.url
|
||||||
}
|
: null,
|
||||||
|
} as PageFrontmatter,
|
||||||
export async function getAllPagesMetadata(locale: string): Promise<Partial<PageMdx>[]> {
|
content: doc.content as any,
|
||||||
const pagesDir = path.join(process.cwd(), 'data', 'pages', locale);
|
};
|
||||||
if (!fs.existsSync(pagesDir)) return [];
|
});
|
||||||
|
}
|
||||||
const files = fs.readdirSync(pagesDir);
|
|
||||||
return files
|
export async function getAllPagesMetadata(locale: string): Promise<Partial<PageMdx>[]> {
|
||||||
.filter((file) => file.endsWith('.mdx'))
|
const payload = await getPayload({ config: configPromise });
|
||||||
.map((file) => {
|
|
||||||
const fileSlug = file.replace(/\.mdx$/, '');
|
const result = await payload.find({
|
||||||
const filePath = path.join(pagesDir, file);
|
collection: 'pages' as any,
|
||||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
where: {
|
||||||
const { data } = matter(fileContent);
|
locale: {
|
||||||
return {
|
equals: locale,
|
||||||
slug: fileSlug,
|
},
|
||||||
frontmatter: data as PageFrontmatter,
|
},
|
||||||
|
limit: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const docs = result.docs as any[];
|
||||||
|
|
||||||
|
return docs.map((doc: any) => {
|
||||||
|
return {
|
||||||
|
slug: doc.slug,
|
||||||
|
frontmatter: {
|
||||||
|
title: doc.title,
|
||||||
|
excerpt: doc.excerpt || '',
|
||||||
|
locale: doc.locale,
|
||||||
|
featuredImage:
|
||||||
|
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
||||||
|
? doc.featuredImage.sizes?.card?.url || doc.featuredImage.url
|
||||||
|
: null,
|
||||||
|
} as PageFrontmatter,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,19 @@ import {
|
|||||||
import { PinoLoggerService } from './logging/pino-logger-service';
|
import { PinoLoggerService } from './logging/pino-logger-service';
|
||||||
import { config, getMaskedConfig } from '../config';
|
import { config, getMaskedConfig } from '../config';
|
||||||
|
|
||||||
let singleton: AppServices | undefined;
|
declare global {
|
||||||
|
var __appServices: AppServices | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a local cache to prevent re-looking up globalThis frequently
|
||||||
|
let serverSingleton: AppServices | undefined;
|
||||||
|
|
||||||
export function getServerAppServices(): AppServices {
|
export function getServerAppServices(): AppServices {
|
||||||
if (singleton) return singleton;
|
if (serverSingleton) return serverSingleton;
|
||||||
|
if (globalThis.__appServices) {
|
||||||
|
serverSingleton = globalThis.__appServices;
|
||||||
|
return serverSingleton;
|
||||||
|
}
|
||||||
|
|
||||||
// Create logger first to log initialization
|
// Create logger first to log initialization
|
||||||
const logger = new PinoLoggerService('server');
|
const logger = new PinoLoggerService('server');
|
||||||
@@ -74,9 +84,9 @@ export function getServerAppServices(): AppServices {
|
|||||||
level: config.logging.level,
|
level: config.logging.level,
|
||||||
});
|
});
|
||||||
|
|
||||||
singleton = new AppServices(analytics, errors, cache, logger, notifications);
|
globalThis.__appServices = new AppServices(analytics, errors, cache, logger, notifications);
|
||||||
|
|
||||||
logger.info('All application services initialized successfully');
|
logger.info('All application services initialized successfully');
|
||||||
|
|
||||||
return singleton;
|
return globalThis.__appServices;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,65 +9,18 @@ import { PinoLoggerService } from './logging/pino-logger-service';
|
|||||||
import { NoopNotificationService } from './notifications/gotify-notification-service';
|
import { NoopNotificationService } from './notifications/gotify-notification-service';
|
||||||
import { config, getMaskedConfig } from '../config';
|
import { config, getMaskedConfig } from '../config';
|
||||||
|
|
||||||
/**
|
declare global {
|
||||||
* Singleton instance of AppServices.
|
var __appServices: AppServices | undefined;
|
||||||
*
|
}
|
||||||
* In Next.js, module singletons are per-process (server) and per-tab (client).
|
|
||||||
* This is sufficient for a small service layer and provides better performance
|
|
||||||
* than creating new instances on every request.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
let singleton: AppServices | undefined;
|
let singleton: AppServices | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the application services singleton.
|
* Get the application services singleton.
|
||||||
*
|
|
||||||
* This function creates and caches the application services, including:
|
|
||||||
* - Analytics service (Umami or no-op)
|
|
||||||
* - Error reporting service (GlitchTip/Sentry or no-op)
|
|
||||||
* - Cache service (in-memory)
|
|
||||||
*
|
|
||||||
* The services are configured based on environment variables:
|
|
||||||
* - `UMAMI_WEBSITE_ID` - Enables Umami analytics
|
|
||||||
* - `SENTRY_DSN` - Enables error reporting (server-side direct, client-side via relay)
|
|
||||||
*
|
|
||||||
* @returns {AppServices} The application services singleton
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* // Get services in a client component
|
|
||||||
* import { getAppServices } from '@/lib/services/create-services';
|
|
||||||
*
|
|
||||||
* const services = getAppServices();
|
|
||||||
* services.analytics.track('button_click', { button_id: 'cta' });
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* // Get services in a server component or API route
|
|
||||||
* import { getAppServices } from '@/lib/services/create-services';
|
|
||||||
*
|
|
||||||
* const services = getAppServices();
|
|
||||||
* await services.cache.set('key', 'value');
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* // Automatic service selection based on environment
|
|
||||||
* // If UMAMI_WEBSITE_ID is set:
|
|
||||||
* // services.analytics = UmamiAnalyticsService
|
|
||||||
* // If not set:
|
|
||||||
* // services.analytics = NoopAnalyticsService (safe no-op)
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @see {@link UmamiAnalyticsService} for analytics implementation
|
|
||||||
* @see {@link NoopAnalyticsService} for no-op fallback
|
|
||||||
* @see {@link GlitchtipErrorReportingService} for error reporting
|
|
||||||
* @see {@link MemoryCacheService} for caching
|
|
||||||
*/
|
*/
|
||||||
export function getAppServices(): AppServices {
|
export function getAppServices(): AppServices {
|
||||||
// Return cached instance if available
|
// Return cached instance if available
|
||||||
|
if (typeof window === 'undefined' && globalThis.__appServices) return globalThis.__appServices;
|
||||||
if (singleton) return singleton;
|
if (singleton) return singleton;
|
||||||
|
|
||||||
// Create logger first to log initialization
|
// Create logger first to log initialization
|
||||||
@@ -127,9 +80,6 @@ export function getAppServices(): AppServices {
|
|||||||
logger.info('Noop error reporting service initialized (error reporting disabled)');
|
logger.info('Noop error reporting service initialized (error reporting disabled)');
|
||||||
}
|
}
|
||||||
|
|
||||||
// IMPORTANT: This module is imported by client components.
|
|
||||||
// Do not import Node-only modules (like the `redis` client) here.
|
|
||||||
// Use [`getServerAppServices()`](lib/services/create-services.server.ts:1) on the server.
|
|
||||||
const cache = new MemoryCacheService();
|
const cache = new MemoryCacheService();
|
||||||
logger.info('Memory cache service initialized');
|
logger.info('Memory cache service initialized');
|
||||||
|
|
||||||
@@ -139,6 +89,11 @@ export function getAppServices(): AppServices {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create and cache the singleton
|
// Create and cache the singleton
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
globalThis.__appServices = new AppServices(analytics, errors, cache, logger, notifications);
|
||||||
|
return globalThis.__appServices;
|
||||||
|
}
|
||||||
|
|
||||||
singleton = new AppServices(analytics, errors, cache, logger, notifications);
|
singleton = new AppServices(analytics, errors, cache, logger, notifications);
|
||||||
|
|
||||||
logger.info('All application services initialized successfully');
|
logger.info('All application services initialized successfully');
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export class GlitchtipErrorReportingService implements ErrorReportingService {
|
|||||||
this.getSentry().then((Sentry) => Sentry.setTag(key, value));
|
this.getSentry().then((Sentry) => Sentry.setTag(key, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
withScope<T>(fn: () => T, context?: Record<string, unknown>): T {
|
withScope<T>(fn: () => T, _context?: Record<string, unknown>): T {
|
||||||
if (!this.options.enabled) return fn();
|
if (!this.options.enabled) return fn();
|
||||||
|
|
||||||
// Since withScope mandates executing fn() synchronously to return T,
|
// Since withScope mandates executing fn() synchronously to return T,
|
||||||
|
|||||||
@@ -10,40 +10,14 @@ const intlMiddleware = createMiddleware({
|
|||||||
defaultLocale: 'en',
|
defaultLocale: 'en',
|
||||||
});
|
});
|
||||||
|
|
||||||
const imgproxyStatus = { isDown: false, lastCheck: 0 };
|
|
||||||
|
|
||||||
async function isImgproxyDown() {
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - imgproxyStatus.lastCheck > 60000) {
|
|
||||||
try {
|
|
||||||
const imgproxyUrl = process.env.IMGPROXY_URL || 'http://klz-imgproxy:8080';
|
|
||||||
const checkUrl = imgproxyUrl.startsWith('http') ? imgproxyUrl : `https://${imgproxyUrl}`;
|
|
||||||
const res = await fetch(checkUrl, { signal: AbortSignal.timeout(2000) });
|
|
||||||
imgproxyStatus.isDown = res.status >= 500;
|
|
||||||
} catch (e) {
|
|
||||||
imgproxyStatus.isDown = true;
|
|
||||||
}
|
|
||||||
imgproxyStatus.lastCheck = now;
|
|
||||||
}
|
|
||||||
return imgproxyStatus.isDown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function middleware(request: NextRequest) {
|
export default async function middleware(request: NextRequest) {
|
||||||
const { method, url, headers } = request;
|
const { method, url, headers } = request;
|
||||||
const { pathname } = request.nextUrl;
|
const { pathname } = request.nextUrl;
|
||||||
|
|
||||||
if (pathname.startsWith('/_img/')) {
|
// Explicit bypass for infrastructure and Payload CMS routes to avoid locale redirects/interception
|
||||||
if (await isImgproxyDown()) {
|
|
||||||
const originalUrl = request.nextUrl.searchParams.get('url');
|
|
||||||
if (originalUrl) {
|
|
||||||
return NextResponse.redirect(originalUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return NextResponse.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Explicit bypass for infrastructure routes to avoid locale redirects/interception
|
|
||||||
if (
|
if (
|
||||||
|
pathname.startsWith('/admin') ||
|
||||||
|
pathname.startsWith('/api') ||
|
||||||
pathname.startsWith('/stats') ||
|
pathname.startsWith('/stats') ||
|
||||||
pathname.startsWith('/errors') ||
|
pathname.startsWith('/errors') ||
|
||||||
pathname.startsWith('/health') ||
|
pathname.startsWith('/health') ||
|
||||||
@@ -125,8 +99,7 @@ export default async function middleware(request: NextRequest) {
|
|||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: [
|
matcher: [
|
||||||
'/((?!api|_next/static|_next/image|favicon.ico|manifest.webmanifest|.*\\.(?:svg|png|jpg|jpeg|gif|webp|pdf|txt|vcf|xml|webm|mp4|map)$).*)',
|
'/((?!api|_next/static|_next/image|favicon.ico|admin|manifest.webmanifest|.*\\.(?:svg|png|jpg|jpeg|gif|webp|pdf|txt|vcf|xml|webm|mp4|map)$).*)',
|
||||||
'/(de|en)/:path*',
|
|
||||||
'/(de|en)/:path*',
|
'/(de|en)/:path*',
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/dev/types/routes.d.ts";
|
import "./.next/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
101
next.config.mjs
101
next.config.mjs
@@ -2,8 +2,9 @@ import withMintelConfig from '@mintel/next-config';
|
|||||||
import withBundleAnalyzer from '@next/bundle-analyzer';
|
import withBundleAnalyzer from '@next/bundle-analyzer';
|
||||||
import { withSentryConfig } from '@sentry/nextjs';
|
import { withSentryConfig } from '@sentry/nextjs';
|
||||||
|
|
||||||
console.log('🚀 NEXT CONFIG LOADED FROM:', process.cwd());
|
import { withPayload } from '@payloadcms/next/withPayload';
|
||||||
|
|
||||||
|
console.log('🚀 NEXT CONFIG LOADED FROM:', process.cwd());
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
onDemandEntries: {
|
onDemandEntries: {
|
||||||
@@ -11,9 +12,10 @@ const nextConfig = {
|
|||||||
maxInactiveAge: 60 * 1000,
|
maxInactiveAge: 60 * 1000,
|
||||||
},
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
optimizeCss: true,
|
|
||||||
optimizePackageImports: ['lucide-react', 'framer-motion', '@/components/ui'],
|
optimizePackageImports: ['lucide-react', 'framer-motion', '@/components/ui'],
|
||||||
},
|
},
|
||||||
|
swcMinify: false,
|
||||||
|
reactStrictMode: false,
|
||||||
productionBrowserSourceMaps: false,
|
productionBrowserSourceMaps: false,
|
||||||
logging: {
|
logging: {
|
||||||
fetches: {
|
fetches: {
|
||||||
@@ -22,38 +24,39 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
async headers() {
|
async headers() {
|
||||||
|
const isProd = process.env.NODE_ENV === 'production';
|
||||||
const umamiDomain = new URL(process.env.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me').origin;
|
const umamiDomain = new URL(process.env.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me').origin;
|
||||||
const directusDomain = new URL(process.env.DIRECTUS_URL || 'https://cms.klz-cables.com').origin;
|
|
||||||
const imgproxyDomain = new URL(process.env.IMGPROXY_URL || 'http://klz-imgproxy:8080').origin;
|
|
||||||
const glitchtipDomain = new URL(process.env.SENTRY_DSN ? new URL(process.env.SENTRY_DSN).origin : 'https://errors.infra.mintel.me').origin;
|
const glitchtipDomain = new URL(process.env.SENTRY_DSN ? new URL(process.env.SENTRY_DSN).origin : 'https://errors.infra.mintel.me').origin;
|
||||||
|
// Additional domains that need to be whitelisted for images
|
||||||
|
const extraImgDomains = [
|
||||||
|
'https://klz-cables.com',
|
||||||
|
'https://staging.klz-cables.com',
|
||||||
|
'https://testing.klz-cables.com',
|
||||||
|
'http://klz.localhost',
|
||||||
|
'https://www.gravatar.com',
|
||||||
|
'https://gravatar.com',
|
||||||
|
].join(' ');
|
||||||
|
|
||||||
const cspHeader = `
|
const cspHeader = `
|
||||||
default-src 'self';
|
default-src 'self';
|
||||||
script-src 'self' 'unsafe-inline' 'unsafe-eval' ${umamiDomain};
|
script-src 'self' 'unsafe-inline' 'unsafe-eval' ${umamiDomain};
|
||||||
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
|
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
|
||||||
font-src 'self' https://fonts.gstatic.com;
|
font-src 'self' https://fonts.gstatic.com;
|
||||||
img-src 'self' data: blob: ${imgproxyDomain} ${directusDomain};
|
img-src 'self' data: blob: ${extraImgDomains};
|
||||||
connect-src 'self' ${umamiDomain} ${glitchtipDomain} ${directusDomain};
|
connect-src 'self' ${umamiDomain} ${glitchtipDomain};
|
||||||
frame-src 'self';
|
frame-src 'self';
|
||||||
object-src 'none';
|
object-src 'none';
|
||||||
base-uri 'self';
|
base-uri 'self';
|
||||||
form-action 'self';
|
form-action 'self';
|
||||||
frame-ancestors 'none';
|
frame-ancestors 'none';
|
||||||
upgrade-insecure-requests;
|
${isProd ? 'upgrade-insecure-requests;' : ''}
|
||||||
`.replace(/\s{2,}/g, ' ').trim();
|
`.replace(/\s{2,}/g, ' ').trim();
|
||||||
|
|
||||||
return [
|
const secureHeaders = [
|
||||||
{
|
|
||||||
source: '/:path*',
|
|
||||||
headers: [
|
|
||||||
{
|
{
|
||||||
key: 'Content-Security-Policy',
|
key: 'Content-Security-Policy',
|
||||||
value: cspHeader,
|
value: cspHeader,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'Strict-Transport-Security',
|
|
||||||
value: 'max-age=63072000; includeSubDomains; preload',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'X-Frame-Options',
|
key: 'X-Frame-Options',
|
||||||
value: 'DENY',
|
value: 'DENY',
|
||||||
@@ -70,7 +73,19 @@ const nextConfig = {
|
|||||||
key: 'Permissions-Policy',
|
key: 'Permissions-Policy',
|
||||||
value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()',
|
value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()',
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
|
|
||||||
|
if (isProd) {
|
||||||
|
secureHeaders.push({
|
||||||
|
key: 'Strict-Transport-Security',
|
||||||
|
value: 'max-age=63072000; includeSubDomains; preload',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/:path*',
|
||||||
|
headers: secureHeaders,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
@@ -373,33 +388,31 @@ const nextConfig = {
|
|||||||
source: '/vcf/michael-bodemer',
|
source: '/vcf/michael-bodemer',
|
||||||
destination: '/michael-bodemer.vcf',
|
destination: '/michael-bodemer.vcf',
|
||||||
permanent: true,
|
permanent: true,
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
images: {
|
images: {
|
||||||
loader: 'custom',
|
formats: ['image/webp'],
|
||||||
loaderFile: './lib/imgproxy-loader.ts',
|
remotePatterns: [
|
||||||
dangerouslyAllowSVG: true,
|
{
|
||||||
contentDispositionType: "attachment",
|
protocol: 'https',
|
||||||
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
|
hostname: 'klz-cables.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: '*.klz-cables.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: 'http',
|
||||||
|
hostname: 'klz.localhost',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: 'http',
|
||||||
|
hostname: 'localhost',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
const umamiUrl =
|
|
||||||
process.env.UMAMI_API_ENDPOINT ||
|
|
||||||
process.env.UMAMI_SCRIPT_URL ||
|
|
||||||
process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL ||
|
|
||||||
'https://analytics.infra.mintel.me';
|
|
||||||
const glitchtipUrl = process.env.SENTRY_DSN
|
|
||||||
? new URL(process.env.SENTRY_DSN).origin
|
|
||||||
: 'https://errors.infra.mintel.me';
|
|
||||||
|
|
||||||
const directusUrl = process.env.INTERNAL_DIRECTUS_URL || process.env.DIRECTUS_URL || 'https://cms.klz-cables.com';
|
|
||||||
|
|
||||||
let imgproxyUrl = process.env.IMGPROXY_URL || 'http://klz-imgproxy:8080';
|
|
||||||
if (!imgproxyUrl.startsWith('http')) {
|
|
||||||
imgproxyUrl = `https://${imgproxyUrl}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
source: '/de/produkte',
|
source: '/de/produkte',
|
||||||
@@ -409,14 +422,6 @@ const nextConfig = {
|
|||||||
source: '/de/produkte/:path*',
|
source: '/de/produkte/:path*',
|
||||||
destination: '/de/products/:path*',
|
destination: '/de/products/:path*',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
source: '/cms/:path*',
|
|
||||||
destination: `${directusUrl}/:path*`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
source: '/_img/:path*',
|
|
||||||
destination: `${imgproxyUrl}/:path*`,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -425,6 +430,6 @@ const withAnalyzer = withBundleAnalyzer({
|
|||||||
enabled: process.env.ANALYZE === 'true',
|
enabled: process.env.ANALYZE === 'true',
|
||||||
});
|
});
|
||||||
|
|
||||||
export default withAnalyzer(withMintelConfig(nextConfig, {
|
export default withPayload(withAnalyzer(withMintelConfig(nextConfig, {
|
||||||
hideSourceMaps: true,
|
hideSourceMaps: true,
|
||||||
}));
|
})));
|
||||||
|
|||||||
13328
openrouter_models.json
Normal file
13328
openrouter_models.json
Normal file
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@@ -4,11 +4,15 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@10.18.3",
|
"packageManager": "pnpm@10.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@directus/sdk": "^21.0.0",
|
|
||||||
"@mintel/mail": "1.8.3",
|
"@mintel/mail": "1.8.3",
|
||||||
"@mintel/next-config": "1.8.3",
|
"@mintel/next-config": "1.8.3",
|
||||||
"@mintel/next-feedback": "1.8.10",
|
"@mintel/next-feedback": "1.8.10",
|
||||||
"@mintel/next-utils": "^1.7.15",
|
"@mintel/next-utils": "^1.7.15",
|
||||||
|
"@payloadcms/db-postgres": "^3.77.0",
|
||||||
|
"@payloadcms/email-nodemailer": "^3.77.0",
|
||||||
|
"@payloadcms/next": "^3.77.0",
|
||||||
|
"@payloadcms/richtext-lexical": "^3.77.0",
|
||||||
|
"@payloadcms/ui": "^3.77.0",
|
||||||
"@react-email/components": "^1.0.7",
|
"@react-email/components": "^1.0.7",
|
||||||
"@react-pdf/renderer": "^4.3.2",
|
"@react-pdf/renderer": "^4.3.2",
|
||||||
"@sentry/nextjs": "^10.39.0",
|
"@sentry/nextjs": "^10.39.0",
|
||||||
@@ -16,6 +20,7 @@
|
|||||||
"axios": "^1.13.5",
|
"axios": "^1.13.5",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^12.34.0",
|
"framer-motion": "^12.34.0",
|
||||||
|
"graphql": "^16.12.0",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"i18next": "^25.7.3",
|
"i18next": "^25.7.3",
|
||||||
"import-in-the-middle": "^1.11.0",
|
"import-in-the-middle": "^1.11.0",
|
||||||
@@ -26,6 +31,7 @@
|
|||||||
"next-intl": "^4.8.2",
|
"next-intl": "^4.8.2",
|
||||||
"next-mdx-remote": "^5.0.0",
|
"next-mdx-remote": "^5.0.0",
|
||||||
"nodemailer": "^7.0.12",
|
"nodemailer": "^7.0.12",
|
||||||
|
"payload": "^3.77.0",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"pino": "^10.3.0",
|
"pino": "^10.3.0",
|
||||||
"pino-pretty": "^13.1.3",
|
"pino-pretty": "^13.1.3",
|
||||||
@@ -85,9 +91,8 @@
|
|||||||
"vitest": "^4.0.16"
|
"vitest": "^4.0.16"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "docker network create infra 2>/dev/null || true && echo '\\n🚀 Development Environment Starting...\\n\\n📱 App (Next.js): http://localhost:3000\\n📱 App (Traefik): http://klz.localhost\\n🗄️ CMS: http://cms.klz.localhost/admin\\n🚦 Traefik: http://localhost:8080\\n\\n(Press Ctrl+C to stop)\\n' && docker-compose down --remove-orphans && docker-compose up --build klz-app klz-cms klz-db klz-gatekeeper",
|
"dev": "docker network create infra 2>/dev/null || true && echo \"\\n🚀 Dockerized Environment Starting...\\n\" && COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml up klz-app klz-db",
|
||||||
"dev:infra": "docker network create infra 2>/dev/null || true && docker-compose up -d klz-cms klz-db klz-gatekeeper",
|
"dev:infra": "docker-compose up klz-db klz-gatekeeper",
|
||||||
"dev:local": "next dev",
|
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
|
|||||||
702
payload-types.ts
Normal file
702
payload-types.ts
Normal file
@@ -0,0 +1,702 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* This file was automatically generated by Payload.
|
||||||
|
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||||
|
* and re-run `payload generate:types` to regenerate this file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supported timezones in IANA format.
|
||||||
|
*
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "supportedTimezones".
|
||||||
|
*/
|
||||||
|
export type SupportedTimezones =
|
||||||
|
| 'Pacific/Midway'
|
||||||
|
| 'Pacific/Niue'
|
||||||
|
| 'Pacific/Honolulu'
|
||||||
|
| 'Pacific/Rarotonga'
|
||||||
|
| 'America/Anchorage'
|
||||||
|
| 'Pacific/Gambier'
|
||||||
|
| 'America/Los_Angeles'
|
||||||
|
| 'America/Tijuana'
|
||||||
|
| 'America/Denver'
|
||||||
|
| 'America/Phoenix'
|
||||||
|
| 'America/Chicago'
|
||||||
|
| 'America/Guatemala'
|
||||||
|
| 'America/New_York'
|
||||||
|
| 'America/Bogota'
|
||||||
|
| 'America/Caracas'
|
||||||
|
| 'America/Santiago'
|
||||||
|
| 'America/Buenos_Aires'
|
||||||
|
| 'America/Sao_Paulo'
|
||||||
|
| 'Atlantic/South_Georgia'
|
||||||
|
| 'Atlantic/Azores'
|
||||||
|
| 'Atlantic/Cape_Verde'
|
||||||
|
| 'Europe/London'
|
||||||
|
| 'Europe/Berlin'
|
||||||
|
| 'Africa/Lagos'
|
||||||
|
| 'Europe/Athens'
|
||||||
|
| 'Africa/Cairo'
|
||||||
|
| 'Europe/Moscow'
|
||||||
|
| 'Asia/Riyadh'
|
||||||
|
| 'Asia/Dubai'
|
||||||
|
| 'Asia/Baku'
|
||||||
|
| 'Asia/Karachi'
|
||||||
|
| 'Asia/Tashkent'
|
||||||
|
| 'Asia/Calcutta'
|
||||||
|
| 'Asia/Dhaka'
|
||||||
|
| 'Asia/Almaty'
|
||||||
|
| 'Asia/Jakarta'
|
||||||
|
| 'Asia/Bangkok'
|
||||||
|
| 'Asia/Shanghai'
|
||||||
|
| 'Asia/Singapore'
|
||||||
|
| 'Asia/Tokyo'
|
||||||
|
| 'Asia/Seoul'
|
||||||
|
| 'Australia/Brisbane'
|
||||||
|
| 'Australia/Sydney'
|
||||||
|
| 'Pacific/Guam'
|
||||||
|
| 'Pacific/Noumea'
|
||||||
|
| 'Pacific/Auckland'
|
||||||
|
| 'Pacific/Fiji';
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
auth: {
|
||||||
|
users: UserAuthOperations;
|
||||||
|
};
|
||||||
|
blocks: {};
|
||||||
|
collections: {
|
||||||
|
users: User;
|
||||||
|
media: Media;
|
||||||
|
posts: Post;
|
||||||
|
'form-submissions': FormSubmission;
|
||||||
|
products: Product;
|
||||||
|
pages: Page;
|
||||||
|
'payload-kv': PayloadKv;
|
||||||
|
'payload-locked-documents': PayloadLockedDocument;
|
||||||
|
'payload-preferences': PayloadPreference;
|
||||||
|
'payload-migrations': PayloadMigration;
|
||||||
|
};
|
||||||
|
collectionsJoins: {};
|
||||||
|
collectionsSelect: {
|
||||||
|
users: UsersSelect<false> | UsersSelect<true>;
|
||||||
|
media: MediaSelect<false> | MediaSelect<true>;
|
||||||
|
posts: PostsSelect<false> | PostsSelect<true>;
|
||||||
|
'form-submissions': FormSubmissionsSelect<false> | FormSubmissionsSelect<true>;
|
||||||
|
products: ProductsSelect<false> | ProductsSelect<true>;
|
||||||
|
pages: PagesSelect<false> | PagesSelect<true>;
|
||||||
|
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
|
||||||
|
'payload-locked-documents':
|
||||||
|
| PayloadLockedDocumentsSelect<false>
|
||||||
|
| PayloadLockedDocumentsSelect<true>;
|
||||||
|
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||||
|
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||||
|
};
|
||||||
|
db: {
|
||||||
|
defaultIDType: number;
|
||||||
|
};
|
||||||
|
fallbackLocale: null;
|
||||||
|
globals: {};
|
||||||
|
globalsSelect: {};
|
||||||
|
locale: null;
|
||||||
|
user: User;
|
||||||
|
jobs: {
|
||||||
|
tasks: unknown;
|
||||||
|
workflows: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export interface UserAuthOperations {
|
||||||
|
forgotPassword: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
login: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
registerFirstUser: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
unlock: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "users".
|
||||||
|
*/
|
||||||
|
export interface User {
|
||||||
|
id: number;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
email: string;
|
||||||
|
resetPasswordToken?: string | null;
|
||||||
|
resetPasswordExpiration?: string | null;
|
||||||
|
salt?: string | null;
|
||||||
|
hash?: string | null;
|
||||||
|
loginAttempts?: number | null;
|
||||||
|
lockUntil?: string | null;
|
||||||
|
sessions?:
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
createdAt?: string | null;
|
||||||
|
expiresAt: string;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
password?: string | null;
|
||||||
|
collection: 'users';
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "media".
|
||||||
|
*/
|
||||||
|
export interface Media {
|
||||||
|
id: number;
|
||||||
|
alt: string;
|
||||||
|
caption?: string | null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
url?: string | null;
|
||||||
|
thumbnailURL?: string | null;
|
||||||
|
filename?: string | null;
|
||||||
|
mimeType?: string | null;
|
||||||
|
filesize?: number | null;
|
||||||
|
width?: number | null;
|
||||||
|
height?: number | null;
|
||||||
|
focalX?: number | null;
|
||||||
|
focalY?: number | null;
|
||||||
|
sizes?: {
|
||||||
|
thumbnail?: {
|
||||||
|
url?: string | null;
|
||||||
|
width?: number | null;
|
||||||
|
height?: number | null;
|
||||||
|
mimeType?: string | null;
|
||||||
|
filesize?: number | null;
|
||||||
|
filename?: string | null;
|
||||||
|
};
|
||||||
|
card?: {
|
||||||
|
url?: string | null;
|
||||||
|
width?: number | null;
|
||||||
|
height?: number | null;
|
||||||
|
mimeType?: string | null;
|
||||||
|
filesize?: number | null;
|
||||||
|
filename?: string | null;
|
||||||
|
};
|
||||||
|
tablet?: {
|
||||||
|
url?: string | null;
|
||||||
|
width?: number | null;
|
||||||
|
height?: number | null;
|
||||||
|
mimeType?: string | null;
|
||||||
|
filesize?: number | null;
|
||||||
|
filename?: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "posts".
|
||||||
|
*/
|
||||||
|
export interface Post {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
/**
|
||||||
|
* A short summary for blog feed cards and SEO.
|
||||||
|
*/
|
||||||
|
excerpt?: string | null;
|
||||||
|
/**
|
||||||
|
* Future dates will schedule the post to publish automatically.
|
||||||
|
*/
|
||||||
|
date: string;
|
||||||
|
/**
|
||||||
|
* The primary Hero image used for headers and OpenGraph previews.
|
||||||
|
*/
|
||||||
|
featuredImage?: (number | null) | Media;
|
||||||
|
locale: 'en' | 'de';
|
||||||
|
/**
|
||||||
|
* Used for tag bucketing (e.g. "Kabel Technologie").
|
||||||
|
*/
|
||||||
|
category?: string | null;
|
||||||
|
content?: {
|
||||||
|
root: {
|
||||||
|
type: string;
|
||||||
|
children: {
|
||||||
|
type: any;
|
||||||
|
version: number;
|
||||||
|
[k: string]: unknown;
|
||||||
|
}[];
|
||||||
|
direction: ('ltr' | 'rtl') | null;
|
||||||
|
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||||
|
indent: number;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
[k: string]: unknown;
|
||||||
|
} | null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
_status?: ('draft' | 'published') | null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Captured leads from Contact and Product Quote forms.
|
||||||
|
*
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "form-submissions".
|
||||||
|
*/
|
||||||
|
export interface FormSubmission {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
type: 'contact' | 'product_quote';
|
||||||
|
/**
|
||||||
|
* The specific KLZ product the user requested a quote for.
|
||||||
|
*/
|
||||||
|
productName?: string | null;
|
||||||
|
message: string;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "products".
|
||||||
|
*/
|
||||||
|
export interface Product {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
sku: string;
|
||||||
|
slug: string;
|
||||||
|
description: string;
|
||||||
|
locale: 'en' | 'de';
|
||||||
|
categories: {
|
||||||
|
category?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[];
|
||||||
|
/**
|
||||||
|
* The primary thumbnail used in list views.
|
||||||
|
*/
|
||||||
|
featuredImage?: (number | null) | Media;
|
||||||
|
images?: (number | Media)[] | null;
|
||||||
|
application?: {
|
||||||
|
root: {
|
||||||
|
type: string;
|
||||||
|
children: {
|
||||||
|
type: any;
|
||||||
|
version: number;
|
||||||
|
[k: string]: unknown;
|
||||||
|
}[];
|
||||||
|
direction: ('ltr' | 'rtl') | null;
|
||||||
|
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||||
|
indent: number;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
[k: string]: unknown;
|
||||||
|
} | null;
|
||||||
|
content?: {
|
||||||
|
root: {
|
||||||
|
type: string;
|
||||||
|
children: {
|
||||||
|
type: any;
|
||||||
|
version: number;
|
||||||
|
[k: string]: unknown;
|
||||||
|
}[];
|
||||||
|
direction: ('ltr' | 'rtl') | null;
|
||||||
|
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||||
|
indent: number;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
[k: string]: unknown;
|
||||||
|
} | null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
_status?: ('draft' | 'published') | null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "pages".
|
||||||
|
*/
|
||||||
|
export interface Page {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
locale: 'en' | 'de';
|
||||||
|
excerpt?: string | null;
|
||||||
|
featuredImage?: (number | null) | Media;
|
||||||
|
content: {
|
||||||
|
root: {
|
||||||
|
type: string;
|
||||||
|
children: {
|
||||||
|
type: any;
|
||||||
|
version: number;
|
||||||
|
[k: string]: unknown;
|
||||||
|
}[];
|
||||||
|
direction: ('ltr' | 'rtl') | null;
|
||||||
|
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||||
|
indent: number;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: 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
|
||||||
|
* via the `definition` "payload-locked-documents".
|
||||||
|
*/
|
||||||
|
export interface PayloadLockedDocument {
|
||||||
|
id: number;
|
||||||
|
document?:
|
||||||
|
| ({
|
||||||
|
relationTo: 'users';
|
||||||
|
value: number | User;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'media';
|
||||||
|
value: number | Media;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'posts';
|
||||||
|
value: number | Post;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'form-submissions';
|
||||||
|
value: number | FormSubmission;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'products';
|
||||||
|
value: number | Product;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'pages';
|
||||||
|
value: number | Page;
|
||||||
|
} | null);
|
||||||
|
globalSlug?: string | null;
|
||||||
|
user: {
|
||||||
|
relationTo: 'users';
|
||||||
|
value: number | User;
|
||||||
|
};
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-preferences".
|
||||||
|
*/
|
||||||
|
export interface PayloadPreference {
|
||||||
|
id: number;
|
||||||
|
user: {
|
||||||
|
relationTo: 'users';
|
||||||
|
value: number | User;
|
||||||
|
};
|
||||||
|
key?: string | null;
|
||||||
|
value?:
|
||||||
|
| {
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
| unknown[]
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-migrations".
|
||||||
|
*/
|
||||||
|
export interface PayloadMigration {
|
||||||
|
id: number;
|
||||||
|
name?: string | null;
|
||||||
|
batch?: number | null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "users_select".
|
||||||
|
*/
|
||||||
|
export interface UsersSelect<T extends boolean = true> {
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
email?: T;
|
||||||
|
resetPasswordToken?: T;
|
||||||
|
resetPasswordExpiration?: T;
|
||||||
|
salt?: T;
|
||||||
|
hash?: T;
|
||||||
|
loginAttempts?: T;
|
||||||
|
lockUntil?: T;
|
||||||
|
sessions?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
id?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
expiresAt?: T;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "media_select".
|
||||||
|
*/
|
||||||
|
export interface MediaSelect<T extends boolean = true> {
|
||||||
|
alt?: T;
|
||||||
|
caption?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
url?: T;
|
||||||
|
thumbnailURL?: T;
|
||||||
|
filename?: T;
|
||||||
|
mimeType?: T;
|
||||||
|
filesize?: T;
|
||||||
|
width?: T;
|
||||||
|
height?: T;
|
||||||
|
focalX?: T;
|
||||||
|
focalY?: T;
|
||||||
|
sizes?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
thumbnail?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
url?: T;
|
||||||
|
width?: T;
|
||||||
|
height?: T;
|
||||||
|
mimeType?: T;
|
||||||
|
filesize?: T;
|
||||||
|
filename?: T;
|
||||||
|
};
|
||||||
|
card?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
url?: T;
|
||||||
|
width?: T;
|
||||||
|
height?: T;
|
||||||
|
mimeType?: T;
|
||||||
|
filesize?: T;
|
||||||
|
filename?: T;
|
||||||
|
};
|
||||||
|
tablet?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
url?: T;
|
||||||
|
width?: T;
|
||||||
|
height?: T;
|
||||||
|
mimeType?: T;
|
||||||
|
filesize?: T;
|
||||||
|
filename?: T;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "posts_select".
|
||||||
|
*/
|
||||||
|
export interface PostsSelect<T extends boolean = true> {
|
||||||
|
title?: T;
|
||||||
|
slug?: T;
|
||||||
|
excerpt?: T;
|
||||||
|
date?: T;
|
||||||
|
featuredImage?: T;
|
||||||
|
locale?: T;
|
||||||
|
category?: T;
|
||||||
|
content?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
_status?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "form-submissions_select".
|
||||||
|
*/
|
||||||
|
export interface FormSubmissionsSelect<T extends boolean = true> {
|
||||||
|
name?: T;
|
||||||
|
email?: T;
|
||||||
|
type?: T;
|
||||||
|
productName?: T;
|
||||||
|
message?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "products_select".
|
||||||
|
*/
|
||||||
|
export interface ProductsSelect<T extends boolean = true> {
|
||||||
|
title?: T;
|
||||||
|
sku?: T;
|
||||||
|
slug?: T;
|
||||||
|
description?: T;
|
||||||
|
locale?: T;
|
||||||
|
categories?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
category?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
featuredImage?: T;
|
||||||
|
images?: T;
|
||||||
|
application?: T;
|
||||||
|
content?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
_status?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "pages_select".
|
||||||
|
*/
|
||||||
|
export interface PagesSelect<T extends boolean = true> {
|
||||||
|
title?: T;
|
||||||
|
slug?: T;
|
||||||
|
locale?: T;
|
||||||
|
excerpt?: T;
|
||||||
|
featuredImage?: T;
|
||||||
|
content?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: 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
|
||||||
|
* via the `definition` "payload-locked-documents_select".
|
||||||
|
*/
|
||||||
|
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
|
||||||
|
document?: T;
|
||||||
|
globalSlug?: T;
|
||||||
|
user?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-preferences_select".
|
||||||
|
*/
|
||||||
|
export interface PayloadPreferencesSelect<T extends boolean = true> {
|
||||||
|
user?: T;
|
||||||
|
key?: T;
|
||||||
|
value?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-migrations_select".
|
||||||
|
*/
|
||||||
|
export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||||
|
name?: T;
|
||||||
|
batch?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "StatsBlock".
|
||||||
|
*/
|
||||||
|
export interface StatsBlock {
|
||||||
|
stats: {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
id?: string | null;
|
||||||
|
}[];
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'stats';
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "SplitHeadingBlock".
|
||||||
|
*/
|
||||||
|
export interface SplitHeadingBlock {
|
||||||
|
title: string;
|
||||||
|
id?: string | null;
|
||||||
|
level?: ('h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6') | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'splitHeading';
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "ProductTabsBlock".
|
||||||
|
*/
|
||||||
|
export interface ProductTabsBlock {
|
||||||
|
technicalItems?:
|
||||||
|
| {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
unit?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
voltageTables?:
|
||||||
|
| {
|
||||||
|
voltageLabel: string;
|
||||||
|
metaItems?:
|
||||||
|
| {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
unit?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
columns?:
|
||||||
|
| {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
rows?:
|
||||||
|
| {
|
||||||
|
configuration: string;
|
||||||
|
cells?:
|
||||||
|
| {
|
||||||
|
value?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'productTabs';
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "auth".
|
||||||
|
*/
|
||||||
|
export interface Auth {
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'payload' {
|
||||||
|
export interface GeneratedTypes extends Config {}
|
||||||
|
}
|
||||||
66
payload.config.ts
Normal file
66
payload.config.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { buildConfig } from 'payload';
|
||||||
|
import { postgresAdapter } from '@payloadcms/db-postgres';
|
||||||
|
import { lexicalEditor } from '@payloadcms/richtext-lexical';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { nodemailerAdapter } from '@payloadcms/email-nodemailer';
|
||||||
|
import { BlocksFeature } from '@payloadcms/richtext-lexical';
|
||||||
|
import { payloadBlocks } from './src/payload/blocks/allBlocks';
|
||||||
|
|
||||||
|
// Disable sharp cache to prevent memory leaks in Docker
|
||||||
|
sharp.cache(false);
|
||||||
|
|
||||||
|
import { Users } from './src/payload/collections/Users';
|
||||||
|
import { Media } from './src/payload/collections/Media';
|
||||||
|
import { Posts } from './src/payload/collections/Posts';
|
||||||
|
import { FormSubmissions } from './src/payload/collections/FormSubmissions';
|
||||||
|
import { Products } from './src/payload/collections/Products';
|
||||||
|
import { Pages } from './src/payload/collections/Pages';
|
||||||
|
|
||||||
|
const filename = fileURLToPath(import.meta.url);
|
||||||
|
const dirname = path.dirname(filename);
|
||||||
|
|
||||||
|
export default buildConfig({
|
||||||
|
admin: {
|
||||||
|
user: Users.slug,
|
||||||
|
importMap: {
|
||||||
|
baseDir: path.resolve(dirname),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
collections: [Users, Media, Posts, FormSubmissions, Products, Pages],
|
||||||
|
editor: lexicalEditor({
|
||||||
|
features: ({ defaultFeatures }) => [
|
||||||
|
...defaultFeatures,
|
||||||
|
BlocksFeature({
|
||||||
|
blocks: payloadBlocks,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
secret: process.env.PAYLOAD_SECRET || 'fallback-secret-for-dev',
|
||||||
|
typescript: {
|
||||||
|
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||||
|
},
|
||||||
|
db: postgresAdapter({
|
||||||
|
pool: {
|
||||||
|
connectionString:
|
||||||
|
process.env.DATABASE_URI ||
|
||||||
|
process.env.POSTGRES_URI ||
|
||||||
|
'postgresql://payload:120in09oenaoinsd9iaidon@localhost:5432/payload',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
email: nodemailerAdapter({
|
||||||
|
defaultFromAddress: process.env.MAIL_FROM?.replace(/.*<|>.*/g, '') || 'postmaster@mg.mintel.me',
|
||||||
|
defaultFromName: process.env.MAIL_FROM?.split('<')[0]?.trim() || 'KLZ Cables',
|
||||||
|
transportOptions: {
|
||||||
|
host: process.env.MAIL_HOST || 'smtp.eu.mailgun.org',
|
||||||
|
port: Number(process.env.MAIL_PORT) || 587,
|
||||||
|
auth: {
|
||||||
|
user: process.env.MAIL_USERNAME,
|
||||||
|
pass: process.env.MAIL_PASSWORD,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
sharp,
|
||||||
|
plugins: [],
|
||||||
|
});
|
||||||
2137
pnpm-lock.yaml
generated
2137
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,65 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
ENV=$1
|
|
||||||
REMOTE_HOST="root@alpha.mintel.me"
|
|
||||||
REMOTE_DIR="/home/deploy/sites/klz-cables.com"
|
|
||||||
|
|
||||||
if [ -z "$ENV" ]; then
|
|
||||||
echo "Usage: ./scripts/cms-apply.sh [local|testing|staging|production]"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
PRJ_ID=$(jq -r .name package.json | sed 's/@mintel\///' | sed 's/\.com$//' | sed 's/-nextjs$//' | sed 's/-cables$//')
|
|
||||||
|
|
||||||
case $ENV in
|
|
||||||
local)
|
|
||||||
CONTAINER=$(docker compose ps -q klz-cms)
|
|
||||||
if [ -z "$CONTAINER" ]; then
|
|
||||||
echo "❌ Local directus container not found."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "🚀 Applying schema locally..."
|
|
||||||
docker exec "$CONTAINER" npx directus schema apply /directus/schema/snapshot.yaml --yes
|
|
||||||
;;
|
|
||||||
testing|staging|production)
|
|
||||||
case $ENV in
|
|
||||||
testing) PROJECT_NAME="${PRJ_ID}-testing" ;;
|
|
||||||
staging) PROJECT_NAME="${PRJ_ID}-staging" ;;
|
|
||||||
production)
|
|
||||||
PROJECT_NAME="${PRJ_ID}-production"
|
|
||||||
OLD_PROJECT_NAME="${PRJ_ID}-prod" # Fallback for previous convention
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo "📤 Uploading snapshot to $ENV..."
|
|
||||||
scp ./directus/schema/snapshot.yaml "$REMOTE_HOST:$REMOTE_DIR/directus/schema/snapshot.yaml"
|
|
||||||
|
|
||||||
echo "🔍 Detecting remote container..."
|
|
||||||
REMOTE_CONTAINER=$(ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $PROJECT_NAME ps -q directus")
|
|
||||||
|
|
||||||
if [ -z "$REMOTE_CONTAINER" ] && [ -n "$OLD_PROJECT_NAME" ]; then
|
|
||||||
echo "⚠️ $PROJECT_NAME not found, trying fallback $OLD_PROJECT_NAME..."
|
|
||||||
REMOTE_CONTAINER=$(ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $OLD_PROJECT_NAME ps -q directus")
|
|
||||||
if [ -n "$REMOTE_CONTAINER" ]; then
|
|
||||||
PROJECT_NAME=$OLD_PROJECT_NAME
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$REMOTE_CONTAINER" ]; then
|
|
||||||
echo "❌ Remote container for $ENV not found (checked $PROJECT_NAME)."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "🚀 Applying schema to $ENV..."
|
|
||||||
ssh "$REMOTE_HOST" "docker exec $REMOTE_CONTAINER npx directus schema apply /directus/schema/snapshot.yaml --yes"
|
|
||||||
|
|
||||||
echo "🔄 Restarting Directus to clear cache..."
|
|
||||||
ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $PROJECT_NAME restart directus"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "❌ Invalid environment."
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo "✨ Schema apply complete!"
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Detect local container
|
|
||||||
LOCAL_CONTAINER=$(docker compose ps -q directus)
|
|
||||||
|
|
||||||
if [ -z "$LOCAL_CONTAINER" ]; then
|
|
||||||
echo "❌ Local directus container not found. Is it running?"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "📸 Creating schema snapshot..."
|
|
||||||
# Note: we save it to the mounted volume path inside the container
|
|
||||||
docker exec "$LOCAL_CONTAINER" npx directus schema snapshot /directus/schema/snapshot.yaml
|
|
||||||
|
|
||||||
echo "✅ Snapshot saved to ./directus/schema/snapshot.yaml"
|
|
||||||
140
scripts/migrate-mdx.ts
Normal file
140
scripts/migrate-mdx.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import { getPayload } from 'payload';
|
||||||
|
import configPromise from '@payload-config';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import matter from 'gray-matter';
|
||||||
|
import { parseMarkdownToLexical } from '../src/payload/utils/lexicalParser';
|
||||||
|
|
||||||
|
async function mapImageToMediaId(payload: any, imagePath: string): Promise<string | null> {
|
||||||
|
if (!imagePath) return null;
|
||||||
|
const filename = path.basename(imagePath);
|
||||||
|
|
||||||
|
const media = await payload.find({
|
||||||
|
collection: 'media',
|
||||||
|
where: {
|
||||||
|
filename: {
|
||||||
|
equals: filename,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (media.docs.length > 0) {
|
||||||
|
return media.docs[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-ingest missing images from legacy public/ directory
|
||||||
|
const cleanPath = imagePath.startsWith('/') ? imagePath.slice(1) : imagePath;
|
||||||
|
const fullPath = path.join(process.cwd(), 'public', cleanPath);
|
||||||
|
|
||||||
|
if (fs.existsSync(fullPath)) {
|
||||||
|
try {
|
||||||
|
console.log(`[Blog Migration] 📤 Ingesting missing Media into Payload: ${filename}`);
|
||||||
|
const newMedia = await payload.create({
|
||||||
|
collection: 'media',
|
||||||
|
data: {
|
||||||
|
alt: filename.replace(/[-_]/g, ' ').replace(/\.[^/.]+$/, ''), // create a human readable alt text
|
||||||
|
},
|
||||||
|
filePath: fullPath,
|
||||||
|
});
|
||||||
|
return newMedia.id;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`[Blog Migration] ❌ Failed to ingest ${filename}:`, err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`[Blog Migration] ⚠️ Missing image entirely from disk: ${fullPath}`);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function migrateBlogPosts() {
|
||||||
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
|
||||||
|
const locales = ['en', 'de'];
|
||||||
|
for (const locale of locales) {
|
||||||
|
const postsDir = path.join(process.cwd(), 'data', 'blog', locale);
|
||||||
|
if (!fs.existsSync(postsDir)) continue;
|
||||||
|
|
||||||
|
const files = fs.readdirSync(postsDir);
|
||||||
|
for (const file of files) {
|
||||||
|
if (!file.endsWith('.mdx')) continue;
|
||||||
|
|
||||||
|
const slug = file.replace(/\.mdx$/, '');
|
||||||
|
const filePath = path.join(postsDir, file);
|
||||||
|
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||||
|
const { data, content } = matter(fileContent);
|
||||||
|
|
||||||
|
console.log(`Migrating ${locale}/${slug}...`);
|
||||||
|
|
||||||
|
const lexicalBlocks = parseMarkdownToLexical(content);
|
||||||
|
const lexicalAST = {
|
||||||
|
root: {
|
||||||
|
type: 'root',
|
||||||
|
format: '',
|
||||||
|
indent: 0,
|
||||||
|
version: 1,
|
||||||
|
children: lexicalBlocks,
|
||||||
|
direction: 'ltr',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const publishDate = data.date ? new Date(data.date).toISOString() : new Date().toISOString();
|
||||||
|
const status = data.public === false ? 'draft' : 'published';
|
||||||
|
|
||||||
|
let featuredImageId = null;
|
||||||
|
if (data.featuredImage || data.image) {
|
||||||
|
featuredImageId = await mapImageToMediaId(payload, data.featuredImage || data.image);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find existing post
|
||||||
|
const existing = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
where: { slug: { equals: slug }, locale: { equals: locale } as any },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (slug.includes('welcome-to-the-future')) {
|
||||||
|
console.log(`\n--- AST for ${slug} ---`);
|
||||||
|
console.log(JSON.stringify(lexicalAST, null, 2));
|
||||||
|
console.log(`-----------------------\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing.docs.length > 0) {
|
||||||
|
await payload.update({
|
||||||
|
collection: 'posts',
|
||||||
|
id: existing.docs[0].id,
|
||||||
|
data: {
|
||||||
|
content: lexicalAST as any,
|
||||||
|
_status: status as any,
|
||||||
|
...(featuredImageId ? { featuredImage: featuredImageId } : {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`✅ AST Components & Image RE-INJECTED for ${slug}`);
|
||||||
|
} else {
|
||||||
|
await payload.create({
|
||||||
|
collection: 'posts',
|
||||||
|
data: {
|
||||||
|
title: data.title,
|
||||||
|
slug: slug,
|
||||||
|
locale: locale,
|
||||||
|
date: publishDate,
|
||||||
|
category: data.category || '',
|
||||||
|
excerpt: data.excerpt || '',
|
||||||
|
content: lexicalAST as any,
|
||||||
|
_status: status as any,
|
||||||
|
...(featuredImageId ? { featuredImage: featuredImageId } : {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`✅ Created ${slug}`);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`❌ Failed ${slug}`, err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Migration completed.');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
migrateBlogPosts().catch(console.error);
|
||||||
135
scripts/migrate-pages.ts
Normal file
135
scripts/migrate-pages.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { getPayload } from 'payload';
|
||||||
|
import configPromise from '@payload-config';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import matter from 'gray-matter';
|
||||||
|
import { parseMarkdownToLexical } from '../src/payload/utils/lexicalParser';
|
||||||
|
|
||||||
|
async function mapImageToMediaId(payload: any, imagePath: string): Promise<string | null> {
|
||||||
|
if (!imagePath) return null;
|
||||||
|
const filename = path.basename(imagePath);
|
||||||
|
|
||||||
|
const media = await payload.find({
|
||||||
|
collection: 'media',
|
||||||
|
where: {
|
||||||
|
filename: {
|
||||||
|
equals: filename,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (media.docs.length > 0) {
|
||||||
|
return media.docs[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-ingest missing images from legacy public/ directory
|
||||||
|
const cleanPath = imagePath.startsWith('/') ? imagePath.slice(1) : imagePath;
|
||||||
|
const fullPath = path.join(process.cwd(), 'public', cleanPath);
|
||||||
|
|
||||||
|
if (fs.existsSync(fullPath)) {
|
||||||
|
try {
|
||||||
|
console.log(`[Blog Migration] 📤 Ingesting missing Media into Payload: ${filename}`);
|
||||||
|
const newMedia = await payload.create({
|
||||||
|
collection: 'media',
|
||||||
|
data: {
|
||||||
|
alt: filename.replace(/[-_]/g, ' ').replace(/\.[^/.]+$/, ''), // create a human readable alt text
|
||||||
|
},
|
||||||
|
filePath: fullPath,
|
||||||
|
});
|
||||||
|
return newMedia.id;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`[Blog Migration] ❌ Failed to ingest ${filename}:`, err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`[Blog Migration] ⚠️ Missing image entirely from disk: ${fullPath}`);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function migrateBlogPages() {
|
||||||
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
|
||||||
|
const locales = ['en', 'de'];
|
||||||
|
for (const locale of locales) {
|
||||||
|
const pagesDir = path.join(process.cwd(), 'data', 'pages', locale);
|
||||||
|
if (!fs.existsSync(pagesDir)) continue;
|
||||||
|
|
||||||
|
const files = fs.readdirSync(pagesDir);
|
||||||
|
for (const file of files) {
|
||||||
|
if (!file.endsWith('.mdx')) continue;
|
||||||
|
|
||||||
|
const slug = file.replace(/\.mdx$/, '');
|
||||||
|
const filePath = path.join(pagesDir, file);
|
||||||
|
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||||
|
const { data, content } = matter(fileContent);
|
||||||
|
|
||||||
|
console.log(`Migrating ${locale}/${slug}...`);
|
||||||
|
|
||||||
|
const lexicalBlocks = parseMarkdownToLexical(content);
|
||||||
|
const lexicalAST = {
|
||||||
|
root: {
|
||||||
|
type: 'root',
|
||||||
|
format: '',
|
||||||
|
indent: 0,
|
||||||
|
version: 1,
|
||||||
|
children: lexicalBlocks,
|
||||||
|
direction: 'ltr',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const status = data.public === false ? 'draft' : 'published';
|
||||||
|
|
||||||
|
let featuredImageId = null;
|
||||||
|
if (data.featuredImage || data.image) {
|
||||||
|
featuredImageId = await mapImageToMediaId(payload, data.featuredImage || data.image);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find existing post
|
||||||
|
const existing = await payload.find({
|
||||||
|
collection: 'pages',
|
||||||
|
where: { slug: { equals: slug }, locale: { equals: locale } as any },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (slug.includes('welcome-to-the-future')) {
|
||||||
|
console.log(`\n--- AST for ${slug} ---`);
|
||||||
|
console.log(JSON.stringify(lexicalAST, null, 2));
|
||||||
|
console.log(`-----------------------\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing.docs.length > 0) {
|
||||||
|
await payload.update({
|
||||||
|
collection: 'pages',
|
||||||
|
id: existing.docs[0].id,
|
||||||
|
data: {
|
||||||
|
content: lexicalAST as any,
|
||||||
|
...(featuredImageId ? { featuredImage: featuredImageId } : {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`✅ AST Components & Image RE-INJECTED for ${slug}`);
|
||||||
|
} else {
|
||||||
|
await payload.create({
|
||||||
|
collection: 'pages',
|
||||||
|
data: {
|
||||||
|
title: data.title,
|
||||||
|
slug: slug,
|
||||||
|
locale: locale,
|
||||||
|
excerpt: data.excerpt || '',
|
||||||
|
content: lexicalAST as any,
|
||||||
|
...(featuredImageId ? { featuredImage: featuredImageId } : {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`✅ Created ${slug}`);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`❌ Failed ${slug}`, err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Migration completed.');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
migrateBlogPages().catch(console.error);
|
||||||
153
scripts/migrate-products.ts
Normal file
153
scripts/migrate-products.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import { getPayload } from 'payload';
|
||||||
|
import configPromise from '../payload.config';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import matter from 'gray-matter';
|
||||||
|
import { parseMarkdownToLexical } from '../src/payload/utils/lexicalParser';
|
||||||
|
|
||||||
|
async function mapImageToMediaId(payload: any, imagePath: string): Promise<string | null> {
|
||||||
|
if (!imagePath) return null;
|
||||||
|
const filename = path.basename(imagePath);
|
||||||
|
|
||||||
|
// Exact match instead of substring to avoid matching "cable-black.jpg" with "cable.jpg"
|
||||||
|
const media = await payload.find({
|
||||||
|
collection: 'media',
|
||||||
|
where: {
|
||||||
|
filename: {
|
||||||
|
equals: filename,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (media.docs.length > 0) {
|
||||||
|
return media.docs[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanPath = imagePath.startsWith('/') ? imagePath.slice(1) : imagePath;
|
||||||
|
const fullPath = path.join(process.cwd(), 'public', cleanPath);
|
||||||
|
|
||||||
|
if (fs.existsSync(fullPath)) {
|
||||||
|
try {
|
||||||
|
console.log(`[Products Migration] 📤 Ingesting missing Media into Payload: ${filename}`);
|
||||||
|
const newMedia = await payload.create({
|
||||||
|
collection: 'media',
|
||||||
|
data: {
|
||||||
|
alt: filename.replace(/[-_]/g, ' ').replace(/\.[^/.]+$/, ''),
|
||||||
|
},
|
||||||
|
filePath: fullPath,
|
||||||
|
});
|
||||||
|
return newMedia.id;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`[Products Migration] ❌ Failed to ingest ${filename}:`, err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`[Products Migration] ⚠️ Missing image entirely from disk: ${fullPath}`);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function migrateProducts() {
|
||||||
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
const productLocales = ['en', 'de'];
|
||||||
|
|
||||||
|
for (const locale of productLocales) {
|
||||||
|
const productsDir = path.join(process.cwd(), 'data', 'products', locale);
|
||||||
|
if (!fs.existsSync(productsDir)) continue;
|
||||||
|
|
||||||
|
// Recursive file finder
|
||||||
|
const mdFiles: string[] = [];
|
||||||
|
const walk = (dir: string) => {
|
||||||
|
const files = fs.readdirSync(dir);
|
||||||
|
for (const file of files) {
|
||||||
|
const fullPath = path.join(dir, file);
|
||||||
|
const stat = fs.statSync(fullPath);
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
walk(fullPath);
|
||||||
|
} else if (file.endsWith('.mdx')) {
|
||||||
|
mdFiles.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
walk(productsDir);
|
||||||
|
|
||||||
|
for (const filePath of mdFiles) {
|
||||||
|
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||||
|
const { data, content } = matter(fileContent);
|
||||||
|
|
||||||
|
console.log(`Processing Product: [${locale.toUpperCase()}] ${data.title}`);
|
||||||
|
|
||||||
|
// 1. Process Images
|
||||||
|
const mediaIds = [];
|
||||||
|
if (data.images && Array.isArray(data.images)) {
|
||||||
|
for (const imgPath of data.images) {
|
||||||
|
const id = await mapImageToMediaId(payload, imgPath);
|
||||||
|
if (id) mediaIds.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Map Lexical AST for deeply nested components (like ProductTabs + Technical data)
|
||||||
|
const lexicalContent = parseMarkdownToLexical(content);
|
||||||
|
|
||||||
|
const wrapLexical = (blocks: any[]) => ({
|
||||||
|
root: {
|
||||||
|
type: 'root',
|
||||||
|
format: '',
|
||||||
|
indent: 0,
|
||||||
|
version: 1,
|
||||||
|
children: blocks,
|
||||||
|
direction: 'ltr',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Payload expects category objects via the 'category' key
|
||||||
|
const formattedCategories = Array.isArray(data.categories)
|
||||||
|
? data.categories.map((c: string) => ({ category: c }))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const productData = {
|
||||||
|
title: data.title,
|
||||||
|
sku: data.sku || path.basename(filePath, '.mdx'),
|
||||||
|
slug: path.basename(filePath, '.mdx'),
|
||||||
|
locale: locale as 'en' | 'de',
|
||||||
|
categories: formattedCategories,
|
||||||
|
description: data.description || '',
|
||||||
|
featuredImage: mediaIds.length > 0 ? mediaIds[0] : undefined,
|
||||||
|
images: mediaIds.length > 0 ? mediaIds : undefined,
|
||||||
|
content: wrapLexical(lexicalContent) as any,
|
||||||
|
application: data.application
|
||||||
|
? (wrapLexical(parseMarkdownToLexical(data.application)) as any)
|
||||||
|
: undefined,
|
||||||
|
_status: 'published' as any,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if product exists (by sku combined with locale, since slug may differ by language)
|
||||||
|
const existing = await payload.find({
|
||||||
|
collection: 'products',
|
||||||
|
where: {
|
||||||
|
and: [{ slug: { equals: productData.slug } }, { locale: { equals: locale } }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing.docs.length > 0) {
|
||||||
|
console.log(`Updating existing product ${productData.slug} (${locale})`);
|
||||||
|
await payload.update({
|
||||||
|
collection: 'products',
|
||||||
|
id: existing.docs[0].id,
|
||||||
|
data: productData,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(`Creating new product ${productData.slug} (${locale})`);
|
||||||
|
await payload.create({
|
||||||
|
collection: 'products',
|
||||||
|
data: productData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n✅ Products Migration Complete!`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
migrateProducts().catch(console.error);
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
import client, { ensureAuthenticated } from '../lib/directus';
|
|
||||||
import { updateSettings, uploadFiles } from '@directus/sdk';
|
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
|
|
||||||
// Helper for ESM __dirname
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
|
|
||||||
async function setupBranding() {
|
|
||||||
console.log('🎨 Refining Directus Branding for Premium Website Look...');
|
|
||||||
|
|
||||||
// 1. Authenticate
|
|
||||||
await ensureAuthenticated();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 2. Upload Assets (MIME FIXED)
|
|
||||||
console.log('📤 Re-uploading assets for clean IDs...');
|
|
||||||
|
|
||||||
const getMimeType = (filePath: string) => {
|
|
||||||
const ext = path.extname(filePath).toLowerCase();
|
|
||||||
switch (ext) {
|
|
||||||
case '.svg':
|
|
||||||
return 'image/svg+xml';
|
|
||||||
case '.png':
|
|
||||||
return 'image/png';
|
|
||||||
case '.jpg':
|
|
||||||
case '.jpeg':
|
|
||||||
return 'image/jpeg';
|
|
||||||
case '.ico':
|
|
||||||
return 'image/x-icon';
|
|
||||||
default:
|
|
||||||
return 'application/octet-stream';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const uploadAsset = async (filePath: string, title: string) => {
|
|
||||||
if (!fs.existsSync(filePath)) {
|
|
||||||
console.warn(`⚠️ File not found: ${filePath}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const mimeType = getMimeType(filePath);
|
|
||||||
const form = new FormData();
|
|
||||||
const fileBuffer = fs.readFileSync(filePath);
|
|
||||||
const blob = new Blob([fileBuffer], { type: mimeType });
|
|
||||||
form.append('file', blob, path.basename(filePath));
|
|
||||||
form.append('title', title);
|
|
||||||
const res = await client.request(uploadFiles(form));
|
|
||||||
return res.id;
|
|
||||||
};
|
|
||||||
|
|
||||||
const logoWhiteId = await uploadAsset(
|
|
||||||
path.resolve(__dirname, '../public/logo-white.svg'),
|
|
||||||
'Logo White',
|
|
||||||
);
|
|
||||||
const logoBlueId = await uploadAsset(
|
|
||||||
path.resolve(__dirname, '../public/logo-blue.svg'),
|
|
||||||
'Logo Blue',
|
|
||||||
);
|
|
||||||
const faviconId = await uploadAsset(
|
|
||||||
path.resolve(__dirname, '../public/favicon.ico'),
|
|
||||||
'Favicon',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Smoother Background SVG
|
|
||||||
const bgSvgPath = path.resolve(__dirname, '../public/login-bg.svg');
|
|
||||||
fs.writeFileSync(
|
|
||||||
bgSvgPath,
|
|
||||||
`<svg width="1920" height="1080" viewBox="0 0 1920 1080" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="1920" height="1080" fill="#001a4d"/>
|
|
||||||
<ellipse cx="960" cy="540" rx="800" ry="600" fill="url(#paint0_radial_premium)"/>
|
|
||||||
<defs>
|
|
||||||
<radialGradient id="paint0_radial_premium" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(960 540) rotate(90) scale(600 800)">
|
|
||||||
<stop stop-color="#003d82" stop-opacity="0.8"/>
|
|
||||||
<stop offset="1" stop-color="#001a4d" stop-opacity="0"/>
|
|
||||||
</radialGradient>
|
|
||||||
</defs>
|
|
||||||
</svg>`,
|
|
||||||
);
|
|
||||||
const backgroundId = await uploadAsset(bgSvgPath, 'Login Bg');
|
|
||||||
if (fs.existsSync(bgSvgPath)) fs.unlinkSync(bgSvgPath);
|
|
||||||
|
|
||||||
// 3. Update Settings with "Premium Web" Theme
|
|
||||||
console.log('⚙️ Updating Directus settings...');
|
|
||||||
|
|
||||||
const COLOR_PRIMARY = '#001a4d'; // Deep Blue
|
|
||||||
const COLOR_ACCENT = '#82ed20'; // Sustainability Green
|
|
||||||
const COLOR_SECONDARY = '#003d82';
|
|
||||||
|
|
||||||
const customCss = `
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
|
||||||
|
|
||||||
/* Global Login Styles */
|
|
||||||
body, .v-app {
|
|
||||||
font-family: 'Inter', sans-serif !important;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Glassmorphism Effect for Login Card */
|
|
||||||
.public-view .v-card {
|
|
||||||
background: rgba(255, 255, 255, 0.95) !important;
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.3) !important;
|
|
||||||
border-radius: 32px !important;
|
|
||||||
box-shadow: 0 50px 100px -20px rgba(0, 0, 0, 0.4) !important;
|
|
||||||
padding: 40px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.public-view .v-button {
|
|
||||||
border-radius: 9999px !important;
|
|
||||||
height: 56px !important;
|
|
||||||
font-weight: 600 !important;
|
|
||||||
letter-spacing: -0.01em !important;
|
|
||||||
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.public-view .v-button:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 15px 30px rgba(130, 237, 32, 0.2) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.public-view .v-input {
|
|
||||||
--v-input-border-radius: 12px !important;
|
|
||||||
--v-input-background-color: #f8f9fa !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Inject Headline via CSS to avoid raw HTML display in public_note */
|
|
||||||
.public-view .form::before {
|
|
||||||
content: 'Sustainable Energy. Industrial Reliability.';
|
|
||||||
display: block;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #ffffff;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.public-view .form::after {
|
|
||||||
content: 'KLZ INFRASTRUCTURE ENGINE';
|
|
||||||
display: block;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
color: rgba(255, 255, 255, 0.5);
|
|
||||||
margin-top: 24px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const publicNote = '';
|
|
||||||
|
|
||||||
await client.request(
|
|
||||||
updateSettings({
|
|
||||||
project_name: 'KLZ Cables',
|
|
||||||
project_url: 'https://klz-cables.com',
|
|
||||||
project_color: COLOR_ACCENT,
|
|
||||||
project_descriptor: 'Sustainable Energy Infrastructure',
|
|
||||||
|
|
||||||
// FIXED: Use WHITE logo for the Blue Sidebar
|
|
||||||
project_logo: logoWhiteId as any,
|
|
||||||
|
|
||||||
public_foreground: logoWhiteId as any,
|
|
||||||
public_background: backgroundId as any,
|
|
||||||
public_note: publicNote,
|
|
||||||
public_favicon: faviconId as any,
|
|
||||||
custom_css: customCss,
|
|
||||||
|
|
||||||
// DEEP PREMIUM THEME
|
|
||||||
theme_light_overrides: {
|
|
||||||
// Brands
|
|
||||||
primary: COLOR_ACCENT, // Buttons/Actions are GREEN like the website
|
|
||||||
secondary: COLOR_SECONDARY,
|
|
||||||
|
|
||||||
// Content Area
|
|
||||||
background: '#f1f3f7',
|
|
||||||
backgroundNormal: '#ffffff',
|
|
||||||
backgroundAccent: '#eef2ff',
|
|
||||||
|
|
||||||
// Sidebar Branding
|
|
||||||
navigationBackground: COLOR_PRIMARY,
|
|
||||||
navigationForeground: '#ffffff',
|
|
||||||
navigationBackgroundHover: 'rgba(255,255,255,0.05)',
|
|
||||||
navigationForegroundHover: '#ffffff',
|
|
||||||
navigationBackgroundActive: 'rgba(130, 237, 32, 0.15)', // Subtle Green highlight
|
|
||||||
navigationForegroundActive: COLOR_ACCENT, // Active item is GREEN
|
|
||||||
|
|
||||||
// Module Bar (Thin far left)
|
|
||||||
moduleBarBackground: '#000d26',
|
|
||||||
moduleBarForeground: '#ffffff',
|
|
||||||
moduleBarForegroundActive: COLOR_ACCENT,
|
|
||||||
|
|
||||||
// UI Standards
|
|
||||||
borderRadius: '16px', // Larger radius for modern feel
|
|
||||||
borderWidth: '1px',
|
|
||||||
borderColor: '#e2e8f0',
|
|
||||||
formFieldHeight: '48px', // Touch-target height
|
|
||||||
} as any,
|
|
||||||
|
|
||||||
theme_dark_overrides: {
|
|
||||||
primary: COLOR_ACCENT,
|
|
||||||
background: '#0a0a0a',
|
|
||||||
navigationBackground: '#000000',
|
|
||||||
moduleBarBackground: COLOR_PRIMARY,
|
|
||||||
borderRadius: '16px',
|
|
||||||
formFieldHeight: '48px',
|
|
||||||
} as any,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('✨ Premium Theme applied successfully!');
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('❌ Error:', JSON.stringify(error, null, 2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupBranding();
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
REMOTE_HOST="root@alpha.mintel.me"
|
|
||||||
REMOTE_DIR="/home/deploy/sites/klz-cables.com"
|
|
||||||
|
|
||||||
# DB Details (matching docker-compose defaults)
|
|
||||||
LOCAL_DB_USER="klz_db_user"
|
|
||||||
REMOTE_DB_USER="directus"
|
|
||||||
DB_NAME="directus"
|
|
||||||
|
|
||||||
ACTION=$1
|
|
||||||
ENV=$2
|
|
||||||
|
|
||||||
# Help
|
|
||||||
if [ -z "$ACTION" ] || [ -z "$ENV" ]; then
|
|
||||||
echo "Usage: ./scripts/sync-directus.sh [push|pull] [testing|staging|production]"
|
|
||||||
echo ""
|
|
||||||
echo "Commands:"
|
|
||||||
echo " push Sync LOCAL data -> REMOTE"
|
|
||||||
echo " pull Sync REMOTE data -> LOCAL"
|
|
||||||
echo ""
|
|
||||||
echo "Environments:"
|
|
||||||
echo " testing, staging, production"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Map Environment to Project Name
|
|
||||||
case $ENV in
|
|
||||||
production|staging|testing)
|
|
||||||
PROJECT_NAME="klz-cablescom"
|
|
||||||
ENV_FILE=".env"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "❌ Invalid environment: $ENV. Use testing, staging, or production."
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Detect local container
|
|
||||||
echo "🔍 Detecting local database..."
|
|
||||||
# Use a more robust way to find the container if multiple projects exist locally
|
|
||||||
LOCAL_DB_CONTAINER=$(docker compose ps -q klz-db)
|
|
||||||
if [ -z "$LOCAL_DB_CONTAINER" ]; then
|
|
||||||
echo "❌ Local klz-directus-db container not found. Is it running? (npm run dev)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$ACTION" == "push" ]; then
|
|
||||||
echo "🚀 Pushing Local Data to $ENV ($PROJECT_NAME)..."
|
|
||||||
|
|
||||||
# 1. DB Dump
|
|
||||||
echo "📦 Dumping local database..."
|
|
||||||
# Note: we use --no-owner --no-privileges to ensure restore works on remote with different user setup
|
|
||||||
docker exec "$LOCAL_DB_CONTAINER" pg_dump -U "$DB_USER" --clean --if-exists --no-owner --no-privileges "$DB_NAME" > dump.sql
|
|
||||||
|
|
||||||
# 2. Upload Dump
|
|
||||||
echo "📤 Uploading dump to remote server..."
|
|
||||||
scp dump.sql "$REMOTE_HOST:$REMOTE_DIR/dump.sql"
|
|
||||||
|
|
||||||
# 3. Restore on Remote
|
|
||||||
echo "🔄 Restoring dump on $ENV..."
|
|
||||||
REMOTE_DB_CONTAINER=$(ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $PROJECT_NAME ps -q directus-db")
|
|
||||||
if [ -z "$REMOTE_DB_CONTAINER" ] && [ -n "$OLD_PROJECT_NAME" ]; then
|
|
||||||
echo "⚠️ $PROJECT_NAME not found, trying fallback $OLD_PROJECT_NAME..."
|
|
||||||
REMOTE_DB_CONTAINER=$(ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $OLD_PROJECT_NAME ps -q directus-db")
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$REMOTE_DB_CONTAINER" ]; then
|
|
||||||
echo "❌ Remote $ENV-db container not found!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Wipe remote DB clean before restore to avoid constraint errors
|
|
||||||
echo "🧹 Wiping remote database schema..."
|
|
||||||
ssh "$REMOTE_HOST" "docker exec $REMOTE_DB_CONTAINER psql -U $REMOTE_DB_USER $DB_NAME -c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;'"
|
|
||||||
|
|
||||||
echo "⚡ Restoring database..."
|
|
||||||
ssh "$REMOTE_HOST" "docker exec -i $REMOTE_DB_CONTAINER psql -U $REMOTE_DB_USER $DB_NAME < $REMOTE_DIR/dump.sql"
|
|
||||||
|
|
||||||
# 4. Sync Uploads
|
|
||||||
echo "📁 Syncing uploads (Local -> $ENV)..."
|
|
||||||
# Note: If environments share the same directory, this might overwrite others' files if not careful.
|
|
||||||
# But since they share the same host directory currently, rsync will update the shared folder.
|
|
||||||
rsync -avz --progress ./directus/uploads/ "$REMOTE_HOST:$REMOTE_DIR/directus/uploads/"
|
|
||||||
|
|
||||||
# Clean up
|
|
||||||
rm dump.sql
|
|
||||||
ssh "$REMOTE_HOST" "rm $REMOTE_DIR/dump.sql"
|
|
||||||
|
|
||||||
# 5. Restart Directus to trigger migrations and refresh schema cache
|
|
||||||
echo "🔄 Restarting remote Directus to apply migrations..."
|
|
||||||
ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $PROJECT_NAME restart directus"
|
|
||||||
|
|
||||||
echo "✨ Push to $ENV complete!"
|
|
||||||
|
|
||||||
elif [ "$ACTION" == "pull" ]; then
|
|
||||||
echo "📥 Pulling $ENV Data to Local..."
|
|
||||||
|
|
||||||
# The remote service name is 'klz-db' according to docker compose config
|
|
||||||
REMOTE_DB_CONTAINER=$(ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $PROJECT_NAME ps -q klz-db")
|
|
||||||
|
|
||||||
if [ -z "$REMOTE_DB_CONTAINER" ]; then
|
|
||||||
echo "❌ Remote $ENV-db container not found!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
ssh "$REMOTE_HOST" "docker exec $REMOTE_DB_CONTAINER pg_dump -U $REMOTE_DB_USER --clean --if-exists --no-owner --no-privileges $DB_NAME > $REMOTE_DIR/dump.sql"
|
|
||||||
|
|
||||||
# 2. Download Dump
|
|
||||||
echo "📥 Downloading dump..."
|
|
||||||
scp "$REMOTE_HOST:$REMOTE_DIR/dump.sql" dump.sql
|
|
||||||
|
|
||||||
# Wipe local DB clean before restore to avoid constraint errors
|
|
||||||
echo "🧹 Wiping local database schema..."
|
|
||||||
docker exec "$LOCAL_DB_CONTAINER" psql -U "$LOCAL_DB_USER" "$DB_NAME" -c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;'
|
|
||||||
|
|
||||||
echo "⚡ Restoring database locally..."
|
|
||||||
docker exec -i "$LOCAL_DB_CONTAINER" psql -U "$LOCAL_DB_USER" "$DB_NAME" < dump.sql
|
|
||||||
|
|
||||||
# 4. Sync Uploads
|
|
||||||
echo "📁 Syncing uploads ($ENV -> Local)..."
|
|
||||||
rsync -avz --progress "$REMOTE_HOST:$REMOTE_DIR/directus/uploads/" ./directus/uploads/
|
|
||||||
|
|
||||||
# Clean up
|
|
||||||
rm dump.sql
|
|
||||||
ssh "$REMOTE_HOST" "rm $REMOTE_DIR/dump.sql"
|
|
||||||
|
|
||||||
echo "✨ Pull to Local complete!"
|
|
||||||
else
|
|
||||||
echo "Invalid action: $ACTION. Use push or pull."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
2621
src/migrations/20260223_195005_products_collection.json
Normal file
2621
src/migrations/20260223_195005_products_collection.json
Normal file
File diff suppressed because it is too large
Load Diff
356
src/migrations/20260223_195005_products_collection.ts
Normal file
356
src/migrations/20260223_195005_products_collection.ts
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres';
|
||||||
|
|
||||||
|
export async function up({ db }: MigrateUpArgs): Promise<void> {
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE TYPE "public"."enum_posts_locale" AS ENUM('en', 'de');
|
||||||
|
CREATE TYPE "public"."enum_posts_status" AS ENUM('draft', 'published');
|
||||||
|
CREATE TYPE "public"."enum__posts_v_version_locale" AS ENUM('en', 'de');
|
||||||
|
CREATE TYPE "public"."enum__posts_v_version_status" AS ENUM('draft', 'published');
|
||||||
|
CREATE TYPE "public"."enum_form_submissions_type" AS ENUM('contact', 'product_quote');
|
||||||
|
CREATE TYPE "public"."enum_products_locale" AS ENUM('en', 'de');
|
||||||
|
CREATE TYPE "public"."enum_products_status" AS ENUM('draft', 'published');
|
||||||
|
CREATE TYPE "public"."enum__products_v_version_locale" AS ENUM('en', 'de');
|
||||||
|
CREATE TYPE "public"."enum__products_v_version_status" AS ENUM('draft', 'published');
|
||||||
|
CREATE TABLE "users_sessions" (
|
||||||
|
"_order" integer NOT NULL,
|
||||||
|
"_parent_id" integer NOT NULL,
|
||||||
|
"id" varchar PRIMARY KEY NOT NULL,
|
||||||
|
"created_at" timestamp(3) with time zone,
|
||||||
|
"expires_at" timestamp(3) with time zone NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "users" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||||
|
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||||
|
"email" varchar NOT NULL,
|
||||||
|
"reset_password_token" varchar,
|
||||||
|
"reset_password_expiration" timestamp(3) with time zone,
|
||||||
|
"salt" varchar,
|
||||||
|
"hash" varchar,
|
||||||
|
"login_attempts" numeric DEFAULT 0,
|
||||||
|
"lock_until" timestamp(3) with time zone
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "media" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"alt" varchar NOT NULL,
|
||||||
|
"caption" varchar,
|
||||||
|
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||||
|
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||||
|
"url" varchar,
|
||||||
|
"thumbnail_u_r_l" varchar,
|
||||||
|
"filename" varchar,
|
||||||
|
"mime_type" varchar,
|
||||||
|
"filesize" numeric,
|
||||||
|
"width" numeric,
|
||||||
|
"height" numeric,
|
||||||
|
"focal_x" numeric,
|
||||||
|
"focal_y" numeric,
|
||||||
|
"sizes_thumbnail_url" varchar,
|
||||||
|
"sizes_thumbnail_width" numeric,
|
||||||
|
"sizes_thumbnail_height" numeric,
|
||||||
|
"sizes_thumbnail_mime_type" varchar,
|
||||||
|
"sizes_thumbnail_filesize" numeric,
|
||||||
|
"sizes_thumbnail_filename" varchar,
|
||||||
|
"sizes_card_url" varchar,
|
||||||
|
"sizes_card_width" numeric,
|
||||||
|
"sizes_card_height" numeric,
|
||||||
|
"sizes_card_mime_type" varchar,
|
||||||
|
"sizes_card_filesize" numeric,
|
||||||
|
"sizes_card_filename" varchar,
|
||||||
|
"sizes_hero_url" varchar,
|
||||||
|
"sizes_hero_width" numeric,
|
||||||
|
"sizes_hero_height" numeric,
|
||||||
|
"sizes_hero_mime_type" varchar,
|
||||||
|
"sizes_hero_filesize" numeric,
|
||||||
|
"sizes_hero_filename" varchar,
|
||||||
|
"sizes_hero_mobile_url" varchar,
|
||||||
|
"sizes_hero_mobile_width" numeric,
|
||||||
|
"sizes_hero_mobile_height" numeric,
|
||||||
|
"sizes_hero_mobile_mime_type" varchar,
|
||||||
|
"sizes_hero_mobile_filesize" numeric,
|
||||||
|
"sizes_hero_mobile_filename" varchar
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "posts" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"title" varchar,
|
||||||
|
"slug" varchar,
|
||||||
|
"excerpt" varchar,
|
||||||
|
"date" timestamp(3) with time zone,
|
||||||
|
"featured_image_id" integer,
|
||||||
|
"locale" "enum_posts_locale" DEFAULT 'en',
|
||||||
|
"category" varchar,
|
||||||
|
"content" jsonb,
|
||||||
|
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||||
|
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||||
|
"_status" "enum_posts_status" DEFAULT 'draft'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "_posts_v" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"parent_id" integer,
|
||||||
|
"version_title" varchar,
|
||||||
|
"version_slug" varchar,
|
||||||
|
"version_excerpt" varchar,
|
||||||
|
"version_date" timestamp(3) with time zone,
|
||||||
|
"version_featured_image_id" integer,
|
||||||
|
"version_locale" "enum__posts_v_version_locale" DEFAULT 'en',
|
||||||
|
"version_category" varchar,
|
||||||
|
"version_content" jsonb,
|
||||||
|
"version_updated_at" timestamp(3) with time zone,
|
||||||
|
"version_created_at" timestamp(3) with time zone,
|
||||||
|
"version__status" "enum__posts_v_version_status" DEFAULT 'draft',
|
||||||
|
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||||
|
"latest" boolean
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "form_submissions" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"name" varchar NOT NULL,
|
||||||
|
"email" varchar NOT NULL,
|
||||||
|
"type" "enum_form_submissions_type" NOT NULL,
|
||||||
|
"product_name" varchar,
|
||||||
|
"message" varchar NOT NULL,
|
||||||
|
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||||
|
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "products_categories" (
|
||||||
|
"_order" integer NOT NULL,
|
||||||
|
"_parent_id" integer NOT NULL,
|
||||||
|
"id" varchar PRIMARY KEY NOT NULL,
|
||||||
|
"category" varchar
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "products" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"title" varchar,
|
||||||
|
"sku" varchar,
|
||||||
|
"slug" varchar,
|
||||||
|
"description" varchar,
|
||||||
|
"locale" "enum_products_locale" DEFAULT 'de',
|
||||||
|
"application" jsonb,
|
||||||
|
"content" jsonb,
|
||||||
|
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||||
|
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||||
|
"_status" "enum_products_status" DEFAULT 'draft'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "products_rels" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"order" integer,
|
||||||
|
"parent_id" integer NOT NULL,
|
||||||
|
"path" varchar NOT NULL,
|
||||||
|
"media_id" integer
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "_products_v_version_categories" (
|
||||||
|
"_order" integer NOT NULL,
|
||||||
|
"_parent_id" integer NOT NULL,
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"category" varchar,
|
||||||
|
"_uuid" varchar
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "_products_v" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"parent_id" integer,
|
||||||
|
"version_title" varchar,
|
||||||
|
"version_sku" varchar,
|
||||||
|
"version_slug" varchar,
|
||||||
|
"version_description" varchar,
|
||||||
|
"version_locale" "enum__products_v_version_locale" DEFAULT 'de',
|
||||||
|
"version_application" jsonb,
|
||||||
|
"version_content" jsonb,
|
||||||
|
"version_updated_at" timestamp(3) with time zone,
|
||||||
|
"version_created_at" timestamp(3) with time zone,
|
||||||
|
"version__status" "enum__products_v_version_status" DEFAULT 'draft',
|
||||||
|
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||||
|
"latest" boolean
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "_products_v_rels" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"order" integer,
|
||||||
|
"parent_id" integer NOT NULL,
|
||||||
|
"path" varchar NOT NULL,
|
||||||
|
"media_id" integer
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "payload_kv" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"key" varchar NOT NULL,
|
||||||
|
"data" jsonb NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "payload_locked_documents" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"global_slug" varchar,
|
||||||
|
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||||
|
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "payload_locked_documents_rels" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"order" integer,
|
||||||
|
"parent_id" integer NOT NULL,
|
||||||
|
"path" varchar NOT NULL,
|
||||||
|
"users_id" integer,
|
||||||
|
"media_id" integer,
|
||||||
|
"posts_id" integer,
|
||||||
|
"form_submissions_id" integer,
|
||||||
|
"products_id" integer
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "payload_preferences" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"key" varchar,
|
||||||
|
"value" jsonb,
|
||||||
|
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||||
|
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "payload_preferences_rels" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"order" integer,
|
||||||
|
"parent_id" integer NOT NULL,
|
||||||
|
"path" varchar NOT NULL,
|
||||||
|
"users_id" integer
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "payload_migrations" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"name" varchar,
|
||||||
|
"batch" numeric,
|
||||||
|
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||||
|
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE "users_sessions" ADD CONSTRAINT "users_sessions_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
ALTER TABLE "posts" ADD CONSTRAINT "posts_featured_image_id_media_id_fk" FOREIGN KEY ("featured_image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
|
||||||
|
ALTER TABLE "_posts_v" ADD CONSTRAINT "_posts_v_parent_id_posts_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."posts"("id") ON DELETE set null ON UPDATE no action;
|
||||||
|
ALTER TABLE "_posts_v" ADD CONSTRAINT "_posts_v_version_featured_image_id_media_id_fk" FOREIGN KEY ("version_featured_image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
|
||||||
|
ALTER TABLE "products_categories" ADD CONSTRAINT "products_categories_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."products"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
ALTER TABLE "products_rels" ADD CONSTRAINT "products_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."products"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
ALTER TABLE "products_rels" ADD CONSTRAINT "products_rels_media_fk" FOREIGN KEY ("media_id") REFERENCES "public"."media"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
ALTER TABLE "_products_v_version_categories" ADD CONSTRAINT "_products_v_version_categories_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_products_v"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
ALTER TABLE "_products_v" ADD CONSTRAINT "_products_v_parent_id_products_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."products"("id") ON DELETE set null ON UPDATE no action;
|
||||||
|
ALTER TABLE "_products_v_rels" ADD CONSTRAINT "_products_v_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."_products_v"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
ALTER TABLE "_products_v_rels" ADD CONSTRAINT "_products_v_rels_media_fk" FOREIGN KEY ("media_id") REFERENCES "public"."media"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."payload_locked_documents"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_users_fk" FOREIGN KEY ("users_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_media_fk" FOREIGN KEY ("media_id") REFERENCES "public"."media"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_posts_fk" FOREIGN KEY ("posts_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_form_submissions_fk" FOREIGN KEY ("form_submissions_id") REFERENCES "public"."form_submissions"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_products_fk" FOREIGN KEY ("products_id") REFERENCES "public"."products"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
ALTER TABLE "payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."payload_preferences"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
ALTER TABLE "payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_users_fk" FOREIGN KEY ("users_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
CREATE INDEX "users_sessions_order_idx" ON "users_sessions" USING btree ("_order");
|
||||||
|
CREATE INDEX "users_sessions_parent_id_idx" ON "users_sessions" USING btree ("_parent_id");
|
||||||
|
CREATE INDEX "users_updated_at_idx" ON "users" USING btree ("updated_at");
|
||||||
|
CREATE INDEX "users_created_at_idx" ON "users" USING btree ("created_at");
|
||||||
|
CREATE UNIQUE INDEX "users_email_idx" ON "users" USING btree ("email");
|
||||||
|
CREATE INDEX "media_updated_at_idx" ON "media" USING btree ("updated_at");
|
||||||
|
CREATE INDEX "media_created_at_idx" ON "media" USING btree ("created_at");
|
||||||
|
CREATE UNIQUE INDEX "media_filename_idx" ON "media" USING btree ("filename");
|
||||||
|
CREATE INDEX "media_sizes_thumbnail_sizes_thumbnail_filename_idx" ON "media" USING btree ("sizes_thumbnail_filename");
|
||||||
|
CREATE INDEX "media_sizes_card_sizes_card_filename_idx" ON "media" USING btree ("sizes_card_filename");
|
||||||
|
CREATE INDEX "media_sizes_hero_sizes_hero_filename_idx" ON "media" USING btree ("sizes_hero_filename");
|
||||||
|
CREATE INDEX "media_sizes_hero_mobile_sizes_hero_mobile_filename_idx" ON "media" USING btree ("sizes_hero_mobile_filename");
|
||||||
|
CREATE UNIQUE INDEX "posts_slug_idx" ON "posts" USING btree ("slug");
|
||||||
|
CREATE INDEX "posts_featured_image_idx" ON "posts" USING btree ("featured_image_id");
|
||||||
|
CREATE INDEX "posts_updated_at_idx" ON "posts" USING btree ("updated_at");
|
||||||
|
CREATE INDEX "posts_created_at_idx" ON "posts" USING btree ("created_at");
|
||||||
|
CREATE INDEX "posts__status_idx" ON "posts" USING btree ("_status");
|
||||||
|
CREATE INDEX "_posts_v_parent_idx" ON "_posts_v" USING btree ("parent_id");
|
||||||
|
CREATE INDEX "_posts_v_version_version_slug_idx" ON "_posts_v" USING btree ("version_slug");
|
||||||
|
CREATE INDEX "_posts_v_version_version_featured_image_idx" ON "_posts_v" USING btree ("version_featured_image_id");
|
||||||
|
CREATE INDEX "_posts_v_version_version_updated_at_idx" ON "_posts_v" USING btree ("version_updated_at");
|
||||||
|
CREATE INDEX "_posts_v_version_version_created_at_idx" ON "_posts_v" USING btree ("version_created_at");
|
||||||
|
CREATE INDEX "_posts_v_version_version__status_idx" ON "_posts_v" USING btree ("version__status");
|
||||||
|
CREATE INDEX "_posts_v_created_at_idx" ON "_posts_v" USING btree ("created_at");
|
||||||
|
CREATE INDEX "_posts_v_updated_at_idx" ON "_posts_v" USING btree ("updated_at");
|
||||||
|
CREATE INDEX "_posts_v_latest_idx" ON "_posts_v" USING btree ("latest");
|
||||||
|
CREATE INDEX "form_submissions_updated_at_idx" ON "form_submissions" USING btree ("updated_at");
|
||||||
|
CREATE INDEX "form_submissions_created_at_idx" ON "form_submissions" USING btree ("created_at");
|
||||||
|
CREATE INDEX "products_categories_order_idx" ON "products_categories" USING btree ("_order");
|
||||||
|
CREATE INDEX "products_categories_parent_id_idx" ON "products_categories" USING btree ("_parent_id");
|
||||||
|
CREATE UNIQUE INDEX "products_sku_idx" ON "products" USING btree ("sku");
|
||||||
|
CREATE INDEX "products_updated_at_idx" ON "products" USING btree ("updated_at");
|
||||||
|
CREATE INDEX "products_created_at_idx" ON "products" USING btree ("created_at");
|
||||||
|
CREATE INDEX "products__status_idx" ON "products" USING btree ("_status");
|
||||||
|
CREATE INDEX "products_rels_order_idx" ON "products_rels" USING btree ("order");
|
||||||
|
CREATE INDEX "products_rels_parent_idx" ON "products_rels" USING btree ("parent_id");
|
||||||
|
CREATE INDEX "products_rels_path_idx" ON "products_rels" USING btree ("path");
|
||||||
|
CREATE INDEX "products_rels_media_id_idx" ON "products_rels" USING btree ("media_id");
|
||||||
|
CREATE INDEX "_products_v_version_categories_order_idx" ON "_products_v_version_categories" USING btree ("_order");
|
||||||
|
CREATE INDEX "_products_v_version_categories_parent_id_idx" ON "_products_v_version_categories" USING btree ("_parent_id");
|
||||||
|
CREATE INDEX "_products_v_parent_idx" ON "_products_v" USING btree ("parent_id");
|
||||||
|
CREATE INDEX "_products_v_version_version_sku_idx" ON "_products_v" USING btree ("version_sku");
|
||||||
|
CREATE INDEX "_products_v_version_version_updated_at_idx" ON "_products_v" USING btree ("version_updated_at");
|
||||||
|
CREATE INDEX "_products_v_version_version_created_at_idx" ON "_products_v" USING btree ("version_created_at");
|
||||||
|
CREATE INDEX "_products_v_version_version__status_idx" ON "_products_v" USING btree ("version__status");
|
||||||
|
CREATE INDEX "_products_v_created_at_idx" ON "_products_v" USING btree ("created_at");
|
||||||
|
CREATE INDEX "_products_v_updated_at_idx" ON "_products_v" USING btree ("updated_at");
|
||||||
|
CREATE INDEX "_products_v_latest_idx" ON "_products_v" USING btree ("latest");
|
||||||
|
CREATE INDEX "_products_v_rels_order_idx" ON "_products_v_rels" USING btree ("order");
|
||||||
|
CREATE INDEX "_products_v_rels_parent_idx" ON "_products_v_rels" USING btree ("parent_id");
|
||||||
|
CREATE INDEX "_products_v_rels_path_idx" ON "_products_v_rels" USING btree ("path");
|
||||||
|
CREATE INDEX "_products_v_rels_media_id_idx" ON "_products_v_rels" USING btree ("media_id");
|
||||||
|
CREATE UNIQUE INDEX "payload_kv_key_idx" ON "payload_kv" USING btree ("key");
|
||||||
|
CREATE INDEX "payload_locked_documents_global_slug_idx" ON "payload_locked_documents" USING btree ("global_slug");
|
||||||
|
CREATE INDEX "payload_locked_documents_updated_at_idx" ON "payload_locked_documents" USING btree ("updated_at");
|
||||||
|
CREATE INDEX "payload_locked_documents_created_at_idx" ON "payload_locked_documents" USING btree ("created_at");
|
||||||
|
CREATE INDEX "payload_locked_documents_rels_order_idx" ON "payload_locked_documents_rels" USING btree ("order");
|
||||||
|
CREATE INDEX "payload_locked_documents_rels_parent_idx" ON "payload_locked_documents_rels" USING btree ("parent_id");
|
||||||
|
CREATE INDEX "payload_locked_documents_rels_path_idx" ON "payload_locked_documents_rels" USING btree ("path");
|
||||||
|
CREATE INDEX "payload_locked_documents_rels_users_id_idx" ON "payload_locked_documents_rels" USING btree ("users_id");
|
||||||
|
CREATE INDEX "payload_locked_documents_rels_media_id_idx" ON "payload_locked_documents_rels" USING btree ("media_id");
|
||||||
|
CREATE INDEX "payload_locked_documents_rels_posts_id_idx" ON "payload_locked_documents_rels" USING btree ("posts_id");
|
||||||
|
CREATE INDEX "payload_locked_documents_rels_form_submissions_id_idx" ON "payload_locked_documents_rels" USING btree ("form_submissions_id");
|
||||||
|
CREATE INDEX "payload_locked_documents_rels_products_id_idx" ON "payload_locked_documents_rels" USING btree ("products_id");
|
||||||
|
CREATE INDEX "payload_preferences_key_idx" ON "payload_preferences" USING btree ("key");
|
||||||
|
CREATE INDEX "payload_preferences_updated_at_idx" ON "payload_preferences" USING btree ("updated_at");
|
||||||
|
CREATE INDEX "payload_preferences_created_at_idx" ON "payload_preferences" USING btree ("created_at");
|
||||||
|
CREATE INDEX "payload_preferences_rels_order_idx" ON "payload_preferences_rels" USING btree ("order");
|
||||||
|
CREATE INDEX "payload_preferences_rels_parent_idx" ON "payload_preferences_rels" USING btree ("parent_id");
|
||||||
|
CREATE INDEX "payload_preferences_rels_path_idx" ON "payload_preferences_rels" USING btree ("path");
|
||||||
|
CREATE INDEX "payload_preferences_rels_users_id_idx" ON "payload_preferences_rels" USING btree ("users_id");
|
||||||
|
CREATE INDEX "payload_migrations_updated_at_idx" ON "payload_migrations" USING btree ("updated_at");
|
||||||
|
CREATE INDEX "payload_migrations_created_at_idx" ON "payload_migrations" USING btree ("created_at");`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down({ db }: MigrateDownArgs): Promise<void> {
|
||||||
|
await db.execute(sql`
|
||||||
|
DROP TABLE "users_sessions" CASCADE;
|
||||||
|
DROP TABLE "users" CASCADE;
|
||||||
|
DROP TABLE "media" CASCADE;
|
||||||
|
DROP TABLE "posts" CASCADE;
|
||||||
|
DROP TABLE "_posts_v" CASCADE;
|
||||||
|
DROP TABLE "form_submissions" CASCADE;
|
||||||
|
DROP TABLE "products_categories" CASCADE;
|
||||||
|
DROP TABLE "products" CASCADE;
|
||||||
|
DROP TABLE "products_rels" CASCADE;
|
||||||
|
DROP TABLE "_products_v_version_categories" CASCADE;
|
||||||
|
DROP TABLE "_products_v" CASCADE;
|
||||||
|
DROP TABLE "_products_v_rels" CASCADE;
|
||||||
|
DROP TABLE "payload_kv" CASCADE;
|
||||||
|
DROP TABLE "payload_locked_documents" CASCADE;
|
||||||
|
DROP TABLE "payload_locked_documents_rels" CASCADE;
|
||||||
|
DROP TABLE "payload_preferences" CASCADE;
|
||||||
|
DROP TABLE "payload_preferences_rels" CASCADE;
|
||||||
|
DROP TABLE "payload_migrations" CASCADE;
|
||||||
|
DROP TYPE "public"."enum_posts_locale";
|
||||||
|
DROP TYPE "public"."enum_posts_status";
|
||||||
|
DROP TYPE "public"."enum__posts_v_version_locale";
|
||||||
|
DROP TYPE "public"."enum__posts_v_version_status";
|
||||||
|
DROP TYPE "public"."enum_form_submissions_type";
|
||||||
|
DROP TYPE "public"."enum_products_locale";
|
||||||
|
DROP TYPE "public"."enum_products_status";
|
||||||
|
DROP TYPE "public"."enum__products_v_version_locale";
|
||||||
|
DROP TYPE "public"."enum__products_v_version_status";`);
|
||||||
|
}
|
||||||
2591
src/migrations/20260223_195151_remove_sku_unique.json
Normal file
2591
src/migrations/20260223_195151_remove_sku_unique.json
Normal file
File diff suppressed because it is too large
Load Diff
13
src/migrations/20260223_195151_remove_sku_unique.ts
Normal file
13
src/migrations/20260223_195151_remove_sku_unique.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres';
|
||||||
|
|
||||||
|
export async function up({ db }: MigrateUpArgs): Promise<void> {
|
||||||
|
await db.execute(sql`
|
||||||
|
DROP INDEX "products_sku_idx";
|
||||||
|
DROP INDEX "_products_v_version_version_sku_idx";`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down({ db }: MigrateDownArgs): Promise<void> {
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE UNIQUE INDEX "products_sku_idx" ON "products" USING btree ("sku");
|
||||||
|
CREATE INDEX "_products_v_version_version_sku_idx" ON "_products_v" USING btree ("version_sku");`);
|
||||||
|
}
|
||||||
15
src/migrations/index.ts
Normal file
15
src/migrations/index.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import * as migration_20260223_195005_products_collection from './20260223_195005_products_collection';
|
||||||
|
import * as migration_20260223_195151_remove_sku_unique from './20260223_195151_remove_sku_unique';
|
||||||
|
|
||||||
|
export const migrations = [
|
||||||
|
{
|
||||||
|
up: migration_20260223_195005_products_collection.up,
|
||||||
|
down: migration_20260223_195005_products_collection.down,
|
||||||
|
name: '20260223_195005_products_collection',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
up: migration_20260223_195151_remove_sku_unique.up,
|
||||||
|
down: migration_20260223_195151_remove_sku_unique.down,
|
||||||
|
name: '20260223_195151_remove_sku_unique',
|
||||||
|
},
|
||||||
|
];
|
||||||
793
src/payload-generated-schema.ts
Normal file
793
src/payload-generated-schema.ts
Normal file
@@ -0,0 +1,793 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* This file was automatically generated by Payload.
|
||||||
|
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||||
|
* and re-run `payload generate:db-schema` to regenerate this file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {} from '@payloadcms/db-postgres';
|
||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
index,
|
||||||
|
uniqueIndex,
|
||||||
|
foreignKey,
|
||||||
|
integer,
|
||||||
|
varchar,
|
||||||
|
timestamp,
|
||||||
|
serial,
|
||||||
|
numeric,
|
||||||
|
jsonb,
|
||||||
|
boolean,
|
||||||
|
pgEnum,
|
||||||
|
} from '@payloadcms/db-postgres/drizzle/pg-core';
|
||||||
|
import { sql, relations } from '@payloadcms/db-postgres/drizzle';
|
||||||
|
export const enum_posts_locale = pgEnum('enum_posts_locale', ['en', 'de']);
|
||||||
|
export const enum_posts_status = pgEnum('enum_posts_status', ['draft', 'published']);
|
||||||
|
export const enum__posts_v_version_locale = pgEnum('enum__posts_v_version_locale', ['en', 'de']);
|
||||||
|
export const enum__posts_v_version_status = pgEnum('enum__posts_v_version_status', [
|
||||||
|
'draft',
|
||||||
|
'published',
|
||||||
|
]);
|
||||||
|
export const enum_form_submissions_type = pgEnum('enum_form_submissions_type', [
|
||||||
|
'contact',
|
||||||
|
'product_quote',
|
||||||
|
]);
|
||||||
|
export const enum_products_locale = pgEnum('enum_products_locale', ['en', 'de']);
|
||||||
|
export const enum_products_status = pgEnum('enum_products_status', ['draft', 'published']);
|
||||||
|
export const enum__products_v_version_locale = pgEnum('enum__products_v_version_locale', [
|
||||||
|
'en',
|
||||||
|
'de',
|
||||||
|
]);
|
||||||
|
export const enum__products_v_version_status = pgEnum('enum__products_v_version_status', [
|
||||||
|
'draft',
|
||||||
|
'published',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const users_sessions = pgTable(
|
||||||
|
'users_sessions',
|
||||||
|
{
|
||||||
|
_order: integer('_order').notNull(),
|
||||||
|
_parentID: integer('_parent_id').notNull(),
|
||||||
|
id: varchar('id').primaryKey(),
|
||||||
|
createdAt: timestamp('created_at', { mode: 'string', withTimezone: true, precision: 3 }),
|
||||||
|
expiresAt: timestamp('expires_at', {
|
||||||
|
mode: 'string',
|
||||||
|
withTimezone: true,
|
||||||
|
precision: 3,
|
||||||
|
}).notNull(),
|
||||||
|
},
|
||||||
|
(columns) => [
|
||||||
|
index('users_sessions_order_idx').on(columns._order),
|
||||||
|
index('users_sessions_parent_id_idx').on(columns._parentID),
|
||||||
|
foreignKey({
|
||||||
|
columns: [columns['_parentID']],
|
||||||
|
foreignColumns: [users.id],
|
||||||
|
name: 'users_sessions_parent_id_fk',
|
||||||
|
}).onDelete('cascade'),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const users = pgTable(
|
||||||
|
'users',
|
||||||
|
{
|
||||||
|
id: serial('id').primaryKey(),
|
||||||
|
updatedAt: timestamp('updated_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
createdAt: timestamp('created_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
email: varchar('email').notNull(),
|
||||||
|
resetPasswordToken: varchar('reset_password_token'),
|
||||||
|
resetPasswordExpiration: timestamp('reset_password_expiration', {
|
||||||
|
mode: 'string',
|
||||||
|
withTimezone: true,
|
||||||
|
precision: 3,
|
||||||
|
}),
|
||||||
|
salt: varchar('salt'),
|
||||||
|
hash: varchar('hash'),
|
||||||
|
loginAttempts: numeric('login_attempts', { mode: 'number' }).default(0),
|
||||||
|
lockUntil: timestamp('lock_until', { mode: 'string', withTimezone: true, precision: 3 }),
|
||||||
|
},
|
||||||
|
(columns) => [
|
||||||
|
index('users_updated_at_idx').on(columns.updatedAt),
|
||||||
|
index('users_created_at_idx').on(columns.createdAt),
|
||||||
|
uniqueIndex('users_email_idx').on(columns.email),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const media = pgTable(
|
||||||
|
'media',
|
||||||
|
{
|
||||||
|
id: serial('id').primaryKey(),
|
||||||
|
alt: varchar('alt').notNull(),
|
||||||
|
caption: varchar('caption'),
|
||||||
|
updatedAt: timestamp('updated_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
createdAt: timestamp('created_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
url: varchar('url'),
|
||||||
|
thumbnailURL: varchar('thumbnail_u_r_l'),
|
||||||
|
filename: varchar('filename'),
|
||||||
|
mimeType: varchar('mime_type'),
|
||||||
|
filesize: numeric('filesize', { mode: 'number' }),
|
||||||
|
width: numeric('width', { mode: 'number' }),
|
||||||
|
height: numeric('height', { mode: 'number' }),
|
||||||
|
focalX: numeric('focal_x', { mode: 'number' }),
|
||||||
|
focalY: numeric('focal_y', { mode: 'number' }),
|
||||||
|
sizes_thumbnail_url: varchar('sizes_thumbnail_url'),
|
||||||
|
sizes_thumbnail_width: numeric('sizes_thumbnail_width', { mode: 'number' }),
|
||||||
|
sizes_thumbnail_height: numeric('sizes_thumbnail_height', { mode: 'number' }),
|
||||||
|
sizes_thumbnail_mimeType: varchar('sizes_thumbnail_mime_type'),
|
||||||
|
sizes_thumbnail_filesize: numeric('sizes_thumbnail_filesize', { mode: 'number' }),
|
||||||
|
sizes_thumbnail_filename: varchar('sizes_thumbnail_filename'),
|
||||||
|
sizes_card_url: varchar('sizes_card_url'),
|
||||||
|
sizes_card_width: numeric('sizes_card_width', { mode: 'number' }),
|
||||||
|
sizes_card_height: numeric('sizes_card_height', { mode: 'number' }),
|
||||||
|
sizes_card_mimeType: varchar('sizes_card_mime_type'),
|
||||||
|
sizes_card_filesize: numeric('sizes_card_filesize', { mode: 'number' }),
|
||||||
|
sizes_card_filename: varchar('sizes_card_filename'),
|
||||||
|
sizes_hero_url: varchar('sizes_hero_url'),
|
||||||
|
sizes_hero_width: numeric('sizes_hero_width', { mode: 'number' }),
|
||||||
|
sizes_hero_height: numeric('sizes_hero_height', { mode: 'number' }),
|
||||||
|
sizes_hero_mimeType: varchar('sizes_hero_mime_type'),
|
||||||
|
sizes_hero_filesize: numeric('sizes_hero_filesize', { mode: 'number' }),
|
||||||
|
sizes_hero_filename: varchar('sizes_hero_filename'),
|
||||||
|
sizes_hero_mobile_url: varchar('sizes_hero_mobile_url'),
|
||||||
|
sizes_hero_mobile_width: numeric('sizes_hero_mobile_width', { mode: 'number' }),
|
||||||
|
sizes_hero_mobile_height: numeric('sizes_hero_mobile_height', { mode: 'number' }),
|
||||||
|
sizes_hero_mobile_mimeType: varchar('sizes_hero_mobile_mime_type'),
|
||||||
|
sizes_hero_mobile_filesize: numeric('sizes_hero_mobile_filesize', { mode: 'number' }),
|
||||||
|
sizes_hero_mobile_filename: varchar('sizes_hero_mobile_filename'),
|
||||||
|
},
|
||||||
|
(columns) => [
|
||||||
|
index('media_updated_at_idx').on(columns.updatedAt),
|
||||||
|
index('media_created_at_idx').on(columns.createdAt),
|
||||||
|
uniqueIndex('media_filename_idx').on(columns.filename),
|
||||||
|
index('media_sizes_thumbnail_sizes_thumbnail_filename_idx').on(
|
||||||
|
columns.sizes_thumbnail_filename,
|
||||||
|
),
|
||||||
|
index('media_sizes_card_sizes_card_filename_idx').on(columns.sizes_card_filename),
|
||||||
|
index('media_sizes_hero_sizes_hero_filename_idx').on(columns.sizes_hero_filename),
|
||||||
|
index('media_sizes_hero_mobile_sizes_hero_mobile_filename_idx').on(
|
||||||
|
columns.sizes_hero_mobile_filename,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const posts = pgTable(
|
||||||
|
'posts',
|
||||||
|
{
|
||||||
|
id: serial('id').primaryKey(),
|
||||||
|
title: varchar('title'),
|
||||||
|
slug: varchar('slug'),
|
||||||
|
excerpt: varchar('excerpt'),
|
||||||
|
date: timestamp('date', { mode: 'string', withTimezone: true, precision: 3 }),
|
||||||
|
featuredImage: integer('featured_image_id').references(() => media.id, {
|
||||||
|
onDelete: 'set null',
|
||||||
|
}),
|
||||||
|
locale: enum_posts_locale('locale').default('en'),
|
||||||
|
category: varchar('category'),
|
||||||
|
content: jsonb('content'),
|
||||||
|
updatedAt: timestamp('updated_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
createdAt: timestamp('created_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
_status: enum_posts_status('_status').default('draft'),
|
||||||
|
},
|
||||||
|
(columns) => [
|
||||||
|
uniqueIndex('posts_slug_idx').on(columns.slug),
|
||||||
|
index('posts_featured_image_idx').on(columns.featuredImage),
|
||||||
|
index('posts_updated_at_idx').on(columns.updatedAt),
|
||||||
|
index('posts_created_at_idx').on(columns.createdAt),
|
||||||
|
index('posts__status_idx').on(columns._status),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const _posts_v = pgTable(
|
||||||
|
'_posts_v',
|
||||||
|
{
|
||||||
|
id: serial('id').primaryKey(),
|
||||||
|
parent: integer('parent_id').references(() => posts.id, {
|
||||||
|
onDelete: 'set null',
|
||||||
|
}),
|
||||||
|
version_title: varchar('version_title'),
|
||||||
|
version_slug: varchar('version_slug'),
|
||||||
|
version_excerpt: varchar('version_excerpt'),
|
||||||
|
version_date: timestamp('version_date', { mode: 'string', withTimezone: true, precision: 3 }),
|
||||||
|
version_featuredImage: integer('version_featured_image_id').references(() => media.id, {
|
||||||
|
onDelete: 'set null',
|
||||||
|
}),
|
||||||
|
version_locale: enum__posts_v_version_locale('version_locale').default('en'),
|
||||||
|
version_category: varchar('version_category'),
|
||||||
|
version_content: jsonb('version_content'),
|
||||||
|
version_updatedAt: timestamp('version_updated_at', {
|
||||||
|
mode: 'string',
|
||||||
|
withTimezone: true,
|
||||||
|
precision: 3,
|
||||||
|
}),
|
||||||
|
version_createdAt: timestamp('version_created_at', {
|
||||||
|
mode: 'string',
|
||||||
|
withTimezone: true,
|
||||||
|
precision: 3,
|
||||||
|
}),
|
||||||
|
version__status: enum__posts_v_version_status('version__status').default('draft'),
|
||||||
|
createdAt: timestamp('created_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
latest: boolean('latest'),
|
||||||
|
},
|
||||||
|
(columns) => [
|
||||||
|
index('_posts_v_parent_idx').on(columns.parent),
|
||||||
|
index('_posts_v_version_version_slug_idx').on(columns.version_slug),
|
||||||
|
index('_posts_v_version_version_featured_image_idx').on(columns.version_featuredImage),
|
||||||
|
index('_posts_v_version_version_updated_at_idx').on(columns.version_updatedAt),
|
||||||
|
index('_posts_v_version_version_created_at_idx').on(columns.version_createdAt),
|
||||||
|
index('_posts_v_version_version__status_idx').on(columns.version__status),
|
||||||
|
index('_posts_v_created_at_idx').on(columns.createdAt),
|
||||||
|
index('_posts_v_updated_at_idx').on(columns.updatedAt),
|
||||||
|
index('_posts_v_latest_idx').on(columns.latest),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const form_submissions = pgTable(
|
||||||
|
'form_submissions',
|
||||||
|
{
|
||||||
|
id: serial('id').primaryKey(),
|
||||||
|
name: varchar('name').notNull(),
|
||||||
|
email: varchar('email').notNull(),
|
||||||
|
type: enum_form_submissions_type('type').notNull(),
|
||||||
|
productName: varchar('product_name'),
|
||||||
|
message: varchar('message').notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
createdAt: timestamp('created_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
},
|
||||||
|
(columns) => [
|
||||||
|
index('form_submissions_updated_at_idx').on(columns.updatedAt),
|
||||||
|
index('form_submissions_created_at_idx').on(columns.createdAt),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const products_categories = pgTable(
|
||||||
|
'products_categories',
|
||||||
|
{
|
||||||
|
_order: integer('_order').notNull(),
|
||||||
|
_parentID: integer('_parent_id').notNull(),
|
||||||
|
id: varchar('id').primaryKey(),
|
||||||
|
category: varchar('category'),
|
||||||
|
},
|
||||||
|
(columns) => [
|
||||||
|
index('products_categories_order_idx').on(columns._order),
|
||||||
|
index('products_categories_parent_id_idx').on(columns._parentID),
|
||||||
|
foreignKey({
|
||||||
|
columns: [columns['_parentID']],
|
||||||
|
foreignColumns: [products.id],
|
||||||
|
name: 'products_categories_parent_id_fk',
|
||||||
|
}).onDelete('cascade'),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const products = pgTable(
|
||||||
|
'products',
|
||||||
|
{
|
||||||
|
id: serial('id').primaryKey(),
|
||||||
|
title: varchar('title'),
|
||||||
|
sku: varchar('sku'),
|
||||||
|
slug: varchar('slug'),
|
||||||
|
description: varchar('description'),
|
||||||
|
locale: enum_products_locale('locale').default('de'),
|
||||||
|
application: jsonb('application'),
|
||||||
|
content: jsonb('content'),
|
||||||
|
updatedAt: timestamp('updated_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
createdAt: timestamp('created_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
_status: enum_products_status('_status').default('draft'),
|
||||||
|
},
|
||||||
|
(columns) => [
|
||||||
|
uniqueIndex('products_sku_idx').on(columns.sku),
|
||||||
|
index('products_updated_at_idx').on(columns.updatedAt),
|
||||||
|
index('products_created_at_idx').on(columns.createdAt),
|
||||||
|
index('products__status_idx').on(columns._status),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const products_rels = pgTable(
|
||||||
|
'products_rels',
|
||||||
|
{
|
||||||
|
id: serial('id').primaryKey(),
|
||||||
|
order: integer('order'),
|
||||||
|
parent: integer('parent_id').notNull(),
|
||||||
|
path: varchar('path').notNull(),
|
||||||
|
mediaID: integer('media_id'),
|
||||||
|
},
|
||||||
|
(columns) => [
|
||||||
|
index('products_rels_order_idx').on(columns.order),
|
||||||
|
index('products_rels_parent_idx').on(columns.parent),
|
||||||
|
index('products_rels_path_idx').on(columns.path),
|
||||||
|
index('products_rels_media_id_idx').on(columns.mediaID),
|
||||||
|
foreignKey({
|
||||||
|
columns: [columns['parent']],
|
||||||
|
foreignColumns: [products.id],
|
||||||
|
name: 'products_rels_parent_fk',
|
||||||
|
}).onDelete('cascade'),
|
||||||
|
foreignKey({
|
||||||
|
columns: [columns['mediaID']],
|
||||||
|
foreignColumns: [media.id],
|
||||||
|
name: 'products_rels_media_fk',
|
||||||
|
}).onDelete('cascade'),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const _products_v_version_categories = pgTable(
|
||||||
|
'_products_v_version_categories',
|
||||||
|
{
|
||||||
|
_order: integer('_order').notNull(),
|
||||||
|
_parentID: integer('_parent_id').notNull(),
|
||||||
|
id: serial('id').primaryKey(),
|
||||||
|
category: varchar('category'),
|
||||||
|
_uuid: varchar('_uuid'),
|
||||||
|
},
|
||||||
|
(columns) => [
|
||||||
|
index('_products_v_version_categories_order_idx').on(columns._order),
|
||||||
|
index('_products_v_version_categories_parent_id_idx').on(columns._parentID),
|
||||||
|
foreignKey({
|
||||||
|
columns: [columns['_parentID']],
|
||||||
|
foreignColumns: [_products_v.id],
|
||||||
|
name: '_products_v_version_categories_parent_id_fk',
|
||||||
|
}).onDelete('cascade'),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const _products_v = pgTable(
|
||||||
|
'_products_v',
|
||||||
|
{
|
||||||
|
id: serial('id').primaryKey(),
|
||||||
|
parent: integer('parent_id').references(() => products.id, {
|
||||||
|
onDelete: 'set null',
|
||||||
|
}),
|
||||||
|
version_title: varchar('version_title'),
|
||||||
|
version_sku: varchar('version_sku'),
|
||||||
|
version_slug: varchar('version_slug'),
|
||||||
|
version_description: varchar('version_description'),
|
||||||
|
version_locale: enum__products_v_version_locale('version_locale').default('de'),
|
||||||
|
version_application: jsonb('version_application'),
|
||||||
|
version_content: jsonb('version_content'),
|
||||||
|
version_updatedAt: timestamp('version_updated_at', {
|
||||||
|
mode: 'string',
|
||||||
|
withTimezone: true,
|
||||||
|
precision: 3,
|
||||||
|
}),
|
||||||
|
version_createdAt: timestamp('version_created_at', {
|
||||||
|
mode: 'string',
|
||||||
|
withTimezone: true,
|
||||||
|
precision: 3,
|
||||||
|
}),
|
||||||
|
version__status: enum__products_v_version_status('version__status').default('draft'),
|
||||||
|
createdAt: timestamp('created_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
latest: boolean('latest'),
|
||||||
|
},
|
||||||
|
(columns) => [
|
||||||
|
index('_products_v_parent_idx').on(columns.parent),
|
||||||
|
index('_products_v_version_version_sku_idx').on(columns.version_sku),
|
||||||
|
index('_products_v_version_version_updated_at_idx').on(columns.version_updatedAt),
|
||||||
|
index('_products_v_version_version_created_at_idx').on(columns.version_createdAt),
|
||||||
|
index('_products_v_version_version__status_idx').on(columns.version__status),
|
||||||
|
index('_products_v_created_at_idx').on(columns.createdAt),
|
||||||
|
index('_products_v_updated_at_idx').on(columns.updatedAt),
|
||||||
|
index('_products_v_latest_idx').on(columns.latest),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const _products_v_rels = pgTable(
|
||||||
|
'_products_v_rels',
|
||||||
|
{
|
||||||
|
id: serial('id').primaryKey(),
|
||||||
|
order: integer('order'),
|
||||||
|
parent: integer('parent_id').notNull(),
|
||||||
|
path: varchar('path').notNull(),
|
||||||
|
mediaID: integer('media_id'),
|
||||||
|
},
|
||||||
|
(columns) => [
|
||||||
|
index('_products_v_rels_order_idx').on(columns.order),
|
||||||
|
index('_products_v_rels_parent_idx').on(columns.parent),
|
||||||
|
index('_products_v_rels_path_idx').on(columns.path),
|
||||||
|
index('_products_v_rels_media_id_idx').on(columns.mediaID),
|
||||||
|
foreignKey({
|
||||||
|
columns: [columns['parent']],
|
||||||
|
foreignColumns: [_products_v.id],
|
||||||
|
name: '_products_v_rels_parent_fk',
|
||||||
|
}).onDelete('cascade'),
|
||||||
|
foreignKey({
|
||||||
|
columns: [columns['mediaID']],
|
||||||
|
foreignColumns: [media.id],
|
||||||
|
name: '_products_v_rels_media_fk',
|
||||||
|
}).onDelete('cascade'),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const payload_kv = pgTable(
|
||||||
|
'payload_kv',
|
||||||
|
{
|
||||||
|
id: serial('id').primaryKey(),
|
||||||
|
key: varchar('key').notNull(),
|
||||||
|
data: jsonb('data').notNull(),
|
||||||
|
},
|
||||||
|
(columns) => [uniqueIndex('payload_kv_key_idx').on(columns.key)],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const payload_locked_documents = pgTable(
|
||||||
|
'payload_locked_documents',
|
||||||
|
{
|
||||||
|
id: serial('id').primaryKey(),
|
||||||
|
globalSlug: varchar('global_slug'),
|
||||||
|
updatedAt: timestamp('updated_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
createdAt: timestamp('created_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
},
|
||||||
|
(columns) => [
|
||||||
|
index('payload_locked_documents_global_slug_idx').on(columns.globalSlug),
|
||||||
|
index('payload_locked_documents_updated_at_idx').on(columns.updatedAt),
|
||||||
|
index('payload_locked_documents_created_at_idx').on(columns.createdAt),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const payload_locked_documents_rels = pgTable(
|
||||||
|
'payload_locked_documents_rels',
|
||||||
|
{
|
||||||
|
id: serial('id').primaryKey(),
|
||||||
|
order: integer('order'),
|
||||||
|
parent: integer('parent_id').notNull(),
|
||||||
|
path: varchar('path').notNull(),
|
||||||
|
usersID: integer('users_id'),
|
||||||
|
mediaID: integer('media_id'),
|
||||||
|
postsID: integer('posts_id'),
|
||||||
|
'form-submissionsID': integer('form_submissions_id'),
|
||||||
|
productsID: integer('products_id'),
|
||||||
|
},
|
||||||
|
(columns) => [
|
||||||
|
index('payload_locked_documents_rels_order_idx').on(columns.order),
|
||||||
|
index('payload_locked_documents_rels_parent_idx').on(columns.parent),
|
||||||
|
index('payload_locked_documents_rels_path_idx').on(columns.path),
|
||||||
|
index('payload_locked_documents_rels_users_id_idx').on(columns.usersID),
|
||||||
|
index('payload_locked_documents_rels_media_id_idx').on(columns.mediaID),
|
||||||
|
index('payload_locked_documents_rels_posts_id_idx').on(columns.postsID),
|
||||||
|
index('payload_locked_documents_rels_form_submissions_id_idx').on(
|
||||||
|
columns['form-submissionsID'],
|
||||||
|
),
|
||||||
|
index('payload_locked_documents_rels_products_id_idx').on(columns.productsID),
|
||||||
|
foreignKey({
|
||||||
|
columns: [columns['parent']],
|
||||||
|
foreignColumns: [payload_locked_documents.id],
|
||||||
|
name: 'payload_locked_documents_rels_parent_fk',
|
||||||
|
}).onDelete('cascade'),
|
||||||
|
foreignKey({
|
||||||
|
columns: [columns['usersID']],
|
||||||
|
foreignColumns: [users.id],
|
||||||
|
name: 'payload_locked_documents_rels_users_fk',
|
||||||
|
}).onDelete('cascade'),
|
||||||
|
foreignKey({
|
||||||
|
columns: [columns['mediaID']],
|
||||||
|
foreignColumns: [media.id],
|
||||||
|
name: 'payload_locked_documents_rels_media_fk',
|
||||||
|
}).onDelete('cascade'),
|
||||||
|
foreignKey({
|
||||||
|
columns: [columns['postsID']],
|
||||||
|
foreignColumns: [posts.id],
|
||||||
|
name: 'payload_locked_documents_rels_posts_fk',
|
||||||
|
}).onDelete('cascade'),
|
||||||
|
foreignKey({
|
||||||
|
columns: [columns['form-submissionsID']],
|
||||||
|
foreignColumns: [form_submissions.id],
|
||||||
|
name: 'payload_locked_documents_rels_form_submissions_fk',
|
||||||
|
}).onDelete('cascade'),
|
||||||
|
foreignKey({
|
||||||
|
columns: [columns['productsID']],
|
||||||
|
foreignColumns: [products.id],
|
||||||
|
name: 'payload_locked_documents_rels_products_fk',
|
||||||
|
}).onDelete('cascade'),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const payload_preferences = pgTable(
|
||||||
|
'payload_preferences',
|
||||||
|
{
|
||||||
|
id: serial('id').primaryKey(),
|
||||||
|
key: varchar('key'),
|
||||||
|
value: jsonb('value'),
|
||||||
|
updatedAt: timestamp('updated_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
createdAt: timestamp('created_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
},
|
||||||
|
(columns) => [
|
||||||
|
index('payload_preferences_key_idx').on(columns.key),
|
||||||
|
index('payload_preferences_updated_at_idx').on(columns.updatedAt),
|
||||||
|
index('payload_preferences_created_at_idx').on(columns.createdAt),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const payload_preferences_rels = pgTable(
|
||||||
|
'payload_preferences_rels',
|
||||||
|
{
|
||||||
|
id: serial('id').primaryKey(),
|
||||||
|
order: integer('order'),
|
||||||
|
parent: integer('parent_id').notNull(),
|
||||||
|
path: varchar('path').notNull(),
|
||||||
|
usersID: integer('users_id'),
|
||||||
|
},
|
||||||
|
(columns) => [
|
||||||
|
index('payload_preferences_rels_order_idx').on(columns.order),
|
||||||
|
index('payload_preferences_rels_parent_idx').on(columns.parent),
|
||||||
|
index('payload_preferences_rels_path_idx').on(columns.path),
|
||||||
|
index('payload_preferences_rels_users_id_idx').on(columns.usersID),
|
||||||
|
foreignKey({
|
||||||
|
columns: [columns['parent']],
|
||||||
|
foreignColumns: [payload_preferences.id],
|
||||||
|
name: 'payload_preferences_rels_parent_fk',
|
||||||
|
}).onDelete('cascade'),
|
||||||
|
foreignKey({
|
||||||
|
columns: [columns['usersID']],
|
||||||
|
foreignColumns: [users.id],
|
||||||
|
name: 'payload_preferences_rels_users_fk',
|
||||||
|
}).onDelete('cascade'),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const payload_migrations = pgTable(
|
||||||
|
'payload_migrations',
|
||||||
|
{
|
||||||
|
id: serial('id').primaryKey(),
|
||||||
|
name: varchar('name'),
|
||||||
|
batch: numeric('batch', { mode: 'number' }),
|
||||||
|
updatedAt: timestamp('updated_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
createdAt: timestamp('created_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
},
|
||||||
|
(columns) => [
|
||||||
|
index('payload_migrations_updated_at_idx').on(columns.updatedAt),
|
||||||
|
index('payload_migrations_created_at_idx').on(columns.createdAt),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const relations_users_sessions = relations(users_sessions, ({ one }) => ({
|
||||||
|
_parentID: one(users, {
|
||||||
|
fields: [users_sessions._parentID],
|
||||||
|
references: [users.id],
|
||||||
|
relationName: 'sessions',
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
export const relations_users = relations(users, ({ many }) => ({
|
||||||
|
sessions: many(users_sessions, {
|
||||||
|
relationName: 'sessions',
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
export const relations_media = relations(media, () => ({}));
|
||||||
|
export const relations_posts = relations(posts, ({ one }) => ({
|
||||||
|
featuredImage: one(media, {
|
||||||
|
fields: [posts.featuredImage],
|
||||||
|
references: [media.id],
|
||||||
|
relationName: 'featuredImage',
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
export const relations__posts_v = relations(_posts_v, ({ one }) => ({
|
||||||
|
parent: one(posts, {
|
||||||
|
fields: [_posts_v.parent],
|
||||||
|
references: [posts.id],
|
||||||
|
relationName: 'parent',
|
||||||
|
}),
|
||||||
|
version_featuredImage: one(media, {
|
||||||
|
fields: [_posts_v.version_featuredImage],
|
||||||
|
references: [media.id],
|
||||||
|
relationName: 'version_featuredImage',
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
export const relations_form_submissions = relations(form_submissions, () => ({}));
|
||||||
|
export const relations_products_categories = relations(products_categories, ({ one }) => ({
|
||||||
|
_parentID: one(products, {
|
||||||
|
fields: [products_categories._parentID],
|
||||||
|
references: [products.id],
|
||||||
|
relationName: 'categories',
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
export const relations_products_rels = relations(products_rels, ({ one }) => ({
|
||||||
|
parent: one(products, {
|
||||||
|
fields: [products_rels.parent],
|
||||||
|
references: [products.id],
|
||||||
|
relationName: '_rels',
|
||||||
|
}),
|
||||||
|
mediaID: one(media, {
|
||||||
|
fields: [products_rels.mediaID],
|
||||||
|
references: [media.id],
|
||||||
|
relationName: 'media',
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
export const relations_products = relations(products, ({ many }) => ({
|
||||||
|
categories: many(products_categories, {
|
||||||
|
relationName: 'categories',
|
||||||
|
}),
|
||||||
|
_rels: many(products_rels, {
|
||||||
|
relationName: '_rels',
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
export const relations__products_v_version_categories = relations(
|
||||||
|
_products_v_version_categories,
|
||||||
|
({ one }) => ({
|
||||||
|
_parentID: one(_products_v, {
|
||||||
|
fields: [_products_v_version_categories._parentID],
|
||||||
|
references: [_products_v.id],
|
||||||
|
relationName: 'version_categories',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
export const relations__products_v_rels = relations(_products_v_rels, ({ one }) => ({
|
||||||
|
parent: one(_products_v, {
|
||||||
|
fields: [_products_v_rels.parent],
|
||||||
|
references: [_products_v.id],
|
||||||
|
relationName: '_rels',
|
||||||
|
}),
|
||||||
|
mediaID: one(media, {
|
||||||
|
fields: [_products_v_rels.mediaID],
|
||||||
|
references: [media.id],
|
||||||
|
relationName: 'media',
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
export const relations__products_v = relations(_products_v, ({ one, many }) => ({
|
||||||
|
parent: one(products, {
|
||||||
|
fields: [_products_v.parent],
|
||||||
|
references: [products.id],
|
||||||
|
relationName: 'parent',
|
||||||
|
}),
|
||||||
|
version_categories: many(_products_v_version_categories, {
|
||||||
|
relationName: 'version_categories',
|
||||||
|
}),
|
||||||
|
_rels: many(_products_v_rels, {
|
||||||
|
relationName: '_rels',
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
export const relations_payload_kv = relations(payload_kv, () => ({}));
|
||||||
|
export const relations_payload_locked_documents_rels = relations(
|
||||||
|
payload_locked_documents_rels,
|
||||||
|
({ one }) => ({
|
||||||
|
parent: one(payload_locked_documents, {
|
||||||
|
fields: [payload_locked_documents_rels.parent],
|
||||||
|
references: [payload_locked_documents.id],
|
||||||
|
relationName: '_rels',
|
||||||
|
}),
|
||||||
|
usersID: one(users, {
|
||||||
|
fields: [payload_locked_documents_rels.usersID],
|
||||||
|
references: [users.id],
|
||||||
|
relationName: 'users',
|
||||||
|
}),
|
||||||
|
mediaID: one(media, {
|
||||||
|
fields: [payload_locked_documents_rels.mediaID],
|
||||||
|
references: [media.id],
|
||||||
|
relationName: 'media',
|
||||||
|
}),
|
||||||
|
postsID: one(posts, {
|
||||||
|
fields: [payload_locked_documents_rels.postsID],
|
||||||
|
references: [posts.id],
|
||||||
|
relationName: 'posts',
|
||||||
|
}),
|
||||||
|
'form-submissionsID': one(form_submissions, {
|
||||||
|
fields: [payload_locked_documents_rels['form-submissionsID']],
|
||||||
|
references: [form_submissions.id],
|
||||||
|
relationName: 'form-submissions',
|
||||||
|
}),
|
||||||
|
productsID: one(products, {
|
||||||
|
fields: [payload_locked_documents_rels.productsID],
|
||||||
|
references: [products.id],
|
||||||
|
relationName: 'products',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
export const relations_payload_locked_documents = relations(
|
||||||
|
payload_locked_documents,
|
||||||
|
({ many }) => ({
|
||||||
|
_rels: many(payload_locked_documents_rels, {
|
||||||
|
relationName: '_rels',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
export const relations_payload_preferences_rels = relations(
|
||||||
|
payload_preferences_rels,
|
||||||
|
({ one }) => ({
|
||||||
|
parent: one(payload_preferences, {
|
||||||
|
fields: [payload_preferences_rels.parent],
|
||||||
|
references: [payload_preferences.id],
|
||||||
|
relationName: '_rels',
|
||||||
|
}),
|
||||||
|
usersID: one(users, {
|
||||||
|
fields: [payload_preferences_rels.usersID],
|
||||||
|
references: [users.id],
|
||||||
|
relationName: 'users',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
export const relations_payload_preferences = relations(payload_preferences, ({ many }) => ({
|
||||||
|
_rels: many(payload_preferences_rels, {
|
||||||
|
relationName: '_rels',
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
export const relations_payload_migrations = relations(payload_migrations, () => ({}));
|
||||||
|
|
||||||
|
type DatabaseSchema = {
|
||||||
|
enum_posts_locale: typeof enum_posts_locale;
|
||||||
|
enum_posts_status: typeof enum_posts_status;
|
||||||
|
enum__posts_v_version_locale: typeof enum__posts_v_version_locale;
|
||||||
|
enum__posts_v_version_status: typeof enum__posts_v_version_status;
|
||||||
|
enum_form_submissions_type: typeof enum_form_submissions_type;
|
||||||
|
enum_products_locale: typeof enum_products_locale;
|
||||||
|
enum_products_status: typeof enum_products_status;
|
||||||
|
enum__products_v_version_locale: typeof enum__products_v_version_locale;
|
||||||
|
enum__products_v_version_status: typeof enum__products_v_version_status;
|
||||||
|
users_sessions: typeof users_sessions;
|
||||||
|
users: typeof users;
|
||||||
|
media: typeof media;
|
||||||
|
posts: typeof posts;
|
||||||
|
_posts_v: typeof _posts_v;
|
||||||
|
form_submissions: typeof form_submissions;
|
||||||
|
products_categories: typeof products_categories;
|
||||||
|
products: typeof products;
|
||||||
|
products_rels: typeof products_rels;
|
||||||
|
_products_v_version_categories: typeof _products_v_version_categories;
|
||||||
|
_products_v: typeof _products_v;
|
||||||
|
_products_v_rels: typeof _products_v_rels;
|
||||||
|
payload_kv: typeof payload_kv;
|
||||||
|
payload_locked_documents: typeof payload_locked_documents;
|
||||||
|
payload_locked_documents_rels: typeof payload_locked_documents_rels;
|
||||||
|
payload_preferences: typeof payload_preferences;
|
||||||
|
payload_preferences_rels: typeof payload_preferences_rels;
|
||||||
|
payload_migrations: typeof payload_migrations;
|
||||||
|
relations_users_sessions: typeof relations_users_sessions;
|
||||||
|
relations_users: typeof relations_users;
|
||||||
|
relations_media: typeof relations_media;
|
||||||
|
relations_posts: typeof relations_posts;
|
||||||
|
relations__posts_v: typeof relations__posts_v;
|
||||||
|
relations_form_submissions: typeof relations_form_submissions;
|
||||||
|
relations_products_categories: typeof relations_products_categories;
|
||||||
|
relations_products_rels: typeof relations_products_rels;
|
||||||
|
relations_products: typeof relations_products;
|
||||||
|
relations__products_v_version_categories: typeof relations__products_v_version_categories;
|
||||||
|
relations__products_v_rels: typeof relations__products_v_rels;
|
||||||
|
relations__products_v: typeof relations__products_v;
|
||||||
|
relations_payload_kv: typeof relations_payload_kv;
|
||||||
|
relations_payload_locked_documents_rels: typeof relations_payload_locked_documents_rels;
|
||||||
|
relations_payload_locked_documents: typeof relations_payload_locked_documents;
|
||||||
|
relations_payload_preferences_rels: typeof relations_payload_preferences_rels;
|
||||||
|
relations_payload_preferences: typeof relations_payload_preferences;
|
||||||
|
relations_payload_migrations: typeof relations_payload_migrations;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare module '@payloadcms/db-postgres' {
|
||||||
|
export interface GeneratedDatabaseSchema {
|
||||||
|
schema: DatabaseSchema;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/payload/blocks/AnimatedImage.ts
Normal file
25
src/payload/blocks/AnimatedImage.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Block } from 'payload';
|
||||||
|
|
||||||
|
export const AnimatedImage: Block = {
|
||||||
|
slug: 'animatedImage',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'src',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'alt',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'width',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'height',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
20
src/payload/blocks/Callout.ts
Normal file
20
src/payload/blocks/Callout.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { Block } from 'payload';
|
||||||
|
import { lexicalEditor } from '@payloadcms/richtext-lexical';
|
||||||
|
|
||||||
|
export const Callout: Block = {
|
||||||
|
slug: 'callout',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
type: 'select',
|
||||||
|
options: ['info', 'warning', 'important', 'tip', 'caution'],
|
||||||
|
defaultValue: 'info',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'content',
|
||||||
|
type: 'richText',
|
||||||
|
editor: lexicalEditor({}),
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
34
src/payload/blocks/ChatBubble.ts
Normal file
34
src/payload/blocks/ChatBubble.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Block } from 'payload';
|
||||||
|
import { lexicalEditor } from '@payloadcms/richtext-lexical';
|
||||||
|
|
||||||
|
export const ChatBubble: Block = {
|
||||||
|
slug: 'chatBubble',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'author',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: 'KLZ Team',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'avatar',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'role',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: 'Assistant',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'align',
|
||||||
|
type: 'select',
|
||||||
|
options: ['left', 'right'],
|
||||||
|
defaultValue: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'content',
|
||||||
|
type: 'richText',
|
||||||
|
editor: lexicalEditor({}),
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
47
src/payload/blocks/ComparisonGrid.ts
Normal file
47
src/payload/blocks/ComparisonGrid.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Block } from 'payload';
|
||||||
|
|
||||||
|
export const ComparisonGrid: Block = {
|
||||||
|
slug: 'comparisonGrid',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
label: 'Main Heading',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'leftLabel',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rightLabel',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'items',
|
||||||
|
type: 'array',
|
||||||
|
required: true,
|
||||||
|
minRows: 1,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'label',
|
||||||
|
label: 'Row Label',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'leftValue',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rightValue',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
20
src/payload/blocks/HighlightBox.ts
Normal file
20
src/payload/blocks/HighlightBox.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { Block } from 'payload';
|
||||||
|
import { lexicalEditor } from '@payloadcms/richtext-lexical';
|
||||||
|
|
||||||
|
export const HighlightBox: Block = {
|
||||||
|
slug: 'highlightBox',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
type: 'select',
|
||||||
|
options: ['info', 'warning', 'success', 'error', 'neutral'],
|
||||||
|
defaultValue: 'neutral',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'content',
|
||||||
|
type: 'richText',
|
||||||
|
editor: lexicalEditor({}),
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
12
src/payload/blocks/PowerCTA.ts
Normal file
12
src/payload/blocks/PowerCTA.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Block } from 'payload';
|
||||||
|
|
||||||
|
export const PowerCTA: Block = {
|
||||||
|
slug: 'powerCTA',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'locale',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
96
src/payload/blocks/ProductTabs.ts
Normal file
96
src/payload/blocks/ProductTabs.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { Block } from 'payload';
|
||||||
|
|
||||||
|
export const ProductTabs: Block = {
|
||||||
|
slug: 'productTabs',
|
||||||
|
interfaceName: 'ProductTabsBlock',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'technicalItems',
|
||||||
|
type: 'array',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'label',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'value',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'unit',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'voltageTables',
|
||||||
|
type: 'array',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'voltageLabel',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'metaItems',
|
||||||
|
type: 'array',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'label',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'value',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'unit',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'columns',
|
||||||
|
type: 'array',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'key',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'label',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rows',
|
||||||
|
type: 'array',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'configuration',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cells',
|
||||||
|
type: 'array',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'value',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
23
src/payload/blocks/SplitHeading.ts
Normal file
23
src/payload/blocks/SplitHeading.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Block } from 'payload';
|
||||||
|
|
||||||
|
export const SplitHeading: Block = {
|
||||||
|
slug: 'splitHeading',
|
||||||
|
interfaceName: 'SplitHeadingBlock',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'level',
|
||||||
|
type: 'select',
|
||||||
|
options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
|
||||||
|
defaultValue: 'h2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
25
src/payload/blocks/Stats.ts
Normal file
25
src/payload/blocks/Stats.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Block } from 'payload';
|
||||||
|
|
||||||
|
export const Stats: Block = {
|
||||||
|
slug: 'stats',
|
||||||
|
interfaceName: 'StatsBlock',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'stats',
|
||||||
|
type: 'array',
|
||||||
|
required: true,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'value',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'label',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
31
src/payload/blocks/StickyNarrative.ts
Normal file
31
src/payload/blocks/StickyNarrative.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Block } from 'payload';
|
||||||
|
|
||||||
|
export const StickyNarrative: Block = {
|
||||||
|
slug: 'stickyNarrative',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
label: 'Main Heading',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'items',
|
||||||
|
type: 'array',
|
||||||
|
required: true,
|
||||||
|
minRows: 1,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'content',
|
||||||
|
type: 'textarea',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
30
src/payload/blocks/TechnicalGrid.ts
Normal file
30
src/payload/blocks/TechnicalGrid.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Block } from 'payload';
|
||||||
|
|
||||||
|
export const TechnicalGrid: Block = {
|
||||||
|
slug: 'technicalGrid',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
label: 'Main Heading',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'items',
|
||||||
|
type: 'array',
|
||||||
|
required: true,
|
||||||
|
minRows: 1,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'label',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'value',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
30
src/payload/blocks/VisualLinkPreview.ts
Normal file
30
src/payload/blocks/VisualLinkPreview.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Block } from 'payload';
|
||||||
|
|
||||||
|
export const VisualLinkPreview: Block = {
|
||||||
|
slug: 'visualLinkPreview',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'url',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'summary',
|
||||||
|
type: 'textarea',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'image',
|
||||||
|
type: 'text',
|
||||||
|
admin: {
|
||||||
|
description: 'Legacy HTTP string from the old hardcoded images.',
|
||||||
|
},
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
27
src/payload/blocks/allBlocks.ts
Normal file
27
src/payload/blocks/allBlocks.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { AnimatedImage } from './AnimatedImage';
|
||||||
|
import { Callout } from './Callout';
|
||||||
|
import { ChatBubble } from './ChatBubble';
|
||||||
|
import { ComparisonGrid } from './ComparisonGrid';
|
||||||
|
import { HighlightBox } from './HighlightBox';
|
||||||
|
import { PowerCTA } from './PowerCTA';
|
||||||
|
import { ProductTabs } from './ProductTabs';
|
||||||
|
import { SplitHeading } from './SplitHeading';
|
||||||
|
import { Stats } from './Stats';
|
||||||
|
import { StickyNarrative } from './StickyNarrative';
|
||||||
|
import { TechnicalGrid } from './TechnicalGrid';
|
||||||
|
import { VisualLinkPreview } from './VisualLinkPreview';
|
||||||
|
|
||||||
|
export const payloadBlocks = [
|
||||||
|
AnimatedImage,
|
||||||
|
Callout,
|
||||||
|
ChatBubble,
|
||||||
|
ComparisonGrid,
|
||||||
|
HighlightBox,
|
||||||
|
PowerCTA,
|
||||||
|
ProductTabs,
|
||||||
|
SplitHeading,
|
||||||
|
Stats,
|
||||||
|
StickyNarrative,
|
||||||
|
TechnicalGrid,
|
||||||
|
VisualLinkPreview,
|
||||||
|
];
|
||||||
67
src/payload/collections/FormSubmissions.ts
Normal file
67
src/payload/collections/FormSubmissions.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import type { CollectionConfig } from 'payload';
|
||||||
|
|
||||||
|
export const FormSubmissions: CollectionConfig = {
|
||||||
|
slug: 'form-submissions',
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'name',
|
||||||
|
defaultColumns: ['name', 'email', 'type', 'createdAt'],
|
||||||
|
description: 'Captured leads from Contact and Product Quote forms.',
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
// Only Admins can view and delete leads via dashboard.
|
||||||
|
read: ({ req: { user } }) => Boolean(user) || process.env.NODE_ENV === 'development',
|
||||||
|
update: ({ req: { user } }) => Boolean(user) || process.env.NODE_ENV === 'development',
|
||||||
|
delete: ({ req: { user } }) => Boolean(user) || process.env.NODE_ENV === 'development',
|
||||||
|
// Next.js server actions handle secure inserts natively. No public client create access.
|
||||||
|
create: () => false,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
readOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'email',
|
||||||
|
type: 'email',
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
readOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'General Contact', value: 'contact' },
|
||||||
|
{ label: 'Product Quote', value: 'product_quote' },
|
||||||
|
],
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
position: 'sidebar',
|
||||||
|
readOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'productName',
|
||||||
|
type: 'text',
|
||||||
|
admin: {
|
||||||
|
position: 'sidebar',
|
||||||
|
readOnly: true,
|
||||||
|
condition: (data) => data.type === 'product_quote',
|
||||||
|
description: 'The specific KLZ product the user requested a quote for.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'message',
|
||||||
|
type: 'textarea',
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
readOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
48
src/payload/collections/Media.ts
Normal file
48
src/payload/collections/Media.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import type { CollectionConfig } from 'payload';
|
||||||
|
|
||||||
|
export const Media: CollectionConfig = {
|
||||||
|
slug: 'media',
|
||||||
|
access: {
|
||||||
|
read: () => true,
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'alt',
|
||||||
|
defaultColumns: ['filename', 'alt', 'updatedAt'],
|
||||||
|
},
|
||||||
|
upload: {
|
||||||
|
staticDir: 'public/media',
|
||||||
|
adminThumbnail: 'thumbnail',
|
||||||
|
imageSizes: [
|
||||||
|
{
|
||||||
|
name: 'thumbnail',
|
||||||
|
width: 600,
|
||||||
|
// height: undefined allows wide 5:1 aspect ratios to be preserved without cropping
|
||||||
|
height: undefined,
|
||||||
|
position: 'centre',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'card',
|
||||||
|
width: 768,
|
||||||
|
height: undefined,
|
||||||
|
position: 'centre',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tablet',
|
||||||
|
width: 1024,
|
||||||
|
height: undefined,
|
||||||
|
position: 'centre',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'alt',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'caption',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
61
src/payload/collections/Pages.ts
Normal file
61
src/payload/collections/Pages.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { CollectionConfig } from 'payload';
|
||||||
|
import { lexicalEditor } from '@payloadcms/richtext-lexical';
|
||||||
|
|
||||||
|
export const Pages: CollectionConfig = {
|
||||||
|
slug: 'pages',
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'title',
|
||||||
|
defaultColumns: ['title', 'slug', 'locale', 'updatedAt'],
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
read: () => true,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'slug',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
position: 'sidebar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'locale',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'English', value: 'en' },
|
||||||
|
{ label: 'German', value: 'de' },
|
||||||
|
],
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
position: 'sidebar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'excerpt',
|
||||||
|
type: 'textarea',
|
||||||
|
admin: {
|
||||||
|
position: 'sidebar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'featuredImage',
|
||||||
|
type: 'upload',
|
||||||
|
relationTo: 'media',
|
||||||
|
admin: {
|
||||||
|
position: 'sidebar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'content',
|
||||||
|
type: 'richText',
|
||||||
|
editor: lexicalEditor({}),
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
154
src/payload/collections/Posts.ts
Normal file
154
src/payload/collections/Posts.ts
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import type { CollectionConfig } from 'payload';
|
||||||
|
import { lexicalEditor, BlocksFeature } from '@payloadcms/richtext-lexical';
|
||||||
|
|
||||||
|
import { StickyNarrative } from '../blocks/StickyNarrative';
|
||||||
|
import { ComparisonGrid } from '../blocks/ComparisonGrid';
|
||||||
|
import { VisualLinkPreview } from '../blocks/VisualLinkPreview';
|
||||||
|
import { TechnicalGrid } from '../blocks/TechnicalGrid';
|
||||||
|
import { HighlightBox } from '../blocks/HighlightBox';
|
||||||
|
import { AnimatedImage } from '../blocks/AnimatedImage';
|
||||||
|
import { ChatBubble } from '../blocks/ChatBubble';
|
||||||
|
import { PowerCTA } from '../blocks/PowerCTA';
|
||||||
|
import { Callout } from '../blocks/Callout';
|
||||||
|
import { Stats } from '../blocks/Stats';
|
||||||
|
import { SplitHeading } from '../blocks/SplitHeading';
|
||||||
|
|
||||||
|
export const Posts: CollectionConfig = {
|
||||||
|
slug: 'posts',
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'title',
|
||||||
|
defaultColumns: ['featuredImage', 'title', 'date', 'updatedAt', '_status'],
|
||||||
|
},
|
||||||
|
versions: {
|
||||||
|
drafts: true, // Enables Draft/Published workflows
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
read: ({ req: { user } }) => {
|
||||||
|
// In local development, always show everything (including Drafts and scheduled future posts)
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If an Admin user is logged in, they can view everything
|
||||||
|
if (user) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For public unauthenticated visitors in PROD/STAGING contexts:
|
||||||
|
// Only serve Posts where Status = "published" AND the publish Date is in the past!
|
||||||
|
return {
|
||||||
|
and: [
|
||||||
|
{
|
||||||
|
_status: {
|
||||||
|
equals: 'published',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: {
|
||||||
|
less_than_equal: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'slug',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
admin: {
|
||||||
|
position: 'sidebar',
|
||||||
|
},
|
||||||
|
hooks: {
|
||||||
|
beforeValidate: [
|
||||||
|
({ value, data }) => {
|
||||||
|
// Auto-generate slug from title if left blank
|
||||||
|
if (value || !data?.title) return value;
|
||||||
|
return data.title
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ /g, '-')
|
||||||
|
.replace(/[^\w-]+/g, '');
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'excerpt',
|
||||||
|
type: 'text',
|
||||||
|
admin: {
|
||||||
|
description: 'A short summary for blog feed cards and SEO.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'date',
|
||||||
|
type: 'date',
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
position: 'sidebar',
|
||||||
|
description: 'Future dates will schedule the post to publish automatically.',
|
||||||
|
},
|
||||||
|
defaultValue: () => new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'featuredImage',
|
||||||
|
type: 'upload',
|
||||||
|
relationTo: 'media',
|
||||||
|
admin: {
|
||||||
|
position: 'sidebar',
|
||||||
|
description: 'The primary Hero image used for headers and OpenGraph previews.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'locale',
|
||||||
|
type: 'select',
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
position: 'sidebar',
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{ label: 'English', value: 'en' },
|
||||||
|
{ label: 'German', value: 'de' },
|
||||||
|
],
|
||||||
|
defaultValue: 'en',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'category',
|
||||||
|
type: 'text',
|
||||||
|
admin: {
|
||||||
|
position: 'sidebar',
|
||||||
|
description: 'Used for tag bucketing (e.g. "Kabel Technologie").',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'content',
|
||||||
|
type: 'richText',
|
||||||
|
editor: lexicalEditor({
|
||||||
|
features: ({ defaultFeatures }) => [
|
||||||
|
...defaultFeatures,
|
||||||
|
BlocksFeature({
|
||||||
|
blocks: [
|
||||||
|
StickyNarrative,
|
||||||
|
ComparisonGrid,
|
||||||
|
VisualLinkPreview,
|
||||||
|
TechnicalGrid,
|
||||||
|
HighlightBox,
|
||||||
|
AnimatedImage,
|
||||||
|
ChatBubble,
|
||||||
|
PowerCTA,
|
||||||
|
Callout,
|
||||||
|
Stats,
|
||||||
|
SplitHeading,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
144
src/payload/collections/Products.ts
Normal file
144
src/payload/collections/Products.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import type { CollectionConfig } from 'payload';
|
||||||
|
import { lexicalEditor, BlocksFeature } from '@payloadcms/richtext-lexical';
|
||||||
|
|
||||||
|
import { StickyNarrative } from '../blocks/StickyNarrative';
|
||||||
|
import { ComparisonGrid } from '../blocks/ComparisonGrid';
|
||||||
|
import { VisualLinkPreview } from '../blocks/VisualLinkPreview';
|
||||||
|
import { TechnicalGrid } from '../blocks/TechnicalGrid';
|
||||||
|
import { HighlightBox } from '../blocks/HighlightBox';
|
||||||
|
import { AnimatedImage } from '../blocks/AnimatedImage';
|
||||||
|
import { ChatBubble } from '../blocks/ChatBubble';
|
||||||
|
import { PowerCTA } from '../blocks/PowerCTA';
|
||||||
|
import { Callout } from '../blocks/Callout';
|
||||||
|
import { Stats } from '../blocks/Stats';
|
||||||
|
import { SplitHeading } from '../blocks/SplitHeading';
|
||||||
|
import { ProductTabs } from '../blocks/ProductTabs';
|
||||||
|
|
||||||
|
export const Products: CollectionConfig = {
|
||||||
|
slug: 'products',
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'title',
|
||||||
|
defaultColumns: ['featuredImage', 'title', 'sku', 'locale', 'updatedAt', '_status'],
|
||||||
|
},
|
||||||
|
versions: {
|
||||||
|
drafts: true,
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
read: ({ req: { user } }) => {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (user) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
_status: {
|
||||||
|
equals: 'published',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sku',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
position: 'sidebar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'slug',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
position: 'sidebar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
type: 'textarea',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'locale',
|
||||||
|
type: 'select',
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
position: 'sidebar',
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{ label: 'English', value: 'en' },
|
||||||
|
{ label: 'German', value: 'de' },
|
||||||
|
],
|
||||||
|
defaultValue: 'de',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'categories',
|
||||||
|
type: 'array',
|
||||||
|
required: true,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'category',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
admin: {
|
||||||
|
position: 'sidebar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'featuredImage',
|
||||||
|
type: 'upload',
|
||||||
|
relationTo: 'media',
|
||||||
|
admin: {
|
||||||
|
position: 'sidebar',
|
||||||
|
description: 'The primary thumbnail used in list views.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'images',
|
||||||
|
type: 'upload',
|
||||||
|
relationTo: 'media',
|
||||||
|
hasMany: true,
|
||||||
|
admin: {
|
||||||
|
position: 'sidebar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'application',
|
||||||
|
type: 'richText',
|
||||||
|
editor: lexicalEditor({}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'content',
|
||||||
|
type: 'richText',
|
||||||
|
editor: lexicalEditor({
|
||||||
|
features: ({ defaultFeatures }) => [
|
||||||
|
...defaultFeatures,
|
||||||
|
BlocksFeature({
|
||||||
|
blocks: [
|
||||||
|
StickyNarrative,
|
||||||
|
ComparisonGrid,
|
||||||
|
VisualLinkPreview,
|
||||||
|
TechnicalGrid,
|
||||||
|
HighlightBox,
|
||||||
|
AnimatedImage,
|
||||||
|
ChatBubble,
|
||||||
|
PowerCTA,
|
||||||
|
Callout,
|
||||||
|
Stats,
|
||||||
|
SplitHeading,
|
||||||
|
ProductTabs,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
12
src/payload/collections/Users.ts
Normal file
12
src/payload/collections/Users.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import type { CollectionConfig } from 'payload';
|
||||||
|
|
||||||
|
export const Users: CollectionConfig = {
|
||||||
|
slug: 'users',
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'email',
|
||||||
|
},
|
||||||
|
auth: true,
|
||||||
|
fields: [
|
||||||
|
// Email added by default
|
||||||
|
],
|
||||||
|
};
|
||||||
296
src/payload/utils/lexicalParser.ts
Normal file
296
src/payload/utils/lexicalParser.ts
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
/**
|
||||||
|
* Converts a Markdown+JSX string into a Lexical AST node array.
|
||||||
|
* Specifically adapted for klz-cables.com custom Component Blocks.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function propValue(chunk: string, prop: string): string {
|
||||||
|
// Match prop="value" or prop='value' or prop={value}
|
||||||
|
// and also multiline props like prop={\n [\n {...}\n ]\n}
|
||||||
|
// For arrays or complex objects passed as props, basic regex might fail,
|
||||||
|
// but the MDX in klz-cables usually uses simpler props or children.
|
||||||
|
const match =
|
||||||
|
chunk.match(new RegExp(`${prop}=["']([^"']+)["']`)) ||
|
||||||
|
chunk.match(new RegExp(`${prop}=\\{([^}]+)\\}`));
|
||||||
|
return match ? match[1] : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractItemsProp(chunk: string, startTag: string): any[] {
|
||||||
|
// Match items={ [ ... ] } robustly without stopping at inner object braces
|
||||||
|
const itemsMatch = chunk.match(/items=\{\s*(\[[\s\S]*?\])\s*\}/);
|
||||||
|
if (itemsMatch) {
|
||||||
|
try {
|
||||||
|
const arrayString = itemsMatch[1].trim();
|
||||||
|
// Since klz-cables MDX passes pure JS object arrays like `items={[{title: 'A', content: 'B'}]}`,
|
||||||
|
// parsing it via Regex to JSON is extremely brittle due to unquoted keys and trailing commas.
|
||||||
|
// Using `new Function` safely evaluates the array AST directly in this Node script environment.
|
||||||
|
const fn = new Function(`return ${arrayString};`);
|
||||||
|
return fn();
|
||||||
|
} catch (_e: any) {
|
||||||
|
console.warn(`Could not parse items array for block ${startTag}:`, _e.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function blockNode(blockType: string, fields: Record<string, any>) {
|
||||||
|
return { type: 'block', format: '', version: 2, fields: { blockType, ...fields } };
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureChildren(parsedNodes: any[]): any[] {
|
||||||
|
// Lexical root nodes require at least one child node, or validation fails
|
||||||
|
if (parsedNodes.length === 0) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
format: '',
|
||||||
|
indent: 0,
|
||||||
|
version: 1,
|
||||||
|
children: [{ mode: 'normal', type: 'text', text: ' ', version: 1 }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return parsedNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseMarkdownToLexical(markdown: string): any[] {
|
||||||
|
const textNode = (text: string) => ({
|
||||||
|
type: 'paragraph',
|
||||||
|
format: '',
|
||||||
|
indent: 0,
|
||||||
|
version: 1,
|
||||||
|
children: [{ mode: 'normal', type: 'text', text, version: 1 }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const nodes: any[] = [];
|
||||||
|
let content = markdown;
|
||||||
|
|
||||||
|
// Strip frontmatter
|
||||||
|
const fm = content.match(/^---\s*\n[\s\S]*?\n---/);
|
||||||
|
if (fm) content = content.replace(fm[0], '').trim();
|
||||||
|
|
||||||
|
// 1. EXTRACT MULTILINE WRAPPERS BEFORE CHUNKING
|
||||||
|
// This allows nested newlines inside components without breaking them.
|
||||||
|
const extractBlocks = [
|
||||||
|
{
|
||||||
|
tag: 'HighlightBox',
|
||||||
|
regex: /<HighlightBox([^>]*)>([\s\S]*?)<\/HighlightBox>/g,
|
||||||
|
build: (props: string, inner: string) =>
|
||||||
|
blockNode('highlightBox', {
|
||||||
|
title: propValue(`<Tag ${props}>`, 'title'),
|
||||||
|
color: propValue(`<Tag ${props}>`, 'color') || 'primary',
|
||||||
|
content: {
|
||||||
|
root: {
|
||||||
|
type: 'root',
|
||||||
|
format: '',
|
||||||
|
indent: 0,
|
||||||
|
version: 1,
|
||||||
|
direction: 'ltr',
|
||||||
|
children: ensureChildren(parseMarkdownToLexical(inner.trim())),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'ChatBubble',
|
||||||
|
regex: /<ChatBubble([^>]*)>([\s\S]*?)<\/ChatBubble>/g,
|
||||||
|
build: (props: string, inner: string) =>
|
||||||
|
blockNode('chatBubble', {
|
||||||
|
author: propValue(`<Tag ${props}>`, 'author') || 'KLZ Team',
|
||||||
|
avatar: propValue(`<Tag ${props}>`, 'avatar'),
|
||||||
|
role: propValue(`<Tag ${props}>`, 'role') || 'Assistant',
|
||||||
|
align: propValue(`<Tag ${props}>`, 'align') || 'left',
|
||||||
|
content: {
|
||||||
|
root: {
|
||||||
|
type: 'root',
|
||||||
|
format: '',
|
||||||
|
indent: 0,
|
||||||
|
version: 1,
|
||||||
|
direction: 'ltr',
|
||||||
|
children: ensureChildren(parseMarkdownToLexical(inner.trim())),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'Callout',
|
||||||
|
regex: /<Callout([^>]*)>([\s\S]*?)<\/Callout>/g,
|
||||||
|
build: (props: string, inner: string) =>
|
||||||
|
blockNode('callout', {
|
||||||
|
type: propValue(`<Tag ${props}>`, 'type') || 'info',
|
||||||
|
title: propValue(`<Tag ${props}>`, 'title'),
|
||||||
|
content: {
|
||||||
|
root: {
|
||||||
|
type: 'root',
|
||||||
|
format: '',
|
||||||
|
indent: 0,
|
||||||
|
version: 1,
|
||||||
|
direction: 'ltr',
|
||||||
|
children: ensureChildren(parseMarkdownToLexical(inner.trim())),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Placeholder map to temporarily store extracted multi-line blocks
|
||||||
|
const placeholders = new Map<string, any>();
|
||||||
|
let placeholderIdx = 0;
|
||||||
|
|
||||||
|
for (const block of extractBlocks) {
|
||||||
|
content = content.replace(block.regex, (match, propsMatch, innerMatch) => {
|
||||||
|
const id = `__BLOCK_PLACEHOLDER_${placeholderIdx++}__`;
|
||||||
|
placeholders.set(id, block.build(propsMatch, innerMatch));
|
||||||
|
return `\n\n${id}\n\n`; // Pad with newlines so it becomes its own chunk
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. CHUNK THE REST (Paragraphs, Single-line Components)
|
||||||
|
const rawChunks = content.split(/\n\s*\n/);
|
||||||
|
|
||||||
|
for (let chunk of rawChunks) {
|
||||||
|
chunk = chunk.trim();
|
||||||
|
if (!chunk) continue;
|
||||||
|
|
||||||
|
// Has Placeholder?
|
||||||
|
if (chunk.startsWith('__BLOCK_PLACEHOLDER_')) {
|
||||||
|
nodes.push(placeholders.get(chunk));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Custom Component: ProductTabs ---
|
||||||
|
if (chunk.includes('<ProductTabs')) {
|
||||||
|
const dataMatch = chunk.match(/data=\{({[\s\S]*?})\}\s*\/>/);
|
||||||
|
if (dataMatch) {
|
||||||
|
try {
|
||||||
|
const parsedData = JSON.parse(dataMatch[1]);
|
||||||
|
|
||||||
|
// Normalize String Arrays to Payload Object Arrays { value: "string" }
|
||||||
|
if (parsedData.voltageTables) {
|
||||||
|
parsedData.voltageTables.forEach((vt: any) => {
|
||||||
|
if (vt.rows) {
|
||||||
|
vt.rows.forEach((row: any) => {
|
||||||
|
if (row.cells && Array.isArray(row.cells)) {
|
||||||
|
row.cells = row.cells.map((cell: any) =>
|
||||||
|
typeof cell !== 'object' || cell === null ? { value: String(cell) } : cell,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes.push(
|
||||||
|
blockNode('productTabs', {
|
||||||
|
technicalItems: parsedData.technicalItems || [],
|
||||||
|
voltageTables: parsedData.voltageTables || [],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.warn(`Could not parse JSON payload for ProductTabs:`, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Custom Component: StickyNarrative ---
|
||||||
|
if (chunk.includes('<StickyNarrative')) {
|
||||||
|
nodes.push(
|
||||||
|
blockNode('stickyNarrative', {
|
||||||
|
title: propValue(chunk, 'title'),
|
||||||
|
items: extractItemsProp(chunk, 'StickyNarrative'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Custom Component: ComparisonGrid ---
|
||||||
|
if (chunk.includes('<ComparisonGrid')) {
|
||||||
|
nodes.push(
|
||||||
|
blockNode('comparisonGrid', {
|
||||||
|
title: propValue(chunk, 'title'),
|
||||||
|
leftLabel: propValue(chunk, 'leftLabel'),
|
||||||
|
rightLabel: propValue(chunk, 'rightLabel'),
|
||||||
|
items: extractItemsProp(chunk, 'ComparisonGrid'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Custom Component: VisualLinkPreview ---
|
||||||
|
if (chunk.includes('<VisualLinkPreview')) {
|
||||||
|
nodes.push(
|
||||||
|
blockNode('visualLinkPreview', {
|
||||||
|
url: propValue(chunk, 'url'),
|
||||||
|
title: propValue(chunk, 'title'),
|
||||||
|
summary: propValue(chunk, 'summary'),
|
||||||
|
image: propValue(chunk, 'image'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Custom Component: TechnicalGrid ---
|
||||||
|
if (chunk.includes('<TechnicalGrid')) {
|
||||||
|
nodes.push(
|
||||||
|
blockNode('technicalGrid', {
|
||||||
|
title: propValue(chunk, 'title'),
|
||||||
|
items: extractItemsProp(chunk, 'TechnicalGrid'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Custom Component: AnimatedImage ---
|
||||||
|
if (chunk.includes('<AnimatedImage')) {
|
||||||
|
const widthMatch = chunk.match(/width=\{?(\d+)\}?/);
|
||||||
|
const heightMatch = chunk.match(/height=\{?(\d+)\}?/);
|
||||||
|
nodes.push(
|
||||||
|
blockNode('animatedImage', {
|
||||||
|
src: propValue(chunk, 'src'),
|
||||||
|
alt: propValue(chunk, 'alt'),
|
||||||
|
width: widthMatch ? parseInt(widthMatch[1], 10) : undefined,
|
||||||
|
height: heightMatch ? parseInt(heightMatch[1], 10) : undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Custom Component: PowerCTA ---
|
||||||
|
if (chunk.includes('<PowerCTA')) {
|
||||||
|
nodes.push(
|
||||||
|
blockNode('powerCTA', {
|
||||||
|
locale: propValue(chunk, 'locale') || 'de',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Standard Markdown: Headings ---
|
||||||
|
const headingMatch = chunk.match(/^(#{1,6})\s+(.*)/);
|
||||||
|
if (headingMatch) {
|
||||||
|
nodes.push({
|
||||||
|
type: 'heading',
|
||||||
|
tag: `h${headingMatch[1].length}`,
|
||||||
|
format: '',
|
||||||
|
indent: 0,
|
||||||
|
version: 1,
|
||||||
|
direction: 'ltr',
|
||||||
|
children: [{ mode: 'normal', type: 'text', text: headingMatch[2], version: 1 }],
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Standard Markdown: Images ---
|
||||||
|
const imageMatch = chunk.match(/^!\[([^\]]*)\]\(([^)]+)\)$/);
|
||||||
|
if (imageMatch) {
|
||||||
|
nodes.push(textNode(chunk));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: plain text paragraph
|
||||||
|
nodes.push(textNode(chunk));
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
@@ -54,6 +54,7 @@
|
|||||||
stroke-dasharray: 1;
|
stroke-dasharray: 1;
|
||||||
stroke-dashoffset: 1;
|
stroke-dashoffset: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
stroke-dasharray: 1;
|
stroke-dasharray: 1;
|
||||||
stroke-dashoffset: 0;
|
stroke-dashoffset: 0;
|
||||||
@@ -174,6 +175,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
|
html {
|
||||||
|
/* Scale text down slightly on mobile (87.5% of 16px = 14px) */
|
||||||
|
font-size: 87.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
html {
|
||||||
|
/* Normal scaling on tablet and desktop (100% = 16px) */
|
||||||
|
font-size: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.bg-primary a,
|
.bg-primary a,
|
||||||
.bg-primary-dark a {
|
.bg-primary-dark a {
|
||||||
@apply text-white/90 hover:text-white transition-colors;
|
@apply text-white/90 hover:text-white transition-colors;
|
||||||
|
|||||||
@@ -56,21 +56,21 @@ module.exports = {
|
|||||||
heading: ['Inter', 'system-ui', 'sans-serif'],
|
heading: ['Inter', 'system-ui', 'sans-serif'],
|
||||||
body: ['Inter', 'system-ui', 'sans-serif'],
|
body: ['Inter', 'system-ui', 'sans-serif'],
|
||||||
},
|
},
|
||||||
// Enhanced Fluid Typography with CSS Clamp
|
// Standard Fluid Typography
|
||||||
// Improved readability with better line heights and spacing
|
// Responsive scaling is handled globally via the html selector percentage in globals.css
|
||||||
fontSize: {
|
fontSize: {
|
||||||
'xs': ['clamp(0.75rem, 0.7rem + 0.2vw, 0.875rem)', { lineHeight: '1.6' }],
|
'xs': ['0.75rem', { lineHeight: '1.6' }],
|
||||||
'sm': ['clamp(0.875rem, 0.8rem + 0.25vw, 1rem)', { lineHeight: '1.6' }],
|
'sm': ['0.875rem', { lineHeight: '1.6' }],
|
||||||
'base': ['clamp(1rem, 0.9rem + 0.35vw, 1.125rem)', { lineHeight: '1.7' }],
|
'base': ['1rem', { lineHeight: '1.7' }],
|
||||||
'lg': ['clamp(1.125rem, 1rem + 0.4vw, 1.25rem)', { lineHeight: '1.7' }],
|
'lg': ['1.125rem', { lineHeight: '1.7' }],
|
||||||
'xl': ['clamp(1.25rem, 1.1rem + 0.5vw, 1.5rem)', { lineHeight: '1.6' }],
|
'xl': ['1.25rem', { lineHeight: '1.6' }],
|
||||||
'2xl': ['clamp(1.5rem, 1.3rem + 0.75vw, 1.875rem)', { lineHeight: '1.5' }],
|
'2xl': ['1.5rem', { lineHeight: '1.5' }],
|
||||||
'3xl': ['clamp(1.875rem, 1.6rem + 1vw, 2.25rem)', { lineHeight: '1.4' }],
|
'3xl': ['1.875rem', { lineHeight: '1.4' }],
|
||||||
'4xl': ['clamp(2.25rem, 1.9rem + 1.25vw, 3rem)', { lineHeight: '1.3' }],
|
'4xl': ['2.25rem', { lineHeight: '1.3' }],
|
||||||
'5xl': ['clamp(3rem, 2.5rem + 2vw, 3.75rem)', { lineHeight: '1.25' }],
|
'5xl': ['3rem', { lineHeight: '1.25' }],
|
||||||
'6xl': ['clamp(3.75rem, 3rem + 2.5vw, 4.5rem)', { lineHeight: '1.2' }],
|
'6xl': ['3.75rem', { lineHeight: '1.2' }],
|
||||||
'7xl': ['clamp(4.5rem, 3.5rem + 3vw, 5.5rem)', { lineHeight: '1.15' }],
|
'7xl': ['4.5rem', { lineHeight: '1.15' }],
|
||||||
'8xl': ['clamp(5.5rem, 4rem + 4vw, 7rem)', { lineHeight: '1.1' }],
|
'8xl': ['6rem', { lineHeight: '1.1' }],
|
||||||
},
|
},
|
||||||
fontWeight: {
|
fontWeight: {
|
||||||
regular: '400',
|
regular: '400',
|
||||||
|
|||||||
@@ -3,18 +3,11 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": ["./*"],
|
||||||
"./*"
|
"lib/*": ["./lib/*"],
|
||||||
],
|
"components/*": ["./components/*"],
|
||||||
"lib/*": [
|
"data/*": ["./data/*"],
|
||||||
"./lib/*"
|
"@payload-config": ["./payload.config.ts"]
|
||||||
],
|
|
||||||
"components/*": [
|
|
||||||
"./components/*"
|
|
||||||
],
|
|
||||||
"data/*": [
|
|
||||||
"./data/*"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
@@ -25,11 +18,5 @@
|
|||||||
"tests/**/*.test.ts",
|
"tests/**/*.test.ts",
|
||||||
".next/dev/types/**/*.ts"
|
".next/dev/types/**/*.ts"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": ["node_modules", "scripts", "reference", "data", "remotion"]
|
||||||
"node_modules",
|
|
||||||
"scripts",
|
|
||||||
"reference",
|
|
||||||
"data",
|
|
||||||
"remotion"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user