Compare commits
4 Commits
v1.8.20
...
4a5017124a
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a5017124a | |||
| 283de11b11 | |||
| 31685a458b | |||
| 6864903cff |
1
.gitignore
vendored
@@ -3,6 +3,7 @@ dist/
|
|||||||
.next/
|
.next/
|
||||||
out/
|
out/
|
||||||
.contentlayer/
|
.contentlayer/
|
||||||
|
.pnpm-store
|
||||||
|
|
||||||
# generated types
|
# generated types
|
||||||
.astro/
|
.astro/
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
# Stage 1: Builder
|
# Stage 1: Builder
|
||||||
FROM registry.infra.mintel.me/mintel/nextjs:latest AS builder
|
FROM registry.infra.mintel.me/mintel/nextjs:v1.8.21 AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Arguments for build-time configuration
|
# Arguments for build-time configuration
|
||||||
ARG NEXT_PUBLIC_BASE_URL
|
ARG NEXT_PUBLIC_BASE_URL
|
||||||
ARG NEXT_PUBLIC_TARGET
|
ARG NEXT_PUBLIC_TARGET
|
||||||
ARG DIRECTUS_URL
|
|
||||||
ARG UMAMI_API_ENDPOINT
|
ARG UMAMI_API_ENDPOINT
|
||||||
ARG NPM_TOKEN
|
ARG NPM_TOKEN
|
||||||
|
|
||||||
# Environment variables for Next.js build
|
# Environment variables for Next.js build
|
||||||
ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
|
ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
|
||||||
ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET
|
ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET
|
||||||
ENV DIRECTUS_URL=$DIRECTUS_URL
|
|
||||||
ENV UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
|
ENV UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
|
||||||
ENV SKIP_RUNTIME_ENV_VALIDATION=true
|
ENV SKIP_RUNTIME_ENV_VALIDATION=true
|
||||||
ENV CI=true
|
ENV CI=true
|
||||||
|
|||||||
20
Dockerfile.dev
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
# Install essential build tools if needed (e.g., for node-gyp)
|
||||||
|
RUN apk add --no-cache libc6-compat python3 make g++
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Enable corepack for pnpm
|
||||||
|
RUN corepack enable
|
||||||
|
|
||||||
|
# Pre-set the pnpm store directory to a location we can volume-mount
|
||||||
|
ENV PNPM_HOME="/pnpm"
|
||||||
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
|
|
||||||
|
# Set up pnpm store configuration
|
||||||
|
RUN pnpm config set store-dir /pnpm/store
|
||||||
|
|
||||||
|
# Note: Dependency installation happens at runtime to support linked packages
|
||||||
|
# and named volumes, but the base image is now optimized for the stack.
|
||||||
|
EXPOSE 3000
|
||||||
95
Posts.ts.tmp
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import type { CollectionConfig } from "payload";
|
||||||
|
import { lexicalEditor, BlocksFeature } from "@payloadcms/richtext-lexical";
|
||||||
|
import { allBlocks } from "../blocks/allBlocks";
|
||||||
|
|
||||||
|
export const Posts: CollectionConfig = {
|
||||||
|
slug: "posts",
|
||||||
|
admin: {
|
||||||
|
useAsTitle: "title",
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
read: () => true, // Publicly readable API
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "aiOptimizer",
|
||||||
|
type: "ui",
|
||||||
|
admin: {
|
||||||
|
position: "sidebar",
|
||||||
|
components: {
|
||||||
|
Field: "@/src/payload/components/OptimizeButton#OptimizeButton",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "title",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "slug",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
admin: {
|
||||||
|
position: "sidebar",
|
||||||
|
},
|
||||||
|
hooks: {
|
||||||
|
beforeValidate: [
|
||||||
|
({ value, data }) => {
|
||||||
|
if (value) return value;
|
||||||
|
if (data?.title) {
|
||||||
|
return data.title
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ /g, "-")
|
||||||
|
.replace(/[^\w-]+/g, "");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "description",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "date",
|
||||||
|
type: "date",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tags",
|
||||||
|
type: "array",
|
||||||
|
required: true,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "tag",
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "featuredImage",
|
||||||
|
type: "upload",
|
||||||
|
relationTo: "media",
|
||||||
|
admin: {
|
||||||
|
description: "The main hero image for the blog post.",
|
||||||
|
position: "sidebar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "content",
|
||||||
|
type: "richText",
|
||||||
|
editor: lexicalEditor({
|
||||||
|
features: ({ defaultFeatures }) => [
|
||||||
|
...defaultFeatures,
|
||||||
|
BlocksFeature({
|
||||||
|
blocks: allBlocks,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -1,4 +1,12 @@
|
|||||||
"use server";
|
"use server";
|
||||||
import { handleServerFunctions as payloadHandleServerFunctions } from "@payloadcms/next/layouts";
|
import { handleServerFunctions as payloadHandleServerFunctions } from "@payloadcms/next/layouts";
|
||||||
|
import config from "@payload-config";
|
||||||
|
import { importMap } from "./admin/importMap";
|
||||||
|
|
||||||
export const handleServerFunctions = payloadHandleServerFunctions;
|
export const handleServerFunctions = async (args: any) => {
|
||||||
|
return payloadHandleServerFunctions({
|
||||||
|
...args,
|
||||||
|
config,
|
||||||
|
importMap,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,99 @@
|
|||||||
|
import { OptimizeButton as OptimizeButton_a629b3460534b7aa208597fdc5e30aec } from "@/src/payload/components/OptimizeButton";
|
||||||
|
import { GenerateSlugButton as GenerateSlugButton_63aadb132a046b3f001fac7a715e5717 } from "@/src/payload/components/FieldGenerators/GenerateSlugButton";
|
||||||
|
import { default as default_76cec558bd86098fa1dab70b12eb818f } from "@/src/payload/components/TagSelector";
|
||||||
|
import { GenerateThumbnailButton as GenerateThumbnailButton_39d416c162062cbe7173a99e3239786e } from "@/src/payload/components/FieldGenerators/GenerateThumbnailButton";
|
||||||
|
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from "@payloadcms/richtext-lexical/rsc";
|
||||||
|
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from "@payloadcms/richtext-lexical/rsc";
|
||||||
|
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from "@payloadcms/richtext-lexical/rsc";
|
||||||
|
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||||
|
import { AiFieldButton as AiFieldButton_da42292f87769a8025025b774910be6d } from "@/src/payload/components/FieldGenerators/AiFieldButton";
|
||||||
|
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||||
|
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||||
|
import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||||
|
import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||||
|
import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||||
|
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||||
|
import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||||
|
import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||||
|
import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||||
|
import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||||
|
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||||
|
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||||
|
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||||
|
import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||||
|
import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||||
|
import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||||
|
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||||
|
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||||
|
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||||
|
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||||
|
import { default as default_2ebf44fdf8ebc607cf0de30cff485248 } from "@/src/payload/components/ColorPicker";
|
||||||
|
import { default as default_a1c6da8fb7dd9846a8b07123ff256d09 } from "@/src/payload/components/IconSelector";
|
||||||
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from "@payloadcms/next/rsc";
|
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from "@payloadcms/next/rsc";
|
||||||
|
|
||||||
export const importMap = {
|
export const importMap = {
|
||||||
|
"@/src/payload/components/OptimizeButton#OptimizeButton":
|
||||||
|
OptimizeButton_a629b3460534b7aa208597fdc5e30aec,
|
||||||
|
"@/src/payload/components/FieldGenerators/GenerateSlugButton#GenerateSlugButton":
|
||||||
|
GenerateSlugButton_63aadb132a046b3f001fac7a715e5717,
|
||||||
|
"@/src/payload/components/TagSelector#default":
|
||||||
|
default_76cec558bd86098fa1dab70b12eb818f,
|
||||||
|
"@/src/payload/components/FieldGenerators/GenerateThumbnailButton#GenerateThumbnailButton":
|
||||||
|
GenerateThumbnailButton_39d416c162062cbe7173a99e3239786e,
|
||||||
|
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell":
|
||||||
|
RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||||
|
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField":
|
||||||
|
RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||||
|
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent":
|
||||||
|
LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||||
|
"@payloadcms/richtext-lexical/client#BlocksFeatureClient":
|
||||||
|
BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton":
|
||||||
|
AiFieldButton_da42292f87769a8025025b774910be6d,
|
||||||
|
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient":
|
||||||
|
InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient":
|
||||||
|
HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#UploadFeatureClient":
|
||||||
|
UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#BlockquoteFeatureClient":
|
||||||
|
BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#RelationshipFeatureClient":
|
||||||
|
RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#LinkFeatureClient":
|
||||||
|
LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#ChecklistFeatureClient":
|
||||||
|
ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#OrderedListFeatureClient":
|
||||||
|
OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#UnorderedListFeatureClient":
|
||||||
|
UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#IndentFeatureClient":
|
||||||
|
IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#AlignFeatureClient":
|
||||||
|
AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#HeadingFeatureClient":
|
||||||
|
HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient":
|
||||||
|
ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#InlineCodeFeatureClient":
|
||||||
|
InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#SuperscriptFeatureClient":
|
||||||
|
SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#SubscriptFeatureClient":
|
||||||
|
SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient":
|
||||||
|
StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient":
|
||||||
|
UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#BoldFeatureClient":
|
||||||
|
BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#ItalicFeatureClient":
|
||||||
|
ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@/src/payload/components/ColorPicker#default":
|
||||||
|
default_2ebf44fdf8ebc607cf0de30cff485248,
|
||||||
|
"@/src/payload/components/IconSelector#default":
|
||||||
|
default_a1c6da8fb7dd9846a8b07123ff256d09,
|
||||||
"@payloadcms/next/rsc#CollectionCards":
|
"@payloadcms/next/rsc#CollectionCards":
|
||||||
CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1,
|
CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
CodeSnippet,
|
CodeSnippet,
|
||||||
AbstractCircuit,
|
AbstractCircuit,
|
||||||
} from "@/src/components/Effects";
|
} from "@/src/components/Effects";
|
||||||
import { getImgproxyUrl } from "@/src/utils/imgproxy";
|
|
||||||
import { Marker } from "@/src/components/Marker";
|
import { Marker } from "@/src/components/Marker";
|
||||||
|
|
||||||
export default function AboutPage() {
|
export default function AboutPage() {
|
||||||
@@ -51,12 +51,7 @@ export default function AboutPage() {
|
|||||||
<div className="relative w-32 h-32 md:w-40 md:h-40 rounded-full overflow-hidden border border-slate-200 shadow-xl bg-white p-1 group">
|
<div className="relative w-32 h-32 md:w-40 md:h-40 rounded-full overflow-hidden border border-slate-200 shadow-xl bg-white p-1 group">
|
||||||
<div className="w-full h-full rounded-full overflow-hidden relative aspect-square">
|
<div className="w-full h-full rounded-full overflow-hidden relative aspect-square">
|
||||||
<img
|
<img
|
||||||
src={getImgproxyUrl("/marc-mintel.png", {
|
src="/marc-mintel.png"
|
||||||
width: 400,
|
|
||||||
height: 400,
|
|
||||||
resizing_type: "fill",
|
|
||||||
gravity: "sm",
|
|
||||||
})}
|
|
||||||
alt="Marc Mintel"
|
alt="Marc Mintel"
|
||||||
className="object-cover grayscale transition-all duration-1000 ease-in-out scale-110 group-hover:scale-100 group-hover:grayscale-0 w-full h-full"
|
className="object-cover grayscale transition-all duration-1000 ease-in-out scale-110 group-hover:scale-100 group-hover:grayscale-0 w-full h-full"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
|
import { getPayloadHMR } from "@payloadcms/next/utilities";
|
||||||
|
import configPromise from "@payload-config";
|
||||||
import { getAllPosts } from "@/src/lib/posts";
|
import { getAllPosts } from "@/src/lib/posts";
|
||||||
import { BlogPostHeader } from "@/src/components/blog/BlogPostHeader";
|
import { BlogPostHeader } from "@/src/components/blog/BlogPostHeader";
|
||||||
import { Section } from "@/src/components/Section";
|
import { Section } from "@/src/components/Section";
|
||||||
@@ -9,6 +11,8 @@ import { BlogPostClient } from "@/src/components/BlogPostClient";
|
|||||||
import { TextSelectionShare } from "@/src/components/TextSelectionShare";
|
import { TextSelectionShare } from "@/src/components/TextSelectionShare";
|
||||||
import { BlogPostStickyBar } from "@/src/components/blog/BlogPostStickyBar";
|
import { BlogPostStickyBar } from "@/src/components/blog/BlogPostStickyBar";
|
||||||
import { MDXContent } from "@/src/components/MDXContent";
|
import { MDXContent } from "@/src/components/MDXContent";
|
||||||
|
import { PayloadRichText } from "@/src/components/PayloadRichText";
|
||||||
|
import { TableOfContents } from "@/src/components/TableOfContents";
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
const allPosts = await getAllPosts();
|
const allPosts = await getAllPosts();
|
||||||
@@ -54,6 +58,18 @@ export default async function BlogPostPage({
|
|||||||
const post = allPosts.find((p) => p.slug === slug);
|
const post = allPosts.find((p) => p.slug === slug);
|
||||||
|
|
||||||
if (!post) {
|
if (!post) {
|
||||||
|
const payload = await getPayloadHMR({ config: configPromise });
|
||||||
|
const redirectDoc = await payload.find({
|
||||||
|
collection: "redirects",
|
||||||
|
where: {
|
||||||
|
from: { equals: slug },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (redirectDoc.docs.length > 0) {
|
||||||
|
redirect(`/blog/${redirectDoc.docs[0].to}`);
|
||||||
|
}
|
||||||
|
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +118,12 @@ export default async function BlogPostPage({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="article-content max-w-none">
|
<div className="article-content max-w-none">
|
||||||
<MDXContent code={post.body.code} />
|
<TableOfContents />
|
||||||
|
{post.lexicalContent ? (
|
||||||
|
<PayloadRichText data={post.lexicalContent} />
|
||||||
|
) : (
|
||||||
|
<MDXContent code={post.body.code} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
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 withMintelConfig from "@mintel/next-config";
|
||||||
import { withPayload } from '@payloadcms/next/withPayload';
|
import { withPayload } from '@payloadcms/next/withPayload';
|
||||||
|
|
||||||
import createMDX from '@next/mdx';
|
import createMDX from '@next/mdx';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const filename = fileURLToPath(import.meta.url);
|
||||||
|
const dirname = path.dirname(filename);
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
|
serverExternalPackages: ['@mintel/content-engine'],
|
||||||
reactStrictMode: true,
|
|
||||||
output: 'standalone',
|
|
||||||
images: {
|
|
||||||
loader: 'custom',
|
|
||||||
loaderFile: './src/utils/imgproxy-loader.ts',
|
|
||||||
},
|
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
return [
|
return [
|
||||||
// Umami proxy rewrite handled in app/stats/api/send/route.ts
|
// 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({
|
const withMDX = createMDX({
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Technical problem solver's blog - practical insights and learning notes",
|
"description": "Technical problem solver's blog - practical insights and learning notes",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "rm -rf .next && next dev",
|
"dev": "pnpm run seed:context && next dev --turbo",
|
||||||
|
"dev:native": "pnpm run seed:context && DATABASE_URI=postgres://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",
|
"build": "next build --webpack",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint app src scripts video",
|
"lint": "eslint app src scripts video",
|
||||||
@@ -40,6 +42,7 @@
|
|||||||
"@payloadcms/email-nodemailer": "^3.77.0",
|
"@payloadcms/email-nodemailer": "^3.77.0",
|
||||||
"@payloadcms/next": "^3.77.0",
|
"@payloadcms/next": "^3.77.0",
|
||||||
"@payloadcms/richtext-lexical": "^3.77.0",
|
"@payloadcms/richtext-lexical": "^3.77.0",
|
||||||
|
"@payloadcms/ui": "^3.77.0",
|
||||||
"@react-pdf/renderer": "^4.3.2",
|
"@react-pdf/renderer": "^4.3.2",
|
||||||
"@remotion/bundler": "^4.0.414",
|
"@remotion/bundler": "^4.0.414",
|
||||||
"@remotion/cli": "^4.0.414",
|
"@remotion/cli": "^4.0.414",
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ export interface Config {
|
|||||||
users: User;
|
users: User;
|
||||||
media: Media;
|
media: Media;
|
||||||
posts: Post;
|
posts: Post;
|
||||||
|
inquiries: Inquiry;
|
||||||
|
redirects: Redirect;
|
||||||
|
"context-files": ContextFile;
|
||||||
"payload-kv": PayloadKv;
|
"payload-kv": PayloadKv;
|
||||||
"payload-locked-documents": PayloadLockedDocument;
|
"payload-locked-documents": PayloadLockedDocument;
|
||||||
"payload-preferences": PayloadPreference;
|
"payload-preferences": PayloadPreference;
|
||||||
@@ -80,6 +83,9 @@ export interface Config {
|
|||||||
users: UsersSelect<false> | UsersSelect<true>;
|
users: UsersSelect<false> | UsersSelect<true>;
|
||||||
media: MediaSelect<false> | MediaSelect<true>;
|
media: MediaSelect<false> | MediaSelect<true>;
|
||||||
posts: PostsSelect<false> | PostsSelect<true>;
|
posts: PostsSelect<false> | PostsSelect<true>;
|
||||||
|
inquiries: InquiriesSelect<false> | InquiriesSelect<true>;
|
||||||
|
redirects: RedirectsSelect<false> | RedirectsSelect<true>;
|
||||||
|
"context-files": ContextFilesSelect<false> | ContextFilesSelect<true>;
|
||||||
"payload-kv": PayloadKvSelect<false> | PayloadKvSelect<true>;
|
"payload-kv": PayloadKvSelect<false> | PayloadKvSelect<true>;
|
||||||
"payload-locked-documents":
|
"payload-locked-documents":
|
||||||
| PayloadLockedDocumentsSelect<false>
|
| PayloadLockedDocumentsSelect<false>
|
||||||
@@ -95,8 +101,12 @@ export interface Config {
|
|||||||
defaultIDType: number;
|
defaultIDType: number;
|
||||||
};
|
};
|
||||||
fallbackLocale: null;
|
fallbackLocale: null;
|
||||||
globals: {};
|
globals: {
|
||||||
globalsSelect: {};
|
"ai-settings": AiSetting;
|
||||||
|
};
|
||||||
|
globalsSelect: {
|
||||||
|
"ai-settings": AiSettingsSelect<false> | AiSettingsSelect<true>;
|
||||||
|
};
|
||||||
locale: null;
|
locale: null;
|
||||||
user: User;
|
user: User;
|
||||||
jobs: {
|
jobs: {
|
||||||
@@ -201,12 +211,99 @@ export interface Post {
|
|||||||
title: string;
|
title: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
/**
|
||||||
|
* Set a future date and save as 'Published' to schedule this post. It will not appear on the frontend until this date is reached.
|
||||||
|
*/
|
||||||
date: string;
|
date: string;
|
||||||
tags: {
|
tags: {
|
||||||
|
/**
|
||||||
|
* Kategorisiere diesen Post mit einem eindeutigen Tag
|
||||||
|
*/
|
||||||
tag?: string | null;
|
tag?: string | null;
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
}[];
|
}[];
|
||||||
thumbnail?: string | null;
|
/**
|
||||||
|
* The main hero image for the blog post.
|
||||||
|
*/
|
||||||
|
featuredImage?: (number | null) | Media;
|
||||||
|
content?: {
|
||||||
|
root: {
|
||||||
|
type: string;
|
||||||
|
children: {
|
||||||
|
type: any;
|
||||||
|
version: number;
|
||||||
|
[k: string]: unknown;
|
||||||
|
}[];
|
||||||
|
direction: ("ltr" | "rtl") | null;
|
||||||
|
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
|
||||||
|
indent: number;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
[k: string]: unknown;
|
||||||
|
} | null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
_status?: ("draft" | "published") | null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Contact form leads and inquiries.
|
||||||
|
*
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "inquiries".
|
||||||
|
*/
|
||||||
|
export interface Inquiry {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
companyName?: string | null;
|
||||||
|
projectType?: string | null;
|
||||||
|
message?: string | null;
|
||||||
|
isFreeText?: boolean | null;
|
||||||
|
/**
|
||||||
|
* The JSON data from the configurator.
|
||||||
|
*/
|
||||||
|
config?:
|
||||||
|
| {
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
| unknown[]
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "redirects".
|
||||||
|
*/
|
||||||
|
export interface Redirect {
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* The old URL slug that should be redirected (e.g. 'old-post-name')
|
||||||
|
*/
|
||||||
|
from: string;
|
||||||
|
/**
|
||||||
|
* The new URL slug to redirect to (e.g. 'new-awesome-post')
|
||||||
|
*/
|
||||||
|
to: string;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "context-files".
|
||||||
|
*/
|
||||||
|
export interface ContextFile {
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* Exact filename (e.g. 'strategy.md'). The system uses this to identify the document during prompt generation.
|
||||||
|
*/
|
||||||
|
filename: string;
|
||||||
|
/**
|
||||||
|
* The raw markdown/text content of the document.
|
||||||
|
*/
|
||||||
content: string;
|
content: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -246,6 +343,18 @@ export interface PayloadLockedDocument {
|
|||||||
| ({
|
| ({
|
||||||
relationTo: "posts";
|
relationTo: "posts";
|
||||||
value: number | Post;
|
value: number | Post;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: "inquiries";
|
||||||
|
value: number | Inquiry;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: "redirects";
|
||||||
|
value: number | Redirect;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: "context-files";
|
||||||
|
value: number | ContextFile;
|
||||||
} | null);
|
} | null);
|
||||||
globalSlug?: string | null;
|
globalSlug?: string | null;
|
||||||
user: {
|
user: {
|
||||||
@@ -378,7 +487,43 @@ export interface PostsSelect<T extends boolean = true> {
|
|||||||
tag?: T;
|
tag?: T;
|
||||||
id?: T;
|
id?: T;
|
||||||
};
|
};
|
||||||
thumbnail?: T;
|
featuredImage?: T;
|
||||||
|
content?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
_status?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "inquiries_select".
|
||||||
|
*/
|
||||||
|
export interface InquiriesSelect<T extends boolean = true> {
|
||||||
|
name?: T;
|
||||||
|
email?: T;
|
||||||
|
companyName?: T;
|
||||||
|
projectType?: T;
|
||||||
|
message?: T;
|
||||||
|
isFreeText?: T;
|
||||||
|
config?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "redirects_select".
|
||||||
|
*/
|
||||||
|
export interface RedirectsSelect<T extends boolean = true> {
|
||||||
|
from?: T;
|
||||||
|
to?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "context-files_select".
|
||||||
|
*/
|
||||||
|
export interface ContextFilesSelect<T extends boolean = true> {
|
||||||
|
filename?: T;
|
||||||
content?: T;
|
content?: T;
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
@@ -423,6 +568,39 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
|
|||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "ai-settings".
|
||||||
|
*/
|
||||||
|
export interface AiSetting {
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* List of trusted B2B/Tech sources (e.g. 'Vercel Blog', 'Fireship', 'Theo - t3.gg') the AI should prioritize when researching facts or videos. This overrides the hardcoded defaults.
|
||||||
|
*/
|
||||||
|
customSources?:
|
||||||
|
| {
|
||||||
|
sourceName: string;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
updatedAt?: string | null;
|
||||||
|
createdAt?: string | null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "ai-settings_select".
|
||||||
|
*/
|
||||||
|
export interface AiSettingsSelect<T extends boolean = true> {
|
||||||
|
customSources?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
sourceName?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
globalType?: T;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "auth".
|
* via the `definition` "auth".
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { buildConfig } from "payload";
|
import { buildConfig } from "payload";
|
||||||
|
// Triggering config re-analysis for blocks visibility - V4
|
||||||
import { postgresAdapter } from "@payloadcms/db-postgres";
|
import { postgresAdapter } from "@payloadcms/db-postgres";
|
||||||
import { lexicalEditor } from "@payloadcms/richtext-lexical";
|
import { lexicalEditor, BlocksFeature } from "@payloadcms/richtext-lexical";
|
||||||
|
import { payloadBlocks } from "./src/payload/blocks/allBlocks";
|
||||||
import { nodemailerAdapter } from "@payloadcms/email-nodemailer";
|
import { nodemailerAdapter } from "@payloadcms/email-nodemailer";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
@@ -9,6 +11,11 @@ import sharp from "sharp";
|
|||||||
import { Users } from "./src/payload/collections/Users";
|
import { Users } from "./src/payload/collections/Users";
|
||||||
import { Media } from "./src/payload/collections/Media";
|
import { Media } from "./src/payload/collections/Media";
|
||||||
import { Posts } from "./src/payload/collections/Posts";
|
import { Posts } from "./src/payload/collections/Posts";
|
||||||
|
import { Inquiries } from "./src/payload/collections/Inquiries";
|
||||||
|
import { Redirects } from "./src/payload/collections/Redirects";
|
||||||
|
import { ContextFiles } from "./src/payload/collections/ContextFiles";
|
||||||
|
|
||||||
|
import { AiSettings } from "./src/payload/globals/AiSettings";
|
||||||
|
|
||||||
const filename = fileURLToPath(import.meta.url);
|
const filename = fileURLToPath(import.meta.url);
|
||||||
const dirname = path.dirname(filename);
|
const dirname = path.dirname(filename);
|
||||||
@@ -20,7 +27,8 @@ export default buildConfig({
|
|||||||
baseDir: path.resolve(dirname),
|
baseDir: path.resolve(dirname),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
collections: [Users, Media, Posts],
|
collections: [Users, Media, Posts, Inquiries, Redirects, ContextFiles],
|
||||||
|
globals: [AiSettings],
|
||||||
...(process.env.MAIL_HOST
|
...(process.env.MAIL_HOST
|
||||||
? {
|
? {
|
||||||
email: nodemailerAdapter({
|
email: nodemailerAdapter({
|
||||||
@@ -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",
|
secret: process.env.PAYLOAD_SECRET || "fallback-secret-for-dev",
|
||||||
typescript: {
|
typescript: {
|
||||||
outputFile: path.resolve(dirname, "payload-types.ts"),
|
outputFile: path.resolve(dirname, "payload-types.ts"),
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.0 MiB |
|
Before Width: | Height: | Size: 5.2 MiB |
|
Before Width: | Height: | Size: 4.8 MiB |
|
Before Width: | Height: | Size: 4.5 MiB |
|
Before Width: | Height: | Size: 4.5 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 4.9 MiB |
|
Before Width: | Height: | Size: 4.8 MiB |
|
Before Width: | Height: | Size: 5.6 MiB |
|
Before Width: | Height: | Size: 4.3 MiB |
|
Before Width: | Height: | Size: 4.5 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 4.6 MiB |
|
Before Width: | Height: | Size: 3.8 MiB |
|
Before Width: | Height: | Size: 4.3 MiB |
|
Before Width: | Height: | Size: 4.3 MiB |
|
Before Width: | Height: | Size: 5.0 MiB |
|
Before Width: | Height: | Size: 4.2 MiB |
|
Before Width: | Height: | Size: 4.3 MiB |
|
Before Width: | Height: | Size: 5.0 MiB |
|
Before Width: | Height: | Size: 4.8 MiB |
87
apps/web/remove-toc.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { getPayload } from "payload";
|
||||||
|
import configPromise from "./payload.config";
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
const { docs } = await payload.find({
|
||||||
|
collection: "posts",
|
||||||
|
limit: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Found ${docs.length} posts. Checking for <TableOfContents />...`,
|
||||||
|
);
|
||||||
|
|
||||||
|
let updatedCount = 0;
|
||||||
|
|
||||||
|
const removeTOC = (node: any): boolean => {
|
||||||
|
let modified = false;
|
||||||
|
if (node.children && Array.isArray(node.children)) {
|
||||||
|
// Filter out raw text nodes or paragraph nodes that are exactly TableOfContents
|
||||||
|
const originalLength = node.children.length;
|
||||||
|
node.children = node.children.filter((child: any) => {
|
||||||
|
if (
|
||||||
|
child.type === "text" &&
|
||||||
|
child.text &&
|
||||||
|
child.text.includes("<TableOfContents />")
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
child.type === "paragraph" &&
|
||||||
|
child.children &&
|
||||||
|
child.children.length === 1 &&
|
||||||
|
child.children[0].text === "<TableOfContents />"
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (node.children.length !== originalLength) {
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also clean up any substrings in remaining text nodes
|
||||||
|
for (const child of node.children) {
|
||||||
|
if (
|
||||||
|
child.type === "text" &&
|
||||||
|
child.text &&
|
||||||
|
child.text.includes("<TableOfContents />")
|
||||||
|
) {
|
||||||
|
child.text = child.text.replace("<TableOfContents />", "").trim();
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
if (removeTOC(child)) {
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return modified;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const doc of docs) {
|
||||||
|
if (doc.content?.root) {
|
||||||
|
const isModified = removeTOC(doc.content.root);
|
||||||
|
if (isModified) {
|
||||||
|
try {
|
||||||
|
await payload.update({
|
||||||
|
collection: "posts",
|
||||||
|
id: doc.id,
|
||||||
|
data: {
|
||||||
|
content: doc.content,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`Cleaned up TOC in "${doc.title}".`);
|
||||||
|
updatedCount++;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to update ${doc.title}:`, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Cleanup complete. Modified ${updatedCount} posts.`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
@@ -2,6 +2,7 @@ import { getPayload } from "payload";
|
|||||||
import configPromise from "../payload.config";
|
import configPromise from "../payload.config";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { parseMarkdownToLexical } from "../src/payload/utils/lexicalParser";
|
||||||
|
|
||||||
function parseMatter(content: string) {
|
function parseMatter(content: string) {
|
||||||
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
||||||
@@ -39,29 +40,112 @@ async function run() {
|
|||||||
const slug = file.replace(/\.mdx$/, "");
|
const slug = file.replace(/\.mdx$/, "");
|
||||||
console.log(`Migrating ${slug}...`);
|
console.log(`Migrating ${slug}...`);
|
||||||
|
|
||||||
const existing = await payload.find({
|
try {
|
||||||
collection: "posts",
|
const existing = await payload.find({
|
||||||
where: { slug: { equals: slug } },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existing.docs.length === 0) {
|
|
||||||
await payload.create({
|
|
||||||
collection: "posts",
|
collection: "posts",
|
||||||
data: {
|
where: { slug: { equals: slug } },
|
||||||
title: data.title || slug,
|
|
||||||
slug,
|
|
||||||
description: data.description || "",
|
|
||||||
date: data.date
|
|
||||||
? new Date(data.date).toISOString()
|
|
||||||
: new Date().toISOString(),
|
|
||||||
tags: (data.tags || []).map((t: string) => ({ tag: t })),
|
|
||||||
thumbnail: data.thumbnail || "",
|
|
||||||
content: body,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
console.log(`✔ Inserted ${slug}`);
|
|
||||||
} else {
|
const lexicalBlocks = parseMarkdownToLexical(body);
|
||||||
console.log(`⚠ Skipped ${slug} (already exists)`);
|
const lexicalAST = {
|
||||||
|
root: {
|
||||||
|
type: "root",
|
||||||
|
format: "",
|
||||||
|
indent: 0,
|
||||||
|
version: 1,
|
||||||
|
children: lexicalBlocks,
|
||||||
|
direction: "ltr",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle thumbnail mapping
|
||||||
|
let featuredImageId = null;
|
||||||
|
if (data.thumbnail) {
|
||||||
|
try {
|
||||||
|
// Remove leading slash and find local file
|
||||||
|
const localPath = path.join(
|
||||||
|
process.cwd(),
|
||||||
|
"public",
|
||||||
|
data.thumbnail.replace(/^\//, ""),
|
||||||
|
);
|
||||||
|
const fileName = path.basename(localPath);
|
||||||
|
|
||||||
|
if (fs.existsSync(localPath)) {
|
||||||
|
// Check if media already exists in Payload
|
||||||
|
const existingMedia = await payload.find({
|
||||||
|
collection: "media",
|
||||||
|
where: { filename: { equals: fileName } },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingMedia.docs.length > 0) {
|
||||||
|
featuredImageId = existingMedia.docs[0].id;
|
||||||
|
} else {
|
||||||
|
// Upload new media item
|
||||||
|
const fileData = fs.readFileSync(localPath);
|
||||||
|
const { size } = fs.statSync(localPath);
|
||||||
|
|
||||||
|
const newMedia = await payload.create({
|
||||||
|
collection: "media",
|
||||||
|
data: {
|
||||||
|
alt: data.title || fileName,
|
||||||
|
},
|
||||||
|
file: {
|
||||||
|
data: fileData,
|
||||||
|
name: fileName,
|
||||||
|
mimetype: fileName.endsWith(".png")
|
||||||
|
? "image/png"
|
||||||
|
: fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")
|
||||||
|
? "image/jpeg"
|
||||||
|
: "image/webp",
|
||||||
|
size,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
featuredImageId = newMedia.id;
|
||||||
|
console.log(` ↑ Uploaded thumbnail: ${fileName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(
|
||||||
|
` ⚠ Warning: Could not process thumbnail ${data.thumbnail}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing.docs.length === 0) {
|
||||||
|
await payload.create({
|
||||||
|
collection: "posts",
|
||||||
|
data: {
|
||||||
|
title: data.title || slug,
|
||||||
|
slug,
|
||||||
|
description: data.description || "",
|
||||||
|
date: data.date
|
||||||
|
? new Date(data.date).toISOString()
|
||||||
|
: new Date().toISOString(),
|
||||||
|
tags: (data.tags || []).map((t: string) => ({ tag: t })),
|
||||||
|
content: lexicalAST as any,
|
||||||
|
featuredImage: featuredImageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`✔ Inserted ${slug}`);
|
||||||
|
} else {
|
||||||
|
await payload.update({
|
||||||
|
collection: "posts",
|
||||||
|
id: existing.docs[0].id,
|
||||||
|
data: {
|
||||||
|
content: lexicalAST as any,
|
||||||
|
featuredImage: featuredImageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`✔ Updated AST and thumbnail for ${slug}`);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`✘ FAILED ${slug}: ${err.message}`);
|
||||||
|
if (err.data?.errors) {
|
||||||
|
console.error(
|
||||||
|
` Validation errors:`,
|
||||||
|
JSON.stringify(err.data.errors, null, 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
56
apps/web/seed-context.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { getPayload } from "payload";
|
||||||
|
import configPromise from "./payload.config";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
try {
|
||||||
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
|
||||||
|
const existing = await payload.find({
|
||||||
|
collection: "context-files",
|
||||||
|
limit: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing.totalDocs > 0) {
|
||||||
|
console.log("Context collection already populated. Skipping seed.");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const seedDir = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"src/payload/collections/ContextFiles/seed",
|
||||||
|
);
|
||||||
|
if (!fs.existsSync(seedDir)) {
|
||||||
|
console.log(`Seed directory not found at ${seedDir}`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = fs.readdirSync(seedDir).filter((f) => f.endsWith(".md"));
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const content = fs.readFileSync(path.join(seedDir, file), "utf8");
|
||||||
|
await payload.create({
|
||||||
|
collection: "context-files",
|
||||||
|
data: {
|
||||||
|
filename: file,
|
||||||
|
content: content,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Seeded ${count} context files.`);
|
||||||
|
process.exit(0);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Seeding failed:", e);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import * as Sentry from "@sentry/nextjs";
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
const dsn = process.env.SENTRY_DSN;
|
const dsn = process.env.SENTRY_DSN;
|
||||||
|
const isProd = process.env.NODE_ENV === "production";
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn,
|
dsn,
|
||||||
enabled: Boolean(dsn),
|
enabled: isProd && Boolean(dsn),
|
||||||
tracesSampleRate: 1,
|
tracesSampleRate: 1,
|
||||||
debug: false,
|
debug: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import * as Sentry from "@sentry/nextjs";
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
const dsn = process.env.SENTRY_DSN;
|
const dsn = process.env.SENTRY_DSN;
|
||||||
|
const isProd = process.env.NODE_ENV === "production";
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn,
|
dsn,
|
||||||
enabled: Boolean(dsn),
|
enabled: isProd && Boolean(dsn),
|
||||||
|
|
||||||
tracesSampleRate: 1,
|
tracesSampleRate: 1,
|
||||||
debug: false,
|
debug: false,
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
getInquiryEmailHtml,
|
getInquiryEmailHtml,
|
||||||
getConfirmationEmailHtml,
|
getConfirmationEmailHtml,
|
||||||
} from "../components/ContactForm/EmailTemplates";
|
} from "../components/ContactForm/EmailTemplates";
|
||||||
|
import { getPayload } from "payload";
|
||||||
|
import configPromise from "@payload-config";
|
||||||
|
|
||||||
export async function sendContactInquiry(data: {
|
export async function sendContactInquiry(data: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -16,7 +18,22 @@ export async function sendContactInquiry(data: {
|
|||||||
config?: any;
|
config?: any;
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
// 1. Send Inquiry to Marc
|
// 1. Save to Payload CMS (Replaces Directus)
|
||||||
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
await payload.create({
|
||||||
|
collection: "inquiries",
|
||||||
|
data: {
|
||||||
|
name: data.name,
|
||||||
|
email: data.email,
|
||||||
|
companyName: data.companyName,
|
||||||
|
projectType: data.projectType,
|
||||||
|
message: data.message,
|
||||||
|
isFreeText: data.isFreeText,
|
||||||
|
config: data.config || null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Send Inquiry to Marc
|
||||||
const inquiryResult = await sendEmail({
|
const inquiryResult = await sendEmail({
|
||||||
subject: `[PROJEKT] ${data.isFreeText ? "DIREKTANFRAGE" : "KONFIGURATION"}: ${data.companyName || data.name}`,
|
subject: `[PROJEKT] ${data.isFreeText ? "DIREKTANFRAGE" : "KONFIGURATION"}: ${data.companyName || data.name}`,
|
||||||
html: getInquiryEmailHtml(data),
|
html: getInquiryEmailHtml(data),
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { cn } from "../utils/cn";
|
import { cn } from "../utils/cn";
|
||||||
import { ShieldCheck, ArrowLeft, ArrowRight, RefreshCw } from "lucide-react";
|
import { ShieldCheck, ArrowLeft, ArrowRight, RefreshCw } from "lucide-react";
|
||||||
@@ -31,8 +30,6 @@ interface IframeSectionProps {
|
|||||||
desktopHeight?: string;
|
desktopHeight?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reusable Browser UI components to maintain consistency
|
* Reusable Browser UI components to maintain consistency
|
||||||
*/
|
*/
|
||||||
@@ -102,11 +99,6 @@ const BrowserChromeComponent: React.FC<{ url: string; minimal?: boolean }> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
BrowserChromeComponent.propTypes = {
|
|
||||||
url: PropTypes.string.isRequired,
|
|
||||||
minimal: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
const BrowserChrome = React.memo(BrowserChromeComponent);
|
const BrowserChrome = React.memo(BrowserChromeComponent);
|
||||||
|
|
||||||
BrowserChrome.displayName = "BrowserChrome";
|
BrowserChrome.displayName = "BrowserChrome";
|
||||||
@@ -212,7 +204,7 @@ export const IframeSection: React.FC<IframeSectionProps> = ({
|
|||||||
const isScrollable = doc.scrollHeight > doc.clientHeight + 10;
|
const isScrollable = doc.scrollHeight > doc.clientHeight + 10;
|
||||||
setScrollState({ atTop, atBottom, isScrollable });
|
setScrollState({ atTop, atBottom, isScrollable });
|
||||||
}
|
}
|
||||||
} catch (_e) { }
|
} catch (_e) {}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Ambilight effect (sampled from iframe if same-origin)
|
// Ambilight effect (sampled from iframe if same-origin)
|
||||||
@@ -257,7 +249,7 @@ export const IframeSection: React.FC<IframeSectionProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
updateScrollState();
|
updateScrollState();
|
||||||
} catch (_e) { }
|
} catch (_e) {}
|
||||||
}, [dynamicGlow, offsetY, updateScrollState]);
|
}, [dynamicGlow, offsetY, updateScrollState]);
|
||||||
|
|
||||||
// Height parse helper
|
// Height parse helper
|
||||||
@@ -376,9 +368,9 @@ export const IframeSection: React.FC<IframeSectionProps> = ({
|
|||||||
"w-full relative flex flex-col z-10",
|
"w-full relative flex flex-col z-10",
|
||||||
minimal ? "bg-transparent" : "bg-slate-50",
|
minimal ? "bg-transparent" : "bg-slate-50",
|
||||||
!minimal &&
|
!minimal &&
|
||||||
"rounded-[2.5rem] border border-slate-200/50 shadow-[0_80px_160px_-40px_rgba(0,0,0,0.18),0_0_1px_rgba(0,0,0,0.1)]",
|
"rounded-[2.5rem] border border-slate-200/50 shadow-[0_80px_160px_-40px_rgba(0,0,0,0.18),0_0_1px_rgba(0,0,0,0.1)]",
|
||||||
perspective &&
|
perspective &&
|
||||||
"hover:scale-[1.03] hover:-translate-y-3 transition-[transform,shadow,scale] duration-1000 ease-[cubic-bezier(0.23,1,0.32,1)]",
|
"hover:scale-[1.03] hover:-translate-y-3 transition-[transform,shadow,scale] duration-1000 ease-[cubic-bezier(0.23,1,0.32,1)]",
|
||||||
"overflow-hidden",
|
"overflow-hidden",
|
||||||
)}
|
)}
|
||||||
style={chassisStyle}
|
style={chassisStyle}
|
||||||
|
|||||||
@@ -1,256 +1,355 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import { ComponentShareButton } from './ComponentShareButton';
|
import { ComponentShareButton } from "./ComponentShareButton";
|
||||||
import { Reveal } from './Reveal';
|
import { Reveal } from "./Reveal";
|
||||||
|
|
||||||
interface MemeCardProps {
|
interface MemeCardProps {
|
||||||
/** Meme template type: drake, ds (daily struggle), gru, fine, clown, expanding, distracted, rollsafe */
|
/** Meme template type: drake, ds (daily struggle), gru, fine, clown, expanding, distracted, rollsafe */
|
||||||
template: string;
|
template: string;
|
||||||
/** Pipe-delimited captions */
|
/** Pipe-delimited captions */
|
||||||
captions: string;
|
captions: string;
|
||||||
/** Optional local image path. If provided, overrides the text-based template. */
|
/** Optional local image path. If provided, overrides the text-based template. */
|
||||||
image?: string;
|
image?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Premium text-based meme cards with dedicated layouts per template.
|
* Premium text-based meme cards with dedicated layouts per template.
|
||||||
* Uses emoji + typography instead of images for on-brand aesthetics.
|
* Uses emoji + typography instead of images for on-brand aesthetics.
|
||||||
*/
|
*/
|
||||||
export const MemeCard: React.FC<MemeCardProps> = ({ template, captions, image, className = '' }) => {
|
export const MemeCard: React.FC<MemeCardProps> = ({
|
||||||
const captionList = (captions || '').split('|').map(s => s.trim()).filter(Boolean);
|
template,
|
||||||
const shareId = `meme-${Math.random().toString(36).substring(7).toUpperCase()}`;
|
captions,
|
||||||
|
image,
|
||||||
if (image) {
|
className = "",
|
||||||
return (
|
}) => {
|
||||||
<Reveal direction="up" delay={0.1}>
|
// Also replace literal `\n` (slash-n) strings from AI output with actual newlines
|
||||||
<div className={`not-prose max-w-xl mx-auto my-12 group relative transition-all duration-500 ease-out z-10 ${className}`}>
|
const processedCaptions = (captions || "").replace(/\\n/g, "\n");
|
||||||
<div className="absolute -inset-1 bg-gradient-to-r from-slate-200 to-slate-100 rounded-[2rem] blur opacity-10 group-hover:opacity-30 transition duration-1000 -z-10" />
|
const captionList = processedCaptions
|
||||||
|
.split("|")
|
||||||
<div id={shareId} className="glass bg-white/80 backdrop-blur-xl border border-slate-100 rounded-2xl overflow-hidden shadow-sm shadow-slate-200 group-hover:shadow-md transition-all duration-500 relative">
|
.map((s) => s.trim())
|
||||||
<div data-share-wrapper="true" className="absolute top-4 right-4 md:opacity-0 group-hover:opacity-100 transition-opacity duration-500 z-50">
|
.filter(Boolean);
|
||||||
<ComponentShareButton targetId={shareId} title={`Meme: ${template}`} />
|
const shareId = `meme-${Math.random().toString(36).substring(7).toUpperCase()}`;
|
||||||
</div>
|
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
||||||
<img
|
|
||||||
src={image}
|
|
||||||
alt={`Meme: ${template} - ${captionList.join(' ')}`}
|
|
||||||
className="w-full h-auto object-cover block"
|
|
||||||
loading="eager"
|
|
||||||
decoding="sync"
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Reveal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (image) {
|
||||||
return (
|
return (
|
||||||
<Reveal direction="up" delay={0.1}>
|
<Reveal direction="up" delay={0.1}>
|
||||||
<div className={`not-prose max-w-xl mx-auto my-12 group relative transition-all duration-500 ease-out z-10 ${className}`}>
|
<div
|
||||||
<div className="absolute -inset-1 bg-gradient-to-r from-slate-200 to-slate-100 rounded-[2rem] blur opacity-10 group-hover:opacity-30 transition duration-1000 -z-10" />
|
className={`not-prose max-w-xl mx-auto my-12 group relative transition-all duration-500 ease-out z-10 ${className}`}
|
||||||
|
>
|
||||||
|
<div className="absolute -inset-1 bg-gradient-to-r from-slate-200 to-slate-100 rounded-[2rem] blur opacity-10 group-hover:opacity-30 transition duration-1000 -z-10" />
|
||||||
|
|
||||||
<div id={shareId} className="glass bg-white/80 backdrop-blur-xl border border-slate-100 rounded-2xl overflow-hidden shadow-sm shadow-slate-200 group-hover:shadow-md transition-all duration-500 relative">
|
<div
|
||||||
<div data-share-wrapper="true" className="absolute top-4 right-4 md:opacity-0 group-hover:opacity-100 transition-opacity duration-500 z-50">
|
id={shareId}
|
||||||
<ComponentShareButton targetId={shareId} title={`Meme: ${template}`} />
|
className="glass bg-white/80 backdrop-blur-xl border border-slate-100 rounded-2xl overflow-hidden shadow-sm shadow-slate-200 group-hover:shadow-md transition-all duration-500 relative"
|
||||||
</div>
|
>
|
||||||
|
<div
|
||||||
{template === 'drake' && <DrakeMeme captions={captionList} />}
|
data-share-wrapper="true"
|
||||||
{template === 'ds' && <DailyStruggleMeme captions={captionList} />}
|
className="absolute top-4 right-4 md:opacity-0 group-hover:opacity-100 transition-opacity duration-500 z-50"
|
||||||
{template === 'gru' && <GruMeme captions={captionList} />}
|
>
|
||||||
{template === 'fine' && <FineMeme captions={captionList} />}
|
<ComponentShareButton
|
||||||
{template === 'clown' && <ClownMeme captions={captionList} />}
|
targetId={shareId}
|
||||||
{template === 'expanding' && <ExpandingBrainMeme captions={captionList} />}
|
title={`Meme: ${template}`}
|
||||||
{template === 'distracted' && <DistractedMeme captions={captionList} />}
|
/>
|
||||||
<GenericMeme captions={captionList} template={template} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Reveal>
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
|
src={image}
|
||||||
|
alt={`Meme: ${template} - ${captionList.join(" ")}`}
|
||||||
|
className="w-full h-auto object-cover block"
|
||||||
|
loading="eager"
|
||||||
|
decoding="sync"
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Reveal direction="up" delay={0.1}>
|
||||||
|
<div
|
||||||
|
className={`not-prose max-w-xl mx-auto my-12 group relative transition-all duration-500 ease-out z-10 ${className}`}
|
||||||
|
>
|
||||||
|
<div className="absolute -inset-1 bg-gradient-to-r from-slate-200 to-slate-100 rounded-[2rem] blur opacity-10 group-hover:opacity-30 transition duration-1000 -z-10" />
|
||||||
|
|
||||||
|
<div
|
||||||
|
id={shareId}
|
||||||
|
className="glass bg-white/80 backdrop-blur-xl border border-slate-100 rounded-2xl overflow-hidden shadow-sm shadow-slate-200 group-hover:shadow-md transition-all duration-500 relative"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-share-wrapper="true"
|
||||||
|
className="absolute top-4 right-4 md:opacity-0 group-hover:opacity-100 transition-opacity duration-500 z-50"
|
||||||
|
>
|
||||||
|
<ComponentShareButton
|
||||||
|
targetId={shareId}
|
||||||
|
title={`Meme: ${template}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{template === "drake" && <DrakeMeme captions={captionList} />}
|
||||||
|
{template === "ds" && <DailyStruggleMeme captions={captionList} />}
|
||||||
|
{template === "gru" && <GruMeme captions={captionList} />}
|
||||||
|
{template === "fine" && <FineMeme captions={captionList} />}
|
||||||
|
{template === "clown" && <ClownMeme captions={captionList} />}
|
||||||
|
{template === "expanding" && (
|
||||||
|
<ExpandingBrainMeme captions={captionList} />
|
||||||
|
)}
|
||||||
|
{template === "distracted" && (
|
||||||
|
<DistractedMeme captions={captionList} />
|
||||||
|
)}
|
||||||
|
<GenericMeme captions={captionList} template={template} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function DrakeMeme({ captions }: { captions: string[] }) {
|
function DrakeMeme({ captions }: { captions: string[] }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex items-stretch border-b border-slate-100">
|
<div className="flex items-stretch border-b border-slate-100">
|
||||||
<div className="w-20 md:w-24 bg-red-400/10 flex items-center justify-center flex-shrink-0 border-r border-slate-100">
|
<div className="w-20 md:w-24 bg-red-400/10 flex items-center justify-center flex-shrink-0 border-r border-slate-100">
|
||||||
<span className="text-3xl md:text-4xl select-none grayscale-0 group-hover:scale-110 transition-transform duration-500">🙅</span>
|
<span className="text-3xl md:text-4xl select-none grayscale-0 group-hover:scale-110 transition-transform duration-500">
|
||||||
</div>
|
🙅
|
||||||
<div className="flex-1 p-5 md:p-6 flex items-center bg-white/40">
|
</span>
|
||||||
<p className="text-lg md:text-xl font-medium text-slate-500 leading-snug">{captions[0]}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-stretch">
|
|
||||||
<div className="w-20 md:w-24 bg-emerald-400/10 flex items-center justify-center flex-shrink-0 border-r border-slate-100">
|
|
||||||
<span className="text-3xl md:text-4xl select-none group-hover:scale-110 transition-transform duration-500">😎</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 p-5 md:p-6 flex items-center bg-white">
|
|
||||||
<p className="text-lg md:text-xl font-bold text-slate-900 leading-snug">{captions[1]}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<div className="flex-1 p-5 md:p-6 flex items-center bg-white/40">
|
||||||
|
<p className="text-lg md:text-xl font-medium text-slate-500 leading-snug whitespace-pre-wrap">
|
||||||
|
{captions[0]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-stretch">
|
||||||
|
<div className="w-20 md:w-24 bg-emerald-400/10 flex items-center justify-center flex-shrink-0 border-r border-slate-100">
|
||||||
|
<span className="text-3xl md:text-4xl select-none group-hover:scale-110 transition-transform duration-500">
|
||||||
|
😎
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 p-5 md:p-6 flex items-center bg-white">
|
||||||
|
<p className="text-lg md:text-xl font-bold text-slate-900 leading-snug whitespace-pre-wrap">
|
||||||
|
{captions[1]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DailyStruggleMeme({ captions }: { captions: string[] }) {
|
function DailyStruggleMeme({ captions }: { captions: string[] }) {
|
||||||
return (
|
return (
|
||||||
<div className="p-8 md:p-10 text-center">
|
<div className="p-8 md:p-10 text-center">
|
||||||
<div className="text-4xl md:text-5xl mb-6 select-none animate-bounce-subtle">😰</div>
|
<div className="text-4xl md:text-5xl mb-6 select-none animate-bounce-subtle">
|
||||||
<p className="text-[10px] font-black text-slate-300 uppercase tracking-[0.3em] mb-8">Daily Struggle</p>
|
😰
|
||||||
<div className="grid grid-cols-2 gap-4">
|
</div>
|
||||||
<div className="p-5 rounded-2xl bg-white border border-slate-100 shadow-sm hover:border-slate-200 transition-colors">
|
<p className="text-[10px] font-black text-slate-300 uppercase tracking-[0.3em] mb-8">
|
||||||
<div className="w-5 h-5 rounded-full bg-red-500 mx-auto mb-3 shadow-[0_0_10px_rgba(239,68,68,0.4)]" />
|
Daily Struggle
|
||||||
<p className="text-sm md:text-base font-bold text-slate-700 leading-snug">{captions[0]}</p>
|
</p>
|
||||||
</div>
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="p-5 rounded-2xl bg-white border border-slate-100 shadow-sm hover:border-slate-200 transition-colors">
|
<div className="p-5 rounded-2xl bg-white border border-slate-100 shadow-sm hover:border-slate-200 transition-colors">
|
||||||
<div className="w-5 h-5 rounded-full bg-red-500 mx-auto mb-3 shadow-[0_0_10px_rgba(239,68,68,0.4)]" />
|
<div className="w-5 h-5 rounded-full bg-red-500 mx-auto mb-3 shadow-[0_0_10px_rgba(239,68,68,0.4)]" />
|
||||||
<p className="text-sm md:text-base font-bold text-slate-700 leading-snug">{captions[1]}</p>
|
<p className="text-sm md:text-base font-bold text-slate-700 leading-snug">
|
||||||
</div>
|
{captions[0]}
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
<div className="p-5 rounded-2xl bg-white border border-slate-100 shadow-sm hover:border-slate-200 transition-colors">
|
||||||
|
<div className="w-5 h-5 rounded-full bg-red-500 mx-auto mb-3 shadow-[0_0_10px_rgba(239,68,68,0.4)]" />
|
||||||
|
<p className="text-sm md:text-base font-bold text-slate-700 leading-snug">
|
||||||
|
{captions[1]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function GruMeme({ captions }: { captions: string[] }) {
|
function GruMeme({ captions }: { captions: string[] }) {
|
||||||
const steps = captions.slice(0, 4);
|
const steps = captions.slice(0, 4);
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-2 grid-rows-2">
|
<div className="grid grid-cols-2 grid-rows-2">
|
||||||
{(steps || []).map((caption, i) => {
|
{(steps || []).map((caption, i) => {
|
||||||
const isLast = i >= 2;
|
const isLast = i >= 2;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className={`p-6 md:p-8 ${i % 2 === 0 ? 'border-r' : ''} ${i < 2 ? 'border-b' : ''} border-slate-100 flex flex-col items-center justify-center text-center gap-3 transition-colors hover:bg-slate-50/30`}
|
className={`p-6 md:p-8 ${i % 2 === 0 ? "border-r" : ""} ${i < 2 ? "border-b" : ""} border-slate-100 flex flex-col items-center justify-center text-center gap-3 transition-colors hover:bg-slate-50/30`}
|
||||||
>
|
>
|
||||||
<span className="text-2xl md:text-3xl select-none transition-transform group-hover:scale-110">
|
<span className="text-2xl md:text-3xl select-none transition-transform group-hover:scale-110">
|
||||||
{isLast ? '😱' : '😏'}
|
{isLast ? "😱" : "😏"}
|
||||||
</span>
|
</span>
|
||||||
<p className={`text-base md:text-lg leading-tight ${isLast ? 'font-black text-red-500' : 'font-bold text-slate-700'}`}>
|
<p
|
||||||
{caption}
|
className={`text-base md:text-lg leading-tight ${isLast ? "font-black text-red-500" : "font-bold text-slate-700"}`}
|
||||||
</p>
|
>
|
||||||
</div>
|
{caption}
|
||||||
);
|
</p>
|
||||||
})}
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FineMeme({ captions }: { captions: string[] }) {
|
function FineMeme({ captions }: { captions: string[] }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="bg-orange-50/50 border-b border-slate-100 p-6 md:p-8">
|
<div className="bg-orange-50/50 border-b border-slate-100 p-6 md:p-8">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<span className="text-3xl md:text-4xl select-none">🔥</span>
|
<span className="text-3xl md:text-4xl select-none">🔥</span>
|
||||||
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest m-0">This is Fine</p>
|
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest m-0">
|
||||||
</div>
|
This is Fine
|
||||||
<p className="text-lg md:text-xl font-bold text-slate-700 leading-snug">{captions[0]}</p>
|
</p>
|
||||||
</div>
|
|
||||||
<div className="p-6 md:p-8 bg-white">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<span className="text-3xl select-none group-hover:rotate-12 transition-transform">☕</span>
|
|
||||||
<p className="text-lg md:text-2xl font-black text-slate-900 leading-tight italic tracking-tight">
|
|
||||||
“{captions[1] || 'Alles im grünen Bereich.'}”
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<p className="text-lg md:text-xl font-bold text-slate-700 leading-snug">
|
||||||
|
{captions[0]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 md:p-8 bg-white">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<span className="text-3xl select-none group-hover:rotate-12 transition-transform">
|
||||||
|
☕
|
||||||
|
</span>
|
||||||
|
<p className="text-lg md:text-2xl font-black text-slate-900 leading-tight italic tracking-tight">
|
||||||
|
“{captions[1] || "Alles im grünen Bereich."}”
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ClownMeme({ captions }: { captions: string[] }) {
|
function ClownMeme({ captions }: { captions: string[] }) {
|
||||||
const steps = captions.slice(0, 4);
|
const steps = captions.slice(0, 4);
|
||||||
const emojis = ['😐', '🤡', '💀', '🎪'];
|
const emojis = ["😐", "🤡", "💀", "🎪"];
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="p-4 md:p-5 border-b border-slate-100 bg-slate-50/50">
|
<div className="p-4 md:p-5 border-b border-slate-100 bg-slate-50/50">
|
||||||
<p className="text-[10px] font-black text-slate-300 uppercase tracking-[0.3em] m-0 text-center">Clown Progression</p>
|
<p className="text-[10px] font-black text-slate-300 uppercase tracking-[0.3em] m-0 text-center">
|
||||||
</div>
|
Clown Progression
|
||||||
{steps.map((caption, i) => (
|
</p>
|
||||||
<div
|
</div>
|
||||||
key={i}
|
{steps.map((caption, i) => (
|
||||||
className={`flex items-center gap-5 p-5 md:p-6 ${i < steps.length - 1 ? 'border-b border-slate-100' : ''} hover:bg-slate-50 transition-colors`}
|
<div
|
||||||
>
|
key={i}
|
||||||
<span className="text-2xl md:text-3xl select-none flex-shrink-0 grayscale opacity-60 group-hover:grayscale-0 group-hover:opacity-100 transition-all duration-500">{emojis[i] || '🤡'}</span>
|
className={`flex items-center gap-5 p-5 md:p-6 ${i < steps.length - 1 ? "border-b border-slate-100" : ""} hover:bg-slate-50 transition-colors`}
|
||||||
<p className={`text-base md:text-lg leading-snug ${i === steps.length - 1 ? 'font-black text-red-500' : 'font-bold text-slate-700'}`}>
|
>
|
||||||
{caption}
|
<span className="text-2xl md:text-3xl select-none flex-shrink-0 grayscale opacity-60 group-hover:grayscale-0 group-hover:opacity-100 transition-all duration-500">
|
||||||
</p>
|
{emojis[i] || "🤡"}
|
||||||
</div>
|
</span>
|
||||||
))}
|
<p
|
||||||
|
className={`text-base md:text-lg leading-snug ${i === steps.length - 1 ? "font-black text-red-500" : "font-bold text-slate-700"}`}
|
||||||
|
>
|
||||||
|
{caption}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ExpandingBrainMeme({ captions }: { captions: string[] }) {
|
function ExpandingBrainMeme({ captions }: { captions: string[] }) {
|
||||||
const steps = captions.slice(0, 4);
|
const steps = captions.slice(0, 4);
|
||||||
const emojis = ['🧠', '🧠✨', '🧠💡', '🧠🚀'];
|
const emojis = ["🧠", "🧠✨", "🧠💡", "🧠🚀"];
|
||||||
const shadows = [
|
const shadows = [
|
||||||
'',
|
"",
|
||||||
'shadow-[0_0_15px_rgba(59,130,246,0.1)]',
|
"shadow-[0_0_15px_rgba(59,130,246,0.1)]",
|
||||||
'shadow-[0_0_20px_rgba(99,102,241,0.2)]',
|
"shadow-[0_0_20px_rgba(99,102,241,0.2)]",
|
||||||
'shadow-[0_0_25px_rgba(168,85,247,0.3)]',
|
"shadow-[0_0_25px_rgba(168,85,247,0.3)]",
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="p-4 md:p-5 border-b border-slate-100 bg-slate-50/50">
|
<div className="p-4 md:p-5 border-b border-slate-100 bg-slate-50/50">
|
||||||
<p className="text-[10px] font-black text-slate-300 uppercase tracking-[0.3em] m-0 text-center">Expanding Intelligence</p>
|
<p className="text-[10px] font-black text-slate-300 uppercase tracking-[0.3em] m-0 text-center">
|
||||||
</div>
|
Expanding Intelligence
|
||||||
{steps.map((caption, i) => (
|
</p>
|
||||||
<div
|
</div>
|
||||||
key={i}
|
{steps.map((caption, i) => (
|
||||||
className={`flex items-center gap-5 p-5 md:p-6 ${i < steps.length - 1 ? 'border-b border-slate-100' : ''} hover:bg-white transition-all duration-500 ${shadows[i]}`}
|
<div
|
||||||
>
|
key={i}
|
||||||
<span className="text-2xl md:text-3xl select-none flex-shrink-0 group-hover:scale-125 transition-transform duration-700">{emojis[i]}</span>
|
className={`flex items-center gap-5 p-5 md:p-6 ${i < steps.length - 1 ? "border-b border-slate-100" : ""} hover:bg-white transition-all duration-500 ${shadows[i]}`}
|
||||||
<p className={`text-base md:text-lg leading-tight ${i === steps.length - 1 ? 'font-black text-indigo-600' : 'font-bold text-slate-700'}`}>
|
>
|
||||||
{caption}
|
<span className="text-2xl md:text-3xl select-none flex-shrink-0 group-hover:scale-125 transition-transform duration-700">
|
||||||
</p>
|
{emojis[i]}
|
||||||
</div>
|
</span>
|
||||||
))}
|
<p
|
||||||
|
className={`text-base md:text-lg leading-tight ${i === steps.length - 1 ? "font-black text-indigo-600" : "font-bold text-slate-700"}`}
|
||||||
|
>
|
||||||
|
{caption}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DistractedMeme({ captions }: { captions: string[] }) {
|
function DistractedMeme({ captions }: { captions: string[] }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="p-4 md:p-5 border-b border-slate-100 bg-slate-50/50">
|
<div className="p-4 md:p-5 border-b border-slate-100 bg-slate-50/50">
|
||||||
<p className="text-[10px] font-black text-slate-300 uppercase tracking-[0.3em] m-0 text-center">The Distraction</p>
|
<p className="text-[10px] font-black text-slate-300 uppercase tracking-[0.3em] m-0 text-center">
|
||||||
</div>
|
The Distraction
|
||||||
<div className="grid grid-cols-3 divide-x divide-slate-100">
|
</p>
|
||||||
<div className="p-6 md:p-8 flex flex-col items-center text-center gap-3 hover:bg-slate-50/50 transition-colors">
|
</div>
|
||||||
<span className="text-3xl md:text-4xl select-none">👤</span>
|
<div className="grid grid-cols-3 divide-x divide-slate-100">
|
||||||
<p className="text-[9px] font-black text-slate-400 uppercase tracking-[0.2em] m-0">Subject</p>
|
<div className="p-6 md:p-8 flex flex-col items-center text-center gap-3 hover:bg-slate-50/50 transition-colors">
|
||||||
<p className="text-sm md:text-base font-bold text-slate-500 leading-tight">{captions[0]}</p>
|
<span className="text-3xl md:text-4xl select-none">👤</span>
|
||||||
</div>
|
<p className="text-[9px] font-black text-slate-400 uppercase tracking-[0.2em] m-0">
|
||||||
<div className="p-6 md:p-8 flex flex-col items-center text-center gap-3 bg-emerald-50/30 hover:bg-emerald-50/60 transition-colors">
|
Subject
|
||||||
<span className="text-3xl md:text-4xl select-none animate-pulse">✨</span>
|
</p>
|
||||||
<p className="text-[9px] font-black text-emerald-500 uppercase tracking-[0.2em] m-0">Temptation</p>
|
<p className="text-sm md:text-base font-bold text-slate-500 leading-tight">
|
||||||
<p className="text-sm md:text-base font-black text-slate-900 leading-tight">{captions[1]}</p>
|
{captions[0]}
|
||||||
</div>
|
</p>
|
||||||
<div className="p-6 md:p-8 flex flex-col items-center text-center gap-3 bg-red-50/30 hover:bg-red-50/60 transition-colors">
|
|
||||||
<span className="text-3xl md:text-4xl select-none">😤</span>
|
|
||||||
<p className="text-[9px] font-black text-red-500 uppercase tracking-[0.2em] m-0">Reality</p>
|
|
||||||
<p className="text-sm md:text-base font-bold text-red-600 leading-tight">{captions[2]}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<div className="p-6 md:p-8 flex flex-col items-center text-center gap-3 bg-emerald-50/30 hover:bg-emerald-50/60 transition-colors">
|
||||||
|
<span className="text-3xl md:text-4xl select-none animate-pulse">
|
||||||
|
✨
|
||||||
|
</span>
|
||||||
|
<p className="text-[9px] font-black text-emerald-500 uppercase tracking-[0.2em] m-0">
|
||||||
|
Temptation
|
||||||
|
</p>
|
||||||
|
<p className="text-sm md:text-base font-black text-slate-900 leading-tight">
|
||||||
|
{captions[1]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 md:p-8 flex flex-col items-center text-center gap-3 bg-red-50/30 hover:bg-red-50/60 transition-colors">
|
||||||
|
<span className="text-3xl md:text-4xl select-none">😤</span>
|
||||||
|
<p className="text-[9px] font-black text-red-500 uppercase tracking-[0.2em] m-0">
|
||||||
|
Reality
|
||||||
|
</p>
|
||||||
|
<p className="text-sm md:text-base font-bold text-red-600 leading-tight">
|
||||||
|
{captions[2]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function GenericMeme({ captions, template }: { captions: string[]; template: string }) {
|
function GenericMeme({
|
||||||
return (
|
captions,
|
||||||
<div className="p-8 md:p-12 text-center bg-gradient-to-br from-white to-slate-50/50">
|
template,
|
||||||
<p className="text-[10px] font-black text-slate-400 uppercase tracking-[0.3em] mb-8">{template}</p>
|
}: {
|
||||||
<div className="space-y-4">
|
captions: string[];
|
||||||
{(captions || []).map((caption, i) => (
|
template: string;
|
||||||
<div key={i} className="p-4 md:p-5 bg-white border border-slate-100 rounded-2xl shadow-sm group-hover:border-slate-200 transition-all duration-300">
|
}) {
|
||||||
<p className="text-base md:text-lg font-bold text-slate-700 m-0">
|
return (
|
||||||
{caption}
|
<div className="p-8 md:p-12 text-center bg-gradient-to-br from-white to-slate-50/50">
|
||||||
</p>
|
<p className="text-[10px] font-black text-slate-400 uppercase tracking-[0.3em] mb-8">
|
||||||
</div>
|
{template}
|
||||||
))}
|
</p>
|
||||||
</div>
|
<div className="space-y-4">
|
||||||
</div>
|
{(captions || []).map((caption, i) => (
|
||||||
);
|
<div
|
||||||
|
key={i}
|
||||||
|
className="p-4 md:p-5 bg-white border border-slate-100 rounded-2xl shadow-sm group-hover:border-slate-200 transition-all duration-300"
|
||||||
|
>
|
||||||
|
<p className="text-base md:text-lg font-bold text-slate-700 m-0">
|
||||||
|
{caption}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
315
apps/web/src/components/PayloadRichText.tsx
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
import { RichText } from "@payloadcms/richtext-lexical/react";
|
||||||
|
import type { JSXConverters } from "@payloadcms/richtext-lexical/react";
|
||||||
|
import { MemeCard } from "@/src/components/MemeCard";
|
||||||
|
import { Mermaid } from "@/src/components/Mermaid";
|
||||||
|
import { LeadMagnet } from "@/src/components/LeadMagnet";
|
||||||
|
import { ComparisonRow } from "@/src/components/Landing/ComparisonRow";
|
||||||
|
import { mdxComponents } from "../content-engine/components";
|
||||||
|
|
||||||
|
const jsxConverters: JSXConverters = {
|
||||||
|
blocks: {
|
||||||
|
memeCard: ({ node }: any) => (
|
||||||
|
<div className="my-8">
|
||||||
|
<MemeCard
|
||||||
|
template={node.fields.template}
|
||||||
|
captions={node.fields.captions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
mermaid: ({ node }: any) => (
|
||||||
|
<div className="my-8">
|
||||||
|
<Mermaid
|
||||||
|
id={node.fields.id}
|
||||||
|
title={node.fields.title}
|
||||||
|
showShare={node.fields.showShare}
|
||||||
|
>
|
||||||
|
{node.fields.chartDefinition}
|
||||||
|
</Mermaid>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
leadMagnet: ({ node }: any) => (
|
||||||
|
<div className="my-12">
|
||||||
|
<LeadMagnet
|
||||||
|
title={node.fields.title}
|
||||||
|
description={node.fields.description}
|
||||||
|
buttonText={node.fields.buttonText}
|
||||||
|
href={node.fields.href}
|
||||||
|
variant={node.fields.variant}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
comparisonRow: ({ node }: any) => (
|
||||||
|
<ComparisonRow
|
||||||
|
description={node.fields.description}
|
||||||
|
negativeLabel={node.fields.negativeLabel}
|
||||||
|
negativeText={node.fields.negativeText}
|
||||||
|
positiveLabel={node.fields.positiveLabel}
|
||||||
|
positiveText={node.fields.positiveText}
|
||||||
|
reverse={node.fields.reverse}
|
||||||
|
showShare={true}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
// --- MDX Registry Injections ---
|
||||||
|
leadParagraph: ({ node }: any) => (
|
||||||
|
<mdxComponents.LeadParagraph>
|
||||||
|
{node.fields.text}
|
||||||
|
</mdxComponents.LeadParagraph>
|
||||||
|
),
|
||||||
|
articleBlockquote: ({ node }: any) => (
|
||||||
|
<mdxComponents.ArticleBlockquote>
|
||||||
|
{node.fields.quote}
|
||||||
|
{node.fields.author && ` - ${node.fields.author}`}
|
||||||
|
</mdxComponents.ArticleBlockquote>
|
||||||
|
),
|
||||||
|
mintelH2: ({ node }: any) => (
|
||||||
|
<mdxComponents.H2>{node.fields.text}</mdxComponents.H2>
|
||||||
|
),
|
||||||
|
mintelH3: ({ node }: any) => (
|
||||||
|
<mdxComponents.H3>{node.fields.text}</mdxComponents.H3>
|
||||||
|
),
|
||||||
|
mintelHeading: ({ node }: any) => {
|
||||||
|
const displayLevel = node.fields.displayLevel || "h2";
|
||||||
|
if (displayLevel === "h3")
|
||||||
|
return <mdxComponents.H3>{node.fields.text}</mdxComponents.H3>;
|
||||||
|
return <mdxComponents.H2>{node.fields.text}</mdxComponents.H2>;
|
||||||
|
},
|
||||||
|
statsDisplay: ({ node }: any) => (
|
||||||
|
<mdxComponents.StatsDisplay
|
||||||
|
label={node.fields.label}
|
||||||
|
value={node.fields.value}
|
||||||
|
subtext={node.fields.subtext}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
diagramState: ({ node }: any) => (
|
||||||
|
<mdxComponents.DiagramState
|
||||||
|
states={[]}
|
||||||
|
transitions={[]}
|
||||||
|
caption={node.fields.definition}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
diagramTimeline: ({ node }: any) => (
|
||||||
|
<mdxComponents.DiagramTimeline
|
||||||
|
events={[]}
|
||||||
|
title={node.fields.definition}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
diagramGantt: ({ node }: any) => (
|
||||||
|
<mdxComponents.DiagramGantt tasks={[]} title={node.fields.definition} />
|
||||||
|
),
|
||||||
|
diagramPie: ({ node }: any) => (
|
||||||
|
<mdxComponents.DiagramPie data={[]} title={node.fields.definition} />
|
||||||
|
),
|
||||||
|
diagramSequence: ({ node }: any) => (
|
||||||
|
<mdxComponents.DiagramSequence
|
||||||
|
participants={[]}
|
||||||
|
steps={[]}
|
||||||
|
title={node.fields.definition}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
diagramFlow: ({ node }: any) => (
|
||||||
|
<mdxComponents.DiagramFlow
|
||||||
|
nodes={[]}
|
||||||
|
edges={[]}
|
||||||
|
title={node.fields.definition}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
|
||||||
|
waterfallChart: ({ node }: any) => (
|
||||||
|
<mdxComponents.WaterfallChart
|
||||||
|
title={node.fields.title}
|
||||||
|
events={node.fields.metrics || []}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
premiumComparisonChart: ({ node }: any) => (
|
||||||
|
<mdxComponents.PremiumComparisonChart
|
||||||
|
title={node.fields.title}
|
||||||
|
items={node.fields.datasets || []}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
iconList: ({ node }: any) => (
|
||||||
|
<mdxComponents.IconList>
|
||||||
|
{node.fields.items?.map((item: any, i: number) => (
|
||||||
|
// @ts-ignore
|
||||||
|
<mdxComponents.IconListItem
|
||||||
|
key={i}
|
||||||
|
icon={item.icon || "check"}
|
||||||
|
title={item.title}
|
||||||
|
>
|
||||||
|
{item.description}
|
||||||
|
</mdxComponents.IconListItem>
|
||||||
|
))}
|
||||||
|
</mdxComponents.IconList>
|
||||||
|
),
|
||||||
|
statsGrid: ({ node }: any) => {
|
||||||
|
const rawStats = node.fields.stats || [];
|
||||||
|
let statsStr = "";
|
||||||
|
if (Array.isArray(rawStats)) {
|
||||||
|
statsStr = rawStats
|
||||||
|
.map((s: any) => `${s.value || ""}|${s.label || ""}`)
|
||||||
|
.join("~");
|
||||||
|
} else if (typeof rawStats === "string") {
|
||||||
|
statsStr = rawStats;
|
||||||
|
}
|
||||||
|
return <mdxComponents.StatsGrid stats={statsStr} />;
|
||||||
|
},
|
||||||
|
metricBar: ({ node }: any) => (
|
||||||
|
<mdxComponents.MetricBar
|
||||||
|
label={node.fields.label}
|
||||||
|
value={node.fields.value}
|
||||||
|
color={node.fields.color as any}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
carousel: ({ node }: any) => (
|
||||||
|
<mdxComponents.Carousel
|
||||||
|
items={
|
||||||
|
node.fields.slides?.map((s: any) => ({
|
||||||
|
title: s.caption || "Image",
|
||||||
|
content: "",
|
||||||
|
icon: undefined,
|
||||||
|
})) || []
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
imageText: ({ node }: any) => (
|
||||||
|
<mdxComponents.ImageText
|
||||||
|
image={node.fields.image?.url || ""}
|
||||||
|
title="ImageText Component"
|
||||||
|
>
|
||||||
|
{node.fields.text}
|
||||||
|
</mdxComponents.ImageText>
|
||||||
|
),
|
||||||
|
revenueLossCalculator: ({ node }: any) => (
|
||||||
|
<mdxComponents.RevenueLossCalculator />
|
||||||
|
),
|
||||||
|
performanceChart: ({ node }: any) => <mdxComponents.PerformanceChart />,
|
||||||
|
performanceROICalculator: ({ node }: any) => (
|
||||||
|
<div className="not-prose my-12">
|
||||||
|
<mdxComponents.PerformanceROICalculator />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
loadTimeSimulator: ({ node }: any) => (
|
||||||
|
<div className="not-prose my-12">
|
||||||
|
<mdxComponents.LoadTimeSimulator />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
architectureBuilder: ({ node }: any) => (
|
||||||
|
<div className="not-prose my-12">
|
||||||
|
<mdxComponents.ArchitectureBuilder />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
digitalAssetVisualizer: ({ node }: any) => (
|
||||||
|
<div className="not-prose my-12">
|
||||||
|
<mdxComponents.DigitalAssetVisualizer />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
|
||||||
|
twitterEmbed: ({ node }: any) => (
|
||||||
|
<mdxComponents.TwitterEmbed
|
||||||
|
tweetId={node.fields.url?.split("/").pop() || ""}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
youTubeEmbed: ({ node }: any) => (
|
||||||
|
<mdxComponents.YouTubeEmbed
|
||||||
|
videoId={node.fields.videoId}
|
||||||
|
title={node.fields.title}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
linkedInEmbed: ({ node }: any) => (
|
||||||
|
<mdxComponents.LinkedInEmbed url={node.fields.url} />
|
||||||
|
),
|
||||||
|
externalLink: ({ node }: any) => (
|
||||||
|
<mdxComponents.ExternalLink href={node.fields.href}>
|
||||||
|
{node.fields.label}
|
||||||
|
</mdxComponents.ExternalLink>
|
||||||
|
),
|
||||||
|
trackedLink: ({ node }: any) => (
|
||||||
|
<mdxComponents.TrackedLink
|
||||||
|
href={node.fields.href}
|
||||||
|
eventName={node.fields.eventName}
|
||||||
|
>
|
||||||
|
{node.fields.label}
|
||||||
|
</mdxComponents.TrackedLink>
|
||||||
|
),
|
||||||
|
articleMeme: ({ node }: any) => (
|
||||||
|
<mdxComponents.ArticleMeme
|
||||||
|
template="drake"
|
||||||
|
captions={node.fields.caption || "Top|Bottom"}
|
||||||
|
image={node.fields.image?.url || undefined}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
marker: ({ node }: any) => (
|
||||||
|
<mdxComponents.Marker color={node.fields.color} delay={node.fields.delay}>
|
||||||
|
{node.fields.text}
|
||||||
|
</mdxComponents.Marker>
|
||||||
|
),
|
||||||
|
boldNumber: ({ node }: any) => (
|
||||||
|
<mdxComponents.BoldNumber
|
||||||
|
value={node.fields.value}
|
||||||
|
label={node.fields.label}
|
||||||
|
source={node.fields.source}
|
||||||
|
sourceUrl={node.fields.sourceUrl}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
webVitalsScore: ({ node }: any) => (
|
||||||
|
<mdxComponents.WebVitalsScore
|
||||||
|
values={{
|
||||||
|
lcp: node.fields.lcp,
|
||||||
|
inp: node.fields.inp,
|
||||||
|
cls: node.fields.cls,
|
||||||
|
}}
|
||||||
|
description={node.fields.description}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
buttonBlock: ({ node }: any) => (
|
||||||
|
<mdxComponents.Button
|
||||||
|
href={node.fields.href}
|
||||||
|
variant={node.fields.variant}
|
||||||
|
size={node.fields.size}
|
||||||
|
showArrow={node.fields.showArrow}
|
||||||
|
>
|
||||||
|
{node.fields.label}
|
||||||
|
</mdxComponents.Button>
|
||||||
|
),
|
||||||
|
articleQuote: ({ node }: any) => (
|
||||||
|
<mdxComponents.ArticleQuote
|
||||||
|
quote={node.fields.quote}
|
||||||
|
author={node.fields.author}
|
||||||
|
role={node.fields.role}
|
||||||
|
source={node.fields.source}
|
||||||
|
sourceUrl={node.fields.sourceUrl}
|
||||||
|
translated={node.fields.translated}
|
||||||
|
isCompany={node.fields.isCompany}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
reveal: ({ node }: any) => (
|
||||||
|
<mdxComponents.Reveal
|
||||||
|
direction={node.fields.direction}
|
||||||
|
delay={node.fields.delay}
|
||||||
|
>
|
||||||
|
{/* Reveal component takes children, which in MDX is nested content */}
|
||||||
|
<PayloadRichText data={node.fields.content} />
|
||||||
|
</mdxComponents.Reveal>
|
||||||
|
),
|
||||||
|
section: ({ node }: any) => (
|
||||||
|
<mdxComponents.Section title={node.fields.title}>
|
||||||
|
<PayloadRichText data={node.fields.content} />
|
||||||
|
</mdxComponents.Section>
|
||||||
|
),
|
||||||
|
tableOfContents: () => <mdxComponents.TableOfContents />,
|
||||||
|
faqSection: ({ node }: any) => (
|
||||||
|
<mdxComponents.FAQSection>
|
||||||
|
<PayloadRichText data={node.fields.content} />
|
||||||
|
</mdxComponents.FAQSection>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PayloadRichText({ data }: { data: any }) {
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="article-content max-w-none">
|
||||||
|
<RichText data={data} converters={jsxConverters} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
43
apps/web/src/components/TLDR.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface TLDRProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
content?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TLDR: React.FC<TLDRProps> = ({
|
||||||
|
children,
|
||||||
|
content,
|
||||||
|
className = "",
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`my-8 p-6 bg-slate-900 border-l-4 border-indigo-500 rounded-r-lg shadow-xl ${className}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="bg-indigo-500 text-white p-1 rounded">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M12 2v20M2 12h20M4.93 4.93l14.14 14.14M4.93 19.07l14.14-14.14" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-white font-bold text-lg uppercase tracking-wider">
|
||||||
|
TL;DR
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="text-slate-300 font-serif text-lg leading-relaxed italic">
|
||||||
|
{children || content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
/* eslint-disable react/prop-types */
|
import type { ThumbnailIcon } from "./blogThumbnails";
|
||||||
import type {
|
|
||||||
ThumbnailIcon,
|
|
||||||
} from "./blogThumbnails";
|
|
||||||
import { blogThumbnails } from "./blogThumbnails";
|
import { blogThumbnails } from "./blogThumbnails";
|
||||||
|
|
||||||
interface BlogThumbnailSVGProps {
|
interface BlogThumbnailSVGProps {
|
||||||
|
|||||||
@@ -1,40 +1,39 @@
|
|||||||
|
import { LeadParagraph } from "../components/ArticleParagraph";
|
||||||
|
import { H1, H2, H3 } from "../components/ArticleHeading";
|
||||||
|
import { Paragraph } from "../components/ArticleParagraph";
|
||||||
|
import { ArticleBlockquote } from "../components/ArticleBlockquote";
|
||||||
|
import { Marker } from "../components/Marker";
|
||||||
|
import { ComparisonRow } from "../components/Landing/ComparisonRow";
|
||||||
|
import { StatsDisplay } from "../components/StatsDisplay";
|
||||||
|
import { Mermaid } from "../components/Mermaid";
|
||||||
|
import { DiagramState } from "../components/DiagramState";
|
||||||
|
import { DiagramTimeline } from "../components/DiagramTimeline";
|
||||||
|
import { DiagramGantt } from "../components/DiagramGantt";
|
||||||
|
import { DiagramPie } from "../components/DiagramPie";
|
||||||
|
import { DiagramSequence } from "../components/DiagramSequence";
|
||||||
|
import { DiagramFlow } from "../components/DiagramFlow";
|
||||||
|
import { IconList, IconListItem } from "../components/IconList";
|
||||||
|
import { ArticleMeme } from "../components/ArticleMeme";
|
||||||
|
import { MemeCard } from "../components/MemeCard";
|
||||||
|
import { ExternalLink } from "../components/ExternalLink";
|
||||||
|
import { StatsGrid } from "../components/StatsGrid";
|
||||||
|
import { MetricBar } from "../components/MetricBar";
|
||||||
|
import { ArticleQuote } from "../components/ArticleQuote";
|
||||||
|
import { BoldNumber } from "../components/BoldNumber";
|
||||||
|
import { WebVitalsScore } from "../components/WebVitalsScore";
|
||||||
|
import { WaterfallChart } from "../components/WaterfallChart";
|
||||||
|
import { Button } from "../components/Button";
|
||||||
|
import { LeadMagnet } from "../components/LeadMagnet";
|
||||||
|
import { TrackedLink } from "../components/analytics/TrackedLink";
|
||||||
|
import { FAQSection } from "../components/FAQSection";
|
||||||
|
|
||||||
import { LeadParagraph } from '../components/ArticleParagraph';
|
import { PremiumComparisonChart } from "../components/PremiumComparisonChart";
|
||||||
import { H1, H2, H3 } from '../components/ArticleHeading';
|
import { ImageText } from "../components/ImageText";
|
||||||
import { Paragraph } from '../components/ArticleParagraph';
|
import { Carousel } from "../components/Carousel";
|
||||||
import { ArticleBlockquote } from '../components/ArticleBlockquote';
|
|
||||||
import { Marker } from '../components/Marker';
|
|
||||||
import { ComparisonRow } from '../components/Landing/ComparisonRow';
|
|
||||||
import { StatsDisplay } from '../components/StatsDisplay';
|
|
||||||
import { Mermaid } from '../components/Mermaid';
|
|
||||||
import { DiagramState } from '../components/DiagramState';
|
|
||||||
import { DiagramTimeline } from '../components/DiagramTimeline';
|
|
||||||
import { DiagramGantt } from '../components/DiagramGantt';
|
|
||||||
import { DiagramPie } from '../components/DiagramPie';
|
|
||||||
import { DiagramSequence } from '../components/DiagramSequence';
|
|
||||||
import { DiagramFlow } from '../components/DiagramFlow';
|
|
||||||
import { IconList, IconListItem } from '../components/IconList';
|
|
||||||
import { ArticleMeme } from '../components/ArticleMeme';
|
|
||||||
import { MemeCard } from '../components/MemeCard';
|
|
||||||
import { ExternalLink } from '../components/ExternalLink';
|
|
||||||
import { StatsGrid } from '../components/StatsGrid';
|
|
||||||
import { MetricBar } from '../components/MetricBar';
|
|
||||||
import { ArticleQuote } from '../components/ArticleQuote';
|
|
||||||
import { BoldNumber } from '../components/BoldNumber';
|
|
||||||
import { WebVitalsScore } from '../components/WebVitalsScore';
|
|
||||||
import { WaterfallChart } from '../components/WaterfallChart';
|
|
||||||
import { Button } from '../components/Button';
|
|
||||||
import { LeadMagnet } from '../components/LeadMagnet';
|
|
||||||
import { TrackedLink } from '../components/analytics/TrackedLink';
|
|
||||||
import { FAQSection } from '../components/FAQSection';
|
|
||||||
|
|
||||||
import { PremiumComparisonChart } from '../components/PremiumComparisonChart';
|
import { Section } from "../components/Section";
|
||||||
import { ImageText } from '../components/ImageText';
|
import { Reveal } from "../components/Reveal";
|
||||||
import { Carousel } from '../components/Carousel';
|
import { TableOfContents } from "../components/TableOfContents";
|
||||||
|
|
||||||
import { Section } from '../components/Section';
|
|
||||||
import { Reveal } from '../components/Reveal';
|
|
||||||
import { TableOfContents } from '../components/TableOfContents';
|
|
||||||
|
|
||||||
import { RevenueLossCalculator } from "../components/RevenueLossCalculator";
|
import { RevenueLossCalculator } from "../components/RevenueLossCalculator";
|
||||||
import { PerformanceChart } from "../components/PerformanceChart";
|
import { PerformanceChart } from "../components/PerformanceChart";
|
||||||
@@ -43,56 +42,61 @@ import { LoadTimeSimulator } from "../components/simulations/LoadTimeSimulator";
|
|||||||
import { ArchitectureBuilder } from "../components/simulations/ArchitectureBuilder";
|
import { ArchitectureBuilder } from "../components/simulations/ArchitectureBuilder";
|
||||||
import { DigitalAssetVisualizer } from "../components/simulations/DigitalAssetVisualizer";
|
import { DigitalAssetVisualizer } from "../components/simulations/DigitalAssetVisualizer";
|
||||||
|
|
||||||
import { TwitterEmbed } from '../components/TwitterEmbed';
|
import { TwitterEmbed } from "../components/TwitterEmbed";
|
||||||
import { YouTubeEmbed } from '../components/YouTubeEmbed';
|
import { YouTubeEmbed } from "../components/YouTubeEmbed";
|
||||||
import { LinkedInEmbed } from '../components/LinkedInEmbed';
|
import { LinkedInEmbed } from "../components/LinkedInEmbed";
|
||||||
|
import { TLDR } from "../components/TLDR";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single Source of Truth for MDX component rendering.
|
||||||
|
* Handled separately from Payload blocks to avoid SVG import issues in Node.js.
|
||||||
|
*/
|
||||||
export const mdxComponents = {
|
export const mdxComponents = {
|
||||||
// Named exports for explicit MDX usage
|
LeadParagraph,
|
||||||
LeadParagraph,
|
H1,
|
||||||
H1,
|
H2,
|
||||||
H2,
|
H3,
|
||||||
H3,
|
Paragraph,
|
||||||
Paragraph,
|
ArticleBlockquote,
|
||||||
ArticleBlockquote,
|
Marker,
|
||||||
Marker,
|
ComparisonRow,
|
||||||
ComparisonRow,
|
StatsDisplay,
|
||||||
StatsDisplay,
|
Mermaid,
|
||||||
Mermaid,
|
DiagramState,
|
||||||
DiagramState,
|
DiagramTimeline,
|
||||||
DiagramTimeline,
|
DiagramGantt,
|
||||||
DiagramGantt,
|
DiagramPie,
|
||||||
DiagramPie,
|
DiagramSequence,
|
||||||
DiagramSequence,
|
DiagramFlow,
|
||||||
DiagramFlow,
|
IconList,
|
||||||
IconList,
|
IconListItem,
|
||||||
IconListItem,
|
ArticleMeme,
|
||||||
ArticleMeme,
|
MemeCard,
|
||||||
MemeCard,
|
ExternalLink,
|
||||||
ExternalLink,
|
StatsGrid,
|
||||||
StatsGrid,
|
MetricBar,
|
||||||
MetricBar,
|
ArticleQuote,
|
||||||
ArticleQuote,
|
BoldNumber,
|
||||||
BoldNumber,
|
WebVitalsScore,
|
||||||
WebVitalsScore,
|
WaterfallChart,
|
||||||
WaterfallChart,
|
PremiumComparisonChart,
|
||||||
PremiumComparisonChart,
|
ImageText,
|
||||||
ImageText,
|
Carousel,
|
||||||
Carousel,
|
Section,
|
||||||
Section,
|
Reveal,
|
||||||
Reveal,
|
TableOfContents,
|
||||||
TableOfContents,
|
RevenueLossCalculator,
|
||||||
RevenueLossCalculator,
|
PerformanceChart,
|
||||||
PerformanceChart,
|
PerformanceROICalculator,
|
||||||
PerformanceROICalculator,
|
LoadTimeSimulator,
|
||||||
LoadTimeSimulator,
|
ArchitectureBuilder,
|
||||||
ArchitectureBuilder,
|
DigitalAssetVisualizer,
|
||||||
DigitalAssetVisualizer,
|
TwitterEmbed,
|
||||||
TwitterEmbed,
|
YouTubeEmbed,
|
||||||
YouTubeEmbed,
|
LinkedInEmbed,
|
||||||
LinkedInEmbed,
|
Button,
|
||||||
Button,
|
LeadMagnet,
|
||||||
LeadMagnet,
|
TrackedLink,
|
||||||
TrackedLink,
|
FAQSection,
|
||||||
FAQSection
|
TLDR,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,296 +1,9 @@
|
|||||||
|
import { ComponentDefinition } from "@mintel/content-engine";
|
||||||
import { ComponentDefinition } from '@mintel/content-engine';
|
import { allComponentDefinitions } from "../payload/blocks/allBlocks";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Single Source of Truth for all MDX component definitions.
|
* Single Source of Truth for all MDX component definitions.
|
||||||
* Used by:
|
* Now dynamically generated from individual Payload block definitions.
|
||||||
* - content-engine.config.ts (for the optimization script)
|
|
||||||
* - The AI content pipeline (for component injection)
|
|
||||||
*
|
|
||||||
* Keep in sync with: src/content-engine/components.ts (the MDX runtime registry)
|
|
||||||
*/
|
*/
|
||||||
export const componentDefinitions: ComponentDefinition[] = [
|
export const componentDefinitions: ComponentDefinition[] =
|
||||||
{
|
allComponentDefinitions;
|
||||||
name: 'LeadParagraph',
|
|
||||||
description: 'Larger, emphasized paragraph for the article introduction. Use 1-3 at the start.',
|
|
||||||
usageExample: '<LeadParagraph>\n Unternehmen investieren oft Unsummen in glänzende Oberflächen, während das technische Fundament bröckelt.\n</LeadParagraph>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'H2',
|
|
||||||
description: 'Main section heading. Used for top-level content sections.',
|
|
||||||
usageExample: '<H2>Der wirtschaftliche Case</H2>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'BoldNumber',
|
|
||||||
description: 'Large centerpiece number with label for primary statistics.',
|
|
||||||
usageExample: '<BoldNumber value="5x" label="höhere Conversion-Rate" source="Portent" />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'PremiumComparisonChart',
|
|
||||||
description: 'Advanced chart for comparing performance metrics with industrial aesthetics.',
|
|
||||||
usageExample: '<PremiumComparisonChart title="TTFB Vergleich" items={[{ label: "Alt", value: 800, max: 1000, color: "red" }, { label: "Mintel", value: 50, max: 1000, color: "green" }]} />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ImageText',
|
|
||||||
description: 'Layout component for image next to explanatory text.',
|
|
||||||
usageExample: '<ImageText image="/img.jpg" title="Architektur">Erklärung...</ImageText>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Carousel',
|
|
||||||
description: 'Interactive swipeable slider for multi-step explanations. IMPORTANT: items array must contain at least 2 items with substantive title and content text (no empty content).',
|
|
||||||
usageExample: '<Carousel items={[{ title: "Schritt 1", content: "Analyse der aktuellen Performance..." }, { title: "Schritt 2", content: "Architektur-Optimierung..." }]} />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'H3',
|
|
||||||
description: 'Subsection heading. Used within H2 sections.',
|
|
||||||
usageExample: '<H3>Die drei Säulen meiner Umsetzung</H3>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Paragraph',
|
|
||||||
description: 'Standard body text paragraph. All body text must be wrapped in this.',
|
|
||||||
usageExample: '<Paragraph>\n Mein System ist kein Kostenfaktor, sondern ein <Marker>ROI-Beschleuniger</Marker>.\n</Paragraph>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ArticleBlockquote',
|
|
||||||
description: 'Styled blockquote for expert quotes or key statements.',
|
|
||||||
usageExample: '<ArticleBlockquote>\n Performance ist keine IT-Kennzahl, sondern ein ökonomischer Hebel.\n</ArticleBlockquote>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Marker',
|
|
||||||
description: 'Inline highlight (yellow marker effect) for emphasizing key phrases within paragraphs.',
|
|
||||||
usageExample: '<Marker>entscheidender Wettbewerbsvorteil</Marker>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ComparisonRow',
|
|
||||||
description: 'Side-by-side comparison: negative "Standard" approach vs positive "Mintel" approach. Props include showShare boolean.',
|
|
||||||
usageExample: `<ComparisonRow
|
|
||||||
description="Architektur-Vergleich"
|
|
||||||
negativeLabel="Legacy CMS"
|
|
||||||
negativeText="Langsame Datenbankabfragen, verwundbare Plugins."
|
|
||||||
positiveLabel="Mintel Stack"
|
|
||||||
positiveText="Statische Generierung, perfekte Sicherheit."
|
|
||||||
showShare={true}
|
|
||||||
/>`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'StatsDisplay',
|
|
||||||
description: 'A single large stat card with prominent value, label, and optional subtext.',
|
|
||||||
usageExample: '<StatsDisplay value="-20%" label="Conversion" subtext="Jede Sekunde Verzögerung kostet." />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Mermaid',
|
|
||||||
description: 'Renders a Mermaid.js diagram (flowchart, sequence, pie, etc.). Diagram code goes as children. Keep it tiny (max 3-4 nodes). Wrap in div with className="my-8".',
|
|
||||||
usageExample: `<div className="my-8">
|
|
||||||
<Mermaid id="my-diagram" title="System Architecture" showShare={true}>
|
|
||||||
graph TD
|
|
||||||
A["Request"] --> B["CDN Edge"]
|
|
||||||
B --> C["Static HTML"]
|
|
||||||
</Mermaid>
|
|
||||||
</div>`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'DiagramFlow',
|
|
||||||
description: 'Structured flowchart diagram. Use for process flows, architecture diagrams, etc. Supports structured nodes/edges. direction defaults to LR.',
|
|
||||||
usageExample: `<DiagramFlow
|
|
||||||
nodes={[
|
|
||||||
{ id: "A", label: "Start" },
|
|
||||||
{ id: "B", label: "Process", style: "fill:#f00" }
|
|
||||||
]}
|
|
||||||
edges={[
|
|
||||||
{ from: "A", to: "B", label: "trigger" }
|
|
||||||
]}
|
|
||||||
title="Process Flow"
|
|
||||||
id="flow-1"
|
|
||||||
showShare={true}
|
|
||||||
/>`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'DiagramPie',
|
|
||||||
description: 'Pie chart with structured data props.',
|
|
||||||
usageExample: `<DiagramPie
|
|
||||||
data={[
|
|
||||||
{ label: "JavaScript", value: 35 },
|
|
||||||
{ label: "CSS", value: 25 },
|
|
||||||
{ label: "Images", value: 20 }
|
|
||||||
]}
|
|
||||||
title="Performance Bottlenecks"
|
|
||||||
id="perf-pie"
|
|
||||||
showShare={true}
|
|
||||||
/>`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'DiagramGantt',
|
|
||||||
description: 'Gantt timeline chart comparing durations of tasks.',
|
|
||||||
usageExample: `<DiagramGantt
|
|
||||||
tasks={[
|
|
||||||
{ id: "task-1", name: "Legacy: 4 Wochen", start: "2024-01-01", duration: "4w" },
|
|
||||||
{ id: "task-2", name: "Mintel: 1 Woche", start: "2024-01-01", duration: "1w" }
|
|
||||||
]}
|
|
||||||
title="Zeitvergleich"
|
|
||||||
id="gantt-comparison"
|
|
||||||
showShare={true}
|
|
||||||
/>`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'DiagramState',
|
|
||||||
description: 'State diagram showing states and transitions.',
|
|
||||||
usageExample: `<DiagramState
|
|
||||||
states={["Idle", "Loading", "Loaded", "Error"]}
|
|
||||||
transitions={[
|
|
||||||
{ from: "Idle", to: "Loading", label: "fetch" },
|
|
||||||
{ from: "Loading", to: "Loaded", label: "success" }
|
|
||||||
]}
|
|
||||||
initialState="Idle"
|
|
||||||
title="Request Lifecycle"
|
|
||||||
id="state-diagram"
|
|
||||||
showShare={true}
|
|
||||||
/>`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'DiagramSequence',
|
|
||||||
description: 'Sequence diagram (uses raw Mermaid sequence syntax as children).',
|
|
||||||
usageExample: `<DiagramSequence id="seq-diagram" title="Request Flow" showShare={true}>
|
|
||||||
sequenceDiagram
|
|
||||||
Browser->>CDN: GET /page
|
|
||||||
CDN->>Browser: Static HTML (< 50ms)
|
|
||||||
</DiagramSequence>`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'DiagramTimeline',
|
|
||||||
description: 'Timeline diagram (uses raw Mermaid timeline syntax as children).',
|
|
||||||
usageExample: `<DiagramTimeline id="timeline" title="Project Timeline" showShare={true}>
|
|
||||||
timeline
|
|
||||||
2024 : Planung
|
|
||||||
2025 : Entwicklung
|
|
||||||
2026 : Launch
|
|
||||||
</DiagramTimeline>`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'IconList',
|
|
||||||
description: 'Checklist with check/cross icons. Wrap IconListItem children inside.',
|
|
||||||
usageExample: `<IconList>
|
|
||||||
<IconListItem check>
|
|
||||||
<strong>Zero-Computation:</strong> Statische Seiten, kein Serverwarten.
|
|
||||||
</IconListItem>
|
|
||||||
<IconListItem cross>
|
|
||||||
<strong>Legacy CMS:</strong> Datenbankabfragen bei jedem Request.
|
|
||||||
</IconListItem>
|
|
||||||
</IconList>`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ArticleMeme',
|
|
||||||
description: 'Real meme image from memegen.link. template must be a valid memegen.link ID. IMPORTANT: Captions must be EXTREMELY SARCASTIC and PUNCHY (mocking bad B2B agencies, max 6 words per line). Best templates: drake (2-line prefer/dislike), gru (4-step plan backfire), disastergirl (burning house), fine (this is fine dog). Use German captions. Wrap in div with className="my-8".',
|
|
||||||
usageExample: `<div className="my-8">
|
|
||||||
<ArticleMeme template="drake" captions="47 WordPress Plugins installieren|Eine saubere Serverless Architektur" />
|
|
||||||
</div>`
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: 'Section',
|
|
||||||
description: 'Wraps a thematic section block with optional heading.',
|
|
||||||
usageExample: '<Section>\n <h3>Section Title</h3>\n <p>Content here.</p>\n</Section>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Reveal',
|
|
||||||
description: 'Scroll-triggered reveal animation wrapper. Wrap any content to animate on scroll.',
|
|
||||||
usageExample: '<Reveal>\n <StatsDisplay value="100" label="PageSpeed Score" />\n</Reveal>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'StatsGrid',
|
|
||||||
description: 'Grid of 2–4 stat cards in a row. Use tilde (~) to separate stats, pipe (|) to separate value|label|subtext within each stat.',
|
|
||||||
usageExample: '<StatsGrid stats="53%|Mehr Umsatz|Rakuten 24~33%|Conversion Boost|nach CWV Fix~24%|Top 3 Ranking|bei bestandenen CWV" />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'MetricBar',
|
|
||||||
description: 'Animated horizontal progress bar. Use multiple in sequence to compare metrics. IMPORTANT: value MUST be a real number > 0, never use 0 or placeholder values. Props: label, value (number), max (default 100), unit (default %), color (red|green|blue|slate).',
|
|
||||||
usageExample: `<MetricBar label="WordPress Sites" value={33} color="red" />
|
|
||||||
<MetricBar label="Static Sites" value={92} color="green" />`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ArticleQuote',
|
|
||||||
description: 'Dark-themed quote card. Use for expert quotes or statements. Use isCompany={true} for brands/orgs to show an entity icon instead of personal initials. MANDATORY: always include source and sourceUrl for verifiability. Props: quote, author, role (optional), source (REQUIRED), sourceUrl (REQUIRED), isCompany (optional), translated (optional boolean).',
|
|
||||||
usageExample: '<ArticleQuote quote="Optimizing for speed." author="Google" isCompany={true} source="web.dev" sourceUrl="https://web.dev" translated={true} />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'BoldNumber',
|
|
||||||
description: 'Full-width hero number card with dark gradient, animated count-up, and share button. Use for the most impactful single statistics. IMPORTANT: Always provide source and sourceUrl. Numbers without comparison context should use PremiumComparisonChart or paired MetricBar instead. Props: value (string like "53%" or "2.5M€"), label (short description), source (REQUIRED), sourceUrl (REQUIRED).',
|
|
||||||
usageExample: '<BoldNumber value="8.4%" label="Conversion-Steigerung pro 0.1s schnellere Ladezeit" source="Deloitte Digital" sourceUrl="https://www2.deloitte.com/..." />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'WebVitalsScore',
|
|
||||||
description: 'Displays Core Web Vitals (LCP, INP, CLS) in a premium card layout with automatic traffic light coloring (Good/Needs Improvement/Poor). Use for performance audits or comparisons.',
|
|
||||||
usageExample: '<WebVitalsScore values={{ lcp: 2.5, inp: 200, cls: 0.1 }} description="All metrics passing Google standards." />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'WaterfallChart',
|
|
||||||
description: 'A timeline visualization of network requests (waterfall). Use to show loading sequences or bottlenecks. Labels auto-color coded by type (JS, HTML, IMG).',
|
|
||||||
usageExample: `<WaterfallChart
|
|
||||||
title="Initial Load"
|
|
||||||
events={[
|
|
||||||
{ name: "Document", start: 0, duration: 150 },
|
|
||||||
{ name: "main.js", start: 150, duration: 50 },
|
|
||||||
{ name: "hero.jpg", start: 200, duration: 300 }
|
|
||||||
]}
|
|
||||||
/>`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ExternalLink',
|
|
||||||
description: 'Inline external link with ↗ icon and outbound analytics tracking. Use for all source citations and external references within Paragraph text.',
|
|
||||||
usageExample: '<ExternalLink href="https://web.dev/articles/vitals">Google Core Web Vitals</ExternalLink>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'TwitterEmbed',
|
|
||||||
description: 'Embeds a post from X.com (Twitter). Used to provide social proof, industry quotes, or examples. Provide the numerical tweetId.',
|
|
||||||
usageExample: '<TwitterEmbed tweetId="1753464161943834945" theme="light" />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'YouTubeEmbed',
|
|
||||||
description: 'Embeds a YouTube video to visualize concepts or provide deep dives. Use the 11-character videoId.',
|
|
||||||
usageExample: '<YouTubeEmbed videoId="dQw4w9WgXcQ" title="Performance Explanation" />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'LinkedInEmbed',
|
|
||||||
description: 'Embeds a professional post from LinkedIn. Use the activity URN (e.g. urn:li:activity:1234567890).',
|
|
||||||
usageExample: '<LinkedInEmbed urn="urn:li:activity:7153664326573674496" />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'TrackedLink',
|
|
||||||
description: 'A wrapper around next/link that tracks clicks. Use for all INTERNAL navigational links that should be tracked.',
|
|
||||||
usageExample: '<TrackedLink href="/contact" className="text-blue-600 font-bold">Jetzt anfragen</TrackedLink>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Button',
|
|
||||||
description: 'DEPRECATED: Use <LeadMagnet /> instead for main CTAs. Only use for small secondary links.',
|
|
||||||
usageExample: '<Button href="/contact" variant="outline">Webprojekt anfragen</Button>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'LeadMagnet',
|
|
||||||
description: 'Premium B2B conversion card. Use 1-2 per article as main high-impact CTAs. Props: title (strong headline), description (value prop), buttonText (action), href (link), variant (performance|security|standard).',
|
|
||||||
usageExample: '<LeadMagnet title="Performance-Check anfragen" description="Wir analysieren Ihre Core Web Vitals und decken Umsatzpotenziale auf." buttonText="Jetzt analysieren lassen" href="/contact" variant="performance" />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'PerformanceROICalculator',
|
|
||||||
description: 'Interactive simulation calculator showing the monetary ROI of improving load times (based on Deloitte B2B metrics). Use exactly once in performance-related articles to provide a highly engaging simulation. Requires no props.',
|
|
||||||
usageExample: '<PerformanceROICalculator />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'LoadTimeSimulator',
|
|
||||||
description: 'Interactive visual race simulating the loading experience of a slow legacy CMS vs a fast headless stack. Great for articles discussing load times, technical debt, or user frustration. Requires no props.',
|
|
||||||
usageExample: '<LoadTimeSimulator />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'FAQSection',
|
|
||||||
description: 'Semantic wrapper for FAQ questions at the end of the article. Put standard Markdown H3/Paragraphs inside.',
|
|
||||||
usageExample: '<FAQSection>\n <H3>Frage 1</H3>\n <Paragraph>Antwort 1</Paragraph>\n</FAQSection>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ArchitectureBuilder',
|
|
||||||
description: 'Interactive comparison between a standard SaaS rental approach and a custom Built-First (Mintel) architecture. Useful for articles discussing digital ownership, software rent vs. build, or technological assets. Requires no props.',
|
|
||||||
usageExample: '<ArchitectureBuilder />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'DigitalAssetVisualizer',
|
|
||||||
description: 'Interactive visualization illustrating the financial difference between software as a liability (SaaS/rent) and software as a digital asset (Custom IP). Great for articles concerning CTO strategies, business value of code, and digital independence. Requires no props.',
|
|
||||||
usageExample: '<DigitalAssetVisualizer />'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|||||||
@@ -9,22 +9,50 @@ export async function getAllPosts() {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = await getPayload({ config: configPromise });
|
try {
|
||||||
const { docs } = await payload.find({
|
const payload = await getPayload({ config: configPromise });
|
||||||
collection: "posts",
|
const { docs } = await payload.find({
|
||||||
limit: 1000,
|
collection: "posts",
|
||||||
sort: "-date",
|
limit: 1000,
|
||||||
});
|
sort: "-date",
|
||||||
|
where: {
|
||||||
|
and: [
|
||||||
|
{
|
||||||
|
_status: {
|
||||||
|
equals: "published",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: {
|
||||||
|
less_than_equal: new Date(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return docs.map((doc) => ({
|
return docs.map((doc) => ({
|
||||||
title: doc.title as string,
|
title: doc.title as string,
|
||||||
description: doc.description as string,
|
description: doc.description as string,
|
||||||
date: doc.date as string,
|
date: doc.date as string,
|
||||||
tags: (doc.tags || []).map((t) =>
|
tags: (doc.tags || []).map((t) =>
|
||||||
typeof t === "object" && t !== null ? t.tag : t,
|
typeof t === "object" && t !== null ? t.tag : t,
|
||||||
) as string[],
|
) as string[],
|
||||||
slug: doc.slug as string,
|
slug: doc.slug as string,
|
||||||
thumbnail: doc.thumbnail as string,
|
thumbnail:
|
||||||
body: { code: doc.content as string },
|
(doc.featuredImage &&
|
||||||
}));
|
typeof doc.featuredImage === "object" &&
|
||||||
|
doc.featuredImage.url
|
||||||
|
? doc.featuredImage.url
|
||||||
|
: "") || "",
|
||||||
|
body: { code: "" as string },
|
||||||
|
lexicalContent: doc.content || null,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
"⚠️ Bypassing Payload fetch during build: Database connection refused.",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
191
apps/web/src/payload/actions/generateField.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { config } from "../../../content-engine.config";
|
||||||
|
import { getPayloadHMR } from "@payloadcms/next/utilities";
|
||||||
|
import configPromise from "@payload-config";
|
||||||
|
import * as fs from "node:fs/promises";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import * as os from "node:os";
|
||||||
|
|
||||||
|
async function getOrchestrator() {
|
||||||
|
const OPENROUTER_KEY =
|
||||||
|
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
|
||||||
|
const REPLICATE_KEY = process.env.REPLICATE_API_KEY;
|
||||||
|
|
||||||
|
if (!OPENROUTER_KEY) {
|
||||||
|
throw new Error(
|
||||||
|
"Missing OPENROUTER_API_KEY in .env (Required for AI generation)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const importDynamic = new Function("modulePath", "return import(modulePath)");
|
||||||
|
const { AiBlogPostOrchestrator } = await importDynamic(
|
||||||
|
"@mintel/content-engine",
|
||||||
|
);
|
||||||
|
|
||||||
|
return new AiBlogPostOrchestrator({
|
||||||
|
apiKey: OPENROUTER_KEY,
|
||||||
|
replicateApiKey: REPLICATE_KEY,
|
||||||
|
model: "google/gemini-3-flash-preview",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateSlugAction(
|
||||||
|
title: string,
|
||||||
|
draftContent: string,
|
||||||
|
oldSlug?: string,
|
||||||
|
instructions?: string,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const orchestrator = await getOrchestrator();
|
||||||
|
const newSlug = await orchestrator.generateSlug(
|
||||||
|
draftContent,
|
||||||
|
title,
|
||||||
|
instructions,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (oldSlug && oldSlug !== newSlug) {
|
||||||
|
const payload = await getPayloadHMR({ config: configPromise });
|
||||||
|
await payload.create({
|
||||||
|
collection: "redirects",
|
||||||
|
data: {
|
||||||
|
from: oldSlug,
|
||||||
|
to: newSlug,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, slug: newSlug };
|
||||||
|
} catch (e: any) {
|
||||||
|
return { success: false, error: e.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateThumbnailAction(
|
||||||
|
draftContent: string,
|
||||||
|
title?: string,
|
||||||
|
instructions?: string,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const payload = await getPayloadHMR({ config: configPromise });
|
||||||
|
const OPENROUTER_KEY =
|
||||||
|
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
|
||||||
|
const REPLICATE_KEY = process.env.REPLICATE_API_KEY;
|
||||||
|
|
||||||
|
if (!OPENROUTER_KEY) {
|
||||||
|
throw new Error("Missing OPENROUTER_API_KEY in .env");
|
||||||
|
}
|
||||||
|
if (!REPLICATE_KEY) {
|
||||||
|
throw new Error(
|
||||||
|
"Missing REPLICATE_API_KEY in .env (Required for Thumbnails)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const importDynamic = new Function(
|
||||||
|
"modulePath",
|
||||||
|
"return import(modulePath)",
|
||||||
|
);
|
||||||
|
const { AiBlogPostOrchestrator } = await importDynamic(
|
||||||
|
"@mintel/content-engine",
|
||||||
|
);
|
||||||
|
const { ThumbnailGenerator } = await importDynamic(
|
||||||
|
"@mintel/thumbnail-generator",
|
||||||
|
);
|
||||||
|
|
||||||
|
const orchestrator = new AiBlogPostOrchestrator({
|
||||||
|
apiKey: OPENROUTER_KEY,
|
||||||
|
replicateApiKey: REPLICATE_KEY,
|
||||||
|
model: "google/gemini-3-flash-preview",
|
||||||
|
});
|
||||||
|
|
||||||
|
const tg = new ThumbnailGenerator({ replicateApiKey: REPLICATE_KEY });
|
||||||
|
|
||||||
|
const prompt = await orchestrator.generateVisualPrompt(
|
||||||
|
draftContent || title || "Technology",
|
||||||
|
instructions,
|
||||||
|
);
|
||||||
|
|
||||||
|
const tmpPath = path.join(os.tmpdir(), `mintel-thumb-${Date.now()}.png`);
|
||||||
|
await tg.generateImage(prompt, tmpPath);
|
||||||
|
|
||||||
|
const fileData = await fs.readFile(tmpPath);
|
||||||
|
const stat = await fs.stat(tmpPath);
|
||||||
|
const fileName = path.basename(tmpPath);
|
||||||
|
|
||||||
|
const newMedia = await payload.create({
|
||||||
|
collection: "media",
|
||||||
|
data: {
|
||||||
|
alt: title ? `Thumbnail for ${title}` : "AI Generated Thumbnail",
|
||||||
|
},
|
||||||
|
file: {
|
||||||
|
data: fileData,
|
||||||
|
name: fileName,
|
||||||
|
mimetype: "image/png",
|
||||||
|
size: stat.size,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup temp file
|
||||||
|
await fs.unlink(tmpPath).catch(() => {});
|
||||||
|
|
||||||
|
return { success: true, mediaId: newMedia.id };
|
||||||
|
} catch (e: any) {
|
||||||
|
return { success: false, error: e.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function generateSingleFieldAction(
|
||||||
|
documentTitle: string,
|
||||||
|
documentContent: string,
|
||||||
|
fieldName: string,
|
||||||
|
fieldDescription: string,
|
||||||
|
instructions?: string,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const OPENROUTER_KEY =
|
||||||
|
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
|
||||||
|
if (!OPENROUTER_KEY) throw new Error("Missing OPENROUTER_API_KEY");
|
||||||
|
|
||||||
|
const payload = await getPayloadHMR({ config: configPromise });
|
||||||
|
|
||||||
|
// Fetch context documents from DB
|
||||||
|
const contextDocsData = await payload.find({
|
||||||
|
collection: "context-files",
|
||||||
|
limit: 100,
|
||||||
|
});
|
||||||
|
const projectContext = contextDocsData.docs
|
||||||
|
.map((doc) => `--- ${doc.filename} ---\n${doc.content}`)
|
||||||
|
.join("\n\n");
|
||||||
|
|
||||||
|
const prompt = `You are an expert AI assistant perfectly trained for generating exact data values for CMS components.
|
||||||
|
PROJECT STRATEGY & CONTEXT:
|
||||||
|
${projectContext}
|
||||||
|
|
||||||
|
DOCUMENT TITLE: ${documentTitle}
|
||||||
|
DOCUMENT DRAFT:\n${documentContent}\n
|
||||||
|
YOUR TASK: Generate the exact value for a specific field named "${fieldName}".
|
||||||
|
${fieldDescription ? `FIELD DESCRIPTION / CONSTRAINTS: ${fieldDescription}\n` : ""}
|
||||||
|
${instructions ? `EDITOR INSTRUCTIONS for this field: ${instructions}\n` : ""}
|
||||||
|
CRITICAL RULES:
|
||||||
|
1. Respond ONLY with the requested content value.
|
||||||
|
2. NO markdown wrapping blocks (like \`\`\`mermaid or \`\`\`html) around the output! Just the raw code or text.
|
||||||
|
3. If the field implies a diagram or flow, output RAW Mermaid.js code.
|
||||||
|
4. If it's standard text, write professional B2B German. No quotes, no conversational filler.`;
|
||||||
|
|
||||||
|
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${OPENROUTER_KEY}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: "google/gemini-3-flash-preview",
|
||||||
|
messages: [{ role: "user", content: prompt }],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
const text = data.choices?.[0]?.message?.content?.trim() || "";
|
||||||
|
return { success: true, text };
|
||||||
|
} catch (e: any) {
|
||||||
|
return { success: false, error: e.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
88
apps/web/src/payload/actions/optimizePost.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { config } from "../../../content-engine.config";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { parseMarkdownToLexical } from "../utils/lexicalParser";
|
||||||
|
import { getPayloadHMR } from "@payloadcms/next/utilities";
|
||||||
|
import configPromise from "@payload-config";
|
||||||
|
|
||||||
|
export async function optimizePostText(
|
||||||
|
draftContent: string,
|
||||||
|
instructions?: string,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const payload = await getPayloadHMR({ config: configPromise });
|
||||||
|
const globalAiSettings = await payload.findGlobal({ slug: "ai-settings" });
|
||||||
|
const customSources =
|
||||||
|
globalAiSettings?.customSources?.map((s: any) => s.sourceName) || [];
|
||||||
|
|
||||||
|
const OPENROUTER_KEY =
|
||||||
|
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
|
||||||
|
const REPLICATE_KEY = process.env.REPLICATE_API_KEY;
|
||||||
|
|
||||||
|
if (!OPENROUTER_KEY) {
|
||||||
|
throw new Error(
|
||||||
|
"OPENROUTER_KEY or OPENROUTER_API_KEY not found in environment.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const importDynamic = new Function(
|
||||||
|
"modulePath",
|
||||||
|
"return import(modulePath)",
|
||||||
|
);
|
||||||
|
const { AiBlogPostOrchestrator } = await importDynamic(
|
||||||
|
"@mintel/content-engine",
|
||||||
|
);
|
||||||
|
|
||||||
|
const orchestrator = new AiBlogPostOrchestrator({
|
||||||
|
apiKey: OPENROUTER_KEY,
|
||||||
|
replicateApiKey: REPLICATE_KEY,
|
||||||
|
model: "google/gemini-3-flash-preview",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch context documents purely from DB
|
||||||
|
const contextDocsData = await payload.find({
|
||||||
|
collection: "context-files",
|
||||||
|
limit: 100,
|
||||||
|
});
|
||||||
|
const projectContext = contextDocsData.docs.map((doc) => doc.content);
|
||||||
|
|
||||||
|
const optimizedMarkdown = await orchestrator.optimizeDocument({
|
||||||
|
content: draftContent,
|
||||||
|
projectContext,
|
||||||
|
availableComponents: config.components,
|
||||||
|
instructions,
|
||||||
|
internalLinks: [],
|
||||||
|
customSources,
|
||||||
|
});
|
||||||
|
|
||||||
|
// The orchestrator currently returns Markdown + JSX tags.
|
||||||
|
// We convert this mixed string into a basic Lexical AST map.
|
||||||
|
|
||||||
|
if (!optimizedMarkdown || typeof optimizedMarkdown !== "string") {
|
||||||
|
throw new Error("AI returned invalid markup.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const blocks = parseMarkdownToLexical(optimizedMarkdown);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
lexicalAST: {
|
||||||
|
root: {
|
||||||
|
type: "root",
|
||||||
|
format: "",
|
||||||
|
indent: 0,
|
||||||
|
version: 1,
|
||||||
|
children: blocks,
|
||||||
|
direction: "ltr",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Failed to optimize post:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message || "An unknown error occurred during optimization.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
28
apps/web/src/payload/blocks/ArchitectureBuilderBlock.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const ArchitectureBuilderBlock: MintelBlock = {
|
||||||
|
slug: "architectureBuilder",
|
||||||
|
labels: {
|
||||||
|
singular: "Architecture Builder",
|
||||||
|
plural: "Architecture Builders",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "ArchitectureBuilder",
|
||||||
|
description:
|
||||||
|
"Interactive comparison between a standard SaaS rental approach and a custom Built-First (Mintel) architecture. Useful for articles discussing digital ownership, software rent vs. build, or technological assets. Requires no props.",
|
||||||
|
usageExample: "'<ArchitectureBuilder />'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "preset",
|
||||||
|
type: "text",
|
||||||
|
defaultValue: "standard",
|
||||||
|
admin: { description: "Geben Sie den Text für preset ein." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
59
apps/web/src/payload/blocks/ArticleBlockquoteBlock.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const ArticleBlockquoteBlock: MintelBlock = {
|
||||||
|
slug: "articleBlockquote",
|
||||||
|
labels: {
|
||||||
|
singular: "Article Blockquote",
|
||||||
|
plural: "Article Blockquotes",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "ArticleBlockquote",
|
||||||
|
description: "Styled blockquote for expert quotes or key statements.",
|
||||||
|
usageExample:
|
||||||
|
"'<ArticleBlockquote>\n Performance ist keine IT-Kennzahl, sondern ein ökonomischer Hebel.\n</ArticleBlockquote>'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "quote",
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den mehrzeiligen Text für quote ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "author",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für author ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "role",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für role ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
54
apps/web/src/payload/blocks/ArticleMemeBlock.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const ArticleMemeBlock: MintelBlock = {
|
||||||
|
slug: "articleMeme",
|
||||||
|
labels: {
|
||||||
|
singular: "Article Meme",
|
||||||
|
plural: "Article Memes",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "ArticleMeme",
|
||||||
|
description:
|
||||||
|
"Real image-based meme from the media library. Use for static screenshots or custom memes that are not available via memegen.link.",
|
||||||
|
usageExample:
|
||||||
|
'<ArticleMeme image="/media/my-meme.png" alt="Sarcastic dev meme" caption="When the code finally builds." />',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "image",
|
||||||
|
type: "upload",
|
||||||
|
relationTo: "media",
|
||||||
|
required: true,
|
||||||
|
admin: { description: "Laden Sie die Datei für image hoch." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "alt",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für alt ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "caption",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für caption ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
97
apps/web/src/payload/blocks/ArticleQuoteBlock.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const ArticleQuoteBlock: MintelBlock = {
|
||||||
|
slug: "articleQuote",
|
||||||
|
labels: {
|
||||||
|
singular: "Article Quote",
|
||||||
|
plural: "Article Quotes",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "ArticleQuote",
|
||||||
|
description:
|
||||||
|
"Dark-themed quote card. Use for expert quotes or statements. Use isCompany={true} for brands/orgs to show an entity icon instead of personal initials. MANDATORY: always include source and sourceUrl for verifiability. Props: quote, author, role (optional), source (REQUIRED), sourceUrl (REQUIRED), isCompany (optional), translated (optional boolean).",
|
||||||
|
usageExample:
|
||||||
|
'\'<ArticleQuote quote="Optimizing for speed." author="Google" isCompany={true',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "quote",
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den mehrzeiligen Text für quote ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "author",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für author ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "role",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für role ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "source",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für source ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sourceUrl",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für sourceUrl ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "translated",
|
||||||
|
type: "checkbox",
|
||||||
|
defaultValue: false,
|
||||||
|
admin: { description: "Wert für translated eingeben." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "isCompany",
|
||||||
|
type: "checkbox",
|
||||||
|
defaultValue: false,
|
||||||
|
admin: { description: "Wert für isCompany eingeben." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
72
apps/web/src/payload/blocks/BoldNumberBlock.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const BoldNumberBlock: MintelBlock = {
|
||||||
|
slug: "boldNumber",
|
||||||
|
labels: {
|
||||||
|
singular: "Bold Number",
|
||||||
|
plural: "Bold Numbers",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "BoldNumber",
|
||||||
|
description: "Large centerpiece number with label for primary statistics.",
|
||||||
|
usageExample:
|
||||||
|
'\'<BoldNumber value="5x" label="höhere Conversion-Rate" source="Portent" />\'',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "value",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description: "e.g. 53% or 2.5M€",
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "label",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für label ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "source",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für source ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sourceUrl",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für sourceUrl ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
69
apps/web/src/payload/blocks/ButtonBlock.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const ButtonBlock: MintelBlock = {
|
||||||
|
slug: "buttonBlock",
|
||||||
|
labels: {
|
||||||
|
singular: "Button Block",
|
||||||
|
plural: "Button Blocks",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "Button",
|
||||||
|
description:
|
||||||
|
"DEPRECATED: Use <LeadMagnet /> instead for main CTAs. Only use for small secondary links.",
|
||||||
|
usageExample:
|
||||||
|
'<Button href="/contact" variant="outline">Webprojekt anfragen</Button>',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "label",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für label ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "href",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: { description: "Geben Sie den Text für href ein." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "variant",
|
||||||
|
type: "select",
|
||||||
|
options: [
|
||||||
|
{ label: "Primary", value: "primary" },
|
||||||
|
{ label: "Outline", value: "outline" },
|
||||||
|
{ label: "Ghost", value: "ghost" },
|
||||||
|
],
|
||||||
|
defaultValue: "primary",
|
||||||
|
admin: { description: "Wählen Sie eine Option für variant aus." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "size",
|
||||||
|
type: "select",
|
||||||
|
options: [
|
||||||
|
{ label: "Normal", value: "normal" },
|
||||||
|
{ label: "Large", value: "large" },
|
||||||
|
],
|
||||||
|
defaultValue: "normal",
|
||||||
|
admin: { description: "Wählen Sie eine Option für size aus." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "showArrow",
|
||||||
|
type: "checkbox",
|
||||||
|
defaultValue: true,
|
||||||
|
admin: { description: "Wert für showArrow eingeben." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
48
apps/web/src/payload/blocks/CarouselBlock.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const CarouselBlock: MintelBlock = {
|
||||||
|
slug: "carousel",
|
||||||
|
labels: {
|
||||||
|
singular: "Carousel",
|
||||||
|
plural: "Carousels",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "Carousel",
|
||||||
|
description:
|
||||||
|
"Interactive swipeable slider for multi-step explanations. IMPORTANT: items array must contain at least 2 items with substantive title and content text (no empty content).",
|
||||||
|
usageExample:
|
||||||
|
'\'<Carousel items={[{ title: "Schritt 1", content: "Analyse der aktuellen Performance..."',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "slides",
|
||||||
|
type: "array",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "image",
|
||||||
|
type: "upload",
|
||||||
|
relationTo: "media",
|
||||||
|
admin: { description: "Laden Sie die Datei für image hoch." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "caption",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für caption ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
admin: { description: "Fügen Sie Elemente zur Liste slides hinzu." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
102
apps/web/src/payload/blocks/ComparisonRowBlock.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const ComparisonRowBlock: MintelBlock = {
|
||||||
|
slug: "comparisonRow",
|
||||||
|
labels: {
|
||||||
|
singular: "Comparison Row",
|
||||||
|
plural: "Comparison Rows",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "ComparisonRow",
|
||||||
|
description:
|
||||||
|
'Side-by-side comparison: negative "Standard" approach vs positive "Mintel" approach. Props include showShare boolean.',
|
||||||
|
usageExample: `<ComparisonRow
|
||||||
|
description="Architektur-Vergleich"
|
||||||
|
negativeLabel="Legacy CMS"
|
||||||
|
negativeText="Langsame Datenbankabfragen, verwundbare Plugins."
|
||||||
|
positiveLabel="Mintel Stack"
|
||||||
|
positiveText="Statische Generierung, perfekte Sicherheit."
|
||||||
|
showShare={true`,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "description",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
description: "Optional overarching description for the comparison.",
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negativeLabel",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
defaultValue: "Legacy",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für negativeLabel ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negativeText",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für negativeText ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "positiveLabel",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
defaultValue: "Mintel Stack",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für positiveLabel ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "positiveText",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für positiveText ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reverse",
|
||||||
|
type: "checkbox",
|
||||||
|
defaultValue: false,
|
||||||
|
admin: {
|
||||||
|
description: "Swap the visual order of the positive/negative cards?",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
35
apps/web/src/payload/blocks/DiagramFlowBlock.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const DiagramFlowBlock: MintelBlock = {
|
||||||
|
slug: "diagramFlow",
|
||||||
|
labels: {
|
||||||
|
singular: "Diagram Flow",
|
||||||
|
plural: "Diagram Flows",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "DiagramFlow",
|
||||||
|
description:
|
||||||
|
"Mermaid flowchart diagram defining the graph structure. MUST output raw mermaid code, no quotes or HTML.",
|
||||||
|
usageExample: "graph TD\\n A[Start] --> B[End]",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "definition",
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
35
apps/web/src/payload/blocks/DiagramGanttBlock.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const DiagramGanttBlock: MintelBlock = {
|
||||||
|
slug: "diagramGantt",
|
||||||
|
labels: {
|
||||||
|
singular: "Diagram Gantt",
|
||||||
|
plural: "Diagram Gantts",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "DiagramGantt",
|
||||||
|
description: "Mermaid Gantt timeline chart. MUST output raw mermaid code.",
|
||||||
|
usageExample:
|
||||||
|
"gantt\\n title Project Roadmap\\n dateFormat YYYY-MM-DD\\n Section Design\\n Draft UI :a1, 2024-01-01, 7d",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "definition",
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
34
apps/web/src/payload/blocks/DiagramPieBlock.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const DiagramPieBlock: MintelBlock = {
|
||||||
|
slug: "diagramPie",
|
||||||
|
labels: {
|
||||||
|
singular: "Diagram Pie",
|
||||||
|
plural: "Diagram Pies",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "DiagramPie",
|
||||||
|
description: "Mermaid pie chart diagram. MUST output raw mermaid code.",
|
||||||
|
usageExample: 'pie title Market Share\\n "Chrome" : 60\\n "Safari" : 20',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "definition",
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
36
apps/web/src/payload/blocks/DiagramSequenceBlock.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const DiagramSequenceBlock: MintelBlock = {
|
||||||
|
slug: "diagramSequence",
|
||||||
|
labels: {
|
||||||
|
singular: "Diagram Sequence",
|
||||||
|
plural: "Diagram Sequences",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "DiagramSequence",
|
||||||
|
description:
|
||||||
|
"Mermaid sequence diagram showing actor interactions. MUST output raw mermaid code.",
|
||||||
|
usageExample:
|
||||||
|
"sequenceDiagram\\n Client->>Server: GET /api\\n Server-->>Client: 200 OK",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "definition",
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
35
apps/web/src/payload/blocks/DiagramStateBlock.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const DiagramStateBlock: MintelBlock = {
|
||||||
|
slug: "diagramState",
|
||||||
|
labels: {
|
||||||
|
singular: "Diagram State",
|
||||||
|
plural: "Diagram States",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "DiagramState",
|
||||||
|
description:
|
||||||
|
"Mermaid state diagram showing states and transitions. MUST output raw mermaid code.",
|
||||||
|
usageExample: "stateDiagram-v2\\n [*] --> Idle\\n Idle --> Loading",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "definition",
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
36
apps/web/src/payload/blocks/DiagramTimelineBlock.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const DiagramTimelineBlock: MintelBlock = {
|
||||||
|
slug: "diagramTimeline",
|
||||||
|
labels: {
|
||||||
|
singular: "Diagram Timeline",
|
||||||
|
plural: "Diagram Timelines",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "DiagramTimeline",
|
||||||
|
description:
|
||||||
|
"Mermaid timeline or journey diagram. MUST output raw mermaid code.",
|
||||||
|
usageExample:
|
||||||
|
"timeline\\n title Project Timeline\\n 2024\\n : Q1 : Planning\\n : Q2 : Execution",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "definition",
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
27
apps/web/src/payload/blocks/DigitalAssetVisualizerBlock.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const DigitalAssetVisualizerBlock: MintelBlock = {
|
||||||
|
slug: "digitalAssetVisualizer",
|
||||||
|
labels: {
|
||||||
|
singular: "Digital Asset Visualizer",
|
||||||
|
plural: "Digital Asset Visualizers",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "DigitalAssetVisualizer",
|
||||||
|
description:
|
||||||
|
"Interactive visualization illustrating the financial difference between software as a liability (SaaS/rent) and software as a digital asset (Custom IP). Great for articles concerning CTO strategies, business value of code, and digital independence. Requires no props.",
|
||||||
|
usageExample: "'<DigitalAssetVisualizer />'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "assetId",
|
||||||
|
type: "text",
|
||||||
|
admin: { description: "Geben Sie den Text für assetId ein." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
42
apps/web/src/payload/blocks/ExternalLinkBlock.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const ExternalLinkBlock: MintelBlock = {
|
||||||
|
slug: "externalLink",
|
||||||
|
labels: {
|
||||||
|
singular: "External Link",
|
||||||
|
plural: "External Links",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "ExternalLink",
|
||||||
|
description:
|
||||||
|
"Inline external link with ↗ icon and outbound analytics tracking. Use for all source citations and external references within Paragraph text.",
|
||||||
|
usageExample:
|
||||||
|
"'<ExternalLink href=\"https://web.dev/articles/vitals\">Google Core Web Vitals</ExternalLink>'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "href",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: { description: "Geben Sie den Text für href ein." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "label",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für label ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
47
apps/web/src/payload/blocks/FAQSectionBlock.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
import { lexicalEditor, BlocksFeature } from "@payloadcms/richtext-lexical";
|
||||||
|
import { HeadingBlock } from "./HeadingBlock";
|
||||||
|
import { ParagraphBlock } from "./ParagraphBlock";
|
||||||
|
import { ExternalLinkBlock } from "./ExternalLinkBlock";
|
||||||
|
import { TrackedLinkBlock } from "./TrackedLinkBlock";
|
||||||
|
|
||||||
|
export const FAQSectionBlock: MintelBlock = {
|
||||||
|
slug: "faqSection",
|
||||||
|
labels: {
|
||||||
|
singular: "Faq Section",
|
||||||
|
plural: "Faq Sections",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "FAQSection",
|
||||||
|
description:
|
||||||
|
"Semantic wrapper for FAQ questions at the end of the article. Put standard Markdown H3/Paragraphs inside.",
|
||||||
|
usageExample:
|
||||||
|
"'<FAQSection>\n <H3>Frage 1</H3>\n <Paragraph>Antwort 1</Paragraph>\n</FAQSection>'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "content",
|
||||||
|
type: "richText",
|
||||||
|
editor: lexicalEditor({
|
||||||
|
features: ({ defaultFeatures }) => [
|
||||||
|
...defaultFeatures,
|
||||||
|
BlocksFeature({
|
||||||
|
blocks: [
|
||||||
|
HeadingBlock,
|
||||||
|
ParagraphBlock,
|
||||||
|
ExternalLinkBlock,
|
||||||
|
TrackedLinkBlock,
|
||||||
|
].map(({ ai, render, ...b }) => b),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
required: true,
|
||||||
|
admin: { description: "Formatierter Textbereich für content." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
24
apps/web/src/payload/blocks/H2Block.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
export const H2Block: MintelBlock = {
|
||||||
|
slug: "mintelH2",
|
||||||
|
labels: {
|
||||||
|
singular: "Heading 2",
|
||||||
|
plural: "Headings 2",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "text",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description: "Geben Sie den Text für die H2-Überschrift ein.",
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
24
apps/web/src/payload/blocks/H3Block.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
export const H3Block: MintelBlock = {
|
||||||
|
slug: "mintelH3",
|
||||||
|
labels: {
|
||||||
|
singular: "Heading 3",
|
||||||
|
plural: "Headings 3",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "text",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description: "Geben Sie den Text für die H3-Überschrift ein.",
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
50
apps/web/src/payload/blocks/HeadingBlock.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
export const HeadingBlock: MintelBlock = {
|
||||||
|
slug: "mintelHeading",
|
||||||
|
labels: {
|
||||||
|
singular: "Heading",
|
||||||
|
plural: "Headings",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "Heading",
|
||||||
|
description:
|
||||||
|
"Flexible heading component with separated SEO and visual display levels.",
|
||||||
|
usageExample:
|
||||||
|
'\'<Heading seoLevel="h2" displayLevel="h3">Titel</Heading>\'',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "text",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description: "Der Text der Überschrift.",
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "seoLevel",
|
||||||
|
type: "select",
|
||||||
|
options: ["h1", "h2", "h3", "h4", "h5", "h6"],
|
||||||
|
defaultValue: "h2",
|
||||||
|
admin: { description: "Das semantische HTML-Tag für SEO." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "displayLevel",
|
||||||
|
type: "select",
|
||||||
|
options: ["h1", "h2", "h3", "h4", "h5", "h6"],
|
||||||
|
defaultValue: "h2",
|
||||||
|
admin: {
|
||||||
|
description: "Die visuelle Größe der Überschrift (unabhängig von SEO).",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
69
apps/web/src/payload/blocks/IconListBlock.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const IconListBlock: MintelBlock = {
|
||||||
|
slug: "iconList",
|
||||||
|
labels: {
|
||||||
|
singular: "Icon List",
|
||||||
|
plural: "Icon Lists",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "IconList",
|
||||||
|
description:
|
||||||
|
"Checklist with check/cross icons. Wrap IconListItem children inside.",
|
||||||
|
usageExample: `<IconList>
|
||||||
|
<IconListItem check>
|
||||||
|
<strong>Zero-Computation:</strong> Statische Seiten, kein Serverwarten.
|
||||||
|
</IconListItem>
|
||||||
|
<IconListItem cross>
|
||||||
|
<strong>Legacy CMS:</strong> Datenbankabfragen bei jedem Request.
|
||||||
|
</IconListItem>
|
||||||
|
</IconList>`,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "items",
|
||||||
|
type: "array",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "icon",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
description: "Lucide icon",
|
||||||
|
components: { Field: "@/src/payload/components/IconSelector" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "title",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für title ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "description",
|
||||||
|
type: "textarea",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den mehrzeiligen Text für description ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
admin: { description: "Fügen Sie Elemente zur Liste items hinzu." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
52
apps/web/src/payload/blocks/ImageTextBlock.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const ImageTextBlock: MintelBlock = {
|
||||||
|
slug: "imageText",
|
||||||
|
labels: {
|
||||||
|
singular: "Image Text",
|
||||||
|
plural: "Image Texts",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "ImageText",
|
||||||
|
description: "Layout component for image next to explanatory text.",
|
||||||
|
usageExample:
|
||||||
|
'\'<ImageText image="/img.jpg" title="Architektur">Erklärung...</ImageText>\'',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "image",
|
||||||
|
type: "upload",
|
||||||
|
relationTo: "media",
|
||||||
|
required: true,
|
||||||
|
admin: { description: "Laden Sie die Datei für image hoch." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "text",
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den mehrzeiligen Text für text ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "alignment",
|
||||||
|
type: "select",
|
||||||
|
options: [
|
||||||
|
{ label: "Left", value: "left" },
|
||||||
|
{ label: "Right", value: "right" },
|
||||||
|
],
|
||||||
|
defaultValue: "left",
|
||||||
|
admin: { description: "Wählen Sie eine Option für alignment aus." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
81
apps/web/src/payload/blocks/LeadMagnetBlock.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const LeadMagnetBlock: MintelBlock = {
|
||||||
|
slug: "leadMagnet",
|
||||||
|
labels: {
|
||||||
|
singular: "Lead Magnet CTA",
|
||||||
|
plural: "Lead Magnet CTAs",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "LeadMagnet",
|
||||||
|
description:
|
||||||
|
"Premium B2B conversion card. Use 1-2 per article as main high-impact CTAs. Props: title (strong headline), description (value prop), buttonText (action), href (link), variant (performance|security|standard).",
|
||||||
|
usageExample:
|
||||||
|
'\'<LeadMagnet title="Performance-Check anfragen" description="Wir analysieren Ihre Core Web Vitals und decken Umsatzpotenziale auf." buttonText="Jetzt analysieren lassen" href="/contact" variant="performance" />\'',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "title",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description: "The strong headline for the Call-to-Action",
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "description",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description: "The value proposition text.",
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "buttonText",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
defaultValue: "Jetzt anfragen",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für buttonText ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "href",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
defaultValue: "/contact",
|
||||||
|
admin: { description: "Geben Sie den Text für href ein." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "variant",
|
||||||
|
type: "select",
|
||||||
|
options: [
|
||||||
|
{ label: "Performance", value: "performance" },
|
||||||
|
{ label: "Security", value: "security" },
|
||||||
|
{ label: "Standard", value: "standard" },
|
||||||
|
],
|
||||||
|
defaultValue: "standard",
|
||||||
|
admin: { description: "Wählen Sie eine Option für variant aus." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
36
apps/web/src/payload/blocks/LeadParagraphBlock.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const LeadParagraphBlock: MintelBlock = {
|
||||||
|
slug: "leadParagraph",
|
||||||
|
labels: {
|
||||||
|
singular: "Lead Paragraph",
|
||||||
|
plural: "Lead Paragraphs",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "LeadParagraph",
|
||||||
|
description:
|
||||||
|
"Larger, emphasized paragraph for the article introduction. Use 1-3 at the start.",
|
||||||
|
usageExample:
|
||||||
|
"'<LeadParagraph>\n Unternehmen investieren oft Unsummen in glänzende Oberflächen, während das technische Fundament bröckelt.\n</LeadParagraph>'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "text",
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den mehrzeiligen Text für text ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
29
apps/web/src/payload/blocks/LinkedInEmbedBlock.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const LinkedInEmbedBlock: MintelBlock = {
|
||||||
|
slug: "linkedInEmbed",
|
||||||
|
labels: {
|
||||||
|
singular: "Linked In Embed",
|
||||||
|
plural: "Linked In Embeds",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "LinkedInEmbed",
|
||||||
|
description:
|
||||||
|
"Embeds a professional post from LinkedIn. Use the activity URN (e.g. urn:li:activity:1234567890).",
|
||||||
|
usageExample:
|
||||||
|
"'<LinkedInEmbed urn=\"urn:li:activity:7153664326573674496\" />'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "url",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: { description: "Geben Sie den Text für url ein." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
31
apps/web/src/payload/blocks/LoadTimeSimulatorBlock.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const LoadTimeSimulatorBlock: MintelBlock = {
|
||||||
|
slug: "loadTimeSimulator",
|
||||||
|
labels: {
|
||||||
|
singular: "Load Time Simulator",
|
||||||
|
plural: "Load Time Simulators",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "LoadTimeSimulator",
|
||||||
|
description:
|
||||||
|
"Interactive visual race simulating the loading experience of a slow legacy CMS vs a fast headless stack. Great for articles discussing load times, technical debt, or user frustration. Requires no props.",
|
||||||
|
usageExample: "'<LoadTimeSimulator />'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "initialLoadTime",
|
||||||
|
type: "number",
|
||||||
|
defaultValue: 3.5,
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
"Tragen Sie einen numerischen Wert für initialLoadTime ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
51
apps/web/src/payload/blocks/MarkerBlock.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const MarkerBlock: MintelBlock = {
|
||||||
|
slug: "marker",
|
||||||
|
labels: {
|
||||||
|
singular: "Marker",
|
||||||
|
plural: "Markers",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "Marker",
|
||||||
|
description:
|
||||||
|
"Inline highlight (yellow marker effect) for emphasizing key phrases within paragraphs.",
|
||||||
|
usageExample: "'<Marker>entscheidender Wettbewerbsvorteil</Marker>'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "text",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für text ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "color",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
description: "Hex or rgba color",
|
||||||
|
components: { Field: "@/src/payload/components/ColorPicker" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "delay",
|
||||||
|
type: "number",
|
||||||
|
defaultValue: 0,
|
||||||
|
admin: {
|
||||||
|
description: "Tragen Sie einen numerischen Wert für delay ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
51
apps/web/src/payload/blocks/MemeCardBlock.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const MemeCardBlock: MintelBlock = {
|
||||||
|
slug: "memeCard",
|
||||||
|
labels: {
|
||||||
|
singular: "Meme Card",
|
||||||
|
plural: "Meme Cards",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "MemeCard",
|
||||||
|
description:
|
||||||
|
'Real meme image from memegen.link. template must be a valid memegen.link ID. IMPORTANT: Captions must be EXTREMELY SARCASTIC and PUNCHY (mocking bad B2B agencies, max 6 words per line). Best templates: drake (2-line prefer/dislike), gru (4-step plan backfire), disastergirl (burning house), fine (this is fine dog). Use German captions. Wrap in div with className="my-8".',
|
||||||
|
usageExample: `<div className="my-8">
|
||||||
|
<MemeCard template="drake" captions="47 WordPress Plugins installieren|Eine saubere Serverless Architektur" />
|
||||||
|
</div>`,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "template",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
"The template ID from memegen.link (e.g. 'drake', 'disastergirl')",
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "captions",
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
"Pipe-separated captions for the meme (e.g. 'Legacy Code|Mintel Stack'). Maximum 6 words per line.",
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
68
apps/web/src/payload/blocks/MermaidBlock.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const MermaidBlock: MintelBlock = {
|
||||||
|
slug: "mermaid",
|
||||||
|
labels: {
|
||||||
|
singular: "Mermaid",
|
||||||
|
plural: "Mermaids",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "Mermaid",
|
||||||
|
description:
|
||||||
|
'Renders a Mermaid.js diagram (flowchart, sequence, pie, etc.). Diagram code goes as children. Keep it tiny (max 3-4 nodes). Wrap in div with className="my-8".',
|
||||||
|
usageExample: `<div className="my-8">
|
||||||
|
<Mermaid id="my-diagram" title="System Architecture" showShare={true`,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "id",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
"A unique ASCII ID for the diagram (e.g. 'architecture-1').",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "title",
|
||||||
|
type: "text",
|
||||||
|
required: false,
|
||||||
|
admin: {
|
||||||
|
description: "Optional title displayed above the diagram.",
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "showShare",
|
||||||
|
type: "checkbox",
|
||||||
|
defaultValue: false,
|
||||||
|
admin: {
|
||||||
|
description: "Show the share button for this diagram?",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "chartDefinition",
|
||||||
|
type: "code",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
language: "markdown",
|
||||||
|
description:
|
||||||
|
"The raw Mermaid.js syntax (e.g. graph TD... shadowing, loops, etc.).",
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
68
apps/web/src/payload/blocks/MetricBarBlock.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const MetricBarBlock: MintelBlock = {
|
||||||
|
slug: "metricBar",
|
||||||
|
labels: {
|
||||||
|
singular: "Metric Bar",
|
||||||
|
plural: "Metric Bars",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "MetricBar",
|
||||||
|
description:
|
||||||
|
"Animated horizontal progress bar. Use multiple in sequence to compare metrics. IMPORTANT: value MUST be a real number > 0, never use 0 or placeholder values. Props: label, value (number), max (default 100), unit (default %), color (red|green|blue|slate).",
|
||||||
|
usageExample: '<MetricBar label="WordPress Sites" value={33',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "label",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für label ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "value",
|
||||||
|
type: "number",
|
||||||
|
required: true,
|
||||||
|
admin: { description: "Percentage 0-100" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "max",
|
||||||
|
type: "number",
|
||||||
|
defaultValue: 100,
|
||||||
|
admin: { description: "Tragen Sie einen numerischen Wert für max ein." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unit",
|
||||||
|
type: "text",
|
||||||
|
defaultValue: "%",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für unit ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "color",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: { Field: "@/src/payload/components/ColorPicker" },
|
||||||
|
description: "Geben Sie den Text für color ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
36
apps/web/src/payload/blocks/ParagraphBlock.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const ParagraphBlock: MintelBlock = {
|
||||||
|
slug: "mintelP",
|
||||||
|
labels: {
|
||||||
|
singular: "Paragraph",
|
||||||
|
plural: "Paragraphs",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "Paragraph",
|
||||||
|
description:
|
||||||
|
"Standard body text paragraph. All body text must be wrapped in this.",
|
||||||
|
usageExample:
|
||||||
|
"'<Paragraph>\n Mein System ist kein Kostenfaktor, sondern ein <Marker>ROI-Beschleuniger</Marker>.\n</Paragraph>'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "text",
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den mehrzeiligen Text für text ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
36
apps/web/src/payload/blocks/PerformanceChartBlock.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const PerformanceChartBlock: MintelBlock = {
|
||||||
|
slug: "performanceChart",
|
||||||
|
labels: {
|
||||||
|
singular: "Performance Chart",
|
||||||
|
plural: "Performance Charts",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "PerformanceChart",
|
||||||
|
description:
|
||||||
|
"A visual chart illustrating performance metrics (e.g. PageSpeed, TTFB) over time or in comparison. Use to emphasize technical improvements.",
|
||||||
|
usageExample:
|
||||||
|
'<PerformanceChart items={[{ label: "Vorher", value: 12 }, { label: "Nachher", value: 98 }]} />',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "title",
|
||||||
|
type: "text",
|
||||||
|
defaultValue: "Website Performance",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für title ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
40
apps/web/src/payload/blocks/PerformanceROICalculatorBlock.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const PerformanceROICalculatorBlock: MintelBlock = {
|
||||||
|
slug: "performanceROICalculator",
|
||||||
|
labels: {
|
||||||
|
singular: "Performance R O I Calculator",
|
||||||
|
plural: "Performance R O I Calculators",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "PerformanceROICalculator",
|
||||||
|
description:
|
||||||
|
"Interactive simulation calculator showing the monetary ROI of improving load times (based on Deloitte B2B metrics). Use exactly once in performance-related articles to provide a highly engaging simulation. Requires no props.",
|
||||||
|
usageExample: "'<PerformanceROICalculator />'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "baseConversionRate",
|
||||||
|
type: "number",
|
||||||
|
defaultValue: 2.5,
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
"Tragen Sie einen numerischen Wert für baseConversionRate ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "monthlyVisitors",
|
||||||
|
type: "number",
|
||||||
|
defaultValue: 50000,
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
"Tragen Sie einen numerischen Wert für monthlyVisitors ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
115
apps/web/src/payload/blocks/PremiumComparisonChartBlock.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const PremiumComparisonChartBlock: MintelBlock = {
|
||||||
|
slug: "premiumComparisonChart",
|
||||||
|
labels: {
|
||||||
|
singular: "Premium Comparison Chart",
|
||||||
|
plural: "Premium Comparison Charts",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "PremiumComparisonChart",
|
||||||
|
description:
|
||||||
|
"Advanced chart for comparing performance metrics with industrial aesthetics.",
|
||||||
|
usageExample:
|
||||||
|
'\'<PremiumComparisonChart title="TTFB Vergleich" items={[{ label: "Alt", value: 800, max: 1000, color: "red"',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "title",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für title ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "subtitle",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für subtitle ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "datasets",
|
||||||
|
type: "array",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "label",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für label ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "value",
|
||||||
|
type: "number",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description: "Tragen Sie einen numerischen Wert für value ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "max",
|
||||||
|
type: "number",
|
||||||
|
defaultValue: 100,
|
||||||
|
admin: {
|
||||||
|
description: "Tragen Sie einen numerischen Wert für max ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unit",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für unit ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "color",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: { Field: "@/src/payload/components/ColorPicker" },
|
||||||
|
description: "Geben Sie den Text für color ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "description",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für description ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
admin: { description: "Fügen Sie Elemente zur Liste datasets hinzu." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
51
apps/web/src/payload/blocks/RevealBlock.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
import { lexicalEditor } from "@payloadcms/richtext-lexical";
|
||||||
|
|
||||||
|
export const RevealBlock: MintelBlock = {
|
||||||
|
slug: "mintelReveal",
|
||||||
|
labels: {
|
||||||
|
singular: "Reveal Wrap",
|
||||||
|
plural: "Reveal Wraps",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "Reveal",
|
||||||
|
description:
|
||||||
|
"Scroll-triggered reveal animation wrapper. Wrap any content to animate on scroll.",
|
||||||
|
usageExample:
|
||||||
|
'\'<Reveal>\n <StatsDisplay value="100" label="PageSpeed Score" />\n</Reveal>\'',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "direction",
|
||||||
|
type: "select",
|
||||||
|
options: [
|
||||||
|
{ label: "Up", value: "up" },
|
||||||
|
{ label: "Down", value: "down" },
|
||||||
|
{ label: "Left", value: "left" },
|
||||||
|
{ label: "Right", value: "right" },
|
||||||
|
],
|
||||||
|
defaultValue: "up",
|
||||||
|
admin: { description: "Wählen Sie eine Option für direction aus." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "delay",
|
||||||
|
type: "number",
|
||||||
|
defaultValue: 0.1,
|
||||||
|
admin: {
|
||||||
|
description: "Tragen Sie einen numerischen Wert für delay ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "content",
|
||||||
|
type: "richText",
|
||||||
|
editor: lexicalEditor({}),
|
||||||
|
required: true,
|
||||||
|
admin: { description: "Formatierter Textbereich für content." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
35
apps/web/src/payload/blocks/RevenueLossCalculatorBlock.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const RevenueLossCalculatorBlock: MintelBlock = {
|
||||||
|
slug: "revenueLossCalculator",
|
||||||
|
labels: {
|
||||||
|
singular: "Revenue Loss Calculator",
|
||||||
|
plural: "Revenue Loss Calculators",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "RevenueLossCalculator",
|
||||||
|
description:
|
||||||
|
"Interactive calculator that estimates financial loss due to slow page load times. Use to build a business case for performance optimization.",
|
||||||
|
usageExample: "<RevenueLossCalculator />",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "title",
|
||||||
|
type: "text",
|
||||||
|
defaultValue: "Performance Revenue Simulator",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für title ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
42
apps/web/src/payload/blocks/SectionBlock.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
import { lexicalEditor } from "@payloadcms/richtext-lexical";
|
||||||
|
|
||||||
|
export const SectionBlock: MintelBlock = {
|
||||||
|
slug: "mintelSection",
|
||||||
|
labels: {
|
||||||
|
singular: "Section Wrap",
|
||||||
|
plural: "Section Wraps",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "Section",
|
||||||
|
description: "Wraps a thematic section block with optional heading.",
|
||||||
|
usageExample:
|
||||||
|
"'<Section>\n <h3>Section Title</h3>\n <p>Content here.</p>\n</Section>'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "title",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für title ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "content",
|
||||||
|
type: "richText",
|
||||||
|
editor: lexicalEditor({}),
|
||||||
|
required: true,
|
||||||
|
admin: { description: "Formatierter Textbereich für content." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
60
apps/web/src/payload/blocks/StatsDisplayBlock.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const StatsDisplayBlock: MintelBlock = {
|
||||||
|
slug: "statsDisplay",
|
||||||
|
labels: {
|
||||||
|
singular: "Stats Display",
|
||||||
|
plural: "Stats Displays",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "StatsDisplay",
|
||||||
|
description:
|
||||||
|
"A single large stat card with prominent value, label, and optional subtext.",
|
||||||
|
usageExample:
|
||||||
|
'\'<StatsDisplay value="-20%" label="Conversion" subtext="Jede Sekunde Verzögerung kostet." />\'',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "label",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für label ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "value",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für value ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "subtext",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für subtext ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
56
apps/web/src/payload/blocks/StatsGridBlock.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const StatsGridBlock: MintelBlock = {
|
||||||
|
slug: "statsGrid",
|
||||||
|
labels: {
|
||||||
|
singular: "Stats Grid",
|
||||||
|
plural: "Stats Grids",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "StatsGrid",
|
||||||
|
description:
|
||||||
|
"Grid of 2–4 stat cards in a row. Use tilde (~) to separate stats, pipe (|) to separate value|label|subtext within each stat.",
|
||||||
|
usageExample:
|
||||||
|
"'<StatsGrid stats=\"53%|Mehr Umsatz|Rakuten 24~33%|Conversion Boost|nach CWV Fix~24%|Top 3 Ranking|bei bestandenen CWV\" />'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "stats",
|
||||||
|
type: "array",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "label",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für label ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "value",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für value ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
admin: { description: "Fügen Sie Elemente zur Liste stats hinzu." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
35
apps/web/src/payload/blocks/TLDRBlock.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const TLDRBlock: MintelBlock = {
|
||||||
|
slug: "mintelTldr",
|
||||||
|
labels: {
|
||||||
|
singular: "TL;DR Block",
|
||||||
|
plural: "TL;DR Blocks",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "TLDR",
|
||||||
|
description:
|
||||||
|
"Presents a bite-sized summary of the article in a premium dark card. Use exactly once at the very beginning.",
|
||||||
|
usageExample:
|
||||||
|
"'<TLDR>\n Stabilität ist kein Zufall, sondern das Ergebnis einer Clean Code Strategie.\n</TLDR>'",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "content",
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description: "The summary content for the TLDR box.",
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
48
apps/web/src/payload/blocks/TrackedLinkBlock.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const TrackedLinkBlock: MintelBlock = {
|
||||||
|
slug: "trackedLink",
|
||||||
|
labels: {
|
||||||
|
singular: "Tracked Link",
|
||||||
|
plural: "Tracked Links",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "TrackedLink",
|
||||||
|
description:
|
||||||
|
"A wrapper around next/link that tracks clicks. Use for all INTERNAL navigational links that should be tracked.",
|
||||||
|
usageExample:
|
||||||
|
'\'<TrackedLink href="/contact" className="text-blue-600 font-bold">Jetzt anfragen</TrackedLink>\'',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "href",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: { description: "Geben Sie den Text für href ein." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "label",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
afterInput: [
|
||||||
|
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: "Geben Sie den Text für label ein.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "eventName",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: { description: "Geben Sie den Text für eventName ein." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
29
apps/web/src/payload/blocks/TwitterEmbedBlock.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { MintelBlock } from "./types";
|
||||||
|
|
||||||
|
import type { Block } from "payload";
|
||||||
|
|
||||||
|
export const TwitterEmbedBlock: MintelBlock = {
|
||||||
|
slug: "twitterEmbed",
|
||||||
|
labels: {
|
||||||
|
singular: "Twitter (X) Embed",
|
||||||
|
plural: "Twitter (X) Embeds",
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: "MDX Components",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
name: "TwitterEmbed",
|
||||||
|
description:
|
||||||
|
"Embeds a post from X.com (Twitter). Used to provide social proof, industry quotes, or examples. Provide the numerical tweetId.",
|
||||||
|
usageExample:
|
||||||
|
'\'<TwitterEmbed tweetId="1753464161943834945" theme="light" />\'',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "url",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
admin: { description: "Geben Sie den Text für url ein." },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
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!);
|
||||||