Compare commits
79 Commits
v1.9.0
...
refactor/r
| Author | SHA1 | Date | |
|---|---|---|---|
| acb9c3df38 | |||
| d5f36274aa | |||
| ce7d485b0c | |||
| 4ac5b15473 | |||
| c08950e865 | |||
| 8dea906d23 | |||
| cb9e136d16 | |||
| a02a419879 | |||
| 222140aa44 | |||
| 1187f5d48d | |||
| 6e64da48d6 | |||
| f2bffea312 | |||
| 1669227f19 | |||
| c644766a64 | |||
| 05514b0c99 | |||
| 66b80117b1 | |||
| 7bceb6cd2b | |||
| 569bb4e270 | |||
| 0a6f5f34ce | |||
| 5f27bb9950 | |||
| d83199ab1a | |||
| 2cfe9115d2 | |||
| d4b586f29f | |||
| 9013d38e31 | |||
| f3cbdd8076 | |||
| f143fc6099 | |||
| 83190f1d00 | |||
| 12848a4f23 | |||
| 0a9cb81841 | |||
| c505050cb4 | |||
| d5d0d77180 | |||
| 9399be3ea9 | |||
| 7ec82027a8 | |||
| 631d051c2e | |||
| 1a8758a88c | |||
| 7d6d34dd7b | |||
| 1f0de18755 | |||
| eeb0920afd | |||
| f8eec731c0 | |||
| 8b80af3d1a | |||
| de42890476 | |||
| fbd33da925 | |||
| f3bab24bd3 | |||
| 3b668fc876 | |||
| 2827239796 | |||
| 7cc1e49ba6 | |||
| 258eb9464e | |||
| 015386ba4a | |||
| 49abaaf2fd | |||
| 7e9005e338 | |||
| bbcc7d159c | |||
| 39f5bd3986 | |||
| 944d369d43 | |||
| 6aaf8ac44f | |||
| 2097b571f3 | |||
| aeb1814a34 | |||
| 57e093ea54 | |||
| 76e25d03b9 | |||
| 19ef042fb6 | |||
| 6032394ac9 | |||
| 033a9ad65e | |||
| 3f6fa36f9b | |||
| c52a132d62 | |||
| 11f735bbdf | |||
| c81dae0b7b | |||
| 716ece1c6c | |||
| e0ccf1cdfb | |||
| 6a6fbb6f19 | |||
| 6b6b2b8ece | |||
| 9f412d81a8 | |||
| 9c401f13de | |||
| 5857404ac1 | |||
| 34a96f8aef | |||
| 4e6f3f29cf | |||
| 1bd516fbe4 | |||
| 4d0e3433a6 | |||
| ee9cde1ed0 | |||
| 33cf701034 | |||
| 1fae5edee3 |
@@ -1,9 +1,13 @@
|
||||
node_modules
|
||||
.next
|
||||
out
|
||||
dist
|
||||
*.log
|
||||
.git
|
||||
# Exclude all binary/dependency folders recursively
|
||||
**/node_modules
|
||||
**/.pnpm-store
|
||||
**/.git
|
||||
**/.next
|
||||
**/dist
|
||||
**/out
|
||||
**/*.log
|
||||
|
||||
# Specific exclusions for this project
|
||||
.DS_Store
|
||||
cloned-websites
|
||||
storage
|
||||
@@ -11,3 +15,11 @@ storage
|
||||
verify_ci
|
||||
pnpm_install_log.txt
|
||||
full_tree.json
|
||||
backups
|
||||
data
|
||||
|
||||
# Ensure we don't copy the sibling's build artifacts either
|
||||
_at-mintel/**/node_modules
|
||||
_at-mintel/**/dist
|
||||
_at-mintel/**/.next
|
||||
_at-mintel/.git
|
||||
|
||||
@@ -113,170 +113,12 @@ jobs:
|
||||
echo "short_sha=$SHORT_SHA"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
# ⏳ Wait for Upstream Packages/Images if Tagged
|
||||
if [[ "${{ github.ref_type }}" == "tag" ]]; then
|
||||
echo "🔎 Checking for @mintel dependencies in package.json..."
|
||||
UPSTREAM_VERSION=$(grep -o '"@mintel/.*": "[^"]*"' package.json | head -1 | cut -d'"' -f4 | sed 's/\^//; s/\~//')
|
||||
TAG_TO_WAIT="v$UPSTREAM_VERSION"
|
||||
|
||||
if [[ -n "$UPSTREAM_VERSION" && "$UPSTREAM_VERSION" != "workspace:"* ]]; then
|
||||
echo "⏳ This release depends on @mintel v$UPSTREAM_VERSION. Waiting for upstream build..."
|
||||
curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
"https://git.infra.mintel.me/mmintel/at-mintel/raw/branch/main/packages/infra/scripts/wait-for-upstream.sh" > wait-for-upstream.sh
|
||||
chmod +x wait-for-upstream.sh
|
||||
|
||||
GITEA_TOKEN=${{ secrets.GITHUB_TOKEN }} ./wait-for-upstream.sh "mmintel/at-mintel" "$TAG_TO_WAIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
else
|
||||
echo "target=skip" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# JOB 2: QA (Lint, Typecheck, Test)
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
qa:
|
||||
name: 🧪 QA
|
||||
needs: [prepare, deploy]
|
||||
if: needs.prepare.outputs.target != 'skip'
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- name: Provide sibling monorepo
|
||||
run: |
|
||||
git clone https://git.infra.mintel.me/mmintel/at-mintel.git _at-mintel
|
||||
|
||||
# Force ALL @mintel packages to use the local clone instead of the registry
|
||||
# This handles root package.json
|
||||
perl -pi -e 's/"\@mintel\/([^"]+)"\s*:\s*"[^"]+"/"\@mintel\/$1": "link:.\/_at-mintel\/packages\/$1"/g' package.json
|
||||
# Special case for pdf -> pdf-library
|
||||
perl -pi -e 's/link:\.\/_at-mintel\/packages\/pdf"/link:.\/_at-mintel\/packages\/pdf-library"/g' package.json
|
||||
|
||||
# Handle apps/web/package.json
|
||||
perl -pi -e 's/"\@mintel\/([^"]+)"\s*:\s*"[^"]+"/"\@mintel\/$1": "link:..\/\.\.\/_at-mintel\/packages\/$1"/g' apps/web/package.json
|
||||
# Special case for pdf -> pdf-library
|
||||
perl -pi -e 's/link:\.\.\/\.\.\/_at-mintel\/packages\/pdf"/link:..\/\.\.\/_at-mintel\/packages\/pdf-library"/g' apps/web/package.json
|
||||
|
||||
# Fix tsconfig paths if they exist
|
||||
sed -i 's|../../../at-mintel|../../_at-mintel|g' apps/web/tsconfig.json || true
|
||||
|
||||
# Fix tsconfig paths if they exist
|
||||
sed -i 's|../../../at-mintel|../../_at-mintel|g' apps/web/tsconfig.json || true
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "Testing available secrets against git.infra.mintel.me Docker registry..."
|
||||
TOKENS="${{ secrets.GITHUB_TOKEN }} ${{ secrets.GITEA_PAT }} ${{ secrets.MINTEL_PRIVATE_TOKEN }} ${{ secrets.NPM_TOKEN }}"
|
||||
USERS="${{ github.repository_owner }} ${{ github.actor }} marcmintel mintel mmintel"
|
||||
|
||||
VALID_TOKEN=""
|
||||
VALID_USER=""
|
||||
|
||||
for T_RAW in $TOKENS; do
|
||||
if [ -n "$T_RAW" ]; then
|
||||
T=$(echo "$T_RAW" | tr -d ' ' | tr -d '\n' | tr -d '\r')
|
||||
|
||||
echo "Testing API with token..."
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token $T" https://git.infra.mintel.me/api/v1/user || echo "failed")
|
||||
echo "API returned: $HTTP_CODE"
|
||||
|
||||
for U in $USERS; do
|
||||
if [ -n "$U" ]; then
|
||||
echo "Attempting docker login for a token with user $U..."
|
||||
if echo "$T" | docker login git.infra.mintel.me -u "$U" --password-stdin > /dev/null 2>&1; then
|
||||
echo "✅ Successfully authenticated with a token."
|
||||
VALID_TOKEN="$T"
|
||||
VALID_USER="$U"
|
||||
break 2
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$VALID_TOKEN" ]; then
|
||||
echo "❌ All token/user combinations failed to authenticate!"
|
||||
T=$(echo "$TOKENS" | awk '{print $1}')
|
||||
echo "Attempting open diagnostic login with first token and user mmintel..."
|
||||
echo "$T" | docker login git.infra.mintel.me -u "mmintel" --password-stdin || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TOKEN="$VALID_TOKEN"
|
||||
echo "::add-mask::$TOKEN"
|
||||
echo "token=$TOKEN" >> $GITHUB_OUTPUT
|
||||
echo "user=$VALID_USER" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "Configuring .npmrc for git.infra.mintel.me..."
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm/" > .npmrc
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${TOKEN}" >> .npmrc
|
||||
echo "always-auth=true" >> .npmrc
|
||||
|
||||
# Also export for pnpm to pick it up from env if needed
|
||||
echo "NPM_TOKEN=${TOKEN}" >> $GITHUB_ENV
|
||||
- name: 🏗️ Compile Sibling Monorepo
|
||||
timeout-minutes: 15
|
||||
run: |
|
||||
mkdir -p ci-logs
|
||||
echo "=== Compile Sibling Monorepo ===" >> ci-logs/summary.txt
|
||||
cp .npmrc _at-mintel/
|
||||
cd _at-mintel
|
||||
pnpm install --no-frozen-lockfile --loglevel info 2>&1 | tee -a ../ci-logs/summary.txt
|
||||
pnpm --filter "...@mintel/payload-ai" \
|
||||
--filter @mintel/pdf... \
|
||||
--filter @mintel/concept-engine... \
|
||||
--filter @mintel/estimation-engine... \
|
||||
--filter @mintel/meme-generator... \
|
||||
build --loglevel info 2>&1 | tee -a ../ci-logs/summary.txt
|
||||
- name: Install dependencies
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
echo "=== Install dependencies (Root) ===" >> ci-logs/summary.txt
|
||||
pnpm install --no-frozen-lockfile --loglevel info 2>&1 | tee -a ci-logs/summary.txt
|
||||
- name: 🧪 Test
|
||||
if: github.event.inputs.skip_checks != 'true'
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
echo "=== Test (@mintel/web) ===" >> ci-logs/summary.txt
|
||||
pnpm --filter @mintel/web test --loglevel info 2>&1 | tee -a ci-logs/summary.txt
|
||||
- name: Inspect on Failure
|
||||
if: failure()
|
||||
run: |
|
||||
echo "==== runner state ===="
|
||||
ls -la
|
||||
echo "==== _at-mintel state ===="
|
||||
ls -la _at-mintel || true
|
||||
echo "==== .npmrc check ===="
|
||||
cat .npmrc | sed -E 's/authToken=[a-f0-9]{5}.*/authToken=REDACTED/'
|
||||
echo "==== pnpm debug logs ===="
|
||||
[ -f pnpm-debug.log ] && tail -n 100 pnpm-debug.log || echo "No root pnpm-debug.log"
|
||||
[ -f _at-mintel/pnpm-debug.log ] && tail -n 100 _at-mintel/pnpm-debug.log || echo "No sibling pnpm-debug.log"
|
||||
- name: Extract QA Error Logs
|
||||
if: failure()
|
||||
run: |
|
||||
mkdir -p ci-logs
|
||||
echo "QA Failure Report" > ci-logs/summary.txt
|
||||
ls -R >> ci-logs/summary.txt
|
||||
[ -f pnpm-debug.log ] && cp pnpm-debug.log ci-logs/ || true
|
||||
[ -f _at-mintel/pnpm-debug.log ] && cp _at-mintel/pnpm-debug.log ci-logs/at-mintel-pnpm-debug.log || true
|
||||
|
||||
SSH_KEY_FILE=$(mktemp)
|
||||
echo "${{ secrets.ALPHA_SSH_KEY }}" > "$SSH_KEY_FILE"
|
||||
chmod 600 "$SSH_KEY_FILE"
|
||||
|
||||
ssh -o StrictHostKeyChecking=no -i "$SSH_KEY_FILE" root@alpha.mintel.me "mkdir -p ~/logs"
|
||||
scp -r -o StrictHostKeyChecking=no -i "$SSH_KEY_FILE" ci-logs/* root@alpha.mintel.me:~/logs/ || true
|
||||
rm "$SSH_KEY_FILE"
|
||||
# (JOB 2: QA was removed to reduce pipeline noise)
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# JOB 3: Build & Push
|
||||
@@ -299,12 +141,43 @@ jobs:
|
||||
perl -pi -e 's/link:\.\/_at-mintel\/packages\/pdf"/link:.\/_at-mintel\/packages\/pdf-library"/g' package.json
|
||||
perl -pi -e 's/"\@mintel\/([^"]+)"\s*:\s*"[^"]+"/"\@mintel\/$1": "link:..\/\.\.\/_at-mintel\/packages\/$1"/g' apps/web/package.json
|
||||
perl -pi -e 's/link:\.\.\/\.\.\/_at-mintel\/packages\/pdf"/link:..\/\.\.\/_at-mintel\/packages\/pdf-library"/g' apps/web/package.json
|
||||
|
||||
- name: 🧹 Free Disk Space
|
||||
run: |
|
||||
docker builder prune -af || true
|
||||
docker image prune -af || true
|
||||
|
||||
- name: 🐳 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: 🔐 Registry Login
|
||||
run: |
|
||||
echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||
|
||||
- name: 🧹 Nuclear Runner Cleanup
|
||||
run: |
|
||||
echo "Disk space before nuclear prune:"
|
||||
df -h
|
||||
|
||||
# Massive cleanup of pre-installed runner software to free up ~5GB+
|
||||
# These are standard on VM-based runners and often unnecessary
|
||||
sudo rm -rf /usr/share/dotnet || true
|
||||
sudo rm -rf /usr/local/lib/android || true
|
||||
sudo rm -rf /opt/ghc || true
|
||||
sudo rm -rf /opt/hostedtoolcache/CodeQL || true
|
||||
|
||||
# Comprehensive Docker purge
|
||||
docker system prune -af --volumes
|
||||
docker builder prune -af
|
||||
docker buildx prune -af
|
||||
|
||||
# Clean temp build artifacts
|
||||
rm -rf /tmp/docker-actions-toolkit-* || true
|
||||
|
||||
echo "Disk space after nuclear prune:"
|
||||
df -h
|
||||
continue-on-error: true
|
||||
|
||||
- name: 🏗️ Build and Push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
@@ -317,9 +190,18 @@ jobs:
|
||||
NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }}
|
||||
DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }}
|
||||
NPM_TOKEN=${{ secrets.NPM_TOKEN }}
|
||||
S3_ENDPOINT=${{ secrets.S3_ENDPOINT || vars.S3_ENDPOINT || 'https://fsn1.your-objectstorage.com' }}
|
||||
S3_ACCESS_KEY=${{ secrets.S3_ACCESS_KEY || vars.S3_ACCESS_KEY }}
|
||||
S3_SECRET_KEY=${{ secrets.S3_SECRET_KEY || vars.S3_SECRET_KEY }}
|
||||
S3_BUCKET=${{ secrets.S3_BUCKET || vars.S3_BUCKET || 'mintel' }}
|
||||
S3_REGION=${{ secrets.S3_REGION || vars.S3_REGION || 'fsn1' }}
|
||||
S3_PREFIX=${{ secrets.S3_PREFIX || vars.S3_PREFIX || 'mintel.me' }}
|
||||
|
||||
BUILD_ID=${{ github.sha }}
|
||||
tags: registry.infra.mintel.me/mintel/mintel.me:${{ needs.prepare.outputs.image_tag }}
|
||||
cache-from: type=registry,ref=registry.infra.mintel.me/mintel/mintel.me:buildcache
|
||||
cache-to: type=registry,ref=registry.infra.mintel.me/mintel/mintel.me:buildcache,mode=max
|
||||
# Temporarily disable registry cache export to save runner disk/bandwidth
|
||||
cache-from: type=registry,ref=registry.infra.mintel.me/mintel/mintel.me:buildcache-${{ needs.prepare.outputs.target }}
|
||||
# cache-to: type=registry,ref=registry.infra.mintel.me/mintel/mintel.me:buildcache-${{ needs.prepare.outputs.target }},mode=max
|
||||
secrets: |
|
||||
NPM_TOKEN=${{ secrets.NPM_TOKEN }}
|
||||
|
||||
@@ -332,17 +214,16 @@ jobs:
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh-keyscan -H alpha.mintel.me >> ~/.ssh/known_hosts 2>/dev/null
|
||||
echo "Re-running docker build with plain progress to capture exact logs..."
|
||||
echo "${{ steps.discover_token.outputs.token }}" | docker login git.infra.mintel.me -u "${{ steps.discover_token.outputs.user }}" --password-stdin > login.log 2>&1
|
||||
echo "${{ steps.discover_token.outputs.token }}" > /tmp/npm_token.txt
|
||||
echo "${{ secrets.NPM_TOKEN }}" > /tmp/npm_token.txt
|
||||
docker build \
|
||||
--build-arg NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }} \
|
||||
--build-arg NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }} \
|
||||
--build-arg DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }} \
|
||||
--build-arg NPM_TOKEN=${{ steps.discover_token.outputs.token }} \
|
||||
--build-arg NPM_TOKEN=${{ secrets.NPM_TOKEN }} \
|
||||
--secret id=NPM_TOKEN,src=/tmp/npm_token.txt \
|
||||
--progress plain \
|
||||
-t temp-image . > docker_build_failed.log 2>&1
|
||||
cat login.log >> docker_build_failed.log
|
||||
cat login.log >> docker_build_failed.log || true
|
||||
scp docker_build_failed.log root@alpha.mintel.me:/root/docker_build_failed.log
|
||||
# JOB 4: Deploy
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
@@ -360,12 +241,7 @@ jobs:
|
||||
DIRECTUS_URL: ${{ needs.prepare.outputs.directus_url }}
|
||||
DIRECTUS_HOST: cms.${{ needs.prepare.outputs.traefik_host }}
|
||||
|
||||
# Database configuration
|
||||
postgres_DB_NAME: ${{ secrets.DIRECTUS_DB_NAME || vars.DIRECTUS_DB_NAME || 'directus' }}
|
||||
postgres_DB_USER: ${{ secrets.DIRECTUS_DB_USER || vars.DIRECTUS_DB_USER || 'directus' }}
|
||||
postgres_DB_PASSWORD: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_DB_PASSWORD) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_DB_PASSWORD) || secrets.DIRECTUS_DB_PASSWORD || vars.DIRECTUS_DB_PASSWORD || 'directus' }}
|
||||
DATABASE_URI: postgres://${{ secrets.DIRECTUS_DB_USER || vars.DIRECTUS_DB_USER || 'directus' }}:${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_DB_PASSWORD) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_DB_PASSWORD) || secrets.DIRECTUS_DB_PASSWORD || vars.DIRECTUS_DB_PASSWORD || 'directus' }}@postgres-db:5432/${{ secrets.DIRECTUS_DB_NAME || vars.DIRECTUS_DB_NAME || 'directus' }}
|
||||
PAYLOAD_SECRET: ${{ secrets.PAYLOAD_SECRET || vars.PAYLOAD_SECRET || 'secret' }}
|
||||
|
||||
|
||||
# Mail
|
||||
MAIL_HOST: ${{ secrets.SMTP_HOST || vars.SMTP_HOST }}
|
||||
@@ -429,11 +305,7 @@ jobs:
|
||||
SENTRY_DSN=$SENTRY_DSN
|
||||
PROJECT_COLOR=$PROJECT_COLOR
|
||||
LOG_LEVEL=$LOG_LEVEL
|
||||
postgres_DB_NAME=$postgres_DB_NAME
|
||||
postgres_DB_USER=$postgres_DB_USER
|
||||
postgres_DB_PASSWORD=$postgres_DB_PASSWORD
|
||||
DATABASE_URI=$DATABASE_URI
|
||||
PAYLOAD_SECRET=$PAYLOAD_SECRET
|
||||
|
||||
MAIL_HOST=$MAIL_HOST
|
||||
MAIL_PORT=$MAIL_PORT
|
||||
MAIL_USERNAME=$MAIL_USERNAME
|
||||
@@ -453,6 +325,7 @@ jobs:
|
||||
S3_REGION=$S3_REGION
|
||||
S3_PREFIX=$S3_PREFIX
|
||||
TARGET=$TARGET
|
||||
SKIP_RUNTIME_ENV_VALIDATION=true
|
||||
SENTRY_ENVIRONMENT=$TARGET
|
||||
PROJECT_NAME=$PROJECT_NAME
|
||||
ENV_FILE=$ENV_FILE
|
||||
@@ -531,9 +404,32 @@ jobs:
|
||||
docker volume create 'mintel-me_payload-db-data' || true
|
||||
cd $SITE_DIR
|
||||
docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file $ENV_FILE pull
|
||||
docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file $ENV_FILE up -d --remove-orphans
|
||||
docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file $ENV_FILE up -d --wait --remove-orphans
|
||||
"
|
||||
|
||||
- name: 🧹 Purge S3 Cache
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Installing rclone..."
|
||||
curl -s -O https://downloads.rclone.org/rclone-current-linux-amd64.deb
|
||||
sudo dpkg -i rclone-current-linux-amd64.deb > /dev/null 2>&1
|
||||
|
||||
echo "Configuring rclone..."
|
||||
cat > rclone.conf <<EOF
|
||||
[mintel-s3]
|
||||
type = s3
|
||||
provider = Other
|
||||
access_key_id = ${{ secrets.S3_ACCESS_KEY || vars.S3_ACCESS_KEY }}
|
||||
secret_access_key = ${{ secrets.S3_SECRET_KEY || vars.S3_SECRET_KEY }}
|
||||
endpoint = ${{ secrets.S3_ENDPOINT || vars.S3_ENDPOINT || 'https://fsn1.your-objectstorage.com' }}
|
||||
region = ${{ secrets.S3_REGION || vars.S3_REGION || 'fsn1' }}
|
||||
EOF
|
||||
|
||||
echo "Purging S3 cache for ${{ env.S3_PREFIX }} ..."
|
||||
rclone --config rclone.conf delete mintel-s3:${{ env.S3_BUCKET }}/${{ env.S3_PREFIX }}/cache/ --include "*" || true
|
||||
|
||||
rm rclone.conf rclone-current-linux-amd64.deb
|
||||
|
||||
- name: 🧹 Post-Deploy Cleanup (Runner)
|
||||
if: always()
|
||||
run: docker builder prune -f --filter "until=1h"
|
||||
@@ -542,112 +438,69 @@ jobs:
|
||||
# JOB 5: Post-Deploy Verification
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
post_deploy_checks:
|
||||
name: 🧪 Post-Deploy Verification
|
||||
needs: [prepare, deploy, qa]
|
||||
if: success() || failure() # Run even if QA fails (due to E2E noise)
|
||||
name: 🩺 Smoke Test
|
||||
needs: [prepare, deploy]
|
||||
runs-on: docker
|
||||
if: needs.deploy.result == 'success'
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
image: alpine:latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- name: Provide sibling monorepo
|
||||
- name: 🌐 Check Production URL
|
||||
shell: sh
|
||||
run: |
|
||||
git clone https://git.infra.mintel.me/mmintel/at-mintel.git _at-mintel
|
||||
|
||||
# Force ALL @mintel packages to use the local clone instead of the registry
|
||||
perl -pi -e 's/"\@mintel\/([^"]+)"\s*:\s*"[^"]+"/"\@mintel\/$1": "link:.\/_at-mintel\/packages\/$1"/g' package.json
|
||||
perl -pi -e 's/link:\.\/_at-mintel\/packages\/pdf"/link:.\/_at-mintel\/packages\/pdf-library"/g' package.json
|
||||
perl -pi -e 's/"\@mintel\/([^"]+)"\s*:\s*"[^"]+"/"\@mintel\/$1": "link:..\/\.\.\/_at-mintel\/packages\/$1"/g' apps/web/package.json
|
||||
perl -pi -e 's/link:\.\.\/\.\.\/_at-mintel\/packages\/pdf"/link:..\/\.\.\/_at-mintel\/packages\/pdf-library"/g' apps/web/package.json
|
||||
|
||||
# Fix tsconfig paths if they exist
|
||||
sed -i 's|../../../at-mintel|../../_at-mintel|g' apps/web/tsconfig.json || true
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "Testing available secrets against git.infra.mintel.me Docker registry..."
|
||||
TOKENS="${{ secrets.GITHUB_TOKEN }} ${{ secrets.GITEA_PAT }} ${{ secrets.MINTEL_PRIVATE_TOKEN }} ${{ secrets.NPM_TOKEN }}"
|
||||
USERS="${{ github.repository_owner }} ${{ github.actor }} marcmintel mintel mmintel"
|
||||
|
||||
VALID_TOKEN=""
|
||||
for TOKEN_RAW in $TOKENS; do
|
||||
if [ -n "$TOKEN_RAW" ]; then
|
||||
TOKEN=$(echo "$TOKEN_RAW" | tr -d '[:space:]' | tr -d '\n' | tr -d '\r')
|
||||
for U in $USERS; do
|
||||
if [ -n "$U" ]; then
|
||||
if echo "$TOKEN" | docker login git.infra.mintel.me -u "$U" --password-stdin > /dev/null 2>&1; then
|
||||
echo "✅ Successfully authenticated with a token."
|
||||
VALID_TOKEN="$TOKEN"
|
||||
break 2
|
||||
fi
|
||||
fi
|
||||
done
|
||||
apk add --no-cache curl
|
||||
# Wait longer (up to 2 minutes) for Next.js to fully prime
|
||||
COUNT=0
|
||||
MAX=24
|
||||
URL="${{ needs.prepare.outputs.next_public_url }}"
|
||||
|
||||
echo "Verifying $URL is responsive..."
|
||||
while [ $COUNT -lt $MAX ]; do
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$URL/" || echo "000")
|
||||
if [ "$STATUS" = "200" ]; then
|
||||
echo "✅ Site is UP (200 OK)"
|
||||
break
|
||||
fi
|
||||
echo "⏳ Wait for 200 OK (Status: $STATUS)..."
|
||||
sleep 5
|
||||
COUNT=$((COUNT + 1))
|
||||
done
|
||||
if [ -z "$VALID_TOKEN" ]; then echo "❌ All tokens failed to authenticate!"; exit 1; fi
|
||||
TOKEN="$VALID_TOKEN"
|
||||
echo "Configuring .npmrc for git.infra.mintel.me..."
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm/" > .npmrc
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${TOKEN}" >> .npmrc
|
||||
echo "always-auth=true" >> .npmrc
|
||||
echo "NPM_TOKEN=${TOKEN}" >> $GITHUB_ENV
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
- name: 🏥 App Health Check
|
||||
shell: bash
|
||||
env:
|
||||
DEPLOY_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||
|
||||
if [ "$STATUS" != "200" ]; then
|
||||
echo "❌ Site failed smoke test after 2 minutes! (Status: $STATUS)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 🌐 Check Case Study Assets and Rewrites
|
||||
shell: sh
|
||||
run: |
|
||||
echo "Waiting for app to start at $DEPLOY_URL ..."
|
||||
for i in {1..30}; do
|
||||
HTTP_CODE=$(curl -sk -o /dev/null -w '%{http_code}' "$DEPLOY_URL" 2>&1) || true
|
||||
echo "Attempt $i: HTTP $HTTP_CODE"
|
||||
if [[ "$HTTP_CODE" =~ ^2 ]]; then
|
||||
echo "✅ App is up (HTTP $HTTP_CODE)"
|
||||
exit 0
|
||||
fi
|
||||
echo "⏳ Waiting... (got $HTTP_CODE)"
|
||||
sleep 10
|
||||
done
|
||||
echo "❌ App health check failed after 30 attempts"
|
||||
exit 1
|
||||
- name: 🚀 OG Image Check
|
||||
continue-on-error: true
|
||||
env:
|
||||
TEST_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||
run: pnpm --filter @mintel/web check:og
|
||||
- name: 📝 E2E Smoke Test
|
||||
continue-on-error: true
|
||||
env:
|
||||
TEST_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
||||
PUPPETEER_SKIP_DOWNLOAD: "true"
|
||||
PUPPETEER_EXECUTABLE_PATH: /usr/bin/chromium
|
||||
run: |
|
||||
# Install system Chromium + dependencies (KLZ pattern)
|
||||
# Ubuntu's default 'chromium' is a snap wrapper, so we use xtradeb PPA for native binary
|
||||
sudo apt-get update && sudo apt-get install -y gnupg wget ca-certificates
|
||||
|
||||
# Setup xtradeb PPA for native chromium
|
||||
CODENAME=$(. /etc/os-release && echo $VERSION_CODENAME)
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
wget -qO- "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x82BB6851C64F6880" | sudo gpg --dearmor -o /etc/apt/keyrings/xtradeb.gpg || true
|
||||
echo "deb [signed-by=/etc/apt/keyrings/xtradeb.gpg] http://ppa.launchpad.net/xtradeb/apps/ubuntu $CODENAME main" | sudo tee /etc/apt/sources.list.d/xtradeb-ppa.list
|
||||
printf "Package: *\nPin: release o=LP-PPA-xtradeb-apps\nPin-Priority: 1001\n" | sudo tee /etc/apt/preferences.d/xtradeb
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --allow-downgrades chromium libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libasound2t64 || sudo apt-get install -y --allow-downgrades chromium libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libasound2
|
||||
|
||||
[ -f /usr/bin/chromium ] && sudo ln -sf /usr/bin/chromium /usr/bin/google-chrome
|
||||
pnpm --filter @mintel/web check:forms
|
||||
BASE_URL="${{ needs.prepare.outputs.next_public_url }}"
|
||||
|
||||
echo "Verifying case study page..."
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/case-studies/klz-cables")
|
||||
if [ "$STATUS" != "200" ]; then
|
||||
echo "❌ Case study page failed! (Status: $STATUS)"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Case study page UP"
|
||||
|
||||
echo "Verifying breeze CSS (root path)..."
|
||||
CSS_PATH="/assets/klz-cables.com/wp-content/cache/breeze-minification/css/breeze_klz-cables-com-1-10895.css"
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL$CSS_PATH")
|
||||
if [ "$STATUS" != "200" ]; then
|
||||
echo "❌ Root asset path failed! (Status: $STATUS)"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Root asset path OK"
|
||||
|
||||
echo "Verifying breeze CSS (relative path from case-study)..."
|
||||
REL_CSS_PATH="/case-studies/assets/klz-cables.com/wp-content/cache/breeze-minification/css/breeze_klz-cables-com-1-10895.css"
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL$REL_CSS_PATH")
|
||||
if [ "$STATUS" != "200" ]; then
|
||||
echo "❌ Relative asset path failed! (Status: $STATUS)"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Relative asset path OK"
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# JOB 6: Notifications
|
||||
@@ -667,7 +520,7 @@ jobs:
|
||||
TARGET="${{ needs.prepare.outputs.target }}"
|
||||
VERSION="${{ needs.prepare.outputs.image_tag }}"
|
||||
|
||||
if [[ "$DEPLOY" == "success" ]] && [[ "$SMOKE" == "success" || "$SMOKE" == "skipped" ]]; then
|
||||
if [[ "$DEPLOY" == "success" ]]; then
|
||||
PRIORITY=5
|
||||
EMOJI="✅"
|
||||
else
|
||||
@@ -677,5 +530,5 @@ jobs:
|
||||
|
||||
curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
|
||||
-F "title=$EMOJI mintel.me $VERSION -> $TARGET" \
|
||||
-F "message=Deploy: $DEPLOY | Smoke: $SMOKE" \
|
||||
-F "message=Deploy: $DEPLOY" \
|
||||
-F "priority=$PRIORITY" || true
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
name: Nightly QA
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build & Deploy"]
|
||||
branches: [main]
|
||||
types:
|
||||
- completed
|
||||
schedule:
|
||||
- cron: "0 3 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
TARGET_URL: "https://testing.mintel.me"
|
||||
PROJECT_NAME: "mintel.me"
|
||||
|
||||
jobs:
|
||||
# ────────────────────────────────────────────────────
|
||||
# 1. Static Checks (HTML, Assets, HTTP)
|
||||
# ────────────────────────────────────────────────────
|
||||
static:
|
||||
name: 🔍 Static Analysis
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc
|
||||
- name: 📦 Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
id: cache-deps
|
||||
with:
|
||||
path: node_modules
|
||||
key: pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
- name: Install
|
||||
if: steps.cache-deps.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
pnpm store prune
|
||||
pnpm install --no-frozen-lockfile
|
||||
- name: 🌐 Install Chrome & Dependencies
|
||||
run: |
|
||||
apt-get update && apt-get install -y --fix-missing \
|
||||
libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \
|
||||
libxkbcommon0 libxcomposite1 libxdamage1 libxext6 libxfixes3 \
|
||||
libxrandr2 libgbm1 libpango-1.0-0 libcairo2 || true
|
||||
apt-get install -y libasound2t64 || apt-get install -y libasound2 || true
|
||||
npx puppeteer browsers install chrome || true
|
||||
- name: 🖼️ OG Images
|
||||
continue-on-error: true
|
||||
env:
|
||||
TEST_URL: ${{ env.TARGET_URL }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
||||
run: pnpm --filter @mintel/web run check:og
|
||||
|
||||
# ────────────────────────────────────────────────────
|
||||
# 2. E2E (Forms)
|
||||
# ────────────────────────────────────────────────────
|
||||
e2e:
|
||||
name: 📝 E2E
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc
|
||||
- name: 📦 Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
id: cache-deps
|
||||
with:
|
||||
path: node_modules
|
||||
key: pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
- name: Install
|
||||
if: steps.cache-deps.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
pnpm store prune
|
||||
pnpm install --no-frozen-lockfile
|
||||
- name: 🌐 Install Chrome & Dependencies
|
||||
run: |
|
||||
apt-get update && apt-get install -y libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxrandr2 libgbm1 libasound2t64 libpango-1.0-0 libcairo2
|
||||
npx puppeteer browsers install chrome || true
|
||||
- name: 📝 E2E Form Submission Test
|
||||
continue-on-error: true
|
||||
env:
|
||||
TEST_URL: ${{ env.TARGET_URL }}
|
||||
NEXT_PUBLIC_BASE_URL: ${{ env.TARGET_URL }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
||||
run: pnpm --filter @mintel/web run check:forms
|
||||
|
||||
# ────────────────────────────────────────────────────
|
||||
# 3. Performance (Lighthouse)
|
||||
# ────────────────────────────────────────────────────
|
||||
lighthouse:
|
||||
name: 🎭 Lighthouse
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc
|
||||
- name: 📦 Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
id: cache-deps
|
||||
with:
|
||||
path: node_modules
|
||||
key: pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
- name: Install
|
||||
if: steps.cache-deps.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
pnpm store prune
|
||||
pnpm install --no-frozen-lockfile
|
||||
- name: 🌐 Install Chrome & Dependencies
|
||||
run: |
|
||||
apt-get update && apt-get install -y libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxrandr2 libgbm1 libasound2t64 libpango-1.0-0 libcairo2
|
||||
npx puppeteer browsers install chrome || true
|
||||
- name: 🎭 Desktop
|
||||
env:
|
||||
NEXT_PUBLIC_BASE_URL: ${{ env.TARGET_URL }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
||||
PAGESPEED_LIMIT: 5
|
||||
run: pnpm --filter @mintel/web run pagespeed:test
|
||||
- name: 📱 Mobile
|
||||
env:
|
||||
NEXT_PUBLIC_BASE_URL: ${{ env.TARGET_URL }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
||||
PAGESPEED_LIMIT: 5
|
||||
run: pnpm --filter @mintel/web run pagespeed:test
|
||||
|
||||
# ────────────────────────────────────────────────────
|
||||
# 4. Link Check & Dependency Audit
|
||||
# ────────────────────────────────────────────────────
|
||||
links:
|
||||
name: 🔗 Links & Deps
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc
|
||||
- name: 📦 Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
id: cache-deps
|
||||
with:
|
||||
path: node_modules
|
||||
key: pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
- name: Install
|
||||
if: steps.cache-deps.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
pnpm store prune
|
||||
pnpm install --no-frozen-lockfile
|
||||
- name: 📦 Depcheck
|
||||
continue-on-error: true
|
||||
run: pnpm dlx depcheck --ignores="*eslint*,*typescript*,*tailwindcss*,*postcss*,*prettier*,*@types/*,*husky*,*lint-staged*,*@next/*,*@lhci/*,*commitlint*,*cspell*,*rimraf*,*@payloadcms/*,*start-server-and-test*,*html-validate*,*critters*,*dotenv*,*turbo*" || true
|
||||
- name: 🔗 Lychee Link Check
|
||||
uses: lycheeverse/lychee-action@v2
|
||||
continue-on-error: true
|
||||
with:
|
||||
args: --accept 200,204,429 --timeout 10 --insecure --exclude "file://*" --exclude "https://logs.infra.mintel.me/*" --exclude "https://git.infra.mintel.me/*" --exclude "https://mintel.me/*" '*.md' 'docs/*.md'
|
||||
fail: false
|
||||
|
||||
# ────────────────────────────────────────────────────
|
||||
# 5. Notification
|
||||
# ────────────────────────────────────────────────────
|
||||
notify:
|
||||
name: 🔔 Notify
|
||||
needs: [static, e2e, lighthouse, links]
|
||||
if: failure()
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: 🔔 Gotify
|
||||
shell: bash
|
||||
run: |
|
||||
STATIC="${{ needs.static.result }}"
|
||||
E2E="${{ needs.e2e.result }}"
|
||||
LIGHTHOUSE="${{ needs.lighthouse.result }}"
|
||||
LINKS="${{ needs.links.result }}"
|
||||
|
||||
if [[ "$STATIC" != "success" || "$LIGHTHOUSE" != "success" ]]; then
|
||||
PRIORITY=8
|
||||
EMOJI="🚨"
|
||||
STATUS="Failed"
|
||||
else
|
||||
PRIORITY=2
|
||||
EMOJI="✅"
|
||||
STATUS="Passed"
|
||||
fi
|
||||
|
||||
TITLE="$EMOJI ${{ env.PROJECT_NAME }} QA $STATUS"
|
||||
MESSAGE="Static: $STATIC | E2E: $E2E | Lighthouse: $LIGHTHOUSE | Links: $LINKS
|
||||
${{ env.TARGET_URL }}"
|
||||
|
||||
curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
|
||||
-F "title=$TITLE" \
|
||||
-F "message=$MESSAGE" \
|
||||
-F "priority=$PRIORITY" || true
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -56,4 +56,8 @@ apps/web/out/estimations/
|
||||
# Backups
|
||||
backups/
|
||||
|
||||
.turbo
|
||||
.turbo
|
||||
# Manual build artifacts
|
||||
_at-mintel/
|
||||
local_build_*.log
|
||||
*.tar
|
||||
|
||||
57
Dockerfile
57
Dockerfile
@@ -7,37 +7,65 @@ ARG NEXT_PUBLIC_BASE_URL
|
||||
ARG NEXT_PUBLIC_TARGET
|
||||
ARG UMAMI_API_ENDPOINT
|
||||
ARG NPM_TOKEN
|
||||
ARG S3_ENDPOINT
|
||||
ARG S3_ACCESS_KEY
|
||||
ARG S3_SECRET_KEY
|
||||
ARG S3_BUCKET
|
||||
ARG S3_REGION
|
||||
ARG S3_PREFIX
|
||||
|
||||
ARG BUILD_ID
|
||||
|
||||
# Environment variables for Next.js build
|
||||
ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
|
||||
ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET
|
||||
ENV UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
|
||||
ENV S3_ENDPOINT=$S3_ENDPOINT
|
||||
ENV S3_ACCESS_KEY=$S3_ACCESS_KEY
|
||||
ENV S3_SECRET_KEY=$S3_SECRET_KEY
|
||||
ENV S3_BUCKET=$S3_BUCKET
|
||||
ENV S3_REGION=$S3_REGION
|
||||
ENV S3_PREFIX=$S3_PREFIX
|
||||
|
||||
ENV SKIP_RUNTIME_ENV_VALIDATION=true
|
||||
ENV NEXT_BUILD_WORKERS=1
|
||||
ENV NEXT_DISABLE_SOURCEMAPS=true
|
||||
ENV SENTRY_SKIP_LOCAL_SOURCES=true
|
||||
ENV NEXT_PRIVATE_LOCAL_WEBPACK=true
|
||||
ENV CI=true
|
||||
|
||||
# Copy manifest files specifically for better layer caching
|
||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json .npmrc* ./
|
||||
COPY apps/web/package.json ./apps/web/package.json
|
||||
# Install build dependencies for native modules (like canvas)
|
||||
RUN apk add --no-cache python3 make g++ pkgconfig pixman-dev cairo-dev pango-dev libjpeg-turbo-dev giflib-dev librsvg-dev
|
||||
|
||||
# Copy sibling monorepo for linked dependencies (cloned during CI)
|
||||
COPY _at-mintel* ./_at-mintel/
|
||||
# Placing it inside /app so relative links like ../../_at-mintel resolve correctly!
|
||||
COPY _at-mintel* /app/_at-mintel/
|
||||
|
||||
|
||||
# Install dependencies with cache mount and dynamic .npmrc (High Fidelity pattern)
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
|
||||
--mount=type=secret,id=NPM_TOKEN \
|
||||
export NPM_TOKEN=$(cat /run/secrets/NPM_TOKEN 2>/dev/null || echo $NPM_TOKEN) && \
|
||||
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm/" > .npmrc && \
|
||||
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=\${NPM_TOKEN}" >> .npmrc && \
|
||||
echo "@mintel:registry=https://npm.infra.mintel.me" > .npmrc && \
|
||||
echo "//npm.infra.mintel.me/:_authToken=\"\${NPM_TOKEN}\"" >> .npmrc && \
|
||||
echo "always-auth=true" >> .npmrc && \
|
||||
cd _at-mintel && pnpm install --no-frozen-lockfile && pnpm build && \
|
||||
cd /app/_at-mintel && pnpm install --no-frozen-lockfile && pnpm build && \
|
||||
cd /app && pnpm install --no-frozen-lockfile && \
|
||||
rm .npmrc
|
||||
|
||||
# Copy source code
|
||||
# We use BUILD_ID here to ensure that if the commit changes, we always COPY the latest files
|
||||
# even if Docker's metadata-based fingerprinting for the public directory fails.
|
||||
ARG BUILD_ID
|
||||
RUN echo "Building with ID: ${BUILD_ID}"
|
||||
COPY . .
|
||||
|
||||
# Build application (monorepo filter)
|
||||
ENV NODE_OPTIONS="--max_old_space_size=4096"
|
||||
RUN pnpm --filter @mintel/web build
|
||||
RUN NEXT_BUILD_WORKERS=1 pnpm --filter @mintel/web build
|
||||
|
||||
# Stage 2: Runner
|
||||
FROM git.infra.mintel.me/mmintel/runtime:latest AS runner
|
||||
@@ -45,11 +73,18 @@ WORKDIR /app
|
||||
|
||||
# Copy standalone output and static files (Monorepo paths)
|
||||
# Note: Base image already handles the non-root user and basic env
|
||||
COPY --from=builder /app/apps/web/public ./apps/web/public
|
||||
COPY --from=builder /app/apps/web/.next/standalone ./
|
||||
COPY --from=builder /app/apps/web/.next/static ./apps/web/.next/static
|
||||
COPY --from=builder /app/apps/web/.next/cache ./apps/web/.next/cache
|
||||
COPY --from=builder --chown=1001:65533 /app/apps/web/public ./apps/web/public
|
||||
# Fallback target: Next.js sometimes resolves from the process root depending on version
|
||||
COPY --from=builder --chown=1001:65533 /app/apps/web/public ./public
|
||||
COPY --from=builder --chown=1001:65533 /app/apps/web/.next/standalone ./
|
||||
COPY --from=builder --chown=1001:65533 /app/apps/web/.next/static ./apps/web/.next/static
|
||||
|
||||
|
||||
|
||||
# Fix permissions for the non-root user (Standard uid/gid from base image)
|
||||
USER nextjs
|
||||
|
||||
# Start from the app directory to ensure references solve correctly
|
||||
WORKDIR /app/apps/web
|
||||
CMD ["node", "server.js"]
|
||||
# In Standalone mode, Next.js expects node_modules and public relative to the server.js
|
||||
WORKDIR /app
|
||||
CMD ["node", "apps/web/server.js"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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++
|
||||
RUN apk add --no-cache libc6-compat git python3 make g++
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
"use server";
|
||||
import { handleServerFunctions as payloadHandleServerFunctions } from "@payloadcms/next/layouts";
|
||||
import config from "@payload-config";
|
||||
// @ts-ignore - Payload generates this file during the build process
|
||||
import { importMap } from "./admin/importMap";
|
||||
|
||||
export const handleServerFunctions = async (args: any) => {
|
||||
return payloadHandleServerFunctions({
|
||||
...args,
|
||||
config,
|
||||
importMap,
|
||||
});
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import configPromise from "@payload-config";
|
||||
import { RootPage, generatePageMetadata } from "@payloadcms/next/views";
|
||||
// @ts-ignore - Payload generates this file during the build process
|
||||
import { importMap } from "../importMap";
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
segments: string[];
|
||||
}>;
|
||||
searchParams: Promise<{
|
||||
[key: string]: string | string[];
|
||||
}>;
|
||||
};
|
||||
|
||||
export const generateMetadata = async ({
|
||||
params,
|
||||
searchParams,
|
||||
}: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config: configPromise, params, searchParams });
|
||||
|
||||
const Page = async ({ params, searchParams }: Args) =>
|
||||
RootPage({ config: configPromise, importMap, params, searchParams });
|
||||
|
||||
export default Page;
|
||||
@@ -1 +0,0 @@
|
||||
export const importMap = {};
|
||||
@@ -1,16 +0,0 @@
|
||||
import config from "@payload-config";
|
||||
import {
|
||||
REST_DELETE,
|
||||
REST_GET,
|
||||
REST_OPTIONS,
|
||||
REST_PATCH,
|
||||
REST_POST,
|
||||
REST_PUT,
|
||||
} from "@payloadcms/next/routes";
|
||||
|
||||
export const GET = REST_GET(config);
|
||||
export const POST = REST_POST(config);
|
||||
export const DELETE = REST_DELETE(config);
|
||||
export const OPTIONS = REST_OPTIONS(config);
|
||||
export const PATCH = REST_PATCH(config);
|
||||
export const PUT = REST_PUT(config);
|
||||
@@ -1,20 +0,0 @@
|
||||
import configPromise from "@payload-config";
|
||||
import "@payloadcms/next/css";
|
||||
import { RootLayout } from "@payloadcms/next/layouts";
|
||||
import React from "react";
|
||||
|
||||
import { handleServerFunctions } from "./actions";
|
||||
// @ts-ignore - Payload generates this file during the build process
|
||||
import { importMap } from "./admin/importMap";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<RootLayout
|
||||
config={configPromise}
|
||||
importMap={importMap}
|
||||
serverFunction={handleServerFunctions}
|
||||
>
|
||||
{children}
|
||||
</RootLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import * as React from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getPayloadHMR } from "@payloadcms/next/utilities";
|
||||
import configPromise from "@payload-config";
|
||||
|
||||
import { getAllPosts } from "@/src/lib/posts";
|
||||
import { BlogPostHeader } from "@/src/components/blog/BlogPostHeader";
|
||||
import { Section } from "@/src/components/Section";
|
||||
@@ -11,7 +10,7 @@ import { BlogPostClient } from "@/src/components/BlogPostClient";
|
||||
import { TextSelectionShare } from "@/src/components/TextSelectionShare";
|
||||
import { BlogPostStickyBar } from "@/src/components/blog/BlogPostStickyBar";
|
||||
import { MDXContent } from "@/src/components/MDXContent";
|
||||
import { PayloadRichText } from "@/src/components/PayloadRichText";
|
||||
|
||||
import { TableOfContents } from "@/src/components/TableOfContents";
|
||||
|
||||
export async function generateStaticParams() {
|
||||
@@ -58,18 +57,6 @@ export default async function BlogPostPage({
|
||||
const post = allPosts.find((p) => p.slug === slug);
|
||||
|
||||
if (!post) {
|
||||
const payload = await getPayloadHMR({ config: configPromise });
|
||||
const redirectDoc = await payload.find({
|
||||
collection: "redirects",
|
||||
where: {
|
||||
from: { equals: slug },
|
||||
},
|
||||
});
|
||||
|
||||
if (redirectDoc.docs.length > 0) {
|
||||
redirect(`/blog/${redirectDoc.docs[0].to}`);
|
||||
}
|
||||
|
||||
notFound();
|
||||
}
|
||||
|
||||
@@ -119,11 +106,7 @@ export default async function BlogPostPage({
|
||||
|
||||
<div className="article-content max-w-none">
|
||||
<TableOfContents />
|
||||
{post.lexicalContent ? (
|
||||
<PayloadRichText data={post.lexicalContent} />
|
||||
) : (
|
||||
<MDXContent code={post.body.code} />
|
||||
)}
|
||||
<MDXContent code={post.mdxContent || ""} />
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,8 @@ import { getAllPosts } from "@/src/lib/posts";
|
||||
import { BlogClient } from "@/src/components/blog/BlogClient";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Blog | Mintel.me",
|
||||
description:
|
||||
|
||||
@@ -24,19 +24,19 @@
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-4xl md:text-8xl leading-[1.1] md:leading-[0.95] mb-6 md:mb-12;
|
||||
@apply text-4xl md:text-6xl lg:text-8xl leading-[1.1] md:leading-[1] lg:leading-[0.95] mb-6 md:mb-8 lg:mb-12;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-2xl md:text-6xl leading-tight mb-4 md:mb-8 mt-12 md:mt-16;
|
||||
@apply text-2xl md:text-4xl lg:text-6xl leading-tight mb-4 md:mb-6 lg:mb-8 mt-12 lg:mt-16;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-xl md:text-5xl leading-tight mb-3 md:mb-6 mt-8 md:mt-12;
|
||||
@apply text-xl md:text-3xl lg:text-5xl leading-tight mb-3 md:mb-4 lg:mb-6 mt-8 lg:mt-12;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply text-lg md:text-3xl leading-tight mb-3 md:mb-4 mt-6 md:mt-8;
|
||||
@apply text-lg md:text-2xl lg:text-3xl leading-tight mb-3 lg:mb-4 mt-6 lg:mt-8;
|
||||
}
|
||||
|
||||
p {
|
||||
@@ -44,7 +44,7 @@
|
||||
}
|
||||
|
||||
.lead {
|
||||
@apply text-base md:text-2xl text-slate-600 mb-6 leading-relaxed;
|
||||
@apply text-base md:text-xl lg:text-2xl text-slate-600 mb-6 leading-relaxed;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@@ -78,6 +78,28 @@
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Interactive elements pointer */
|
||||
a,
|
||||
button,
|
||||
[role="button"],
|
||||
input[type="button"],
|
||||
input[type="submit"],
|
||||
input[type="reset"],
|
||||
select,
|
||||
summary,
|
||||
label[for] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Ensure disabled elements don't show pointer */
|
||||
button:disabled,
|
||||
[role="button"]:disabled,
|
||||
input:disabled,
|
||||
select:disabled,
|
||||
button[disabled] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Remove default tap highlight on mobile */
|
||||
* {
|
||||
-webkit-tap-highlight-color: transparent !important;
|
||||
@@ -92,15 +114,15 @@
|
||||
}
|
||||
|
||||
.container {
|
||||
@apply max-w-6xl mx-auto px-5 md:px-6 py-8 md:py-12;
|
||||
@apply max-w-6xl mx-auto px-5 md:px-6 lg:px-8 py-8 md:py-10 lg:py-12;
|
||||
}
|
||||
|
||||
.wide-container {
|
||||
@apply max-w-7xl mx-auto px-5 md:px-6 py-10 md:py-16;
|
||||
@apply max-w-7xl mx-auto px-5 md:px-6 lg:px-8 py-10 md:py-12 lg:py-16;
|
||||
}
|
||||
|
||||
.narrow-container {
|
||||
@apply max-w-4xl mx-auto px-5 md:px-6 py-6 md:py-10;
|
||||
@apply max-w-4xl mx-auto px-5 md:px-6 lg:px-8 py-6 md:py-8 lg:py-10;
|
||||
}
|
||||
|
||||
.highlighter-tag {
|
||||
|
||||
@@ -26,6 +26,7 @@ import { HeroSection } from "@/src/components/HeroSection";
|
||||
import { GlitchText } from "@/src/components/GlitchText";
|
||||
import { Marker } from "@/src/components/Marker";
|
||||
import { PenCircle } from "@/src/components/PenCircle";
|
||||
import { Availability } from "@/src/components/Availability";
|
||||
|
||||
export default function LandingPage() {
|
||||
return (
|
||||
@@ -50,7 +51,7 @@ export default function LandingPage() {
|
||||
</H3>
|
||||
</Reveal>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 relative z-10">
|
||||
<div className="grid grid-cols-1 md:grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12 relative z-10">
|
||||
{[
|
||||
{
|
||||
icon: <ConceptCommunication className="w-8 h-8" />,
|
||||
@@ -136,7 +137,7 @@ export default function LandingPage() {
|
||||
|
||||
{/* Section 04: Target Group */}
|
||||
<Section number="04" title="Für wen" borderTop>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6 relative z-10">
|
||||
<div className="grid grid-cols-1 md:grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6 lg:gap-10 relative z-10">
|
||||
<Reveal>
|
||||
<Card variant="glass" padding="normal" techBorder className="group">
|
||||
<div className="space-y-4 md:space-y-6 relative overflow-hidden">
|
||||
@@ -215,8 +216,8 @@ export default function LandingPage() {
|
||||
},
|
||||
].map((service, i) => (
|
||||
<Reveal key={i} delay={0.1 + i * 0.15}>
|
||||
<div className="group py-8 md:py-16 border-b border-slate-100 last:border-b-0 cursor-pointer transition-all duration-500">
|
||||
<div className="flex flex-col md:flex-row md:items-start gap-6 md:gap-16">
|
||||
<div className="group py-8 md:py-12 lg:py-16 border-b border-slate-100 last:border-b-0 cursor-pointer transition-all duration-500">
|
||||
<div className="flex flex-col md:flex-row md:items-start gap-6 md:gap-10 lg:gap-16">
|
||||
{/* Number + Binary */}
|
||||
<div className="shrink-0 flex md:block items-baseline gap-4">
|
||||
<span className="text-4xl md:text-6xl font-black text-slate-100 group-hover:text-slate-200 transition-colors duration-500 tracking-tighter block leading-none">
|
||||
@@ -280,13 +281,13 @@ export default function LandingPage() {
|
||||
<Section number="06" title="Kontakt" borderTop>
|
||||
<div className="relative py-4 md:py-12" id="contact">
|
||||
<Reveal>
|
||||
<div className="space-y-8 md:space-y-16">
|
||||
<H1 className="text-3xl md:text-8xl">
|
||||
<div className="space-y-8 md:space-y-12 lg:space-y-16">
|
||||
<H1 className="text-4xl md:text-6xl lg:text-8xl">
|
||||
Lassen Sie uns <br />
|
||||
<span className="text-slate-400">starten.</span>
|
||||
</H1>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-6 md:gap-16 items-start relative z-10">
|
||||
<div className="flex flex-col md:flex-row gap-6 md:gap-10 lg:gap-16 items-start relative z-10">
|
||||
<div className="space-y-4 md:space-y-8 flex-1">
|
||||
<LeadText className="text-lg md:text-3xl text-slate-400">
|
||||
Beschreiben Sie kurz Ihr Vorhaben. Ich melde mich{" "}
|
||||
@@ -313,7 +314,7 @@ export default function LandingPage() {
|
||||
</div>
|
||||
<BodyText className="text-sm md:text-base leading-snug">
|
||||
Aktuell nehme ich Projekte für{" "}
|
||||
<span className="font-bold text-slate-900">Q2 2026</span>{" "}
|
||||
<span className="font-bold text-slate-900"><Availability /></span>{" "}
|
||||
an.
|
||||
</BodyText>
|
||||
</div>
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getPayload } from "payload";
|
||||
import configPromise from "@payload-config";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* Deep CMS Health Check
|
||||
* Validates that Payload CMS can actually query the database.
|
||||
* Used by post-deploy smoke tests to catch migration/schema issues.
|
||||
*/
|
||||
export async function GET() {
|
||||
const checks: Record<string, string> = {};
|
||||
|
||||
try {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
checks.init = "ok";
|
||||
|
||||
// Verify each collection can be queried (catches missing locale tables, broken migrations)
|
||||
// Adjusted for mintel.me collections
|
||||
const collections = ["posts", "projects", "media", "inquiries"] as const;
|
||||
for (const collection of collections) {
|
||||
try {
|
||||
await payload.find({ collection, limit: 1 });
|
||||
checks[collection] = "ok";
|
||||
} catch (e: any) {
|
||||
checks[collection] = `error: ${e.message?.substring(0, 100)}`;
|
||||
}
|
||||
}
|
||||
|
||||
const hasErrors = Object.values(checks).some((v) => v.startsWith("error"));
|
||||
return NextResponse.json(
|
||||
{ status: hasErrors ? "degraded" : "ok", checks },
|
||||
{ status: hasErrors ? 503 : 200 },
|
||||
);
|
||||
} catch (e: any) {
|
||||
return NextResponse.json(
|
||||
{ status: "error", message: e.message?.substring(0, 200), checks },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -172,10 +172,7 @@ sequenceDiagram
|
||||
Gartner schätzt, dass **40% aller CRM-Fehlschläge** auf Prozessprobleme oder menschliches Versagen zurückzuführen sind. Eine automatisierte Integration ist die wirksamste Versicherung gegen dieses Risiko. [Vermeiden Sie technische Altlasten](/blog/slow-loading-costs-customers) und machen Sie Ihre Website zu einem integralen Bestandteil Ihres Sales-Motors.
|
||||
</Paragraph>
|
||||
|
||||
<PortfolioHeader
|
||||
title="Bereit für echte Automatisierung?"
|
||||
sub="Präzision in der Schnittstelle, Klarheit im Ergebnis."
|
||||
/>
|
||||
|
||||
|
||||
<FAQSection>
|
||||
<H3>Wie lange dauert die Integration eines CRMs?</H3>
|
||||
|
||||
@@ -18,7 +18,7 @@ tags: ["management", "business"]
|
||||
|
||||
<Section>
|
||||
<H2>Inhaltsverzeichnis</H2>
|
||||
<TOC />
|
||||
<TableOfContents />
|
||||
</Section>
|
||||
|
||||
<H2>Die Falle der unendlichen Stunden</H2>
|
||||
|
||||
@@ -53,10 +53,7 @@ tags: ["performance", "seo", "conversion-optimization"]
|
||||
Google hat bestätigt, dass <ExternalLink href="https://developers.google.com/search/docs/appearance/core-web-vitals">Core Web Vitals</ExternalLink> als Ranking-Signale für Suchergebnisse genutzt werden. Wer hier rote Zahlen schreibt, wird vom Algorithmus abgestraft. Webseiten, die den Schwellenwert „Gut“ in allen Kategorien erreichen, verzeichnen laut Deloitte eine deutlich höhere Interaktion. Dennoch zeigt das HTTP Archive, dass ein signifikanter Prozentsatz der Websites die empfohlenen Grenzwerte weiterhin massiv verfehlt.
|
||||
</Paragraph>
|
||||
|
||||
<WebVitalsScore
|
||||
values={{ lcp: 2.1, inp: 180, cls: 0.05 }}
|
||||
description="Exzellente Werte signalisieren Google eine hohe Nutzerzufriedenheit und fördern das Ranking."
|
||||
/>
|
||||
<WebVitalsScore values={{ lcp: 2.1, inp: 180, cls: 0.05 }} description="Exzellente Werte signalisieren Google eine hohe Nutzerzufriedenheit und fördern das Ranking." />
|
||||
|
||||
<Paragraph>
|
||||
Dabei fokussiert sich das Framework auf drei wesentliche Säulen der User Experience, für die Google im PageSpeed Insights Tool spezifische Optimierungsvorschläge liefert:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Wartungsfrei durch Verzicht: Warum '
|
||||
thumbnail: "/blog/maintenance-for-headless-systems.png"No-CMS' die überlegene B2B-Architektur ist"
|
||||
title: "Wartungsfrei durch Verzicht: Warum 'No-CMS' die überlegene B2B-Architektur ist"
|
||||
thumbnail: "/blog/maintenance-for-headless-systems.png"
|
||||
description: "Sicherheit und Geschwindigkeit durch architektonische Reduktion: Warum Git-basierte Workflows klassische CMS-Backends in Performance und ROI schlagen."
|
||||
date: "2026-02-01"
|
||||
tags: ["maintenance", "architecture"]
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
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();
|
||||
@@ -1,34 +0,0 @@
|
||||
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,5 +1,5 @@
|
||||
import withMintelConfig from "@mintel/next-config";
|
||||
import { withPayload } from '@payloadcms/next/withPayload';
|
||||
|
||||
import createMDX from '@next/mdx';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
@@ -10,16 +10,20 @@ const dirname = path.dirname(filename);
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
serverExternalPackages: [
|
||||
'@mintel/content-engine',
|
||||
'@mintel/concept-engine',
|
||||
'@mintel/estimation-engine',
|
||||
'@mintel/payload-ai',
|
||||
'@mintel/pdf',
|
||||
'canvas',
|
||||
'sharp',
|
||||
'puppeteer',
|
||||
'require-in-the-middle',
|
||||
'import-in-the-middle' // Sentry 10+ instrumentation dependencies
|
||||
'import-in-the-middle'
|
||||
],
|
||||
transpilePackages: [
|
||||
'@mintel/content-engine',
|
||||
'@mintel/concept-engine',
|
||||
'@mintel/estimation-engine',
|
||||
'@mintel/meme-generator',
|
||||
|
||||
'@mintel/pdf',
|
||||
'@mintel/thumbnail-generator'
|
||||
],
|
||||
images: {
|
||||
remotePatterns: [
|
||||
@@ -33,12 +37,6 @@ const nextConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
// Umami proxy rewrite handled in app/stats/api/send/route.ts
|
||||
// Sentry relay handled in app/errors/api/relay/route.ts
|
||||
];
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
@@ -48,10 +46,42 @@ const nextConfig = {
|
||||
},
|
||||
];
|
||||
},
|
||||
async rewrites() {
|
||||
return {
|
||||
beforeFiles: [
|
||||
// Map Patttern: Global assets moved into KLZ showcase folder
|
||||
{
|
||||
source: '/assets/klz-cables.com/:path*',
|
||||
destination: '/showcase/klz-cables.com/assets/klz-cables.com/:path*',
|
||||
},
|
||||
{
|
||||
source: '/wp-content/:path*',
|
||||
destination: '/showcase/klz-cables.com/wp-content/:path*',
|
||||
},
|
||||
{
|
||||
source: '/wp-includes/:path*',
|
||||
destination: '/showcase/klz-cables.com/wp-includes/:path*',
|
||||
},
|
||||
{
|
||||
source: '/case-studies/assets/:directory/:path*',
|
||||
destination: '/showcase/:directory/assets/:directory/:path*',
|
||||
},
|
||||
]
|
||||
};
|
||||
},
|
||||
// In Standalone mode, Next.js expects the tracing root to be the monorepo root
|
||||
outputFileTracingRoot: path.join(dirname, '../../'),
|
||||
};
|
||||
|
||||
const withMDX = createMDX({
|
||||
// Add markdown plugins here, as desired
|
||||
});
|
||||
export default withPayload(withMintelConfig(withMDX(nextConfig)));
|
||||
const withMDX = createMDX({});
|
||||
|
||||
// Clean, standard wrapper application
|
||||
// Rewrites are now handled by src/middleware.ts for maximum robustness
|
||||
const config = withMintelConfig(withMDX(nextConfig));
|
||||
|
||||
// Cleanup config to prevent Next.js 16 warnings about deprecated keys
|
||||
if (config.serverActions) {
|
||||
delete config.serverActions;
|
||||
}
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
"version": "0.1.0",
|
||||
"description": "Technical problem solver's blog - practical insights and learning notes",
|
||||
"scripts": {
|
||||
"dev": "pnpm run seed:context && next dev --webpack --hostname 0.0.0.0",
|
||||
"dev:native": "DATABASE_URI=postgres://payload:payload@127.0.0.1:54321/payload PAYLOAD_SECRET=dev-secret pnpm run seed:context && DATABASE_URI=postgres://payload:payload@127.0.0.1:54321/payload PAYLOAD_SECRET=dev-secret next dev --webpack",
|
||||
"seed:context": "node --import tsx --experimental-loader ./ignore-css.mjs ./seed-context.ts",
|
||||
"dev": "next dev --webpack --hostname 0.0.0.0",
|
||||
"dev:native": "next dev --webpack",
|
||||
"build": "next build --webpack",
|
||||
"start": "next start",
|
||||
"lint": "eslint app src scripts video",
|
||||
@@ -23,14 +22,7 @@
|
||||
"pagespeed:test": "npx tsx ./scripts/pagespeed-sitemap.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"check:og": "tsx scripts/check-og-images.ts",
|
||||
"check:forms": "tsx scripts/check-forms.ts",
|
||||
"cms:push:testing": "bash ./scripts/cms-sync.sh push testing",
|
||||
"cms:pull:testing": "bash ./scripts/cms-sync.sh pull testing",
|
||||
"cms:push:staging": "bash ./scripts/cms-sync.sh push staging",
|
||||
"cms:pull:staging": "bash ./scripts/cms-sync.sh pull staging",
|
||||
"cms:push:prod": "bash ./scripts/cms-sync.sh push prod",
|
||||
"cms:pull:prod": "bash ./scripts/cms-sync.sh pull prod",
|
||||
"db:restore": "bash ./scripts/restore-db.sh"
|
||||
"check:forms": "tsx scripts/check-forms.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.750.0",
|
||||
@@ -41,7 +33,6 @@
|
||||
"@mintel/content-engine": "link:../../../at-mintel/packages/content-engine",
|
||||
"@mintel/estimation-engine": "link:../../../at-mintel/packages/estimation-engine",
|
||||
"@mintel/meme-generator": "link:../../../at-mintel/packages/meme-generator",
|
||||
"@mintel/payload-ai": "^1.9.15",
|
||||
"@mintel/pdf": "link:../../../at-mintel/packages/pdf-library",
|
||||
"@mintel/thumbnail-generator": "link:../../../at-mintel/packages/thumbnail-generator",
|
||||
"@next/mdx": "^16.1.6",
|
||||
@@ -50,12 +41,6 @@
|
||||
"@opentelemetry/context-async-hooks": "^2.1.0",
|
||||
"@opentelemetry/core": "^2.1.0",
|
||||
"@opentelemetry/sdk-trace-base": "^2.1.0",
|
||||
"@payloadcms/db-postgres": "^3.77.0",
|
||||
"@payloadcms/email-nodemailer": "^3.77.0",
|
||||
"@payloadcms/next": "^3.77.0",
|
||||
"@payloadcms/richtext-lexical": "^3.77.0",
|
||||
"@payloadcms/storage-s3": "^3.77.0",
|
||||
"@payloadcms/ui": "^3.77.0",
|
||||
"@react-pdf/renderer": "^4.3.2",
|
||||
"@remotion/bundler": "^4.0.414",
|
||||
"@remotion/cli": "^4.0.414",
|
||||
@@ -77,6 +62,7 @@
|
||||
"esbuild": "^0.27.3",
|
||||
"framer-motion": "^12.29.2",
|
||||
"graphql": "^16.12.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"html-to-image": "^1.11.13",
|
||||
"import-in-the-middle": "^1.11.0",
|
||||
"ioredis": "^5.9.1",
|
||||
@@ -85,7 +71,6 @@
|
||||
"next": "^16.1.6",
|
||||
"next-mdx-remote": "^6.0.0",
|
||||
"nodemailer": "^8.0.1",
|
||||
"payload": "^3.77.0",
|
||||
"playwright": "^1.58.1",
|
||||
"prismjs": "^1.30.0",
|
||||
"puppeteer": "^24.36.1",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,114 +0,0 @@
|
||||
import { buildConfig } from "payload";
|
||||
// Triggering config re-analysis for blocks visibility - V4
|
||||
import { postgresAdapter } from "@payloadcms/db-postgres";
|
||||
import { lexicalEditor, BlocksFeature } from "@payloadcms/richtext-lexical";
|
||||
import { payloadBlocks } from "./src/payload/blocks/allBlocks";
|
||||
import { nodemailerAdapter } from "@payloadcms/email-nodemailer";
|
||||
import { s3Storage } from "@payloadcms/storage-s3";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import sharp from "sharp";
|
||||
|
||||
import { Users } from "./src/payload/collections/Users";
|
||||
import { Media } from "./src/payload/collections/Media";
|
||||
import { Posts } from "./src/payload/collections/Posts";
|
||||
import { emailWebhookHandler } from "./src/payload/endpoints/emailWebhook";
|
||||
import { aiEndpointHandler } from "./src/payload/endpoints/aiEndpoint";
|
||||
import { Inquiries } from "./src/payload/collections/Inquiries";
|
||||
import { Redirects } from "./src/payload/collections/Redirects";
|
||||
import { ContextFiles } from "./src/payload/collections/ContextFiles";
|
||||
import { CrmAccounts } from "./src/payload/collections/CrmAccounts";
|
||||
import { CrmContacts } from "./src/payload/collections/CrmContacts";
|
||||
import { CrmInteractions } from "./src/payload/collections/CrmInteractions";
|
||||
import { CrmTopics } from "./src/payload/collections/CrmTopics";
|
||||
import { Projects } from "./src/payload/collections/Projects";
|
||||
|
||||
const filename = fileURLToPath(import.meta.url);
|
||||
const dirname = path.dirname(filename);
|
||||
|
||||
export default buildConfig({
|
||||
admin: {
|
||||
user: Users.slug,
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
},
|
||||
collections: [
|
||||
Users,
|
||||
Media,
|
||||
Posts,
|
||||
Inquiries,
|
||||
Redirects,
|
||||
ContextFiles,
|
||||
CrmAccounts,
|
||||
CrmContacts,
|
||||
CrmTopics,
|
||||
CrmInteractions,
|
||||
Projects,
|
||||
],
|
||||
globals: [
|
||||
/* AiSettings as any */
|
||||
],
|
||||
email: nodemailerAdapter({
|
||||
defaultFromAddress: process.env.MAIL_FROM || "info@mintel.me",
|
||||
defaultFromName: "Mintel.me",
|
||||
transportOptions: {
|
||||
host: process.env.MAIL_HOST || "localhost",
|
||||
port: parseInt(process.env.MAIL_PORT || "587", 10),
|
||||
auth: {
|
||||
user: process.env.MAIL_USERNAME || "user",
|
||||
pass: process.env.MAIL_PASSWORD || "pass",
|
||||
},
|
||||
...(process.env.MAIL_HOST ? {} : { ignoreTLS: true }),
|
||||
},
|
||||
}),
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
BlocksFeature({
|
||||
blocks: payloadBlocks,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
secret: process.env.PAYLOAD_SECRET || "fallback-secret-for-dev",
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, "payload-types.ts"),
|
||||
},
|
||||
db: postgresAdapter({
|
||||
pool: {
|
||||
connectionString:
|
||||
process.env.DATABASE_URI || process.env.POSTGRES_URI || "",
|
||||
},
|
||||
}),
|
||||
sharp,
|
||||
plugins: [
|
||||
...(process.env.S3_ENDPOINT
|
||||
? [
|
||||
s3Storage({
|
||||
collections: {
|
||||
media: {
|
||||
prefix: `${process.env.S3_PREFIX || "mintel-me"}/media`,
|
||||
},
|
||||
},
|
||||
bucket: process.env.S3_BUCKET || "",
|
||||
config: {
|
||||
credentials: {
|
||||
accessKeyId: process.env.S3_ACCESS_KEY || "",
|
||||
secretAccessKey: process.env.S3_SECRET_KEY || "",
|
||||
},
|
||||
region: process.env.S3_REGION || "fsn1",
|
||||
endpoint: process.env.S3_ENDPOINT,
|
||||
forcePathStyle: true,
|
||||
},
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
endpoints: [
|
||||
{
|
||||
path: "/crm/incoming-email",
|
||||
method: "post",
|
||||
handler: emailWebhookHandler,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -1,87 +0,0 @@
|
||||
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();
|
||||
@@ -89,14 +89,14 @@ resolve_target() {
|
||||
prod|production)
|
||||
REMOTE_PROJECT="mintel-me-production"
|
||||
REMOTE_DB_CONTAINER="mintel-me-production-postgres-db-1"
|
||||
REMOTE_APP_CONTAINER="mintel-me-production-app-1"
|
||||
REMOTE_APP_CONTAINER="mintel-me-production-mintel-me-app-1"
|
||||
REMOTE_SITE_DIR="/home/deploy/sites/mintel.me"
|
||||
;;
|
||||
branch-*)
|
||||
local SLUG=${TARGET#branch-}
|
||||
REMOTE_PROJECT="mintel-me-branch-$SLUG"
|
||||
REMOTE_DB_CONTAINER="${REMOTE_PROJECT}-postgres-db-1"
|
||||
REMOTE_APP_CONTAINER="${REMOTE_PROJECT}-app-1"
|
||||
REMOTE_APP_CONTAINER="${REMOTE_PROJECT}-mintel-me-app-1"
|
||||
REMOTE_SITE_DIR="/home/deploy/sites/branch.mintel.me/$SLUG"
|
||||
;;
|
||||
*)
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { getPayload } from "payload";
|
||||
import configPromise from "../payload.config";
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
|
||||
const existing = await payload.find({
|
||||
collection: "users",
|
||||
where: { email: { equals: "marc@mintel.me" } },
|
||||
});
|
||||
|
||||
if (existing.totalDocs > 0) {
|
||||
console.log("User already exists, updating password...");
|
||||
await payload.update({
|
||||
collection: "users",
|
||||
where: { email: { equals: "marc@mintel.me" } },
|
||||
data: {
|
||||
password: "Tim300493.",
|
||||
},
|
||||
});
|
||||
console.log("Password updated.");
|
||||
} else {
|
||||
console.log("Creating user...");
|
||||
await payload.create({
|
||||
collection: "users",
|
||||
data: {
|
||||
email: "marc@mintel.me",
|
||||
password: "Tim300493.",
|
||||
},
|
||||
});
|
||||
console.log("User marc@mintel.me created.");
|
||||
}
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error("Failed to create user:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -1,168 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import * as xlsxImport from "xlsx";
|
||||
const xlsx = (xlsxImport as any).default || xlsxImport;
|
||||
import { getPayload } from "payload";
|
||||
import configPromise from "../payload.config";
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
console.log("Initializing Payload...");
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
|
||||
const filePath = "/Users/marcmintel/Downloads/Akquise_Branchen.xlsx";
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error("File not found:", filePath);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Reading Excel file: ${filePath}`);
|
||||
const wb = xlsx.readFile(filePath);
|
||||
|
||||
let accountsCreated = 0;
|
||||
let contactsCreated = 0;
|
||||
|
||||
for (const sheetName of wb.SheetNames) {
|
||||
if (
|
||||
sheetName === "Weitere Kundenideen" ||
|
||||
sheetName.includes("BKF Firmen")
|
||||
)
|
||||
continue;
|
||||
|
||||
let industry = sheetName
|
||||
.replace(/^\d+_/, "")
|
||||
.replace(/^\d+\.\s*/, "")
|
||||
.replace(/_/g, " ");
|
||||
console.log(
|
||||
`\n--- Importing Sheet: ${sheetName} -> Industry: ${industry} ---`,
|
||||
);
|
||||
const rows = xlsx.utils.sheet_to_json(wb.Sheets[sheetName]);
|
||||
|
||||
for (const row of rows) {
|
||||
const companyName = row["Unternehmen"]?.trim();
|
||||
const website = row["Webseitenlink"]?.trim();
|
||||
let email = row["Emailadresse"]?.trim();
|
||||
const contactName = row["Ansprechpartner"]?.trim();
|
||||
const position = row["Position"]?.trim();
|
||||
const statusRaw = row["Webseiten-Status (alt/gut/schlecht)"]
|
||||
?.trim()
|
||||
?.toLowerCase();
|
||||
const notes = row["Notizen"]?.trim();
|
||||
|
||||
if (!companyName) continue;
|
||||
|
||||
let websiteStatus = "unknown";
|
||||
if (statusRaw === "gut") websiteStatus = "gut";
|
||||
else if (statusRaw === "ok" || statusRaw === "okay")
|
||||
websiteStatus = "ok";
|
||||
else if (
|
||||
statusRaw === "schlecht" ||
|
||||
statusRaw === "alt" ||
|
||||
statusRaw === "veraltet"
|
||||
)
|
||||
websiteStatus = "schlecht";
|
||||
|
||||
// Find or create account
|
||||
let accountId;
|
||||
const whereClause = website
|
||||
? { website: { equals: website } }
|
||||
: { name: { equals: companyName } };
|
||||
|
||||
const existingAccounts = await payload.find({
|
||||
collection: "crm-accounts",
|
||||
where: whereClause,
|
||||
});
|
||||
|
||||
if (existingAccounts.docs.length > 0) {
|
||||
accountId = existingAccounts.docs[0].id;
|
||||
console.log(`[SKIP] Account exists: ${companyName}`);
|
||||
} else {
|
||||
try {
|
||||
const newAccount = await payload.create({
|
||||
collection: "crm-accounts",
|
||||
data: {
|
||||
name: companyName,
|
||||
website: website || "",
|
||||
status: "lead",
|
||||
leadTemperature: "cold",
|
||||
industry,
|
||||
websiteStatus,
|
||||
notes,
|
||||
} as any,
|
||||
});
|
||||
accountId = newAccount.id;
|
||||
accountsCreated++;
|
||||
console.log(`[OK] Created account: ${companyName}`);
|
||||
} catch (err: any) {
|
||||
console.error(
|
||||
`[ERROR] Failed to create account ${companyName}:`,
|
||||
err.message,
|
||||
);
|
||||
continue; // Skip contact creation if account failed
|
||||
}
|
||||
}
|
||||
|
||||
// Handle contact
|
||||
if (email) {
|
||||
// Some rows have multiple emails or contacts. Let's just pick the first email if there are commas.
|
||||
if (email.includes(",")) email = email.split(",")[0].trim();
|
||||
|
||||
const existingContacts = await payload.find({
|
||||
collection: "crm-contacts",
|
||||
where: { email: { equals: email } },
|
||||
});
|
||||
|
||||
if (existingContacts.docs.length === 0) {
|
||||
let firstName = "Team";
|
||||
let lastName = companyName; // fallback
|
||||
|
||||
if (contactName) {
|
||||
// If multiple contacts are listed, just take the first one
|
||||
const firstContact = contactName.split(",")[0].trim();
|
||||
const parts = firstContact.split(" ");
|
||||
if (parts.length > 1) {
|
||||
lastName = parts.pop();
|
||||
firstName = parts.join(" ");
|
||||
} else {
|
||||
firstName = firstContact;
|
||||
lastName = "Contact";
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await payload.create({
|
||||
collection: "crm-contacts",
|
||||
data: {
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
role: position,
|
||||
account: accountId as any,
|
||||
},
|
||||
});
|
||||
contactsCreated++;
|
||||
console.log(` -> [OK] Created contact: ${email}`);
|
||||
} catch (err: any) {
|
||||
console.error(
|
||||
` -> [ERROR] Failed to create contact ${email}:`,
|
||||
err.message,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log(` -> [SKIP] Contact exists: ${email}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nMigration completed successfully!`);
|
||||
console.log(
|
||||
`Created ${accountsCreated} Accounts and ${contactsCreated} Contacts.`,
|
||||
);
|
||||
process.exit(0);
|
||||
} catch (e) {
|
||||
console.error("Migration failed:", e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -1,89 +0,0 @@
|
||||
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 {
|
||||
let payload;
|
||||
let retries = 5;
|
||||
while (retries > 0) {
|
||||
try {
|
||||
console.log(
|
||||
`Connecting to database (URI: ${process.env.DATABASE_URI || "default"})...`,
|
||||
);
|
||||
payload = await getPayload({ config: configPromise });
|
||||
break;
|
||||
} catch (e: any) {
|
||||
if (
|
||||
e.code === "ECONNREFUSED" ||
|
||||
e.code === "ENOTFOUND" ||
|
||||
e.message?.includes("ECONNREFUSED") ||
|
||||
e.message?.includes("ENOTFOUND") ||
|
||||
e.message?.includes("cannot connect to Postgres")
|
||||
) {
|
||||
console.log(
|
||||
`Database not ready (${e.code || "UNKNOWN"}), retrying in 3 seconds... (${retries} retries left)`,
|
||||
);
|
||||
retries--;
|
||||
await new Promise((res) => setTimeout(res, 3000));
|
||||
} else {
|
||||
console.error("Fatal connection error:", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!payload) {
|
||||
throw new Error(
|
||||
"Failed to connect to the database after multiple retries.",
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -5,34 +5,21 @@ import {
|
||||
getInquiryEmailHtml,
|
||||
getConfirmationEmailHtml,
|
||||
} from "../components/ContactForm/EmailTemplates";
|
||||
import { getPayload } from "payload";
|
||||
import configPromise from "@payload-config";
|
||||
|
||||
export async function sendContactInquiry(data: {
|
||||
name: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
role?: string;
|
||||
companyName: string;
|
||||
projectType: string;
|
||||
deadline?: string;
|
||||
message: string;
|
||||
isFreeText: boolean;
|
||||
config?: any;
|
||||
}) {
|
||||
try {
|
||||
// 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,
|
||||
},
|
||||
});
|
||||
|
||||
// Payload removed, directly send emails
|
||||
// 2. Send Inquiry to Marc
|
||||
const inquiryResult = await sendEmail({
|
||||
subject: `[PROJEKT] ${data.isFreeText ? "DIREKTANFRAGE" : "KONFIGURATION"}: ${data.companyName || data.name}`,
|
||||
|
||||
26
apps/web/src/components/Availability.tsx
Normal file
26
apps/web/src/components/Availability.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
/**
|
||||
* Automatisierte Anzeige der Projekt-Verfügbarkeit nach Quartalen.
|
||||
* Berechnet das aktuelle Quartal basierend auf dem heutigen Datum.
|
||||
*/
|
||||
export const Availability: React.FC = () => {
|
||||
const [text, setText] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
const now = new Date();
|
||||
const month = now.getMonth(); // 0-11
|
||||
const year = now.getFullYear();
|
||||
const quarter = Math.floor(month / 3) + 1;
|
||||
|
||||
// Aktuelles Quartal formatiert (z.B. Q2 2026)
|
||||
setText(`Q${quarter} ${year}`);
|
||||
}, []);
|
||||
|
||||
// Während Hydrierung nichts rendern, um Mismatches zu vermeiden (bei Client-only Logic)
|
||||
if (!text) return <span className="opacity-0">Q- 202-</span>;
|
||||
|
||||
return <>{text}</>;
|
||||
};
|
||||
@@ -62,7 +62,7 @@ export function ContactForm({
|
||||
initialStepIndex = 0,
|
||||
initialState: injectedState,
|
||||
}: ContactFormProps) {
|
||||
const [flow, setFlow] = useState<FlowState>("discovery");
|
||||
const [flow, setFlow] = useState<FlowState>("direct-message");
|
||||
const [stepIndex, setStepIndex] = useState(initialStepIndex);
|
||||
const [state, setState] = useState<FormState>({
|
||||
...initialState,
|
||||
@@ -125,8 +125,11 @@ export function ContactForm({
|
||||
const result = await sendContactInquiry({
|
||||
name: state.name,
|
||||
email: state.email,
|
||||
phone: state.phone,
|
||||
role: state.role,
|
||||
companyName: state.companyName,
|
||||
projectType: state.projectType,
|
||||
deadline: state.deadline,
|
||||
message: state.message,
|
||||
isFreeText: flow === "direct-message",
|
||||
config: flow === "configurator" ? state : undefined,
|
||||
@@ -190,7 +193,7 @@ export function ContactForm({
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsSubmitted(false);
|
||||
setFlow("discovery");
|
||||
setFlow("direct-message"); // Reset back to dm
|
||||
setStepIndex(0);
|
||||
setState(initialState);
|
||||
}}
|
||||
@@ -204,7 +207,7 @@ export function ContactForm({
|
||||
);
|
||||
}
|
||||
|
||||
// Gateway Flow
|
||||
// Gateway Flow (Disabled but kept logically if needed, can just not render)
|
||||
if (flow === "discovery") {
|
||||
return (
|
||||
<ContactGateway
|
||||
@@ -225,14 +228,25 @@ export function ContactForm({
|
||||
return (
|
||||
<DirectMessageFlow
|
||||
name={state.name}
|
||||
setName={(v) => updateState({ name: v })}
|
||||
email={state.email}
|
||||
setEmail={(v) => updateState({ email: v })}
|
||||
phone={state.phone}
|
||||
setPhone={(v) => updateState({ phone: v })}
|
||||
role={state.role}
|
||||
setRole={(v) => updateState({ role: v })}
|
||||
company={state.companyName}
|
||||
setCompany={(v) => updateState({ companyName: v })}
|
||||
projectType={state.projectType}
|
||||
setProjectType={(v) => updateState({ projectType: v })}
|
||||
deadline={state.deadline}
|
||||
setDeadline={(v) => updateState({ deadline: v })}
|
||||
message={state.message}
|
||||
setMessage={(v) => updateState({ message: v })}
|
||||
onBack={() => setFlow("discovery")}
|
||||
onSubmit={handleSubmit}
|
||||
onBack={() => setFlow("discovery")} // Can keep, but won't be seen if we hide button
|
||||
onSubmit={() => handleSubmit()}
|
||||
isSubmitting={isSubmitting}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,41 +88,40 @@ export const ContactGateway = ({
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 w-full">
|
||||
{/* Configurator Path */}
|
||||
<Reveal width="100%" delay={0.3} direction="up">
|
||||
<button
|
||||
onClick={onChooseConfigurator}
|
||||
disabled={!name}
|
||||
className={cn(
|
||||
"group relative flex flex-col items-start p-8 rounded-3xl border text-left transition-all duration-500 overflow-hidden",
|
||||
name
|
||||
? "bg-slate-900 border-slate-800 text-white shadow-2xl hover:-translate-y-2"
|
||||
: "bg-slate-50 border-slate-100 text-slate-400 cursor-not-allowed opacity-60",
|
||||
)}
|
||||
>
|
||||
<div className="absolute top-0 right-0 p-8 opacity-10 group-hover:opacity-20 transition-opacity">
|
||||
<Settings2 size={120} />
|
||||
</div>
|
||||
<div className="relative group p-[1px] rounded-3xl overflow-hidden transition-all duration-500">
|
||||
{/* Disabled Overlay Background */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-slate-200 to-slate-100 dark:from-slate-800 dark:to-slate-900 opacity-50" />
|
||||
|
||||
<Settings2 size={24} className="mb-6 text-green-400" />
|
||||
<h3 className="text-2xl font-bold mb-2 tracking-tight">
|
||||
System-Konfigurator
|
||||
</h3>
|
||||
<p className="text-sm text-slate-400 font-medium mb-8 max-w-[280px]">
|
||||
Konfigurieren Sie Ihr Projekt modular für eine präzise
|
||||
Aufwandsschätzung.
|
||||
</p>
|
||||
<button
|
||||
disabled
|
||||
className={cn(
|
||||
"w-full h-full relative flex flex-col items-start p-8 rounded-[23px] border text-left bg-slate-50 border-slate-100 text-slate-400 cursor-not-allowed",
|
||||
)}
|
||||
>
|
||||
<div className="absolute top-0 right-0 p-8 opacity-5">
|
||||
<Settings2 size={120} />
|
||||
</div>
|
||||
|
||||
<div className="mt-auto flex items-center gap-2 text-[10px] font-mono uppercase tracking-widest font-bold">
|
||||
<span>Sitzung starten</span>
|
||||
<ArrowRight
|
||||
size={14}
|
||||
className="group-hover:translate-x-1 transition-transform"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Settings2 size={24} className="text-slate-300" />
|
||||
<span className="px-2 py-0.5 rounded-full bg-slate-200 text-[8px] font-bold uppercase tracking-wider text-slate-500">
|
||||
Wartungsmodus
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{!name && (
|
||||
<div className="absolute inset-0 bg-slate-50/60 backdrop-blur-[6px] z-20" />
|
||||
)}
|
||||
</button>
|
||||
<h3 className="text-2xl font-bold mb-2 tracking-tight opacity-50">
|
||||
System-Konfigurator
|
||||
</h3>
|
||||
<p className="text-sm text-slate-400 font-medium mb-8 max-w-[280px] opacity-70">
|
||||
Dieser Modus wird aktuell optimiert und steht in Kürze wieder
|
||||
zur Verfügung.
|
||||
</p>
|
||||
|
||||
<div className="mt-auto flex items-center gap-2 text-[10px] font-mono uppercase tracking-widest font-bold opacity-30">
|
||||
<span>Konfigurator offline</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* Direct Mail Path */}
|
||||
|
||||
@@ -3,99 +3,247 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { cn } from "../../utils/cn";
|
||||
import { Reveal } from "../Reveal";
|
||||
import { Mail, MessageSquare, ArrowLeft, Send } from "lucide-react";
|
||||
import { ProjectType } from "./types";
|
||||
import {
|
||||
Mail,
|
||||
MessageSquare,
|
||||
ArrowLeft,
|
||||
Send,
|
||||
Phone,
|
||||
User as UserIcon,
|
||||
Calendar,
|
||||
Layers,
|
||||
} from "lucide-react";
|
||||
|
||||
interface DirectMessageFlowProps {
|
||||
name: string;
|
||||
setName: (val: string) => void;
|
||||
email: string;
|
||||
setEmail: (val: string) => void;
|
||||
phone: string;
|
||||
setPhone: (val: string) => void;
|
||||
role: string;
|
||||
setRole: (val: string) => void;
|
||||
company: string;
|
||||
setCompany: (val: string) => void;
|
||||
projectType: ProjectType;
|
||||
setProjectType: (val: ProjectType) => void;
|
||||
deadline: string;
|
||||
setDeadline: (val: string) => void;
|
||||
message: string;
|
||||
setMessage: (val: string) => void;
|
||||
onBack: () => void;
|
||||
onSubmit: () => void;
|
||||
isSubmitting: boolean;
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export const DirectMessageFlow = ({
|
||||
name,
|
||||
setName,
|
||||
email,
|
||||
setEmail,
|
||||
phone,
|
||||
setPhone,
|
||||
role,
|
||||
setRole,
|
||||
company,
|
||||
setCompany,
|
||||
projectType,
|
||||
setProjectType,
|
||||
deadline,
|
||||
setDeadline,
|
||||
message,
|
||||
setMessage,
|
||||
onBack,
|
||||
onSubmit,
|
||||
isSubmitting,
|
||||
error,
|
||||
}: DirectMessageFlowProps) => {
|
||||
return (
|
||||
<div className="w-full max-w-3xl mx-auto px-4 py-12">
|
||||
<Reveal width="100%" delay={0.1}>
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="flex items-center gap-2 text-[10px] font-mono font-bold uppercase tracking-widest text-slate-400 hover:text-slate-900 transition-colors mb-12"
|
||||
>
|
||||
<ArrowLeft size={14} /> Zurück zur Auswahl
|
||||
</button>
|
||||
</Reveal>
|
||||
|
||||
<div className="space-y-12">
|
||||
{error && (
|
||||
<Reveal width="100%" delay={0.1}>
|
||||
<div className="bg-red-50 border border-red-100 rounded-2xl p-6 flex items-start gap-4 mb-8">
|
||||
<div className="p-2 bg-red-100 rounded-lg text-red-600">
|
||||
<Mail size={20} className="animate-pulse" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-red-900 font-bold text-sm uppercase tracking-wider">
|
||||
Übertragungsfehler
|
||||
</h3>
|
||||
<p className="text-red-700/80 font-medium text-base">
|
||||
{error}
|
||||
</p>
|
||||
<div className="mt-2 text-[10px] font-mono text-red-400 uppercase tracking-widest">
|
||||
Status: FEHLER_BEI_SEQUENZ_INIT
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
)}
|
||||
|
||||
<Reveal width="100%" delay={0.2}>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-4">
|
||||
<span className="text-[10px] font-mono text-green-600 uppercase tracking-[0.3em] font-bold">
|
||||
DIREKTNACHRICHT // MODUS_AKTIVIERT
|
||||
DIREKTANFRAGE // SCHRITT_01
|
||||
</span>
|
||||
<h2 className="text-3xl font-bold tracking-tight text-slate-900">
|
||||
Wie kann ich helfen, {name.split(" ")[0]}?
|
||||
<h2 className="text-3xl md:text-5xl font-bold tracking-tight text-slate-900 line-clamp-2">
|
||||
Lassen Sie uns sprechen.
|
||||
</h2>
|
||||
<p className="text-slate-500 font-medium">
|
||||
Sende mir eine Nachricht zu {company || "deinem Projekt"} und ich
|
||||
melde mich in Kürze.
|
||||
<p className="text-slate-500 font-medium text-lg">
|
||||
Senden Sie mir eine Nachricht und ich melde mich zeitnah zurück.
|
||||
</p>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<div className="space-y-8">
|
||||
{/* Email Input */}
|
||||
<div className="space-y-12">
|
||||
{/* Section: Mission Focus */}
|
||||
<Reveal width="100%" delay={0.3} direction="up">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-6">
|
||||
<label className="flex items-center gap-2 text-[10px] font-mono font-bold uppercase tracking-widest text-slate-400">
|
||||
<Mail size={12} /> Rückantwort an
|
||||
<Layers size={12} /> Mission // Projekt-Typ
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="ihre@email.de"
|
||||
className="w-full bg-slate-50 border border-slate-100 rounded-2xl px-6 py-4 text-lg font-medium focus:outline-none focus:ring-2 focus:ring-slate-900/5 focus:border-slate-900 transition-all"
|
||||
/>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
{(["website", "web-app", "ecommerce"] as ProjectType[]).map(
|
||||
(type) => {
|
||||
const labels = {
|
||||
website: "Website",
|
||||
"web-app": "Web Application",
|
||||
ecommerce: "E-Commerce",
|
||||
};
|
||||
return (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => setProjectType(type)}
|
||||
className={cn(
|
||||
"px-6 py-4 rounded-xl border font-bold text-sm transition-all duration-200 text-left",
|
||||
projectType === type
|
||||
? "bg-slate-900 text-white border-slate-900 shadow-lg"
|
||||
: "bg-white border-slate-100 text-slate-500 hover:border-slate-300",
|
||||
)}
|
||||
>
|
||||
{labels[type]}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* Message Input */}
|
||||
{/* Section: Identity Details */}
|
||||
<Reveal width="100%" delay={0.4} direction="up">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center gap-2 text-[10px] font-mono font-bold uppercase tracking-widest text-slate-400">
|
||||
<UserIcon size={12} /> Ihr Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Max Mustermann"
|
||||
className="w-full bg-slate-50 border border-slate-100 rounded-xl px-4 py-3 text-base font-medium focus:outline-none focus:ring-2 focus:ring-slate-900/5 focus:border-slate-900 transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center gap-2 text-[10px] font-mono font-bold uppercase tracking-widest text-slate-400">
|
||||
<Layers size={12} /> Unternehmen (Optional)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={company}
|
||||
onChange={(e) => setCompany(e.target.value)}
|
||||
placeholder="Beispiel GmbH"
|
||||
className="w-full bg-slate-50 border border-slate-100 rounded-xl px-4 py-3 text-base font-medium focus:outline-none focus:ring-2 focus:ring-slate-900/5 focus:border-slate-900 transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center gap-2 text-[10px] font-mono font-bold uppercase tracking-widest text-slate-400">
|
||||
<Mail size={12} /> E-Mail Adresse
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="name@firma.de"
|
||||
className="w-full bg-slate-50 border border-slate-100 rounded-xl px-4 py-3 text-base font-medium focus:outline-none focus:ring-2 focus:ring-slate-900/5 focus:border-slate-900 transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center gap-2 text-[10px] font-mono font-bold uppercase tracking-widest text-slate-400">
|
||||
<Phone size={12} /> Rückruf-Nummer (Optional)
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
placeholder="+49 123 456789"
|
||||
className="w-full bg-slate-50 border border-slate-100 rounded-xl px-4 py-3 text-base font-medium focus:outline-none focus:ring-2 focus:ring-slate-900/5 focus:border-slate-900 transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* Section: Additional Context */}
|
||||
<Reveal width="100%" delay={0.5} direction="up">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center gap-2 text-[10px] font-mono font-bold uppercase tracking-widest text-slate-400">
|
||||
<UserIcon size={12} /> Ihre Position
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={role}
|
||||
onChange={(e) => setRole(e.target.value)}
|
||||
placeholder="z.B. Gründer, Marketing Lead..."
|
||||
className="w-full bg-slate-50 border border-slate-100 rounded-xl px-4 py-3 text-base font-medium focus:outline-none focus:ring-2 focus:ring-slate-900/5 focus:border-slate-900 transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center gap-2 text-[10px] font-mono font-bold uppercase tracking-widest text-slate-400">
|
||||
<Calendar size={12} /> Zeitfenster
|
||||
</label>
|
||||
<select
|
||||
value={deadline}
|
||||
onChange={(e) => setDeadline(e.target.value)}
|
||||
className="w-full bg-slate-50 border border-slate-100 rounded-xl px-4 py-3 text-base font-medium focus:outline-none focus:ring-2 focus:ring-slate-900/5 focus:border-slate-900 transition-all appearance-none cursor-pointer"
|
||||
>
|
||||
<option value="asap">ASAP (Sofort)</option>
|
||||
<option value="1month">< 1 Monat (Priorität)</option>
|
||||
<option value="3months">1-3 Monate (Standard)</option>
|
||||
<option value="flexible">Flexibel</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* Section: Payload */}
|
||||
<Reveal width="100%" delay={0.6} direction="up">
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center gap-2 text-[10px] font-mono font-bold uppercase tracking-widest text-slate-400">
|
||||
<MessageSquare size={12} /> Ihre Nachricht
|
||||
<MessageSquare size={12} /> Nachricht // Briefing
|
||||
</label>
|
||||
<textarea
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
placeholder="Beschreiben Sie kurz Ihr Anliegen..."
|
||||
rows={6}
|
||||
className="w-full bg-slate-50 border border-slate-100 rounded-2xl px-6 py-4 text-lg font-medium focus:outline-none focus:ring-2 focus:ring-slate-900/5 focus:border-slate-900 transition-all resize-none"
|
||||
placeholder="Beschreiben Sie kurz Ihr Anliegen oder hinterlassen Sie einen Link zum Briefing..."
|
||||
rows={5}
|
||||
className="w-full bg-slate-50 border border-slate-100 rounded-2xl px-6 py-4 text-base font-medium focus:outline-none focus:ring-2 focus:ring-slate-900/5 focus:border-slate-900 transition-all resize-none"
|
||||
/>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* Submit Button */}
|
||||
<Reveal width="100%" delay={0.5} direction="up">
|
||||
<Reveal width="100%" delay={0.7} direction="up">
|
||||
<button
|
||||
onClick={onSubmit}
|
||||
disabled={isSubmitting || !email || !message}
|
||||
disabled={isSubmitting || !email || !message || !name}
|
||||
className={cn(
|
||||
"group relative w-full py-5 rounded-2xl font-bold text-lg transition-all duration-300 flex items-center justify-center gap-3 overflow-hidden",
|
||||
isSubmitting || !email || !message
|
||||
isSubmitting || !email || !message || !name
|
||||
? "bg-slate-100 text-slate-400 cursor-not-allowed"
|
||||
: "bg-slate-900 text-white shadow-xl hover:shadow-2xl hover:-translate-y-1 active:scale-[0.98]",
|
||||
)}
|
||||
@@ -109,7 +257,7 @@ export const DirectMessageFlow = ({
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>Nachricht absenden</span>
|
||||
<span>Anfrage senden</span>
|
||||
<Send
|
||||
size={20}
|
||||
className="group-hover:translate-x-1 group-hover:-translate-y-1 transition-transform"
|
||||
|
||||
@@ -4,55 +4,86 @@ export const getInquiryEmailHtml = (data: any) => `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: 'Courier New', Courier, monospace; background-color: #0f172a; color: #f8fafc; margin: 0; padding: 20px; }
|
||||
.container { max-width: 600px; margin: 0 auto; background-color: #1e293b; border: 1px solid #334155; padding: 40px; border-radius: 8px; }
|
||||
.header { border-bottom: 2px solid #22c55e; padding-bottom: 20px; margin-bottom: 30px; }
|
||||
.title { font-size: 24px; font-weight: bold; letter-spacing: 2px; color: #f8fafc; }
|
||||
.label { color: #94a3b8; font-size: 12px; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 4px; }
|
||||
.value { font-size: 16px; margin-bottom: 20px; color: #22c55e; }
|
||||
.section { margin-bottom: 30px; }
|
||||
.footer { font-size: 10px; color: #64748b; margin-top: 40px; border-top: 1px solid #334155; padding-top: 20px; }
|
||||
body { font-family: Georgia, 'Times New Roman', Times, serif; background-color: #f8fafc; color: #1e293b; margin: 0; padding: 40px 20px; line-height: 1.6; }
|
||||
.wrapper { max-width: 600px; margin: 0 auto; background-color: #ffffff; border: 1px solid #e2e8f0; border-radius: 4px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
|
||||
.top-bar { background-color: #0f172a; color: #ffffff; padding: 12px 24px; display: flex; align-items: center; justify-content: space-between; }
|
||||
.brand { font-family: system-ui, sans-serif; font-size: 10px; font-weight: 800; letter-spacing: 0.2em; text-transform: uppercase; }
|
||||
.status { font-family: ui-monospace, monospace; font-size: 9px; color: #10b981; text-transform: uppercase; font-weight: bold; }
|
||||
.header { padding: 40px 32px 24px 32px; border-bottom: 2px solid #f1f5f9; }
|
||||
.heading { font-family: system-ui, sans-serif; font-size: 24px; font-weight: 800; color: #0f172a; margin: 0; letter-spacing: -0.02em; }
|
||||
.content { padding: 32px; }
|
||||
.data-grid { width: 100%; border-collapse: collapse; margin-bottom: 32px; }
|
||||
.data-row td { padding: 12px 0; border-bottom: 1px solid #f8fafc; vertical-align: top; }
|
||||
.label { font-family: system-ui, sans-serif; font-size: 9px; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.1em; width: 140px; }
|
||||
.value { font-size: 15px; color: #334155; }
|
||||
.message-container { background-color: #f8fafc; border: 1px solid #f1f5f9; border-radius: 4px; padding: 24px; margin-top: 12px; }
|
||||
.message-text { font-family: Georgia, serif; font-size: 16px; color: #1e293b; white-space: pre-wrap; margin: 0; }
|
||||
.config-code { font-family: ui-monospace, monospace; font-size: 12px; color: #64748b; background: #f1f5f9; padding: 16px; border-radius: 4px; display: block; overflow-x: auto; }
|
||||
.footer { padding: 32px; color: #94a3b8; font-size: 11px; text-align: center; border-top: 1px solid #f1f5f9; }
|
||||
.footer-meta { font-family: ui-monospace, monospace; font-size: 9px; margin-top: 12px; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="title">NEUE_ANFRAGE_INPUT</div>
|
||||
<div class="wrapper">
|
||||
<div class="top-bar">
|
||||
<div class="brand">MINTEL_TECHNICAL_OPERATIONS</div>
|
||||
<div class="status">● INCOMING_INQUIRY</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="label">ABSENDER</div>
|
||||
<div class="value">${data.name} (${data.email})</div>
|
||||
|
||||
<div class="label">UNTERNEHMEN</div>
|
||||
<div class="value">${data.companyName || "N/A"}</div>
|
||||
|
||||
<div class="label">PROJEKT_TYP</div>
|
||||
<div class="value">${data.projectType}</div>
|
||||
<div class="header">
|
||||
<h1 class="heading">${data.isFreeText ? "Direktanfrage" : "System-Konfiguration"}</h1>
|
||||
<p style="margin: 8px 0 0 0; color: #64748b; font-size: 14px;">Eingang für ${data.companyName || data.name}</p>
|
||||
</div>
|
||||
|
||||
${
|
||||
data.isFreeText
|
||||
? `
|
||||
<div class="section">
|
||||
<div class="label">NACHRICHT (FREITEXT)</div>
|
||||
<div class="value" style="white-space: pre-wrap; color: #f8fafc;">${data.message}</div>
|
||||
</div>
|
||||
`
|
||||
: `
|
||||
<div class="section">
|
||||
<div class="label">KONFIGURATION</div>
|
||||
<div class="value" style="font-size: 12px; color: #94a3b8; background: #0f172a; padding: 15px; border-radius: 4px;">
|
||||
${JSON.stringify(data.config, null, 2)}
|
||||
<div class="content">
|
||||
<table class="data-grid">
|
||||
<tr class="data-row">
|
||||
<td class="label">Absender</td>
|
||||
<td class="value"><strong>${data.name}</strong><br/><span style="color: #94a3b8; font-size: 13px;">${data.email}</span></td>
|
||||
</tr>
|
||||
${data.phone ? `
|
||||
<tr class="data-row">
|
||||
<td class="label">Telefon</td>
|
||||
<td class="value">${data.phone}</td>
|
||||
</tr>` : ""}
|
||||
${data.role ? `
|
||||
<tr class="data-row">
|
||||
<td class="label">Position</td>
|
||||
<td class="value">${data.role}</td>
|
||||
</tr>` : ""}
|
||||
<tr class="data-row">
|
||||
<td class="label">Unternehmen</td>
|
||||
<td class="value">${data.companyName || "—"}</td>
|
||||
</tr>
|
||||
<tr class="data-row">
|
||||
<td class="label">Projekt-Typ</td>
|
||||
<td class="value">${data.projectType}</td>
|
||||
</tr>
|
||||
${data.deadline ? `
|
||||
<tr class="data-row">
|
||||
<td class="label">Zeitraum</td>
|
||||
<td class="value">${data.deadline}</td>
|
||||
</tr>` : ""}
|
||||
</table>
|
||||
|
||||
${data.isFreeText ? `
|
||||
<div class="label" style="margin-bottom: 8px;">Nachricht</div>
|
||||
<div class="message-container">
|
||||
<div class="message-text">${data.message}</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
` : `
|
||||
<div class="label" style="margin-bottom: 8px;">Konfigurations-Daten</div>
|
||||
<code class="config-code">${JSON.stringify(data.config, null, 2)}</code>
|
||||
`}
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
SISTEM_STATUS: VALIDATED<br>
|
||||
TIMESTAMP: ${new Date().toISOString()}
|
||||
Dies ist eine systemgenerierte Benachrichtigung von mintel.me.<br/>
|
||||
<div class="footer-meta">
|
||||
TIMESTAMP: ${new Date().toISOString()} | STATUS: VALIDATED
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
@@ -63,32 +94,42 @@ export const getConfirmationEmailHtml = (data: any) => `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: 'Courier New', Courier, monospace; background-color: #f8fafc; color: #0f172a; margin: 0; padding: 20px; }
|
||||
.container { max-width: 600px; margin: 0 auto; background-color: #ffffff; border: 1px solid #e2e8f0; padding: 40px; border-radius: 12px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); }
|
||||
.header { text-align: center; margin-bottom: 40px; }
|
||||
.status-badge { display: inline-block; padding: 4px 12px; background-color: #22c55e; color: #0f172a; font-size: 10px; font-weight: bold; border-radius: 9999px; margin-bottom: 16px; }
|
||||
.title { font-size: 28px; font-weight: bold; letter-spacing: -0.02em; margin-bottom: 8px; }
|
||||
.subtitle { color: #64748b; font-size: 16px; line-height: 1.5; }
|
||||
.content { line-height: 1.6; color: #334155; margin-bottom: 40px; }
|
||||
.footer { text-align: center; font-size: 12px; color: #94a3b8; border-top: 1px solid #f1f5f9; padding-top: 30px; }
|
||||
body { font-family: Georgia, 'Times New Roman', Times, serif; background-color: #ffffff; color: #1e293b; margin: 0; padding: 40px 20px; line-height: 1.8; }
|
||||
.wrapper { max-width: 600px; margin: 0 auto; }
|
||||
.logo-container { padding-bottom: 48px; border-bottom: 1px solid #f1f5f9; margin-bottom: 48px; }
|
||||
.status-badge { display: inline-block; font-family: system-ui, sans-serif; font-size: 9px; font-weight: 800; color: #3b82f6; text-transform: uppercase; letter-spacing: 0.2em; border: 1px solid #dbeafe; background: #eff6ff; padding: 4px 12px; border-radius: 99px; margin-bottom: 24px; }
|
||||
.greeting { font-family: system-ui, sans-serif; font-size: 32px; font-weight: 800; color: #0f172a; margin: 0 0 16px 0; letter-spacing: -0.03em; }
|
||||
.body-text { font-size: 18px; color: #334155; margin-bottom: 32px; }
|
||||
.cta-box { border-left: 2px solid #0f172a; padding: 8px 0 8px 24px; margin: 40px 0; }
|
||||
.footer { margin-top: 80px; padding-top: 24px; border-top: 1px solid #f1f5f9; font-family: system-ui, sans-serif; font-size: 12px; color: #94a3b8; text-align: center; }
|
||||
.footer a { color: #0f172a; text-decoration: none; font-weight: 600; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="status-badge">SEQUENZ_INITIIERT</div>
|
||||
<div class="title">Hallo ${data.name.split(" ")[0]},</div>
|
||||
<div class="subtitle">vielen Dank für deine Anfrage.</div>
|
||||
<div class="wrapper">
|
||||
<div class="logo-container">
|
||||
<div class="status-badge">Anfrage eingegangen</div>
|
||||
<h1 class="greeting">Hallo ${data.name.split(" ")[0]},</h1>
|
||||
<p class="body-text">
|
||||
vielen Dank für dein Interesse an einer Zusammenarbeit. Deine Nachricht bezüglich <strong>${data.companyName || "deines Projekts"}</strong> ist sicher bei mir angekommen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<p>Ich habe deine Nachricht erhalten und schaue mir die Details zu <strong>${data.companyName || "deinem Projekt"}</strong> umgehend an.</p>
|
||||
<p>Normalerweise melde ich mich innerhalb von 24 Stunden bei dir zurück, um die nächsten Schritte zu besprechen.</p>
|
||||
<div class="body-text">
|
||||
Ich werde mir die Details umgehend ansehen und mich in der Regel innerhalb der nächsten 24 Stunden persönlich bei dir zurückmelden, um die nächsten Schritte zu besprechen.
|
||||
</div>
|
||||
|
||||
<div class="cta-box">
|
||||
<p style="margin:0; font-style: italic; color: #64748b;">
|
||||
„Technical problems are just puzzles with more moving parts.“
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
© ${new Date().getFullYear()} mintel.me — Technical Problem Solving
|
||||
© ${new Date().getFullYear()} <a href="https://mintel.me">mintel.me</a> — Marc Mintel<br/>
|
||||
Professional Website Systems & Technical Architecture
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -35,6 +35,7 @@ export interface FormState {
|
||||
storageExpansion: number;
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
role: string;
|
||||
message: string;
|
||||
sitemapFile: File | null;
|
||||
|
||||
@@ -47,7 +47,7 @@ export const HeroSection: React.FC = () => {
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 1, delay: 0.2 }}
|
||||
className="mb-4 md:mb-10 inline-flex items-center gap-3 md:gap-4 px-4 md:px-6 py-2 border border-slate-100 bg-white/40 backdrop-blur-sm rounded-full"
|
||||
className="mb-4 md:mb-8 lg:mb-10 inline-flex items-center gap-3 md:gap-4 px-4 md:px-6 py-2 border border-slate-100 bg-white/40 backdrop-blur-sm rounded-full"
|
||||
>
|
||||
<div className="flex gap-1">
|
||||
<div className="w-1 h-1 rounded-full bg-blue-500 animate-pulse" />
|
||||
@@ -59,7 +59,7 @@ export const HeroSection: React.FC = () => {
|
||||
</motion.div>
|
||||
|
||||
{/* Headline */}
|
||||
<h1 className="text-3xl md:text-[11rem] font-black tracking-tighter leading-[0.9] md:leading-[0.8] text-slate-900 mb-6 md:mb-12 uppercase">
|
||||
<h1 className="text-4xl md:text-8xl lg:text-[10rem] xl:text-[11rem] font-black tracking-tighter leading-[0.9] md:leading-[0.85] lg:leading-[0.8] text-slate-900 mb-6 md:mb-8 lg:mb-12 uppercase">
|
||||
<div className="block">
|
||||
<GlitchText delay={0.5} duration={1.2}>
|
||||
Websites
|
||||
@@ -80,9 +80,9 @@ export const HeroSection: React.FC = () => {
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 1, delay: 0.8 }}
|
||||
className="flex flex-col items-center gap-6 md:gap-12"
|
||||
className="flex flex-col items-center gap-6 md:gap-8 lg:gap-12"
|
||||
>
|
||||
<p className="text-base md:text-3xl text-slate-400 font-medium max-w-2xl leading-relaxed px-4">
|
||||
<p className="text-base md:text-xl lg:text-3xl text-slate-400 font-medium max-w-2xl leading-relaxed px-4">
|
||||
Ein Entwickler. Ein Ansprechpartner.{" "}
|
||||
<br className="hidden md:block" />
|
||||
<span className="text-slate-900 font-bold tracking-tight">
|
||||
|
||||
@@ -196,11 +196,13 @@ export const IframeSection: React.FC<IframeSectionProps> = ({
|
||||
|
||||
const updateScrollState = React.useCallback(() => {
|
||||
try {
|
||||
const win = iframeRef.current?.contentWindow;
|
||||
const doc = iframeRef.current?.contentDocument?.documentElement;
|
||||
if (doc) {
|
||||
const atTop = doc.scrollTop <= 5;
|
||||
if (doc && win) {
|
||||
const scrollTop = doc.scrollTop || win.scrollY || 0;
|
||||
const atTop = scrollTop <= 5;
|
||||
const atBottom =
|
||||
doc.scrollTop + doc.clientHeight >= doc.scrollHeight - 5;
|
||||
scrollTop + doc.clientHeight >= doc.scrollHeight - 5;
|
||||
const isScrollable = doc.scrollHeight > doc.clientHeight + 10;
|
||||
setScrollState({ atTop, atBottom, isScrollable });
|
||||
}
|
||||
@@ -493,7 +495,6 @@ export const IframeSection: React.FC<IframeSectionProps> = ({
|
||||
style.textContent = `
|
||||
*::-webkit-scrollbar { display: none !important; }
|
||||
* { -ms-overflow-style: none !important; scrollbar-width: none !important; }
|
||||
body { background: transparent !important; }
|
||||
`;
|
||||
iframe.contentDocument.head.appendChild(style);
|
||||
setTimeout(updateAmbilight, 600);
|
||||
|
||||
@@ -1,311 +0,0 @@
|
||||
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"}>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
@@ -82,9 +82,9 @@ export const Section: React.FC<SectionProps> = ({
|
||||
|
||||
<div className={cn("relative z-10", containerClass)}>
|
||||
{hasSidebar ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-4 md:gap-24">
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-4 md:gap-12 lg:gap-24">
|
||||
{/* Sidebar: Number & Title */}
|
||||
<div className="md:col-span-3">
|
||||
<div className="md:col-span-4 lg:col-span-3">
|
||||
<div className="md:sticky md:top-40 flex flex-col gap-4 md:gap-8">
|
||||
<div className="flex items-end md:flex-col md:items-start gap-6 md:gap-8">
|
||||
{number && (
|
||||
@@ -124,7 +124,7 @@ export const Section: React.FC<SectionProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="md:col-span-9">{children}</div>
|
||||
<div className="md:col-span-8 lg:col-span-9">{children}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full">{children}</div>
|
||||
|
||||
@@ -16,6 +16,10 @@ interface WebVitalsScoreProps {
|
||||
}
|
||||
|
||||
export const WebVitalsScore: React.FC<WebVitalsScoreProps> = ({ values, description }) => {
|
||||
if (!values) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const getStatus = (metric: 'lcp' | 'inp' | 'cls', value: number): 'good' | 'needs-improvement' | 'poor' => {
|
||||
if (metric === 'lcp') return value <= 2.5 ? 'good' : value <= 4.0 ? 'needs-improvement' : 'poor';
|
||||
if (metric === 'inp') return value <= 200 ? 'good' : value <= 500 ? 'needs-improvement' : 'poor';
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { ComponentShareButton } from '../ComponentShareButton';
|
||||
import { Reveal } from '../Reveal';
|
||||
import { Play, RotateCcw } from 'lucide-react';
|
||||
import { RotateCcw } from 'lucide-react';
|
||||
|
||||
export function LoadTimeSimulator({ className = '' }: { className?: string }) {
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [timeElapsed, setTimeElapsed] = useState(0);
|
||||
const [legacyState, setLegacyState] = useState(0);
|
||||
const [hasAutoStarted, setHasAutoStarted] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [mintelState, setMintelState] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -36,6 +38,25 @@ export function LoadTimeSimulator({ className = '' }: { className?: string }) {
|
||||
return () => clearInterval(interval);
|
||||
}, [isRunning, timeElapsed]);
|
||||
|
||||
// Auto-start the race when scrolled into viewport
|
||||
useEffect(() => {
|
||||
if (hasAutoStarted) return;
|
||||
const el = containerRef.current;
|
||||
if (!el) return;
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setHasAutoStarted(true);
|
||||
setIsRunning(true);
|
||||
observer.disconnect();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.4 }
|
||||
);
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, [hasAutoStarted]);
|
||||
|
||||
const startRace = () => {
|
||||
setTimeElapsed(0);
|
||||
setLegacyState(0);
|
||||
@@ -45,7 +66,7 @@ export function LoadTimeSimulator({ className = '' }: { className?: string }) {
|
||||
|
||||
return (
|
||||
<Reveal direction="up" delay={0.1}>
|
||||
<div className={`not-prose max-w-4xl mx-auto my-12 relative group ${className}`}>
|
||||
<div ref={containerRef} className={`not-prose max-w-4xl mx-auto my-12 relative group ${className}`}>
|
||||
<div className="absolute -inset-1 bg-gradient-to-r from-red-100 to-emerald-100 rounded-3xl blur opacity-30" />
|
||||
|
||||
<div id="sim-load-time" className="relative bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden flex flex-col">
|
||||
@@ -63,13 +84,15 @@ export function LoadTimeSimulator({ className = '' }: { className?: string }) {
|
||||
Simulieren Sie den Unterschied zwischen dynamischem Server-Rendering (PHP/MySQL) und statischer Edge-Auslieferung (<span className="font-mono bg-slate-200 px-1 rounded text-[10px]">TTV < 500ms</span>).
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={startRace}
|
||||
className="shrink-0 flex items-center gap-2 px-6 py-2.5 bg-slate-900 !text-white rounded-full font-bold text-sm hover:hover:bg-black hover:scale-105 active:scale-95 transition-all shadow-md"
|
||||
>
|
||||
{timeElapsed > 0 ? <RotateCcw size={16} /> : <Play size={16} />}
|
||||
{timeElapsed > 0 ? "Neustart" : "Rennen Starten"}
|
||||
</button>
|
||||
{timeElapsed > 0 && !isRunning && (
|
||||
<button
|
||||
onClick={startRace}
|
||||
className="shrink-0 flex items-center gap-2 px-6 py-2.5 bg-slate-900 !text-white rounded-full font-bold text-sm hover:hover:bg-black hover:scale-105 active:scale-95 transition-all shadow-md"
|
||||
>
|
||||
<RotateCcw size={16} />
|
||||
Neustart
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 divide-y md:divide-y-0 md:divide-x divide-slate-100 bg-slate-50/50">
|
||||
|
||||
@@ -85,6 +85,7 @@ export const mdxComponents = {
|
||||
Section,
|
||||
Reveal,
|
||||
TableOfContents,
|
||||
TOC: TableOfContents,
|
||||
RevenueLossCalculator,
|
||||
PerformanceChart,
|
||||
PerformanceROICalculator,
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
import { ComponentDefinition } from "@mintel/content-engine";
|
||||
import { allComponentDefinitions } from "../payload/blocks/allBlocks";
|
||||
|
||||
/**
|
||||
* Single Source of Truth for all MDX component definitions.
|
||||
* Now dynamically generated from individual Payload block definitions.
|
||||
*/
|
||||
export const componentDefinitions: ComponentDefinition[] =
|
||||
allComponentDefinitions;
|
||||
export const componentDefinitions: ComponentDefinition[] = [];
|
||||
|
||||
@@ -8,8 +8,8 @@ const envExtension = {
|
||||
// Mail Configuration
|
||||
MAIL_HOST: z.string().optional(),
|
||||
MAIL_PORT: z.coerce.number().optional().default(587),
|
||||
MAIL_USER: z.string().optional(),
|
||||
MAIL_PASS: z.string().optional(),
|
||||
MAIL_USERNAME: z.string().optional(),
|
||||
MAIL_PASSWORD: z.string().optional(),
|
||||
MAIL_FROM: z.string().optional().default("marc@mintel.me"),
|
||||
MAIL_RECIPIENTS: z.string().optional().default("marc@mintel.me"),
|
||||
|
||||
@@ -21,8 +21,16 @@ const envExtension = {
|
||||
.optional()
|
||||
.default("https://analytics.infra.mintel.me"),
|
||||
|
||||
// S3 Storage (Required for importMap at build-time)
|
||||
S3_ENDPOINT: z.string().optional(),
|
||||
S3_ACCESS_KEY: z.string().optional(),
|
||||
S3_SECRET_KEY: z.string().optional(),
|
||||
S3_BUCKET: z.string().optional(),
|
||||
S3_REGION: z.string().optional(),
|
||||
S3_PREFIX: z.string().optional(),
|
||||
|
||||
// Error Tracking
|
||||
SENTRY_DSN: z.string().url().optional(),
|
||||
SENTRY_DSN: z.string().optional(),
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,8 +24,8 @@ function getTransporter() {
|
||||
port: env.MAIL_PORT,
|
||||
secure: env.MAIL_PORT === 465,
|
||||
auth: {
|
||||
user: env.MAIL_USER,
|
||||
pass: env.MAIL_PASS,
|
||||
user: env.MAIL_USERNAME,
|
||||
pass: env.MAIL_PASSWORD,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -45,11 +45,23 @@ export async function sendEmail({
|
||||
subject,
|
||||
html,
|
||||
}: SendEmailOptions) {
|
||||
const recipients = to || env.MAIL_RECIPIENTS;
|
||||
let recipients = to || env.MAIL_RECIPIENTS;
|
||||
let from = env.MAIL_FROM;
|
||||
|
||||
if (!from) {
|
||||
from = "info@mintel.me";
|
||||
console.warn("MAIL_FROM is empty. Using fallback: info@mintel.me");
|
||||
}
|
||||
|
||||
if (!recipients) {
|
||||
recipients = "marc@mintel.me";
|
||||
console.warn("MAIL_RECIPIENTS is empty. Using fallback: marc@mintel.me");
|
||||
}
|
||||
|
||||
const transporter = getTransporter();
|
||||
|
||||
const mailOptions = {
|
||||
from: env.MAIL_FROM,
|
||||
from,
|
||||
to: recipients,
|
||||
replyTo,
|
||||
subject,
|
||||
|
||||
@@ -1,58 +1,37 @@
|
||||
import { getPayload } from "payload";
|
||||
import configPromise from "@payload-config";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import matter from "gray-matter";
|
||||
|
||||
const POSTS_DIR = path.join(process.cwd(), "content/blog");
|
||||
|
||||
export async function getAllPosts() {
|
||||
if (!process.env.DATABASE_URI && !process.env.POSTGRES_URI) {
|
||||
console.warn(
|
||||
"⚠️ Bypassing Payload fetch during Next.js build: DATABASE_URI is missing.",
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
const { docs } = await payload.find({
|
||||
collection: "posts",
|
||||
limit: 1000,
|
||||
sort: "-date",
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
_status: {
|
||||
equals: "published",
|
||||
},
|
||||
},
|
||||
{
|
||||
date: {
|
||||
less_than_equal: new Date(),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
if (!fs.existsSync(POSTS_DIR)) return [];
|
||||
|
||||
const files = fs.readdirSync(POSTS_DIR);
|
||||
const posts = files
|
||||
.filter((filename) => filename.endsWith(".mdx"))
|
||||
.map((filename) => {
|
||||
const filePath = path.join(POSTS_DIR, filename);
|
||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
||||
const { data, content } = matter(fileContent);
|
||||
|
||||
return docs.map((doc) => ({
|
||||
title: doc.title as string,
|
||||
description: doc.description as string,
|
||||
date: doc.date as string,
|
||||
tags: (doc.tags || []).map((t) =>
|
||||
typeof t === "object" && t !== null ? t.tag : t,
|
||||
) as string[],
|
||||
slug: doc.slug as string,
|
||||
thumbnail:
|
||||
(doc.featuredImage &&
|
||||
typeof doc.featuredImage === "object" &&
|
||||
doc.featuredImage.url
|
||||
? doc.featuredImage.url
|
||||
: "") || "",
|
||||
body: { code: "" as string },
|
||||
lexicalContent: doc.content || null,
|
||||
}));
|
||||
return {
|
||||
title: data.title || "",
|
||||
description: data.description || "",
|
||||
date: data.date || new Date().toISOString(),
|
||||
tags: data.tags || [],
|
||||
slug: filename.replace(/\.mdx$/, ""),
|
||||
thumbnail: data.thumbnail || "",
|
||||
body: { code: "" },
|
||||
lexicalContent: null,
|
||||
mdxContent: content,
|
||||
};
|
||||
});
|
||||
|
||||
return posts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"⚠️ Bypassing Payload fetch during build: Database connection refused.",
|
||||
error,
|
||||
);
|
||||
console.error("Error reading MDX posts:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ export const initialState: FormState = {
|
||||
storageExpansion: 0,
|
||||
name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
role: "",
|
||||
message: "",
|
||||
sitemapFile: null,
|
||||
|
||||
@@ -1,89 +1,90 @@
|
||||
export type ProjectType = 'website' | 'web-app';
|
||||
export type ProjectType = "website" | "web-app";
|
||||
|
||||
export interface FormState {
|
||||
projectType: ProjectType;
|
||||
// Company
|
||||
companyName: string;
|
||||
employeeCount: string;
|
||||
// Existing Presence
|
||||
existingWebsite: string;
|
||||
socialMedia: string[];
|
||||
socialMediaUrls: Record<string, string>;
|
||||
existingDomain: string;
|
||||
wishedDomain: string;
|
||||
// Project
|
||||
websiteTopic: string;
|
||||
selectedPages: string[];
|
||||
otherPages: string[];
|
||||
otherPagesCount: number;
|
||||
features: string[];
|
||||
otherFeatures: string[];
|
||||
otherFeaturesCount: number;
|
||||
functions: string[];
|
||||
otherFunctions: string[];
|
||||
otherFunctionsCount: number;
|
||||
apiSystems: string[];
|
||||
otherTech: string[];
|
||||
otherTechCount: number;
|
||||
assets: string[];
|
||||
otherAssets: string[];
|
||||
otherAssetsCount: number;
|
||||
newDatasets: number;
|
||||
cmsSetup: boolean;
|
||||
storageExpansion: number;
|
||||
name: string;
|
||||
email: string;
|
||||
role: string;
|
||||
message: string;
|
||||
sitemapFile: any; // Using any for File/null to be CLI-compatible
|
||||
contactFiles: any[]; // Using any[] for File[]
|
||||
// Design
|
||||
designVibe: string;
|
||||
colorScheme: string[];
|
||||
references: string[];
|
||||
designWishes: string;
|
||||
// Maintenance
|
||||
expectedAdjustments: string;
|
||||
languagesList: string[];
|
||||
// Timeline
|
||||
deadline: string;
|
||||
// Web App specific
|
||||
targetAudience: string;
|
||||
userRoles: string[];
|
||||
dataSensitivity: string;
|
||||
platformType: string;
|
||||
// Meta
|
||||
dontKnows: string[];
|
||||
visualStaging: string;
|
||||
complexInteractions: string;
|
||||
gridDontKnows?: Record<string, string>;
|
||||
briefingSummary?: string;
|
||||
companyAddress?: string;
|
||||
companyPhone?: string;
|
||||
personName?: string;
|
||||
taxId?: string;
|
||||
designVision?: string;
|
||||
positionDescriptions?: Record<string, string>;
|
||||
sitemap?: {
|
||||
category: string;
|
||||
pages: { title: string; desc: string }[];
|
||||
}[];
|
||||
projectType: ProjectType;
|
||||
// Company
|
||||
companyName: string;
|
||||
employeeCount: string;
|
||||
// Existing Presence
|
||||
existingWebsite: string;
|
||||
socialMedia: string[];
|
||||
socialMediaUrls: Record<string, string>;
|
||||
existingDomain: string;
|
||||
wishedDomain: string;
|
||||
// Project
|
||||
websiteTopic: string;
|
||||
selectedPages: string[];
|
||||
otherPages: string[];
|
||||
otherPagesCount: number;
|
||||
features: string[];
|
||||
otherFeatures: string[];
|
||||
otherFeaturesCount: number;
|
||||
functions: string[];
|
||||
otherFunctions: string[];
|
||||
otherFunctionsCount: number;
|
||||
apiSystems: string[];
|
||||
otherTech: string[];
|
||||
otherTechCount: number;
|
||||
assets: string[];
|
||||
otherAssets: string[];
|
||||
otherAssetsCount: number;
|
||||
newDatasets: number;
|
||||
cmsSetup: boolean;
|
||||
storageExpansion: number;
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
role: string;
|
||||
message: string;
|
||||
sitemapFile: any; // Using any for File/null to be CLI-compatible
|
||||
contactFiles: any[]; // Using any[] for File[]
|
||||
// Design
|
||||
designVibe: string;
|
||||
colorScheme: string[];
|
||||
references: string[];
|
||||
designWishes: string;
|
||||
// Maintenance
|
||||
expectedAdjustments: string;
|
||||
languagesList: string[];
|
||||
// Timeline
|
||||
deadline: string;
|
||||
// Web App specific
|
||||
targetAudience: string;
|
||||
userRoles: string[];
|
||||
dataSensitivity: string;
|
||||
platformType: string;
|
||||
// Meta
|
||||
dontKnows: string[];
|
||||
visualStaging: string;
|
||||
complexInteractions: string;
|
||||
gridDontKnows?: Record<string, string>;
|
||||
briefingSummary?: string;
|
||||
companyAddress?: string;
|
||||
companyPhone?: string;
|
||||
personName?: string;
|
||||
taxId?: string;
|
||||
designVision?: string;
|
||||
positionDescriptions?: Record<string, string>;
|
||||
sitemap?: {
|
||||
category: string;
|
||||
pages: { title: string; desc: string }[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
pos: number;
|
||||
title: string;
|
||||
desc: string;
|
||||
qty: number;
|
||||
price: number;
|
||||
isRecurring?: boolean;
|
||||
pos: number;
|
||||
title: string;
|
||||
desc: string;
|
||||
qty: number;
|
||||
price: number;
|
||||
isRecurring?: boolean;
|
||||
}
|
||||
export interface Totals {
|
||||
totalPrice: number;
|
||||
monthlyPrice: number;
|
||||
totalPagesCount: number;
|
||||
totalFeatures: number;
|
||||
totalFunctions: number;
|
||||
totalApis: number;
|
||||
languagesCount: number;
|
||||
totalPrice: number;
|
||||
monthlyPrice: number;
|
||||
totalPagesCount: number;
|
||||
totalFeatures: number;
|
||||
totalFunctions: number;
|
||||
totalApis: number;
|
||||
languagesCount: number;
|
||||
}
|
||||
|
||||
69
apps/web/src/middleware.ts
Normal file
69
apps/web/src/middleware.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
/**
|
||||
* PRODUCTION STABILIZATION MIDDLEWARE
|
||||
* This middleware handles legacy asset routing for the KLZ showcase,
|
||||
* ensuring assets are correctly mapped regardless of Next.js config wrapper behavior.
|
||||
*/
|
||||
export function middleware(request: NextRequest) {
|
||||
const { pathname } = request.nextUrl;
|
||||
|
||||
|
||||
// 2. Legacy Showcase Asset Mapping
|
||||
// Handles:
|
||||
// - Global assets (/wp-content/*, /wp-includes/*, /assets/*)
|
||||
// - Showcase-specific resources (/case-studies/klz-cables/index.html, etc.)
|
||||
|
||||
// Normalize pathname to ensure we're matching correctly
|
||||
const path = pathname;
|
||||
|
||||
// Map Pattern 2: Showcase-prefixed assets (used for relative links within the showcase)
|
||||
const showcaseMatch = path.match(/^\/(?:case-studies|work|blog)\/([^\/]+)\/(.*)/);
|
||||
if (showcaseMatch) {
|
||||
let [, slug, remainingPath] = showcaseMatch;
|
||||
|
||||
// Safety: Only rewrite if it looks like a static asset (has extension)
|
||||
// or belongs to known legacy folders. This prevents catching Next.js routes.
|
||||
if (remainingPath.match(/\.(?:html|php|js|css|png|jpg|jpeg|svg|gif|webp|woff2?|ttf|pdf|json|xml)$/) ||
|
||||
remainingPath.includes('wp-content/') ||
|
||||
remainingPath.includes('wp-includes/') ||
|
||||
remainingPath.includes('assets/')) {
|
||||
|
||||
// If the URL resolved to /case-studies/assets/..., 'assets' is captured as the slug
|
||||
if (slug === 'assets') {
|
||||
const nextSlash = remainingPath.indexOf('/');
|
||||
if (nextSlash !== -1) {
|
||||
const actualDir = remainingPath.substring(0, nextSlash);
|
||||
slug = actualDir === 'klz-cables.com' ? 'klz-cables' : actualDir; // Normalize back to expected slug
|
||||
remainingPath = remainingPath.substring(nextSlash + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize slug: klz-cables -> klz-cables.com (literal folder name)
|
||||
const directory = slug === 'klz-cables' ? 'klz-cables.com' : slug;
|
||||
|
||||
// If the remaining path already starts with assets/directory, don't double it
|
||||
const targetPath = remainingPath.startsWith(`assets/${directory}/`)
|
||||
? `/${remainingPath}`
|
||||
: `/assets/${directory}/${remainingPath}`;
|
||||
|
||||
console.log(`[Middleware] Rewriting to: /showcase/${directory}${targetPath}`);
|
||||
return NextResponse.rewrite(new URL(`/showcase/${directory}${targetPath}`, request.url));
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// Optimization: Only run middleware for legacy asset paths to minimize overhead
|
||||
export const config = {
|
||||
matcher: [
|
||||
'/wp-content/:path*',
|
||||
'/wp-includes/:path*',
|
||||
'/assets/:path*',
|
||||
'/case-studies/:path*',
|
||||
'/work/:path*',
|
||||
'/blog/:path*',
|
||||
],
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,392 +0,0 @@
|
||||
import { MigrateUpArgs, MigrateDownArgs, sql } from "@payloadcms/db-postgres";
|
||||
|
||||
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
CREATE TYPE "public"."enum_posts_status" AS ENUM('draft', 'published');
|
||||
CREATE TYPE "public"."enum__posts_v_version_status" AS ENUM('draft', 'published');
|
||||
CREATE TYPE "public"."enum_crm_accounts_status" AS ENUM('lead', 'client', 'lost');
|
||||
CREATE TYPE "public"."enum_crm_accounts_lead_temperature" AS ENUM('cold', 'warm', 'hot');
|
||||
CREATE TYPE "public"."enum_crm_interactions_type" AS ENUM('email', 'call', 'meeting', 'note');
|
||||
CREATE TYPE "public"."enum_crm_interactions_direction" AS ENUM('inbound', 'outbound');
|
||||
CREATE TABLE "users_sessions" (
|
||||
"_order" integer NOT NULL,
|
||||
"_parent_id" integer NOT NULL,
|
||||
"id" varchar PRIMARY KEY NOT NULL,
|
||||
"created_at" timestamp(3) with time zone,
|
||||
"expires_at" timestamp(3) with time zone NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "users" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"email" varchar NOT NULL,
|
||||
"reset_password_token" varchar,
|
||||
"reset_password_expiration" timestamp(3) with time zone,
|
||||
"salt" varchar,
|
||||
"hash" varchar,
|
||||
"login_attempts" numeric DEFAULT 0,
|
||||
"lock_until" timestamp(3) with time zone
|
||||
);
|
||||
|
||||
CREATE TABLE "media" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"alt" varchar NOT NULL,
|
||||
"prefix" varchar DEFAULT 'mintel-me/media',
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"url" varchar,
|
||||
"thumbnail_u_r_l" varchar,
|
||||
"filename" varchar,
|
||||
"mime_type" varchar,
|
||||
"filesize" numeric,
|
||||
"width" numeric,
|
||||
"height" numeric,
|
||||
"focal_x" numeric,
|
||||
"focal_y" numeric,
|
||||
"sizes_thumbnail_url" varchar,
|
||||
"sizes_thumbnail_width" numeric,
|
||||
"sizes_thumbnail_height" numeric,
|
||||
"sizes_thumbnail_mime_type" varchar,
|
||||
"sizes_thumbnail_filesize" numeric,
|
||||
"sizes_thumbnail_filename" varchar,
|
||||
"sizes_card_url" varchar,
|
||||
"sizes_card_width" numeric,
|
||||
"sizes_card_height" numeric,
|
||||
"sizes_card_mime_type" varchar,
|
||||
"sizes_card_filesize" numeric,
|
||||
"sizes_card_filename" varchar,
|
||||
"sizes_tablet_url" varchar,
|
||||
"sizes_tablet_width" numeric,
|
||||
"sizes_tablet_height" numeric,
|
||||
"sizes_tablet_mime_type" varchar,
|
||||
"sizes_tablet_filesize" numeric,
|
||||
"sizes_tablet_filename" varchar
|
||||
);
|
||||
|
||||
CREATE TABLE "posts_tags" (
|
||||
"_order" integer NOT NULL,
|
||||
"_parent_id" integer NOT NULL,
|
||||
"id" varchar PRIMARY KEY NOT NULL,
|
||||
"tag" varchar
|
||||
);
|
||||
|
||||
CREATE TABLE "posts" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"title" varchar,
|
||||
"slug" varchar,
|
||||
"description" varchar,
|
||||
"date" timestamp(3) with time zone,
|
||||
"featured_image_id" integer,
|
||||
"content" jsonb,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"_status" "enum_posts_status" DEFAULT 'draft'
|
||||
);
|
||||
|
||||
CREATE TABLE "_posts_v_version_tags" (
|
||||
"_order" integer NOT NULL,
|
||||
"_parent_id" integer NOT NULL,
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"tag" varchar,
|
||||
"_uuid" varchar
|
||||
);
|
||||
|
||||
CREATE TABLE "_posts_v" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"parent_id" integer,
|
||||
"version_title" varchar,
|
||||
"version_slug" varchar,
|
||||
"version_description" varchar,
|
||||
"version_date" timestamp(3) with time zone,
|
||||
"version_featured_image_id" integer,
|
||||
"version_content" jsonb,
|
||||
"version_updated_at" timestamp(3) with time zone,
|
||||
"version_created_at" timestamp(3) with time zone,
|
||||
"version__status" "enum__posts_v_version_status" DEFAULT 'draft',
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"latest" boolean
|
||||
);
|
||||
|
||||
CREATE TABLE "inquiries" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"name" varchar NOT NULL,
|
||||
"email" varchar NOT NULL,
|
||||
"company_name" varchar,
|
||||
"project_type" varchar,
|
||||
"message" varchar,
|
||||
"is_free_text" boolean DEFAULT false,
|
||||
"config" jsonb,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "redirects" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"from" varchar NOT NULL,
|
||||
"to" varchar NOT NULL,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "context_files" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"filename" varchar NOT NULL,
|
||||
"content" varchar NOT NULL,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "crm_accounts" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"name" varchar NOT NULL,
|
||||
"website" varchar,
|
||||
"status" "enum_crm_accounts_status" DEFAULT 'lead',
|
||||
"lead_temperature" "enum_crm_accounts_lead_temperature",
|
||||
"assigned_to_id" integer,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "crm_accounts_rels" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"order" integer,
|
||||
"parent_id" integer NOT NULL,
|
||||
"path" varchar NOT NULL,
|
||||
"media_id" integer
|
||||
);
|
||||
|
||||
CREATE TABLE "crm_contacts" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"first_name" varchar NOT NULL,
|
||||
"last_name" varchar NOT NULL,
|
||||
"email" varchar NOT NULL,
|
||||
"phone" varchar,
|
||||
"linked_in" varchar,
|
||||
"role" varchar,
|
||||
"account_id" integer,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "crm_interactions" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"type" "enum_crm_interactions_type" DEFAULT 'email' NOT NULL,
|
||||
"direction" "enum_crm_interactions_direction",
|
||||
"date" timestamp(3) with time zone NOT NULL,
|
||||
"contact_id" integer,
|
||||
"account_id" integer,
|
||||
"subject" varchar NOT NULL,
|
||||
"content" jsonb,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "payload_kv" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"key" varchar NOT NULL,
|
||||
"data" jsonb NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "payload_locked_documents" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"global_slug" varchar,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "payload_locked_documents_rels" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"order" integer,
|
||||
"parent_id" integer NOT NULL,
|
||||
"path" varchar NOT NULL,
|
||||
"users_id" integer,
|
||||
"media_id" integer,
|
||||
"posts_id" integer,
|
||||
"inquiries_id" integer,
|
||||
"redirects_id" integer,
|
||||
"context_files_id" integer,
|
||||
"crm_accounts_id" integer,
|
||||
"crm_contacts_id" integer,
|
||||
"crm_interactions_id" integer
|
||||
);
|
||||
|
||||
CREATE TABLE "payload_preferences" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"key" varchar,
|
||||
"value" jsonb,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "payload_preferences_rels" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"order" integer,
|
||||
"parent_id" integer NOT NULL,
|
||||
"path" varchar NOT NULL,
|
||||
"users_id" integer
|
||||
);
|
||||
|
||||
CREATE TABLE "payload_migrations" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"name" varchar,
|
||||
"batch" numeric,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "ai_settings_custom_sources" (
|
||||
"_order" integer NOT NULL,
|
||||
"_parent_id" integer NOT NULL,
|
||||
"id" varchar PRIMARY KEY NOT NULL,
|
||||
"source_name" varchar NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "ai_settings" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"updated_at" timestamp(3) with time zone,
|
||||
"created_at" timestamp(3) with time zone
|
||||
);
|
||||
|
||||
ALTER TABLE "users_sessions" ADD CONSTRAINT "users_sessions_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "posts_tags" ADD CONSTRAINT "posts_tags_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "posts" ADD CONSTRAINT "posts_featured_image_id_media_id_fk" FOREIGN KEY ("featured_image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
|
||||
ALTER TABLE "_posts_v_version_tags" ADD CONSTRAINT "_posts_v_version_tags_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_posts_v"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "_posts_v" ADD CONSTRAINT "_posts_v_parent_id_posts_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."posts"("id") ON DELETE set null ON UPDATE no action;
|
||||
ALTER TABLE "_posts_v" ADD CONSTRAINT "_posts_v_version_featured_image_id_media_id_fk" FOREIGN KEY ("version_featured_image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
|
||||
ALTER TABLE "crm_accounts" ADD CONSTRAINT "crm_accounts_assigned_to_id_users_id_fk" FOREIGN KEY ("assigned_to_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;
|
||||
ALTER TABLE "crm_accounts_rels" ADD CONSTRAINT "crm_accounts_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."crm_accounts"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "crm_accounts_rels" ADD CONSTRAINT "crm_accounts_rels_media_fk" FOREIGN KEY ("media_id") REFERENCES "public"."media"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "crm_contacts" ADD CONSTRAINT "crm_contacts_account_id_crm_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."crm_accounts"("id") ON DELETE set null ON UPDATE no action;
|
||||
ALTER TABLE "crm_interactions" ADD CONSTRAINT "crm_interactions_contact_id_crm_contacts_id_fk" FOREIGN KEY ("contact_id") REFERENCES "public"."crm_contacts"("id") ON DELETE set null ON UPDATE no action;
|
||||
ALTER TABLE "crm_interactions" ADD CONSTRAINT "crm_interactions_account_id_crm_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."crm_accounts"("id") ON DELETE set null ON UPDATE no action;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."payload_locked_documents"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_users_fk" FOREIGN KEY ("users_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_media_fk" FOREIGN KEY ("media_id") REFERENCES "public"."media"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_posts_fk" FOREIGN KEY ("posts_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_inquiries_fk" FOREIGN KEY ("inquiries_id") REFERENCES "public"."inquiries"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_redirects_fk" FOREIGN KEY ("redirects_id") REFERENCES "public"."redirects"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_context_files_fk" FOREIGN KEY ("context_files_id") REFERENCES "public"."context_files"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_crm_accounts_fk" FOREIGN KEY ("crm_accounts_id") REFERENCES "public"."crm_accounts"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_crm_contacts_fk" FOREIGN KEY ("crm_contacts_id") REFERENCES "public"."crm_contacts"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_crm_interactions_fk" FOREIGN KEY ("crm_interactions_id") REFERENCES "public"."crm_interactions"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."payload_preferences"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_users_fk" FOREIGN KEY ("users_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "ai_settings_custom_sources" ADD CONSTRAINT "ai_settings_custom_sources_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."ai_settings"("id") ON DELETE cascade ON UPDATE no action;
|
||||
CREATE INDEX "users_sessions_order_idx" ON "users_sessions" USING btree ("_order");
|
||||
CREATE INDEX "users_sessions_parent_id_idx" ON "users_sessions" USING btree ("_parent_id");
|
||||
CREATE INDEX "users_updated_at_idx" ON "users" USING btree ("updated_at");
|
||||
CREATE INDEX "users_created_at_idx" ON "users" USING btree ("created_at");
|
||||
CREATE UNIQUE INDEX "users_email_idx" ON "users" USING btree ("email");
|
||||
CREATE INDEX "media_updated_at_idx" ON "media" USING btree ("updated_at");
|
||||
CREATE INDEX "media_created_at_idx" ON "media" USING btree ("created_at");
|
||||
CREATE UNIQUE INDEX "media_filename_idx" ON "media" USING btree ("filename");
|
||||
CREATE INDEX "media_sizes_thumbnail_sizes_thumbnail_filename_idx" ON "media" USING btree ("sizes_thumbnail_filename");
|
||||
CREATE INDEX "media_sizes_card_sizes_card_filename_idx" ON "media" USING btree ("sizes_card_filename");
|
||||
CREATE INDEX "media_sizes_tablet_sizes_tablet_filename_idx" ON "media" USING btree ("sizes_tablet_filename");
|
||||
CREATE INDEX "posts_tags_order_idx" ON "posts_tags" USING btree ("_order");
|
||||
CREATE INDEX "posts_tags_parent_id_idx" ON "posts_tags" USING btree ("_parent_id");
|
||||
CREATE UNIQUE INDEX "posts_slug_idx" ON "posts" USING btree ("slug");
|
||||
CREATE INDEX "posts_featured_image_idx" ON "posts" USING btree ("featured_image_id");
|
||||
CREATE INDEX "posts_updated_at_idx" ON "posts" USING btree ("updated_at");
|
||||
CREATE INDEX "posts_created_at_idx" ON "posts" USING btree ("created_at");
|
||||
CREATE INDEX "posts__status_idx" ON "posts" USING btree ("_status");
|
||||
CREATE INDEX "_posts_v_version_tags_order_idx" ON "_posts_v_version_tags" USING btree ("_order");
|
||||
CREATE INDEX "_posts_v_version_tags_parent_id_idx" ON "_posts_v_version_tags" USING btree ("_parent_id");
|
||||
CREATE INDEX "_posts_v_parent_idx" ON "_posts_v" USING btree ("parent_id");
|
||||
CREATE INDEX "_posts_v_version_version_slug_idx" ON "_posts_v" USING btree ("version_slug");
|
||||
CREATE INDEX "_posts_v_version_version_featured_image_idx" ON "_posts_v" USING btree ("version_featured_image_id");
|
||||
CREATE INDEX "_posts_v_version_version_updated_at_idx" ON "_posts_v" USING btree ("version_updated_at");
|
||||
CREATE INDEX "_posts_v_version_version_created_at_idx" ON "_posts_v" USING btree ("version_created_at");
|
||||
CREATE INDEX "_posts_v_version_version__status_idx" ON "_posts_v" USING btree ("version__status");
|
||||
CREATE INDEX "_posts_v_created_at_idx" ON "_posts_v" USING btree ("created_at");
|
||||
CREATE INDEX "_posts_v_updated_at_idx" ON "_posts_v" USING btree ("updated_at");
|
||||
CREATE INDEX "_posts_v_latest_idx" ON "_posts_v" USING btree ("latest");
|
||||
CREATE INDEX "inquiries_updated_at_idx" ON "inquiries" USING btree ("updated_at");
|
||||
CREATE INDEX "inquiries_created_at_idx" ON "inquiries" USING btree ("created_at");
|
||||
CREATE UNIQUE INDEX "redirects_from_idx" ON "redirects" USING btree ("from");
|
||||
CREATE INDEX "redirects_updated_at_idx" ON "redirects" USING btree ("updated_at");
|
||||
CREATE INDEX "redirects_created_at_idx" ON "redirects" USING btree ("created_at");
|
||||
CREATE UNIQUE INDEX "context_files_filename_idx" ON "context_files" USING btree ("filename");
|
||||
CREATE INDEX "context_files_updated_at_idx" ON "context_files" USING btree ("updated_at");
|
||||
CREATE INDEX "context_files_created_at_idx" ON "context_files" USING btree ("created_at");
|
||||
CREATE INDEX "crm_accounts_assigned_to_idx" ON "crm_accounts" USING btree ("assigned_to_id");
|
||||
CREATE INDEX "crm_accounts_updated_at_idx" ON "crm_accounts" USING btree ("updated_at");
|
||||
CREATE INDEX "crm_accounts_created_at_idx" ON "crm_accounts" USING btree ("created_at");
|
||||
CREATE INDEX "crm_accounts_rels_order_idx" ON "crm_accounts_rels" USING btree ("order");
|
||||
CREATE INDEX "crm_accounts_rels_parent_idx" ON "crm_accounts_rels" USING btree ("parent_id");
|
||||
CREATE INDEX "crm_accounts_rels_path_idx" ON "crm_accounts_rels" USING btree ("path");
|
||||
CREATE INDEX "crm_accounts_rels_media_id_idx" ON "crm_accounts_rels" USING btree ("media_id");
|
||||
CREATE UNIQUE INDEX "crm_contacts_email_idx" ON "crm_contacts" USING btree ("email");
|
||||
CREATE INDEX "crm_contacts_account_idx" ON "crm_contacts" USING btree ("account_id");
|
||||
CREATE INDEX "crm_contacts_updated_at_idx" ON "crm_contacts" USING btree ("updated_at");
|
||||
CREATE INDEX "crm_contacts_created_at_idx" ON "crm_contacts" USING btree ("created_at");
|
||||
CREATE INDEX "crm_interactions_contact_idx" ON "crm_interactions" USING btree ("contact_id");
|
||||
CREATE INDEX "crm_interactions_account_idx" ON "crm_interactions" USING btree ("account_id");
|
||||
CREATE INDEX "crm_interactions_updated_at_idx" ON "crm_interactions" USING btree ("updated_at");
|
||||
CREATE INDEX "crm_interactions_created_at_idx" ON "crm_interactions" USING btree ("created_at");
|
||||
CREATE UNIQUE INDEX "payload_kv_key_idx" ON "payload_kv" USING btree ("key");
|
||||
CREATE INDEX "payload_locked_documents_global_slug_idx" ON "payload_locked_documents" USING btree ("global_slug");
|
||||
CREATE INDEX "payload_locked_documents_updated_at_idx" ON "payload_locked_documents" USING btree ("updated_at");
|
||||
CREATE INDEX "payload_locked_documents_created_at_idx" ON "payload_locked_documents" USING btree ("created_at");
|
||||
CREATE INDEX "payload_locked_documents_rels_order_idx" ON "payload_locked_documents_rels" USING btree ("order");
|
||||
CREATE INDEX "payload_locked_documents_rels_parent_idx" ON "payload_locked_documents_rels" USING btree ("parent_id");
|
||||
CREATE INDEX "payload_locked_documents_rels_path_idx" ON "payload_locked_documents_rels" USING btree ("path");
|
||||
CREATE INDEX "payload_locked_documents_rels_users_id_idx" ON "payload_locked_documents_rels" USING btree ("users_id");
|
||||
CREATE INDEX "payload_locked_documents_rels_media_id_idx" ON "payload_locked_documents_rels" USING btree ("media_id");
|
||||
CREATE INDEX "payload_locked_documents_rels_posts_id_idx" ON "payload_locked_documents_rels" USING btree ("posts_id");
|
||||
CREATE INDEX "payload_locked_documents_rels_inquiries_id_idx" ON "payload_locked_documents_rels" USING btree ("inquiries_id");
|
||||
CREATE INDEX "payload_locked_documents_rels_redirects_id_idx" ON "payload_locked_documents_rels" USING btree ("redirects_id");
|
||||
CREATE INDEX "payload_locked_documents_rels_context_files_id_idx" ON "payload_locked_documents_rels" USING btree ("context_files_id");
|
||||
CREATE INDEX "payload_locked_documents_rels_crm_accounts_id_idx" ON "payload_locked_documents_rels" USING btree ("crm_accounts_id");
|
||||
CREATE INDEX "payload_locked_documents_rels_crm_contacts_id_idx" ON "payload_locked_documents_rels" USING btree ("crm_contacts_id");
|
||||
CREATE INDEX "payload_locked_documents_rels_crm_interactions_id_idx" ON "payload_locked_documents_rels" USING btree ("crm_interactions_id");
|
||||
CREATE INDEX "payload_preferences_key_idx" ON "payload_preferences" USING btree ("key");
|
||||
CREATE INDEX "payload_preferences_updated_at_idx" ON "payload_preferences" USING btree ("updated_at");
|
||||
CREATE INDEX "payload_preferences_created_at_idx" ON "payload_preferences" USING btree ("created_at");
|
||||
CREATE INDEX "payload_preferences_rels_order_idx" ON "payload_preferences_rels" USING btree ("order");
|
||||
CREATE INDEX "payload_preferences_rels_parent_idx" ON "payload_preferences_rels" USING btree ("parent_id");
|
||||
CREATE INDEX "payload_preferences_rels_path_idx" ON "payload_preferences_rels" USING btree ("path");
|
||||
CREATE INDEX "payload_preferences_rels_users_id_idx" ON "payload_preferences_rels" USING btree ("users_id");
|
||||
CREATE INDEX "payload_migrations_updated_at_idx" ON "payload_migrations" USING btree ("updated_at");
|
||||
CREATE INDEX "payload_migrations_created_at_idx" ON "payload_migrations" USING btree ("created_at");
|
||||
CREATE INDEX "ai_settings_custom_sources_order_idx" ON "ai_settings_custom_sources" USING btree ("_order");
|
||||
CREATE INDEX "ai_settings_custom_sources_parent_id_idx" ON "ai_settings_custom_sources" USING btree ("_parent_id");`);
|
||||
}
|
||||
|
||||
export async function down({
|
||||
db,
|
||||
payload,
|
||||
req,
|
||||
}: MigrateDownArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
DROP TABLE "users_sessions" CASCADE;
|
||||
DROP TABLE "users" CASCADE;
|
||||
DROP TABLE "media" CASCADE;
|
||||
DROP TABLE "posts_tags" CASCADE;
|
||||
DROP TABLE "posts" CASCADE;
|
||||
DROP TABLE "_posts_v_version_tags" CASCADE;
|
||||
DROP TABLE "_posts_v" CASCADE;
|
||||
DROP TABLE "inquiries" CASCADE;
|
||||
DROP TABLE "redirects" CASCADE;
|
||||
DROP TABLE "context_files" CASCADE;
|
||||
DROP TABLE "crm_accounts" CASCADE;
|
||||
DROP TABLE "crm_accounts_rels" CASCADE;
|
||||
DROP TABLE "crm_contacts" CASCADE;
|
||||
DROP TABLE "crm_interactions" CASCADE;
|
||||
DROP TABLE "payload_kv" CASCADE;
|
||||
DROP TABLE "payload_locked_documents" CASCADE;
|
||||
DROP TABLE "payload_locked_documents_rels" CASCADE;
|
||||
DROP TABLE "payload_preferences" CASCADE;
|
||||
DROP TABLE "payload_preferences_rels" CASCADE;
|
||||
DROP TABLE "payload_migrations" CASCADE;
|
||||
DROP TABLE "ai_settings_custom_sources" CASCADE;
|
||||
DROP TABLE "ai_settings" CASCADE;
|
||||
DROP TYPE "public"."enum_posts_status";
|
||||
DROP TYPE "public"."enum__posts_v_version_status";
|
||||
DROP TYPE "public"."enum_crm_accounts_status";
|
||||
DROP TYPE "public"."enum_crm_accounts_lead_temperature";
|
||||
DROP TYPE "public"."enum_crm_interactions_type";
|
||||
DROP TYPE "public"."enum_crm_interactions_direction";`);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,155 +0,0 @@
|
||||
import { MigrateUpArgs, MigrateDownArgs, sql } from "@payloadcms/db-postgres";
|
||||
|
||||
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
CREATE TYPE "public"."enum_crm_topics_status" AS ENUM('active', 'paused', 'won', 'lost');
|
||||
CREATE TYPE "public"."enum_crm_topics_stage" AS ENUM('discovery', 'proposal', 'negotiation', 'implementation');
|
||||
CREATE TYPE "public"."enum_projects_milestones_status" AS ENUM('todo', 'in_progress', 'done');
|
||||
CREATE TYPE "public"."enum_projects_milestones_priority" AS ENUM('low', 'medium', 'high');
|
||||
CREATE TYPE "public"."enum_projects_status" AS ENUM('draft', 'in_progress', 'review', 'completed');
|
||||
ALTER TYPE "public"."enum_crm_accounts_status" ADD VALUE 'partner' BEFORE 'lost';
|
||||
ALTER TYPE "public"."enum_crm_interactions_type" ADD VALUE 'whatsapp' BEFORE 'note';
|
||||
ALTER TYPE "public"."enum_crm_interactions_type" ADD VALUE 'social' BEFORE 'note';
|
||||
ALTER TYPE "public"."enum_crm_interactions_type" ADD VALUE 'document' BEFORE 'note';
|
||||
CREATE TABLE "crm_topics" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"title" varchar NOT NULL,
|
||||
"account_id" integer NOT NULL,
|
||||
"status" "enum_crm_topics_status" DEFAULT 'active' NOT NULL,
|
||||
"stage" "enum_crm_topics_stage",
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "crm_interactions_rels" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"order" integer,
|
||||
"parent_id" integer NOT NULL,
|
||||
"path" varchar NOT NULL,
|
||||
"media_id" integer
|
||||
);
|
||||
|
||||
CREATE TABLE "projects_milestones" (
|
||||
"_order" integer NOT NULL,
|
||||
"_parent_id" integer NOT NULL,
|
||||
"id" varchar PRIMARY KEY NOT NULL,
|
||||
"name" varchar NOT NULL,
|
||||
"status" "enum_projects_milestones_status" DEFAULT 'todo' NOT NULL,
|
||||
"priority" "enum_projects_milestones_priority" DEFAULT 'medium',
|
||||
"start_date" timestamp(3) with time zone,
|
||||
"target_date" timestamp(3) with time zone,
|
||||
"assignee_id" integer
|
||||
);
|
||||
|
||||
CREATE TABLE "projects" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"title" varchar NOT NULL,
|
||||
"account_id" integer NOT NULL,
|
||||
"status" "enum_projects_status" DEFAULT 'draft' NOT NULL,
|
||||
"start_date" timestamp(3) with time zone,
|
||||
"target_date" timestamp(3) with time zone,
|
||||
"value_min" numeric,
|
||||
"value_max" numeric,
|
||||
"briefing" jsonb,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "projects_rels" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"order" integer,
|
||||
"parent_id" integer NOT NULL,
|
||||
"path" varchar NOT NULL,
|
||||
"crm_contacts_id" integer,
|
||||
"media_id" integer
|
||||
);
|
||||
|
||||
ALTER TABLE "crm_interactions" ALTER COLUMN "type" SET DEFAULT 'note';
|
||||
ALTER TABLE "inquiries" ADD COLUMN "processed" boolean DEFAULT false;
|
||||
ALTER TABLE "crm_contacts" ADD COLUMN "full_name" varchar;
|
||||
ALTER TABLE "crm_interactions" ADD COLUMN "topic_id" integer;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "crm_topics_id" integer;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "projects_id" integer;
|
||||
ALTER TABLE "crm_topics" ADD CONSTRAINT "crm_topics_account_id_crm_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."crm_accounts"("id") ON DELETE set null ON UPDATE no action;
|
||||
ALTER TABLE "crm_interactions_rels" ADD CONSTRAINT "crm_interactions_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."crm_interactions"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "crm_interactions_rels" ADD CONSTRAINT "crm_interactions_rels_media_fk" FOREIGN KEY ("media_id") REFERENCES "public"."media"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "projects_milestones" ADD CONSTRAINT "projects_milestones_assignee_id_users_id_fk" FOREIGN KEY ("assignee_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;
|
||||
ALTER TABLE "projects_milestones" ADD CONSTRAINT "projects_milestones_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "projects" ADD CONSTRAINT "projects_account_id_crm_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."crm_accounts"("id") ON DELETE set null ON UPDATE no action;
|
||||
ALTER TABLE "projects_rels" ADD CONSTRAINT "projects_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "projects_rels" ADD CONSTRAINT "projects_rels_crm_contacts_fk" FOREIGN KEY ("crm_contacts_id") REFERENCES "public"."crm_contacts"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "projects_rels" ADD CONSTRAINT "projects_rels_media_fk" FOREIGN KEY ("media_id") REFERENCES "public"."media"("id") ON DELETE cascade ON UPDATE no action;
|
||||
CREATE INDEX "crm_topics_account_idx" ON "crm_topics" USING btree ("account_id");
|
||||
CREATE INDEX "crm_topics_updated_at_idx" ON "crm_topics" USING btree ("updated_at");
|
||||
CREATE INDEX "crm_topics_created_at_idx" ON "crm_topics" USING btree ("created_at");
|
||||
CREATE INDEX "crm_interactions_rels_order_idx" ON "crm_interactions_rels" USING btree ("order");
|
||||
CREATE INDEX "crm_interactions_rels_parent_idx" ON "crm_interactions_rels" USING btree ("parent_id");
|
||||
CREATE INDEX "crm_interactions_rels_path_idx" ON "crm_interactions_rels" USING btree ("path");
|
||||
CREATE INDEX "crm_interactions_rels_media_id_idx" ON "crm_interactions_rels" USING btree ("media_id");
|
||||
CREATE INDEX "projects_milestones_order_idx" ON "projects_milestones" USING btree ("_order");
|
||||
CREATE INDEX "projects_milestones_parent_id_idx" ON "projects_milestones" USING btree ("_parent_id");
|
||||
CREATE INDEX "projects_milestones_assignee_idx" ON "projects_milestones" USING btree ("assignee_id");
|
||||
CREATE INDEX "projects_account_idx" ON "projects" USING btree ("account_id");
|
||||
CREATE INDEX "projects_updated_at_idx" ON "projects" USING btree ("updated_at");
|
||||
CREATE INDEX "projects_created_at_idx" ON "projects" USING btree ("created_at");
|
||||
CREATE INDEX "projects_rels_order_idx" ON "projects_rels" USING btree ("order");
|
||||
CREATE INDEX "projects_rels_parent_idx" ON "projects_rels" USING btree ("parent_id");
|
||||
CREATE INDEX "projects_rels_path_idx" ON "projects_rels" USING btree ("path");
|
||||
CREATE INDEX "projects_rels_crm_contacts_id_idx" ON "projects_rels" USING btree ("crm_contacts_id");
|
||||
CREATE INDEX "projects_rels_media_id_idx" ON "projects_rels" USING btree ("media_id");
|
||||
ALTER TABLE "crm_interactions" ADD CONSTRAINT "crm_interactions_topic_id_crm_topics_id_fk" FOREIGN KEY ("topic_id") REFERENCES "public"."crm_topics"("id") ON DELETE set null ON UPDATE no action;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_crm_topics_fk" FOREIGN KEY ("crm_topics_id") REFERENCES "public"."crm_topics"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_projects_fk" FOREIGN KEY ("projects_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;
|
||||
CREATE INDEX "crm_interactions_topic_idx" ON "crm_interactions" USING btree ("topic_id");
|
||||
CREATE INDEX "payload_locked_documents_rels_crm_topics_id_idx" ON "payload_locked_documents_rels" USING btree ("crm_topics_id");
|
||||
CREATE INDEX "payload_locked_documents_rels_projects_id_idx" ON "payload_locked_documents_rels" USING btree ("projects_id");`);
|
||||
}
|
||||
|
||||
export async function down({
|
||||
db,
|
||||
payload,
|
||||
req,
|
||||
}: MigrateDownArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "crm_topics" DISABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE "crm_interactions_rels" DISABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE "projects_milestones" DISABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE "projects" DISABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE "projects_rels" DISABLE ROW LEVEL SECURITY;
|
||||
DROP TABLE "crm_topics" CASCADE;
|
||||
DROP TABLE "crm_interactions_rels" CASCADE;
|
||||
DROP TABLE "projects_milestones" CASCADE;
|
||||
DROP TABLE "projects" CASCADE;
|
||||
DROP TABLE "projects_rels" CASCADE;
|
||||
ALTER TABLE "crm_interactions" DROP CONSTRAINT "crm_interactions_topic_id_crm_topics_id_fk";
|
||||
|
||||
ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_crm_topics_fk";
|
||||
|
||||
ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_projects_fk";
|
||||
|
||||
ALTER TABLE "crm_accounts" ALTER COLUMN "status" SET DATA TYPE text;
|
||||
ALTER TABLE "crm_accounts" ALTER COLUMN "status" SET DEFAULT 'lead'::text;
|
||||
DROP TYPE "public"."enum_crm_accounts_status";
|
||||
CREATE TYPE "public"."enum_crm_accounts_status" AS ENUM('lead', 'client', 'lost');
|
||||
ALTER TABLE "crm_accounts" ALTER COLUMN "status" SET DEFAULT 'lead'::"public"."enum_crm_accounts_status";
|
||||
ALTER TABLE "crm_accounts" ALTER COLUMN "status" SET DATA TYPE "public"."enum_crm_accounts_status" USING "status"::"public"."enum_crm_accounts_status";
|
||||
ALTER TABLE "crm_interactions" ALTER COLUMN "type" SET DATA TYPE text;
|
||||
ALTER TABLE "crm_interactions" ALTER COLUMN "type" SET DEFAULT 'email'::text;
|
||||
DROP TYPE "public"."enum_crm_interactions_type";
|
||||
CREATE TYPE "public"."enum_crm_interactions_type" AS ENUM('email', 'call', 'meeting', 'note');
|
||||
ALTER TABLE "crm_interactions" ALTER COLUMN "type" SET DEFAULT 'email'::"public"."enum_crm_interactions_type";
|
||||
ALTER TABLE "crm_interactions" ALTER COLUMN "type" SET DATA TYPE "public"."enum_crm_interactions_type" USING "type"::"public"."enum_crm_interactions_type";
|
||||
DROP INDEX "crm_interactions_topic_idx";
|
||||
DROP INDEX "payload_locked_documents_rels_crm_topics_id_idx";
|
||||
DROP INDEX "payload_locked_documents_rels_projects_id_idx";
|
||||
ALTER TABLE "inquiries" DROP COLUMN "processed";
|
||||
ALTER TABLE "crm_contacts" DROP COLUMN "full_name";
|
||||
ALTER TABLE "crm_interactions" DROP COLUMN "topic_id";
|
||||
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "crm_topics_id";
|
||||
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "projects_id";
|
||||
DROP TYPE "public"."enum_crm_topics_status";
|
||||
DROP TYPE "public"."enum_crm_topics_stage";
|
||||
DROP TYPE "public"."enum_projects_milestones_status";
|
||||
DROP TYPE "public"."enum_projects_milestones_priority";
|
||||
DROP TYPE "public"."enum_projects_status";`);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import * as migration_20260227_171023_crm_collections from "./20260227_171023_crm_collections";
|
||||
import * as migration_20260301_151838 from "./20260301_151838";
|
||||
|
||||
export const migrations = [
|
||||
{
|
||||
up: migration_20260227_171023_crm_collections.up,
|
||||
down: migration_20260227_171023_crm_collections.down,
|
||||
name: "20260227_171023_crm_collections",
|
||||
},
|
||||
{
|
||||
up: migration_20260301_151838.up,
|
||||
down: migration_20260301_151838.down,
|
||||
name: "20260301_151838",
|
||||
},
|
||||
];
|
||||
@@ -1,28 +0,0 @@
|
||||
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." },
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für quote ein.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "author",
|
||||
type: "text",
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für author ein.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "role",
|
||||
type: "text",
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für role ein.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,54 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für alt ein.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "caption",
|
||||
type: "text",
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für caption ein.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,97 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für quote ein.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "author",
|
||||
type: "text",
|
||||
required: true,
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für author ein.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "role",
|
||||
type: "text",
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für role ein.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "source",
|
||||
type: "text",
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für source ein.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sourceUrl",
|
||||
type: "text",
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/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." },
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,72 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "label",
|
||||
type: "text",
|
||||
required: true,
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für label ein.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "source",
|
||||
type: "text",
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für source ein.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sourceUrl",
|
||||
type: "text",
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für sourceUrl ein.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/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." },
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für caption ein.",
|
||||
},
|
||||
},
|
||||
],
|
||||
admin: { description: "Fügen Sie Elemente zur Liste slides hinzu." },
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,102 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "negativeLabel",
|
||||
type: "text",
|
||||
required: true,
|
||||
defaultValue: "Legacy",
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für negativeLabel ein.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "negativeText",
|
||||
type: "text",
|
||||
required: true,
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für negativeText ein.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "positiveLabel",
|
||||
type: "text",
|
||||
required: true,
|
||||
defaultValue: "Mintel Stack",
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für positiveLabel ein.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "positiveText",
|
||||
type: "text",
|
||||
required: true,
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/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?",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für definition ein.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
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." },
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für label ein.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
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." },
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/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).",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für title ein.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
type: "textarea",
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für description ein.",
|
||||
},
|
||||
},
|
||||
],
|
||||
admin: { description: "Fügen Sie Elemente zur Liste items hinzu." },
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/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." },
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,81 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
type: "text",
|
||||
required: true,
|
||||
admin: {
|
||||
description: "The value proposition text.",
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "buttonText",
|
||||
type: "text",
|
||||
required: true,
|
||||
defaultValue: "Jetzt anfragen",
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/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." },
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für text ein.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
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." },
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
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.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/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.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,68 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,68 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/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: [
|
||||
"@mintel/payload-ai/components/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.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den mehrzeiligen Text für text ein.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für title ein.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
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.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,115 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für title ein.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "subtitle",
|
||||
type: "text",
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/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: [
|
||||
"@mintel/payload-ai/components/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: [
|
||||
"@mintel/payload-ai/components/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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für description ein.",
|
||||
},
|
||||
},
|
||||
],
|
||||
admin: { description: "Fügen Sie Elemente zur Liste datasets hinzu." },
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
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." },
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für title ein.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/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." },
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für label ein.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "value",
|
||||
type: "text",
|
||||
required: true,
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für value ein.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "subtext",
|
||||
type: "text",
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für subtext ein.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,56 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für label ein.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "value",
|
||||
type: "text",
|
||||
required: true,
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
description: "Geben Sie den Text für value ein.",
|
||||
},
|
||||
},
|
||||
],
|
||||
admin: { description: "Fügen Sie Elemente zur Liste stats hinzu." },
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/AiFieldButton#AiFieldButton",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/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." },
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
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." },
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,73 +0,0 @@
|
||||
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: [
|
||||
"@mintel/payload-ai/components/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: [
|
||||
"@mintel/payload-ai/components/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." },
|
||||
},
|
||||
],
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user