Compare commits
4 Commits
v1.8.20
...
4a5017124a
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a5017124a | |||
| 283de11b11 | |||
| 31685a458b | |||
| 6864903cff |
1
.gitignore
vendored
@@ -3,6 +3,7 @@ dist/
|
||||
.next/
|
||||
out/
|
||||
.contentlayer/
|
||||
.pnpm-store
|
||||
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
# 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
|
||||
|
||||
# Arguments for build-time configuration
|
||||
ARG NEXT_PUBLIC_BASE_URL
|
||||
ARG NEXT_PUBLIC_TARGET
|
||||
ARG DIRECTUS_URL
|
||||
ARG UMAMI_API_ENDPOINT
|
||||
ARG NPM_TOKEN
|
||||
|
||||
# Environment variables for Next.js build
|
||||
ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
|
||||
ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET
|
||||
ENV DIRECTUS_URL=$DIRECTUS_URL
|
||||
ENV UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
|
||||
ENV SKIP_RUNTIME_ENV_VALIDATION=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";
|
||||
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";
|
||||
|
||||
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":
|
||||
CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1,
|
||||
};
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
CodeSnippet,
|
||||
AbstractCircuit,
|
||||
} from "@/src/components/Effects";
|
||||
import { getImgproxyUrl } from "@/src/utils/imgproxy";
|
||||
|
||||
import { Marker } from "@/src/components/Marker";
|
||||
|
||||
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="w-full h-full rounded-full overflow-hidden relative aspect-square">
|
||||
<img
|
||||
src={getImgproxyUrl("/marc-mintel.png", {
|
||||
width: 400,
|
||||
height: 400,
|
||||
resizing_type: "fill",
|
||||
gravity: "sm",
|
||||
})}
|
||||
src="/marc-mintel.png"
|
||||
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"
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as React from "react";
|
||||
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 { BlogPostHeader } from "@/src/components/blog/BlogPostHeader";
|
||||
import { Section } from "@/src/components/Section";
|
||||
@@ -9,6 +11,8 @@ import { BlogPostClient } from "@/src/components/BlogPostClient";
|
||||
import { TextSelectionShare } from "@/src/components/TextSelectionShare";
|
||||
import { BlogPostStickyBar } from "@/src/components/blog/BlogPostStickyBar";
|
||||
import { MDXContent } from "@/src/components/MDXContent";
|
||||
import { PayloadRichText } from "@/src/components/PayloadRichText";
|
||||
import { TableOfContents } from "@/src/components/TableOfContents";
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const allPosts = await getAllPosts();
|
||||
@@ -54,6 +58,18 @@ export default async function BlogPostPage({
|
||||
const post = allPosts.find((p) => p.slug === slug);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -102,7 +118,12 @@ export default async function BlogPostPage({
|
||||
)}
|
||||
|
||||
<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>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
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,17 +1,15 @@
|
||||
import withMintelConfig from "@mintel/next-config";
|
||||
import { withPayload } from '@payloadcms/next/withPayload';
|
||||
|
||||
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} */
|
||||
const nextConfig = {
|
||||
pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
|
||||
reactStrictMode: true,
|
||||
output: 'standalone',
|
||||
images: {
|
||||
loader: 'custom',
|
||||
loaderFile: './src/utils/imgproxy-loader.ts',
|
||||
},
|
||||
serverExternalPackages: ['@mintel/content-engine'],
|
||||
async rewrites() {
|
||||
return [
|
||||
// Umami proxy rewrite handled in app/stats/api/send/route.ts
|
||||
@@ -27,6 +25,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({
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
"version": "0.1.0",
|
||||
"description": "Technical problem solver's blog - practical insights and learning notes",
|
||||
"scripts": {
|
||||
"dev": "rm -rf .next && next dev",
|
||||
"dev": "pnpm run seed:context && next dev --turbo",
|
||||
"dev:native": "pnpm run seed:context && DATABASE_URI=postgres://directus:directus@127.0.0.1:54321/directus PAYLOAD_SECRET=dev-secret next dev --webpack",
|
||||
"seed:context": "tsx ./seed-context.ts",
|
||||
"build": "next build --webpack",
|
||||
"start": "next start",
|
||||
"lint": "eslint app src scripts video",
|
||||
@@ -40,6 +42,7 @@
|
||||
"@payloadcms/email-nodemailer": "^3.77.0",
|
||||
"@payloadcms/next": "^3.77.0",
|
||||
"@payloadcms/richtext-lexical": "^3.77.0",
|
||||
"@payloadcms/ui": "^3.77.0",
|
||||
"@react-pdf/renderer": "^4.3.2",
|
||||
"@remotion/bundler": "^4.0.414",
|
||||
"@remotion/cli": "^4.0.414",
|
||||
|
||||
@@ -70,6 +70,9 @@ export interface Config {
|
||||
users: User;
|
||||
media: Media;
|
||||
posts: Post;
|
||||
inquiries: Inquiry;
|
||||
redirects: Redirect;
|
||||
"context-files": ContextFile;
|
||||
"payload-kv": PayloadKv;
|
||||
"payload-locked-documents": PayloadLockedDocument;
|
||||
"payload-preferences": PayloadPreference;
|
||||
@@ -80,6 +83,9 @@ export interface Config {
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
media: MediaSelect<false> | MediaSelect<true>;
|
||||
posts: PostsSelect<false> | PostsSelect<true>;
|
||||
inquiries: InquiriesSelect<false> | InquiriesSelect<true>;
|
||||
redirects: RedirectsSelect<false> | RedirectsSelect<true>;
|
||||
"context-files": ContextFilesSelect<false> | ContextFilesSelect<true>;
|
||||
"payload-kv": PayloadKvSelect<false> | PayloadKvSelect<true>;
|
||||
"payload-locked-documents":
|
||||
| PayloadLockedDocumentsSelect<false>
|
||||
@@ -95,8 +101,12 @@ export interface Config {
|
||||
defaultIDType: number;
|
||||
};
|
||||
fallbackLocale: null;
|
||||
globals: {};
|
||||
globalsSelect: {};
|
||||
globals: {
|
||||
"ai-settings": AiSetting;
|
||||
};
|
||||
globalsSelect: {
|
||||
"ai-settings": AiSettingsSelect<false> | AiSettingsSelect<true>;
|
||||
};
|
||||
locale: null;
|
||||
user: User;
|
||||
jobs: {
|
||||
@@ -201,12 +211,99 @@ export interface Post {
|
||||
title: string;
|
||||
slug: 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;
|
||||
tags: {
|
||||
/**
|
||||
* Kategorisiere diesen Post mit einem eindeutigen Tag
|
||||
*/
|
||||
tag?: 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;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -246,6 +343,18 @@ export interface PayloadLockedDocument {
|
||||
| ({
|
||||
relationTo: "posts";
|
||||
value: number | Post;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: "inquiries";
|
||||
value: number | Inquiry;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: "redirects";
|
||||
value: number | Redirect;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: "context-files";
|
||||
value: number | ContextFile;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
@@ -378,7 +487,43 @@ export interface PostsSelect<T extends boolean = true> {
|
||||
tag?: 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;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
@@ -423,6 +568,39 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||
updatedAt?: 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
|
||||
* via the `definition` "auth".
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { buildConfig } from "payload";
|
||||
// Triggering config re-analysis for blocks visibility - V4
|
||||
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 path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
@@ -9,6 +11,11 @@ import sharp from "sharp";
|
||||
import { Users } from "./src/payload/collections/Users";
|
||||
import { Media } from "./src/payload/collections/Media";
|
||||
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 dirname = path.dirname(filename);
|
||||
@@ -20,7 +27,8 @@ export default buildConfig({
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
},
|
||||
collections: [Users, Media, Posts],
|
||||
collections: [Users, Media, Posts, Inquiries, Redirects, ContextFiles],
|
||||
globals: [AiSettings],
|
||||
...(process.env.MAIL_HOST
|
||||
? {
|
||||
email: nodemailerAdapter({
|
||||
@@ -37,7 +45,14 @@ export default buildConfig({
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
editor: lexicalEditor(),
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
BlocksFeature({
|
||||
blocks: payloadBlocks,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
secret: process.env.PAYLOAD_SECRET || "fallback-secret-for-dev",
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, "payload-types.ts"),
|
||||
|
||||
|
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 fs from "fs";
|
||||
import path from "path";
|
||||
import { parseMarkdownToLexical } from "../src/payload/utils/lexicalParser";
|
||||
|
||||
function parseMatter(content: string) {
|
||||
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
||||
@@ -39,29 +40,112 @@ async function run() {
|
||||
const slug = file.replace(/\.mdx$/, "");
|
||||
console.log(`Migrating ${slug}...`);
|
||||
|
||||
const existing = await payload.find({
|
||||
collection: "posts",
|
||||
where: { slug: { equals: slug } },
|
||||
});
|
||||
|
||||
if (existing.docs.length === 0) {
|
||||
await payload.create({
|
||||
try {
|
||||
const existing = await payload.find({
|
||||
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 })),
|
||||
thumbnail: data.thumbnail || "",
|
||||
content: body,
|
||||
},
|
||||
where: { slug: { equals: slug } },
|
||||
});
|
||||
console.log(`✔ Inserted ${slug}`);
|
||||
} else {
|
||||
console.log(`⚠ Skipped ${slug} (already exists)`);
|
||||
|
||||
const lexicalBlocks = parseMarkdownToLexical(body);
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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";
|
||||
|
||||
const dsn = process.env.SENTRY_DSN;
|
||||
const isProd = process.env.NODE_ENV === "production";
|
||||
|
||||
Sentry.init({
|
||||
dsn,
|
||||
enabled: Boolean(dsn),
|
||||
enabled: isProd && Boolean(dsn),
|
||||
tracesSampleRate: 1,
|
||||
debug: false,
|
||||
});
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
const dsn = process.env.SENTRY_DSN;
|
||||
const isProd = process.env.NODE_ENV === "production";
|
||||
|
||||
Sentry.init({
|
||||
dsn,
|
||||
enabled: Boolean(dsn),
|
||||
enabled: isProd && Boolean(dsn),
|
||||
|
||||
tracesSampleRate: 1,
|
||||
debug: false,
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
getInquiryEmailHtml,
|
||||
getConfirmationEmailHtml,
|
||||
} from "../components/ContactForm/EmailTemplates";
|
||||
import { getPayload } from "payload";
|
||||
import configPromise from "@payload-config";
|
||||
|
||||
export async function sendContactInquiry(data: {
|
||||
name: string;
|
||||
@@ -16,7 +18,22 @@ export async function sendContactInquiry(data: {
|
||||
config?: any;
|
||||
}) {
|
||||
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({
|
||||
subject: `[PROJEKT] ${data.isFreeText ? "DIREKTANFRAGE" : "KONFIGURATION"}: ${data.companyName || data.name}`,
|
||||
html: getInquiryEmailHtml(data),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
|
||||
import * as React from "react";
|
||||
import { cn } from "../utils/cn";
|
||||
import { ShieldCheck, ArrowLeft, ArrowRight, RefreshCw } from "lucide-react";
|
||||
@@ -31,8 +30,6 @@ interface IframeSectionProps {
|
||||
desktopHeight?: string;
|
||||
}
|
||||
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
BrowserChrome.displayName = "BrowserChrome";
|
||||
@@ -212,7 +204,7 @@ export const IframeSection: React.FC<IframeSectionProps> = ({
|
||||
const isScrollable = doc.scrollHeight > doc.clientHeight + 10;
|
||||
setScrollState({ atTop, atBottom, isScrollable });
|
||||
}
|
||||
} catch (_e) { }
|
||||
} catch (_e) {}
|
||||
}, []);
|
||||
|
||||
// Ambilight effect (sampled from iframe if same-origin)
|
||||
@@ -257,7 +249,7 @@ export const IframeSection: React.FC<IframeSectionProps> = ({
|
||||
);
|
||||
|
||||
updateScrollState();
|
||||
} catch (_e) { }
|
||||
} catch (_e) {}
|
||||
}, [dynamicGlow, offsetY, updateScrollState]);
|
||||
|
||||
// Height parse helper
|
||||
@@ -376,9 +368,9 @@ export const IframeSection: React.FC<IframeSectionProps> = ({
|
||||
"w-full relative flex flex-col z-10",
|
||||
minimal ? "bg-transparent" : "bg-slate-50",
|
||||
!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 &&
|
||||
"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",
|
||||
)}
|
||||
style={chassisStyle}
|
||||
|
||||
@@ -1,256 +1,355 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { ComponentShareButton } from './ComponentShareButton';
|
||||
import { Reveal } from './Reveal';
|
||||
import React from "react";
|
||||
import { ComponentShareButton } from "./ComponentShareButton";
|
||||
import { Reveal } from "./Reveal";
|
||||
|
||||
interface MemeCardProps {
|
||||
/** Meme template type: drake, ds (daily struggle), gru, fine, clown, expanding, distracted, rollsafe */
|
||||
template: string;
|
||||
/** Pipe-delimited captions */
|
||||
captions: string;
|
||||
/** Optional local image path. If provided, overrides the text-based template. */
|
||||
image?: string;
|
||||
className?: string;
|
||||
/** Meme template type: drake, ds (daily struggle), gru, fine, clown, expanding, distracted, rollsafe */
|
||||
template: string;
|
||||
/** Pipe-delimited captions */
|
||||
captions: string;
|
||||
/** Optional local image path. If provided, overrides the text-based template. */
|
||||
image?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Premium text-based meme cards with dedicated layouts per template.
|
||||
* Uses emoji + typography instead of images for on-brand aesthetics.
|
||||
*/
|
||||
export const MemeCard: React.FC<MemeCardProps> = ({ template, captions, image, className = '' }) => {
|
||||
const captionList = (captions || '').split('|').map(s => s.trim()).filter(Boolean);
|
||||
const shareId = `meme-${Math.random().toString(36).substring(7).toUpperCase()}`;
|
||||
|
||||
if (image) {
|
||||
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>
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
export const MemeCard: React.FC<MemeCardProps> = ({
|
||||
template,
|
||||
captions,
|
||||
image,
|
||||
className = "",
|
||||
}) => {
|
||||
// Also replace literal `\n` (slash-n) strings from AI output with actual newlines
|
||||
const processedCaptions = (captions || "").replace(/\\n/g, "\n");
|
||||
const captionList = processedCaptions
|
||||
.split("|")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
const shareId = `meme-${Math.random().toString(36).substring(7).toUpperCase()}`;
|
||||
|
||||
if (image) {
|
||||
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" />
|
||||
<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
|
||||
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>
|
||||
</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[] }) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<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">
|
||||
<span className="text-3xl md:text-4xl select-none grayscale-0 group-hover:scale-110 transition-transform duration-500">🙅</span>
|
||||
</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">{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>
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<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">
|
||||
<span className="text-3xl md:text-4xl select-none grayscale-0 group-hover:scale-110 transition-transform duration-500">
|
||||
🙅
|
||||
</span>
|
||||
</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[] }) {
|
||||
return (
|
||||
<div className="p-8 md:p-10 text-center">
|
||||
<div className="text-4xl md:text-5xl mb-6 select-none animate-bounce-subtle">😰</div>
|
||||
<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 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[0]}</p>
|
||||
</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>
|
||||
return (
|
||||
<div className="p-8 md:p-10 text-center">
|
||||
<div className="text-4xl md:text-5xl mb-6 select-none animate-bounce-subtle">
|
||||
😰
|
||||
</div>
|
||||
<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 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[0]}
|
||||
</p>
|
||||
</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[] }) {
|
||||
const steps = captions.slice(0, 4);
|
||||
return (
|
||||
<div className="grid grid-cols-2 grid-rows-2">
|
||||
{(steps || []).map((caption, i) => {
|
||||
const isLast = i >= 2;
|
||||
return (
|
||||
<div
|
||||
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`}
|
||||
>
|
||||
<span className="text-2xl md:text-3xl select-none transition-transform group-hover:scale-110">
|
||||
{isLast ? '😱' : '😏'}
|
||||
</span>
|
||||
<p className={`text-base md:text-lg leading-tight ${isLast ? 'font-black text-red-500' : 'font-bold text-slate-700'}`}>
|
||||
{caption}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
const steps = captions.slice(0, 4);
|
||||
return (
|
||||
<div className="grid grid-cols-2 grid-rows-2">
|
||||
{(steps || []).map((caption, i) => {
|
||||
const isLast = i >= 2;
|
||||
return (
|
||||
<div
|
||||
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`}
|
||||
>
|
||||
<span className="text-2xl md:text-3xl select-none transition-transform group-hover:scale-110">
|
||||
{isLast ? "😱" : "😏"}
|
||||
</span>
|
||||
<p
|
||||
className={`text-base md:text-lg leading-tight ${isLast ? "font-black text-red-500" : "font-bold text-slate-700"}`}
|
||||
>
|
||||
{caption}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FineMeme({ captions }: { captions: string[] }) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<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">
|
||||
<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>
|
||||
</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>
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<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">
|
||||
<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>
|
||||
</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[] }) {
|
||||
const steps = captions.slice(0, 4);
|
||||
const emojis = ['😐', '🤡', '💀', '🎪'];
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<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>
|
||||
</div>
|
||||
{steps.map((caption, i) => (
|
||||
<div
|
||||
key={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`}
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
))}
|
||||
const steps = captions.slice(0, 4);
|
||||
const emojis = ["😐", "🤡", "💀", "🎪"];
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<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>
|
||||
</div>
|
||||
{steps.map((caption, i) => (
|
||||
<div
|
||||
key={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`}
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
function ExpandingBrainMeme({ captions }: { captions: string[] }) {
|
||||
const steps = captions.slice(0, 4);
|
||||
const emojis = ['🧠', '🧠✨', '🧠💡', '🧠🚀'];
|
||||
const shadows = [
|
||||
'',
|
||||
'shadow-[0_0_15px_rgba(59,130,246,0.1)]',
|
||||
'shadow-[0_0_20px_rgba(99,102,241,0.2)]',
|
||||
'shadow-[0_0_25px_rgba(168,85,247,0.3)]',
|
||||
];
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<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>
|
||||
</div>
|
||||
{steps.map((caption, i) => (
|
||||
<div
|
||||
key={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]}`}
|
||||
>
|
||||
<span className="text-2xl md:text-3xl select-none flex-shrink-0 group-hover:scale-125 transition-transform duration-700">{emojis[i]}</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>
|
||||
))}
|
||||
const steps = captions.slice(0, 4);
|
||||
const emojis = ["🧠", "🧠✨", "🧠💡", "🧠🚀"];
|
||||
const shadows = [
|
||||
"",
|
||||
"shadow-[0_0_15px_rgba(59,130,246,0.1)]",
|
||||
"shadow-[0_0_20px_rgba(99,102,241,0.2)]",
|
||||
"shadow-[0_0_25px_rgba(168,85,247,0.3)]",
|
||||
];
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<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>
|
||||
</div>
|
||||
{steps.map((caption, i) => (
|
||||
<div
|
||||
key={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]}`}
|
||||
>
|
||||
<span className="text-2xl md:text-3xl select-none flex-shrink-0 group-hover:scale-125 transition-transform duration-700">
|
||||
{emojis[i]}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
function DistractedMeme({ captions }: { captions: string[] }) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<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>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 divide-x divide-slate-100">
|
||||
<div className="p-6 md:p-8 flex flex-col items-center text-center gap-3 hover:bg-slate-50/50 transition-colors">
|
||||
<span className="text-3xl md:text-4xl select-none">👤</span>
|
||||
<p className="text-[9px] font-black text-slate-400 uppercase tracking-[0.2em] m-0">Subject</p>
|
||||
<p className="text-sm md:text-base font-bold text-slate-500 leading-tight">{captions[0]}</p>
|
||||
</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>
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<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>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 divide-x divide-slate-100">
|
||||
<div className="p-6 md:p-8 flex flex-col items-center text-center gap-3 hover:bg-slate-50/50 transition-colors">
|
||||
<span className="text-3xl md:text-4xl select-none">👤</span>
|
||||
<p className="text-[9px] font-black text-slate-400 uppercase tracking-[0.2em] m-0">
|
||||
Subject
|
||||
</p>
|
||||
<p className="text-sm md:text-base font-bold text-slate-500 leading-tight">
|
||||
{captions[0]}
|
||||
</p>
|
||||
</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 }) {
|
||||
return (
|
||||
<div className="p-8 md:p-12 text-center bg-gradient-to-br from-white to-slate-50/50">
|
||||
<p className="text-[10px] font-black text-slate-400 uppercase tracking-[0.3em] mb-8">{template}</p>
|
||||
<div className="space-y-4">
|
||||
{(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>
|
||||
);
|
||||
function GenericMeme({
|
||||
captions,
|
||||
template,
|
||||
}: {
|
||||
captions: string[];
|
||||
template: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="p-8 md:p-12 text-center bg-gradient-to-br from-white to-slate-50/50">
|
||||
<p className="text-[10px] font-black text-slate-400 uppercase tracking-[0.3em] mb-8">
|
||||
{template}
|
||||
</p>
|
||||
<div className="space-y-4">
|
||||
{(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";
|
||||
|
||||
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 { 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 { PremiumComparisonChart } from "../components/PremiumComparisonChart";
|
||||
import { ImageText } from "../components/ImageText";
|
||||
import { Carousel } from "../components/Carousel";
|
||||
|
||||
import { PremiumComparisonChart } from '../components/PremiumComparisonChart';
|
||||
import { ImageText } from '../components/ImageText';
|
||||
import { Carousel } from '../components/Carousel';
|
||||
|
||||
import { Section } from '../components/Section';
|
||||
import { Reveal } from '../components/Reveal';
|
||||
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 { PerformanceChart } from "../components/PerformanceChart";
|
||||
@@ -43,56 +42,61 @@ import { LoadTimeSimulator } from "../components/simulations/LoadTimeSimulator";
|
||||
import { ArchitectureBuilder } from "../components/simulations/ArchitectureBuilder";
|
||||
import { DigitalAssetVisualizer } from "../components/simulations/DigitalAssetVisualizer";
|
||||
|
||||
import { TwitterEmbed } from '../components/TwitterEmbed';
|
||||
import { YouTubeEmbed } from '../components/YouTubeEmbed';
|
||||
import { LinkedInEmbed } from '../components/LinkedInEmbed';
|
||||
import { TwitterEmbed } from "../components/TwitterEmbed";
|
||||
import { YouTubeEmbed } from "../components/YouTubeEmbed";
|
||||
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 = {
|
||||
// Named exports for explicit MDX usage
|
||||
LeadParagraph,
|
||||
H1,
|
||||
H2,
|
||||
H3,
|
||||
Paragraph,
|
||||
ArticleBlockquote,
|
||||
Marker,
|
||||
ComparisonRow,
|
||||
StatsDisplay,
|
||||
Mermaid,
|
||||
DiagramState,
|
||||
DiagramTimeline,
|
||||
DiagramGantt,
|
||||
DiagramPie,
|
||||
DiagramSequence,
|
||||
DiagramFlow,
|
||||
IconList,
|
||||
IconListItem,
|
||||
ArticleMeme,
|
||||
MemeCard,
|
||||
ExternalLink,
|
||||
StatsGrid,
|
||||
MetricBar,
|
||||
ArticleQuote,
|
||||
BoldNumber,
|
||||
WebVitalsScore,
|
||||
WaterfallChart,
|
||||
PremiumComparisonChart,
|
||||
ImageText,
|
||||
Carousel,
|
||||
Section,
|
||||
Reveal,
|
||||
TableOfContents,
|
||||
RevenueLossCalculator,
|
||||
PerformanceChart,
|
||||
PerformanceROICalculator,
|
||||
LoadTimeSimulator,
|
||||
ArchitectureBuilder,
|
||||
DigitalAssetVisualizer,
|
||||
TwitterEmbed,
|
||||
YouTubeEmbed,
|
||||
LinkedInEmbed,
|
||||
Button,
|
||||
LeadMagnet,
|
||||
TrackedLink,
|
||||
FAQSection
|
||||
LeadParagraph,
|
||||
H1,
|
||||
H2,
|
||||
H3,
|
||||
Paragraph,
|
||||
ArticleBlockquote,
|
||||
Marker,
|
||||
ComparisonRow,
|
||||
StatsDisplay,
|
||||
Mermaid,
|
||||
DiagramState,
|
||||
DiagramTimeline,
|
||||
DiagramGantt,
|
||||
DiagramPie,
|
||||
DiagramSequence,
|
||||
DiagramFlow,
|
||||
IconList,
|
||||
IconListItem,
|
||||
ArticleMeme,
|
||||
MemeCard,
|
||||
ExternalLink,
|
||||
StatsGrid,
|
||||
MetricBar,
|
||||
ArticleQuote,
|
||||
BoldNumber,
|
||||
WebVitalsScore,
|
||||
WaterfallChart,
|
||||
PremiumComparisonChart,
|
||||
ImageText,
|
||||
Carousel,
|
||||
Section,
|
||||
Reveal,
|
||||
TableOfContents,
|
||||
RevenueLossCalculator,
|
||||
PerformanceChart,
|
||||
PerformanceROICalculator,
|
||||
LoadTimeSimulator,
|
||||
ArchitectureBuilder,
|
||||
DigitalAssetVisualizer,
|
||||
TwitterEmbed,
|
||||
YouTubeEmbed,
|
||||
LinkedInEmbed,
|
||||
Button,
|
||||
LeadMagnet,
|
||||
TrackedLink,
|
||||
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.
|
||||
* Used by:
|
||||
* - 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)
|
||||
* Now dynamically generated from individual Payload block definitions.
|
||||
*/
|
||||
export const componentDefinitions: ComponentDefinition[] = [
|
||||
{
|
||||
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 />'
|
||||
}
|
||||
];
|
||||
export const componentDefinitions: ComponentDefinition[] =
|
||||
allComponentDefinitions;
|
||||
|
||||
@@ -9,22 +9,50 @@ export async function getAllPosts() {
|
||||
return [];
|
||||
}
|
||||
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
const { docs } = await payload.find({
|
||||
collection: "posts",
|
||||
limit: 1000,
|
||||
sort: "-date",
|
||||
});
|
||||
try {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
const { docs } = await payload.find({
|
||||
collection: "posts",
|
||||
limit: 1000,
|
||||
sort: "-date",
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
_status: {
|
||||
equals: "published",
|
||||
},
|
||||
},
|
||||
{
|
||||
date: {
|
||||
less_than_equal: new Date(),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
return docs.map((doc) => ({
|
||||
title: doc.title as string,
|
||||
description: doc.description as string,
|
||||
date: doc.date as string,
|
||||
tags: (doc.tags || []).map((t) =>
|
||||
typeof t === "object" && t !== null ? t.tag : t,
|
||||
) as string[],
|
||||
slug: doc.slug as string,
|
||||
thumbnail: doc.thumbnail as string,
|
||||
body: { code: doc.content as string },
|
||||
}));
|
||||
return docs.map((doc) => ({
|
||||
title: doc.title as string,
|
||||
description: doc.description as string,
|
||||
date: doc.date as string,
|
||||
tags: (doc.tags || []).map((t) =>
|
||||
typeof t === "object" && t !== null ? t.tag : t,
|
||||
) as string[],
|
||||
slug: doc.slug as string,
|
||||
thumbnail:
|
||||
(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." },
|
||||
},
|
||||
],
|
||||
};
|
||||
73
apps/web/src/payload/blocks/WaterfallChartBlock.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { MintelBlock } from "./types";
|
||||
|
||||
import type { Block } from "payload";
|
||||
|
||||
export const WaterfallChartBlock: MintelBlock = {
|
||||
slug: "waterfallChart",
|
||||
labels: {
|
||||
singular: "Waterfall Chart",
|
||||
plural: "Waterfall Charts",
|
||||
},
|
||||
admin: {
|
||||
group: "MDX Components",
|
||||
},
|
||||
ai: {
|
||||
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`,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "title",
|
||||
type: "text",
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für title ein.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "metrics",
|
||||
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: "duration",
|
||||
type: "number",
|
||||
required: true,
|
||||
admin: {
|
||||
description: "Tragen Sie einen numerischen Wert für duration ein.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color",
|
||||
type: "text",
|
||||
admin: {
|
||||
components: { Field: "@/src/payload/components/ColorPicker" },
|
||||
description: "Geben Sie den Text für color ein.",
|
||||
},
|
||||
},
|
||||
],
|
||||
admin: { description: "Fügen Sie Elemente zur Liste metrics hinzu." },
|
||||
},
|
||||
],
|
||||
};
|
||||
52
apps/web/src/payload/blocks/WebVitalsScoreBlock.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { MintelBlock } from "./types";
|
||||
|
||||
import type { Block } from "payload";
|
||||
|
||||
export const WebVitalsScoreBlock: MintelBlock = {
|
||||
slug: "webVitalsScore",
|
||||
labels: {
|
||||
singular: "Web Vitals Score",
|
||||
plural: "Web Vitals Scores",
|
||||
},
|
||||
admin: {
|
||||
group: "MDX Components",
|
||||
},
|
||||
ai: {
|
||||
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",
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "lcp",
|
||||
type: "number",
|
||||
required: true,
|
||||
admin: { description: "Largest Contentful Paint (s)" },
|
||||
},
|
||||
{
|
||||
name: "inp",
|
||||
type: "number",
|
||||
required: true,
|
||||
admin: { description: "Interaction to Next Paint (ms)" },
|
||||
},
|
||||
{
|
||||
name: "cls",
|
||||
type: "number",
|
||||
required: true,
|
||||
admin: { description: "Cumulative Layout Shift" },
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
type: "text",
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für description ein.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
41
apps/web/src/payload/blocks/YouTubeEmbedBlock.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { MintelBlock } from "./types";
|
||||
|
||||
import type { Block } from "payload";
|
||||
|
||||
export const YouTubeEmbedBlock: MintelBlock = {
|
||||
slug: "youTubeEmbed",
|
||||
labels: {
|
||||
singular: "You Tube Embed",
|
||||
plural: "You Tube Embeds",
|
||||
},
|
||||
admin: {
|
||||
group: "MDX Components",
|
||||
},
|
||||
ai: {
|
||||
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" />\'',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "videoId",
|
||||
type: "text",
|
||||
required: true,
|
||||
admin: { description: "Geben Sie den Text für videoId ein." },
|
||||
},
|
||||
{
|
||||
name: "title",
|
||||
type: "text",
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für title ein.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
104
apps/web/src/payload/blocks/allBlocks.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { MintelBlock } from "./types";
|
||||
import { MemeCardBlock } from "./MemeCardBlock";
|
||||
import { MermaidBlock } from "./MermaidBlock";
|
||||
import { LeadMagnetBlock } from "./LeadMagnetBlock";
|
||||
import { ComparisonRowBlock } from "./ComparisonRowBlock";
|
||||
import { LeadParagraphBlock } from "./LeadParagraphBlock";
|
||||
import { ArticleBlockquoteBlock } from "./ArticleBlockquoteBlock";
|
||||
import { FAQSectionBlock } from "./FAQSectionBlock";
|
||||
import { StatsDisplayBlock } from "./StatsDisplayBlock";
|
||||
import { DiagramStateBlock } from "./DiagramStateBlock";
|
||||
import { DiagramTimelineBlock } from "./DiagramTimelineBlock";
|
||||
import { DiagramGanttBlock } from "./DiagramGanttBlock";
|
||||
import { DiagramPieBlock } from "./DiagramPieBlock";
|
||||
import { DiagramSequenceBlock } from "./DiagramSequenceBlock";
|
||||
import { DiagramFlowBlock } from "./DiagramFlowBlock";
|
||||
import { WaterfallChartBlock } from "./WaterfallChartBlock";
|
||||
import { PremiumComparisonChartBlock } from "./PremiumComparisonChartBlock";
|
||||
import { IconListBlock } from "./IconListBlock";
|
||||
import { StatsGridBlock } from "./StatsGridBlock";
|
||||
import { MetricBarBlock } from "./MetricBarBlock";
|
||||
import { CarouselBlock } from "./CarouselBlock";
|
||||
import { ImageTextBlock } from "./ImageTextBlock";
|
||||
import { RevenueLossCalculatorBlock } from "./RevenueLossCalculatorBlock";
|
||||
import { PerformanceChartBlock } from "./PerformanceChartBlock";
|
||||
import { PerformanceROICalculatorBlock } from "./PerformanceROICalculatorBlock";
|
||||
import { LoadTimeSimulatorBlock } from "./LoadTimeSimulatorBlock";
|
||||
import { ArchitectureBuilderBlock } from "./ArchitectureBuilderBlock";
|
||||
import { DigitalAssetVisualizerBlock } from "./DigitalAssetVisualizerBlock";
|
||||
import { TwitterEmbedBlock } from "./TwitterEmbedBlock";
|
||||
import { YouTubeEmbedBlock } from "./YouTubeEmbedBlock";
|
||||
import { LinkedInEmbedBlock } from "./LinkedInEmbedBlock";
|
||||
import { ExternalLinkBlock } from "./ExternalLinkBlock";
|
||||
import { TrackedLinkBlock } from "./TrackedLinkBlock";
|
||||
import { ArticleMemeBlock } from "./ArticleMemeBlock";
|
||||
import { MarkerBlock } from "./MarkerBlock";
|
||||
import { BoldNumberBlock } from "./BoldNumberBlock";
|
||||
import { WebVitalsScoreBlock } from "./WebVitalsScoreBlock";
|
||||
import { ButtonBlock } from "./ButtonBlock";
|
||||
import { ArticleQuoteBlock } from "./ArticleQuoteBlock";
|
||||
import { RevealBlock } from "./RevealBlock";
|
||||
import { SectionBlock } from "./SectionBlock";
|
||||
import { TLDRBlock } from "./TLDRBlock";
|
||||
import { HeadingBlock } from "./HeadingBlock";
|
||||
import { ParagraphBlock } from "./ParagraphBlock";
|
||||
import { H2Block } from "./H2Block";
|
||||
import { H3Block } from "./H3Block";
|
||||
|
||||
export const allBlocks: MintelBlock[] = [
|
||||
TLDRBlock,
|
||||
HeadingBlock,
|
||||
H2Block,
|
||||
H3Block,
|
||||
ParagraphBlock,
|
||||
MemeCardBlock,
|
||||
MermaidBlock,
|
||||
LeadMagnetBlock,
|
||||
ComparisonRowBlock,
|
||||
LeadParagraphBlock,
|
||||
ArticleBlockquoteBlock,
|
||||
FAQSectionBlock,
|
||||
StatsDisplayBlock,
|
||||
DiagramStateBlock,
|
||||
DiagramTimelineBlock,
|
||||
DiagramGanttBlock,
|
||||
DiagramPieBlock,
|
||||
DiagramSequenceBlock,
|
||||
DiagramFlowBlock,
|
||||
WaterfallChartBlock,
|
||||
PremiumComparisonChartBlock,
|
||||
IconListBlock,
|
||||
StatsGridBlock,
|
||||
MetricBarBlock,
|
||||
CarouselBlock,
|
||||
ImageTextBlock,
|
||||
RevenueLossCalculatorBlock,
|
||||
PerformanceChartBlock,
|
||||
PerformanceROICalculatorBlock,
|
||||
LoadTimeSimulatorBlock,
|
||||
ArchitectureBuilderBlock,
|
||||
DigitalAssetVisualizerBlock,
|
||||
TwitterEmbedBlock,
|
||||
YouTubeEmbedBlock,
|
||||
LinkedInEmbedBlock,
|
||||
ExternalLinkBlock,
|
||||
TrackedLinkBlock,
|
||||
ArticleMemeBlock,
|
||||
MarkerBlock,
|
||||
BoldNumberBlock,
|
||||
WebVitalsScoreBlock,
|
||||
ButtonBlock,
|
||||
ArticleQuoteBlock,
|
||||
RevealBlock,
|
||||
SectionBlock,
|
||||
];
|
||||
|
||||
/**
|
||||
* Payload 3.x silently drops blocks containing unknown properties.
|
||||
* We strip `ai` and `render` so Payload gets clean Block objects.
|
||||
*/
|
||||
export const payloadBlocks = allBlocks.map(({ ai, render, ...block }) => block);
|
||||
|
||||
export const allComponentDefinitions = allBlocks
|
||||
.filter((block) => !!block.ai)
|
||||
.map((block) => block.ai!);
|
||||