Compare commits
5 Commits
v1.8.20
...
96de68a063
| Author | SHA1 | Date | |
|---|---|---|---|
| 96de68a063 | |||
| 4a5017124a | |||
| 283de11b11 | |||
| 31685a458b | |||
| 6864903cff |
@@ -235,6 +235,14 @@ jobs:
|
|||||||
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID || vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}
|
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID || vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}
|
||||||
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
||||||
PROJECT_COLOR: ${{ secrets.PROJECT_COLOR || vars.PROJECT_COLOR || '#ff00ff' }}
|
PROJECT_COLOR: ${{ secrets.PROJECT_COLOR || vars.PROJECT_COLOR || '#ff00ff' }}
|
||||||
|
|
||||||
|
# S3 Object Storage
|
||||||
|
S3_ENDPOINT: ${{ secrets.S3_ENDPOINT || vars.S3_ENDPOINT || 'https://fsn1.your-objectstorage.com' }}
|
||||||
|
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY || vars.S3_ACCESS_KEY }}
|
||||||
|
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY || vars.S3_SECRET_KEY }}
|
||||||
|
S3_BUCKET: ${{ secrets.S3_BUCKET || vars.S3_BUCKET || 'mintel' }}
|
||||||
|
S3_REGION: ${{ secrets.S3_REGION || vars.S3_REGION || 'fsn1' }}
|
||||||
|
S3_PREFIX: ${{ secrets.S3_PREFIX || vars.S3_PREFIX || github.event.repository.name }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -297,6 +305,14 @@ jobs:
|
|||||||
NEXT_PUBLIC_UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID
|
NEXT_PUBLIC_UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID
|
||||||
UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
|
UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
|
||||||
|
|
||||||
|
# S3 Object Storage
|
||||||
|
S3_ENDPOINT=$S3_ENDPOINT
|
||||||
|
S3_ACCESS_KEY=$S3_ACCESS_KEY
|
||||||
|
S3_SECRET_KEY=$S3_SECRET_KEY
|
||||||
|
S3_BUCKET=$S3_BUCKET
|
||||||
|
S3_REGION=$S3_REGION
|
||||||
|
S3_PREFIX=$S3_PREFIX
|
||||||
|
|
||||||
TARGET=$TARGET
|
TARGET=$TARGET
|
||||||
SENTRY_ENVIRONMENT=$TARGET
|
SENTRY_ENVIRONMENT=$TARGET
|
||||||
PROJECT_NAME=$PROJECT_NAME
|
PROJECT_NAME=$PROJECT_NAME
|
||||||
|
|||||||
5
.gitignore
vendored
@@ -3,6 +3,7 @@ dist/
|
|||||||
.next/
|
.next/
|
||||||
out/
|
out/
|
||||||
.contentlayer/
|
.contentlayer/
|
||||||
|
.pnpm-store
|
||||||
|
|
||||||
# generated types
|
# generated types
|
||||||
.astro/
|
.astro/
|
||||||
@@ -46,3 +47,7 @@ pnpm-debug.log*
|
|||||||
.cache/
|
.cache/
|
||||||
cloned-websites/
|
cloned-websites/
|
||||||
storage/
|
storage/
|
||||||
|
|
||||||
|
# Estimation Engine Data
|
||||||
|
data/crawls/
|
||||||
|
apps/web/out/estimations/
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
# Stage 1: Builder
|
# Stage 1: Builder
|
||||||
FROM registry.infra.mintel.me/mintel/nextjs:latest AS builder
|
FROM registry.infra.mintel.me/mintel/nextjs:v1.8.21 AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Arguments for build-time configuration
|
# Arguments for build-time configuration
|
||||||
ARG NEXT_PUBLIC_BASE_URL
|
ARG NEXT_PUBLIC_BASE_URL
|
||||||
ARG NEXT_PUBLIC_TARGET
|
ARG NEXT_PUBLIC_TARGET
|
||||||
ARG DIRECTUS_URL
|
|
||||||
ARG UMAMI_API_ENDPOINT
|
ARG UMAMI_API_ENDPOINT
|
||||||
ARG NPM_TOKEN
|
ARG NPM_TOKEN
|
||||||
|
|
||||||
# Environment variables for Next.js build
|
# Environment variables for Next.js build
|
||||||
ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
|
ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
|
||||||
ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET
|
ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET
|
||||||
ENV DIRECTUS_URL=$DIRECTUS_URL
|
|
||||||
ENV UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
|
ENV UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
|
||||||
ENV SKIP_RUNTIME_ENV_VALIDATION=true
|
ENV SKIP_RUNTIME_ENV_VALIDATION=true
|
||||||
ENV CI=true
|
ENV CI=true
|
||||||
|
|||||||
20
Dockerfile.dev
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
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 to a location we can volume-mount
|
||||||
|
ENV PNPM_HOME="/pnpm"
|
||||||
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
|
|
||||||
|
# Set up pnpm store configuration
|
||||||
|
RUN pnpm config set store-dir /pnpm/store
|
||||||
|
|
||||||
|
# Note: Dependency installation happens at runtime to support linked packages
|
||||||
|
# and named volumes, but the base image is now optimized for the stack.
|
||||||
|
EXPOSE 3000
|
||||||
95
Posts.ts.tmp
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import type { CollectionConfig } from "payload";
|
||||||
|
import { lexicalEditor, BlocksFeature } from "@payloadcms/richtext-lexical";
|
||||||
|
import { allBlocks } from "../blocks/allBlocks";
|
||||||
|
|
||||||
|
export const Posts: CollectionConfig = {
|
||||||
|
slug: "posts",
|
||||||
|
admin: {
|
||||||
|
useAsTitle: "title",
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
read: () => true, // Publicly readable API
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "aiOptimizer",
|
||||||
|
type: "ui",
|
||||||
|
admin: {
|
||||||
|
position: "sidebar",
|
||||||
|
components: {
|
||||||
|
Field: "@/src/payload/components/OptimizeButton#OptimizeButton",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "title",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "slug",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
admin: {
|
||||||
|
position: "sidebar",
|
||||||
|
},
|
||||||
|
hooks: {
|
||||||
|
beforeValidate: [
|
||||||
|
({ value, data }) => {
|
||||||
|
if (value) return value;
|
||||||
|
if (data?.title) {
|
||||||
|
return data.title
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ /g, "-")
|
||||||
|
.replace(/[^\w-]+/g, "");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "description",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "date",
|
||||||
|
type: "date",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tags",
|
||||||
|
type: "array",
|
||||||
|
required: true,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "tag",
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "featuredImage",
|
||||||
|
type: "upload",
|
||||||
|
relationTo: "media",
|
||||||
|
admin: {
|
||||||
|
description: "The main hero image for the blog post.",
|
||||||
|
position: "sidebar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "content",
|
||||||
|
type: "richText",
|
||||||
|
editor: lexicalEditor({
|
||||||
|
features: ({ defaultFeatures }) => [
|
||||||
|
...defaultFeatures,
|
||||||
|
BlocksFeature({
|
||||||
|
blocks: allBlocks,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -1,4 +1,12 @@
|
|||||||
"use server";
|
"use server";
|
||||||
import { handleServerFunctions as payloadHandleServerFunctions } from "@payloadcms/next/layouts";
|
import { handleServerFunctions as payloadHandleServerFunctions } from "@payloadcms/next/layouts";
|
||||||
|
import config from "@payload-config";
|
||||||
|
import { importMap } from "./admin/importMap";
|
||||||
|
|
||||||
export const handleServerFunctions = payloadHandleServerFunctions;
|
export const handleServerFunctions = async (args: any) => {
|
||||||
|
return payloadHandleServerFunctions({
|
||||||
|
...args,
|
||||||
|
config,
|
||||||
|
importMap,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,99 @@
|
|||||||
|
import { OptimizeButton as OptimizeButton_a629b3460534b7aa208597fdc5e30aec } from "@/src/payload/components/OptimizeButton";
|
||||||
|
import { GenerateSlugButton as GenerateSlugButton_63aadb132a046b3f001fac7a715e5717 } from "@/src/payload/components/FieldGenerators/GenerateSlugButton";
|
||||||
|
import { default as default_76cec558bd86098fa1dab70b12eb818f } from "@/src/payload/components/TagSelector";
|
||||||
|
import { GenerateThumbnailButton as GenerateThumbnailButton_39d416c162062cbe7173a99e3239786e } from "@/src/payload/components/FieldGenerators/GenerateThumbnailButton";
|
||||||
|
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 { AiFieldButton as AiFieldButton_da42292f87769a8025025b774910be6d } from "@/src/payload/components/FieldGenerators/AiFieldButton";
|
||||||
|
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 { default as default_2ebf44fdf8ebc607cf0de30cff485248 } from "@/src/payload/components/ColorPicker";
|
||||||
|
import { default as default_a1c6da8fb7dd9846a8b07123ff256d09 } from "@/src/payload/components/IconSelector";
|
||||||
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from "@payloadcms/next/rsc";
|
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from "@payloadcms/next/rsc";
|
||||||
|
|
||||||
export const importMap = {
|
export const importMap = {
|
||||||
|
"@/src/payload/components/OptimizeButton#OptimizeButton":
|
||||||
|
OptimizeButton_a629b3460534b7aa208597fdc5e30aec,
|
||||||
|
"@/src/payload/components/FieldGenerators/GenerateSlugButton#GenerateSlugButton":
|
||||||
|
GenerateSlugButton_63aadb132a046b3f001fac7a715e5717,
|
||||||
|
"@/src/payload/components/TagSelector#default":
|
||||||
|
default_76cec558bd86098fa1dab70b12eb818f,
|
||||||
|
"@/src/payload/components/FieldGenerators/GenerateThumbnailButton#GenerateThumbnailButton":
|
||||||
|
GenerateThumbnailButton_39d416c162062cbe7173a99e3239786e,
|
||||||
|
"@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,
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton":
|
||||||
|
AiFieldButton_da42292f87769a8025025b774910be6d,
|
||||||
|
"@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,
|
||||||
|
"@/src/payload/components/ColorPicker#default":
|
||||||
|
default_2ebf44fdf8ebc607cf0de30cff485248,
|
||||||
|
"@/src/payload/components/IconSelector#default":
|
||||||
|
default_a1c6da8fb7dd9846a8b07123ff256d09,
|
||||||
"@payloadcms/next/rsc#CollectionCards":
|
"@payloadcms/next/rsc#CollectionCards":
|
||||||
CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1,
|
CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
CodeSnippet,
|
CodeSnippet,
|
||||||
AbstractCircuit,
|
AbstractCircuit,
|
||||||
} from "@/src/components/Effects";
|
} from "@/src/components/Effects";
|
||||||
import { getImgproxyUrl } from "@/src/utils/imgproxy";
|
|
||||||
import { Marker } from "@/src/components/Marker";
|
import { Marker } from "@/src/components/Marker";
|
||||||
|
|
||||||
export default function AboutPage() {
|
export default function AboutPage() {
|
||||||
@@ -51,12 +51,7 @@ export default function AboutPage() {
|
|||||||
<div className="relative w-32 h-32 md:w-40 md:h-40 rounded-full overflow-hidden border border-slate-200 shadow-xl bg-white p-1 group">
|
<div className="relative w-32 h-32 md:w-40 md:h-40 rounded-full overflow-hidden border border-slate-200 shadow-xl bg-white p-1 group">
|
||||||
<div className="w-full h-full rounded-full overflow-hidden relative aspect-square">
|
<div className="w-full h-full rounded-full overflow-hidden relative aspect-square">
|
||||||
<img
|
<img
|
||||||
src={getImgproxyUrl("/marc-mintel.png", {
|
src="/marc-mintel.png"
|
||||||
width: 400,
|
|
||||||
height: 400,
|
|
||||||
resizing_type: "fill",
|
|
||||||
gravity: "sm",
|
|
||||||
})}
|
|
||||||
alt="Marc Mintel"
|
alt="Marc Mintel"
|
||||||
className="object-cover grayscale transition-all duration-1000 ease-in-out scale-110 group-hover:scale-100 group-hover:grayscale-0 w-full h-full"
|
className="object-cover grayscale transition-all duration-1000 ease-in-out scale-110 group-hover:scale-100 group-hover:grayscale-0 w-full h-full"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
|
import { getPayloadHMR } from "@payloadcms/next/utilities";
|
||||||
|
import configPromise from "@payload-config";
|
||||||
import { getAllPosts } from "@/src/lib/posts";
|
import { getAllPosts } from "@/src/lib/posts";
|
||||||
import { BlogPostHeader } from "@/src/components/blog/BlogPostHeader";
|
import { BlogPostHeader } from "@/src/components/blog/BlogPostHeader";
|
||||||
import { Section } from "@/src/components/Section";
|
import { Section } from "@/src/components/Section";
|
||||||
@@ -9,6 +11,8 @@ import { BlogPostClient } from "@/src/components/BlogPostClient";
|
|||||||
import { TextSelectionShare } from "@/src/components/TextSelectionShare";
|
import { TextSelectionShare } from "@/src/components/TextSelectionShare";
|
||||||
import { BlogPostStickyBar } from "@/src/components/blog/BlogPostStickyBar";
|
import { BlogPostStickyBar } from "@/src/components/blog/BlogPostStickyBar";
|
||||||
import { MDXContent } from "@/src/components/MDXContent";
|
import { MDXContent } from "@/src/components/MDXContent";
|
||||||
|
import { PayloadRichText } from "@/src/components/PayloadRichText";
|
||||||
|
import { TableOfContents } from "@/src/components/TableOfContents";
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
const allPosts = await getAllPosts();
|
const allPosts = await getAllPosts();
|
||||||
@@ -54,6 +58,18 @@ export default async function BlogPostPage({
|
|||||||
const post = allPosts.find((p) => p.slug === slug);
|
const post = allPosts.find((p) => p.slug === slug);
|
||||||
|
|
||||||
if (!post) {
|
if (!post) {
|
||||||
|
const payload = await getPayloadHMR({ config: configPromise });
|
||||||
|
const redirectDoc = await payload.find({
|
||||||
|
collection: "redirects",
|
||||||
|
where: {
|
||||||
|
from: { equals: slug },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (redirectDoc.docs.length > 0) {
|
||||||
|
redirect(`/blog/${redirectDoc.docs[0].to}`);
|
||||||
|
}
|
||||||
|
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +118,12 @@ export default async function BlogPostPage({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="article-content max-w-none">
|
<div className="article-content max-w-none">
|
||||||
<MDXContent code={post.body.code} />
|
<TableOfContents />
|
||||||
|
{post.lexicalContent ? (
|
||||||
|
<PayloadRichText data={post.lexicalContent} />
|
||||||
|
) : (
|
||||||
|
<MDXContent code={post.body.code} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,30 +48,10 @@ export const technologies: Record<string, TechInfo> = {
|
|||||||
'Using TypeScript means your application is robust and reliable from day one. It dramatically reduces the risk of "runtime errors" that could crash your site, saving time and money on bug fixes down the line.',
|
'Using TypeScript means your application is robust and reliable from day one. It dramatically reduces the risk of "runtime errors" that could crash your site, saving time and money on bug fixes down the line.',
|
||||||
color: "bg-blue-600 text-white",
|
color: "bg-blue-600 text-white",
|
||||||
related: [
|
related: [
|
||||||
{ name: "Directus CMS", slug: "directus-cms" },
|
|
||||||
{ name: "Next.js 14", slug: "next-js-14" },
|
{ name: "Next.js 14", slug: "next-js-14" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"directus-cms": {
|
|
||||||
title: "Directus CMS",
|
|
||||||
subtitle: "The Open Data Platform",
|
|
||||||
description:
|
|
||||||
"Directus is a modern, headless Content Management System (CMS) that instantly turns any database into a beautiful, easy-to-use application for managing your content. Unlike traditional CMSs, it doesn't dictate how your website looks.",
|
|
||||||
icon: Database,
|
|
||||||
benefits: [
|
|
||||||
"Intuitive interface for non-technical editors",
|
|
||||||
"Complete freedom regarding front-end design",
|
|
||||||
"Real-time updates and live previews",
|
|
||||||
"Highly secure and role-based access control",
|
|
||||||
],
|
|
||||||
customerValue:
|
|
||||||
"Directus gives you full control over your content without needing a developer for every text change. It separates your data from the design, ensuring your website can evolve visually without rebuilding your entire content library.",
|
|
||||||
color: "bg-purple-600 text-white",
|
|
||||||
related: [
|
|
||||||
{ name: "Next.js 14", slug: "next-js-14" },
|
|
||||||
{ name: "Tailwind CSS", slug: "tailwind-css" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"tailwind-css": {
|
"tailwind-css": {
|
||||||
title: "Tailwind CSS",
|
title: "Tailwind CSS",
|
||||||
subtitle: "Utility-First CSS Framework",
|
subtitle: "Utility-First CSS Framework",
|
||||||
|
|||||||
12
apps/web/check-db.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export const run = async ({ payload }) => {
|
||||||
|
const docs = await payload.find({
|
||||||
|
collection: "context-files",
|
||||||
|
limit: 100,
|
||||||
|
});
|
||||||
|
console.log(`--- DB CHECK ---`);
|
||||||
|
console.log(`Found ${docs.totalDocs} context files.`);
|
||||||
|
docs.docs.forEach((doc) => {
|
||||||
|
console.log(`- ${doc.filename}`);
|
||||||
|
});
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
# Marc — digital problem solver
|
|
||||||
|
|
||||||
## Identity
|
|
||||||
- Name: Marc Mintel
|
|
||||||
- Mail: marc@mintel.me
|
|
||||||
- Location: Vulkaneifel, Germany
|
|
||||||
- Role: Independent digital problem solver
|
|
||||||
- Mode: Solo
|
|
||||||
- Focus: Understanding problems and building practical solutions
|
|
||||||
|
|
||||||
## What I do
|
|
||||||
I work on digital problems and build tools, scripts, and systems to solve them.
|
|
||||||
Sometimes that means code, sometimes automation, sometimes AI, sometimes something else.
|
|
||||||
The tool is secondary. The problem comes first.
|
|
||||||
|
|
||||||
## How I work
|
|
||||||
- I try things
|
|
||||||
- I break things
|
|
||||||
- I fix things
|
|
||||||
- I write down what I learned
|
|
||||||
|
|
||||||
## What this blog is
|
|
||||||
A public notebook of:
|
|
||||||
- things I figured out
|
|
||||||
- mistakes I made
|
|
||||||
- tools I tested
|
|
||||||
- small insights that might be useful later
|
|
||||||
|
|
||||||
Mostly short entries.
|
|
||||||
Mostly practical.
|
|
||||||
|
|
||||||
## Why no portfolio
|
|
||||||
Finished projects get outdated.
|
|
||||||
Understanding doesn’t.
|
|
||||||
|
|
||||||
This blog shows how I approach problems, not how pretty something looked last year.
|
|
||||||
|
|
||||||
## Topics
|
|
||||||
- Vibe coding with AI
|
|
||||||
- Debugging and problem solving
|
|
||||||
- Mac tools and workflows
|
|
||||||
- Automation
|
|
||||||
- Small scripts and systems
|
|
||||||
- Learning notes
|
|
||||||
- FOSS
|
|
||||||
|
|
||||||
## Audience
|
|
||||||
People who:
|
|
||||||
- build things
|
|
||||||
- work with computers
|
|
||||||
- solve problems
|
|
||||||
- and don’t need marketing talk
|
|
||||||
|
|
||||||
## Tone
|
|
||||||
- calm
|
|
||||||
- factual
|
|
||||||
- direct
|
|
||||||
- no hype
|
|
||||||
- no self-promotion
|
|
||||||
|
|
||||||
## Core idea
|
|
||||||
Write things down.
|
|
||||||
So I don’t forget.
|
|
||||||
And so others might find them useful.
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
Prinzipien
|
|
||||||
|
|
||||||
Ich arbeite nach klaren Grundsätzen, die sicherstellen, dass meine Kunden fair, transparent und langfristig profitieren.
|
|
||||||
|
|
||||||
⸻
|
|
||||||
|
|
||||||
1. Volle Preis-Transparenz
|
|
||||||
Alle Kosten sind offen und nachvollziehbar.
|
|
||||||
Es gibt keine versteckten Gebühren, keine Abos, keine Lock-ins.
|
|
||||||
Jeder Kunde sieht genau, wofür er bezahlt.
|
|
||||||
|
|
||||||
⸻
|
|
||||||
|
|
||||||
2. Quellcode & Projektzugang
|
|
||||||
Auf Wunsch erhalten Kunden jederzeit den vollständigen Source Code und eine nachvollziehbare Struktur.
|
|
||||||
Damit kann jeder andere Entwickler problemlos weiterarbeiten.
|
|
||||||
Niemand kann später behaupten, der Code sei „Messy“ oder unbrauchbar.
|
|
||||||
|
|
||||||
⸻
|
|
||||||
|
|
||||||
3. Best Practices & saubere Technik
|
|
||||||
Ich setze konsequent bewährte Standards und dokumentierte Abläufe ein.
|
|
||||||
Das sorgt dafür, dass Systeme wartbar, verständlich und erweiterbar bleiben – langfristig.
|
|
||||||
|
|
||||||
⸻
|
|
||||||
|
|
||||||
4. Verantwortung & Fairness
|
|
||||||
Ich übernehme die technische Verantwortung für die Website.
|
|
||||||
Ich garantiere keine Umsätze, Rankings oder rechtliche Ergebnisse – nur saubere Umsetzung und stabile Systeme.
|
|
||||||
Wenn etwas nicht sinnvoll ist, sage ich es ehrlich.
|
|
||||||
|
|
||||||
⸻
|
|
||||||
|
|
||||||
5. Langfristiger Wert
|
|
||||||
Eine Website ist ein Investment.
|
|
||||||
Ich baue sie so, dass Anpassungen, Erweiterungen und Übergaben an andere Entwickler problemlos möglich sind.
|
|
||||||
Das schützt Ihre Investition und vermeidet teure Neuaufbauten.
|
|
||||||
|
|
||||||
⸻
|
|
||||||
|
|
||||||
6. Zusammenarbeit ohne Tricks
|
|
||||||
Keine künstlichen Deadlines, kein unnötiger Overhead.
|
|
||||||
Kommunikation ist klar, Entscheidungen nachvollziehbar, Übergaben sauber dokumentiert.
|
|
||||||
54
apps/web/migrate-docs.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { getPayload } from "payload";
|
||||||
|
import configPromise from "./payload.config";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
try {
|
||||||
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
console.log("Payload initialized.");
|
||||||
|
|
||||||
|
const docsDir = path.resolve(process.cwd(), "docs");
|
||||||
|
|
||||||
|
if (!fs.existsSync(docsDir)) {
|
||||||
|
console.log(`Docs directory not found at ${docsDir}`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = fs.readdirSync(docsDir);
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.endsWith(".md")) {
|
||||||
|
const content = fs.readFileSync(path.join(docsDir, file), "utf8");
|
||||||
|
|
||||||
|
// Check if already exists
|
||||||
|
const existing = await payload.find({
|
||||||
|
collection: "context-files",
|
||||||
|
where: { filename: { equals: file } },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing.totalDocs === 0) {
|
||||||
|
await payload.create({
|
||||||
|
collection: "context-files",
|
||||||
|
data: {
|
||||||
|
filename: file,
|
||||||
|
content: content,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Migration successful! Added ${count} new context files to the database.`,
|
||||||
|
);
|
||||||
|
process.exit(0);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Migration failed:", e);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
34
apps/web/migrate-drafts.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { getPayload } from "payload";
|
||||||
|
import configPromise from "./payload.config";
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
const { docs } = await payload.find({
|
||||||
|
collection: "posts",
|
||||||
|
limit: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Found ${docs.length} posts. Checking status...`);
|
||||||
|
|
||||||
|
for (const doc of docs) {
|
||||||
|
if (doc._status !== "published") {
|
||||||
|
try {
|
||||||
|
await payload.update({
|
||||||
|
collection: "posts",
|
||||||
|
id: doc.id,
|
||||||
|
data: {
|
||||||
|
_status: "published",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`Updated "${doc.title}" to published.`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to update ${doc.title}:`, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Migration complete.");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
@@ -1,16 +1,26 @@
|
|||||||
import withMintelConfig from "@mintel/next-config";
|
import withMintelConfig from "@mintel/next-config";
|
||||||
import { withPayload } from '@payloadcms/next/withPayload';
|
import { withPayload } from '@payloadcms/next/withPayload';
|
||||||
|
|
||||||
import createMDX from '@next/mdx';
|
import createMDX from '@next/mdx';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const filename = fileURLToPath(import.meta.url);
|
||||||
|
const dirname = path.dirname(filename);
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
|
serverExternalPackages: ['@mintel/content-engine'],
|
||||||
reactStrictMode: true,
|
|
||||||
output: 'standalone',
|
|
||||||
images: {
|
images: {
|
||||||
loader: 'custom',
|
remotePatterns: [
|
||||||
loaderFile: './src/utils/imgproxy-loader.ts',
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: '*.your-objectstorage.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: 'fsn1.your-objectstorage.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
return [
|
return [
|
||||||
@@ -27,6 +37,13 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
webpack: (config) => {
|
||||||
|
config.resolve.alias = {
|
||||||
|
...config.resolve.alias,
|
||||||
|
'@mintel/content-engine': path.resolve(dirname, 'node_modules/@mintel/content-engine'),
|
||||||
|
};
|
||||||
|
return config;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const withMDX = createMDX({
|
const withMDX = createMDX({
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Technical problem solver's blog - practical insights and learning notes",
|
"description": "Technical problem solver's blog - practical insights and learning notes",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "rm -rf .next && next dev",
|
"dev": "pnpm run seed:context && next dev --turbo",
|
||||||
|
"dev:native": "pnpm run seed:context && DATABASE_URI=postgres://payload:payload@127.0.0.1:54321/payload PAYLOAD_SECRET=dev-secret next dev --webpack",
|
||||||
|
"seed:context": "tsx ./seed-context.ts",
|
||||||
"build": "next build --webpack",
|
"build": "next build --webpack",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint app src scripts video",
|
"lint": "eslint app src scripts video",
|
||||||
@@ -22,13 +24,16 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.750.0",
|
||||||
"@emotion/is-prop-valid": "^1.4.0",
|
"@emotion/is-prop-valid": "^1.4.0",
|
||||||
"@mdx-js/loader": "^3.1.1",
|
"@mdx-js/loader": "^3.1.1",
|
||||||
"@mdx-js/react": "^3.1.1",
|
"@mdx-js/react": "^3.1.1",
|
||||||
"@mintel/cloner": "^1.8.0",
|
"@mintel/cloner": "^1.8.0",
|
||||||
|
"@mintel/concept-engine": "link:../../../at-mintel/packages/concept-engine",
|
||||||
"@mintel/content-engine": "link:../../../at-mintel/packages/content-engine",
|
"@mintel/content-engine": "link:../../../at-mintel/packages/content-engine",
|
||||||
|
"@mintel/estimation-engine": "link:../../../at-mintel/packages/estimation-engine",
|
||||||
"@mintel/meme-generator": "link:../../../at-mintel/packages/meme-generator",
|
"@mintel/meme-generator": "link:../../../at-mintel/packages/meme-generator",
|
||||||
"@mintel/pdf": "^1.8.0",
|
"@mintel/pdf": "link:../../../at-mintel/packages/pdf-library",
|
||||||
"@mintel/thumbnail-generator": "link:../../../at-mintel/packages/thumbnail-generator",
|
"@mintel/thumbnail-generator": "link:../../../at-mintel/packages/thumbnail-generator",
|
||||||
"@next/mdx": "^16.1.6",
|
"@next/mdx": "^16.1.6",
|
||||||
"@next/third-parties": "^16.1.6",
|
"@next/third-parties": "^16.1.6",
|
||||||
@@ -40,6 +45,8 @@
|
|||||||
"@payloadcms/email-nodemailer": "^3.77.0",
|
"@payloadcms/email-nodemailer": "^3.77.0",
|
||||||
"@payloadcms/next": "^3.77.0",
|
"@payloadcms/next": "^3.77.0",
|
||||||
"@payloadcms/richtext-lexical": "^3.77.0",
|
"@payloadcms/richtext-lexical": "^3.77.0",
|
||||||
|
"@payloadcms/storage-s3": "^3.77.0",
|
||||||
|
"@payloadcms/ui": "^3.77.0",
|
||||||
"@react-pdf/renderer": "^4.3.2",
|
"@react-pdf/renderer": "^4.3.2",
|
||||||
"@remotion/bundler": "^4.0.414",
|
"@remotion/bundler": "^4.0.414",
|
||||||
"@remotion/cli": "^4.0.414",
|
"@remotion/cli": "^4.0.414",
|
||||||
@@ -57,6 +64,7 @@
|
|||||||
"canvas-confetti": "^1.9.4",
|
"canvas-confetti": "^1.9.4",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"crawlee": "^3.15.3",
|
"crawlee": "^3.15.3",
|
||||||
|
"dotenv": "^17.3.1",
|
||||||
"esbuild": "^0.27.3",
|
"esbuild": "^0.27.3",
|
||||||
"framer-motion": "^12.29.2",
|
"framer-motion": "^12.29.2",
|
||||||
"graphql": "^16.12.0",
|
"graphql": "^16.12.0",
|
||||||
@@ -99,6 +107,7 @@
|
|||||||
"@mintel/tsconfig": "^1.7.3",
|
"@mintel/tsconfig": "^1.7.3",
|
||||||
"@next/eslint-plugin-next": "^16.1.6",
|
"@next/eslint-plugin-next": "^16.1.6",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
|
"@types/mime-types": "^3.0.1",
|
||||||
"@types/node": "^25.0.6",
|
"@types/node": "^25.0.6",
|
||||||
"@types/nodemailer": "^7.0.10",
|
"@types/nodemailer": "^7.0.10",
|
||||||
"@types/prismjs": "^1.26.5",
|
"@types/prismjs": "^1.26.5",
|
||||||
@@ -109,9 +118,14 @@
|
|||||||
"eslint": "10.0.0",
|
"eslint": "10.0.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"mime-types": "^3.0.2",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"typescript-eslint": "^8.54.0"
|
"typescript-eslint": "^8.54.0"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git@git.infra.mintel.me:mmintel/mintel.me.git"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,53 +13,53 @@
|
|||||||
* via the `definition` "supportedTimezones".
|
* via the `definition` "supportedTimezones".
|
||||||
*/
|
*/
|
||||||
export type SupportedTimezones =
|
export type SupportedTimezones =
|
||||||
| "Pacific/Midway"
|
| 'Pacific/Midway'
|
||||||
| "Pacific/Niue"
|
| 'Pacific/Niue'
|
||||||
| "Pacific/Honolulu"
|
| 'Pacific/Honolulu'
|
||||||
| "Pacific/Rarotonga"
|
| 'Pacific/Rarotonga'
|
||||||
| "America/Anchorage"
|
| 'America/Anchorage'
|
||||||
| "Pacific/Gambier"
|
| 'Pacific/Gambier'
|
||||||
| "America/Los_Angeles"
|
| 'America/Los_Angeles'
|
||||||
| "America/Tijuana"
|
| 'America/Tijuana'
|
||||||
| "America/Denver"
|
| 'America/Denver'
|
||||||
| "America/Phoenix"
|
| 'America/Phoenix'
|
||||||
| "America/Chicago"
|
| 'America/Chicago'
|
||||||
| "America/Guatemala"
|
| 'America/Guatemala'
|
||||||
| "America/New_York"
|
| 'America/New_York'
|
||||||
| "America/Bogota"
|
| 'America/Bogota'
|
||||||
| "America/Caracas"
|
| 'America/Caracas'
|
||||||
| "America/Santiago"
|
| 'America/Santiago'
|
||||||
| "America/Buenos_Aires"
|
| 'America/Buenos_Aires'
|
||||||
| "America/Sao_Paulo"
|
| 'America/Sao_Paulo'
|
||||||
| "Atlantic/South_Georgia"
|
| 'Atlantic/South_Georgia'
|
||||||
| "Atlantic/Azores"
|
| 'Atlantic/Azores'
|
||||||
| "Atlantic/Cape_Verde"
|
| 'Atlantic/Cape_Verde'
|
||||||
| "Europe/London"
|
| 'Europe/London'
|
||||||
| "Europe/Berlin"
|
| 'Europe/Berlin'
|
||||||
| "Africa/Lagos"
|
| 'Africa/Lagos'
|
||||||
| "Europe/Athens"
|
| 'Europe/Athens'
|
||||||
| "Africa/Cairo"
|
| 'Africa/Cairo'
|
||||||
| "Europe/Moscow"
|
| 'Europe/Moscow'
|
||||||
| "Asia/Riyadh"
|
| 'Asia/Riyadh'
|
||||||
| "Asia/Dubai"
|
| 'Asia/Dubai'
|
||||||
| "Asia/Baku"
|
| 'Asia/Baku'
|
||||||
| "Asia/Karachi"
|
| 'Asia/Karachi'
|
||||||
| "Asia/Tashkent"
|
| 'Asia/Tashkent'
|
||||||
| "Asia/Calcutta"
|
| 'Asia/Calcutta'
|
||||||
| "Asia/Dhaka"
|
| 'Asia/Dhaka'
|
||||||
| "Asia/Almaty"
|
| 'Asia/Almaty'
|
||||||
| "Asia/Jakarta"
|
| 'Asia/Jakarta'
|
||||||
| "Asia/Bangkok"
|
| 'Asia/Bangkok'
|
||||||
| "Asia/Shanghai"
|
| 'Asia/Shanghai'
|
||||||
| "Asia/Singapore"
|
| 'Asia/Singapore'
|
||||||
| "Asia/Tokyo"
|
| 'Asia/Tokyo'
|
||||||
| "Asia/Seoul"
|
| 'Asia/Seoul'
|
||||||
| "Australia/Brisbane"
|
| 'Australia/Brisbane'
|
||||||
| "Australia/Sydney"
|
| 'Australia/Sydney'
|
||||||
| "Pacific/Guam"
|
| 'Pacific/Guam'
|
||||||
| "Pacific/Noumea"
|
| 'Pacific/Noumea'
|
||||||
| "Pacific/Auckland"
|
| 'Pacific/Auckland'
|
||||||
| "Pacific/Fiji";
|
| 'Pacific/Fiji';
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
auth: {
|
auth: {
|
||||||
@@ -70,33 +70,37 @@ export interface Config {
|
|||||||
users: User;
|
users: User;
|
||||||
media: Media;
|
media: Media;
|
||||||
posts: Post;
|
posts: Post;
|
||||||
"payload-kv": PayloadKv;
|
inquiries: Inquiry;
|
||||||
"payload-locked-documents": PayloadLockedDocument;
|
redirects: Redirect;
|
||||||
"payload-preferences": PayloadPreference;
|
'context-files': ContextFile;
|
||||||
"payload-migrations": PayloadMigration;
|
'payload-kv': PayloadKv;
|
||||||
|
'payload-locked-documents': PayloadLockedDocument;
|
||||||
|
'payload-preferences': PayloadPreference;
|
||||||
|
'payload-migrations': PayloadMigration;
|
||||||
};
|
};
|
||||||
collectionsJoins: {};
|
collectionsJoins: {};
|
||||||
collectionsSelect: {
|
collectionsSelect: {
|
||||||
users: UsersSelect<false> | UsersSelect<true>;
|
users: UsersSelect<false> | UsersSelect<true>;
|
||||||
media: MediaSelect<false> | MediaSelect<true>;
|
media: MediaSelect<false> | MediaSelect<true>;
|
||||||
posts: PostsSelect<false> | PostsSelect<true>;
|
posts: PostsSelect<false> | PostsSelect<true>;
|
||||||
"payload-kv": PayloadKvSelect<false> | PayloadKvSelect<true>;
|
inquiries: InquiriesSelect<false> | InquiriesSelect<true>;
|
||||||
"payload-locked-documents":
|
redirects: RedirectsSelect<false> | RedirectsSelect<true>;
|
||||||
| PayloadLockedDocumentsSelect<false>
|
'context-files': ContextFilesSelect<false> | ContextFilesSelect<true>;
|
||||||
| PayloadLockedDocumentsSelect<true>;
|
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
|
||||||
"payload-preferences":
|
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||||
| PayloadPreferencesSelect<false>
|
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||||
| PayloadPreferencesSelect<true>;
|
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||||
"payload-migrations":
|
|
||||||
| PayloadMigrationsSelect<false>
|
|
||||||
| PayloadMigrationsSelect<true>;
|
|
||||||
};
|
};
|
||||||
db: {
|
db: {
|
||||||
defaultIDType: number;
|
defaultIDType: number;
|
||||||
};
|
};
|
||||||
fallbackLocale: null;
|
fallbackLocale: null;
|
||||||
globals: {};
|
globals: {
|
||||||
globalsSelect: {};
|
'ai-settings': AiSetting;
|
||||||
|
};
|
||||||
|
globalsSelect: {
|
||||||
|
'ai-settings': AiSettingsSelect<false> | AiSettingsSelect<true>;
|
||||||
|
};
|
||||||
locale: null;
|
locale: null;
|
||||||
user: User;
|
user: User;
|
||||||
jobs: {
|
jobs: {
|
||||||
@@ -145,7 +149,7 @@ export interface User {
|
|||||||
}[]
|
}[]
|
||||||
| null;
|
| null;
|
||||||
password?: string | null;
|
password?: string | null;
|
||||||
collection: "users";
|
collection: 'users';
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
@@ -201,12 +205,99 @@ export interface Post {
|
|||||||
title: string;
|
title: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
/**
|
||||||
|
* Set a future date and save as 'Published' to schedule this post. It will not appear on the frontend until this date is reached.
|
||||||
|
*/
|
||||||
date: string;
|
date: string;
|
||||||
tags: {
|
tags: {
|
||||||
|
/**
|
||||||
|
* Kategorisiere diesen Post mit einem eindeutigen Tag
|
||||||
|
*/
|
||||||
tag?: string | null;
|
tag?: string | null;
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
}[];
|
}[];
|
||||||
thumbnail?: string | null;
|
/**
|
||||||
|
* The main hero image for the blog post.
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
} | null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
_status?: ('draft' | 'published') | null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Contact form leads and inquiries.
|
||||||
|
*
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "inquiries".
|
||||||
|
*/
|
||||||
|
export interface Inquiry {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
companyName?: string | null;
|
||||||
|
projectType?: string | null;
|
||||||
|
message?: string | null;
|
||||||
|
isFreeText?: boolean | null;
|
||||||
|
/**
|
||||||
|
* The JSON data from the configurator.
|
||||||
|
*/
|
||||||
|
config?:
|
||||||
|
| {
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
| unknown[]
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "redirects".
|
||||||
|
*/
|
||||||
|
export interface Redirect {
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* The old URL slug that should be redirected (e.g. 'old-post-name')
|
||||||
|
*/
|
||||||
|
from: string;
|
||||||
|
/**
|
||||||
|
* The new URL slug to redirect to (e.g. 'new-awesome-post')
|
||||||
|
*/
|
||||||
|
to: string;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "context-files".
|
||||||
|
*/
|
||||||
|
export interface ContextFile {
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* Exact filename (e.g. 'strategy.md'). The system uses this to identify the document during prompt generation.
|
||||||
|
*/
|
||||||
|
filename: string;
|
||||||
|
/**
|
||||||
|
* The raw markdown/text content of the document.
|
||||||
|
*/
|
||||||
content: string;
|
content: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -236,20 +327,32 @@ export interface PayloadLockedDocument {
|
|||||||
id: number;
|
id: number;
|
||||||
document?:
|
document?:
|
||||||
| ({
|
| ({
|
||||||
relationTo: "users";
|
relationTo: 'users';
|
||||||
value: number | User;
|
value: number | User;
|
||||||
} | null)
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: "media";
|
relationTo: 'media';
|
||||||
value: number | Media;
|
value: number | Media;
|
||||||
} | null)
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: "posts";
|
relationTo: 'posts';
|
||||||
value: number | Post;
|
value: number | Post;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'inquiries';
|
||||||
|
value: number | Inquiry;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'redirects';
|
||||||
|
value: number | Redirect;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'context-files';
|
||||||
|
value: number | ContextFile;
|
||||||
} | null);
|
} | null);
|
||||||
globalSlug?: string | null;
|
globalSlug?: string | null;
|
||||||
user: {
|
user: {
|
||||||
relationTo: "users";
|
relationTo: 'users';
|
||||||
value: number | User;
|
value: number | User;
|
||||||
};
|
};
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@@ -262,7 +365,7 @@ export interface PayloadLockedDocument {
|
|||||||
export interface PayloadPreference {
|
export interface PayloadPreference {
|
||||||
id: number;
|
id: number;
|
||||||
user: {
|
user: {
|
||||||
relationTo: "users";
|
relationTo: 'users';
|
||||||
value: number | User;
|
value: number | User;
|
||||||
};
|
};
|
||||||
key?: string | null;
|
key?: string | null;
|
||||||
@@ -378,7 +481,43 @@ export interface PostsSelect<T extends boolean = true> {
|
|||||||
tag?: T;
|
tag?: T;
|
||||||
id?: T;
|
id?: T;
|
||||||
};
|
};
|
||||||
thumbnail?: T;
|
featuredImage?: T;
|
||||||
|
content?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
_status?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "inquiries_select".
|
||||||
|
*/
|
||||||
|
export interface InquiriesSelect<T extends boolean = true> {
|
||||||
|
name?: T;
|
||||||
|
email?: T;
|
||||||
|
companyName?: T;
|
||||||
|
projectType?: T;
|
||||||
|
message?: T;
|
||||||
|
isFreeText?: T;
|
||||||
|
config?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "redirects_select".
|
||||||
|
*/
|
||||||
|
export interface RedirectsSelect<T extends boolean = true> {
|
||||||
|
from?: T;
|
||||||
|
to?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "context-files_select".
|
||||||
|
*/
|
||||||
|
export interface ContextFilesSelect<T extends boolean = true> {
|
||||||
|
filename?: T;
|
||||||
content?: T;
|
content?: T;
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
@@ -423,6 +562,39 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
|
|||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "ai-settings".
|
||||||
|
*/
|
||||||
|
export interface AiSetting {
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* List of trusted B2B/Tech sources (e.g. 'Vercel Blog', 'Fireship', 'Theo - t3.gg') the AI should prioritize when researching facts or videos. This overrides the hardcoded defaults.
|
||||||
|
*/
|
||||||
|
customSources?:
|
||||||
|
| {
|
||||||
|
sourceName: string;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
updatedAt?: string | null;
|
||||||
|
createdAt?: string | null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "ai-settings_select".
|
||||||
|
*/
|
||||||
|
export interface AiSettingsSelect<T extends boolean = true> {
|
||||||
|
customSources?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
sourceName?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
globalType?: T;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "auth".
|
* via the `definition` "auth".
|
||||||
@@ -431,6 +603,7 @@ export interface Auth {
|
|||||||
[k: string]: unknown;
|
[k: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "payload" {
|
|
||||||
|
declare module 'payload' {
|
||||||
export interface GeneratedTypes extends Config {}
|
export interface GeneratedTypes extends Config {}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import { buildConfig } from "payload";
|
import { buildConfig } from "payload";
|
||||||
|
// Triggering config re-analysis for blocks visibility - V4
|
||||||
import { postgresAdapter } from "@payloadcms/db-postgres";
|
import { postgresAdapter } from "@payloadcms/db-postgres";
|
||||||
import { lexicalEditor } from "@payloadcms/richtext-lexical";
|
import { lexicalEditor, BlocksFeature } from "@payloadcms/richtext-lexical";
|
||||||
|
import { payloadBlocks } from "./src/payload/blocks/allBlocks";
|
||||||
import { nodemailerAdapter } from "@payloadcms/email-nodemailer";
|
import { nodemailerAdapter } from "@payloadcms/email-nodemailer";
|
||||||
|
import { s3Storage } from "@payloadcms/storage-s3";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
@@ -9,6 +12,11 @@ import sharp from "sharp";
|
|||||||
import { Users } from "./src/payload/collections/Users";
|
import { Users } from "./src/payload/collections/Users";
|
||||||
import { Media } from "./src/payload/collections/Media";
|
import { Media } from "./src/payload/collections/Media";
|
||||||
import { Posts } from "./src/payload/collections/Posts";
|
import { Posts } from "./src/payload/collections/Posts";
|
||||||
|
import { Inquiries } from "./src/payload/collections/Inquiries";
|
||||||
|
import { Redirects } from "./src/payload/collections/Redirects";
|
||||||
|
import { ContextFiles } from "./src/payload/collections/ContextFiles";
|
||||||
|
|
||||||
|
import { AiSettings } from "./src/payload/globals/AiSettings";
|
||||||
|
|
||||||
const filename = fileURLToPath(import.meta.url);
|
const filename = fileURLToPath(import.meta.url);
|
||||||
const dirname = path.dirname(filename);
|
const dirname = path.dirname(filename);
|
||||||
@@ -20,24 +28,32 @@ export default buildConfig({
|
|||||||
baseDir: path.resolve(dirname),
|
baseDir: path.resolve(dirname),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
collections: [Users, Media, Posts],
|
collections: [Users, Media, Posts, Inquiries, Redirects, ContextFiles],
|
||||||
|
globals: [AiSettings],
|
||||||
...(process.env.MAIL_HOST
|
...(process.env.MAIL_HOST
|
||||||
? {
|
? {
|
||||||
email: nodemailerAdapter({
|
email: nodemailerAdapter({
|
||||||
defaultFromAddress: process.env.MAIL_FROM || "info@mintel.me",
|
defaultFromAddress: process.env.MAIL_FROM || "info@mintel.me",
|
||||||
defaultFromName: "Mintel.me",
|
defaultFromName: "Mintel.me",
|
||||||
transportOptions: {
|
transportOptions: {
|
||||||
host: process.env.MAIL_HOST,
|
host: process.env.MAIL_HOST,
|
||||||
port: parseInt(process.env.MAIL_PORT || "587"),
|
port: parseInt(process.env.MAIL_PORT || "587"),
|
||||||
auth: {
|
auth: {
|
||||||
user: process.env.MAIL_USERNAME,
|
user: process.env.MAIL_USERNAME,
|
||||||
pass: process.env.MAIL_PASSWORD,
|
pass: process.env.MAIL_PASSWORD,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}),
|
},
|
||||||
}
|
}),
|
||||||
|
}
|
||||||
: {}),
|
: {}),
|
||||||
editor: lexicalEditor(),
|
editor: lexicalEditor({
|
||||||
|
features: ({ defaultFeatures }) => [
|
||||||
|
...defaultFeatures,
|
||||||
|
BlocksFeature({
|
||||||
|
blocks: payloadBlocks,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
secret: process.env.PAYLOAD_SECRET || "fallback-secret-for-dev",
|
secret: process.env.PAYLOAD_SECRET || "fallback-secret-for-dev",
|
||||||
typescript: {
|
typescript: {
|
||||||
outputFile: path.resolve(dirname, "payload-types.ts"),
|
outputFile: path.resolve(dirname, "payload-types.ts"),
|
||||||
@@ -49,5 +65,27 @@ export default buildConfig({
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
sharp,
|
sharp,
|
||||||
plugins: [],
|
plugins: [
|
||||||
|
...(process.env.S3_ENDPOINT
|
||||||
|
? [
|
||||||
|
s3Storage({
|
||||||
|
collections: {
|
||||||
|
media: {
|
||||||
|
prefix: `${process.env.S3_PREFIX || "mintel-me"}/media`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bucket: process.env.S3_BUCKET || "",
|
||||||
|
config: {
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: process.env.S3_ACCESS_KEY || "",
|
||||||
|
secretAccessKey: process.env.S3_SECRET_KEY || "",
|
||||||
|
},
|
||||||
|
region: process.env.S3_REGION || "fsn1",
|
||||||
|
endpoint: process.env.S3_ENDPOINT,
|
||||||
|
forcePathStyle: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.0 MiB |
|
Before Width: | Height: | Size: 5.2 MiB |
|
Before Width: | Height: | Size: 4.8 MiB |
|
Before Width: | Height: | Size: 4.5 MiB |
|
Before Width: | Height: | Size: 4.5 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 4.9 MiB |
|
Before Width: | Height: | Size: 4.8 MiB |
|
Before Width: | Height: | Size: 5.6 MiB |
|
Before Width: | Height: | Size: 4.3 MiB |
|
Before Width: | Height: | Size: 4.5 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 4.6 MiB |
|
Before Width: | Height: | Size: 3.8 MiB |
|
Before Width: | Height: | Size: 4.3 MiB |
|
Before Width: | Height: | Size: 4.3 MiB |
|
Before Width: | Height: | Size: 5.0 MiB |
|
Before Width: | Height: | Size: 4.2 MiB |
|
Before Width: | Height: | Size: 4.3 MiB |
|
Before Width: | Height: | Size: 5.0 MiB |
|
Before Width: | Height: | Size: 4.8 MiB |
87
apps/web/remove-toc.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { getPayload } from "payload";
|
||||||
|
import configPromise from "./payload.config";
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
const { docs } = await payload.find({
|
||||||
|
collection: "posts",
|
||||||
|
limit: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Found ${docs.length} posts. Checking for <TableOfContents />...`,
|
||||||
|
);
|
||||||
|
|
||||||
|
let updatedCount = 0;
|
||||||
|
|
||||||
|
const removeTOC = (node: any): boolean => {
|
||||||
|
let modified = false;
|
||||||
|
if (node.children && Array.isArray(node.children)) {
|
||||||
|
// Filter out raw text nodes or paragraph nodes that are exactly TableOfContents
|
||||||
|
const originalLength = node.children.length;
|
||||||
|
node.children = node.children.filter((child: any) => {
|
||||||
|
if (
|
||||||
|
child.type === "text" &&
|
||||||
|
child.text &&
|
||||||
|
child.text.includes("<TableOfContents />")
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
child.type === "paragraph" &&
|
||||||
|
child.children &&
|
||||||
|
child.children.length === 1 &&
|
||||||
|
child.children[0].text === "<TableOfContents />"
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (node.children.length !== originalLength) {
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also clean up any substrings in remaining text nodes
|
||||||
|
for (const child of node.children) {
|
||||||
|
if (
|
||||||
|
child.type === "text" &&
|
||||||
|
child.text &&
|
||||||
|
child.text.includes("<TableOfContents />")
|
||||||
|
) {
|
||||||
|
child.text = child.text.replace("<TableOfContents />", "").trim();
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
if (removeTOC(child)) {
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return modified;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const doc of docs) {
|
||||||
|
if (doc.content?.root) {
|
||||||
|
const isModified = removeTOC(doc.content.root);
|
||||||
|
if (isModified) {
|
||||||
|
try {
|
||||||
|
await payload.update({
|
||||||
|
collection: "posts",
|
||||||
|
id: doc.id,
|
||||||
|
data: {
|
||||||
|
content: doc.content,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`Cleaned up TOC in "${doc.title}".`);
|
||||||
|
updatedCount++;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to update ${doc.title}:`, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Cleanup complete. Modified ${updatedCount} posts.`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
@@ -2,6 +2,7 @@ import { getPayload } from "payload";
|
|||||||
import configPromise from "../payload.config";
|
import configPromise from "../payload.config";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { parseMarkdownToLexical } from "../src/payload/utils/lexicalParser";
|
||||||
|
|
||||||
function parseMatter(content: string) {
|
function parseMatter(content: string) {
|
||||||
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
||||||
@@ -39,29 +40,112 @@ async function run() {
|
|||||||
const slug = file.replace(/\.mdx$/, "");
|
const slug = file.replace(/\.mdx$/, "");
|
||||||
console.log(`Migrating ${slug}...`);
|
console.log(`Migrating ${slug}...`);
|
||||||
|
|
||||||
const existing = await payload.find({
|
try {
|
||||||
collection: "posts",
|
const existing = await payload.find({
|
||||||
where: { slug: { equals: slug } },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existing.docs.length === 0) {
|
|
||||||
await payload.create({
|
|
||||||
collection: "posts",
|
collection: "posts",
|
||||||
data: {
|
where: { slug: { equals: slug } },
|
||||||
title: data.title || slug,
|
|
||||||
slug,
|
|
||||||
description: data.description || "",
|
|
||||||
date: data.date
|
|
||||||
? new Date(data.date).toISOString()
|
|
||||||
: new Date().toISOString(),
|
|
||||||
tags: (data.tags || []).map((t: string) => ({ tag: t })),
|
|
||||||
thumbnail: data.thumbnail || "",
|
|
||||||
content: body,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
console.log(`✔ Inserted ${slug}`);
|
|
||||||
} else {
|
const lexicalBlocks = parseMarkdownToLexical(body);
|
||||||
console.log(`⚠ Skipped ${slug} (already exists)`);
|
const lexicalAST = {
|
||||||
|
root: {
|
||||||
|
type: "root",
|
||||||
|
format: "",
|
||||||
|
indent: 0,
|
||||||
|
version: 1,
|
||||||
|
children: lexicalBlocks,
|
||||||
|
direction: "ltr",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle thumbnail mapping
|
||||||
|
let featuredImageId = null;
|
||||||
|
if (data.thumbnail) {
|
||||||
|
try {
|
||||||
|
// Remove leading slash and find local file
|
||||||
|
const localPath = path.join(
|
||||||
|
process.cwd(),
|
||||||
|
"public",
|
||||||
|
data.thumbnail.replace(/^\//, ""),
|
||||||
|
);
|
||||||
|
const fileName = path.basename(localPath);
|
||||||
|
|
||||||
|
if (fs.existsSync(localPath)) {
|
||||||
|
// Check if media already exists in Payload
|
||||||
|
const existingMedia = await payload.find({
|
||||||
|
collection: "media",
|
||||||
|
where: { filename: { equals: fileName } },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingMedia.docs.length > 0) {
|
||||||
|
featuredImageId = existingMedia.docs[0].id;
|
||||||
|
} else {
|
||||||
|
// Upload new media item
|
||||||
|
const fileData = fs.readFileSync(localPath);
|
||||||
|
const { size } = fs.statSync(localPath);
|
||||||
|
|
||||||
|
const newMedia = await payload.create({
|
||||||
|
collection: "media",
|
||||||
|
data: {
|
||||||
|
alt: data.title || fileName,
|
||||||
|
},
|
||||||
|
file: {
|
||||||
|
data: fileData,
|
||||||
|
name: fileName,
|
||||||
|
mimetype: fileName.endsWith(".png")
|
||||||
|
? "image/png"
|
||||||
|
: fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")
|
||||||
|
? "image/jpeg"
|
||||||
|
: "image/webp",
|
||||||
|
size,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
featuredImageId = newMedia.id;
|
||||||
|
console.log(` ↑ Uploaded thumbnail: ${fileName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(
|
||||||
|
` ⚠ Warning: Could not process thumbnail ${data.thumbnail}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing.docs.length === 0) {
|
||||||
|
await payload.create({
|
||||||
|
collection: "posts",
|
||||||
|
data: {
|
||||||
|
title: data.title || slug,
|
||||||
|
slug,
|
||||||
|
description: data.description || "",
|
||||||
|
date: data.date
|
||||||
|
? new Date(data.date).toISOString()
|
||||||
|
: new Date().toISOString(),
|
||||||
|
tags: (data.tags || []).map((t: string) => ({ tag: t })),
|
||||||
|
content: lexicalAST as any,
|
||||||
|
featuredImage: featuredImageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`✔ Inserted ${slug}`);
|
||||||
|
} else {
|
||||||
|
await payload.update({
|
||||||
|
collection: "posts",
|
||||||
|
id: existing.docs[0].id,
|
||||||
|
data: {
|
||||||
|
content: lexicalAST as any,
|
||||||
|
featuredImage: featuredImageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`✔ Updated AST and thumbnail for ${slug}`);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`✘ FAILED ${slug}: ${err.message}`);
|
||||||
|
if (err.data?.errors) {
|
||||||
|
console.error(
|
||||||
|
` Validation errors:`,
|
||||||
|
JSON.stringify(err.data.errors, null, 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
122
apps/web/scripts/seed-blog-posts.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { getPayload } from "payload";
|
||||||
|
import configPromise from "../payload.config";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { parseMarkdownToLexical } from "../src/payload/utils/lexicalParser";
|
||||||
|
|
||||||
|
function extractFrontmatter(content: string) {
|
||||||
|
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
||||||
|
if (!fmMatch) return {};
|
||||||
|
const fm = fmMatch[1];
|
||||||
|
const titleMatch = fm.match(/title:\s*"?([^"\n]+)"?/);
|
||||||
|
const descMatch = fm.match(/description:\s*"?([^"\n]+)"?/);
|
||||||
|
const tagsMatch = fm.match(/tags:\s*\[(.*?)\]/);
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: titleMatch ? titleMatch[1] : "Untitled Draft",
|
||||||
|
description: descMatch ? descMatch[1] : "No description",
|
||||||
|
tags: tagsMatch ? tagsMatch[1].split(",").map(s => s.trim().replace(/"/g, "")) : []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
try {
|
||||||
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
console.log("Payload initialized.");
|
||||||
|
|
||||||
|
const draftsDir = path.resolve(process.cwd(), "content/drafts");
|
||||||
|
const publicBlogDir = path.resolve(process.cwd(), "public/blog");
|
||||||
|
|
||||||
|
if (!fs.existsSync(draftsDir)) {
|
||||||
|
console.log(`Drafts directory not found at ${draftsDir}`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = fs.readdirSync(draftsDir).filter(f => f.endsWith(".md"));
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
console.log(`Processing ${file}...`);
|
||||||
|
const filePath = path.join(draftsDir, file);
|
||||||
|
const content = fs.readFileSync(filePath, "utf8");
|
||||||
|
|
||||||
|
const fm = extractFrontmatter(content);
|
||||||
|
const lexicalNodes = parseMarkdownToLexical(content);
|
||||||
|
const lexicalContent = {
|
||||||
|
root: {
|
||||||
|
type: "root",
|
||||||
|
format: "" as const,
|
||||||
|
indent: 0,
|
||||||
|
version: 1,
|
||||||
|
direction: "ltr" as const,
|
||||||
|
children: lexicalNodes
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Upload thumbnail if exists
|
||||||
|
let featuredImageId = null;
|
||||||
|
const thumbPath = path.join(publicBlogDir, `${file}.png`);
|
||||||
|
if (fs.existsSync(thumbPath)) {
|
||||||
|
console.log(`Uploading thumbnail ${file}.png...`);
|
||||||
|
const fileData = fs.readFileSync(thumbPath);
|
||||||
|
const stat = fs.statSync(thumbPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newMedia = await payload.create({
|
||||||
|
collection: "media",
|
||||||
|
data: {
|
||||||
|
alt: `Thumbnail for ${fm.title}`,
|
||||||
|
},
|
||||||
|
file: {
|
||||||
|
data: fileData,
|
||||||
|
name: `optimized-${file}.png`,
|
||||||
|
mimetype: "image/png",
|
||||||
|
size: stat.size,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
featuredImageId = newMedia.id;
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Failed to upload thumbnail", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagsArray = fm.tags.map(tag => ({ tag }));
|
||||||
|
|
||||||
|
const slug = fm.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "").substring(0, 60);
|
||||||
|
|
||||||
|
// Check if already exists
|
||||||
|
const existing = await payload.find({
|
||||||
|
collection: "posts",
|
||||||
|
where: { slug: { equals: slug } },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing.totalDocs === 0) {
|
||||||
|
await payload.create({
|
||||||
|
collection: "posts",
|
||||||
|
data: {
|
||||||
|
title: fm.title,
|
||||||
|
slug: slug,
|
||||||
|
description: fm.description,
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
tags: tagsArray,
|
||||||
|
featuredImage: featuredImageId,
|
||||||
|
content: lexicalContent,
|
||||||
|
_status: "published"
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`Created CMS entry for ${file}.`);
|
||||||
|
count++;
|
||||||
|
} else {
|
||||||
|
console.log(`Post with slug ${slug} already exists. Skipping.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Migration successful! Added ${count} new optimized posts to the database.`);
|
||||||
|
process.exit(0);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Migration failed:", e);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
56
apps/web/seed-context.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { getPayload } from "payload";
|
||||||
|
import configPromise from "./payload.config";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
try {
|
||||||
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
|
||||||
|
const existing = await payload.find({
|
||||||
|
collection: "context-files",
|
||||||
|
limit: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing.totalDocs > 0) {
|
||||||
|
console.log("Context collection already populated. Skipping seed.");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const seedDir = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"src/payload/collections/ContextFiles/seed",
|
||||||
|
);
|
||||||
|
if (!fs.existsSync(seedDir)) {
|
||||||
|
console.log(`Seed directory not found at ${seedDir}`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = fs.readdirSync(seedDir).filter((f) => f.endsWith(".md"));
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const content = fs.readFileSync(path.join(seedDir, file), "utf8");
|
||||||
|
await payload.create({
|
||||||
|
collection: "context-files",
|
||||||
|
data: {
|
||||||
|
filename: file,
|
||||||
|
content: content,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Seeded ${count} context files.`);
|
||||||
|
process.exit(0);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Seeding failed:", e);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import * as Sentry from "@sentry/nextjs";
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
const dsn = process.env.SENTRY_DSN;
|
const dsn = process.env.SENTRY_DSN;
|
||||||
|
const isProd = process.env.NODE_ENV === "production";
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn,
|
dsn,
|
||||||
enabled: Boolean(dsn),
|
enabled: isProd && Boolean(dsn),
|
||||||
tracesSampleRate: 1,
|
tracesSampleRate: 1,
|
||||||
debug: false,
|
debug: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import * as Sentry from "@sentry/nextjs";
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
const dsn = process.env.SENTRY_DSN;
|
const dsn = process.env.SENTRY_DSN;
|
||||||
|
const isProd = process.env.NODE_ENV === "production";
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn,
|
dsn,
|
||||||
enabled: Boolean(dsn),
|
enabled: isProd && Boolean(dsn),
|
||||||
|
|
||||||
tracesSampleRate: 1,
|
tracesSampleRate: 1,
|
||||||
debug: false,
|
debug: false,
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
getInquiryEmailHtml,
|
getInquiryEmailHtml,
|
||||||
getConfirmationEmailHtml,
|
getConfirmationEmailHtml,
|
||||||
} from "../components/ContactForm/EmailTemplates";
|
} from "../components/ContactForm/EmailTemplates";
|
||||||
|
import { getPayload } from "payload";
|
||||||
|
import configPromise from "@payload-config";
|
||||||
|
|
||||||
export async function sendContactInquiry(data: {
|
export async function sendContactInquiry(data: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -16,7 +18,22 @@ export async function sendContactInquiry(data: {
|
|||||||
config?: any;
|
config?: any;
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
// 1. Send Inquiry to Marc
|
// 1. Save to Payload CMS (Replaces Directus)
|
||||||
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
await payload.create({
|
||||||
|
collection: "inquiries",
|
||||||
|
data: {
|
||||||
|
name: data.name,
|
||||||
|
email: data.email,
|
||||||
|
companyName: data.companyName,
|
||||||
|
projectType: data.projectType,
|
||||||
|
message: data.message,
|
||||||
|
isFreeText: data.isFreeText,
|
||||||
|
config: data.config || null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Send Inquiry to Marc
|
||||||
const inquiryResult = await sendEmail({
|
const inquiryResult = await sendEmail({
|
||||||
subject: `[PROJEKT] ${data.isFreeText ? "DIREKTANFRAGE" : "KONFIGURATION"}: ${data.companyName || data.name}`,
|
subject: `[PROJEKT] ${data.isFreeText ? "DIREKTANFRAGE" : "KONFIGURATION"}: ${data.companyName || data.name}`,
|
||||||
html: getInquiryEmailHtml(data),
|
html: getInquiryEmailHtml(data),
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { cn } from "../utils/cn";
|
import { cn } from "../utils/cn";
|
||||||
import { ShieldCheck, ArrowLeft, ArrowRight, RefreshCw } from "lucide-react";
|
import { ShieldCheck, ArrowLeft, ArrowRight, RefreshCw } from "lucide-react";
|
||||||
@@ -31,8 +30,6 @@ interface IframeSectionProps {
|
|||||||
desktopHeight?: string;
|
desktopHeight?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reusable Browser UI components to maintain consistency
|
* Reusable Browser UI components to maintain consistency
|
||||||
*/
|
*/
|
||||||
@@ -102,11 +99,6 @@ const BrowserChromeComponent: React.FC<{ url: string; minimal?: boolean }> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
BrowserChromeComponent.propTypes = {
|
|
||||||
url: PropTypes.string.isRequired,
|
|
||||||
minimal: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
const BrowserChrome = React.memo(BrowserChromeComponent);
|
const BrowserChrome = React.memo(BrowserChromeComponent);
|
||||||
|
|
||||||
BrowserChrome.displayName = "BrowserChrome";
|
BrowserChrome.displayName = "BrowserChrome";
|
||||||
@@ -212,7 +204,7 @@ export const IframeSection: React.FC<IframeSectionProps> = ({
|
|||||||
const isScrollable = doc.scrollHeight > doc.clientHeight + 10;
|
const isScrollable = doc.scrollHeight > doc.clientHeight + 10;
|
||||||
setScrollState({ atTop, atBottom, isScrollable });
|
setScrollState({ atTop, atBottom, isScrollable });
|
||||||
}
|
}
|
||||||
} catch (_e) { }
|
} catch (_e) {}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Ambilight effect (sampled from iframe if same-origin)
|
// Ambilight effect (sampled from iframe if same-origin)
|
||||||
@@ -257,7 +249,7 @@ export const IframeSection: React.FC<IframeSectionProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
updateScrollState();
|
updateScrollState();
|
||||||
} catch (_e) { }
|
} catch (_e) {}
|
||||||
}, [dynamicGlow, offsetY, updateScrollState]);
|
}, [dynamicGlow, offsetY, updateScrollState]);
|
||||||
|
|
||||||
// Height parse helper
|
// Height parse helper
|
||||||
@@ -376,9 +368,9 @@ export const IframeSection: React.FC<IframeSectionProps> = ({
|
|||||||
"w-full relative flex flex-col z-10",
|
"w-full relative flex flex-col z-10",
|
||||||
minimal ? "bg-transparent" : "bg-slate-50",
|
minimal ? "bg-transparent" : "bg-slate-50",
|
||||||
!minimal &&
|
!minimal &&
|
||||||
"rounded-[2.5rem] border border-slate-200/50 shadow-[0_80px_160px_-40px_rgba(0,0,0,0.18),0_0_1px_rgba(0,0,0,0.1)]",
|
"rounded-[2.5rem] border border-slate-200/50 shadow-[0_80px_160px_-40px_rgba(0,0,0,0.18),0_0_1px_rgba(0,0,0,0.1)]",
|
||||||
perspective &&
|
perspective &&
|
||||||
"hover:scale-[1.03] hover:-translate-y-3 transition-[transform,shadow,scale] duration-1000 ease-[cubic-bezier(0.23,1,0.32,1)]",
|
"hover:scale-[1.03] hover:-translate-y-3 transition-[transform,shadow,scale] duration-1000 ease-[cubic-bezier(0.23,1,0.32,1)]",
|
||||||
"overflow-hidden",
|
"overflow-hidden",
|
||||||
)}
|
)}
|
||||||
style={chassisStyle}
|
style={chassisStyle}
|
||||||
|
|||||||
@@ -1,256 +1,355 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import { ComponentShareButton } from './ComponentShareButton';
|
import { ComponentShareButton } from "./ComponentShareButton";
|
||||||
import { Reveal } from './Reveal';
|
import { Reveal } from "./Reveal";
|
||||||
|
|
||||||
interface MemeCardProps {
|
interface MemeCardProps {
|
||||||
/** Meme template type: drake, ds (daily struggle), gru, fine, clown, expanding, distracted, rollsafe */
|
/** Meme template type: drake, ds (daily struggle), gru, fine, clown, expanding, distracted, rollsafe */
|
||||||
template: string;
|
template: string;
|
||||||
/** Pipe-delimited captions */
|
/** Pipe-delimited captions */
|
||||||
captions: string;
|
captions: string;
|
||||||
/** Optional local image path. If provided, overrides the text-based template. */
|
/** Optional local image path. If provided, overrides the text-based template. */
|
||||||
image?: string;
|
image?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Premium text-based meme cards with dedicated layouts per template.
|
* Premium text-based meme cards with dedicated layouts per template.
|
||||||
* Uses emoji + typography instead of images for on-brand aesthetics.
|
* Uses emoji + typography instead of images for on-brand aesthetics.
|
||||||
*/
|
*/
|
||||||
export const MemeCard: React.FC<MemeCardProps> = ({ template, captions, image, className = '' }) => {
|
export const MemeCard: React.FC<MemeCardProps> = ({
|
||||||
const captionList = (captions || '').split('|').map(s => s.trim()).filter(Boolean);
|
template,
|
||||||
const shareId = `meme-${Math.random().toString(36).substring(7).toUpperCase()}`;
|
captions,
|
||||||
|
image,
|
||||||
if (image) {
|
className = "",
|
||||||
return (
|
}) => {
|
||||||
<Reveal direction="up" delay={0.1}>
|
// Also replace literal `\n` (slash-n) strings from AI output with actual newlines
|
||||||
<div className={`not-prose max-w-xl mx-auto my-12 group relative transition-all duration-500 ease-out z-10 ${className}`}>
|
const processedCaptions = (captions || "").replace(/\\n/g, "\n");
|
||||||
<div className="absolute -inset-1 bg-gradient-to-r from-slate-200 to-slate-100 rounded-[2rem] blur opacity-10 group-hover:opacity-30 transition duration-1000 -z-10" />
|
const captionList = processedCaptions
|
||||||
|
.split("|")
|
||||||
<div id={shareId} className="glass bg-white/80 backdrop-blur-xl border border-slate-100 rounded-2xl overflow-hidden shadow-sm shadow-slate-200 group-hover:shadow-md transition-all duration-500 relative">
|
.map((s) => s.trim())
|
||||||
<div data-share-wrapper="true" className="absolute top-4 right-4 md:opacity-0 group-hover:opacity-100 transition-opacity duration-500 z-50">
|
.filter(Boolean);
|
||||||
<ComponentShareButton targetId={shareId} title={`Meme: ${template}`} />
|
const shareId = `meme-${Math.random().toString(36).substring(7).toUpperCase()}`;
|
||||||
</div>
|
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
||||||
<img
|
|
||||||
src={image}
|
|
||||||
alt={`Meme: ${template} - ${captionList.join(' ')}`}
|
|
||||||
className="w-full h-auto object-cover block"
|
|
||||||
loading="eager"
|
|
||||||
decoding="sync"
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Reveal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (image) {
|
||||||
return (
|
return (
|
||||||
<Reveal direction="up" delay={0.1}>
|
<Reveal direction="up" delay={0.1}>
|
||||||
<div className={`not-prose max-w-xl mx-auto my-12 group relative transition-all duration-500 ease-out z-10 ${className}`}>
|
<div
|
||||||
<div className="absolute -inset-1 bg-gradient-to-r from-slate-200 to-slate-100 rounded-[2rem] blur opacity-10 group-hover:opacity-30 transition duration-1000 -z-10" />
|
className={`not-prose max-w-xl mx-auto my-12 group relative transition-all duration-500 ease-out z-10 ${className}`}
|
||||||
|
>
|
||||||
|
<div className="absolute -inset-1 bg-gradient-to-r from-slate-200 to-slate-100 rounded-[2rem] blur opacity-10 group-hover:opacity-30 transition duration-1000 -z-10" />
|
||||||
|
|
||||||
<div id={shareId} className="glass bg-white/80 backdrop-blur-xl border border-slate-100 rounded-2xl overflow-hidden shadow-sm shadow-slate-200 group-hover:shadow-md transition-all duration-500 relative">
|
<div
|
||||||
<div data-share-wrapper="true" className="absolute top-4 right-4 md:opacity-0 group-hover:opacity-100 transition-opacity duration-500 z-50">
|
id={shareId}
|
||||||
<ComponentShareButton targetId={shareId} title={`Meme: ${template}`} />
|
className="glass bg-white/80 backdrop-blur-xl border border-slate-100 rounded-2xl overflow-hidden shadow-sm shadow-slate-200 group-hover:shadow-md transition-all duration-500 relative"
|
||||||
</div>
|
>
|
||||||
|
<div
|
||||||
{template === 'drake' && <DrakeMeme captions={captionList} />}
|
data-share-wrapper="true"
|
||||||
{template === 'ds' && <DailyStruggleMeme captions={captionList} />}
|
className="absolute top-4 right-4 md:opacity-0 group-hover:opacity-100 transition-opacity duration-500 z-50"
|
||||||
{template === 'gru' && <GruMeme captions={captionList} />}
|
>
|
||||||
{template === 'fine' && <FineMeme captions={captionList} />}
|
<ComponentShareButton
|
||||||
{template === 'clown' && <ClownMeme captions={captionList} />}
|
targetId={shareId}
|
||||||
{template === 'expanding' && <ExpandingBrainMeme captions={captionList} />}
|
title={`Meme: ${template}`}
|
||||||
{template === 'distracted' && <DistractedMeme captions={captionList} />}
|
/>
|
||||||
<GenericMeme captions={captionList} template={template} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Reveal>
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
|
src={image}
|
||||||
|
alt={`Meme: ${template} - ${captionList.join(" ")}`}
|
||||||
|
className="w-full h-auto object-cover block"
|
||||||
|
loading="eager"
|
||||||
|
decoding="sync"
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Reveal direction="up" delay={0.1}>
|
||||||
|
<div
|
||||||
|
className={`not-prose max-w-xl mx-auto my-12 group relative transition-all duration-500 ease-out z-10 ${className}`}
|
||||||
|
>
|
||||||
|
<div className="absolute -inset-1 bg-gradient-to-r from-slate-200 to-slate-100 rounded-[2rem] blur opacity-10 group-hover:opacity-30 transition duration-1000 -z-10" />
|
||||||
|
|
||||||
|
<div
|
||||||
|
id={shareId}
|
||||||
|
className="glass bg-white/80 backdrop-blur-xl border border-slate-100 rounded-2xl overflow-hidden shadow-sm shadow-slate-200 group-hover:shadow-md transition-all duration-500 relative"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-share-wrapper="true"
|
||||||
|
className="absolute top-4 right-4 md:opacity-0 group-hover:opacity-100 transition-opacity duration-500 z-50"
|
||||||
|
>
|
||||||
|
<ComponentShareButton
|
||||||
|
targetId={shareId}
|
||||||
|
title={`Meme: ${template}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{template === "drake" && <DrakeMeme captions={captionList} />}
|
||||||
|
{template === "ds" && <DailyStruggleMeme captions={captionList} />}
|
||||||
|
{template === "gru" && <GruMeme captions={captionList} />}
|
||||||
|
{template === "fine" && <FineMeme captions={captionList} />}
|
||||||
|
{template === "clown" && <ClownMeme captions={captionList} />}
|
||||||
|
{template === "expanding" && (
|
||||||
|
<ExpandingBrainMeme captions={captionList} />
|
||||||
|
)}
|
||||||
|
{template === "distracted" && (
|
||||||
|
<DistractedMeme captions={captionList} />
|
||||||
|
)}
|
||||||
|
<GenericMeme captions={captionList} template={template} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function DrakeMeme({ captions }: { captions: string[] }) {
|
function DrakeMeme({ captions }: { captions: string[] }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex items-stretch border-b border-slate-100">
|
<div className="flex items-stretch border-b border-slate-100">
|
||||||
<div className="w-20 md:w-24 bg-red-400/10 flex items-center justify-center flex-shrink-0 border-r border-slate-100">
|
<div className="w-20 md:w-24 bg-red-400/10 flex items-center justify-center flex-shrink-0 border-r border-slate-100">
|
||||||
<span className="text-3xl md:text-4xl select-none grayscale-0 group-hover:scale-110 transition-transform duration-500">🙅</span>
|
<span className="text-3xl md:text-4xl select-none grayscale-0 group-hover:scale-110 transition-transform duration-500">
|
||||||
</div>
|
🙅
|
||||||
<div className="flex-1 p-5 md:p-6 flex items-center bg-white/40">
|
</span>
|
||||||
<p className="text-lg md:text-xl font-medium text-slate-500 leading-snug">{captions[0]}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-stretch">
|
|
||||||
<div className="w-20 md:w-24 bg-emerald-400/10 flex items-center justify-center flex-shrink-0 border-r border-slate-100">
|
|
||||||
<span className="text-3xl md:text-4xl select-none group-hover:scale-110 transition-transform duration-500">😎</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 p-5 md:p-6 flex items-center bg-white">
|
|
||||||
<p className="text-lg md:text-xl font-bold text-slate-900 leading-snug">{captions[1]}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<div className="flex-1 p-5 md:p-6 flex items-center bg-white/40">
|
||||||
|
<p className="text-lg md:text-xl font-medium text-slate-500 leading-snug whitespace-pre-wrap">
|
||||||
|
{captions[0]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-stretch">
|
||||||
|
<div className="w-20 md:w-24 bg-emerald-400/10 flex items-center justify-center flex-shrink-0 border-r border-slate-100">
|
||||||
|
<span className="text-3xl md:text-4xl select-none group-hover:scale-110 transition-transform duration-500">
|
||||||
|
😎
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 p-5 md:p-6 flex items-center bg-white">
|
||||||
|
<p className="text-lg md:text-xl font-bold text-slate-900 leading-snug whitespace-pre-wrap">
|
||||||
|
{captions[1]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DailyStruggleMeme({ captions }: { captions: string[] }) {
|
function DailyStruggleMeme({ captions }: { captions: string[] }) {
|
||||||
return (
|
return (
|
||||||
<div className="p-8 md:p-10 text-center">
|
<div className="p-8 md:p-10 text-center">
|
||||||
<div className="text-4xl md:text-5xl mb-6 select-none animate-bounce-subtle">😰</div>
|
<div className="text-4xl md:text-5xl mb-6 select-none animate-bounce-subtle">
|
||||||
<p className="text-[10px] font-black text-slate-300 uppercase tracking-[0.3em] mb-8">Daily Struggle</p>
|
😰
|
||||||
<div className="grid grid-cols-2 gap-4">
|
</div>
|
||||||
<div className="p-5 rounded-2xl bg-white border border-slate-100 shadow-sm hover:border-slate-200 transition-colors">
|
<p className="text-[10px] font-black text-slate-300 uppercase tracking-[0.3em] mb-8">
|
||||||
<div className="w-5 h-5 rounded-full bg-red-500 mx-auto mb-3 shadow-[0_0_10px_rgba(239,68,68,0.4)]" />
|
Daily Struggle
|
||||||
<p className="text-sm md:text-base font-bold text-slate-700 leading-snug">{captions[0]}</p>
|
</p>
|
||||||
</div>
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="p-5 rounded-2xl bg-white border border-slate-100 shadow-sm hover:border-slate-200 transition-colors">
|
<div className="p-5 rounded-2xl bg-white border border-slate-100 shadow-sm hover:border-slate-200 transition-colors">
|
||||||
<div className="w-5 h-5 rounded-full bg-red-500 mx-auto mb-3 shadow-[0_0_10px_rgba(239,68,68,0.4)]" />
|
<div className="w-5 h-5 rounded-full bg-red-500 mx-auto mb-3 shadow-[0_0_10px_rgba(239,68,68,0.4)]" />
|
||||||
<p className="text-sm md:text-base font-bold text-slate-700 leading-snug">{captions[1]}</p>
|
<p className="text-sm md:text-base font-bold text-slate-700 leading-snug">
|
||||||
</div>
|
{captions[0]}
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
<div className="p-5 rounded-2xl bg-white border border-slate-100 shadow-sm hover:border-slate-200 transition-colors">
|
||||||
|
<div className="w-5 h-5 rounded-full bg-red-500 mx-auto mb-3 shadow-[0_0_10px_rgba(239,68,68,0.4)]" />
|
||||||
|
<p className="text-sm md:text-base font-bold text-slate-700 leading-snug">
|
||||||
|
{captions[1]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function GruMeme({ captions }: { captions: string[] }) {
|
function GruMeme({ captions }: { captions: string[] }) {
|
||||||
const steps = captions.slice(0, 4);
|
const steps = captions.slice(0, 4);
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-2 grid-rows-2">
|
<div className="grid grid-cols-2 grid-rows-2">
|
||||||
{(steps || []).map((caption, i) => {
|
{(steps || []).map((caption, i) => {
|
||||||
const isLast = i >= 2;
|
const isLast = i >= 2;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className={`p-6 md:p-8 ${i % 2 === 0 ? 'border-r' : ''} ${i < 2 ? 'border-b' : ''} border-slate-100 flex flex-col items-center justify-center text-center gap-3 transition-colors hover:bg-slate-50/30`}
|
className={`p-6 md:p-8 ${i % 2 === 0 ? "border-r" : ""} ${i < 2 ? "border-b" : ""} border-slate-100 flex flex-col items-center justify-center text-center gap-3 transition-colors hover:bg-slate-50/30`}
|
||||||
>
|
>
|
||||||
<span className="text-2xl md:text-3xl select-none transition-transform group-hover:scale-110">
|
<span className="text-2xl md:text-3xl select-none transition-transform group-hover:scale-110">
|
||||||
{isLast ? '😱' : '😏'}
|
{isLast ? "😱" : "😏"}
|
||||||
</span>
|
</span>
|
||||||
<p className={`text-base md:text-lg leading-tight ${isLast ? 'font-black text-red-500' : 'font-bold text-slate-700'}`}>
|
<p
|
||||||
{caption}
|
className={`text-base md:text-lg leading-tight ${isLast ? "font-black text-red-500" : "font-bold text-slate-700"}`}
|
||||||
</p>
|
>
|
||||||
</div>
|
{caption}
|
||||||
);
|
</p>
|
||||||
})}
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FineMeme({ captions }: { captions: string[] }) {
|
function FineMeme({ captions }: { captions: string[] }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="bg-orange-50/50 border-b border-slate-100 p-6 md:p-8">
|
<div className="bg-orange-50/50 border-b border-slate-100 p-6 md:p-8">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<span className="text-3xl md:text-4xl select-none">🔥</span>
|
<span className="text-3xl md:text-4xl select-none">🔥</span>
|
||||||
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest m-0">This is Fine</p>
|
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest m-0">
|
||||||
</div>
|
This is Fine
|
||||||
<p className="text-lg md:text-xl font-bold text-slate-700 leading-snug">{captions[0]}</p>
|
</p>
|
||||||
</div>
|
|
||||||
<div className="p-6 md:p-8 bg-white">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<span className="text-3xl select-none group-hover:rotate-12 transition-transform">☕</span>
|
|
||||||
<p className="text-lg md:text-2xl font-black text-slate-900 leading-tight italic tracking-tight">
|
|
||||||
“{captions[1] || 'Alles im grünen Bereich.'}”
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<p className="text-lg md:text-xl font-bold text-slate-700 leading-snug">
|
||||||
|
{captions[0]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 md:p-8 bg-white">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<span className="text-3xl select-none group-hover:rotate-12 transition-transform">
|
||||||
|
☕
|
||||||
|
</span>
|
||||||
|
<p className="text-lg md:text-2xl font-black text-slate-900 leading-tight italic tracking-tight">
|
||||||
|
“{captions[1] || "Alles im grünen Bereich."}”
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ClownMeme({ captions }: { captions: string[] }) {
|
function ClownMeme({ captions }: { captions: string[] }) {
|
||||||
const steps = captions.slice(0, 4);
|
const steps = captions.slice(0, 4);
|
||||||
const emojis = ['😐', '🤡', '💀', '🎪'];
|
const emojis = ["😐", "🤡", "💀", "🎪"];
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="p-4 md:p-5 border-b border-slate-100 bg-slate-50/50">
|
<div className="p-4 md:p-5 border-b border-slate-100 bg-slate-50/50">
|
||||||
<p className="text-[10px] font-black text-slate-300 uppercase tracking-[0.3em] m-0 text-center">Clown Progression</p>
|
<p className="text-[10px] font-black text-slate-300 uppercase tracking-[0.3em] m-0 text-center">
|
||||||
</div>
|
Clown Progression
|
||||||
{steps.map((caption, i) => (
|
</p>
|
||||||
<div
|
</div>
|
||||||
key={i}
|
{steps.map((caption, i) => (
|
||||||
className={`flex items-center gap-5 p-5 md:p-6 ${i < steps.length - 1 ? 'border-b border-slate-100' : ''} hover:bg-slate-50 transition-colors`}
|
<div
|
||||||
>
|
key={i}
|
||||||
<span className="text-2xl md:text-3xl select-none flex-shrink-0 grayscale opacity-60 group-hover:grayscale-0 group-hover:opacity-100 transition-all duration-500">{emojis[i] || '🤡'}</span>
|
className={`flex items-center gap-5 p-5 md:p-6 ${i < steps.length - 1 ? "border-b border-slate-100" : ""} hover:bg-slate-50 transition-colors`}
|
||||||
<p className={`text-base md:text-lg leading-snug ${i === steps.length - 1 ? 'font-black text-red-500' : 'font-bold text-slate-700'}`}>
|
>
|
||||||
{caption}
|
<span className="text-2xl md:text-3xl select-none flex-shrink-0 grayscale opacity-60 group-hover:grayscale-0 group-hover:opacity-100 transition-all duration-500">
|
||||||
</p>
|
{emojis[i] || "🤡"}
|
||||||
</div>
|
</span>
|
||||||
))}
|
<p
|
||||||
|
className={`text-base md:text-lg leading-snug ${i === steps.length - 1 ? "font-black text-red-500" : "font-bold text-slate-700"}`}
|
||||||
|
>
|
||||||
|
{caption}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ExpandingBrainMeme({ captions }: { captions: string[] }) {
|
function ExpandingBrainMeme({ captions }: { captions: string[] }) {
|
||||||
const steps = captions.slice(0, 4);
|
const steps = captions.slice(0, 4);
|
||||||
const emojis = ['🧠', '🧠✨', '🧠💡', '🧠🚀'];
|
const emojis = ["🧠", "🧠✨", "🧠💡", "🧠🚀"];
|
||||||
const shadows = [
|
const shadows = [
|
||||||
'',
|
"",
|
||||||
'shadow-[0_0_15px_rgba(59,130,246,0.1)]',
|
"shadow-[0_0_15px_rgba(59,130,246,0.1)]",
|
||||||
'shadow-[0_0_20px_rgba(99,102,241,0.2)]',
|
"shadow-[0_0_20px_rgba(99,102,241,0.2)]",
|
||||||
'shadow-[0_0_25px_rgba(168,85,247,0.3)]',
|
"shadow-[0_0_25px_rgba(168,85,247,0.3)]",
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="p-4 md:p-5 border-b border-slate-100 bg-slate-50/50">
|
<div className="p-4 md:p-5 border-b border-slate-100 bg-slate-50/50">
|
||||||
<p className="text-[10px] font-black text-slate-300 uppercase tracking-[0.3em] m-0 text-center">Expanding Intelligence</p>
|
<p className="text-[10px] font-black text-slate-300 uppercase tracking-[0.3em] m-0 text-center">
|
||||||
</div>
|
Expanding Intelligence
|
||||||
{steps.map((caption, i) => (
|
</p>
|
||||||
<div
|
</div>
|
||||||
key={i}
|
{steps.map((caption, i) => (
|
||||||
className={`flex items-center gap-5 p-5 md:p-6 ${i < steps.length - 1 ? 'border-b border-slate-100' : ''} hover:bg-white transition-all duration-500 ${shadows[i]}`}
|
<div
|
||||||
>
|
key={i}
|
||||||
<span className="text-2xl md:text-3xl select-none flex-shrink-0 group-hover:scale-125 transition-transform duration-700">{emojis[i]}</span>
|
className={`flex items-center gap-5 p-5 md:p-6 ${i < steps.length - 1 ? "border-b border-slate-100" : ""} hover:bg-white transition-all duration-500 ${shadows[i]}`}
|
||||||
<p className={`text-base md:text-lg leading-tight ${i === steps.length - 1 ? 'font-black text-indigo-600' : 'font-bold text-slate-700'}`}>
|
>
|
||||||
{caption}
|
<span className="text-2xl md:text-3xl select-none flex-shrink-0 group-hover:scale-125 transition-transform duration-700">
|
||||||
</p>
|
{emojis[i]}
|
||||||
</div>
|
</span>
|
||||||
))}
|
<p
|
||||||
|
className={`text-base md:text-lg leading-tight ${i === steps.length - 1 ? "font-black text-indigo-600" : "font-bold text-slate-700"}`}
|
||||||
|
>
|
||||||
|
{caption}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DistractedMeme({ captions }: { captions: string[] }) {
|
function DistractedMeme({ captions }: { captions: string[] }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="p-4 md:p-5 border-b border-slate-100 bg-slate-50/50">
|
<div className="p-4 md:p-5 border-b border-slate-100 bg-slate-50/50">
|
||||||
<p className="text-[10px] font-black text-slate-300 uppercase tracking-[0.3em] m-0 text-center">The Distraction</p>
|
<p className="text-[10px] font-black text-slate-300 uppercase tracking-[0.3em] m-0 text-center">
|
||||||
</div>
|
The Distraction
|
||||||
<div className="grid grid-cols-3 divide-x divide-slate-100">
|
</p>
|
||||||
<div className="p-6 md:p-8 flex flex-col items-center text-center gap-3 hover:bg-slate-50/50 transition-colors">
|
</div>
|
||||||
<span className="text-3xl md:text-4xl select-none">👤</span>
|
<div className="grid grid-cols-3 divide-x divide-slate-100">
|
||||||
<p className="text-[9px] font-black text-slate-400 uppercase tracking-[0.2em] m-0">Subject</p>
|
<div className="p-6 md:p-8 flex flex-col items-center text-center gap-3 hover:bg-slate-50/50 transition-colors">
|
||||||
<p className="text-sm md:text-base font-bold text-slate-500 leading-tight">{captions[0]}</p>
|
<span className="text-3xl md:text-4xl select-none">👤</span>
|
||||||
</div>
|
<p className="text-[9px] font-black text-slate-400 uppercase tracking-[0.2em] m-0">
|
||||||
<div className="p-6 md:p-8 flex flex-col items-center text-center gap-3 bg-emerald-50/30 hover:bg-emerald-50/60 transition-colors">
|
Subject
|
||||||
<span className="text-3xl md:text-4xl select-none animate-pulse">✨</span>
|
</p>
|
||||||
<p className="text-[9px] font-black text-emerald-500 uppercase tracking-[0.2em] m-0">Temptation</p>
|
<p className="text-sm md:text-base font-bold text-slate-500 leading-tight">
|
||||||
<p className="text-sm md:text-base font-black text-slate-900 leading-tight">{captions[1]}</p>
|
{captions[0]}
|
||||||
</div>
|
</p>
|
||||||
<div className="p-6 md:p-8 flex flex-col items-center text-center gap-3 bg-red-50/30 hover:bg-red-50/60 transition-colors">
|
|
||||||
<span className="text-3xl md:text-4xl select-none">😤</span>
|
|
||||||
<p className="text-[9px] font-black text-red-500 uppercase tracking-[0.2em] m-0">Reality</p>
|
|
||||||
<p className="text-sm md:text-base font-bold text-red-600 leading-tight">{captions[2]}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<div className="p-6 md:p-8 flex flex-col items-center text-center gap-3 bg-emerald-50/30 hover:bg-emerald-50/60 transition-colors">
|
||||||
|
<span className="text-3xl md:text-4xl select-none animate-pulse">
|
||||||
|
✨
|
||||||
|
</span>
|
||||||
|
<p className="text-[9px] font-black text-emerald-500 uppercase tracking-[0.2em] m-0">
|
||||||
|
Temptation
|
||||||
|
</p>
|
||||||
|
<p className="text-sm md:text-base font-black text-slate-900 leading-tight">
|
||||||
|
{captions[1]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 md:p-8 flex flex-col items-center text-center gap-3 bg-red-50/30 hover:bg-red-50/60 transition-colors">
|
||||||
|
<span className="text-3xl md:text-4xl select-none">😤</span>
|
||||||
|
<p className="text-[9px] font-black text-red-500 uppercase tracking-[0.2em] m-0">
|
||||||
|
Reality
|
||||||
|
</p>
|
||||||
|
<p className="text-sm md:text-base font-bold text-red-600 leading-tight">
|
||||||
|
{captions[2]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function GenericMeme({ captions, template }: { captions: string[]; template: string }) {
|
function GenericMeme({
|
||||||
return (
|
captions,
|
||||||
<div className="p-8 md:p-12 text-center bg-gradient-to-br from-white to-slate-50/50">
|
template,
|
||||||
<p className="text-[10px] font-black text-slate-400 uppercase tracking-[0.3em] mb-8">{template}</p>
|
}: {
|
||||||
<div className="space-y-4">
|
captions: string[];
|
||||||
{(captions || []).map((caption, i) => (
|
template: string;
|
||||||
<div key={i} className="p-4 md:p-5 bg-white border border-slate-100 rounded-2xl shadow-sm group-hover:border-slate-200 transition-all duration-300">
|
}) {
|
||||||
<p className="text-base md:text-lg font-bold text-slate-700 m-0">
|
return (
|
||||||
{caption}
|
<div className="p-8 md:p-12 text-center bg-gradient-to-br from-white to-slate-50/50">
|
||||||
</p>
|
<p className="text-[10px] font-black text-slate-400 uppercase tracking-[0.3em] mb-8">
|
||||||
</div>
|
{template}
|
||||||
))}
|
</p>
|
||||||
</div>
|
<div className="space-y-4">
|
||||||
</div>
|
{(captions || []).map((caption, i) => (
|
||||||
);
|
<div
|
||||||
|
key={i}
|
||||||
|
className="p-4 md:p-5 bg-white border border-slate-100 rounded-2xl shadow-sm group-hover:border-slate-200 transition-all duration-300"
|
||||||
|
>
|
||||||
|
<p className="text-base md:text-lg font-bold text-slate-700 m-0">
|
||||||
|
{caption}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
315
apps/web/src/components/PayloadRichText.tsx
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
import { RichText } from "@payloadcms/richtext-lexical/react";
|
||||||
|
import type { JSXConverters } from "@payloadcms/richtext-lexical/react";
|
||||||
|
import { MemeCard } from "@/src/components/MemeCard";
|
||||||
|
import { Mermaid } from "@/src/components/Mermaid";
|
||||||
|
import { LeadMagnet } from "@/src/components/LeadMagnet";
|
||||||
|
import { ComparisonRow } from "@/src/components/Landing/ComparisonRow";
|
||||||
|
import { mdxComponents } from "../content-engine/components";
|
||||||
|
|
||||||
|
const jsxConverters: JSXConverters = {
|
||||||
|
blocks: {
|
||||||
|
memeCard: ({ node }: any) => (
|
||||||
|
<div className="my-8">
|
||||||
|
<MemeCard
|
||||||
|
template={node.fields.template}
|
||||||
|
captions={node.fields.captions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
mermaid: ({ node }: any) => (
|
||||||
|
<div className="my-8">
|
||||||
|
<Mermaid
|
||||||
|
id={node.fields.id}
|
||||||
|
title={node.fields.title}
|
||||||
|
showShare={node.fields.showShare}
|
||||||
|
>
|
||||||
|
{node.fields.chartDefinition}
|
||||||
|
</Mermaid>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
leadMagnet: ({ node }: any) => (
|
||||||
|
<div className="my-12">
|
||||||
|
<LeadMagnet
|
||||||
|
title={node.fields.title}
|
||||||
|
description={node.fields.description}
|
||||||
|
buttonText={node.fields.buttonText}
|
||||||
|
href={node.fields.href}
|
||||||
|
variant={node.fields.variant}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
comparisonRow: ({ node }: any) => (
|
||||||
|
<ComparisonRow
|
||||||
|
description={node.fields.description}
|
||||||
|
negativeLabel={node.fields.negativeLabel}
|
||||||
|
negativeText={node.fields.negativeText}
|
||||||
|
positiveLabel={node.fields.positiveLabel}
|
||||||
|
positiveText={node.fields.positiveText}
|
||||||
|
reverse={node.fields.reverse}
|
||||||
|
showShare={true}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
// --- MDX Registry Injections ---
|
||||||
|
leadParagraph: ({ node }: any) => (
|
||||||
|
<mdxComponents.LeadParagraph>
|
||||||
|
{node.fields.text}
|
||||||
|
</mdxComponents.LeadParagraph>
|
||||||
|
),
|
||||||
|
articleBlockquote: ({ node }: any) => (
|
||||||
|
<mdxComponents.ArticleBlockquote>
|
||||||
|
{node.fields.quote}
|
||||||
|
{node.fields.author && ` - ${node.fields.author}`}
|
||||||
|
</mdxComponents.ArticleBlockquote>
|
||||||
|
),
|
||||||
|
mintelH2: ({ node }: any) => (
|
||||||
|
<mdxComponents.H2>{node.fields.text}</mdxComponents.H2>
|
||||||
|
),
|
||||||
|
mintelH3: ({ node }: any) => (
|
||||||
|
<mdxComponents.H3>{node.fields.text}</mdxComponents.H3>
|
||||||
|
),
|
||||||
|
mintelHeading: ({ node }: any) => {
|
||||||
|
const displayLevel = node.fields.displayLevel || "h2";
|
||||||
|
if (displayLevel === "h3")
|
||||||
|
return <mdxComponents.H3>{node.fields.text}</mdxComponents.H3>;
|
||||||
|
return <mdxComponents.H2>{node.fields.text}</mdxComponents.H2>;
|
||||||
|
},
|
||||||
|
statsDisplay: ({ node }: any) => (
|
||||||
|
<mdxComponents.StatsDisplay
|
||||||
|
label={node.fields.label}
|
||||||
|
value={node.fields.value}
|
||||||
|
subtext={node.fields.subtext}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
diagramState: ({ node }: any) => (
|
||||||
|
<mdxComponents.DiagramState
|
||||||
|
states={[]}
|
||||||
|
transitions={[]}
|
||||||
|
caption={node.fields.definition}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
diagramTimeline: ({ node }: any) => (
|
||||||
|
<mdxComponents.DiagramTimeline
|
||||||
|
events={[]}
|
||||||
|
title={node.fields.definition}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
diagramGantt: ({ node }: any) => (
|
||||||
|
<mdxComponents.DiagramGantt tasks={[]} title={node.fields.definition} />
|
||||||
|
),
|
||||||
|
diagramPie: ({ node }: any) => (
|
||||||
|
<mdxComponents.DiagramPie data={[]} title={node.fields.definition} />
|
||||||
|
),
|
||||||
|
diagramSequence: ({ node }: any) => (
|
||||||
|
<mdxComponents.DiagramSequence
|
||||||
|
participants={[]}
|
||||||
|
steps={[]}
|
||||||
|
title={node.fields.definition}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
diagramFlow: ({ node }: any) => (
|
||||||
|
<mdxComponents.DiagramFlow
|
||||||
|
nodes={[]}
|
||||||
|
edges={[]}
|
||||||
|
title={node.fields.definition}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
|
||||||
|
waterfallChart: ({ node }: any) => (
|
||||||
|
<mdxComponents.WaterfallChart
|
||||||
|
title={node.fields.title}
|
||||||
|
events={node.fields.metrics || []}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
premiumComparisonChart: ({ node }: any) => (
|
||||||
|
<mdxComponents.PremiumComparisonChart
|
||||||
|
title={node.fields.title}
|
||||||
|
items={node.fields.datasets || []}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
iconList: ({ node }: any) => (
|
||||||
|
<mdxComponents.IconList>
|
||||||
|
{node.fields.items?.map((item: any, i: number) => (
|
||||||
|
// @ts-ignore
|
||||||
|
<mdxComponents.IconListItem
|
||||||
|
key={i}
|
||||||
|
icon={item.icon || "check"}
|
||||||
|
title={item.title}
|
||||||
|
>
|
||||||
|
{item.description}
|
||||||
|
</mdxComponents.IconListItem>
|
||||||
|
))}
|
||||||
|
</mdxComponents.IconList>
|
||||||
|
),
|
||||||
|
statsGrid: ({ node }: any) => {
|
||||||
|
const rawStats = node.fields.stats || [];
|
||||||
|
let statsStr = "";
|
||||||
|
if (Array.isArray(rawStats)) {
|
||||||
|
statsStr = rawStats
|
||||||
|
.map((s: any) => `${s.value || ""}|${s.label || ""}`)
|
||||||
|
.join("~");
|
||||||
|
} else if (typeof rawStats === "string") {
|
||||||
|
statsStr = rawStats;
|
||||||
|
}
|
||||||
|
return <mdxComponents.StatsGrid stats={statsStr} />;
|
||||||
|
},
|
||||||
|
metricBar: ({ node }: any) => (
|
||||||
|
<mdxComponents.MetricBar
|
||||||
|
label={node.fields.label}
|
||||||
|
value={node.fields.value}
|
||||||
|
color={node.fields.color as any}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
carousel: ({ node }: any) => (
|
||||||
|
<mdxComponents.Carousel
|
||||||
|
items={
|
||||||
|
node.fields.slides?.map((s: any) => ({
|
||||||
|
title: s.caption || "Image",
|
||||||
|
content: "",
|
||||||
|
icon: undefined,
|
||||||
|
})) || []
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
imageText: ({ node }: any) => (
|
||||||
|
<mdxComponents.ImageText
|
||||||
|
image={node.fields.image?.url || ""}
|
||||||
|
title="ImageText Component"
|
||||||
|
>
|
||||||
|
{node.fields.text}
|
||||||
|
</mdxComponents.ImageText>
|
||||||
|
),
|
||||||
|
revenueLossCalculator: ({ node }: any) => (
|
||||||
|
<mdxComponents.RevenueLossCalculator />
|
||||||
|
),
|
||||||
|
performanceChart: ({ node }: any) => <mdxComponents.PerformanceChart />,
|
||||||
|
performanceROICalculator: ({ node }: any) => (
|
||||||
|
<div className="not-prose my-12">
|
||||||
|
<mdxComponents.PerformanceROICalculator />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
loadTimeSimulator: ({ node }: any) => (
|
||||||
|
<div className="not-prose my-12">
|
||||||
|
<mdxComponents.LoadTimeSimulator />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
architectureBuilder: ({ node }: any) => (
|
||||||
|
<div className="not-prose my-12">
|
||||||
|
<mdxComponents.ArchitectureBuilder />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
digitalAssetVisualizer: ({ node }: any) => (
|
||||||
|
<div className="not-prose my-12">
|
||||||
|
<mdxComponents.DigitalAssetVisualizer />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
|
||||||
|
twitterEmbed: ({ node }: any) => (
|
||||||
|
<mdxComponents.TwitterEmbed
|
||||||
|
tweetId={node.fields.url?.split("/").pop() || ""}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
youTubeEmbed: ({ node }: any) => (
|
||||||
|
<mdxComponents.YouTubeEmbed
|
||||||
|
videoId={node.fields.videoId}
|
||||||
|
title={node.fields.title}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
linkedInEmbed: ({ node }: any) => (
|
||||||
|
<mdxComponents.LinkedInEmbed url={node.fields.url} />
|
||||||
|
),
|
||||||
|
externalLink: ({ node }: any) => (
|
||||||
|
<mdxComponents.ExternalLink href={node.fields.href}>
|
||||||
|
{node.fields.label}
|
||||||
|
</mdxComponents.ExternalLink>
|
||||||
|
),
|
||||||
|
trackedLink: ({ node }: any) => (
|
||||||
|
<mdxComponents.TrackedLink
|
||||||
|
href={node.fields.href}
|
||||||
|
eventName={node.fields.eventName}
|
||||||
|
>
|
||||||
|
{node.fields.label}
|
||||||
|
</mdxComponents.TrackedLink>
|
||||||
|
),
|
||||||
|
articleMeme: ({ node }: any) => (
|
||||||
|
<mdxComponents.ArticleMeme
|
||||||
|
template="drake"
|
||||||
|
captions={node.fields.caption || "Top|Bottom"}
|
||||||
|
image={node.fields.image?.url || undefined}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
marker: ({ node }: any) => (
|
||||||
|
<mdxComponents.Marker color={node.fields.color} delay={node.fields.delay}>
|
||||||
|
{node.fields.text}
|
||||||
|
</mdxComponents.Marker>
|
||||||
|
),
|
||||||
|
boldNumber: ({ node }: any) => (
|
||||||
|
<mdxComponents.BoldNumber
|
||||||
|
value={node.fields.value}
|
||||||
|
label={node.fields.label}
|
||||||
|
source={node.fields.source}
|
||||||
|
sourceUrl={node.fields.sourceUrl}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
webVitalsScore: ({ node }: any) => (
|
||||||
|
<mdxComponents.WebVitalsScore
|
||||||
|
values={{
|
||||||
|
lcp: node.fields.lcp,
|
||||||
|
inp: node.fields.inp,
|
||||||
|
cls: node.fields.cls,
|
||||||
|
}}
|
||||||
|
description={node.fields.description}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
buttonBlock: ({ node }: any) => (
|
||||||
|
<mdxComponents.Button
|
||||||
|
href={node.fields.href}
|
||||||
|
variant={node.fields.variant}
|
||||||
|
size={node.fields.size}
|
||||||
|
showArrow={node.fields.showArrow}
|
||||||
|
>
|
||||||
|
{node.fields.label}
|
||||||
|
</mdxComponents.Button>
|
||||||
|
),
|
||||||
|
articleQuote: ({ node }: any) => (
|
||||||
|
<mdxComponents.ArticleQuote
|
||||||
|
quote={node.fields.quote}
|
||||||
|
author={node.fields.author}
|
||||||
|
role={node.fields.role}
|
||||||
|
source={node.fields.source}
|
||||||
|
sourceUrl={node.fields.sourceUrl}
|
||||||
|
translated={node.fields.translated}
|
||||||
|
isCompany={node.fields.isCompany}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
reveal: ({ node }: any) => (
|
||||||
|
<mdxComponents.Reveal
|
||||||
|
direction={node.fields.direction}
|
||||||
|
delay={node.fields.delay}
|
||||||
|
>
|
||||||
|
{/* Reveal component takes children, which in MDX is nested content */}
|
||||||
|
<PayloadRichText data={node.fields.content} />
|
||||||
|
</mdxComponents.Reveal>
|
||||||
|
),
|
||||||
|
section: ({ node }: any) => (
|
||||||
|
<mdxComponents.Section title={node.fields.title}>
|
||||||
|
<PayloadRichText data={node.fields.content} />
|
||||||
|
</mdxComponents.Section>
|
||||||
|
),
|
||||||
|
tableOfContents: () => <mdxComponents.TableOfContents />,
|
||||||
|
faqSection: ({ node }: any) => (
|
||||||
|
<mdxComponents.FAQSection>
|
||||||
|
<PayloadRichText data={node.fields.content} />
|
||||||
|
</mdxComponents.FAQSection>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PayloadRichText({ data }: { data: any }) {
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="article-content max-w-none">
|
||||||
|
<RichText data={data} converters={jsxConverters} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
43
apps/web/src/components/TLDR.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface TLDRProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
content?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TLDR: React.FC<TLDRProps> = ({
|
||||||
|
children,
|
||||||
|
content,
|
||||||
|
className = "",
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`my-8 p-6 bg-slate-900 border-l-4 border-indigo-500 rounded-r-lg shadow-xl ${className}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="bg-indigo-500 text-white p-1 rounded">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M12 2v20M2 12h20M4.93 4.93l14.14 14.14M4.93 19.07l14.14-14.14" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-white font-bold text-lg uppercase tracking-wider">
|
||||||
|
TL;DR
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="text-slate-300 font-serif text-lg leading-relaxed italic">
|
||||||
|
{children || content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
/* eslint-disable react/prop-types */
|
import type { ThumbnailIcon } from "./blogThumbnails";
|
||||||
import type {
|
|
||||||
ThumbnailIcon,
|
|
||||||
} from "./blogThumbnails";
|
|
||||||
import { blogThumbnails } from "./blogThumbnails";
|
import { blogThumbnails } from "./blogThumbnails";
|
||||||
|
|
||||||
interface BlogThumbnailSVGProps {
|
interface BlogThumbnailSVGProps {
|
||||||
|
|||||||
@@ -1,40 +1,39 @@
|
|||||||
|
import { LeadParagraph } from "../components/ArticleParagraph";
|
||||||
|
import { H1, H2, H3 } from "../components/ArticleHeading";
|
||||||
|
import { Paragraph } from "../components/ArticleParagraph";
|
||||||
|
import { ArticleBlockquote } from "../components/ArticleBlockquote";
|
||||||
|
import { Marker } from "../components/Marker";
|
||||||
|
import { ComparisonRow } from "../components/Landing/ComparisonRow";
|
||||||
|
import { StatsDisplay } from "../components/StatsDisplay";
|
||||||
|
import { Mermaid } from "../components/Mermaid";
|
||||||
|
import { DiagramState } from "../components/DiagramState";
|
||||||
|
import { DiagramTimeline } from "../components/DiagramTimeline";
|
||||||
|
import { DiagramGantt } from "../components/DiagramGantt";
|
||||||
|
import { DiagramPie } from "../components/DiagramPie";
|
||||||
|
import { DiagramSequence } from "../components/DiagramSequence";
|
||||||
|
import { DiagramFlow } from "../components/DiagramFlow";
|
||||||
|
import { IconList, IconListItem } from "../components/IconList";
|
||||||
|
import { ArticleMeme } from "../components/ArticleMeme";
|
||||||
|
import { MemeCard } from "../components/MemeCard";
|
||||||
|
import { ExternalLink } from "../components/ExternalLink";
|
||||||
|
import { StatsGrid } from "../components/StatsGrid";
|
||||||
|
import { MetricBar } from "../components/MetricBar";
|
||||||
|
import { ArticleQuote } from "../components/ArticleQuote";
|
||||||
|
import { BoldNumber } from "../components/BoldNumber";
|
||||||
|
import { WebVitalsScore } from "../components/WebVitalsScore";
|
||||||
|
import { WaterfallChart } from "../components/WaterfallChart";
|
||||||
|
import { Button } from "../components/Button";
|
||||||
|
import { LeadMagnet } from "../components/LeadMagnet";
|
||||||
|
import { TrackedLink } from "../components/analytics/TrackedLink";
|
||||||
|
import { FAQSection } from "../components/FAQSection";
|
||||||
|
|
||||||
import { LeadParagraph } from '../components/ArticleParagraph';
|
import { PremiumComparisonChart } from "../components/PremiumComparisonChart";
|
||||||
import { H1, H2, H3 } from '../components/ArticleHeading';
|
import { ImageText } from "../components/ImageText";
|
||||||
import { Paragraph } from '../components/ArticleParagraph';
|
import { Carousel } from "../components/Carousel";
|
||||||
import { ArticleBlockquote } from '../components/ArticleBlockquote';
|
|
||||||
import { Marker } from '../components/Marker';
|
|
||||||
import { ComparisonRow } from '../components/Landing/ComparisonRow';
|
|
||||||
import { StatsDisplay } from '../components/StatsDisplay';
|
|
||||||
import { Mermaid } from '../components/Mermaid';
|
|
||||||
import { DiagramState } from '../components/DiagramState';
|
|
||||||
import { DiagramTimeline } from '../components/DiagramTimeline';
|
|
||||||
import { DiagramGantt } from '../components/DiagramGantt';
|
|
||||||
import { DiagramPie } from '../components/DiagramPie';
|
|
||||||
import { DiagramSequence } from '../components/DiagramSequence';
|
|
||||||
import { DiagramFlow } from '../components/DiagramFlow';
|
|
||||||
import { IconList, IconListItem } from '../components/IconList';
|
|
||||||
import { ArticleMeme } from '../components/ArticleMeme';
|
|
||||||
import { MemeCard } from '../components/MemeCard';
|
|
||||||
import { ExternalLink } from '../components/ExternalLink';
|
|
||||||
import { StatsGrid } from '../components/StatsGrid';
|
|
||||||
import { MetricBar } from '../components/MetricBar';
|
|
||||||
import { ArticleQuote } from '../components/ArticleQuote';
|
|
||||||
import { BoldNumber } from '../components/BoldNumber';
|
|
||||||
import { WebVitalsScore } from '../components/WebVitalsScore';
|
|
||||||
import { WaterfallChart } from '../components/WaterfallChart';
|
|
||||||
import { Button } from '../components/Button';
|
|
||||||
import { LeadMagnet } from '../components/LeadMagnet';
|
|
||||||
import { TrackedLink } from '../components/analytics/TrackedLink';
|
|
||||||
import { FAQSection } from '../components/FAQSection';
|
|
||||||
|
|
||||||
import { PremiumComparisonChart } from '../components/PremiumComparisonChart';
|
import { Section } from "../components/Section";
|
||||||
import { ImageText } from '../components/ImageText';
|
import { Reveal } from "../components/Reveal";
|
||||||
import { Carousel } from '../components/Carousel';
|
import { TableOfContents } from "../components/TableOfContents";
|
||||||
|
|
||||||
import { Section } from '../components/Section';
|
|
||||||
import { Reveal } from '../components/Reveal';
|
|
||||||
import { TableOfContents } from '../components/TableOfContents';
|
|
||||||
|
|
||||||
import { RevenueLossCalculator } from "../components/RevenueLossCalculator";
|
import { RevenueLossCalculator } from "../components/RevenueLossCalculator";
|
||||||
import { PerformanceChart } from "../components/PerformanceChart";
|
import { PerformanceChart } from "../components/PerformanceChart";
|
||||||
@@ -43,56 +42,61 @@ import { LoadTimeSimulator } from "../components/simulations/LoadTimeSimulator";
|
|||||||
import { ArchitectureBuilder } from "../components/simulations/ArchitectureBuilder";
|
import { ArchitectureBuilder } from "../components/simulations/ArchitectureBuilder";
|
||||||
import { DigitalAssetVisualizer } from "../components/simulations/DigitalAssetVisualizer";
|
import { DigitalAssetVisualizer } from "../components/simulations/DigitalAssetVisualizer";
|
||||||
|
|
||||||
import { TwitterEmbed } from '../components/TwitterEmbed';
|
import { TwitterEmbed } from "../components/TwitterEmbed";
|
||||||
import { YouTubeEmbed } from '../components/YouTubeEmbed';
|
import { YouTubeEmbed } from "../components/YouTubeEmbed";
|
||||||
import { LinkedInEmbed } from '../components/LinkedInEmbed';
|
import { LinkedInEmbed } from "../components/LinkedInEmbed";
|
||||||
|
import { TLDR } from "../components/TLDR";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single Source of Truth for MDX component rendering.
|
||||||
|
* Handled separately from Payload blocks to avoid SVG import issues in Node.js.
|
||||||
|
*/
|
||||||
export const mdxComponents = {
|
export const mdxComponents = {
|
||||||
// Named exports for explicit MDX usage
|
LeadParagraph,
|
||||||
LeadParagraph,
|
H1,
|
||||||
H1,
|
H2,
|
||||||
H2,
|
H3,
|
||||||
H3,
|
Paragraph,
|
||||||
Paragraph,
|
ArticleBlockquote,
|
||||||
ArticleBlockquote,
|
Marker,
|
||||||
Marker,
|
ComparisonRow,
|
||||||
ComparisonRow,
|
StatsDisplay,
|
||||||
StatsDisplay,
|
Mermaid,
|
||||||
Mermaid,
|
DiagramState,
|
||||||
DiagramState,
|
DiagramTimeline,
|
||||||
DiagramTimeline,
|
DiagramGantt,
|
||||||
DiagramGantt,
|
DiagramPie,
|
||||||
DiagramPie,
|
DiagramSequence,
|
||||||
DiagramSequence,
|
DiagramFlow,
|
||||||
DiagramFlow,
|
IconList,
|
||||||
IconList,
|
IconListItem,
|
||||||
IconListItem,
|
ArticleMeme,
|
||||||
ArticleMeme,
|
MemeCard,
|
||||||
MemeCard,
|
ExternalLink,
|
||||||
ExternalLink,
|
StatsGrid,
|
||||||
StatsGrid,
|
MetricBar,
|
||||||
MetricBar,
|
ArticleQuote,
|
||||||
ArticleQuote,
|
BoldNumber,
|
||||||
BoldNumber,
|
WebVitalsScore,
|
||||||
WebVitalsScore,
|
WaterfallChart,
|
||||||
WaterfallChart,
|
PremiumComparisonChart,
|
||||||
PremiumComparisonChart,
|
ImageText,
|
||||||
ImageText,
|
Carousel,
|
||||||
Carousel,
|
Section,
|
||||||
Section,
|
Reveal,
|
||||||
Reveal,
|
TableOfContents,
|
||||||
TableOfContents,
|
RevenueLossCalculator,
|
||||||
RevenueLossCalculator,
|
PerformanceChart,
|
||||||
PerformanceChart,
|
PerformanceROICalculator,
|
||||||
PerformanceROICalculator,
|
LoadTimeSimulator,
|
||||||
LoadTimeSimulator,
|
ArchitectureBuilder,
|
||||||
ArchitectureBuilder,
|
DigitalAssetVisualizer,
|
||||||
DigitalAssetVisualizer,
|
TwitterEmbed,
|
||||||
TwitterEmbed,
|
YouTubeEmbed,
|
||||||
YouTubeEmbed,
|
LinkedInEmbed,
|
||||||
LinkedInEmbed,
|
Button,
|
||||||
Button,
|
LeadMagnet,
|
||||||
LeadMagnet,
|
TrackedLink,
|
||||||
TrackedLink,
|
FAQSection,
|
||||||
FAQSection
|
TLDR,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,296 +1,9 @@
|
|||||||
|
import { ComponentDefinition } from "@mintel/content-engine";
|
||||||
import { ComponentDefinition } from '@mintel/content-engine';
|
import { allComponentDefinitions } from "../payload/blocks/allBlocks";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Single Source of Truth for all MDX component definitions.
|
* Single Source of Truth for all MDX component definitions.
|
||||||
* Used by:
|
* Now dynamically generated from individual Payload block definitions.
|
||||||
* - content-engine.config.ts (for the optimization script)
|
|
||||||
* - The AI content pipeline (for component injection)
|
|
||||||
*
|
|
||||||
* Keep in sync with: src/content-engine/components.ts (the MDX runtime registry)
|
|
||||||
*/
|
*/
|
||||||
export const componentDefinitions: ComponentDefinition[] = [
|
export const componentDefinitions: ComponentDefinition[] =
|
||||||
{
|
allComponentDefinitions;
|
||||||
name: 'LeadParagraph',
|
|
||||||
description: 'Larger, emphasized paragraph for the article introduction. Use 1-3 at the start.',
|
|
||||||
usageExample: '<LeadParagraph>\n Unternehmen investieren oft Unsummen in glänzende Oberflächen, während das technische Fundament bröckelt.\n</LeadParagraph>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'H2',
|
|
||||||
description: 'Main section heading. Used for top-level content sections.',
|
|
||||||
usageExample: '<H2>Der wirtschaftliche Case</H2>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'BoldNumber',
|
|
||||||
description: 'Large centerpiece number with label for primary statistics.',
|
|
||||||
usageExample: '<BoldNumber value="5x" label="höhere Conversion-Rate" source="Portent" />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'PremiumComparisonChart',
|
|
||||||
description: 'Advanced chart for comparing performance metrics with industrial aesthetics.',
|
|
||||||
usageExample: '<PremiumComparisonChart title="TTFB Vergleich" items={[{ label: "Alt", value: 800, max: 1000, color: "red" }, { label: "Mintel", value: 50, max: 1000, color: "green" }]} />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ImageText',
|
|
||||||
description: 'Layout component for image next to explanatory text.',
|
|
||||||
usageExample: '<ImageText image="/img.jpg" title="Architektur">Erklärung...</ImageText>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Carousel',
|
|
||||||
description: 'Interactive swipeable slider for multi-step explanations. IMPORTANT: items array must contain at least 2 items with substantive title and content text (no empty content).',
|
|
||||||
usageExample: '<Carousel items={[{ title: "Schritt 1", content: "Analyse der aktuellen Performance..." }, { title: "Schritt 2", content: "Architektur-Optimierung..." }]} />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'H3',
|
|
||||||
description: 'Subsection heading. Used within H2 sections.',
|
|
||||||
usageExample: '<H3>Die drei Säulen meiner Umsetzung</H3>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Paragraph',
|
|
||||||
description: 'Standard body text paragraph. All body text must be wrapped in this.',
|
|
||||||
usageExample: '<Paragraph>\n Mein System ist kein Kostenfaktor, sondern ein <Marker>ROI-Beschleuniger</Marker>.\n</Paragraph>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ArticleBlockquote',
|
|
||||||
description: 'Styled blockquote for expert quotes or key statements.',
|
|
||||||
usageExample: '<ArticleBlockquote>\n Performance ist keine IT-Kennzahl, sondern ein ökonomischer Hebel.\n</ArticleBlockquote>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Marker',
|
|
||||||
description: 'Inline highlight (yellow marker effect) for emphasizing key phrases within paragraphs.',
|
|
||||||
usageExample: '<Marker>entscheidender Wettbewerbsvorteil</Marker>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ComparisonRow',
|
|
||||||
description: 'Side-by-side comparison: negative "Standard" approach vs positive "Mintel" approach. Props include showShare boolean.',
|
|
||||||
usageExample: `<ComparisonRow
|
|
||||||
description="Architektur-Vergleich"
|
|
||||||
negativeLabel="Legacy CMS"
|
|
||||||
negativeText="Langsame Datenbankabfragen, verwundbare Plugins."
|
|
||||||
positiveLabel="Mintel Stack"
|
|
||||||
positiveText="Statische Generierung, perfekte Sicherheit."
|
|
||||||
showShare={true}
|
|
||||||
/>`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'StatsDisplay',
|
|
||||||
description: 'A single large stat card with prominent value, label, and optional subtext.',
|
|
||||||
usageExample: '<StatsDisplay value="-20%" label="Conversion" subtext="Jede Sekunde Verzögerung kostet." />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Mermaid',
|
|
||||||
description: 'Renders a Mermaid.js diagram (flowchart, sequence, pie, etc.). Diagram code goes as children. Keep it tiny (max 3-4 nodes). Wrap in div with className="my-8".',
|
|
||||||
usageExample: `<div className="my-8">
|
|
||||||
<Mermaid id="my-diagram" title="System Architecture" showShare={true}>
|
|
||||||
graph TD
|
|
||||||
A["Request"] --> B["CDN Edge"]
|
|
||||||
B --> C["Static HTML"]
|
|
||||||
</Mermaid>
|
|
||||||
</div>`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'DiagramFlow',
|
|
||||||
description: 'Structured flowchart diagram. Use for process flows, architecture diagrams, etc. Supports structured nodes/edges. direction defaults to LR.',
|
|
||||||
usageExample: `<DiagramFlow
|
|
||||||
nodes={[
|
|
||||||
{ id: "A", label: "Start" },
|
|
||||||
{ id: "B", label: "Process", style: "fill:#f00" }
|
|
||||||
]}
|
|
||||||
edges={[
|
|
||||||
{ from: "A", to: "B", label: "trigger" }
|
|
||||||
]}
|
|
||||||
title="Process Flow"
|
|
||||||
id="flow-1"
|
|
||||||
showShare={true}
|
|
||||||
/>`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'DiagramPie',
|
|
||||||
description: 'Pie chart with structured data props.',
|
|
||||||
usageExample: `<DiagramPie
|
|
||||||
data={[
|
|
||||||
{ label: "JavaScript", value: 35 },
|
|
||||||
{ label: "CSS", value: 25 },
|
|
||||||
{ label: "Images", value: 20 }
|
|
||||||
]}
|
|
||||||
title="Performance Bottlenecks"
|
|
||||||
id="perf-pie"
|
|
||||||
showShare={true}
|
|
||||||
/>`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'DiagramGantt',
|
|
||||||
description: 'Gantt timeline chart comparing durations of tasks.',
|
|
||||||
usageExample: `<DiagramGantt
|
|
||||||
tasks={[
|
|
||||||
{ id: "task-1", name: "Legacy: 4 Wochen", start: "2024-01-01", duration: "4w" },
|
|
||||||
{ id: "task-2", name: "Mintel: 1 Woche", start: "2024-01-01", duration: "1w" }
|
|
||||||
]}
|
|
||||||
title="Zeitvergleich"
|
|
||||||
id="gantt-comparison"
|
|
||||||
showShare={true}
|
|
||||||
/>`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'DiagramState',
|
|
||||||
description: 'State diagram showing states and transitions.',
|
|
||||||
usageExample: `<DiagramState
|
|
||||||
states={["Idle", "Loading", "Loaded", "Error"]}
|
|
||||||
transitions={[
|
|
||||||
{ from: "Idle", to: "Loading", label: "fetch" },
|
|
||||||
{ from: "Loading", to: "Loaded", label: "success" }
|
|
||||||
]}
|
|
||||||
initialState="Idle"
|
|
||||||
title="Request Lifecycle"
|
|
||||||
id="state-diagram"
|
|
||||||
showShare={true}
|
|
||||||
/>`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'DiagramSequence',
|
|
||||||
description: 'Sequence diagram (uses raw Mermaid sequence syntax as children).',
|
|
||||||
usageExample: `<DiagramSequence id="seq-diagram" title="Request Flow" showShare={true}>
|
|
||||||
sequenceDiagram
|
|
||||||
Browser->>CDN: GET /page
|
|
||||||
CDN->>Browser: Static HTML (< 50ms)
|
|
||||||
</DiagramSequence>`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'DiagramTimeline',
|
|
||||||
description: 'Timeline diagram (uses raw Mermaid timeline syntax as children).',
|
|
||||||
usageExample: `<DiagramTimeline id="timeline" title="Project Timeline" showShare={true}>
|
|
||||||
timeline
|
|
||||||
2024 : Planung
|
|
||||||
2025 : Entwicklung
|
|
||||||
2026 : Launch
|
|
||||||
</DiagramTimeline>`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'IconList',
|
|
||||||
description: 'Checklist with check/cross icons. Wrap IconListItem children inside.',
|
|
||||||
usageExample: `<IconList>
|
|
||||||
<IconListItem check>
|
|
||||||
<strong>Zero-Computation:</strong> Statische Seiten, kein Serverwarten.
|
|
||||||
</IconListItem>
|
|
||||||
<IconListItem cross>
|
|
||||||
<strong>Legacy CMS:</strong> Datenbankabfragen bei jedem Request.
|
|
||||||
</IconListItem>
|
|
||||||
</IconList>`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ArticleMeme',
|
|
||||||
description: 'Real meme image from memegen.link. template must be a valid memegen.link ID. IMPORTANT: Captions must be EXTREMELY SARCASTIC and PUNCHY (mocking bad B2B agencies, max 6 words per line). Best templates: drake (2-line prefer/dislike), gru (4-step plan backfire), disastergirl (burning house), fine (this is fine dog). Use German captions. Wrap in div with className="my-8".',
|
|
||||||
usageExample: `<div className="my-8">
|
|
||||||
<ArticleMeme template="drake" captions="47 WordPress Plugins installieren|Eine saubere Serverless Architektur" />
|
|
||||||
</div>`
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: 'Section',
|
|
||||||
description: 'Wraps a thematic section block with optional heading.',
|
|
||||||
usageExample: '<Section>\n <h3>Section Title</h3>\n <p>Content here.</p>\n</Section>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Reveal',
|
|
||||||
description: 'Scroll-triggered reveal animation wrapper. Wrap any content to animate on scroll.',
|
|
||||||
usageExample: '<Reveal>\n <StatsDisplay value="100" label="PageSpeed Score" />\n</Reveal>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'StatsGrid',
|
|
||||||
description: 'Grid of 2–4 stat cards in a row. Use tilde (~) to separate stats, pipe (|) to separate value|label|subtext within each stat.',
|
|
||||||
usageExample: '<StatsGrid stats="53%|Mehr Umsatz|Rakuten 24~33%|Conversion Boost|nach CWV Fix~24%|Top 3 Ranking|bei bestandenen CWV" />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'MetricBar',
|
|
||||||
description: 'Animated horizontal progress bar. Use multiple in sequence to compare metrics. IMPORTANT: value MUST be a real number > 0, never use 0 or placeholder values. Props: label, value (number), max (default 100), unit (default %), color (red|green|blue|slate).',
|
|
||||||
usageExample: `<MetricBar label="WordPress Sites" value={33} color="red" />
|
|
||||||
<MetricBar label="Static Sites" value={92} color="green" />`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ArticleQuote',
|
|
||||||
description: 'Dark-themed quote card. Use for expert quotes or statements. Use isCompany={true} for brands/orgs to show an entity icon instead of personal initials. MANDATORY: always include source and sourceUrl for verifiability. Props: quote, author, role (optional), source (REQUIRED), sourceUrl (REQUIRED), isCompany (optional), translated (optional boolean).',
|
|
||||||
usageExample: '<ArticleQuote quote="Optimizing for speed." author="Google" isCompany={true} source="web.dev" sourceUrl="https://web.dev" translated={true} />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'BoldNumber',
|
|
||||||
description: 'Full-width hero number card with dark gradient, animated count-up, and share button. Use for the most impactful single statistics. IMPORTANT: Always provide source and sourceUrl. Numbers without comparison context should use PremiumComparisonChart or paired MetricBar instead. Props: value (string like "53%" or "2.5M€"), label (short description), source (REQUIRED), sourceUrl (REQUIRED).',
|
|
||||||
usageExample: '<BoldNumber value="8.4%" label="Conversion-Steigerung pro 0.1s schnellere Ladezeit" source="Deloitte Digital" sourceUrl="https://www2.deloitte.com/..." />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'WebVitalsScore',
|
|
||||||
description: 'Displays Core Web Vitals (LCP, INP, CLS) in a premium card layout with automatic traffic light coloring (Good/Needs Improvement/Poor). Use for performance audits or comparisons.',
|
|
||||||
usageExample: '<WebVitalsScore values={{ lcp: 2.5, inp: 200, cls: 0.1 }} description="All metrics passing Google standards." />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'WaterfallChart',
|
|
||||||
description: 'A timeline visualization of network requests (waterfall). Use to show loading sequences or bottlenecks. Labels auto-color coded by type (JS, HTML, IMG).',
|
|
||||||
usageExample: `<WaterfallChart
|
|
||||||
title="Initial Load"
|
|
||||||
events={[
|
|
||||||
{ name: "Document", start: 0, duration: 150 },
|
|
||||||
{ name: "main.js", start: 150, duration: 50 },
|
|
||||||
{ name: "hero.jpg", start: 200, duration: 300 }
|
|
||||||
]}
|
|
||||||
/>`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ExternalLink',
|
|
||||||
description: 'Inline external link with ↗ icon and outbound analytics tracking. Use for all source citations and external references within Paragraph text.',
|
|
||||||
usageExample: '<ExternalLink href="https://web.dev/articles/vitals">Google Core Web Vitals</ExternalLink>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'TwitterEmbed',
|
|
||||||
description: 'Embeds a post from X.com (Twitter). Used to provide social proof, industry quotes, or examples. Provide the numerical tweetId.',
|
|
||||||
usageExample: '<TwitterEmbed tweetId="1753464161943834945" theme="light" />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'YouTubeEmbed',
|
|
||||||
description: 'Embeds a YouTube video to visualize concepts or provide deep dives. Use the 11-character videoId.',
|
|
||||||
usageExample: '<YouTubeEmbed videoId="dQw4w9WgXcQ" title="Performance Explanation" />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'LinkedInEmbed',
|
|
||||||
description: 'Embeds a professional post from LinkedIn. Use the activity URN (e.g. urn:li:activity:1234567890).',
|
|
||||||
usageExample: '<LinkedInEmbed urn="urn:li:activity:7153664326573674496" />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'TrackedLink',
|
|
||||||
description: 'A wrapper around next/link that tracks clicks. Use for all INTERNAL navigational links that should be tracked.',
|
|
||||||
usageExample: '<TrackedLink href="/contact" className="text-blue-600 font-bold">Jetzt anfragen</TrackedLink>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Button',
|
|
||||||
description: 'DEPRECATED: Use <LeadMagnet /> instead for main CTAs. Only use for small secondary links.',
|
|
||||||
usageExample: '<Button href="/contact" variant="outline">Webprojekt anfragen</Button>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'LeadMagnet',
|
|
||||||
description: 'Premium B2B conversion card. Use 1-2 per article as main high-impact CTAs. Props: title (strong headline), description (value prop), buttonText (action), href (link), variant (performance|security|standard).',
|
|
||||||
usageExample: '<LeadMagnet title="Performance-Check anfragen" description="Wir analysieren Ihre Core Web Vitals und decken Umsatzpotenziale auf." buttonText="Jetzt analysieren lassen" href="/contact" variant="performance" />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'PerformanceROICalculator',
|
|
||||||
description: 'Interactive simulation calculator showing the monetary ROI of improving load times (based on Deloitte B2B metrics). Use exactly once in performance-related articles to provide a highly engaging simulation. Requires no props.',
|
|
||||||
usageExample: '<PerformanceROICalculator />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'LoadTimeSimulator',
|
|
||||||
description: 'Interactive visual race simulating the loading experience of a slow legacy CMS vs a fast headless stack. Great for articles discussing load times, technical debt, or user frustration. Requires no props.',
|
|
||||||
usageExample: '<LoadTimeSimulator />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'FAQSection',
|
|
||||||
description: 'Semantic wrapper for FAQ questions at the end of the article. Put standard Markdown H3/Paragraphs inside.',
|
|
||||||
usageExample: '<FAQSection>\n <H3>Frage 1</H3>\n <Paragraph>Antwort 1</Paragraph>\n</FAQSection>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ArchitectureBuilder',
|
|
||||||
description: 'Interactive comparison between a standard SaaS rental approach and a custom Built-First (Mintel) architecture. Useful for articles discussing digital ownership, software rent vs. build, or technological assets. Requires no props.',
|
|
||||||
usageExample: '<ArchitectureBuilder />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'DigitalAssetVisualizer',
|
|
||||||
description: 'Interactive visualization illustrating the financial difference between software as a liability (SaaS/rent) and software as a digital asset (Custom IP). Great for articles concerning CTO strategies, business value of code, and digital independence. Requires no props.',
|
|
||||||
usageExample: '<DigitalAssetVisualizer />'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|||||||
@@ -9,22 +9,50 @@ export async function getAllPosts() {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = await getPayload({ config: configPromise });
|
try {
|
||||||
const { docs } = await payload.find({
|
const payload = await getPayload({ config: configPromise });
|
||||||
collection: "posts",
|
const { docs } = await payload.find({
|
||||||
limit: 1000,
|
collection: "posts",
|
||||||
sort: "-date",
|
limit: 1000,
|
||||||
});
|
sort: "-date",
|
||||||
|
where: {
|
||||||
|
and: [
|
||||||
|
{
|
||||||
|
_status: {
|
||||||
|
equals: "published",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: {
|
||||||
|
less_than_equal: new Date(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return docs.map((doc) => ({
|
return docs.map((doc) => ({
|
||||||
title: doc.title as string,
|
title: doc.title as string,
|
||||||
description: doc.description as string,
|
description: doc.description as string,
|
||||||
date: doc.date as string,
|
date: doc.date as string,
|
||||||
tags: (doc.tags || []).map((t) =>
|
tags: (doc.tags || []).map((t) =>
|
||||||
typeof t === "object" && t !== null ? t.tag : t,
|
typeof t === "object" && t !== null ? t.tag : t,
|
||||||
) as string[],
|
) as string[],
|
||||||
slug: doc.slug as string,
|
slug: doc.slug as string,
|
||||||
thumbnail: doc.thumbnail as string,
|
thumbnail:
|
||||||
body: { code: doc.content as string },
|
(doc.featuredImage &&
|
||||||
}));
|
typeof doc.featuredImage === "object" &&
|
||||||
|
doc.featuredImage.url
|
||||||
|
? doc.featuredImage.url
|
||||||
|
: "") || "",
|
||||||
|
body: { code: "" as string },
|
||||||
|
lexicalContent: doc.content || null,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
"⚠️ Bypassing Payload fetch during build: Database connection refused.",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
191
apps/web/src/payload/actions/generateField.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { config } from "../../../content-engine.config";
|
||||||
|
import { getPayloadHMR } from "@payloadcms/next/utilities";
|
||||||
|
import configPromise from "@payload-config";
|
||||||
|
import * as fs from "node:fs/promises";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import * as os from "node:os";
|
||||||
|
|
||||||
|
async function getOrchestrator() {
|
||||||
|
const OPENROUTER_KEY =
|
||||||
|
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
|
||||||
|
const REPLICATE_KEY = process.env.REPLICATE_API_KEY;
|
||||||
|
|
||||||
|
if (!OPENROUTER_KEY) {
|
||||||
|
throw new Error(
|
||||||
|
"Missing OPENROUTER_API_KEY in .env (Required for AI generation)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const importDynamic = new Function("modulePath", "return import(modulePath)");
|
||||||
|
const { AiBlogPostOrchestrator } = await importDynamic(
|
||||||
|
"@mintel/content-engine",
|
||||||
|
);
|
||||||
|
|
||||||
|
return new AiBlogPostOrchestrator({
|
||||||
|
apiKey: OPENROUTER_KEY,
|
||||||
|
replicateApiKey: REPLICATE_KEY,
|
||||||
|
model: "google/gemini-3-flash-preview",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateSlugAction(
|
||||||
|
title: string,
|
||||||
|
draftContent: string,
|
||||||
|
oldSlug?: string,
|
||||||
|
instructions?: string,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const orchestrator = await getOrchestrator();
|
||||||
|
const newSlug = await orchestrator.generateSlug(
|
||||||
|
draftContent,
|
||||||
|
title,
|
||||||
|
instructions,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (oldSlug && oldSlug !== newSlug) {
|
||||||
|
const payload = await getPayloadHMR({ config: configPromise });
|
||||||
|
await payload.create({
|
||||||
|
collection: "redirects",
|
||||||
|
data: {
|
||||||
|
from: oldSlug,
|
||||||
|
to: newSlug,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, slug: newSlug };
|
||||||
|
} catch (e: any) {
|
||||||
|
return { success: false, error: e.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateThumbnailAction(
|
||||||
|
draftContent: string,
|
||||||
|
title?: string,
|
||||||
|
instructions?: string,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const payload = await getPayloadHMR({ config: configPromise });
|
||||||
|
const OPENROUTER_KEY =
|
||||||
|
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
|
||||||
|
const REPLICATE_KEY = process.env.REPLICATE_API_KEY;
|
||||||
|
|
||||||
|
if (!OPENROUTER_KEY) {
|
||||||
|
throw new Error("Missing OPENROUTER_API_KEY in .env");
|
||||||
|
}
|
||||||
|
if (!REPLICATE_KEY) {
|
||||||
|
throw new Error(
|
||||||
|
"Missing REPLICATE_API_KEY in .env (Required for Thumbnails)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const importDynamic = new Function(
|
||||||
|
"modulePath",
|
||||||
|
"return import(modulePath)",
|
||||||
|
);
|
||||||
|
const { AiBlogPostOrchestrator } = await importDynamic(
|
||||||
|
"@mintel/content-engine",
|
||||||
|
);
|
||||||
|
const { ThumbnailGenerator } = await importDynamic(
|
||||||
|
"@mintel/thumbnail-generator",
|
||||||
|
);
|
||||||
|
|
||||||
|
const orchestrator = new AiBlogPostOrchestrator({
|
||||||
|
apiKey: OPENROUTER_KEY,
|
||||||
|
replicateApiKey: REPLICATE_KEY,
|
||||||
|
model: "google/gemini-3-flash-preview",
|
||||||
|
});
|
||||||
|
|
||||||
|
const tg = new ThumbnailGenerator({ replicateApiKey: REPLICATE_KEY });
|
||||||
|
|
||||||
|
const prompt = await orchestrator.generateVisualPrompt(
|
||||||
|
draftContent || title || "Technology",
|
||||||
|
instructions,
|
||||||
|
);
|
||||||
|
|
||||||
|
const tmpPath = path.join(os.tmpdir(), `mintel-thumb-${Date.now()}.png`);
|
||||||
|
await tg.generateImage(prompt, tmpPath);
|
||||||
|
|
||||||
|
const fileData = await fs.readFile(tmpPath);
|
||||||
|
const stat = await fs.stat(tmpPath);
|
||||||
|
const fileName = path.basename(tmpPath);
|
||||||
|
|
||||||
|
const newMedia = await payload.create({
|
||||||
|
collection: "media",
|
||||||
|
data: {
|
||||||
|
alt: title ? `Thumbnail for ${title}` : "AI Generated Thumbnail",
|
||||||
|
},
|
||||||
|
file: {
|
||||||
|
data: fileData,
|
||||||
|
name: fileName,
|
||||||
|
mimetype: "image/png",
|
||||||
|
size: stat.size,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup temp file
|
||||||
|
await fs.unlink(tmpPath).catch(() => {});
|
||||||
|
|
||||||
|
return { success: true, mediaId: newMedia.id };
|
||||||
|
} catch (e: any) {
|
||||||
|
return { success: false, error: e.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function generateSingleFieldAction(
|
||||||
|
documentTitle: string,
|
||||||
|
documentContent: string,
|
||||||
|
fieldName: string,
|
||||||
|
fieldDescription: string,
|
||||||
|
instructions?: string,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const OPENROUTER_KEY =
|
||||||
|
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
|
||||||
|
if (!OPENROUTER_KEY) throw new Error("Missing OPENROUTER_API_KEY");
|
||||||
|
|
||||||
|
const payload = await getPayloadHMR({ config: configPromise });
|
||||||
|
|
||||||
|
// Fetch context documents from DB
|
||||||
|
const contextDocsData = await payload.find({
|
||||||
|
collection: "context-files",
|
||||||
|
limit: 100,
|
||||||
|
});
|
||||||
|
const projectContext = contextDocsData.docs
|
||||||
|
.map((doc) => `--- ${doc.filename} ---\n${doc.content}`)
|
||||||
|
.join("\n\n");
|
||||||
|
|
||||||
|
const prompt = `You are an expert AI assistant perfectly trained for generating exact data values for CMS components.
|
||||||
|
PROJECT STRATEGY & CONTEXT:
|
||||||
|
${projectContext}
|
||||||
|
|
||||||
|
DOCUMENT TITLE: ${documentTitle}
|
||||||
|
DOCUMENT DRAFT:\n${documentContent}\n
|
||||||
|
YOUR TASK: Generate the exact value for a specific field named "${fieldName}".
|
||||||
|
${fieldDescription ? `FIELD DESCRIPTION / CONSTRAINTS: ${fieldDescription}\n` : ""}
|
||||||
|
${instructions ? `EDITOR INSTRUCTIONS for this field: ${instructions}\n` : ""}
|
||||||
|
CRITICAL RULES:
|
||||||
|
1. Respond ONLY with the requested content value.
|
||||||
|
2. NO markdown wrapping blocks (like \`\`\`mermaid or \`\`\`html) around the output! Just the raw code or text.
|
||||||
|
3. If the field implies a diagram or flow, output RAW Mermaid.js code.
|
||||||
|
4. If it's standard text, write professional B2B German. No quotes, no conversational filler.`;
|
||||||
|
|
||||||
|
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${OPENROUTER_KEY}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: "google/gemini-3-flash-preview",
|
||||||
|
messages: [{ role: "user", content: prompt }],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
const text = data.choices?.[0]?.message?.content?.trim() || "";
|
||||||
|
return { success: true, text };
|
||||||
|
} catch (e: any) {
|
||||||
|
return { success: false, error: e.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
88
apps/web/src/payload/actions/optimizePost.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { config } from "../../../content-engine.config";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { parseMarkdownToLexical } from "../utils/lexicalParser";
|
||||||
|
import { getPayloadHMR } from "@payloadcms/next/utilities";
|
||||||
|
import configPromise from "@payload-config";
|
||||||
|
|
||||||
|
export async function optimizePostText(
|
||||||
|
draftContent: string,
|
||||||
|
instructions?: string,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const payload = await getPayloadHMR({ config: configPromise });
|
||||||
|
const globalAiSettings = await payload.findGlobal({ slug: "ai-settings" });
|
||||||
|
const customSources =
|
||||||
|
globalAiSettings?.customSources?.map((s: any) => s.sourceName) || [];
|
||||||
|
|
||||||
|
const OPENROUTER_KEY =
|
||||||
|
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
|
||||||
|
const REPLICATE_KEY = process.env.REPLICATE_API_KEY;
|
||||||
|
|
||||||
|
if (!OPENROUTER_KEY) {
|
||||||
|
throw new Error(
|
||||||
|
"OPENROUTER_KEY or OPENROUTER_API_KEY not found in environment.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const importDynamic = new Function(
|
||||||
|
"modulePath",
|
||||||
|
"return import(modulePath)",
|
||||||
|
);
|
||||||
|
const { AiBlogPostOrchestrator } = await importDynamic(
|
||||||
|
"@mintel/content-engine",
|
||||||
|
);
|
||||||
|
|
||||||
|
const orchestrator = new AiBlogPostOrchestrator({
|
||||||
|
apiKey: OPENROUTER_KEY,
|
||||||
|
replicateApiKey: REPLICATE_KEY,
|
||||||
|
model: "google/gemini-3-flash-preview",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch context documents purely from DB
|
||||||
|
const contextDocsData = await payload.find({
|
||||||
|
collection: "context-files",
|
||||||
|
limit: 100,
|
||||||
|
});
|
||||||
|
const projectContext = contextDocsData.docs.map((doc) => doc.content);
|
||||||
|
|
||||||
|
const optimizedMarkdown = await orchestrator.optimizeDocument({
|
||||||
|
content: draftContent,
|
||||||
|
projectContext,
|
||||||
|
availableComponents: config.components,
|
||||||
|
instructions,
|
||||||
|
internalLinks: [],
|
||||||
|
customSources,
|
||||||
|
});
|
||||||
|
|
||||||
|
// The orchestrator currently returns Markdown + JSX tags.
|
||||||
|
// We convert this mixed string into a basic Lexical AST map.
|
||||||
|
|
||||||
|
if (!optimizedMarkdown || typeof optimizedMarkdown !== "string") {
|
||||||
|
throw new Error("AI returned invalid markup.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const blocks = parseMarkdownToLexical(optimizedMarkdown);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
lexicalAST: {
|
||||||
|
root: {
|
||||||
|
type: "root",
|
||||||
|
format: "",
|
||||||
|
indent: 0,
|
||||||
|
version: 1,
|
||||||
|
children: blocks,
|
||||||
|
direction: "ltr",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Failed to optimize post:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message || "An unknown error occurred during optimization.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
28
apps/web/src/payload/blocks/ArchitectureBuilderBlock.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const ArchitectureBuilderBlock: MintelBlock = {
|
||||||
|
slug: "architectureBuilder",
|
||||||
|
labels: {
|
||||||
|
singular: "Architecture Builder",
|
||||||
|
plural: "Architecture Builders",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "ArchitectureBuilder",
|
||||||
|
description:
|
||||||
|
"Interactive comparison between a standard SaaS rental approach and a custom Built-First (Mintel) architecture. Useful for articles discussing digital ownership, software rent vs. build, or technological assets. Requires no props.",
|
||||||
|
usageExample: "'<ArchitectureBuilder />'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "preset",
|
||||||
|
type: "text",
|
||||||
|
defaultValue: "standard",
|
||||||
|
admin: { description: "Geben Sie den Text für preset ein." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
59
apps/web/src/payload/blocks/ArticleBlockquoteBlock.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const ArticleBlockquoteBlock: MintelBlock = {
|
||||||
|
slug: "articleBlockquote",
|
||||||
|
labels: {
|
||||||
|
singular: "Article Blockquote",
|
||||||
|
plural: "Article Blockquotes",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "ArticleBlockquote",
|
||||||
|
description: "Styled blockquote for expert quotes or key statements.",
|
||||||
|
usageExample:
|
||||||
|
"'<ArticleBlockquote>\n Performance ist keine IT-Kennzahl, sondern ein ökonomischer Hebel.\n</ArticleBlockquote>'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "quote",
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den mehrzeiligen Text für quote ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "author",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für author ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "role",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für role ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
54
apps/web/src/payload/blocks/ArticleMemeBlock.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const ArticleMemeBlock: MintelBlock = {
|
||||||
|
slug: "articleMeme",
|
||||||
|
labels: {
|
||||||
|
singular: "Article Meme",
|
||||||
|
plural: "Article Memes",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "ArticleMeme",
|
||||||
|
description:
|
||||||
|
"Real image-based meme from the media library. Use for static screenshots or custom memes that are not available via memegen.link.",
|
||||||
|
usageExample:
|
||||||
|
'<ArticleMeme image="/media/my-meme.png" alt="Sarcastic dev meme" caption="When the code finally builds." />',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "image",
|
||||||
|
type: "upload",
|
||||||
|
relationTo: "media",
|
||||||
|
required: true,
|
||||||
|
admin: { description: "Laden Sie die Datei für image hoch." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "alt",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für alt ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "caption",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für caption ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
97
apps/web/src/payload/blocks/ArticleQuoteBlock.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const ArticleQuoteBlock: MintelBlock = {
|
||||||
|
slug: "articleQuote",
|
||||||
|
labels: {
|
||||||
|
singular: "Article Quote",
|
||||||
|
plural: "Article Quotes",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "ArticleQuote",
|
||||||
|
description:
|
||||||
|
"Dark-themed quote card. Use for expert quotes or statements. Use isCompany={true} for brands/orgs to show an entity icon instead of personal initials. MANDATORY: always include source and sourceUrl for verifiability. Props: quote, author, role (optional), source (REQUIRED), sourceUrl (REQUIRED), isCompany (optional), translated (optional boolean).",
|
||||||
|
usageExample:
|
||||||
|
'\'<ArticleQuote quote="Optimizing for speed." author="Google" isCompany={true',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "quote",
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den mehrzeiligen Text für quote ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "author",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für author ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "role",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für role ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "source",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für source ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sourceUrl",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für sourceUrl ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "translated",
|
||||||
|
type: "checkbox",
|
||||||
|
defaultValue: false,
|
||||||
|
admin: { description: "Wert für translated eingeben." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "isCompany",
|
||||||
|
type: "checkbox",
|
||||||
|
defaultValue: false,
|
||||||
|
admin: { description: "Wert für isCompany eingeben." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
72
apps/web/src/payload/blocks/BoldNumberBlock.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const BoldNumberBlock: MintelBlock = {
|
||||||
|
slug: "boldNumber",
|
||||||
|
labels: {
|
||||||
|
singular: "Bold Number",
|
||||||
|
plural: "Bold Numbers",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "BoldNumber",
|
||||||
|
description: "Large centerpiece number with label for primary statistics.",
|
||||||
|
usageExample:
|
||||||
|
'\'<BoldNumber value="5x" label="höhere Conversion-Rate" source="Portent" />\'',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "value",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description: "e.g. 53% or 2.5M€",
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "label",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für label ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "source",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für source ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sourceUrl",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für sourceUrl ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
69
apps/web/src/payload/blocks/ButtonBlock.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const ButtonBlock: MintelBlock = {
|
||||||
|
slug: "buttonBlock",
|
||||||
|
labels: {
|
||||||
|
singular: "Button Block",
|
||||||
|
plural: "Button Blocks",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "Button",
|
||||||
|
description:
|
||||||
|
"DEPRECATED: Use <LeadMagnet /> instead for main CTAs. Only use for small secondary links.",
|
||||||
|
usageExample:
|
||||||
|
'<Button href="/contact" variant="outline">Webprojekt anfragen</Button>',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "label",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für label ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "href",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: { description: "Geben Sie den Text für href ein." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "variant",
|
||||||
|
type: "select",
|
||||||
|
options: [
|
||||||
|
{ label: "Primary", value: "primary" },
|
||||||
|
{ label: "Outline", value: "outline" },
|
||||||
|
{ label: "Ghost", value: "ghost" },
|
||||||
|
],
|
||||||
|
defaultValue: "primary",
|
||||||
|
admin: { description: "Wählen Sie eine Option für variant aus." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "size",
|
||||||
|
type: "select",
|
||||||
|
options: [
|
||||||
|
{ label: "Normal", value: "normal" },
|
||||||
|
{ label: "Large", value: "large" },
|
||||||
|
],
|
||||||
|
defaultValue: "normal",
|
||||||
|
admin: { description: "Wählen Sie eine Option für size aus." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "showArrow",
|
||||||
|
type: "checkbox",
|
||||||
|
defaultValue: true,
|
||||||
|
admin: { description: "Wert für showArrow eingeben." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
48
apps/web/src/payload/blocks/CarouselBlock.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const CarouselBlock: MintelBlock = {
|
||||||
|
slug: "carousel",
|
||||||
|
labels: {
|
||||||
|
singular: "Carousel",
|
||||||
|
plural: "Carousels",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "Carousel",
|
||||||
|
description:
|
||||||
|
"Interactive swipeable slider for multi-step explanations. IMPORTANT: items array must contain at least 2 items with substantive title and content text (no empty content).",
|
||||||
|
usageExample:
|
||||||
|
'\'<Carousel items={[{ title: "Schritt 1", content: "Analyse der aktuellen Performance..."',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "slides",
|
||||||
|
type: "array",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "image",
|
||||||
|
type: "upload",
|
||||||
|
relationTo: "media",
|
||||||
|
admin: { description: "Laden Sie die Datei für image hoch." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "caption",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für caption ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
admin: { description: "Fügen Sie Elemente zur Liste slides hinzu." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
102
apps/web/src/payload/blocks/ComparisonRowBlock.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const ComparisonRowBlock: MintelBlock = {
|
||||||
|
slug: "comparisonRow",
|
||||||
|
labels: {
|
||||||
|
singular: "Comparison Row",
|
||||||
|
plural: "Comparison Rows",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "ComparisonRow",
|
||||||
|
description:
|
||||||
|
'Side-by-side comparison: negative "Standard" approach vs positive "Mintel" approach. Props include showShare boolean.',
|
||||||
|
usageExample: `<ComparisonRow
|
||||||
|
description="Architektur-Vergleich"
|
||||||
|
negativeLabel="Legacy CMS"
|
||||||
|
negativeText="Langsame Datenbankabfragen, verwundbare Plugins."
|
||||||
|
positiveLabel="Mintel Stack"
|
||||||
|
positiveText="Statische Generierung, perfekte Sicherheit."
|
||||||
|
showShare={true`,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "description",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
description: "Optional overarching description for the comparison.",
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negativeLabel",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
defaultValue: "Legacy",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für negativeLabel ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negativeText",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für negativeText ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "positiveLabel",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
defaultValue: "Mintel Stack",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für positiveLabel ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "positiveText",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für positiveText ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reverse",
|
||||||
|
type: "checkbox",
|
||||||
|
defaultValue: false,
|
||||||
|
admin: {
|
||||||
|
description: "Swap the visual order of the positive/negative cards?",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
35
apps/web/src/payload/blocks/DiagramFlowBlock.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const DiagramFlowBlock: MintelBlock = {
|
||||||
|
slug: "diagramFlow",
|
||||||
|
labels: {
|
||||||
|
singular: "Diagram Flow",
|
||||||
|
plural: "Diagram Flows",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "DiagramFlow",
|
||||||
|
description:
|
||||||
|
"Mermaid flowchart diagram defining the graph structure. MUST output raw mermaid code, no quotes or HTML.",
|
||||||
|
usageExample: "graph TD\\n A[Start] --> B[End]",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "definition",
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
35
apps/web/src/payload/blocks/DiagramGanttBlock.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const DiagramGanttBlock: MintelBlock = {
|
||||||
|
slug: "diagramGantt",
|
||||||
|
labels: {
|
||||||
|
singular: "Diagram Gantt",
|
||||||
|
plural: "Diagram Gantts",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "DiagramGantt",
|
||||||
|
description: "Mermaid Gantt timeline chart. MUST output raw mermaid code.",
|
||||||
|
usageExample:
|
||||||
|
"gantt\\n title Project Roadmap\\n dateFormat YYYY-MM-DD\\n Section Design\\n Draft UI :a1, 2024-01-01, 7d",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "definition",
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
34
apps/web/src/payload/blocks/DiagramPieBlock.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const DiagramPieBlock: MintelBlock = {
|
||||||
|
slug: "diagramPie",
|
||||||
|
labels: {
|
||||||
|
singular: "Diagram Pie",
|
||||||
|
plural: "Diagram Pies",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "DiagramPie",
|
||||||
|
description: "Mermaid pie chart diagram. MUST output raw mermaid code.",
|
||||||
|
usageExample: 'pie title Market Share\\n "Chrome" : 60\\n "Safari" : 20',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "definition",
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
36
apps/web/src/payload/blocks/DiagramSequenceBlock.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const DiagramSequenceBlock: MintelBlock = {
|
||||||
|
slug: "diagramSequence",
|
||||||
|
labels: {
|
||||||
|
singular: "Diagram Sequence",
|
||||||
|
plural: "Diagram Sequences",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "DiagramSequence",
|
||||||
|
description:
|
||||||
|
"Mermaid sequence diagram showing actor interactions. MUST output raw mermaid code.",
|
||||||
|
usageExample:
|
||||||
|
"sequenceDiagram\\n Client->>Server: GET /api\\n Server-->>Client: 200 OK",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "definition",
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
35
apps/web/src/payload/blocks/DiagramStateBlock.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const DiagramStateBlock: MintelBlock = {
|
||||||
|
slug: "diagramState",
|
||||||
|
labels: {
|
||||||
|
singular: "Diagram State",
|
||||||
|
plural: "Diagram States",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "DiagramState",
|
||||||
|
description:
|
||||||
|
"Mermaid state diagram showing states and transitions. MUST output raw mermaid code.",
|
||||||
|
usageExample: "stateDiagram-v2\\n [*] --> Idle\\n Idle --> Loading",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "definition",
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
36
apps/web/src/payload/blocks/DiagramTimelineBlock.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const DiagramTimelineBlock: MintelBlock = {
|
||||||
|
slug: "diagramTimeline",
|
||||||
|
labels: {
|
||||||
|
singular: "Diagram Timeline",
|
||||||
|
plural: "Diagram Timelines",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "DiagramTimeline",
|
||||||
|
description:
|
||||||
|
"Mermaid timeline or journey diagram. MUST output raw mermaid code.",
|
||||||
|
usageExample:
|
||||||
|
"timeline\\n title Project Timeline\\n 2024\\n : Q1 : Planning\\n : Q2 : Execution",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "definition",
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
27
apps/web/src/payload/blocks/DigitalAssetVisualizerBlock.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const DigitalAssetVisualizerBlock: MintelBlock = {
|
||||||
|
slug: "digitalAssetVisualizer",
|
||||||
|
labels: {
|
||||||
|
singular: "Digital Asset Visualizer",
|
||||||
|
plural: "Digital Asset Visualizers",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "DigitalAssetVisualizer",
|
||||||
|
description:
|
||||||
|
"Interactive visualization illustrating the financial difference between software as a liability (SaaS/rent) and software as a digital asset (Custom IP). Great for articles concerning CTO strategies, business value of code, and digital independence. Requires no props.",
|
||||||
|
usageExample: "'<DigitalAssetVisualizer />'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "assetId",
|
||||||
|
type: "text",
|
||||||
|
admin: { description: "Geben Sie den Text für assetId ein." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
42
apps/web/src/payload/blocks/ExternalLinkBlock.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const ExternalLinkBlock: MintelBlock = {
|
||||||
|
slug: "externalLink",
|
||||||
|
labels: {
|
||||||
|
singular: "External Link",
|
||||||
|
plural: "External Links",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "ExternalLink",
|
||||||
|
description:
|
||||||
|
"Inline external link with ↗ icon and outbound analytics tracking. Use for all source citations and external references within Paragraph text.",
|
||||||
|
usageExample:
|
||||||
|
"'<ExternalLink href=\"https://web.dev/articles/vitals\">Google Core Web Vitals</ExternalLink>'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "href",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: { description: "Geben Sie den Text für href ein." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "label",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für label ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
47
apps/web/src/payload/blocks/FAQSectionBlock.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
import { lexicalEditor, BlocksFeature } from "@payloadcms/richtext-lexical";
|
||||||
|
import { HeadingBlock } from "./HeadingBlock";
|
||||||
|
import { ParagraphBlock } from "./ParagraphBlock";
|
||||||
|
import { ExternalLinkBlock } from "./ExternalLinkBlock";
|
||||||
|
import { TrackedLinkBlock } from "./TrackedLinkBlock";
|
||||||
|
|
||||||
|
export const FAQSectionBlock: MintelBlock = {
|
||||||
|
slug: "faqSection",
|
||||||
|
labels: {
|
||||||
|
singular: "Faq Section",
|
||||||
|
plural: "Faq Sections",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "FAQSection",
|
||||||
|
description:
|
||||||
|
"Semantic wrapper for FAQ questions at the end of the article. Put standard Markdown H3/Paragraphs inside.",
|
||||||
|
usageExample:
|
||||||
|
"'<FAQSection>\n <H3>Frage 1</H3>\n <Paragraph>Antwort 1</Paragraph>\n</FAQSection>'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "content",
|
||||||
|
type: "richText",
|
||||||
|
editor: lexicalEditor({
|
||||||
|
features: ({ defaultFeatures }) => [
|
||||||
|
...defaultFeatures,
|
||||||
|
BlocksFeature({
|
||||||
|
blocks: [
|
||||||
|
HeadingBlock,
|
||||||
|
ParagraphBlock,
|
||||||
|
ExternalLinkBlock,
|
||||||
|
TrackedLinkBlock,
|
||||||
|
].map(({ ai, render, ...b }) => b),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
required: true,
|
||||||
|
admin: { description: "Formatierter Textbereich für content." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
24
apps/web/src/payload/blocks/H2Block.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
export const H2Block: MintelBlock = {
|
||||||
|
slug: "mintelH2",
|
||||||
|
labels: {
|
||||||
|
singular: "Heading 2",
|
||||||
|
plural: "Headings 2",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "text",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description: "Geben Sie den Text für die H2-Überschrift ein.",
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
24
apps/web/src/payload/blocks/H3Block.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
export const H3Block: MintelBlock = {
|
||||||
|
slug: "mintelH3",
|
||||||
|
labels: {
|
||||||
|
singular: "Heading 3",
|
||||||
|
plural: "Headings 3",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "text",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description: "Geben Sie den Text für die H3-Überschrift ein.",
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
50
apps/web/src/payload/blocks/HeadingBlock.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
export const HeadingBlock: MintelBlock = {
|
||||||
|
slug: "mintelHeading",
|
||||||
|
labels: {
|
||||||
|
singular: "Heading",
|
||||||
|
plural: "Headings",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "Heading",
|
||||||
|
description:
|
||||||
|
"Flexible heading component with separated SEO and visual display levels.",
|
||||||
|
usageExample:
|
||||||
|
'\'<Heading seoLevel="h2" displayLevel="h3">Titel</Heading>\'',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "text",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description: "Der Text der Überschrift.",
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "seoLevel",
|
||||||
|
type: "select",
|
||||||
|
options: ["h1", "h2", "h3", "h4", "h5", "h6"],
|
||||||
|
defaultValue: "h2",
|
||||||
|
admin: { description: "Das semantische HTML-Tag für SEO." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "displayLevel",
|
||||||
|
type: "select",
|
||||||
|
options: ["h1", "h2", "h3", "h4", "h5", "h6"],
|
||||||
|
defaultValue: "h2",
|
||||||
|
admin: {
|
||||||
|
description: "Die visuelle Größe der Überschrift (unabhängig von SEO).",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
69
apps/web/src/payload/blocks/IconListBlock.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const IconListBlock: MintelBlock = {
|
||||||
|
slug: "iconList",
|
||||||
|
labels: {
|
||||||
|
singular: "Icon List",
|
||||||
|
plural: "Icon Lists",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "IconList",
|
||||||
|
description:
|
||||||
|
"Checklist with check/cross icons. Wrap IconListItem children inside.",
|
||||||
|
usageExample: `<IconList>
|
||||||
|
<IconListItem check>
|
||||||
|
<strong>Zero-Computation:</strong> Statische Seiten, kein Serverwarten.
|
||||||
|
</IconListItem>
|
||||||
|
<IconListItem cross>
|
||||||
|
<strong>Legacy CMS:</strong> Datenbankabfragen bei jedem Request.
|
||||||
|
</IconListItem>
|
||||||
|
</IconList>`,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "items",
|
||||||
|
type: "array",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "icon",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
description: "Lucide icon",
|
||||||
|
components: { Field: "@/src/payload/components/IconSelector" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "title",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für title ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "description",
|
||||||
|
type: "textarea",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den mehrzeiligen Text für description ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
admin: { description: "Fügen Sie Elemente zur Liste items hinzu." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
52
apps/web/src/payload/blocks/ImageTextBlock.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const ImageTextBlock: MintelBlock = {
|
||||||
|
slug: "imageText",
|
||||||
|
labels: {
|
||||||
|
singular: "Image Text",
|
||||||
|
plural: "Image Texts",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "ImageText",
|
||||||
|
description: "Layout component for image next to explanatory text.",
|
||||||
|
usageExample:
|
||||||
|
'\'<ImageText image="/img.jpg" title="Architektur">Erklärung...</ImageText>\'',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "image",
|
||||||
|
type: "upload",
|
||||||
|
relationTo: "media",
|
||||||
|
required: true,
|
||||||
|
admin: { description: "Laden Sie die Datei für image hoch." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "text",
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den mehrzeiligen Text für text ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "alignment",
|
||||||
|
type: "select",
|
||||||
|
options: [
|
||||||
|
{ label: "Left", value: "left" },
|
||||||
|
{ label: "Right", value: "right" },
|
||||||
|
],
|
||||||
|
defaultValue: "left",
|
||||||
|
admin: { description: "Wählen Sie eine Option für alignment aus." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
81
apps/web/src/payload/blocks/LeadMagnetBlock.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const LeadMagnetBlock: MintelBlock = {
|
||||||
|
slug: "leadMagnet",
|
||||||
|
labels: {
|
||||||
|
singular: "Lead Magnet CTA",
|
||||||
|
plural: "Lead Magnet CTAs",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "LeadMagnet",
|
||||||
|
description:
|
||||||
|
"Premium B2B conversion card. Use 1-2 per article as main high-impact CTAs. Props: title (strong headline), description (value prop), buttonText (action), href (link), variant (performance|security|standard).",
|
||||||
|
usageExample:
|
||||||
|
'\'<LeadMagnet title="Performance-Check anfragen" description="Wir analysieren Ihre Core Web Vitals und decken Umsatzpotenziale auf." buttonText="Jetzt analysieren lassen" href="/contact" variant="performance" />\'',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "title",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description: "The strong headline for the Call-to-Action",
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "description",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description: "The value proposition text.",
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "buttonText",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
defaultValue: "Jetzt anfragen",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für buttonText ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "href",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
defaultValue: "/contact",
|
||||||
|
admin: { description: "Geben Sie den Text für href ein." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "variant",
|
||||||
|
type: "select",
|
||||||
|
options: [
|
||||||
|
{ label: "Performance", value: "performance" },
|
||||||
|
{ label: "Security", value: "security" },
|
||||||
|
{ label: "Standard", value: "standard" },
|
||||||
|
],
|
||||||
|
defaultValue: "standard",
|
||||||
|
admin: { description: "Wählen Sie eine Option für variant aus." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
36
apps/web/src/payload/blocks/LeadParagraphBlock.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const LeadParagraphBlock: MintelBlock = {
|
||||||
|
slug: "leadParagraph",
|
||||||
|
labels: {
|
||||||
|
singular: "Lead Paragraph",
|
||||||
|
plural: "Lead Paragraphs",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "LeadParagraph",
|
||||||
|
description:
|
||||||
|
"Larger, emphasized paragraph for the article introduction. Use 1-3 at the start.",
|
||||||
|
usageExample:
|
||||||
|
"'<LeadParagraph>\n Unternehmen investieren oft Unsummen in glänzende Oberflächen, während das technische Fundament bröckelt.\n</LeadParagraph>'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "text",
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den mehrzeiligen Text für text ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
29
apps/web/src/payload/blocks/LinkedInEmbedBlock.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const LinkedInEmbedBlock: MintelBlock = {
|
||||||
|
slug: "linkedInEmbed",
|
||||||
|
labels: {
|
||||||
|
singular: "Linked In Embed",
|
||||||
|
plural: "Linked In Embeds",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "LinkedInEmbed",
|
||||||
|
description:
|
||||||
|
"Embeds a professional post from LinkedIn. Use the activity URN (e.g. urn:li:activity:1234567890).",
|
||||||
|
usageExample:
|
||||||
|
"'<LinkedInEmbed urn=\"urn:li:activity:7153664326573674496\" />'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "url",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: { description: "Geben Sie den Text für url ein." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
31
apps/web/src/payload/blocks/LoadTimeSimulatorBlock.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const LoadTimeSimulatorBlock: MintelBlock = {
|
||||||
|
slug: "loadTimeSimulator",
|
||||||
|
labels: {
|
||||||
|
singular: "Load Time Simulator",
|
||||||
|
plural: "Load Time Simulators",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "LoadTimeSimulator",
|
||||||
|
description:
|
||||||
|
"Interactive visual race simulating the loading experience of a slow legacy CMS vs a fast headless stack. Great for articles discussing load times, technical debt, or user frustration. Requires no props.",
|
||||||
|
usageExample: "'<LoadTimeSimulator />'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "initialLoadTime",
|
||||||
|
type: "number",
|
||||||
|
defaultValue: 3.5,
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
"Tragen Sie einen numerischen Wert für initialLoadTime ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
51
apps/web/src/payload/blocks/MarkerBlock.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const MarkerBlock: MintelBlock = {
|
||||||
|
slug: "marker",
|
||||||
|
labels: {
|
||||||
|
singular: "Marker",
|
||||||
|
plural: "Markers",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "Marker",
|
||||||
|
description:
|
||||||
|
"Inline highlight (yellow marker effect) for emphasizing key phrases within paragraphs.",
|
||||||
|
usageExample: "'<Marker>entscheidender Wettbewerbsvorteil</Marker>'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "text",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für text ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "color",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
description: "Hex or rgba color",
|
||||||
|
components: { Field: "@/src/payload/components/ColorPicker" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "delay",
|
||||||
|
type: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
admin: {
|
||||||
|
description: "Tragen Sie einen numerischen Wert für delay ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
51
apps/web/src/payload/blocks/MemeCardBlock.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const MemeCardBlock: MintelBlock = {
|
||||||
|
slug: "memeCard",
|
||||||
|
labels: {
|
||||||
|
singular: "Meme Card",
|
||||||
|
plural: "Meme Cards",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "MemeCard",
|
||||||
|
description:
|
||||||
|
'Real meme image from memegen.link. template must be a valid memegen.link ID. IMPORTANT: Captions must be EXTREMELY SARCASTIC and PUNCHY (mocking bad B2B agencies, max 6 words per line). Best templates: drake (2-line prefer/dislike), gru (4-step plan backfire), disastergirl (burning house), fine (this is fine dog). Use German captions. Wrap in div with className="my-8".',
|
||||||
|
usageExample: `<div className="my-8">
|
||||||
|
<MemeCard template="drake" captions="47 WordPress Plugins installieren|Eine saubere Serverless Architektur" />
|
||||||
|
</div>`,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "template",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
"The template ID from memegen.link (e.g. 'drake', 'disastergirl')",
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "captions",
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
"Pipe-separated captions for the meme (e.g. 'Legacy Code|Mintel Stack'). Maximum 6 words per line.",
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
68
apps/web/src/payload/blocks/MermaidBlock.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const MermaidBlock: MintelBlock = {
|
||||||
|
slug: "mermaid",
|
||||||
|
labels: {
|
||||||
|
singular: "Mermaid",
|
||||||
|
plural: "Mermaids",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "Mermaid",
|
||||||
|
description:
|
||||||
|
'Renders a Mermaid.js diagram (flowchart, sequence, pie, etc.). Diagram code goes as children. Keep it tiny (max 3-4 nodes). Wrap in div with className="my-8".',
|
||||||
|
usageExample: `<div className="my-8">
|
||||||
|
<Mermaid id="my-diagram" title="System Architecture" showShare={true`,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "id",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
"A unique ASCII ID for the diagram (e.g. 'architecture-1').",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "title",
|
||||||
|
type: "text",
|
||||||
|
required: false,
|
||||||
|
admin: {
|
||||||
|
description: "Optional title displayed above the diagram.",
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "showShare",
|
||||||
|
type: "checkbox",
|
||||||
|
defaultValue: false,
|
||||||
|
admin: {
|
||||||
|
description: "Show the share button for this diagram?",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "chartDefinition",
|
||||||
|
type: "code",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
language: "markdown",
|
||||||
|
description:
|
||||||
|
"The raw Mermaid.js syntax (e.g. graph TD... shadowing, loops, etc.).",
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
68
apps/web/src/payload/blocks/MetricBarBlock.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const MetricBarBlock: MintelBlock = {
|
||||||
|
slug: "metricBar",
|
||||||
|
labels: {
|
||||||
|
singular: "Metric Bar",
|
||||||
|
plural: "Metric Bars",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "MetricBar",
|
||||||
|
description:
|
||||||
|
"Animated horizontal progress bar. Use multiple in sequence to compare metrics. IMPORTANT: value MUST be a real number > 0, never use 0 or placeholder values. Props: label, value (number), max (default 100), unit (default %), color (red|green|blue|slate).",
|
||||||
|
usageExample: '<MetricBar label="WordPress Sites" value={33',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "label",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für label ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "value",
|
||||||
|
type: "number",
|
||||||
|
required: true,
|
||||||
|
admin: { description: "Percentage 0-100" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "max",
|
||||||
|
type: "number",
|
||||||
|
defaultValue: 100,
|
||||||
|
admin: { description: "Tragen Sie einen numerischen Wert für max ein." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unit",
|
||||||
|
type: "text",
|
||||||
|
defaultValue: "%",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für unit ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "color",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: { Field: "@/src/payload/components/ColorPicker" },
|
||||||
|
description: "Geben Sie den Text für color ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
36
apps/web/src/payload/blocks/ParagraphBlock.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const ParagraphBlock: MintelBlock = {
|
||||||
|
slug: "mintelP",
|
||||||
|
labels: {
|
||||||
|
singular: "Paragraph",
|
||||||
|
plural: "Paragraphs",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "Paragraph",
|
||||||
|
description:
|
||||||
|
"Standard body text paragraph. All body text must be wrapped in this.",
|
||||||
|
usageExample:
|
||||||
|
"'<Paragraph>\n Mein System ist kein Kostenfaktor, sondern ein <Marker>ROI-Beschleuniger</Marker>.\n</Paragraph>'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "text",
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den mehrzeiligen Text für text ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
36
apps/web/src/payload/blocks/PerformanceChartBlock.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const PerformanceChartBlock: MintelBlock = {
|
||||||
|
slug: "performanceChart",
|
||||||
|
labels: {
|
||||||
|
singular: "Performance Chart",
|
||||||
|
plural: "Performance Charts",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "PerformanceChart",
|
||||||
|
description:
|
||||||
|
"A visual chart illustrating performance metrics (e.g. PageSpeed, TTFB) over time or in comparison. Use to emphasize technical improvements.",
|
||||||
|
usageExample:
|
||||||
|
'<PerformanceChart items={[{ label: "Vorher", value: 12 }, { label: "Nachher", value: 98 }]} />',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "title",
|
||||||
|
type: "text",
|
||||||
|
defaultValue: "Website Performance",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für title ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
40
apps/web/src/payload/blocks/PerformanceROICalculatorBlock.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const PerformanceROICalculatorBlock: MintelBlock = {
|
||||||
|
slug: "performanceROICalculator",
|
||||||
|
labels: {
|
||||||
|
singular: "Performance R O I Calculator",
|
||||||
|
plural: "Performance R O I Calculators",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "PerformanceROICalculator",
|
||||||
|
description:
|
||||||
|
"Interactive simulation calculator showing the monetary ROI of improving load times (based on Deloitte B2B metrics). Use exactly once in performance-related articles to provide a highly engaging simulation. Requires no props.",
|
||||||
|
usageExample: "'<PerformanceROICalculator />'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "baseConversionRate",
|
||||||
|
type: "number",
|
||||||
|
defaultValue: 2.5,
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
"Tragen Sie einen numerischen Wert für baseConversionRate ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "monthlyVisitors",
|
||||||
|
type: "number",
|
||||||
|
defaultValue: 50000,
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
"Tragen Sie einen numerischen Wert für monthlyVisitors ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
115
apps/web/src/payload/blocks/PremiumComparisonChartBlock.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const PremiumComparisonChartBlock: MintelBlock = {
|
||||||
|
slug: "premiumComparisonChart",
|
||||||
|
labels: {
|
||||||
|
singular: "Premium Comparison Chart",
|
||||||
|
plural: "Premium Comparison Charts",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "PremiumComparisonChart",
|
||||||
|
description:
|
||||||
|
"Advanced chart for comparing performance metrics with industrial aesthetics.",
|
||||||
|
usageExample:
|
||||||
|
'\'<PremiumComparisonChart title="TTFB Vergleich" items={[{ label: "Alt", value: 800, max: 1000, color: "red"',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "title",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für title ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "subtitle",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für subtitle ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "datasets",
|
||||||
|
type: "array",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "label",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für label ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "value",
|
||||||
|
type: "number",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description: "Tragen Sie einen numerischen Wert für value ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "max",
|
||||||
|
type: "number",
|
||||||
|
defaultValue: 100,
|
||||||
|
admin: {
|
||||||
|
description: "Tragen Sie einen numerischen Wert für max ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unit",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für unit ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "color",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: { Field: "@/src/payload/components/ColorPicker" },
|
||||||
|
description: "Geben Sie den Text für color ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "description",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für description ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
admin: { description: "Fügen Sie Elemente zur Liste datasets hinzu." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
51
apps/web/src/payload/blocks/RevealBlock.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
import { lexicalEditor } from "@payloadcms/richtext-lexical";
|
||||||
|
|
||||||
|
export const RevealBlock: MintelBlock = {
|
||||||
|
slug: "mintelReveal",
|
||||||
|
labels: {
|
||||||
|
singular: "Reveal Wrap",
|
||||||
|
plural: "Reveal Wraps",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "Reveal",
|
||||||
|
description:
|
||||||
|
"Scroll-triggered reveal animation wrapper. Wrap any content to animate on scroll.",
|
||||||
|
usageExample:
|
||||||
|
'\'<Reveal>\n <StatsDisplay value="100" label="PageSpeed Score" />\n</Reveal>\'',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "direction",
|
||||||
|
type: "select",
|
||||||
|
options: [
|
||||||
|
{ label: "Up", value: "up" },
|
||||||
|
{ label: "Down", value: "down" },
|
||||||
|
{ label: "Left", value: "left" },
|
||||||
|
{ label: "Right", value: "right" },
|
||||||
|
],
|
||||||
|
defaultValue: "up",
|
||||||
|
admin: { description: "Wählen Sie eine Option für direction aus." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "delay",
|
||||||
|
type: "number",
|
||||||
|
defaultValue: 0.1,
|
||||||
|
admin: {
|
||||||
|
description: "Tragen Sie einen numerischen Wert für delay ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "content",
|
||||||
|
type: "richText",
|
||||||
|
editor: lexicalEditor({}),
|
||||||
|
required: true,
|
||||||
|
admin: { description: "Formatierter Textbereich für content." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
35
apps/web/src/payload/blocks/RevenueLossCalculatorBlock.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const RevenueLossCalculatorBlock: MintelBlock = {
|
||||||
|
slug: "revenueLossCalculator",
|
||||||
|
labels: {
|
||||||
|
singular: "Revenue Loss Calculator",
|
||||||
|
plural: "Revenue Loss Calculators",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "RevenueLossCalculator",
|
||||||
|
description:
|
||||||
|
"Interactive calculator that estimates financial loss due to slow page load times. Use to build a business case for performance optimization.",
|
||||||
|
usageExample: "<RevenueLossCalculator />",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "title",
|
||||||
|
type: "text",
|
||||||
|
defaultValue: "Performance Revenue Simulator",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für title ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
42
apps/web/src/payload/blocks/SectionBlock.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
import { lexicalEditor } from "@payloadcms/richtext-lexical";
|
||||||
|
|
||||||
|
export const SectionBlock: MintelBlock = {
|
||||||
|
slug: "mintelSection",
|
||||||
|
labels: {
|
||||||
|
singular: "Section Wrap",
|
||||||
|
plural: "Section Wraps",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "Section",
|
||||||
|
description: "Wraps a thematic section block with optional heading.",
|
||||||
|
usageExample:
|
||||||
|
"'<Section>\n <h3>Section Title</h3>\n <p>Content here.</p>\n</Section>'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "title",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für title ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "content",
|
||||||
|
type: "richText",
|
||||||
|
editor: lexicalEditor({}),
|
||||||
|
required: true,
|
||||||
|
admin: { description: "Formatierter Textbereich für content." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
60
apps/web/src/payload/blocks/StatsDisplayBlock.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const StatsDisplayBlock: MintelBlock = {
|
||||||
|
slug: "statsDisplay",
|
||||||
|
labels: {
|
||||||
|
singular: "Stats Display",
|
||||||
|
plural: "Stats Displays",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "StatsDisplay",
|
||||||
|
description:
|
||||||
|
"A single large stat card with prominent value, label, and optional subtext.",
|
||||||
|
usageExample:
|
||||||
|
'\'<StatsDisplay value="-20%" label="Conversion" subtext="Jede Sekunde Verzögerung kostet." />\'',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "label",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für label ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "value",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für value ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "subtext",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für subtext ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
56
apps/web/src/payload/blocks/StatsGridBlock.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const StatsGridBlock: MintelBlock = {
|
||||||
|
slug: "statsGrid",
|
||||||
|
labels: {
|
||||||
|
singular: "Stats Grid",
|
||||||
|
plural: "Stats Grids",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "StatsGrid",
|
||||||
|
description:
|
||||||
|
"Grid of 2–4 stat cards in a row. Use tilde (~) to separate stats, pipe (|) to separate value|label|subtext within each stat.",
|
||||||
|
usageExample:
|
||||||
|
"'<StatsGrid stats=\"53%|Mehr Umsatz|Rakuten 24~33%|Conversion Boost|nach CWV Fix~24%|Top 3 Ranking|bei bestandenen CWV\" />'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "stats",
|
||||||
|
type: "array",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "label",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für label ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "value",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für value ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
admin: { description: "Fügen Sie Elemente zur Liste stats hinzu." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
35
apps/web/src/payload/blocks/TLDRBlock.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const TLDRBlock: MintelBlock = {
|
||||||
|
slug: "mintelTldr",
|
||||||
|
labels: {
|
||||||
|
singular: "TL;DR Block",
|
||||||
|
plural: "TL;DR Blocks",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "TLDR",
|
||||||
|
description:
|
||||||
|
"Presents a bite-sized summary of the article in a premium dark card. Use exactly once at the very beginning.",
|
||||||
|
usageExample:
|
||||||
|
"'<TLDR>\n Stabilität ist kein Zufall, sondern das Ergebnis einer Clean Code Strategie.\n</TLDR>'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "content",
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description: "The summary content for the TLDR box.",
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
48
apps/web/src/payload/blocks/TrackedLinkBlock.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const TrackedLinkBlock: MintelBlock = {
|
||||||
|
slug: "trackedLink",
|
||||||
|
labels: {
|
||||||
|
singular: "Tracked Link",
|
||||||
|
plural: "Tracked Links",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "TrackedLink",
|
||||||
|
description:
|
||||||
|
"A wrapper around next/link that tracks clicks. Use for all INTERNAL navigational links that should be tracked.",
|
||||||
|
usageExample:
|
||||||
|
'\'<TrackedLink href="/contact" className="text-blue-600 font-bold">Jetzt anfragen</TrackedLink>\'',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "href",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: { description: "Geben Sie den Text für href ein." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "label",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für label ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "eventName",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: { description: "Geben Sie den Text für eventName ein." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
29
apps/web/src/payload/blocks/TwitterEmbedBlock.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const TwitterEmbedBlock: MintelBlock = {
|
||||||
|
slug: "twitterEmbed",
|
||||||
|
labels: {
|
||||||
|
singular: "Twitter (X) Embed",
|
||||||
|
plural: "Twitter (X) Embeds",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "TwitterEmbed",
|
||||||
|
description:
|
||||||
|
"Embeds a post from X.com (Twitter). Used to provide social proof, industry quotes, or examples. Provide the numerical tweetId.",
|
||||||
|
usageExample:
|
||||||
|
'\'<TwitterEmbed tweetId="1753464161943834945" theme="light" />\'',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "url",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: { description: "Geben Sie den Text für url ein." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||