Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 037794c4b7 | |||
| 06147dade4 | |||
| 08e16ce754 | |||
| df37f23270 | |||
| 503676dab5 | |||
| 52480b4f55 | |||
| 77d87e7be9 | |||
| 5fb73d57dd | |||
| 41ef9092d6 | |||
| afeb53e7cf | |||
| 8dd5c077c5 | |||
| dbae239031 | |||
| bbcf859d12 | |||
| 81c6e02faf | |||
| ed98c40079 | |||
| 0633326c1e | |||
| cad27d6a02 | |||
| f4aad36ea6 | |||
| 88edf08993 | |||
| e0cdac971f | |||
| cc728410c2 | |||
| 59e1ae7926 | |||
| a36a7fe19d | |||
| 13cbc0291f | |||
| 1315985ba2 | |||
| 04309e3c02 | |||
| 8e2a6a0378 | |||
| 5a48dcd75f | |||
| 73cb55ee8d | |||
| 5ffd78d60f | |||
| b0b8902f30 | |||
| 0dcb64ddca | |||
| 6bd4914586 | |||
| 4de69aa7ed | |||
| e217d9c7f7 | |||
| f97b37d3c8 | |||
| d4705d616b | |||
| d336980f27 | |||
| f663497277 | |||
| 3dee7c73f1 | |||
| ac4677ca50 | |||
| ce492c221c | |||
| fbd405c270 | |||
| acfee13df7 | |||
| a5d43df53a | |||
| adda98eb00 | |||
| 3523547eca | |||
| 2ddef98924 | |||
| 272d3eccbf | |||
| 24475adef3 | |||
| 473079181f | |||
| 67e86b0fa3 | |||
| 1c441b2d99 | |||
| e7446a5444 | |||
| d50daa8db9 | |||
| b69986572e | |||
| 18e02e3a33 | |||
| f726b3e978 | |||
| 832be8c6bf | |||
| 2b934dd70e | |||
| 33e84fb538 | |||
| 52f4f30336 | |||
| e8b8a4073e | |||
| 5821a5dee4 | |||
| dfc7536268 |
@@ -22,12 +22,16 @@ jobs:
|
||||
version: 10
|
||||
- name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
|
||||
echo "//${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc
|
||||
REGISTRY="${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}"
|
||||
echo "@mintel:registry=https://$REGISTRY" > .npmrc
|
||||
echo "//$REGISTRY/:_authToken=${{ secrets.NPM_TOKEN || secrets.GITEA_PAT || secrets.MINTEL_PRIVATE_TOKEN || secrets.REGISTRY_PASS }}" >> .npmrc
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: 🧪 Parallel Checks
|
||||
run: |
|
||||
pnpm lint &
|
||||
pnpm build &
|
||||
wait
|
||||
rm -rf .next .turbo node_modules || true
|
||||
pnpm install --frozen-lockfile
|
||||
- name: 🧪 QA Checks
|
||||
env:
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
run: |
|
||||
pnpm lint
|
||||
pnpm build
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Infrastructure Maintenance: Production stabilization and email adapter fix triggered on 2026-04-12.
|
||||
# Infrastructure Maintenance: Production stabilization and runner cleanup triggered on 2026-04-23.
|
||||
name: 🚀 Build & Deploy
|
||||
|
||||
on:
|
||||
@@ -93,6 +93,15 @@ jobs:
|
||||
echo "short_sha=$SHORT_SHA"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "--- DEBUG ---"
|
||||
echo "TARGET: $TARGET"
|
||||
echo "IMAGE_TAG: $IMAGE_TAG"
|
||||
echo "ENV_FILE: $ENV_FILE"
|
||||
echo "PRIMARY_HOST: $PRIMARY_HOST"
|
||||
echo "TRAEFIK_RULE: $TRAEFIK_RULE"
|
||||
echo "PROJECT_NAME: $PRJ-$TARGET"
|
||||
echo "--- END DEBUG ---"
|
||||
|
||||
# ⏳ Wait for Upstream Packages/Images if Tagged
|
||||
if [[ "${{ github.ref_type }}" == "tag" ]]; then
|
||||
echo "🔎 Checking for @mintel dependencies in package.json..."
|
||||
@@ -140,6 +149,12 @@ jobs:
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: 🧹 Clean Runner Infrastructure
|
||||
run: |
|
||||
docker builder prune -f --filter "until=24h"
|
||||
docker image prune -f --filter "until=24h"
|
||||
docker system prune -f --volumes --filter "until=24h" || true
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
@@ -156,16 +171,21 @@ jobs:
|
||||
echo "//${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
rm -rf .next .turbo node_modules || true
|
||||
pnpm store prune
|
||||
pnpm install --no-frozen-lockfile
|
||||
- name: 🧪 QA Checks
|
||||
if: github.event.inputs.skip_checks != 'true'
|
||||
env:
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
run: |
|
||||
pnpm lint
|
||||
pnpm exec tsc --noEmit
|
||||
pnpm test run
|
||||
- name: 🏗️ Build Test
|
||||
if: github.event.inputs.skip_checks != 'true'
|
||||
env:
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
run: pnpm build
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
@@ -185,6 +205,11 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: 🐳 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: 🧹 Pre-Build Cache Cleanup
|
||||
run: |
|
||||
docker builder prune -a -f --filter "until=12h"
|
||||
docker image prune -a -f --filter "until=12h"
|
||||
docker system prune -f --volumes || true
|
||||
- name: 🔐 Registry Login
|
||||
run: echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||
- name: 🏗️ Build and Push
|
||||
@@ -219,14 +244,7 @@ jobs:
|
||||
IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }}
|
||||
PROJECT_NAME: ${{ needs.prepare.outputs.project_name }}
|
||||
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||
|
||||
# Secrets mapping (Database & CMS)
|
||||
PAYLOAD_SECRET: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_PAYLOAD_SECRET) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_PAYLOAD_SECRET) || secrets.PAYLOAD_SECRET || secrets.MINTEL_PRIVATE_TOKEN || secrets.DIRECTUS_SECRET || vars.PAYLOAD_SECRET || 'you-need-to-set-a-payload-secret' }}
|
||||
DATABASE_URI: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DATABASE_URI) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DATABASE_URI) || secrets.DATABASE_URI || vars.DATABASE_URI }}
|
||||
POSTGRES_DB: ${{ secrets.POSTGRES_DB || vars.POSTGRES_DB || 'payload' }}
|
||||
POSTGRES_USER: ${{ secrets.POSTGRES_USER || vars.POSTGRES_USER || 'directus' }}
|
||||
POSTGRES_PASSWORD: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_POSTGRES_PASSWORD) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_POSTGRES_PASSWORD) || secrets.POSTGRES_PASSWORD || vars.POSTGRES_PASSWORD || 'directus' }}
|
||||
|
||||
MINTEL_PRIVATE_TOKEN: ${{ secrets.MINTEL_PRIVATE_TOKEN }}
|
||||
|
||||
# Secrets mapping (Mail)
|
||||
MAIL_HOST: ${{ secrets.SMTP_HOST || vars.SMTP_HOST }}
|
||||
@@ -284,13 +302,6 @@ jobs:
|
||||
PROJECT_COLOR=$PROJECT_COLOR
|
||||
LOG_LEVEL=$LOG_LEVEL
|
||||
|
||||
# Database & Payload
|
||||
DATABASE_URI=\${DATABASE_URI:-postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@mb-grid-db:5432/$POSTGRES_DB}
|
||||
PAYLOAD_SECRET=${PAYLOAD_SECRET:-you-need-to-set-a-payload-secret}
|
||||
POSTGRES_DB=$POSTGRES_DB
|
||||
POSTGRES_USER=$POSTGRES_USER
|
||||
POSTGRES_PASSWORD=$POSTGRES_PASSWORD
|
||||
|
||||
# Mail
|
||||
MAIL_HOST=$MAIL_HOST
|
||||
MAIL_PORT=$MAIL_PORT
|
||||
@@ -339,28 +350,6 @@ jobs:
|
||||
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' pull"
|
||||
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' up -d --remove-orphans"
|
||||
|
||||
# Apply Payload Migrations using the target app container's programmatic endpoint
|
||||
ssh root@alpha.mintel.me "cd $SITE_DIR && echo '→ Waiting for DB and Running Payload Migrations...' && \
|
||||
for i in {1..15}; do \
|
||||
echo \"Attempt \$i...\"; \
|
||||
docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' exec -T mb-grid-app sh -c 'curl -s -i -X POST -H \"Authorization: Bearer \$PAYLOAD_SECRET\" http://localhost:3000/api/payload/migrate' > /tmp/migrate_res 2>&1; \
|
||||
if grep -q \"200 OK\" /tmp/migrate_res; then \
|
||||
echo \"✅ Migrations successful!\"; \
|
||||
cat /tmp/migrate_res; \
|
||||
break; \
|
||||
else \
|
||||
echo \"❌ Attempt \$i failed. Response:\"; \
|
||||
cat /tmp/migrate_res; \
|
||||
if [ \$i -eq 15 ]; then \
|
||||
echo \"❌ Migration failed after 15 attempts! Dumping app logs...\"; \
|
||||
docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' logs --tail 100 mb-grid-app; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
echo \"⏳ Retrying in 5s...\"; \
|
||||
sleep 5; \
|
||||
fi; \
|
||||
done"
|
||||
|
||||
ssh root@alpha.mintel.me "docker system prune -f --filter 'until=24h'"
|
||||
|
||||
- name: 🧹 Post-Deploy Cleanup (Runner)
|
||||
@@ -397,41 +386,6 @@ jobs:
|
||||
run: |
|
||||
pnpm store prune
|
||||
pnpm install --no-frozen-lockfile
|
||||
- name: 📦 Cache APT Packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /var/cache/apt/archives
|
||||
key: apt-cache-${{ runner.os }}-${{ runner.arch }}-chromium
|
||||
|
||||
- name: 💾 Cache Chromium
|
||||
id: cache-chromium
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /usr/bin/chromium
|
||||
key: ${{ runner.os }}-chromium-native-${{ hashFiles('package.json') }}
|
||||
|
||||
- name: 🔍 Install Chromium (Native & ARM64)
|
||||
if: steps.cache-chromium.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
rm -f /etc/apt/apt.conf.d/docker-clean
|
||||
apt-get update
|
||||
apt-get install -y gnupg wget ca-certificates
|
||||
OS_ID=$(. /etc/os-release && echo $ID)
|
||||
CODENAME=$(. /etc/os-release && echo $VERSION_CODENAME)
|
||||
if [ "$OS_ID" = "debian" ]; then
|
||||
apt-get install -y chromium
|
||||
else
|
||||
mkdir -p /etc/apt/keyrings
|
||||
KEY_ID="82BB6851C64F6880"
|
||||
wget -qO- "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_ID" | gpg --dearmor > /etc/apt/keyrings/xtradeb.gpg
|
||||
echo "deb [signed-by=/etc/apt/keyrings/xtradeb.gpg] http://ppa.launchpad.net/xtradeb/apps/ubuntu $CODENAME main" > /etc/apt/sources.list.d/xtradeb-ppa.list
|
||||
printf "Package: *\nPin: release o=LP-PPA-xtradeb-apps\nPin-Priority: 1001\n" > /etc/apt/preferences.d/xtradeb
|
||||
apt-get update
|
||||
apt-get install -y --allow-downgrades chromium
|
||||
fi
|
||||
[ -f /usr/bin/chromium ] && ln -sf /usr/bin/chromium /usr/bin/google-chrome
|
||||
[ -f /usr/bin/chromium ] && ln -sf /usr/bin/chromium /usr/bin/chromium-browser
|
||||
|
||||
# ── Critical Smoke Tests (MUST pass) ──────────────────────────────────
|
||||
- name: 🏥 CMS Deep Health Check
|
||||
env:
|
||||
@@ -453,14 +407,6 @@ jobs:
|
||||
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN || vars.SENTRY_DSN }}
|
||||
|
||||
- name: 📝 E2E Form Submission Test
|
||||
if: always() && steps.deps.outcome == 'success'
|
||||
env:
|
||||
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || vars.GATEKEEPER_PASSWORD }}
|
||||
PUPPETEER_EXECUTABLE_PATH: /usr/bin/chromium
|
||||
run: pnpm test run
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# JOB 6: Notifications
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1,17 +1,106 @@
|
||||
name: Nightly QA
|
||||
name: Nightly QA (Inlined)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [fix/nightly-qa-stabilization]
|
||||
schedule:
|
||||
- cron: '0 4 * * *'
|
||||
- cron: '0 2 * * *' # Every night at 02:00
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
call-qa-workflow:
|
||||
uses: mmintel/at-mintel/.gitea/workflows/quality-assurance-template.yml@main
|
||||
with:
|
||||
TARGET_URL: 'https://testing.mb-grid-solutions.com'
|
||||
PROJECT_NAME: 'mb-grid-solutions'
|
||||
secrets:
|
||||
GOTIFY_URL: ${{ secrets.GOTIFY_URL }}
|
||||
GOTIFY_TOKEN: ${{ secrets.GOTIFY_TOKEN }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || vars.GATEKEEPER_PASSWORD }}
|
||||
qa:
|
||||
name: 🧪 Quality Assurance
|
||||
runs-on: docker
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
env:
|
||||
NEXT_PUBLIC_BASE_URL: https://testing.mb-grid-solutions.com
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
||||
steps:
|
||||
- name: 🛑 Cleanup Workspace
|
||||
run: |
|
||||
rm -rf .next .turbo node_modules || true
|
||||
|
||||
- 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: 🔐 Registry Auth
|
||||
run: |
|
||||
REGISTRY="${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}"
|
||||
echo "@mintel:registry=https://$REGISTRY" > .npmrc
|
||||
echo "//$REGISTRY/:_authToken=${{ secrets.NPM_TOKEN || secrets.GITEA_PAT || secrets.MINTEL_PRIVATE_TOKEN || secrets.REGISTRY_PASS }}" >> .npmrc
|
||||
|
||||
- name: 🏗️ Install Dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: 🛠️ Patch @mintel/cli
|
||||
run: |
|
||||
CLI_PATH=$(find node_modules -name index.js | grep "@mintel/cli/dist/index.js" | head -n 1)
|
||||
if [ -n "$CLI_PATH" ] && [ -f "$CLI_PATH" ]; then
|
||||
echo "Patching CLI at $CLI_PATH"
|
||||
sed -i '2{/^#!\/usr\/bin\/env node/d;}' "$CLI_PATH"
|
||||
fi
|
||||
|
||||
- name: 🔍 Static Analysis
|
||||
id: static
|
||||
run: |
|
||||
echo "Running Lint..."
|
||||
pnpm run lint
|
||||
echo "Running Depcheck..."
|
||||
pnpm dlx depcheck . --ignores="framer-motion,lucide-react,next-intl,vitest,@testing-library/jest-dom,@testing-library/react,@vitejs/plugin-react,autoprefixer,axios,cheerio,eslint-config-next,happy-dom,html-validate,husky,jsdom,lint-staged,pa11y-ci,pino-pretty,postcss,prettier,puppeteer,tailwindcss,tsx,typescript,@mintel/eslint-config,@mintel/husky-config,@mintel/tsconfig,@tailwindcss/postcss,@types/node,@types/nodemailer,@types/react,@types/react-dom,@payloadcms/ui,@commitlint/*"
|
||||
|
||||
- name: ♿ Accessibility Audit
|
||||
id: a11y
|
||||
continue-on-error: true
|
||||
run: pnpm run check:wcag
|
||||
|
||||
- name: 🔗 Link Checker
|
||||
id: links
|
||||
continue-on-error: true
|
||||
run: |
|
||||
curl -sLO https://github.com/lycheeverse/lychee/releases/download/v0.14.3/lychee-v0.14.3-x86_64-unknown-linux-gnu.tar.gz
|
||||
tar -xzf lychee-v0.14.3-x86_64-unknown-linux-gnu.tar.gz
|
||||
mv lychee /usr/local/bin/
|
||||
lychee --accept 200,429,999 --exclude-mail --exclude "https://www.linkedin.com/.*" --exclude "https://fonts.gstatic.com/.*" https://testing.mb-grid-solutions.com
|
||||
|
||||
- name: 🎭 Performance & Lighthouse
|
||||
id: perf
|
||||
run: pnpm exec mintel pagespeed test -- --collect.settings.preset=desktop
|
||||
|
||||
- name: 🔔 Notify Status
|
||||
if: always()
|
||||
run: |
|
||||
STATUS="success"
|
||||
if [[ "${{ steps.static.outcome }}" != "success" || "${{ steps.perf.outcome }}" != "success" ]]; then
|
||||
STATUS="failure"
|
||||
fi
|
||||
|
||||
PAYLOAD=$(cat <<EOF
|
||||
{
|
||||
"status": "$STATUS",
|
||||
"project": "mb-grid-solutions.com",
|
||||
"workflow": "Nightly QA",
|
||||
"details": {
|
||||
"Static": "${{ steps.static.outcome }}",
|
||||
"A11y": "${{ steps.a11y.outcome }}",
|
||||
"Links": "${{ steps.links.outcome }}",
|
||||
"Perf": "${{ steps.perf.outcome }}"
|
||||
},
|
||||
"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
curl -X POST -H "Content-Type: application/json" \
|
||||
-d "$PAYLOAD" \
|
||||
${{ secrets.WEBHOOK_URL }} || echo "Notification failed"
|
||||
|
||||
6
.htmlvalidate-tmp/_.html
Normal file
6
.htmlvalidate-tmp/_.html
Normal file
File diff suppressed because one or more lines are too long
1
.htmlvalidate-tmp/_agb.html
Normal file
1
.htmlvalidate-tmp/_agb.html
Normal file
File diff suppressed because one or more lines are too long
1
.htmlvalidate-tmp/_datenschutz.html
Normal file
1
.htmlvalidate-tmp/_datenschutz.html
Normal file
File diff suppressed because one or more lines are too long
2
.htmlvalidate-tmp/_impressum.html
Normal file
2
.htmlvalidate-tmp/_impressum.html
Normal file
File diff suppressed because one or more lines are too long
3
.htmlvalidate-tmp/_kontakt.html
Normal file
3
.htmlvalidate-tmp/_kontakt.html
Normal file
File diff suppressed because one or more lines are too long
5
.htmlvalidate-tmp/_ueber_uns.html
Normal file
5
.htmlvalidate-tmp/_ueber_uns.html
Normal file
File diff suppressed because one or more lines are too long
@@ -21,6 +21,7 @@ ENV UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
|
||||
ENV NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID
|
||||
ENV SKIP_RUNTIME_ENV_VALIDATION=true
|
||||
ENV CI=true
|
||||
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
||||
|
||||
# Enable pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@10.3.0 --activate
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import configPromise from "@payload-config";
|
||||
import { RootPage } from "@payloadcms/next/views";
|
||||
import { importMap } from "../importMap";
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
segments: string[];
|
||||
}>;
|
||||
searchParams: Promise<{
|
||||
[key: string]: string | string[];
|
||||
}>;
|
||||
};
|
||||
|
||||
const Page = ({ params, searchParams }: Args) =>
|
||||
RootPage({ config: configPromise, importMap, params, searchParams });
|
||||
|
||||
export default Page;
|
||||
@@ -1,78 +0,0 @@
|
||||
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from "@payloadcms/richtext-lexical/rsc";
|
||||
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from "@payloadcms/richtext-lexical/rsc";
|
||||
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from "@payloadcms/richtext-lexical/rsc";
|
||||
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
|
||||
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from "@payloadcms/next/rsc";
|
||||
|
||||
export const importMap = {
|
||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell":
|
||||
RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField":
|
||||
RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent":
|
||||
LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/client#BlocksFeatureClient":
|
||||
BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient":
|
||||
InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient":
|
||||
HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UploadFeatureClient":
|
||||
UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BlockquoteFeatureClient":
|
||||
BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#RelationshipFeatureClient":
|
||||
RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#LinkFeatureClient":
|
||||
LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ChecklistFeatureClient":
|
||||
ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#OrderedListFeatureClient":
|
||||
OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UnorderedListFeatureClient":
|
||||
UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#IndentFeatureClient":
|
||||
IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#AlignFeatureClient":
|
||||
AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#HeadingFeatureClient":
|
||||
HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient":
|
||||
ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#InlineCodeFeatureClient":
|
||||
InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#SuperscriptFeatureClient":
|
||||
SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#SubscriptFeatureClient":
|
||||
SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient":
|
||||
StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient":
|
||||
UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BoldFeatureClient":
|
||||
BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ItalicFeatureClient":
|
||||
ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/next/rsc#CollectionCards":
|
||||
CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1,
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export const importMap = {};
|
||||
@@ -1,14 +0,0 @@
|
||||
import config from "@payload-config";
|
||||
import {
|
||||
REST_GET,
|
||||
REST_OPTIONS,
|
||||
REST_PATCH,
|
||||
REST_POST,
|
||||
REST_DELETE,
|
||||
} from "@payloadcms/next/routes";
|
||||
|
||||
export const GET = REST_GET(config);
|
||||
export const POST = REST_POST(config);
|
||||
export const DELETE = REST_DELETE(config);
|
||||
export const PATCH = REST_PATCH(config);
|
||||
export const OPTIONS = REST_OPTIONS(config);
|
||||
@@ -1 +0,0 @@
|
||||
/* Custom SCSS for Payload Admin Panel */
|
||||
@@ -1,36 +0,0 @@
|
||||
import configPromise from "@payload-config";
|
||||
import { RootLayout } from "@payloadcms/next/layouts";
|
||||
import React from "react";
|
||||
|
||||
import "@payloadcms/next/css";
|
||||
import "./custom.scss";
|
||||
import { handleServerFunctions } from "@payloadcms/next/layouts";
|
||||
import { importMap } from "./admin/importMap";
|
||||
|
||||
type Args = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const serverFunction: any = async function (args: any) {
|
||||
"use server";
|
||||
return handleServerFunctions({
|
||||
...args,
|
||||
config: configPromise,
|
||||
importMap,
|
||||
});
|
||||
};
|
||||
|
||||
const Layout = ({ children }: Args) => {
|
||||
return (
|
||||
<RootLayout
|
||||
config={configPromise}
|
||||
importMap={importMap}
|
||||
serverFunction={serverFunction}
|
||||
>
|
||||
{children}
|
||||
</RootLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
@@ -1,107 +1,37 @@
|
||||
import React from "react";
|
||||
import { Download } from "lucide-react";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { notFound } from "next/navigation";
|
||||
import { LegalLayout } from "@/components/LegalLayout";
|
||||
|
||||
export default function AVB() {
|
||||
const filePath = path.join(process.cwd(), "context/avbs.md");
|
||||
const fileContent = fs.readFileSync(filePath, "utf8");
|
||||
interface PageProps {
|
||||
params: Promise<{ locale: string }>;
|
||||
}
|
||||
|
||||
// Split by double newlines to get major blocks
|
||||
const rawBlocks = fileContent
|
||||
.split(/\n\s*\n/)
|
||||
.map((b) => b.trim())
|
||||
.filter((b) => b !== "");
|
||||
export default async function Page({ params }: PageProps) {
|
||||
const { locale } = await params;
|
||||
|
||||
// Extract title and stand more robustly
|
||||
const title = rawBlocks.find(b => b.startsWith("# "))?.replace(/^#\s+/, "") || "Allgemeine Verkaufsbedingungen";
|
||||
const stand = rawBlocks.find(b => b.toLowerCase().startsWith("stand:")) || "Stand: April 2026";
|
||||
let Content: React.ComponentType;
|
||||
try {
|
||||
const mdx = await import(`@/content/${locale}/avb.mdx`);
|
||||
Content = mdx.default;
|
||||
} catch {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const sections: { title: string; content: string[] }[] = [];
|
||||
let currentSection: { title: string; content: string[] } | null = null;
|
||||
|
||||
// Process sections, skipping title and stand blocks
|
||||
rawBlocks.forEach((block) => {
|
||||
if (block.startsWith("# ") || block.toLowerCase().startsWith("stand:")) return;
|
||||
|
||||
if (block.startsWith("##")) {
|
||||
// New section header: e.g. "## 1. Geltungsbereich"
|
||||
if (currentSection) sections.push(currentSection);
|
||||
currentSection = {
|
||||
title: block.replace(/^##\s+/, "").trim(),
|
||||
content: []
|
||||
};
|
||||
} else if (currentSection) {
|
||||
// Clean up bold markers for better display
|
||||
const cleanedBlock = block.replace(/\*\*(.*?)\*\*/g, "$1");
|
||||
currentSection.content.push(cleanedBlock);
|
||||
}
|
||||
});
|
||||
if (currentSection) sections.push(currentSection);
|
||||
|
||||
// The very last block might be a footer/schlussbestimmung if it's not in a section
|
||||
// In our MD, everything is in a section, so we just use the last block of the last section as potential footer if we want,
|
||||
// but the current UI expects a separate 'footer' variable.
|
||||
const footer = "MB Grid Solutions & Services";
|
||||
const downloadButton = (
|
||||
<a
|
||||
href="/assets/avb-mb-grid-4-2026.pdf"
|
||||
download
|
||||
className="btn-primary !py-3 !px-6 flex items-center gap-2"
|
||||
>
|
||||
<Download size={18} />
|
||||
Als PDF herunterladen
|
||||
</a>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-slate-50 min-h-screen pt-40 pb-20">
|
||||
<div className="container-custom">
|
||||
<div className="max-w-4xl mx-auto bg-white p-8 md:p-12 rounded-[2.5rem] shadow-sm border border-slate-100">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8">
|
||||
<div>
|
||||
<h1 className="text-4xl font-extrabold text-primary mb-2">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="text-slate-500 font-medium">{stand}</p>
|
||||
</div>
|
||||
<a
|
||||
href="/assets/AVB MB Grid 4-2026.pdf"
|
||||
download
|
||||
className="btn-primary !py-3 !px-6 flex items-center gap-2"
|
||||
>
|
||||
<Download size={18} />
|
||||
Als PDF herunterladen
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8 text-slate-600 leading-relaxed">
|
||||
{sections.map((section, index) => (
|
||||
<div key={index}>
|
||||
<h2 className="text-2xl font-bold text-primary mb-4">
|
||||
{section.title}
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
{section.content.map((paragraph, pIndex) => (
|
||||
<p key={pIndex}>
|
||||
{paragraph.split(/(\[.*?\]\(.*?\))/g).map((part, i) => {
|
||||
const match = part.match(/\[(.*?)\]\((.*?)\)/);
|
||||
if (match) {
|
||||
return (
|
||||
<a
|
||||
key={i}
|
||||
href={match[2]}
|
||||
className="text-primary hover:underline font-medium"
|
||||
download={match[2].endsWith(".pdf")}
|
||||
>
|
||||
{match[1]}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return part;
|
||||
})}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="pt-8 border-t border-slate-100 flex justify-between items-center text-slate-400 text-sm italic">
|
||||
<p>{footer}</p>
|
||||
<p>{stand}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<LegalLayout title="AVB" heroAction={downloadButton}>
|
||||
<Content />
|
||||
</LegalLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,66 +1,25 @@
|
||||
export default function Privacy() {
|
||||
import React from "react";
|
||||
import { notFound } from "next/navigation";
|
||||
import { LegalLayout } from "@/components/LegalLayout";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ locale: string }>;
|
||||
}
|
||||
|
||||
export default async function Page({ params }: PageProps) {
|
||||
const { locale } = await params;
|
||||
|
||||
let Content: React.ComponentType;
|
||||
try {
|
||||
const mdx = await import(`@/content/${locale}/datenschutz.mdx`);
|
||||
Content = mdx.default;
|
||||
} catch {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-slate-50 min-h-screen pt-40 pb-20">
|
||||
<div className="container-custom">
|
||||
<div className="max-w-4xl mx-auto bg-white p-8 md:p-12 rounded-[2.5rem] shadow-sm border border-slate-100">
|
||||
<h1 className="text-4xl font-extrabold text-primary mb-8">
|
||||
Datenschutzerklärung
|
||||
</h1>
|
||||
|
||||
<div className="space-y-8 text-slate-600 leading-relaxed">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-primary mb-4">
|
||||
1. Datenschutz auf einen Blick
|
||||
</h2>
|
||||
<p>
|
||||
Wir nehmen den Schutz Ihrer persönlichen Daten sehr ernst. Wir
|
||||
behandeln Ihre personenbezogenen Daten vertraulich und
|
||||
entsprechend der gesetzlichen Datenschutzvorschriften sowie
|
||||
dieser Datenschutzerklärung.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-primary mb-4">
|
||||
2. Hosting
|
||||
</h2>
|
||||
<p>
|
||||
Unsere Website wird bei Hetzner Online GmbH gehostet. Der
|
||||
Serverstandort ist Deutschland. Wir haben einen Vertrag über
|
||||
Auftragsverarbeitung (AVV) mit Hetzner geschlossen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-primary mb-4">
|
||||
3. Kontaktformular
|
||||
</h2>
|
||||
<p>
|
||||
Wenn Sie uns per Kontaktformular Anfragen zukommen lassen,
|
||||
werden Ihre Angaben aus dem Anfrageformular inklusive der von
|
||||
Ihnen dort angegebenen Kontaktdaten zwecks Bearbeitung der
|
||||
Anfrage und für den Fall von Anschlussfragen bei uns
|
||||
gespeichert. Diese Daten geben wir nicht ohne Ihre Einwilligung
|
||||
weiter.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-primary mb-4">
|
||||
4. Server-Log-Dateien
|
||||
</h2>
|
||||
<p>
|
||||
Der Provider der Seiten erhebt und speichert automatisch
|
||||
Informationen in sogenannten Server-Log-Dateien, die Ihr Browser
|
||||
automatisch an uns übermittelt. Dies sind: Browsertyp und
|
||||
Browserversion, verwendetes Betriebssystem, Referrer URL,
|
||||
Hostname des zugreifenden Rechners, Uhrzeit der Serveranfrage,
|
||||
IP-Adresse.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<LegalLayout title="Datenschutz">
|
||||
<Content />
|
||||
</LegalLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,101 +1,25 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { notFound } from "next/navigation";
|
||||
import { LegalLayout } from "@/components/LegalLayout";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { TechBackground } from "@/components/TechBackground";
|
||||
interface PageProps {
|
||||
params: Promise<{ locale: string }>;
|
||||
}
|
||||
|
||||
export default async function Page({ params }: PageProps) {
|
||||
const { locale } = await params;
|
||||
|
||||
let Content: React.ComponentType;
|
||||
try {
|
||||
const mdx = await import(`@/content/${locale}/impressum.mdx`);
|
||||
Content = mdx.default;
|
||||
} catch {
|
||||
notFound();
|
||||
}
|
||||
|
||||
export default function Legal() {
|
||||
return (
|
||||
<div className="bg-slate-50 min-h-screen pt-40 pb-20 relative overflow-hidden">
|
||||
<TechBackground />
|
||||
<div className="container-custom relative z-10">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ type: "spring", stiffness: 50, damping: 20 }}
|
||||
className="max-w-4xl mx-auto bg-white p-8 md:p-12 rounded-[2.5rem] shadow-sm border border-slate-100 relative overflow-hidden group"
|
||||
>
|
||||
<div className="tech-corner top-8 left-8 border-t-2 border-l-2 opacity-20" />
|
||||
<div className="tech-corner bottom-8 right-8 border-b-2 border-r-2 opacity-20" />
|
||||
|
||||
<h1 className="text-4xl font-extrabold text-primary mb-8 relative z-10">
|
||||
Impressum
|
||||
</h1>
|
||||
|
||||
<div className="space-y-8 text-slate-600 leading-relaxed relative z-10">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-primary mb-4">
|
||||
Angaben gemäß § 5 TMG
|
||||
</h2>
|
||||
<p>
|
||||
MB Grid Solutions & Services GmbH
|
||||
<br />
|
||||
Raiffeisenstraße 22
|
||||
<br />
|
||||
73630 Remshalden
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-primary mb-4">
|
||||
Vertreten durch
|
||||
</h2>
|
||||
<p>
|
||||
Michael Bodemer
|
||||
<br />
|
||||
Klaus Mintel
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-primary mb-4">Kontakt</h2>
|
||||
<p>
|
||||
E-Mail:{" "}
|
||||
<a
|
||||
href="mailto:info@mb-grid-solutions.com"
|
||||
className="text-accent hover:underline"
|
||||
>
|
||||
info@mb-grid-solutions.com
|
||||
</a>
|
||||
<br />
|
||||
Web:{" "}
|
||||
<a
|
||||
href="https://www.mb-grid-solutions.com"
|
||||
className="text-accent hover:underline"
|
||||
>
|
||||
www.mb-grid-solutions.com
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-primary mb-4">
|
||||
Registereintrag
|
||||
</h2>
|
||||
<p>
|
||||
Eintragung im Handelsregister.
|
||||
<br />
|
||||
Registergericht: Amtsgericht Stuttgart
|
||||
<br />
|
||||
Registernummer: HRB 803379
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-primary mb-4">
|
||||
Urheberrecht
|
||||
</h2>
|
||||
<p>
|
||||
Alle auf der Website veröffentlichten Texte, Bilder und
|
||||
sonstigen Informationen unterliegen – sofern nicht anders
|
||||
gekennzeichnet – dem Urheberrecht. Jede Vervielfältigung,
|
||||
Verbreitung, Speicherung, Übermittlung, Wiedergabe bzw.
|
||||
Weitergabe der Inhalte ohne schriftliche Genehmigung ist
|
||||
ausdrücklich untersagt.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
<LegalLayout title="Impressum">
|
||||
<Content />
|
||||
</LegalLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import "../globals.css";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getMessages } from "next-intl/server";
|
||||
import { notFound } from "next/navigation";
|
||||
import { LazyMotion, domAnimation } from "framer-motion";
|
||||
import AnalyticsProvider from "@/components/analytics/AnalyticsProvider";
|
||||
import { config } from "@/lib/config";
|
||||
|
||||
@@ -143,9 +142,7 @@ export default async function RootLayout({
|
||||
<body className="antialiased">
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<AnalyticsProvider websiteId={config.analytics.umami.websiteId} />
|
||||
<LazyMotion features={domAnimation}>
|
||||
<Layout>{children}</Layout>
|
||||
</LazyMotion>
|
||||
<Layout>{children}</Layout>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from "react";
|
||||
import { Metadata } from "next";
|
||||
|
||||
import HomeContent from "@/components/HomeContent";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -7,6 +9,24 @@ export const metadata: Metadata = {
|
||||
"Ihr spezialisierter Partner für herstellerneutrale technische Beratung und Projektbegleitung bei Energiekabelprojekten bis 110 kV.",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return <HomeContent />;
|
||||
interface PageProps {
|
||||
params: Promise<{ locale: string }>;
|
||||
}
|
||||
|
||||
export default async function Page({ params }: PageProps) {
|
||||
const { locale } = await params;
|
||||
|
||||
// Dynamically import the MDX content based on locale
|
||||
let Content: React.ComponentType;
|
||||
try {
|
||||
const mdx = await import(`@/content/${locale}/index.mdx`);
|
||||
Content = mdx.default;
|
||||
} catch {
|
||||
// Fallback if MDX is not yet created for this locale
|
||||
return <HomeContent />;
|
||||
}
|
||||
|
||||
// Render the MDX content.
|
||||
// We can use mdx-components.tsx to map MDX tags to our high-fidelity sections.
|
||||
return <Content />;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { Metadata } from "next";
|
||||
import AboutContent from "@/components/AboutContent";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Über uns",
|
||||
@@ -7,6 +8,21 @@ export const metadata: Metadata = {
|
||||
"Erfahren Sie mehr über MB Grid Solutions, unsere Expertise und unser Manifest für technische Exzellenz.",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return <AboutContent />;
|
||||
interface PageProps {
|
||||
params: Promise<{ locale: string }>;
|
||||
}
|
||||
|
||||
export default async function Page({ params }: PageProps) {
|
||||
const { locale } = await params;
|
||||
|
||||
// Dynamically import the MDX content based on locale
|
||||
let Content: React.ComponentType;
|
||||
try {
|
||||
const mdx = await import(`@/content/${locale}/about.mdx`);
|
||||
Content = mdx.default;
|
||||
} catch {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return <Content />;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getPayload } from "payload";
|
||||
import configPromise from "@payload-config";
|
||||
import { getServerAppServices } from "@/lib/services/create-services.server";
|
||||
import {
|
||||
render,
|
||||
@@ -50,34 +48,6 @@ export async function POST(req: Request) {
|
||||
return NextResponse.json({ error: "message_too_long" }, { status: 400 });
|
||||
}
|
||||
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
|
||||
// 1. Payload save
|
||||
let payloadSaved = false;
|
||||
try {
|
||||
await payload.create({
|
||||
collection: "form-submissions",
|
||||
data: {
|
||||
name,
|
||||
email,
|
||||
company: company || "Nicht angegeben",
|
||||
message,
|
||||
},
|
||||
});
|
||||
logger.info("Contact submission saved to PayloadCMS");
|
||||
payloadSaved = true;
|
||||
} catch (payloadError) {
|
||||
const errorMessage =
|
||||
payloadError instanceof Error
|
||||
? payloadError.message
|
||||
: String(payloadError);
|
||||
logger.error("Failed to save to Payload", {
|
||||
error: errorMessage,
|
||||
details: payloadError,
|
||||
});
|
||||
services.errors.captureException(payloadError, { phase: "payload_save" });
|
||||
}
|
||||
|
||||
// 2. Email sending via standalone Nodemailer (bypassing Payload adapter)
|
||||
try {
|
||||
const { config } = await import("@/lib/config");
|
||||
@@ -87,7 +57,7 @@ export async function POST(req: Request) {
|
||||
const recipients = Array.isArray(config.mail.recipients)
|
||||
? config.mail.recipients.filter(Boolean)
|
||||
: [];
|
||||
|
||||
|
||||
const to =
|
||||
recipients.length > 0
|
||||
? recipients.join(",")
|
||||
@@ -103,7 +73,10 @@ export async function POST(req: Request) {
|
||||
},
|
||||
});
|
||||
|
||||
logger.info("Preparing to send notification email", { to, host: config.mail.host });
|
||||
logger.info("Preparing to send notification email", {
|
||||
to,
|
||||
host: config.mail.host,
|
||||
});
|
||||
|
||||
// 2a. Notification to MB Grid
|
||||
const notificationHtml = await render(
|
||||
@@ -123,9 +96,13 @@ export async function POST(req: Request) {
|
||||
subject: `Kontaktanfrage von ${name}`,
|
||||
html: notificationHtml,
|
||||
});
|
||||
logger.info("Notification email sent successfully", { messageId: info?.messageId });
|
||||
logger.info("Notification email sent successfully", {
|
||||
messageId: info?.messageId,
|
||||
});
|
||||
} catch (notifyError) {
|
||||
logger.error("Failed to send notification email", { error: notifyError });
|
||||
logger.error("Failed to send notification email", {
|
||||
error: notifyError,
|
||||
});
|
||||
throw notifyError; // Re-throw to be caught by the outer SMTP catch
|
||||
}
|
||||
|
||||
@@ -145,11 +122,18 @@ export async function POST(req: Request) {
|
||||
subject: `Ihre Kontaktanfrage bei ${clientName}`,
|
||||
html: confirmationHtml,
|
||||
});
|
||||
logger.info("Confirmation email sent successfully", { messageId: info?.messageId });
|
||||
logger.info("Confirmation email sent successfully", {
|
||||
messageId: info?.messageId,
|
||||
});
|
||||
} catch (confirmError) {
|
||||
logger.warn(
|
||||
"Failed to send confirmation email, but notification was sent",
|
||||
{ error: confirmError instanceof Error ? confirmError.message : String(confirmError) },
|
||||
{
|
||||
error:
|
||||
confirmError instanceof Error
|
||||
? confirmError.message
|
||||
: String(confirmError),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -165,18 +149,10 @@ export async function POST(req: Request) {
|
||||
logger.error("SMTP Error", { error: smtpError });
|
||||
services.errors.captureException(smtpError, { phase: "smtp_send" });
|
||||
|
||||
if (!payloadSaved) {
|
||||
return NextResponse.json(
|
||||
{ error: "Systemfehler (Speicherung und Versand fehlgeschlagen)" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
await services.notifications.notify({
|
||||
title: "🚨 SMTP Fehler (Kontaktformular)",
|
||||
message: `Anfrage von ${name} (${email}) in Payload gespeichert, aber E-Mail-Versand fehlgeschlagen: ${smtpError instanceof Error ? smtpError.message : String(smtpError)}`,
|
||||
priority: 8,
|
||||
});
|
||||
return NextResponse.json(
|
||||
{ error: "Systemfehler (E-Mail-Versand fehlgeschlagen)" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
// Track success
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getPayload } from "payload";
|
||||
import configPromise from "@payload-config";
|
||||
import { getServerAppServices } from "@/lib/services/create-services.server";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const authHeader = req.headers.get("authorization");
|
||||
if (authHeader !== `Bearer ${process.env.PAYLOAD_SECRET}`) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { logger } = getServerAppServices();
|
||||
|
||||
try {
|
||||
logger.info("Starting programmatic Payload migrations...");
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
|
||||
await payload.db.migrate();
|
||||
|
||||
logger.info("Successfully executed Payload migrations.");
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "Migrations executed successfully.",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Failed to run migrations remotely", { error });
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
error instanceof Error ? error.message : "Unknown error occurred",
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,10 @@ import {
|
||||
ShieldCheck,
|
||||
Truck,
|
||||
} from "lucide-react";
|
||||
import { Reveal } from "./Reveal";
|
||||
import { TechBackground } from "./TechBackground";
|
||||
import { Counter } from "./Counter";
|
||||
import { Button } from "./Button";
|
||||
import { Reveal } from "@/components/Reveal";
|
||||
import { TechBackground } from "@/components/TechBackground";
|
||||
import { Counter } from "@/components/Counter";
|
||||
import { Button } from "@/components/Button";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function About() {
|
||||
@@ -102,14 +102,15 @@ export default function About() {
|
||||
<Reveal key={i} delay={i * 0.1}>
|
||||
<div className="card-modern !p-6 hover:-translate-y-1 transition-[box-shadow,transform] duration-300 relative overflow-hidden tech-card-border">
|
||||
<div className="flex justify-between items-start mb-4 relative z-10">
|
||||
<h3 className="text-xl font-bold text-primary">
|
||||
<h2 className="text-xl font-bold text-primary">
|
||||
{person.name}
|
||||
</h3>
|
||||
</h2>
|
||||
<a
|
||||
href={person.linkedin}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#0077b5] hover:scale-110 transition-transform"
|
||||
aria-label={`LinkedIn Profile of ${person.name}`}
|
||||
>
|
||||
<Linkedin size={20} />
|
||||
</a>
|
||||
@@ -154,9 +155,9 @@ export default function About() {
|
||||
<div className="text-accent mb-6">
|
||||
<Icon size={32} />
|
||||
</div>
|
||||
<h4 className="text-xl font-bold text-white mb-4">
|
||||
<h3 className="text-xl font-bold text-white mb-4">
|
||||
{i + 1}. {item.title}
|
||||
</h4>
|
||||
</h3>
|
||||
<p className="text-slate-400 leading-relaxed">
|
||||
{item.desc}
|
||||
</p>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { m } from "framer-motion";
|
||||
import { motion } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
|
||||
@@ -62,7 +62,7 @@ export const Button = ({
|
||||
);
|
||||
|
||||
const spotlight = (
|
||||
<m.div
|
||||
<motion.div
|
||||
className="absolute inset-0 z-0 pointer-events-none transition-opacity duration-500"
|
||||
style={{
|
||||
opacity: isHovered ? 1 : 0,
|
||||
|
||||
@@ -4,11 +4,11 @@ import React, { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Mail, MapPin, CheckCircle } from "lucide-react";
|
||||
import { Button } from "./Button";
|
||||
import { Counter } from "./Counter";
|
||||
import { Reveal } from "./Reveal";
|
||||
import { TechBackground } from "./TechBackground";
|
||||
import { StatusModal } from "./StatusModal";
|
||||
import { Button } from "@/components/Button";
|
||||
import { Counter } from "@/components/Counter";
|
||||
import { Reveal } from "@/components/Reveal";
|
||||
import { TechBackground } from "@/components/TechBackground";
|
||||
import { StatusModal } from "@/components/StatusModal";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function Contact() {
|
||||
@@ -125,9 +125,9 @@ export default function Contact() {
|
||||
<Mail size={24} />
|
||||
</div>
|
||||
<div className="relative z-10">
|
||||
<h4 className="text-slate-400 font-bold text-xs uppercase tracking-widest mb-1 md:mb-2">
|
||||
<h2 className="text-slate-400 font-bold text-xs uppercase tracking-widest mb-1 md:mb-2">
|
||||
{t("info.email")}
|
||||
</h4>
|
||||
</h2>
|
||||
<a
|
||||
href="mailto:info@mb-grid-solutions.com"
|
||||
className="text-white text-lg md:text-xl font-bold hover:text-accent transition-colors break-all"
|
||||
@@ -144,9 +144,9 @@ export default function Contact() {
|
||||
<MapPin size={24} />
|
||||
</div>
|
||||
<div className="relative z-10">
|
||||
<h4 className="text-slate-400 font-bold text-xs uppercase tracking-widest mb-1 md:mb-2">
|
||||
<h2 className="text-slate-400 font-bold text-xs uppercase tracking-widest mb-1 md:mb-2">
|
||||
{t("info.address")}
|
||||
</h4>
|
||||
</h2>
|
||||
<p className="text-white text-lg md:text-xl font-bold leading-relaxed">
|
||||
{t("info.company")}
|
||||
<br />
|
||||
@@ -162,12 +162,12 @@ export default function Contact() {
|
||||
<div className="w-full h-[300px] rounded-[2.5rem] overflow-hidden border border-white/10 shadow-sm grayscale hover:grayscale-0 transition-all duration-700 relative group">
|
||||
<div className="absolute inset-0 border-2 border-accent/0 group-hover:border-accent/20 transition-all duration-500 z-10 pointer-events-none rounded-[2.5rem]" />
|
||||
<iframe
|
||||
width="100%"
|
||||
height="100%"
|
||||
frameBorder="0"
|
||||
scrolling="no"
|
||||
marginHeight={0}
|
||||
marginWidth={0}
|
||||
className="w-full h-full border-0"
|
||||
title={
|
||||
t.has("info.mapTitle")
|
||||
? t("info.mapTitle")
|
||||
: "Location Map"
|
||||
}
|
||||
src="https://www.openstreetmap.org/export/embed.html?bbox=9.445,48.815,9.465,48.825&layer=mapnik&marker=48.8198,9.4552"
|
||||
></iframe>
|
||||
</div>
|
||||
@@ -184,9 +184,9 @@ export default function Contact() {
|
||||
<div className="w-20 h-20 rounded-full bg-accent/10 text-accent flex items-center justify-center mx-auto mb-8">
|
||||
<CheckCircle size={40} />
|
||||
</div>
|
||||
<h3 className="text-3xl font-bold text-primary mb-4">
|
||||
<h2 className="text-3xl font-bold text-primary mb-4">
|
||||
{t("form.successTitle")}
|
||||
</h3>
|
||||
</h2>
|
||||
<p className="text-slate-600 text-lg mb-10">
|
||||
{t("form.successMessage")}
|
||||
</p>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { Button } from "./Button";
|
||||
import { Counter } from "./Counter";
|
||||
import { Reveal } from "./Reveal";
|
||||
import { TechBackground } from "./TechBackground";
|
||||
import { Button } from "@/components/Button";
|
||||
import { Counter } from "@/components/Counter";
|
||||
import { Reveal } from "@/components/Reveal";
|
||||
import { TechBackground } from "@/components/TechBackground";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { AnimatePresence, m } from "framer-motion";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { ArrowUp, Home, Info, Menu, X } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button } from "./Button";
|
||||
import { Reveal } from "./Reveal";
|
||||
import { Button } from "@/components/Button";
|
||||
import { Reveal } from "@/components/Reveal";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
@@ -118,7 +118,7 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
{/* Mobile Menu Overlay */}
|
||||
<AnimatePresence>
|
||||
{isMobileMenuOpen && (
|
||||
<m.div
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
@@ -143,7 +143,7 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
{t("nav.cta")}
|
||||
</Button>
|
||||
</nav>
|
||||
</m.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
@@ -166,12 +166,12 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
<div className="absolute inset-0 grid-pattern opacity-[0.08] pointer-events-none" />
|
||||
|
||||
{/* Animated Tech Lines */}
|
||||
<m.div
|
||||
<motion.div
|
||||
animate={{ x: ["-100%", "100%"] }}
|
||||
transition={{ duration: 15, repeat: Infinity, ease: "linear" }}
|
||||
className="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-accent/30 to-transparent"
|
||||
/>
|
||||
<m.div
|
||||
<motion.div
|
||||
animate={{ x: ["100%", "-100%"] }}
|
||||
transition={{ duration: 20, repeat: Infinity, ease: "linear" }}
|
||||
className="absolute bottom-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-accent/20 to-transparent"
|
||||
@@ -184,7 +184,11 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
<div className="container-custom relative z-10">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-10 md:gap-12 mb-12 md:mb-16">
|
||||
<div className="lg:col-span-2">
|
||||
<Link href="/" className="inline-block mb-6 md:mb-8 group">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-block mb-6 md:mb-8 group"
|
||||
aria-label={t("nav.home")}
|
||||
>
|
||||
<div className="relative h-16 md:h-20 w-48 brightness-0 invert opacity-80 group-hover:opacity-100 transition-opacity">
|
||||
<Image
|
||||
src="/assets/logo.png"
|
||||
@@ -203,9 +207,9 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-white font-bold mb-6">
|
||||
<h2 className="text-white font-bold mb-6 text-base">
|
||||
{t("footer.navigation")}
|
||||
</h4>
|
||||
</h2>
|
||||
<nav className="flex flex-col gap-4">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
@@ -226,9 +230,9 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-white font-bold mb-6">
|
||||
<h2 className="text-white font-bold mb-6 text-base">
|
||||
{t("footer.legal")}
|
||||
</h4>
|
||||
</h2>
|
||||
<nav className="flex flex-col gap-4">
|
||||
<Link
|
||||
href="/impressum"
|
||||
|
||||
39
components/LegalLayout.tsx
Normal file
39
components/LegalLayout.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { TechBackground } from "@/components/TechBackground";
|
||||
|
||||
interface LegalLayoutProps {
|
||||
title: string;
|
||||
heroAction?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function LegalLayout({ title, heroAction, children }: LegalLayoutProps) {
|
||||
return (
|
||||
<div className="bg-slate-50 min-h-screen pt-40 pb-20 relative overflow-hidden">
|
||||
<TechBackground />
|
||||
<div className="container-custom relative z-10">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ type: "spring", stiffness: 50, damping: 20 }}
|
||||
className="max-w-4xl mx-auto bg-white p-8 md:p-12 rounded-[2.5rem] shadow-sm border border-slate-100 relative overflow-hidden group"
|
||||
>
|
||||
<div className="tech-corner top-8 left-8 border-t-2 border-l-2 opacity-20" />
|
||||
<div className="tech-corner bottom-8 right-8 border-b-2 border-r-2 opacity-20" />
|
||||
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8 relative z-10">
|
||||
<h1 className="text-4xl font-extrabold text-primary">{title}</h1>
|
||||
{heroAction}
|
||||
</div>
|
||||
|
||||
<div className="space-y-8 text-slate-600 leading-relaxed relative z-10 prose prose-slate max-w-none prose-h1:hidden prose-h2:text-xl prose-h2:font-bold prose-h2:text-primary prose-h2:mb-4">
|
||||
{children}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { m } from "framer-motion";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
interface RevealProps {
|
||||
children: React.ReactNode;
|
||||
@@ -30,7 +30,7 @@ export const Reveal = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<m.div
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
...directions[direction],
|
||||
@@ -68,7 +68,7 @@ export const Reveal = ({
|
||||
className={`${fullWidth ? "w-full" : ""} ${className} motion-fix will-change-[transform,opacity]`}
|
||||
>
|
||||
{children}
|
||||
</m.div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -84,7 +84,7 @@ export const Stagger = ({
|
||||
staggerDelay = 0.1,
|
||||
}: StaggerProps) => {
|
||||
return (
|
||||
<m.div
|
||||
<motion.div
|
||||
initial="initial"
|
||||
whileInView="animate"
|
||||
viewport={{ once: true, margin: "-50px" }}
|
||||
@@ -98,6 +98,6 @@ export const Stagger = ({
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</m.div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { m, AnimatePresence, LazyMotion, domAnimation } from "framer-motion";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { CheckCircle, AlertCircle, X } from "lucide-react";
|
||||
import { Button } from "./Button";
|
||||
import { Button } from "@/components/Button";
|
||||
|
||||
interface StatusModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -23,82 +23,80 @@ export const StatusModal = ({
|
||||
buttonText,
|
||||
}: StatusModalProps) => {
|
||||
return (
|
||||
<LazyMotion features={domAnimation}>
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 md:p-6">
|
||||
{/* Backdrop */}
|
||||
<m.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 md:p-6">
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={onClose}
|
||||
className="absolute inset-0 bg-slate-950/80 backdrop-blur-sm"
|
||||
/>
|
||||
|
||||
{/* Modal Content */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||
transition={{ type: "spring", damping: 25, stiffness: 300 }}
|
||||
className="relative w-full max-w-lg bg-white rounded-[2.5rem] border border-slate-100 shadow-2xl overflow-hidden group"
|
||||
>
|
||||
{/* Tech Decoration */}
|
||||
<div className="absolute top-0 left-0 w-full h-2 bg-slate-100 overflow-hidden">
|
||||
<motion.div
|
||||
initial={{ x: "-100%" }}
|
||||
animate={{ x: "100%" }}
|
||||
transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
|
||||
className={`absolute inset-0 w-1/2 ${type === "success" ? "bg-accent" : "bg-red-500"} opacity-30`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute inset-0 bg-slate-950/80 backdrop-blur-sm"
|
||||
/>
|
||||
|
||||
{/* Modal Content */}
|
||||
<m.div
|
||||
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||
transition={{ type: "spring", damping: 25, stiffness: 300 }}
|
||||
className="relative w-full max-w-lg bg-white rounded-[2.5rem] border border-slate-100 shadow-2xl overflow-hidden group"
|
||||
className="absolute top-6 right-6 p-2 text-slate-400 hover:text-primary transition-colors hover:bg-slate-50 rounded-xl"
|
||||
>
|
||||
{/* Tech Decoration */}
|
||||
<div className="absolute top-0 left-0 w-full h-2 bg-slate-100 overflow-hidden">
|
||||
<m.div
|
||||
initial={{ x: "-100%" }}
|
||||
animate={{ x: "100%" }}
|
||||
transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
|
||||
className={`absolute inset-0 w-1/2 ${type === "success" ? "bg-accent" : "bg-red-500"} opacity-30`}
|
||||
/>
|
||||
</div>
|
||||
<X size={20} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-6 right-6 p-2 text-slate-400 hover:text-primary transition-colors hover:bg-slate-50 rounded-xl"
|
||||
<div className="p-8 md:p-12 text-center">
|
||||
<div
|
||||
className={`w-20 h-20 rounded-full ${type === "success" ? "bg-accent/10 text-accent" : "bg-red-50 text-red-500"} flex items-center justify-center mx-auto mb-8 relative`}
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
|
||||
<div className="p-8 md:p-12 text-center">
|
||||
<div
|
||||
className={`w-20 h-20 rounded-full ${type === "success" ? "bg-accent/10 text-accent" : "bg-red-50 text-red-500"} flex items-center justify-center mx-auto mb-8 relative`}
|
||||
>
|
||||
<div
|
||||
className={`absolute inset-0 ${type === "success" ? "bg-accent/20" : "bg-red-500/20"} rounded-full animate-ping opacity-20`}
|
||||
/>
|
||||
{type === "success" ? (
|
||||
<CheckCircle size={40} />
|
||||
) : (
|
||||
<AlertCircle size={40} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h3 className="text-3xl font-extrabold text-primary mb-4 leading-tight">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="text-slate-600 text-lg mb-10 leading-relaxed">
|
||||
{message}
|
||||
</p>
|
||||
|
||||
<Button
|
||||
onClick={onClose}
|
||||
variant={type === "success" ? "accent" : "primary"}
|
||||
className="w-full py-5 text-lg"
|
||||
showArrow
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
className={`absolute inset-0 ${type === "success" ? "bg-accent/20" : "bg-red-500/20"} rounded-full animate-ping opacity-20`}
|
||||
/>
|
||||
{type === "success" ? (
|
||||
<CheckCircle size={40} />
|
||||
) : (
|
||||
<AlertCircle size={40} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Decorative Corners */}
|
||||
<div className="tech-corner top-4 left-4 border-t-2 border-l-2 opacity-20" />
|
||||
<div className="tech-corner bottom-4 right-4 border-b-2 border-r-2 opacity-20" />
|
||||
</m.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</LazyMotion>
|
||||
<h2 className="text-3xl font-extrabold text-primary mb-4 leading-tight">
|
||||
{title}
|
||||
</h2>
|
||||
<p className="text-slate-600 text-lg mb-10 leading-relaxed">
|
||||
{message}
|
||||
</p>
|
||||
|
||||
<Button
|
||||
onClick={onClose}
|
||||
variant={type === "success" ? "accent" : "primary"}
|
||||
className="w-full py-5 text-lg"
|
||||
showArrow
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Decorative Corners */}
|
||||
<div className="tech-corner top-4 left-4 border-t-2 border-l-2 opacity-20" />
|
||||
<div className="tech-corner bottom-4 right-4 border-b-2 border-r-2 opacity-20" />
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
52
components/sections/AboutHero.tsx
Normal file
52
components/sections/AboutHero.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import { Reveal } from "@/components/Reveal";
|
||||
import { TechBackground } from "@/components/TechBackground";
|
||||
import { Counter } from "@/components/Counter";
|
||||
|
||||
interface AboutHeroProps {
|
||||
tagline: string;
|
||||
title: React.ReactNode;
|
||||
subtitle: string;
|
||||
}
|
||||
|
||||
export function AboutHero({ tagline, title, subtitle }: AboutHeroProps) {
|
||||
return (
|
||||
<section className="relative min-h-[60vh] flex items-center pt-44 pb-20 overflow-hidden">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src="/media/drums/about-hero.jpg"
|
||||
alt="About MB Grid Solutions"
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-white via-white/95 to-white/40" />
|
||||
<TechBackground />
|
||||
</div>
|
||||
|
||||
<div className="container-custom relative z-10">
|
||||
<div className="text-left relative">
|
||||
<Counter value={1} className="section-number" />
|
||||
<Reveal delay={0.1}>
|
||||
<span className="text-accent font-bold uppercase tracking-widest text-sm mb-4 block">
|
||||
{tagline}
|
||||
</span>
|
||||
</Reveal>
|
||||
<Reveal delay={0.2}>
|
||||
<h1 className="text-4xl sm:text-5xl md:text-6xl font-extrabold text-primary mb-6 md:mb-8 leading-tight">
|
||||
{title}
|
||||
</h1>
|
||||
</Reveal>
|
||||
<Reveal delay={0.3}>
|
||||
<p className="text-slate-600 text-lg md:text-2xl leading-relaxed mb-8">
|
||||
{subtitle}
|
||||
</p>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
63
components/sections/AboutIntro.tsx
Normal file
63
components/sections/AboutIntro.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Linkedin } from "lucide-react";
|
||||
import { Reveal } from "@/components/Reveal";
|
||||
import { TechBackground } from "@/components/TechBackground";
|
||||
|
||||
interface Person {
|
||||
name: string;
|
||||
role: string;
|
||||
linkedin: string;
|
||||
}
|
||||
|
||||
interface AboutIntroProps {
|
||||
paragraphs: string[];
|
||||
team: Person[];
|
||||
}
|
||||
|
||||
export function AboutIntro({ paragraphs, team }: AboutIntroProps) {
|
||||
return (
|
||||
<section className="bg-white relative overflow-hidden">
|
||||
<TechBackground />
|
||||
<div className="container-custom relative z-10">
|
||||
<div className="section-number">02</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
|
||||
<Reveal direction="right">
|
||||
<div className="space-y-6 text-lg text-slate-600 leading-relaxed relative">
|
||||
<div className="absolute -left-4 top-0 w-1 h-full bg-accent/10" />
|
||||
{paragraphs.map((p, i) => (
|
||||
<p key={i}>{p}</p>
|
||||
))}
|
||||
</div>
|
||||
</Reveal>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
{team.map((person, i) => (
|
||||
<Reveal key={i} delay={i * 0.1}>
|
||||
<div className="card-modern !p-6 hover:-translate-y-1 transition-[box-shadow,transform] duration-300 relative overflow-hidden tech-card-border">
|
||||
<div className="flex justify-between items-start mb-4 relative z-10">
|
||||
<h2 className="text-xl font-bold text-primary">
|
||||
{person.name}
|
||||
</h2>
|
||||
<a
|
||||
href={person.linkedin}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#0077b5] hover:scale-110 transition-transform"
|
||||
aria-label={`LinkedIn Profile of ${person.name}`}
|
||||
>
|
||||
<Linkedin size={20} />
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-accent text-sm font-bold uppercase tracking-wider relative z-10">
|
||||
{person.role}
|
||||
</p>
|
||||
</div>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { m } from "framer-motion";
|
||||
import { Reveal } from "../Reveal";
|
||||
import { Counter } from "../Counter";
|
||||
import { TechBackground } from "../TechBackground";
|
||||
import { Button } from "../Button";
|
||||
import { motion } from "framer-motion";
|
||||
import { Reveal } from "@/components/Reveal";
|
||||
import { Counter } from "@/components/Counter";
|
||||
import { TechBackground } from "@/components/TechBackground";
|
||||
import { Button } from "@/components/Button";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export const CTASection = () => {
|
||||
interface CTASectionProps {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
buttonText?: string;
|
||||
}
|
||||
|
||||
export const CTASection = ({
|
||||
title,
|
||||
subtitle,
|
||||
buttonText,
|
||||
}: CTASectionProps) => {
|
||||
const t = useTranslations("Index");
|
||||
|
||||
const displayTitle = title || t("cta.title");
|
||||
const displaySubtitle = subtitle || t("cta.subtitle");
|
||||
const displayButtonText = buttonText || t("cta.button");
|
||||
|
||||
return (
|
||||
<section className="bg-white relative overflow-hidden">
|
||||
<TechBackground />
|
||||
@@ -34,7 +48,7 @@ export const CTASection = () => {
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<m.circle
|
||||
<motion.circle
|
||||
animate={{ r: [400, 410, 400], opacity: [0.1, 0.2, 0.1] }}
|
||||
transition={{
|
||||
duration: 5,
|
||||
@@ -47,7 +61,7 @@ export const CTASection = () => {
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<m.circle
|
||||
<motion.circle
|
||||
animate={{ r: [300, 310, 300], opacity: [0.1, 0.2, 0.1] }}
|
||||
transition={{
|
||||
duration: 4,
|
||||
@@ -61,7 +75,7 @@ export const CTASection = () => {
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<m.circle
|
||||
<motion.circle
|
||||
animate={{ r: [200, 210, 200], opacity: [0.1, 0.2, 0.1] }}
|
||||
transition={{
|
||||
duration: 3,
|
||||
@@ -80,10 +94,10 @@ export const CTASection = () => {
|
||||
|
||||
<div className="relative z-10">
|
||||
<h2 className="text-3xl md:text-6xl font-bold text-white mb-6 md:mb-8 leading-tight">
|
||||
{t("cta.title")}
|
||||
{displayTitle}
|
||||
</h2>
|
||||
<p className="text-slate-300 text-lg md:text-xl mb-8 md:mb-12 leading-relaxed">
|
||||
{t("cta.subtitle")}
|
||||
{displaySubtitle}
|
||||
</p>
|
||||
<Button
|
||||
href="/kontakt"
|
||||
@@ -91,7 +105,7 @@ export const CTASection = () => {
|
||||
showArrow
|
||||
className="w-full sm:w-auto !px-10 !py-5 text-lg"
|
||||
>
|
||||
{t("cta.button")}
|
||||
{displayButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,14 +3,31 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import { CheckCircle2 } from "lucide-react";
|
||||
import { Reveal } from "../Reveal";
|
||||
import { Counter } from "../Counter";
|
||||
import { TechBackground } from "../TechBackground";
|
||||
import { Reveal } from "@/components/Reveal";
|
||||
import { Counter } from "@/components/Counter";
|
||||
import { TechBackground } from "@/components/TechBackground";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export const ExpertiseSection = () => {
|
||||
interface ExpertiseSectionProps {
|
||||
tag?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
groups?: string[];
|
||||
}
|
||||
|
||||
export const ExpertiseSection = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
groups,
|
||||
}: ExpertiseSectionProps) => {
|
||||
const t = useTranslations("Index");
|
||||
|
||||
const displayTag = tag || t("expertise.tag");
|
||||
const displayTitle = title || t("expertise.title");
|
||||
const displayDescription = description || t("expertise.description");
|
||||
const displayGroups = groups || t.raw("expertise.groups");
|
||||
|
||||
return (
|
||||
<section className="bg-white relative overflow-hidden">
|
||||
<TechBackground />
|
||||
@@ -34,17 +51,17 @@ export const ExpertiseSection = () => {
|
||||
<div>
|
||||
<Reveal>
|
||||
<span className="text-accent font-bold uppercase tracking-widest text-sm mb-4 block">
|
||||
{t("expertise.tag")}
|
||||
{displayTag}
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-5xl font-bold text-primary mb-6 md:mb-8">
|
||||
{t("expertise.title")}
|
||||
{displayTitle}
|
||||
</h2>
|
||||
<p className="text-slate-600 text-base md:text-xl mb-8 md:mb-12">
|
||||
{t("expertise.description")}
|
||||
{displayDescription}
|
||||
</p>
|
||||
</Reveal>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{t.raw("expertise.groups").map((item: string, i: number) => (
|
||||
{displayGroups.map((item: string, i: number) => (
|
||||
<Reveal key={i} delay={i * 0.05}>
|
||||
<div className="flex items-center gap-4 p-4 bg-slate-50 rounded-xl border border-slate-100 hover:border-accent/30 transition-colors group relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-1 h-full bg-accent/0 group-hover:bg-accent/100 transition-all duration-300" />
|
||||
|
||||
79
components/sections/Hero.tsx
Normal file
79
components/sections/Hero.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import { Button } from "@/components/Button";
|
||||
import { Counter } from "@/components/Counter";
|
||||
import { Reveal } from "@/components/Reveal";
|
||||
import { TechBackground } from "@/components/TechBackground";
|
||||
|
||||
interface HeroProps {
|
||||
tag: string;
|
||||
title: React.ReactNode;
|
||||
subtitle: string;
|
||||
ctaPrimary: string;
|
||||
ctaSecondary: string;
|
||||
}
|
||||
|
||||
export function Hero({
|
||||
tag,
|
||||
title,
|
||||
subtitle,
|
||||
ctaPrimary,
|
||||
ctaSecondary,
|
||||
}: HeroProps) {
|
||||
return (
|
||||
<section className="relative min-h-[90vh] flex items-center pt-44 pb-20 overflow-hidden">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src="/media/business/hero-bg.jpg"
|
||||
alt="MB Grid Solutions Hero"
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
quality={75}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-slate-100/80 via-white/90 to-white/40 md:to-transparent" />
|
||||
<TechBackground />
|
||||
</div>
|
||||
|
||||
<div className="container-custom relative z-10">
|
||||
<div className="text-left relative">
|
||||
<Counter value={1} className="section-number" />
|
||||
<Reveal delay={0.1}>
|
||||
<span className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-accent/10 text-accent text-xs font-bold uppercase tracking-wider mb-6">
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-accent opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-accent"></span>
|
||||
</span>
|
||||
{tag}
|
||||
</span>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.2}>
|
||||
<h1 className="text-4xl sm:text-5xl md:text-7xl font-extrabold text-primary mb-6 md:mb-8 leading-[1.1]">
|
||||
{title}
|
||||
</h1>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.3}>
|
||||
<p className="text-slate-600 text-lg md:text-2xl leading-relaxed mb-8 md:mb-12 max-w-2xl">
|
||||
{subtitle}
|
||||
</p>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.4}>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Button href="/kontakt" variant="accent" showArrow>
|
||||
{ctaPrimary}
|
||||
</Button>
|
||||
<Button href="/ueber-uns" variant="ghost">
|
||||
{ctaSecondary}
|
||||
</Button>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
80
components/sections/ManifestSection.tsx
Normal file
80
components/sections/ManifestSection.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
Award,
|
||||
Clock,
|
||||
Lightbulb,
|
||||
Truck,
|
||||
MessageSquare,
|
||||
ShieldCheck,
|
||||
} from "lucide-react";
|
||||
import { Reveal } from "@/components/Reveal";
|
||||
import { TechBackground } from "@/components/TechBackground";
|
||||
import { Counter } from "@/components/Counter";
|
||||
|
||||
const manifestIcons = [
|
||||
Award,
|
||||
Clock,
|
||||
Lightbulb,
|
||||
Truck,
|
||||
MessageSquare,
|
||||
ShieldCheck,
|
||||
];
|
||||
|
||||
interface ManifestItem {
|
||||
title: string;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
interface ManifestSectionProps {
|
||||
tagline: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
items: ManifestItem[];
|
||||
}
|
||||
|
||||
export function ManifestSection({
|
||||
tagline,
|
||||
title,
|
||||
subtitle,
|
||||
items,
|
||||
}: ManifestSectionProps) {
|
||||
return (
|
||||
<section className="bg-slate-950 text-accent relative overflow-hidden">
|
||||
<TechBackground />
|
||||
<div className="container-custom relative z-10">
|
||||
<Counter value={3} className="section-number !text-white/5" />
|
||||
<Reveal className="mb-20">
|
||||
<span className="text-accent font-bold uppercase tracking-widest text-sm mb-4 block">
|
||||
{tagline}
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-5xl font-bold text-white mb-6">
|
||||
{title}
|
||||
</h2>
|
||||
<p className="text-slate-400 text-base md:text-lg">{subtitle}</p>
|
||||
</Reveal>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{items.map((item, i) => {
|
||||
const Icon = manifestIcons[i % manifestIcons.length];
|
||||
return (
|
||||
<Reveal key={i} delay={i * 0.1}>
|
||||
<div className="bg-white/5 p-10 rounded-3xl border border-white/10 group hover:-translate-y-1 transition-[box-shadow,transform] duration-300 h-full motion-fix relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-full h-1 bg-accent/0 group-hover:bg-accent/50 transition-all duration-500" />
|
||||
<div className="text-accent mb-6">
|
||||
<Icon size={32} />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-white mb-4">
|
||||
{i + 1}. {item.title}
|
||||
</h3>
|
||||
<p className="text-slate-400 leading-relaxed">{item.desc}</p>
|
||||
</div>
|
||||
</Reveal>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -3,14 +3,59 @@
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import { ChevronRight, Zap, Shield, BarChart3 } from "lucide-react";
|
||||
import { Reveal } from "../Reveal";
|
||||
import { Counter } from "../Counter";
|
||||
import { TechBackground } from "../TechBackground";
|
||||
import { Reveal } from "@/components/Reveal";
|
||||
import { Counter } from "@/components/Counter";
|
||||
import { TechBackground } from "@/components/TechBackground";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export const PortfolioSection = () => {
|
||||
interface PortfolioItem {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
interface PortfolioSectionProps {
|
||||
tag?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
linkText?: string;
|
||||
items?: PortfolioItem[];
|
||||
}
|
||||
|
||||
export const PortfolioSection = ({
|
||||
tag,
|
||||
title,
|
||||
description,
|
||||
linkText,
|
||||
items,
|
||||
}: PortfolioSectionProps) => {
|
||||
const t = useTranslations("Index");
|
||||
|
||||
const displayTag = tag || t("portfolio.tag");
|
||||
const displayTitle = title || t("portfolio.title");
|
||||
const displayDescription = description || t("portfolio.description");
|
||||
const displayLinkText = linkText || t("portfolio.link");
|
||||
|
||||
const defaultItems = [
|
||||
{
|
||||
icon: <Zap size={32} />,
|
||||
title: t("portfolio.items.beratung.title"),
|
||||
desc: t("portfolio.items.beratung.desc"),
|
||||
},
|
||||
{
|
||||
icon: <Shield size={32} />,
|
||||
title: t("portfolio.items.begleitung.title"),
|
||||
desc: t("portfolio.items.begleitung.desc"),
|
||||
},
|
||||
{
|
||||
icon: <BarChart3 size={32} />,
|
||||
title: t("portfolio.items.beschaffung.title"),
|
||||
desc: t("portfolio.items.beschaffung.desc"),
|
||||
},
|
||||
];
|
||||
|
||||
const displayItems = items || defaultItems;
|
||||
|
||||
return (
|
||||
<section className="bg-slate-950 text-accent relative overflow-hidden">
|
||||
<TechBackground />
|
||||
@@ -19,20 +64,20 @@ export const PortfolioSection = () => {
|
||||
<Reveal className="flex flex-col md:flex-row md:items-end justify-between gap-8 mb-16">
|
||||
<div>
|
||||
<span className="text-accent font-bold uppercase tracking-widest text-sm mb-4 block">
|
||||
{t("portfolio.tag")}
|
||||
{displayTag}
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-5xl font-bold text-white mb-6">
|
||||
{t("portfolio.title")}
|
||||
{displayTitle}
|
||||
</h2>
|
||||
<p className="text-slate-400 text-base md:text-xl">
|
||||
{t("portfolio.description")}
|
||||
{displayDescription}
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/ueber-uns"
|
||||
className="text-accent font-bold flex items-center gap-2 hover:text-white transition-colors group"
|
||||
>
|
||||
{t("portfolio.link")}{" "}
|
||||
{displayLinkText}{" "}
|
||||
<ChevronRight
|
||||
className="transition-transform group-hover:translate-x-1"
|
||||
size={20}
|
||||
@@ -41,23 +86,7 @@ export const PortfolioSection = () => {
|
||||
</Reveal>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{[
|
||||
{
|
||||
icon: <Zap size={32} />,
|
||||
title: t("portfolio.items.beratung.title"),
|
||||
desc: t("portfolio.items.beratung.desc"),
|
||||
},
|
||||
{
|
||||
icon: <Shield size={32} />,
|
||||
title: t("portfolio.items.begleitung.title"),
|
||||
desc: t("portfolio.items.begleitung.desc"),
|
||||
},
|
||||
{
|
||||
icon: <BarChart3 size={32} />,
|
||||
title: t("portfolio.items.beschaffung.title"),
|
||||
desc: t("portfolio.items.beschaffung.desc"),
|
||||
},
|
||||
].map((item, i) => (
|
||||
{displayItems.map((item, i) => (
|
||||
<Reveal key={i} delay={i * 0.1}>
|
||||
<div className="bg-white/5 p-8 rounded-2xl border border-white/10 group hover:-translate-y-2 transition-[box-shadow,transform] duration-300 h-full relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-16 h-16 bg-accent/5 -mr-8 -mt-8 rounded-full group-hover:bg-accent/10 transition-colors" />
|
||||
|
||||
@@ -2,14 +2,53 @@
|
||||
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import { Reveal } from "../Reveal";
|
||||
import { Counter } from "../Counter";
|
||||
import { TechBackground } from "../TechBackground";
|
||||
import { Reveal } from "@/components/Reveal";
|
||||
import { Counter } from "@/components/Counter";
|
||||
import { TechBackground } from "@/components/TechBackground";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export const TechnicalSpecsSection = () => {
|
||||
interface SpecItem {
|
||||
label: string;
|
||||
value: string;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
interface TechnicalSpecsSectionProps {
|
||||
tag?: string;
|
||||
title?: string;
|
||||
items?: SpecItem[];
|
||||
}
|
||||
|
||||
export const TechnicalSpecsSection = ({
|
||||
tag,
|
||||
title,
|
||||
items,
|
||||
}: TechnicalSpecsSectionProps) => {
|
||||
const t = useTranslations("Index");
|
||||
|
||||
const displayTag = tag || t("specs.tag");
|
||||
const displayTitle = title || t("specs.title");
|
||||
|
||||
const defaultItems = [
|
||||
{
|
||||
label: t("specs.items.kabel.label"),
|
||||
value: t("specs.items.kabel.value"),
|
||||
desc: t("specs.items.kabel.desc"),
|
||||
},
|
||||
{
|
||||
label: t("specs.items.spannung.label"),
|
||||
value: t("specs.items.spannung.value"),
|
||||
desc: t("specs.items.spannung.desc"),
|
||||
},
|
||||
{
|
||||
label: t("specs.items.technologie.label"),
|
||||
value: t("specs.items.technologie.value"),
|
||||
desc: t("specs.items.technologie.desc"),
|
||||
},
|
||||
];
|
||||
|
||||
const displayItems = items || defaultItems;
|
||||
|
||||
return (
|
||||
<section className="relative py-24 md:py-32 text-white overflow-hidden bg-slate-900">
|
||||
<div className="absolute inset-0 opacity-20">
|
||||
@@ -31,37 +70,21 @@ export const TechnicalSpecsSection = () => {
|
||||
|
||||
<Reveal className="mb-20">
|
||||
<span className="text-accent font-bold uppercase tracking-widest text-sm mb-4 block">
|
||||
{t("specs.tag")}
|
||||
{displayTag}
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-5xl font-bold text-white mb-6">
|
||||
{t("specs.title")}
|
||||
{displayTitle}
|
||||
</h2>
|
||||
</Reveal>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 md:gap-8">
|
||||
{[
|
||||
{
|
||||
label: t("specs.items.kabel.label"),
|
||||
value: t("specs.items.kabel.value"),
|
||||
desc: t("specs.items.kabel.desc"),
|
||||
},
|
||||
{
|
||||
label: t("specs.items.spannung.label"),
|
||||
value: t("specs.items.spannung.value"),
|
||||
desc: t("specs.items.spannung.desc"),
|
||||
},
|
||||
{
|
||||
label: t("specs.items.technologie.label"),
|
||||
value: t("specs.items.technologie.value"),
|
||||
desc: t("specs.items.technologie.desc"),
|
||||
},
|
||||
].map((item, i) => (
|
||||
{displayItems.map((item, i) => (
|
||||
<Reveal key={i} delay={i * 0.1}>
|
||||
<div className="p-10 rounded-3xl bg-white/5 border border-white/10 backdrop-blur-sm hover:bg-white/10 transition-colors h-full relative group overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-full h-1 bg-accent/0 group-hover:bg-accent/50 transition-all duration-500" />
|
||||
<h4 className="text-accent font-bold text-xs uppercase tracking-widest mb-6">
|
||||
<h3 className="text-accent font-bold text-xs uppercase tracking-widest mb-6">
|
||||
{item.label}
|
||||
</h4>
|
||||
</h3>
|
||||
<p className="text-2xl font-bold text-white mb-4 leading-tight">
|
||||
{item.value}
|
||||
</p>
|
||||
|
||||
67
content/de/about.mdx
Normal file
67
content/de/about.mdx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { AboutHero } from "@/components/sections/AboutHero";
|
||||
import { AboutIntro } from "@/components/sections/AboutIntro";
|
||||
import { ManifestSection } from "@/components/sections/ManifestSection";
|
||||
import { CTASection } from "@/components/sections/CTASection";
|
||||
|
||||
<AboutHero
|
||||
tagline="Über uns"
|
||||
title={<>Zuverlässige Begleitung für Ihre <span className="text-accent">Netzinfrastruktur</span></>}
|
||||
subtitle="Herstellerneutrale Beratung in der Energiekabeltechnologie. Wir verstehen uns als Ihr technischer Lotse."
|
||||
/>
|
||||
|
||||
<AboutIntro
|
||||
paragraphs={[
|
||||
"Unsere Wurzeln liegen in der tiefen praktischen Erfahrung unserer technischen Berater und unserer Netzwerke im globalem Kabelmarkt. Wir vereinen Tradition mit modernster Innovation, um zuverlässige Energielösungen für Projekte bis 110 kV zu realisieren.",
|
||||
"Wir verstehen die Herausforderungen der Energiewende und bieten herstellerneutrale Beratung, die auf Fakten, Normen und jahrzehntelanger Erfahrung basiert."
|
||||
]}
|
||||
team={[
|
||||
{
|
||||
name: "Michael Bodemer",
|
||||
role: "Geschäftsführung & Inhaber",
|
||||
linkedin: "https://www.linkedin.com/in/michael-bodemer-33b493122/"
|
||||
},
|
||||
{
|
||||
name: "Klaus Mintel",
|
||||
role: "Geschäftsführung",
|
||||
linkedin: "https://www.linkedin.com/in/klaus-mintel-b80a8b193/"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<ManifestSection
|
||||
tagline="Werte"
|
||||
title="Unser Manifest"
|
||||
subtitle="Werte, die unsere tägliche Arbeit leiten und den Erfolg Ihrer Projekte sichern."
|
||||
items={[
|
||||
{
|
||||
title: "Kompetenz",
|
||||
desc: "Jahrzehntelange Erfahrung kombiniert mit europaweitem Know-how in modernsten Anlagen."
|
||||
},
|
||||
{
|
||||
title: "Verfügbarkeit",
|
||||
desc: "Schnelle und verlässliche Unterstützung ohne unnötige Verzögerungen."
|
||||
},
|
||||
{
|
||||
title: "Lösungen",
|
||||
desc: "Wir stellen die richtigen Fragen, um die technisch und wirtschaftlich beste Lösung zu finden."
|
||||
},
|
||||
{
|
||||
title: "Logistik",
|
||||
desc: "Von der Fertigungsüberwachung bis zum termingerechten Fracht-Tracking."
|
||||
},
|
||||
{
|
||||
title: "Offenheit",
|
||||
desc: "Wir hören zu und passen unsere Prozesse kontinuierlich an Ihren Erfolg an."
|
||||
},
|
||||
{
|
||||
title: "Zuverlässigkeit",
|
||||
desc: "Wir halten, was wir versprechen – ohne Ausnahme. Verbindlichkeit ist unser Fundament."
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<CTASection
|
||||
title="Bereit für Ihr nächstes Projekt?"
|
||||
subtitle="Lassen Sie uns gemeinsam die optimale Lösung für Ihre Energieinfrastruktur finden. Wir beraten Sie herstellerneutral und kompetent."
|
||||
buttonText="Jetzt Kontakt aufnehmen"
|
||||
/>
|
||||
254
content/de/avb.mdx
Normal file
254
content/de/avb.mdx
Normal file
@@ -0,0 +1,254 @@
|
||||
# Allgemeine Verkaufsbedingungen (AVB)
|
||||
|
||||
Stand: April 2026
|
||||
|
||||
|
||||
## 1. Geltungsbereich
|
||||
|
||||
|
||||
**1.1** Ausschließlich die nachfolgenden Allgemeinen Verkaufsbedingungen (nachfolgend „AVB“) gelten für den gesamten Geschäftsverkehr (nachfolgend „Lieferung von Waren“) der MB Grid Solutions & Services, Raiffeisenstraße 22, 73630 Remshalden (nachfolgend „MB Grid Solutions & Services“) und Unternehmern in Ausübung ihrer gewerblichen oder selbständigen beruflichen Tätigkeit und juristischen Personen des öffentlichen Rechts (nachfolgend „Kunde“). Der Geltung von abweichenden, zusätzlichen oder entgegenstehenden allgemeinen Geschäftsbedingungen des Kunden wird hiermit widersprochen.
|
||||
|
||||
**1.2** Diese AVB gelten auch dann, wenn MB Grid Solutions & Services die Lieferung von Waren an den Kunden vorbehaltlos in Kenntnis entgegenstehender oder von diesen AVB abweichenden allgemeinen Geschäftsbedingungen des Kunden ausführt.
|
||||
|
||||
**1.3** Im Rahmen laufender Geschäftsbeziehungen gelten diese AVB auch dann für künftige Lieferungen von Waren, wenn sie nicht ausdrücklich erneut in den Vertrag einbezogen wurden.
|
||||
|
||||
**1.4** Änderungen dieser AVB werden dem Kunden schriftlich, per Telefax oder per E-Mail mitgeteilt. Widerspricht der Kunde diesen Änderungen nicht innerhalb von vier (4) Wochen nach Zugang der Mitteilung, gelten die Änderungen als durch den Kunden anerkannt. Auf das Widerspruchsrecht und die Rechtsfolgen des Schweigens wird der Kunde im Rahmen der Mitteilung über die Änderung der Geschäftsbedingungen gesondert hingewiesen.
|
||||
|
||||
**1.5** Hinweise auf die Geltung gesetzlicher Vorschriften im Rahmen dieser AVB haben nur klarstellende Bedeutung. Auch ohne eine derartige Klarstellung gelten daher die gesetzlichen Vorschriften, soweit sie in diesen AVB nicht unmittelbar abgeändert oder ausdrücklich ausgeschlossen werden.
|
||||
|
||||
## 2. Vertragsschluss
|
||||
|
||||
|
||||
**2.1** Der Vertrag über die Lieferung von Waren kommt grundsätzlich durch ein Angebot des Kunden, z.B. über ein Auftragsformular, per E-Mail oder per Brief, und eine Annahme dieses Angebots durch MB Grid Solutions & Services in Textform z.B. durch eine Auftragsbestätigung per E-Mail oder Fax zustande. Der Annahme des Angebots durch MB Grid Solutions & Services steht es gleich, wenn MB Grid Solutions & Services den Auftrag oder die Bestellung binnen vierzehn (14) Kalendertagen ab Zugang des Angebots bei MB Grid Solutions & Services vorbehaltlos ausführt.
|
||||
|
||||
**2.2** Alle Angebote von MB Grid Solutions & Services sind – insbesondere hinsichtlich Verfügbarkeit, angegebener Mengen, Lieferfristen und Nebenleistungen – freibleibend und unverbindlich, sofern sie nicht ausdrücklich in Textform als verbindlich gekennzeichnet wurden. Die Angebote stehen unter dem Vorbehalt der zeitgerechten, qualitativen und quantitativen Selbstbelieferung von MB Grid Solutions & Services.
|
||||
|
||||
**2.3** Der Umfang der von MB Grid Solutions & Services zu liefernden Waren ergibt sich aus dem jeweiligen Angebot oder der jeweiligen Auftragsbestätigung von MB Grid Solutions & Services.
|
||||
|
||||
**2.4** Technische Angaben, Muster, Preise, Abbildungen, Zeichnungen, Gewichts-, Maß- , Leistungs- und Gebrauchsangaben, sowie sonstige Beschreibungen der zu liefernden Ware in dem Angebot oder der Auftragsbestätigung sind nur annähernd maßgebend, soweit sie nicht ausdrücklich von MB Grid Solutions & Services in Textform als verbindlich gekennzeichnet wurden. Sie stellen keine Beschaffenheitsvereinbarung oder Garantie einer entsprechenden Beschaffenheit oder Haltbarkeit der zu liefernden Ware dar, es sei denn, sie wurden ausdrücklich in Textform als solche vereinbart. Entsprechendes gilt für Erwartungen des Kunden hinsichtlich der zu liefernden Ware oder deren Verwendung.
|
||||
|
||||
**2.5** Handelsübliche und branchenübliche Abweichungen vom Leistungsumfang der zu liefernden Ware durch MB Grid Solutions & Services sind zulässig, soweit sie die Verwendbarkeit der zu liefernden Ware zum vertraglich vorgesehenen Zweck nicht beeinträchtigen. Dies umfasst auch Über- oder Unterlieferungen. Abweichungen der zu liefernden Ware, die durch zwingende rechtliche oder technische Normen, die nach Auftragsbestätigung in Kraft treten, bedingt sind, sind zulässig, soweit sie nicht erheblich und dem Kunden zumutbar sind. Änderungen, die zu einer technischen Verbesserung der zu liefernden Ware führen, sind stets zulässig.
|
||||
|
||||
**2.6** Änderungen des Lieferumfangs durch den Kunden bedürfen zu ihrer Wirksamkeit stets der in Textform erklärten Einwilligung von MB Grid Solutions & Services. Mehraufwände und sonstige Kosten, die durch die Änderung des Lieferumfangs auf Wunsch des Kunden entstehen, sind von dem Kunden gesondert zu vergüten.
|
||||
|
||||
**2.7** Soweit das Angebot oder die Auftragsbestätigung von MB Grid Solutions & Services offensichtliche Irrtümer, Schreib- oder Rechenfehler enthält, ist das Dokument für MB Grid Solutions & Services hinsichtlich der offensichtlichen Fehler nicht verbindlich. Der Kunde wird MB Grid Solutions & Services auf entsprechende Fehler hinweisen.
|
||||
|
||||
**2.8** Verschlechtern sich die Vermögensverhältnisse des Kunden wesentlich oder wird der Antrag zur Eröffnung eines Insolvenz- oder vergleichbaren Verfahrens über das Vermögen des Kunden mangels Masse abgelehnt, ist MB Grid Solutions & Services berechtigt, ganz oder teilweise vom Vertrag zurückzutreten.
|
||||
|
||||
## 3. Abrufaufträge
|
||||
|
||||
|
||||
**3.1** Erteilt der Kunde MB Grid Solutions & Services einen Abrufauftrag und werden über die Abruftermine keine gesonderten Vereinbarungen in Textform getroffen, ist der Kunde verpflichtet, MB Grid Solutions & Services die einzelnen Abruftermine so mitzuteilen, dass zwischen Eingang der Abrufmitteilung bei MB Grid Solutions & Services und Auslieferung der Ware mindestens vierzehn (14) Werktage und zwischen Eingang der Abrufmitteilung und der letzten Auslieferung der Ware maximal neunzig (90) Werktage liegen. Werktage sind dabei alle Kalendertage, bei denen es sich nicht um einen Sonntag oder einen gesetzlichen Feiertag am Sitz von MB Grid Solutions & Services handelt.
|
||||
|
||||
## 4. Preise und Zahlungsbedingungen
|
||||
|
||||
|
||||
**4.1** Die in dem Angebot oder der Auftragsbestätigung von MB Grid Solutions & Services angegebenen Preise gelten für den in dem Angebot oder der Auftragsbestätigung von MB Grid Solutions & Services aufgeführten Lieferumfang und verstehenden sich grundsätzlich in EURO und ohne Abzüge zuzüglich Versandkosten ab dem jeweiligen Werk des Herstellers der Ware, exklusive Verpackung und zuzüglich der jeweils geltenden gesetzlichen Umsatzsteuer, sofern nicht ausdrücklich etwas anderes in Textform vereinbart wurde. Die in dem Angebot oder der Auftragsbestätigung von MB Grid Solutions & Services ausgewiesenen Hohlpreise verstehen sich darüber hinaus zuzüglich Metallzuschlag und auftragsspezifischer Schnittkosten.
|
||||
|
||||
**4.2** Sollte sich nach Vertragsschluss herausstellen, dass zur Durchführung des Vertrages bzw. zur Lieferung der Waren Mehrleistungen erforderlich sind, so werden diese Mehrleistungen gesondert abgerechnet und vergütet.
|
||||
|
||||
**4.3** Hat sich der Preis für eine zu liefernde Ware, bzw. der vereinbarte Hohlpreis z.B. aufgrund gestiegener Preise für Energie, Transport, Arbeitskraft oder Material zwischen Vertragsschluss und Lieferdatum um mehr als fünf (5) % erhöht, werden die Parteien partnerschaftlich und konstruktiv über eine Anpassung des Preises bzw. des Hohlpreises diskutieren. MB Grid Solutions & Services ist zum Rücktritt von dem betroffenen Vertrag berechtigt, wenn innerhalb von vierzehn (14) Kalendertagen ab Anzeige der Preiserhöhung durch MB Grid Solutions & Services keine einvernehmliche Lösung gefunden wird.
|
||||
|
||||
**4.4** Der Kaufpreis wird sofort mit Erhalt der Rechnung oder Lieferung der Ware, je nachdem welcher Zeitpunkt früher eintritt, zur Zahlung fällig, sofern keine anderen Zahlungskonditionen in Textform vereinbart sind. Der Kunde hat den ausstehenden Kaufpreis innerhalb von zehn (10) Kalendertagen nach Erhalt der Rechnung oder Erhalt der Ware, je nachdem welcher Zeitpunkt früher eintritt, frei Konto von MB Grid Solutions & Services und ohne Abzug zu bezahlen. Die Zahlungsfrist gilt mit vollständiger Gutschrift des ausstehenden Kaufpreises auf dem Konto von MB Grid Solutions & Services als gewahrt. Nach Ablauf der Zahlungsfrist kommt der Kunde automatisch mit der Zahlung des ausstehenden Kaufpreises in Verzug, ohne dass es hierfür einer gesonderten Mahnung von MB Grid Solutions & Services bedarf.
|
||||
|
||||
**4.5** Befindet sich der Kunde mit einer Zahlung in Verzug, ist MB Grid Solutions & Services berechtigt, die zu liefernde Ware nur Zug-um-Zug gegen Zahlung der Summe, mit der sich der Kunde in Verzug befindet, zu liefern. Dies gilt auch für alle künftigen Lieferungen von Waren von MB Grid Solutions & Services an den Kunden. Darüber hinaus stehen MB Grid Solutions & Services sämtliche Ansprüche gegen den Kunden aufgrund dessen Zahlungsverzug uneingeschränkt zu.
|
||||
|
||||
**4.6** Befindet sich der Kunde seit mehr als vierzehn (14) Kalendertagen in Zahlungsverzug, ist MB Grid Solutions & Services dazu berechtigt, vom Vertrag zurückzutreten und die gelieferte Ware unverzüglich zurück zu verlangen, ohne dass dem Kunden hieraus Ansprüche entstehen.
|
||||
|
||||
**4.7** Das Recht des Kunden, Zahlungen zurückzuhalten oder mit Gegenansprüchen aufzurechnen, steht dem Kunden nur insoweit zu, als seine Gegenansprüche unbestritten oder rechtskräftig festgestellt sind, oder wenn die jeweiligen Ansprüche in einem Gegenseitigkeitsverhältnis zueinanderstehen.
|
||||
|
||||
## 5. Metallnotierung
|
||||
|
||||
|
||||
**5.1** Basis zur Berechnung des Kupferpreises ist der Durchschnitt des Liefervormonats der Notierung „LME Copper official price cash offer“, zuzüglich der jeweils aktuellen von MB Grid Solutions & Services benannten Kupfer-Prämie.
|
||||
|
||||
**5.2** Basis zur Berechnung des Aluminiumpreises ist der Durchschnitt des Liefervormonats der Notierung „LME Aluminium official price cash offer" zuzüglich der jeweils von MB Grid Solutions & Services benannten Aluminium-Prämie.
|
||||
|
||||
**5.3** USD werden auf Basis des EUR/USD LME-FX-Rate (MTLE) in EUR umgerechnet. Die entsprechenden Notierungen können der Webseite www.westmetall.com entnommen werden. Die Prämienzuschläge können stark variieren und MB Grid Solutions & Services behält sich das Recht vor, diese fristgerecht anzupassen, ungeachtet der Angebotslegung.
|
||||
|
||||
## 6. Metallzahl
|
||||
|
||||
|
||||
**6.1** Die von MB Grid Solutions & Services in dem Angebot oder der Auftragsbestätigung ausgewiesene Metallzahl ist eine rein kaufmännische Berechnungsgröße für den Metallinhalt, der in die Berechnung des Gesamtpreises eines Kabels eingeht.
|
||||
|
||||
**6.2** Die Angabe der Metallzahl dient ausschließlich dazu, dem Kunden eine Vergleichbarkeit auf Grundlage der Holpreisbasis zu ermöglichen. Die Metallzahl definiert nicht das Gewicht des tatsächlich im Kabel enthaltenen Leitermetalls. Die Metallzahl ist ein rein kalkulatorischer Berechnungsfaktor, der keine unmittelbaren Rückschlüsse auf die im Kabel verwendeten Kupfer- bzw. Aluminiummengen zulässt. MB Grid Solutions & Services weist den Kunden hiermit ausdrücklich darauf hin, final nur den Vollpreis für Vergleichszwecke heranzuziehen.
|
||||
|
||||
**6.3** Auf Anfrage des Kunden in Textform ist MB Grid Solutions & Services grundsätzlich dazu bereit, andere Metallzahlen zu berechnen und auszuweisen. Bei jeglicher Änderung bleibt der Vollpreis der gleiche Betrag. Ein Anspruch des Kunden auf Berechnung und Angabe anderer Metallzahlen besteht nicht.
|
||||
|
||||
## 7. Lieferung
|
||||
|
||||
|
||||
**7.1** Lieferungen erfolgen grundsätzlich DAP (INCOTERMS® 2020) sofern die Parteien keine anderweitige Vereinbarung in Textform getroffen haben.
|
||||
|
||||
**7.2** Die Lieferung der Ware wird nach Möglichkeit geschlossen vorgenommen. MB Grid Solutions & Services ist jedoch berechtigt, Teillieferungen vorzunehmen, wenn dies für den Kunden zumutbar ist, insbesondere wenn wegen Fehlens von Waren eine erhebliche Verzögerung der gesamten Lieferung eintreten würde.
|
||||
|
||||
**7.3** Lieferfristen aus Angeboten und Auftragsbestätigungen sind grundsätzlich für MB Grid Solutions & Services unverbindlich. Eine verbindliche Lieferfrist muss ausdrücklich in Textform als solche gekennzeichnet werden.
|
||||
|
||||
**7.4** Die Lieferfrist beginnt mit dem Zugang der Auftragsbestätigung bei dem Kunden. Die Einhaltung der Lieferfrist setzt die rechtzeitige Klärung aller kaufmännischen und technischen Fragen, sowie die Einhaltung der vereinbarten Zahlungsbedingungen und sonstigen Verpflichtungen des Kunden voraus. Werden diese Voraussetzungen nicht rechtzeitig erfüllt, so verlängern sich die Fristen angemessen; dies gilt nicht, wenn ausschließlich MB Grid Solutions & Services die Verzögerung zu vertreten hat.
|
||||
|
||||
**7.5** Die Einhaltung auch einer verbindlichen Lieferfrist steht unter dem Vorbehalt ordnungsgemäßer, insbesondere rechtzeitiger quantitativer und qualitativer Selbstbelieferung von MB Grid Solutions & Services durch dessen Vorlieferanten. MB Grid Solutions & Services ist im Fall der nicht ordnungsgemäßen Selbstbelieferung zum Rücktritt vom Vertrag berechtigt. MB Grid Solutions & Services informiert den Kunden unverzüglich, wenn und soweit MB Grid Solutions & Services von seinem Recht zum Rücktritt vom Vertrag Gebrauch macht und gewährt etwa erbrachte Vorleistungen des Kunden zurück.
|
||||
|
||||
**7.6** Die Lieferfrist gilt als gewahrt, wenn MB Grid Solutions & Services die zu liefernde Ware dem Kunden bis zum Ablauf der Lieferfrist an dem benannten Lieferort innerhalb der üblichen Geschäftszeiten zur Annahme erstmalig angeboten hat. .
|
||||
|
||||
**7.7** Auch wenn eine verbindliche Lieferfrist vereinbart wurde, kommt MB Grid Solutions & Services mit der Lieferung der Ware nur dann in Verzug, wenn der Kunde MB Grid Solutions & Services nach Ablauf der Lieferfrist eine angemessene Nachfrist zur Lieferung der Ware in Textform gesetzt hat und diese Nachfrist erfolglos verstrichen ist.
|
||||
|
||||
**7.8** Wird die Lieferung der Ware, oder das Abladen der Ware, auf Wunsch des Kunden verzögert, oder verzögert sich die Lieferung der Ware, oder das Abladen der Ware, aus sonstigen Gründen, die der Kunde zu vertreten hat, ist MB Grid Solutions & Services berechtigt, dem Kunden nach Ablauf der vertraglich vereinbarten Lieferfrist ein Lagergeld in Höhe von zwei (2) % des jeweiligen Nettoauftragswertes für jeden angefangenen Monat, maximal jedoch fünf (5) % des jeweiligen Nettoauftragswertes in Rechnung zu stellen. Das Lagergeld ist der Höhe nach auf maximal fünf (5) % des Nettoauftragswertes pro Vertrag beschränkt.
|
||||
|
||||
**7.9** MB Grid Solutions & Services behält sich die Geltendmachung aller darüberhinausgehenden Ansprüche gegenüber dem Kunden aufgrund dessen verzögerter Übernahme der Ware uneingeschränkt vor. Das Lagergeld ist auf weitergehende Zahlungsansprüche von MB Grid Solutions & Services gegenüber dem Kunden aufgrund dessen verzögerter Übernahme der Ware anzurechnen. Dem Kunden bleibt der Nachweis gestattet, dass MB Grid Solutions & Services überhaupt kein oder nur ein geringerer Schaden entstanden ist.
|
||||
|
||||
**7.10** Kabel werden von MB Grid Solutions & Services auf stabilen Vollholztrommeln geliefert. Auf Wunsch vermittelt MB Grid Solutions & Services dem Kunden Partner, die diese Trommeln gegen eine Gebühr abholen. Bei Lagerzeiten über drei (3) Monaten ist zu beachten, dass die Trommel regelmäßig bewegt wird, um eine einseitige Belastung zu vermeiden. Folgeschäden bei Nichtbeachtung wie Instabilität schließt MB Grid Solutions & Services aus.
|
||||
|
||||
**7.11** Rücksendungen von Waren an MB Grid Solutions & Services, die nicht vorher von MB Grid Solutions & Services in Textform bestätigt wurden, erfolgen auf alleinige Gefahr des Kunden.
|
||||
|
||||
## 8. Gefahrübergang
|
||||
|
||||
|
||||
**8.1** Die Gefahr des zufälligen Untergangs und der zufälligen Verschlechterung der zu liefernden Ware geht grundsätzlich DAP (INCOTERMS® 2020) auf den Kunden über, sofern die Parteien keine anderweitige Vereinbarung in Textform getroffen haben.
|
||||
|
||||
**8.2** Verzögert sich die Versendung bzw. Übergabe der Ware aus Gründen, die der Kunde zu vertreten hat, geht die Gefahr des zufälligen Untergangs und der zufälligen Verschlechterung der Ware mit der Durchführung des ersten Zustellversuchs innerhalb der gewöhnlichen Öffnungszeiten des Kunden durch MB Grid Solutions & Services auf den Kunden über.
|
||||
|
||||
## 9. Eigentumsvorbehalt
|
||||
|
||||
|
||||
**9.1** Die jeweils gelieferten Waren bleiben bis zur vollständigen Bezahlung sämtlicher fälliger Forderungen aus der Geschäftsbeziehung zwischen dem Kunden und MB Grid Solutions & Services in dem Eigentum von MB Grid Solutions & Services (nachfolgend „Vorbehaltsware“). Dabei ist unerheblich, aus welchem Rechtsgrund die Forderungen von MB Grid Solutions & Services gegen den Kunden bestehen. Erfasst sind insbesondere auch Saldoforderungen aus laufender Rechnung.
|
||||
|
||||
**9.2** Der Kunde ist berechtigt, die gelieferte Ware im Rahmen des ordnungsgemäßen Geschäftsbetriebes zu veräußern, umzubilden oder weiterzuverarbeiten.
|
||||
|
||||
**9.3** Die Verarbeitung oder Umbildung der Vorbehaltsware durch den Kunden erfolgt jedoch stets für MB Grid Solutions & Services und MB Grid Solutions & Services erwirbt Eigentum an der neuen Sache. Wird die Vorbehaltsware mit anderen Sachen zu einer einheitlichen Sache verbunden, verarbeitet oder untrennbar vermischt oder vermengt oder in sonstiger Weise verbunden, erwirbt MB Grid Solutions & Services Miteigentum an der neuen Sache im Verhältnis des Wertes der Vorbehaltsware zu den anderen Gegenständen zur Zeit der Verarbeitung bzw. Vermischung, Vermengung oder sonstigen Verbindung. Erfolgt die Vermischung, Vermengung oder sonstige Verbindung dergestalt, dass die Sache des Kunden als Hauptsache anzusehen ist, so willigt der Kunde bereits jetzt dazu ein, MB Grid Solutions & Services anteiliges Miteigentum an der neuen Sache, in dem Mengenverhältnis der Vorbehaltsware zu den fremden Sachen, zu übertragen. Für die so entstandenen Sachen gelten die Bestimmungen für Vorbehaltsware.
|
||||
|
||||
**9.4** Der Kunde hat die Vorbehaltsware mit kaufmännischer Sorgfalt für MB Grid Solutions & Services zu verwahren, pfleglich zu behandeln und auf seine Kosten gegen Feuer, Wasser und Diebstahl zum Neuwert zu versichern. Der Kunde tritt seine entsprechenden Ansprüche aus den Versicherungsverträgen an MB Grid Solutions & Services ab; MB Grid Solutions & Services nimmt die Abtretung an.
|
||||
|
||||
**9.5** Der Kunde darf die Vorbehaltsware nur im ordnungsgemäßen Geschäftsgang bis zum Eintritt des Verwertungsfalles veräußern. Verpfändungen und Sicherungsübereignungen hinsichtlich der Vorbehaltsware sind unzulässig. Eingriffe Dritter, wie etwa die Pfändung oder Zwangsvollstreckung, durch welche Rechte von MB Grid Solutions & Services beeinträchtigt werden, hat der Kunde MB Grid Solutions & Services unverzüglich in Textform anzuzeigen. Der Kunde hat den Dritten darüber hinaus unverzüglich in Textform darauf hinzuweisen, dass MB Grid Solutions & Services Eigentümer der Vorbehaltsware ist. Soweit der Dritte nicht in der Lage ist, MB Grid Solutions & Services die gerichtlichen und außergerichtlichen Kosten einer Klage nach § 771 ZPO zu erstatten, haftet der Kunde für den dadurch entstandenen Ausfall. Weitergehende Ansprüche gegen den Kunden bleiben unberührt.
|
||||
|
||||
**9.6** Für den Fall der Weiterveräußerung der Vorbehaltsware tritt der Kunde die ihm daraus seinerseits erwachsenden Forderungen gegen seinen Abnehmer bereits jetzt an MB Grid Solutions & Services ab; MB Grid Solutions & Services nimmt die Abtretung an. Gleiches gilt für sonstige Forderungen, die an die Stelle der Vorbehaltsware treten oder sonst hinsichtlich der Vorbehaltsware entstehen, wie z. B. Versicherungsansprüche oder Ansprüche aus unerlaubter Handlung bei Verlust oder Zerstörung.
|
||||
|
||||
**9.7** Wird die Vorbehaltsware nach Verarbeitung oder zusammen mit anderen Waren, die MB Grid Solutions & Services nicht gehören, weiterveräußert oder wird sie mit einem Grundstück oder mit beweglichen Sachen verbunden, so erklärt der Kunde bereits jetzt die Abtretung der Forderung des Kunden gegen seine Abnehmer in Höhe des zwischen dem Kunden und MB Grid Solutions & Services vereinbarten Kaufpreises für die Vorbehaltsware; MB Grid Solutions & Services nimmt die Abtretung an.
|
||||
|
||||
**9.8** Der Kunde wird ermächtigt, die abgetretenen Forderungen für MB Grid Solutions & Services einzuziehen. MB Grid Solutions & Services bleibt auch weiterhin zur Einziehung der abgetretenen Forderungen berechtigt, wird die abgetretenen Forderungen jedoch nicht selbst einziehen, solange kein Grund nach Ziffer 9.9 vorliegt, der MB Grid Solutions & Services zum Widerruf der Einziehungsermächtigung berechtigt. Zur Abtretung der Forderung an Dritte, einschließlich Forderungsverkauf an Factoring Banken, ist der Kunde nur mit vorheriger schriftlicher Zustimmung von MB Grid Solutions & Services berechtigt. MB Grid Solutions & Services wird die Zustimmung nicht verweigern, sofern eine gleichwertige Sicherheit gestellt wird.
|
||||
|
||||
**9.9** MB Grid Solutions & Services kann die Ermächtigung zur Weiterveräußerung und Einziehung widerrufen, sobald sich der Kunde gegenüber MB Grid Solutions & Services im Zahlungsverzug befindet, oder das Insolvenzverfahren oder ein vergleichbares Verfahren über das Vermögen des Kunden eröffnet oder mangels Masse abgewiesen wurde.
|
||||
|
||||
**9.10** Der Kunde hat die eingezogenen Forderungen binnen zehn (10) Werktagen auf ein von MB Grid Solutions & Services benanntes Konto zu überweisen.
|
||||
|
||||
**9.11** MB Grid Solutions & Services ist zur Offenlegung der Abtretung berechtigt und kann von dem Kunden verlangen, dass dieser die Abtretung seinen Abnehmern anzeigt. Auf Verlangen ist der Kunde verpflichtet, MB Grid Solutions & Services die Namen seiner Abnehmer bekanntzugeben und alle erforderlichen Unterlagen herauszugeben, die für die Einziehung der Forderung durch MB Grid Solutions & Services selbst erforderlich sind. Auch ist der Kunde ab Verzugseintritt verpflichtet, die Abtretung der Forderung an MB Grid Solutions & Services seinem Abnehmer gegenüber schriftlich mitzuteilen.
|
||||
|
||||
**9.12** MB Grid Solutions & Services wird die Vorbehaltsware sowie die an ihre Stelle tretenden Sachen oder Forderungen freigeben, soweit ihr Wert die Höhe der gesicherten Forderungen um mehr als zehn (10) % übersteigt. Die Auswahl der danach freizugebenden Gegenstände liegt bei MB Grid Solutions & Services.
|
||||
|
||||
**9.13** Tritt MB Grid Solutions & Services bei vertragswidrigem Verhalten des Kunden, insbesondere Zahlungsverzug, vom Vertrag zurück (Verwertungsfall), ist MB Grid Solutions & Services berechtigt, die Vorbehaltsware herauszuverlangen. Der Kunde hat in diesem Fall MB Grid Solutions & Services alle Informationen und Unterlagen zu übergeben, die erforderlich sind, um die Vorbehaltsware herauszuverlangen und die an MB Grid Solutions & Services abgetretenen Ansprüche des Kunden gegenüber seinen Abnehmern geltend zu machen, mindestens jedoch eine Aufstellung über die bei dem Kunden oder bei den Abnehmern befindliche Vorbehaltsware, auch soweit sie bereits verarbeitet ist, und eine Aufstellung aller abgetretenen Forderungen nebst Rechnungsgutschriften.
|
||||
|
||||
## 10. Höhere Gewalt
|
||||
|
||||
|
||||
**10.1** „Höhere Gewalt“ bedeutet das Eintreten eines Ereignisses oder Umstands, welcher eine Partei daran hindert, eine oder mehrere ihrer vertraglichen Verpflichtungen zu erfüllen, wenn und soweit die von dem Hindernis betroffene Partei nachweist, dass (a) dieses Hindernis außerhalb der ihr zumutbaren Kontrolle liegt und (b) es zu dem Zeitpunkt des Vertragsschlusses nicht in zumutbarer Weise vorhersehbar war und (c) die Auswirkungen des Hindernisses von der betroffenen Partei nicht in zumutbarer Weise hätte vermieden oder überwunden werden können.
|
||||
|
||||
**10.2** Bei dem Beweis des Gegenteils wird insbesondere, aber nicht abschließend, bei den folgenden Ereignissen ein Fall Höherer Gewalt im Sinne des vorstehenden Absatzes vermutet: Krieg, Terrorakte, Währungs- und Handelsbeschränkungen, Embargo, Sanktionen, Amtshandlungen, Befolgung von Gesetzen oder Regierungsanordnungen, staatliche Ausreise- und Exportverbote oder staatliche Einreise- und Importverbote, Enteignung, Epidemie, Naturkatastrophe, extremes Naturereignis, Explosionen, Feuer, Zerstörung von Ausrüstungen, Demonstrationen oder Versammlungen, Ereignisse, die das Passieren von wichtigen Transportrouten verhindern, allgemeine Arbeitsunruhen, insbesondere Boykott, Streik und Aussperrungen, Energieknappheit oder Beeinträchtigung von Transportmitteln.
|
||||
|
||||
**10.3** Eine Partei, die sich mit Erfolg auf diese Klausel beruft, ist ab dem Zeitpunkt, zu dem das Hindernis ihr die Leistungserbringung unmöglich macht, von ihrer Pflicht zur Erfüllung ihrer vertraglichen Verpflichtungen und von jeder Schadensersatzpflicht oder jedem anderen vertraglichen Rechtsbehelf wegen Vertragsverletzung suspendiert, sofern der Eintritt eines Ereignisses Höherer Gewalt der anderen Partei unverzüglich mitgeteilt wird. Ist die Auswirkung des geltend gemachten Hindernisses oder Ereignisses vorübergehend, so gelten die eben dargelegten Folgen nur so lange, wie das geltend gemachte Hindernis die Vertragserfüllung durch die betroffene Partei verhindert sowie einer angemessenen Anlaufzeit. Dauert das Hindernis oder Ereignis höherer Gewalt bei der betroffenen Partei länger als vier (4) Monate an, ist die andere Partei zur ganzen oder teilweisen außerordentlichen Kündigung bzw. zum Rücktritt vom Vertrag berechtigt. Wechselseitige Schadensersatzansprüche wegen dieser Kündigung bzw. dieses Rücktritts sind ausgeschlossen.
|
||||
|
||||
**10.4** Der Kunde ist verpflichtet, auf Verlangen von MB Grid Solutions & Services innerhalb einer angemessenen Frist zu erklären, ob er wegen einem bei MB Grid Solutions & Services eingetretenen Ereignis Höherer Gewalt den Vertrag kündigt bzw. von diesem Zurücktritt oder weiterhin auf der Durchführung der Lieferung der Ware besteht.
|
||||
|
||||
**10.5** Sofern sich aufgrund eines Ereignisses Höherer Gewalt die wirtschaftliche Bedeutung oder der Inhalt der Lieferung der Ware erheblich verändert oder auf den Betrieb von MB Grid Solutions & Services erheblich einwirkt, werden die Parteien den zugrundeliegenden Vertrag unter Beachtung von Treu und Glauben angemessen anpassen. Soweit dies wirtschaftlich nicht vertretbar ist, steht MB Grid Solutions & Services das Recht zu, von dem Vertrag zurückzutreten. Will MB Grid Solutions & Services von diesem Rücktrittsrecht Gebrauch machen, so hat er dies nach Erkenntnis der Tragweite des Ereignisses Höherer Gewalt unverzüglich dem Kunden mitzuteilen, und zwar auch dann, wenn zunächst mit dem Kunden eine Verlängerung der Lieferzeit vereinbart war.
|
||||
|
||||
## 11. Gewährleistung und Mängelrüge
|
||||
|
||||
|
||||
**11.1** Mängelansprüche bestehen nicht bei nur unerheblicher Abweichung der gelieferten Ware von den vereinbarten Spezifikationen, bei unerheblicher Beeinträchtigung der von MB Grid Solutions & Services vorgegebenen Brauchbarkeit, bei natürlicher Abnutzung, übermäßiger Beanspruchung, Verwendung ungeeigneter Betriebsmittel, oder bei Mängeln, die aufgrund besonderer äußerer Einflüsse entstehen, sowie bei nicht reproduzierbaren Softwarefehlern. Werden vom Kunden oder von Dritten unsachgemäß Änderungen oder Instandsetzungsarbeiten vorgenommen, so bestehen für diese und die daraus entstehenden Folgen ebenfalls keine Mängelansprüche.
|
||||
|
||||
**11.2** Eine Gewährleistungsverpflichtung von MB Grid Solutions & Services besteht nur, wenn der Kunde den ihm obliegenden Untersuchungs- und Rügepflichten fristgerecht nachgekommen ist. Eingehende Waren hat der Kunde unverzüglich auf äußerlich erkennbare Transportschäden und Identität der gelieferten Ware mit der Bestellung zu überprüfen. Äußerlich erkennbare Mängel sind unverzüglich, spätestens jedoch innerhalb von drei (3) Werktagen ab Übergabe, sonstige Mängel innerhalb von fünf (5) Werktagen nach ihrer Entdeckung gegenüber MB Grid Solutions & Services im Rahmen einer Mängelrüge mindestens in Textform geltend zu machen.
|
||||
|
||||
**11.3** Mängelrügen müssen eine genaue Beschreibung der aufgetretenen Mängel, der betroffenen Lieferungen und wenn möglich, auch eine bildhafte Dokumentation des Mangels enthalten. MB Grid Solutions & Services ist berechtigt, mangelhafte Teile auf eigene Kosten zur Übersendung und Untersuchung anzufordern.
|
||||
|
||||
**11.4** Beim Verlegen von Kabeln in Gräben oder Rohren, bzw. in Bauwerken ist eine ständige Sichtkontrolle durch den Kabelverleger vorzunehmen. Der Kabelverleger hat laufend zu überprüfen, ob Auffälligkeiten zu vermerken sind.
|
||||
|
||||
**11.5** MB Grid Solutions & Services ist berechtigt, innerhalb einer angemessenen Frist, anerkannte Mängel nach eigener Wahl durch Beseitigung (Nachbesserung) oder durch Lieferung mangelfreier Ware (Ersatzlieferung) zu beheben. Der Kunde wird MB Grid Solutions & Services bei der Beseitigung von Mängeln in zumutbarem Umfang unterstützen.
|
||||
|
||||
**11.6** Die Kosten der Nacherfüllung und Mangeluntersuchung werden bei berechtigten Beanstandungen von MB Grid Solutions & Services getragen. Dies gilt nicht für Aufwendungen, die dadurch entstehen, dass die gelieferten Waren durch den Kunden an einen anderen als den Ort der gewerblichen Niederlassung des Kunden verbracht worden sind, es sei denn, das Verbringen entspricht dem vertraglich vorgesehenen Verwendungszweck der gelieferten Ware. Satz 1 gilt ebenfalls nicht für Mehraufwendungen, die dadurch entstehen, dass der Kunde die gelieferte Ware ohne Zustimmung von MB Grid Solutions & Services verändert hat.
|
||||
|
||||
**11.7** Hat der Kunde die Ware verbaut, obwohl er positive Kenntnis oder grob fahrlässige Unkenntnis des Mangels hatte, ist MB Grid Solutions & Services nicht dazu verpflichtet, die Ein- und Ausbaukosten der Ware zu erstatten.
|
||||
|
||||
**11.8** Der Kunde kann erst nach zweimaligem Fehlschlagen der Mängelbeseitigung vom Vertrag zurücktreten oder den vereinbarten Kaufpreis angemessen mindern. Bei nur unerheblicher Minderung des Werts oder der Tauglichkeit der gelieferten Waren ist der Rücktritt ausgeschlossen, ebenso wenn der Kunde sich im Annahmeverzug befindet oder wenn der Kunde den Mangel zu vertreten hat. Solange der Kunde MB Grid Solutions & Services gegenüber nicht den Rücktritt vom Vertrag erklärt oder Schadensersatz wegen Nichterfüllung verlangt hat, ist MB Grid Solutions & Services auch nach Ablauf der vom Kunden gesetzten Frist zur Nacherfüllung berechtigt, es sei denn, der Kunde hat zuvor schriftlich die Ablehnung der Nacherfüllung angezeigt.
|
||||
|
||||
**11.9** Beruht ein Mangel auf dem Verschulden von MB Grid Solutions & Services, kann der Kunde unter den in Ziffer 12 bestimmten Voraussetzungen Schadensersatz verlangen. Ziffer 12 gilt entsprechend für den Ersatz vergeblicher Aufwendungen, den der Kunde anstelle eines Schadensersatzes statt der Leistung verlangen kann.
|
||||
|
||||
**11.10** Besteht Uneinigkeit zwischen den Parteien in Bezug auf Mängel an den Waren, ist ein Gutachten bei dem VDE-Institut über die streitige Frage einzuholen. Das Ergebnis des Gutachtens des VDE-Instituts ist für die Feststellung der Mangelfreiheit oder eines Mangels maßgebend. MB Grid Solutions & Services ist nur dann verpflichtet, die Kosten des Gutachtens des VDE-Instituts zu tragen, wenn das Gutachten einen Mangel feststellt.
|
||||
|
||||
**11.11** Die gesetzlichen Mängelgewährleistungsansprüche des Kunden verjähren innerhalb von zwölf (12) Monaten ab Gefahrübergang. §§ 438 Abs. 1 Nr. 2, 479 Abs. 1 und 634a Abs. 1 Nr. 2 BGB bleiben hiervon unberührt. Diese Verkürzung der Verjährungsfrist gilt nicht bei Vorsatz, grober Fahrlässigkeit, der Verletzung von Leben, Körper, Gesundheit, oder jedweder wesentlicher Vertragspflichtverletzung, zwingender gesetzlicher Haftungsnormen wie denen des ProdHaftG, sowie bei Nichteinhaltung einer Beschaffenheitsgarantie oder dem arglistigen Verschweigen eines Mangels. Die gesetzlichen Regelungen über Ablaufhemmung, Hemmung und Neubeginn der Fristen bleiben unberührt.
|
||||
|
||||
## 12. Haftung
|
||||
|
||||
|
||||
**12.1** MB Grid Solutions & Services haftet uneingeschränkt für Vorsatz und grobe Fahrlässigkeit. MB Grid Solutions & Services haftet ferner uneingeschränkt für Schäden aus der Verletzung des Lebens, des Körpers oder der Gesundheit, sowie für arglistiges Verschweigen eines Mangels und Verletzung von garantierten Beschaffenheitsmerkmalen, sowie im Rahmen zwingender gesetzlicher Vorschriften, wie etwa denen des Produkthaftungsgesetzes.
|
||||
|
||||
**12.2** Bei der Verletzung von wesentlichen Vertragspflichten haftet MB Grid Solutions & Services im Übrigen der Höhe nach begrenzt auf den bei Vertragsschluss für MB Grid Solutions & Services typischerweise vorhersehbaren Schaden bzw. die typischerweise vorhersehbaren Aufwendungen. Vertragswesentlich sind die Verpflichtungen, deren Erfüllung die ordnungsgemäße Vertragsdurchführung erst ermöglichen und auf deren Einhaltung der Kunde vertraut und auch vertrauen darf. Im Übrigen haftet MB Grid Solutions & Services bei leichter Fahrlässigkeit nicht auf Schadensersatz.
|
||||
|
||||
**12.3** Eine Haftung für mittelbare Schäden und Folgeschäden, die Folge von Mängeln der gelieferten Waren sind, besteht nur für solche Schäden, die bei bestimmungsgemäßer Verwendung der gelieferten Ware typischerweise zu erwarten sind.
|
||||
|
||||
**12.4** Die Haftung entfällt für Schäden, die auf Reparaturen, Änderungen oder anderen Eingriffen oder auf die Verwendung von Nicht-Originalteilen von MB Grid Solutions & Services im Rahmen der Reparatur oder Änderung bzw. des Eingriffs zurückzuführen sind, die der Kunde ohne Zustimmung von MB Grid Solutions & Services vorgenommen hat.
|
||||
|
||||
**12.5** Soweit die Haftung von MB Grid Solutions & Services ausgeschlossen oder beschränkt ist, gilt dies auch für die Haftung der Angestellten, Arbeitnehmer, Mitarbeiter, Vertreter und Erfüllungsgehilfen von MB Grid Solutions & Services.
|
||||
|
||||
## 13. Produkthaftung
|
||||
|
||||
|
||||
**13.1** Der Kunde wird die gelieferten Waren ausschließlich vertragsgemäß und entsprechend den verfügbaren Anleitungen von MB Grid Solutions & Services verwenden, insbesondere wird er vorhandene Warnhinweise über Gefahren bei Gebrauch der gelieferten Waren nicht verändern oder entfernen. Bei Verletzung dieser Pflicht stellt der Kunde MB Grid Solutions & Services im Innenverhältnis von Produkthaftungsansprüchen Dritter frei, es sei denn der Kunde ist für den die Haftung auslösenden Fehler nicht verantwortlich.
|
||||
|
||||
**13.2** Wird MB Grid Solutions & Services aufgrund eines Produktfehlers zu einem Produktrückruf oder einer -warnung veranlasst, so wird der Kunde nach besten Kräften bei den Maßnahmen mitwirken, die MB Grid Solutions & Services für erforderlich und zweckmäßig hält. MB Grid Solutions & Services ist verpflichtet, die Kosten des Produktrückrufs oder der -warnung zu tragen, es sei denn MB Grid Solutions & Services ist für den Produktfehler und den eingetretenen Schaden nach produkthaftungsrechtlichen Grundsätzen nicht verantwortlich.
|
||||
|
||||
**13.3** Der Kunde wird MB Grid Solutions & Services unverzüglich über ihm bekanntwerdende Risiken bei der Verwendung der gelieferten Ware und mögliche Produktfehler in Textform informieren.
|
||||
|
||||
## 14. Exportkontrolle
|
||||
|
||||
|
||||
**14.1** Lieferungen von Waren von MB Grid Solutions & Services stehen unter dem Vorbehalt, dass der Vertragserfüllung keine Hindernisse aufgrund von nationalen oder internationalen exportkontrollrechtlichen Vorschriften entgegenstehen. Sofern sich die Durchführung des Vertrages aufgrund von Exportkontrollprüfungen oder Genehmigungsverfahren verzögert, werden Fristen und Lieferzeiten außer Kraft gesetzt. MB Grid Solutions & Services ist berechtigt, von der Einzelbestellung zurückzutreten und den Liefervertrag fristlos zu kündigen, wenn ein solcher Rücktritt oder eine Kündigung zur Einhaltung nationaler oder internationalen Exportkontrollbestimmung erforderlich ist. Die Geltendmachung von Schadensersatzansprüchen oder anderen Rechten durch den Kunden wegen vorgenannter Kündigung oder Verzögerung ist ausgeschlossen.
|
||||
|
||||
**14.2** Der Kunde verpflichtet sich zur Einhaltung des nationalen und internationalen Exportkontrollrechts, insbesondere bei Weitergabe der gelieferten Ware an Dritte.
|
||||
|
||||
## 15. Urheberrechte und Datenschutz
|
||||
|
||||
|
||||
**15.1** An Kostenvoranschlägen, Zeichnungen, Modellen, Teilen, Schablonen, Berechnungen, Beschreibungen, Mustern und anderen Unterlagen (im Folgenden: „Unterlagen“ genannt) behält sich MB Grid Solutions & Services seine eigentums- und urheberrechtlichen Verwertungsrechte uneingeschränkt vor. Die Unterlagen dürfen nur nach vorheriger schriftlicher Zustimmung von MB Grid Solutions & Services Dritten zugänglich gemacht werden und sind MB Grid Solutions & Services auf Verlangen unverzüglich zurückzugeben.
|
||||
|
||||
**15.2** Ohne ausdrückliche Zustimmung von MB Grid Solutions & Services in Textform ist der Kunde nicht berechtigt, die gelieferte Ware zu Werbezwecken zu verwenden.
|
||||
|
||||
**15.3** MB Grid Solutions & Services erhebt und verwendet personenbezogenen Daten ausschließlich gemäß den Bestimmungen der Datenschutzgrundverordnung und des Bundesdatenschutzgesetzes.
|
||||
|
||||
**15.4** Personenbezogenen Daten, die für die Begründung, inhaltliche Ausgestaltung, Abwicklung oder Änderung des Vertragsverhältnisses erforderlich sind (Bestandsdaten), werden ausschließlich zur Durchführung des zwischen MB Grid Solutions & Services und dem Kunden abgeschlossenen Vertrages verwendet. Zur Abwicklung des Vertragsverhältnisses müssen diese Daten im erforderlichen Umfang an Dritte (z.B. Transporteure oder Spediteure) weitergegeben werden. Der Kunde erteilt für die vorbeschriebene Verwendung seiner personenbezogenen Daten hiermit eine entsprechende Einwilligung.
|
||||
|
||||
**15.5** Diese Einwilligungserklärung ist jederzeit -ohne Angabe von Gründen- widerruflich. Der Widerruf ist zu richten an info@klz-cables.com .
|
||||
|
||||
**15.6** Eine darüberhinausgehende Nutzung der Bestandsdaten des Kunden für Zwecke der Werbung, der Marktforschung oder zur bedarfsgerechten Gestaltung der Angebote von MB Grid Solutions & Services bedarf der gesonderten ausdrücklichen Einwilligung des Kunden.
|
||||
|
||||
**15.7** Auf Anordnung einer zuständigen Stelle darf MB Grid Solutions & Services im Einzelfall Auskunft über personenbezogene Daten des Kunden im Rahmen der Anordnung an die zuständige Stelle erteilen, soweit dies für Zwecke der Strafverfolgung, zur Gefahrenabwehr, zur Erfüllung der gesetzlichen Aufgaben der Verfassungsschutzbehörden oder zur Durchsetzung der Rechte am geistigen Eigentum erforderlich ist.
|
||||
|
||||
## 16. Geheimhaltung
|
||||
|
||||
|
||||
**16.1** „Vertrauliche Informationen“ sind (i) alle Informationen, Daten, Zeichnungen, Unterlagen, Materialien, Know-how oder sonstige Geschäftsgeheimnisse im Sinne von § 2 Nr. 1 des Gesetzes zum Schutz von Geschäftsgeheimnissen (GeschGehG), insbesondere alle Entwürfe, Skizzen, Zeichnungen, technischen Protokolle, Modelle oder elektronische Daten, gleich in welcher Verkörperung, (ii) sowie alle nicht offenkundigen kaufmännischen, betrieblichen oder technischen Informationen oder Gegenstände, gleich in welcher Verkörperung, die dem Kunden im Rahmen der Geschäftsbeziehung mit MB Grid Solutions & Services offengelegt oder anderweitig bekannt werden, (iii) die entweder direkt als vertraulich gekennzeichnet wurden oder nachträglich schriftlich oder in Textform als vertraulich gekennzeichnet werden. Des Weiteren gelten auch solche Informationen als vertraulich, welche der Natur nach bzw. aus den Umständen heraus oder aus anderen Gründen erkennbar als vertraulich anzusehen sind, soweit die Parteien nichts anderes in Schriftform vereinbart haben.
|
||||
|
||||
**16.2** Vertrauliche Informationen im Sinne von Ziffer 16.1 sind jedoch nicht solche Informationen, (i) die der Kunde von öffentlich zugänglichen Quellen bezogen hat und die allgemein bekannt oder auf legalem Wege zugänglich sind und diese Quellen die Informationen ebenfalls auf legalem Wege erlangt haben, (ii) die der Kunde von einem berechtigten Dritten rechtmäßig erhalten hat, (iii) die der Kunde eigenständig entwickelt hat, (iv) die kraft Gesetzes oder aufgrund gerichtlicher oder behördlicher Anordnung offengelegt wurden oder offenzulegen sind. Der Kunde ist in diesem Fall verpflichtet, MB Grid Solutions & Services vor einer solchen Offenlegung der Informationen unverzüglich zu informieren und zu versuchen, die Offenlegung zu verhindern, soweit dies mit angemessenen und gesetzlich zulässigen Mitteln möglich ist.
|
||||
|
||||
**16.3** Die Darlegungs- und Beweislast für das Vorliegen einer in Ziffer 16.2 benannten Ausnahmetatbestände trägt diejenige Partei, die sich auf das Vorliegen einer oder mehrerer dieser Ausnahmetatbestände beruft.
|
||||
|
||||
**16.4** Der Kunde verpflichtet sich, die Vertraulichen Informationen strikt vertraulich zu behandeln und ausschließlich zur Erfüllung des Vertragszwecks zu verwenden. Die Vertraulichen Informationen dürfen durch den Kunden nur an solche Mitarbeiter oder andere Personen weitergegeben werden, die sie zur Erreichung des Vertragszwecks zwingend erhalten müssen. Sämtliche Mitarbeiter des Kunden, welche Kenntnis von Vertraulichen Informationen erhalten, müssen durch den Kunden ebenfalls zur Geheimhaltung hinsichtlich der Vertraulichen Informationen verpflichtet sein oder werden. Diese Geheimhaltungspflicht soll auch für die Zeit nach einem etwaigen Ausscheiden des Mitarbeiters aus dem Unternehmen des Kunden für weitere fünf (5) Jahre gelten, soweit dies rechtlich möglich ist.
|
||||
|
||||
**16.5** Der Kunde verpflichtet sich, auf erstes Anfordern von MB Grid Solutions & Services und/oder nach Beendigung der Geschäftsbeziehung oder Beendigung des zugrundeliegenden Vertrage, gleich aus welchem Rechtsgrund, alle unter diese Geheimhaltungsvereinbarung fallenden Vertraulichen Informationen und davon angefertigte Kopien an MB Grid Solutions & Services herauszugeben oder sachgemäß zu zerstören bzw. sicher und dauerhaft zu löschen und die vollständige Herausgabe, vollständige sachgemäße Zerstörung oder vollständige sichere und dauerhafte Löschung MB Grid Solutions & Services gegenüber schriftlich zu bestätigen.
|
||||
|
||||
**16.6** Ziffer 16.5 gilt nicht für Vertrauliche Informationen, die zwingenden gesetzlichen Aufbewahrungspflichten unterliegen oder in automatisierten Back-Up Dateien im Rahmen des ordnungsgemäßen Sicherungsprozesses sicher gespeichert wurden. Für diese Vertraulichen Informationen gelten die Bestimmungen dieser Geheimhaltungsvereinbarung bis zu ihrer Herausgabe, sicheren und dauerhaften Löschung oder vollständigen sachgemäßen Zerstörung fort.
|
||||
|
||||
**16.7** Die Geheimhaltungsverpflichtungen gemäß dieser Ziffer 16 gelten für einen Zeitraum von fünf (5) Jahren nach Beendigung des zugrundeliegenden Vertrages, gleich aus welchem Rechtsgrund, fort.
|
||||
|
||||
**16.8** Für jeden Einzelfall eines Verstoßes des Kunden gegen die Pflichten aus dieser Geheimhaltungsvereinbarung ist der Kunde zur Zahlung einer Vertragsstrafe an MB Grid Solutions & Services verpflichtet, deren Höhe in das billige Ermessen von MB Grid Solutions & Services gestellt wird und im Streitfall durch das zuständige Gericht überprüft werden kann. Durch die Geltendmachung der Vertragsstrafe werden weitergehende Schadensersatzansprüche von MB Grid Solutions & Services nicht ausgeschlossen.
|
||||
|
||||
## 17. Technische Beratungsdienstleistungen
|
||||
|
||||
|
||||
**17.1** Die technische Unterstützung ersetzt weder die Fachplanung noch die Ausführungs- oder Prüfverantwortung des beauftragten Ingenieurbüros, Planers oder der ausführenden Fachfirma bzw. verantwortlichen Abteilung. Alle Hinweise, Einschätzungen und Empfehlungen der MB Grid Solutions and Services erfolgen ohne Gewähr und entbinden den jeweiligen Auftragnehmer nicht von seiner eigenen fachlichen Prüfung, Planung und Verantwortung.
|
||||
|
||||
## 18. Schlussbestimmungen
|
||||
|
||||
|
||||
**18.1** Änderungen oder Ergänzungen dieser AVB bedürfen der Textform. Dies gilt auch für die Abänderung des Textformerfordernisses selbst.
|
||||
|
||||
**18.2** Auf Verträge zwischen MB Grid Solutions & Services und dem Kunden, der Auslegung dieser AVB, sowie die gesamten Rechtsbeziehungen zwischen MB Grid Solutions & Services und dem Kunden gilt ausschließlich das Recht der Bundesrepublik Deutschland unter Ausschluss des UN-Kaufrechts und des Kollisionsrechts. Erfüllungsort sowie ausschließlicher Gerichtsstand für alle Streitigkeiten aus oder im Zusammenhang mit der Rechtsbeziehung zwischen MB Grid Solutions & Services und dem Kunden ist Stuttgart.
|
||||
|
||||
**18.3** Sollten einzelne Bestimmungen dieser AVB unwirksam oder undurchführbar sein oder werden, so wird hierdurch die Wirksamkeit aller sonstigen Bestimmungen dieser AVB im Übrigen nicht berührt. Die unwirksame oder undurchführbare Bestimmung wird von den Vertragsparteien einvernehmlich durch eine rechtswirksame Regelung ersetzt, welche dem wirtschaftlichen Erfolg und Zweck der unwirksamen oder undurchführbaren Bestimmung am nächsten kommt. Dies gilt gleichermaßen für den Fall einer Regelungslücke.
|
||||
15
content/de/datenschutz.mdx
Normal file
15
content/de/datenschutz.mdx
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: Datenschutzerklärung
|
||||
---
|
||||
|
||||
# 1. Datenschutz auf einen Blick
|
||||
Wir nehmen den Schutz Ihrer persönlichen Daten sehr ernst. Wir behandeln Ihre personenbezogenen Daten vertraulich und entsprechend der gesetzlichen Datenschutzvorschriften sowie dieser Datenschutzerklärung.
|
||||
|
||||
# 2. Hosting
|
||||
Unsere Website wird bei Hetzner Online GmbH gehostet. Der Serverstandort ist Deutschland. Wir haben einen Vertrag über Auftragsverarbeitung (AVV) mit Hetzner geschlossen.
|
||||
|
||||
# 3. Kontaktformular
|
||||
Wenn Sie uns per Kontaktformular Anfragen zukommen lassen, werden Ihre Angaben aus dem Anfrageformular inklusive der von Ihnen dort angegebenen Kontaktdaten zwecks Bearbeitung der Anfrage und für den Fall von Anschlussfragen bei uns gespeichert. Diese Daten geben wir nicht ohne Ihre Einwilligung weiter.
|
||||
|
||||
# 4. Server-Log-Dateien
|
||||
Der Provider der Seiten erhebt und speichert automatisch Informationen in sogenannten Server-Log-Dateien, die Ihr Browser automatisch an uns übermittelt. Dies sind: Browsertyp und Browserversion, verwendetes Betriebssystem, Referrer URL, Hostname des zugreifenden Rechners, Uhrzeit der Serveranfrage, IP-Adresse.
|
||||
24
content/de/impressum.mdx
Normal file
24
content/de/impressum.mdx
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: Impressum
|
||||
---
|
||||
|
||||
# Angaben gemäß § 5 TMG
|
||||
MB Grid Solutions & Services GmbH
|
||||
Raiffeisenstraße 22
|
||||
73630 Remshalden
|
||||
|
||||
# Vertreten durch
|
||||
Michael Bodemer
|
||||
Klaus Mintel
|
||||
|
||||
# Kontakt
|
||||
E-Mail: [info@mb-grid-solutions.com](mailto:info@mb-grid-solutions.com)
|
||||
Web: [www.mb-grid-solutions.com](https://www.mb-grid-solutions.com)
|
||||
|
||||
# Registereintrag
|
||||
Eintragung im Handelsregister.
|
||||
Registergericht: Amtsgericht Stuttgart
|
||||
Registernummer: HRB 803379
|
||||
|
||||
# Urheberrecht
|
||||
Alle auf der Website veröffentlichten Texte, Bilder und sonstigen Informationen unterliegen – sofern nicht anders gekennzeichnet – dem Urheberrecht. Jede Vervielfältigung, Verbreitung, Speicherung, Übermittlung, Wiedergabe bzw. Weitergabe der Inhalte ohne schriftliche Genehmigung ist ausdrücklich untersagt.
|
||||
62
content/de/index.mdx
Normal file
62
content/de/index.mdx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Hero } from "@/components/sections/Hero";
|
||||
import { PortfolioSection } from "@/components/sections/PortfolioSection";
|
||||
import { ExpertiseSection } from "@/components/sections/ExpertiseSection";
|
||||
import { TechnicalSpecsSection } from "@/components/sections/TechnicalSpecsSection";
|
||||
import { CTASection } from "@/components/sections/CTASection";
|
||||
|
||||
<Hero
|
||||
tag="Technische Beratung"
|
||||
title={<>Spezialisierter Partner für <span className="text-accent">Energiekabelprojekte</span></>}
|
||||
subtitle="Herstellerneutrale technische Beratung für Ihre Projekte in Mittel- und Hochspannungsnetzen bis zu 110 kV."
|
||||
ctaPrimary="Projekt anfragen"
|
||||
ctaSecondary="Mehr erfahren"
|
||||
/>
|
||||
|
||||
<PortfolioSection
|
||||
tag="Portfolio"
|
||||
title="Unsere Leistungen"
|
||||
description="Beratung durch unabhängige Experten mit jahrzehntelanger Erfahrung aus Engineering, Normengremien, Planung und Produktion."
|
||||
linkText="Alle Details ansehen"
|
||||
/>
|
||||
|
||||
<ExpertiseSection
|
||||
tag="Expertise"
|
||||
title="Anwendungen & Zielgruppen"
|
||||
description="Wir unterstützen Sie bei der Realisierung Ihrer Kabelprojekte."
|
||||
groups={[
|
||||
"Energieversorger",
|
||||
"Ingenieurbüros",
|
||||
"Tiefbauunternehmen",
|
||||
"Industrie",
|
||||
"Projektierer EE",
|
||||
"Planungsbüros"
|
||||
]}
|
||||
/>
|
||||
|
||||
<TechnicalSpecsSection
|
||||
tag="Spezifikationen"
|
||||
title="Technische Expertise"
|
||||
items={[
|
||||
{
|
||||
label: "Kabeltypen",
|
||||
value: "N2XS(FL)2Y, N2X(F)KLD2Y...",
|
||||
desc: "Umfassende Expertise im Design gängiger Hochspannungskabel."
|
||||
},
|
||||
{
|
||||
label: "Spannungsebenen",
|
||||
value: "64/110 kV & Mittelspannung",
|
||||
desc: "Spezialisierte Beratung für komplexe Infrastrukturprojekte."
|
||||
},
|
||||
{
|
||||
label: "Leitertechnologie",
|
||||
value: "Massiv-, Mehrdraht- & Milliken",
|
||||
desc: "Optimierung des Leiterdesigns hinsichtlich Stromtragfähigkeit."
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<CTASection
|
||||
title="Bereit für Ihr nächstes Projekt?"
|
||||
subtitle="Lassen Sie uns gemeinsam die optimale Lösung für Ihre Energieinfrastruktur finden. Wir beraten Sie herstellerneutral und kompetent."
|
||||
buttonText="Jetzt Kontakt aufnehmen"
|
||||
/>
|
||||
67
content/en/about.mdx
Normal file
67
content/en/about.mdx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { AboutHero } from "@/components/sections/AboutHero";
|
||||
import { AboutIntro } from "@/components/sections/AboutIntro";
|
||||
import { ManifestSection } from "@/components/sections/ManifestSection";
|
||||
import { CTASection } from "@/components/sections/CTASection";
|
||||
|
||||
<AboutHero
|
||||
tagline="About us"
|
||||
title={<>Reliable support for your <span className="text-accent">Grid Infrastructure</span></>}
|
||||
subtitle="Manufacturer-neutral consulting in power cable technology. We see ourselves as your technical pilot."
|
||||
/>
|
||||
|
||||
<AboutIntro
|
||||
paragraphs={[
|
||||
"Our roots lie in the deep practical experience of our technical consultants and our networks in the global cable market. We combine tradition with state-of-the-art innovation to realize reliable energy solutions for projects up to 110 kV.",
|
||||
"We understand the challenges of the energy transition and offer manufacturer-neutral consulting based on facts, standards, and decades of experience."
|
||||
]}
|
||||
team={[
|
||||
{
|
||||
name: "Michael Bodemer",
|
||||
role: "Managing Director & Owner",
|
||||
linkedin: "https://www.linkedin.com/in/michael-bodemer-33b493122/"
|
||||
},
|
||||
{
|
||||
name: "Klaus Mintel",
|
||||
role: "Managing Director",
|
||||
linkedin: "https://www.linkedin.com/in/klaus-mintel-b80a8b193/"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<ManifestSection
|
||||
tagline="Values"
|
||||
title="Our Manifest"
|
||||
subtitle="Values that guide our daily work and ensure the success of your projects."
|
||||
items={[
|
||||
{
|
||||
title: "Competence",
|
||||
desc: "Decades of experience combined with Europe-wide know-how in state-of-the-art facilities."
|
||||
},
|
||||
{
|
||||
title: "Availability",
|
||||
desc: "Fast and reliable support without unnecessary delays."
|
||||
},
|
||||
{
|
||||
title: "Solutions",
|
||||
desc: "We ask the right questions to find the technically and economically best solution."
|
||||
},
|
||||
{
|
||||
title: "Logistics",
|
||||
desc: "From manufacturing monitoring to timely freight tracking."
|
||||
},
|
||||
{
|
||||
title: "Openness",
|
||||
desc: "We listen and continuously adapt our processes to your success."
|
||||
},
|
||||
{
|
||||
title: "Reliability",
|
||||
desc: "We keep what we promise – without exception. Commitment is our foundation."
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<CTASection
|
||||
title="Ready for your next project?"
|
||||
subtitle="Let's find the optimal solution for your energy infrastructure together. We advise you manufacturer-neutrally and competently."
|
||||
buttonText="Contact us now"
|
||||
/>
|
||||
62
content/en/index.mdx
Normal file
62
content/en/index.mdx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Hero } from "@/components/sections/Hero";
|
||||
import { PortfolioSection } from "@/components/sections/PortfolioSection";
|
||||
import { ExpertiseSection } from "@/components/sections/ExpertiseSection";
|
||||
import { TechnicalSpecsSection } from "@/components/sections/TechnicalSpecsSection";
|
||||
import { CTASection } from "@/components/sections/CTASection";
|
||||
|
||||
<Hero
|
||||
tag="Technical Consulting"
|
||||
title={<>Specialized Partner for <span className="text-accent">Power Cable Projects</span></>}
|
||||
subtitle="Manufacturer-neutral technical consulting for your projects in medium and high voltage networks up to 110 kV."
|
||||
ctaPrimary="Inquire Project"
|
||||
ctaSecondary="Learn More"
|
||||
/>
|
||||
|
||||
<PortfolioSection
|
||||
tag="Portfolio"
|
||||
title="Our Services"
|
||||
description="Consulting by independent experts with decades of experience in engineering, standards committees, planning, and production."
|
||||
linkText="View all details"
|
||||
/>
|
||||
|
||||
<ExpertiseSection
|
||||
tag="Expertise"
|
||||
title="Applications & Target Groups"
|
||||
description="We support you in the realization of your cable projects."
|
||||
groups={[
|
||||
"Energy Suppliers",
|
||||
"Engineering Offices",
|
||||
"Civil Engineering Companies",
|
||||
"Industry",
|
||||
"RE Developers",
|
||||
"Planning Offices"
|
||||
]}
|
||||
/>
|
||||
|
||||
<TechnicalSpecsSection
|
||||
tag="Specifications"
|
||||
title="Technical Expertise"
|
||||
items={[
|
||||
{
|
||||
label: "Cable Types",
|
||||
value: "N2XS(FL)2Y, N2X(F)KLD2Y...",
|
||||
desc: "Comprehensive expertise in the design of common high voltage cables."
|
||||
},
|
||||
{
|
||||
label: "Voltage Levels",
|
||||
value: "64/110 kV & Medium Voltage",
|
||||
desc: "Specialized consulting for complex infrastructure projects."
|
||||
},
|
||||
{
|
||||
label: "Conductor Technology",
|
||||
value: "Solid, Multi-wire & Milliken",
|
||||
desc: "Optimization of conductor design regarding current carrying capacity."
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<CTASection
|
||||
title="Ready for your next project?"
|
||||
subtitle="Let's find the optimal solution for your energy infrastructure together. We advise you manufacturer-neutrally and competently."
|
||||
buttonText="Contact us now"
|
||||
/>
|
||||
@@ -1,27 +1,31 @@
|
||||
services:
|
||||
app:
|
||||
image: node:20-alpine
|
||||
mb-grid-app:
|
||||
image: node:20
|
||||
working_dir: /app
|
||||
# Use pnpm since the project uses it, and run the next dev script directly
|
||||
command: sh -c "corepack enable pnpm && pnpm i && pnpm dev:next"
|
||||
volumes:
|
||||
- .:/app
|
||||
- mb-grid-node-modules:/app/node_modules
|
||||
ports:
|
||||
- "3000:3000"
|
||||
command: sh -c "corepack enable pnpm && pnpm install && pnpm dev:next"
|
||||
environment:
|
||||
NODE_ENV: development
|
||||
DATABASE_URI: postgresql://postgres:postgres@mb-grid-db:5432/payload
|
||||
# Build / dependency installation
|
||||
GATEKEEPER_ORIGIN: ${GATEKEEPER_ORIGIN}
|
||||
COOKIE_DOMAIN: ${COOKIE_DOMAIN}
|
||||
NEXT_PUBLIC_BASE_URL: ${NEXT_PUBLIC_BASE_URL}
|
||||
NPM_TOKEN: ${NPM_TOKEN}
|
||||
CI: 'true'
|
||||
# ports:
|
||||
# - "3000:3000"
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
# Clear all production-related TLS/Middleware settings for the main routers
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mb-grid-solutions}.entrypoints=web"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mb-grid-solutions}.rule=Host(`${TRAEFIK_HOST:-mb-grid-solutions.localhost}`)"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mb-grid-solutions}.tls=false"
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mb-grid-solutions}.middlewares="
|
||||
# Remove Gatekeeper for local dev simply by not defining it or overwriting?
|
||||
# Actually, gatekeeper is a separate service. We can keep it or ignore it.
|
||||
# But the app router normally points to gatekeeper middleware.
|
||||
# By clearing middlewares above, we bypass gatekeeper for local dev.
|
||||
- "caddy=http://mb-grid-solutions.localhost"
|
||||
- "caddy.reverse_proxy=mb-grid-app:3000"
|
||||
networks:
|
||||
- default
|
||||
- infra
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: mb-grid-solutions-internal
|
||||
infra:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
mb-grid-node-modules:
|
||||
|
||||
@@ -81,19 +81,7 @@ services:
|
||||
- "traefik.http.routers.${PROJECT_NAME:-mb-grid}-gatekeeper.service=${PROJECT_NAME:-mb-grid}-gatekeeper-svc"
|
||||
- "traefik.docker.network=infra"
|
||||
|
||||
mb-grid-db:
|
||||
image: postgres:15-alpine
|
||||
restart: always
|
||||
networks:
|
||||
- default
|
||||
env_file:
|
||||
- ${ENV_FILE:-.env}
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-payload}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
||||
volumes:
|
||||
- mb-grid-db-data:/var/lib/postgresql/data
|
||||
|
||||
|
||||
networks:
|
||||
default:
|
||||
@@ -101,5 +89,4 @@ networks:
|
||||
infra:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
mb-grid-db-data:
|
||||
|
||||
|
||||
10
lib/env.ts
10
lib/env.ts
@@ -24,14 +24,20 @@ const envExtension = {
|
||||
/**
|
||||
* Full schema including Mintel base and refinements
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
export const envSchema = withMintelRefinements(
|
||||
z.object(mintelEnvSchema).extend(envExtension),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(z as any).object(mintelEnvSchema).extend(envExtension) as any,
|
||||
);
|
||||
|
||||
/**
|
||||
* Validated environment object.
|
||||
*/
|
||||
export const env = validateMintelEnv(envExtension);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const env = validateMintelEnv(envExtension) as any;
|
||||
|
||||
/**
|
||||
* For legacy compatibility with existing code.
|
||||
|
||||
7
mdx-components.tsx
Normal file
7
mdx-components.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { MDXComponents } from "mdx/types";
|
||||
|
||||
export function useMDXComponents(components: MDXComponents): MDXComponents {
|
||||
return {
|
||||
...components,
|
||||
};
|
||||
}
|
||||
@@ -148,7 +148,8 @@
|
||||
"info": {
|
||||
"email": "E-Mail",
|
||||
"address": "Anschrift",
|
||||
"company": "MB Grid Solutions & Services GmbH"
|
||||
"company": "MB Grid Solutions & Services GmbH",
|
||||
"mapTitle": "Standort Karte"
|
||||
},
|
||||
"form": {
|
||||
"name": "Name *",
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
import createMiddleware from "next-intl/middleware";
|
||||
|
||||
const intlMiddleware = createMiddleware({
|
||||
export default createMiddleware({
|
||||
// A list of all locales that are supported
|
||||
locales: ["de"],
|
||||
|
||||
@@ -13,23 +11,8 @@ const intlMiddleware = createMiddleware({
|
||||
localePrefix: "as-needed",
|
||||
});
|
||||
|
||||
export default function middleware(request: NextRequest) {
|
||||
const { pathname } = request.nextUrl;
|
||||
|
||||
// Explicit bypass for analytics and error tracking paths
|
||||
if (pathname.startsWith("/stats") || pathname.startsWith("/errors")) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
return intlMiddleware(request);
|
||||
}
|
||||
|
||||
export const config = {
|
||||
// Matcher for all pages and internationalized pathnames
|
||||
// excluding api, _next, static files, etc.
|
||||
matcher: [
|
||||
"/((?!api|admin|stats|errors|_next|_vercel|.*\\..*).*)",
|
||||
"/",
|
||||
"/(de)/:path*",
|
||||
],
|
||||
matcher: ["/((?!api|_next|.*\\..*).*)"],
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,183 +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 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 'mb-grid-solutions/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
|
||||
);
|
||||
|
||||
CREATE TABLE "form_submissions" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"name" varchar NOT NULL,
|
||||
"email" varchar NOT NULL,
|
||||
"company" varchar,
|
||||
"message" varchar NOT NULL,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "pages" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"title" varchar NOT NULL,
|
||||
"slug" varchar NOT NULL,
|
||||
"content" jsonb 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 "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,
|
||||
"form_submissions_id" integer,
|
||||
"pages_id" integer
|
||||
);
|
||||
|
||||
CREATE TABLE "payload_preferences" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"key" varchar,
|
||||
"value" jsonb,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "payload_preferences_rels" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"order" integer,
|
||||
"parent_id" integer NOT NULL,
|
||||
"path" varchar NOT NULL,
|
||||
"users_id" integer
|
||||
);
|
||||
|
||||
CREATE TABLE "payload_migrations" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"name" varchar,
|
||||
"batch" numeric,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE "users_sessions" ADD CONSTRAINT "users_sessions_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "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_form_submissions_fk" FOREIGN KEY ("form_submissions_id") REFERENCES "public"."form_submissions"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_pages_fk" FOREIGN KEY ("pages_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."payload_preferences"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_users_fk" FOREIGN KEY ("users_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
CREATE INDEX "users_sessions_order_idx" ON "users_sessions" USING btree ("_order");
|
||||
CREATE INDEX "users_sessions_parent_id_idx" ON "users_sessions" USING btree ("_parent_id");
|
||||
CREATE INDEX "users_updated_at_idx" ON "users" USING btree ("updated_at");
|
||||
CREATE INDEX "users_created_at_idx" ON "users" USING btree ("created_at");
|
||||
CREATE UNIQUE INDEX "users_email_idx" ON "users" USING btree ("email");
|
||||
CREATE INDEX "media_updated_at_idx" ON "media" USING btree ("updated_at");
|
||||
CREATE INDEX "media_created_at_idx" ON "media" USING btree ("created_at");
|
||||
CREATE UNIQUE INDEX "media_filename_idx" ON "media" USING btree ("filename");
|
||||
CREATE INDEX "media_sizes_thumbnail_sizes_thumbnail_filename_idx" ON "media" USING btree ("sizes_thumbnail_filename");
|
||||
CREATE INDEX "media_sizes_card_sizes_card_filename_idx" ON "media" USING btree ("sizes_card_filename");
|
||||
CREATE INDEX "form_submissions_updated_at_idx" ON "form_submissions" USING btree ("updated_at");
|
||||
CREATE INDEX "form_submissions_created_at_idx" ON "form_submissions" USING btree ("created_at");
|
||||
CREATE INDEX "pages_updated_at_idx" ON "pages" USING btree ("updated_at");
|
||||
CREATE INDEX "pages_created_at_idx" ON "pages" 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_form_submissions_id_idx" ON "payload_locked_documents_rels" USING btree ("form_submissions_id");
|
||||
CREATE INDEX "payload_locked_documents_rels_pages_id_idx" ON "payload_locked_documents_rels" USING btree ("pages_id");
|
||||
CREATE INDEX "payload_preferences_key_idx" ON "payload_preferences" USING btree ("key");
|
||||
CREATE INDEX "payload_preferences_updated_at_idx" ON "payload_preferences" USING btree ("updated_at");
|
||||
CREATE INDEX "payload_preferences_created_at_idx" ON "payload_preferences" USING btree ("created_at");
|
||||
CREATE INDEX "payload_preferences_rels_order_idx" ON "payload_preferences_rels" USING btree ("order");
|
||||
CREATE INDEX "payload_preferences_rels_parent_idx" ON "payload_preferences_rels" USING btree ("parent_id");
|
||||
CREATE INDEX "payload_preferences_rels_path_idx" ON "payload_preferences_rels" USING btree ("path");
|
||||
CREATE INDEX "payload_preferences_rels_users_id_idx" ON "payload_preferences_rels" USING btree ("users_id");
|
||||
CREATE INDEX "payload_migrations_updated_at_idx" ON "payload_migrations" USING btree ("updated_at");
|
||||
CREATE INDEX "payload_migrations_created_at_idx" ON "payload_migrations" USING btree ("created_at");`);
|
||||
}
|
||||
|
||||
export async function down({
|
||||
db,
|
||||
payload,
|
||||
req,
|
||||
}: MigrateDownArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
DROP TABLE "users_sessions" CASCADE;
|
||||
DROP TABLE "users" CASCADE;
|
||||
DROP TABLE "media" CASCADE;
|
||||
DROP TABLE "form_submissions" CASCADE;
|
||||
DROP TABLE "pages" 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;`);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import * as migration_20260227_113637_v1_initial from "./20260227_113637_v1_initial";
|
||||
|
||||
export const migrations = [
|
||||
{
|
||||
up: migration_20260227_113637_v1_initial.up,
|
||||
down: migration_20260227_113637_v1_initial.down,
|
||||
name: "20260227_113637_v1_initial",
|
||||
},
|
||||
];
|
||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { withPayload } from "@payloadcms/next/withPayload";
|
||||
import withMintelConfig from "@mintel/next-config";
|
||||
import createMDX from "@next/mdx";
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"],
|
||||
async rewrites() {
|
||||
const umamiUrl =
|
||||
process.env.UMAMI_API_ENDPOINT ||
|
||||
@@ -34,4 +35,6 @@ const nextConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
export default withPayload(withMintelConfig(nextConfig));
|
||||
const withMDX = createMDX({});
|
||||
|
||||
export default withMintelConfig(withMDX(nextConfig));
|
||||
|
||||
56
package.json
56
package.json
@@ -1,18 +1,16 @@
|
||||
{
|
||||
"name": "mb-grid-solutions.com",
|
||||
"version": "1.3.19",
|
||||
"version": "1.4.0",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.18.3",
|
||||
"scripts": {
|
||||
"dev": "docker network create infra 2>/dev/null || true && echo '\\n🚀 Development Environment Starting...\\n\\n📱 App: http://mb-grid-solutions.localhost\\n🗄️ CMS: http://mb-grid-solutions.localhost/admin\\n🚦 Traefik: http://localhost:8080\\n\\n(Press Ctrl+C to stop)\\n' && docker compose down --remove-orphans && docker compose up app mb-grid-db",
|
||||
"dev": "docker network create infra 2>/dev/null || true && echo '\\n🚀 Development Environment Starting...\\n\\n📱 App: http://mb-grid-solutions.localhost\\n🚦 Traefik: http://localhost:8080\\n\\n(Press Ctrl+C to stop)\\n' && docker compose down --remove-orphans && docker compose up mb-grid-app",
|
||||
"dev:next": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint app components lib scripts",
|
||||
"test": "vitest",
|
||||
"prepare": "husky",
|
||||
"generate:types": "payload generate:types",
|
||||
"generate:importmap": "payload generate:importmap",
|
||||
"pagespeed:test": "mintel pagespeed test",
|
||||
"check:http": "tsx ./scripts/check-http.ts",
|
||||
"check:apis": "tsx ./scripts/check-apis.ts",
|
||||
@@ -28,39 +26,32 @@
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.999.0",
|
||||
"@mdx-js/loader": "^3.1.1",
|
||||
"@mdx-js/react": "^3.1.1",
|
||||
"@mintel/mail": "^1.8.21",
|
||||
"@mintel/next-config": "^1.8.20",
|
||||
"@mintel/next-utils": "^1.8.20",
|
||||
"@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-email/components": "^1.0.8",
|
||||
"@mintel/next-config": "^1.8.21",
|
||||
"@mintel/next-utils": "^1.8.21",
|
||||
"@next/mdx": "^16.2.4",
|
||||
"@sentry/nextjs": "^10.38.0",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"framer-motion": "^12.29.2",
|
||||
"graphql": "^16.13.0",
|
||||
"lucide-react": "^0.562.0",
|
||||
"next": "^16.1.6",
|
||||
"next-intl": "^4.8.2",
|
||||
"nodemailer": "^7.0.12",
|
||||
"payload": "^3.77.0",
|
||||
"pino": "^10.3.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-email": "^5.2.8",
|
||||
"sharp": "^0.34.5",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^20.4.0",
|
||||
"@commitlint/config-conventional": "^20.4.0",
|
||||
"@mintel/cli": "^1.8.20",
|
||||
"@mintel/eslint-config": "^1.8.20",
|
||||
"@mintel/husky-config": "^1.8.20",
|
||||
"@mintel/tsconfig": "^1.8.20",
|
||||
"@mintel/cli": "^1.8.21",
|
||||
"@mintel/eslint-config": "^1.8.21",
|
||||
"@mintel/husky-config": "^1.8.21",
|
||||
"@mintel/tsconfig": "^1.8.21",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
@@ -69,26 +60,37 @@
|
||||
"@types/react": "^19.2.10",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"axios": "^1.13.5",
|
||||
"autoprefixer": "^10.4.24",
|
||||
"axios": "^1.13.6",
|
||||
"cheerio": "^1.2.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-next": "15.1.6",
|
||||
"happy-dom": "^20.6.1",
|
||||
"html-validate": "^10.8.0",
|
||||
"html-validate": "^10.13.1",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^27.4.0",
|
||||
"lint-staged": "^16.2.7",
|
||||
"pa11y-ci": "^4.0.1",
|
||||
"pa11y-ci": "^4.1.0",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.5.0",
|
||||
"puppeteer": "^24.37.3",
|
||||
"prettier": "^3.8.1",
|
||||
"puppeteer": "^24.42.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"puppeteer",
|
||||
"sharp",
|
||||
"esbuild",
|
||||
"@parcel/watcher",
|
||||
"@swc/core",
|
||||
"@sentry/cli",
|
||||
"unrs-resolver"
|
||||
]
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "ssh://git@git.infra.mintel.me:2222/mmintel/mb-grid-solutions.com.git"
|
||||
|
||||
4540
pnpm-lock.yaml
generated
4540
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,53 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
# Payload CMS Database Backup
|
||||
# Creates a timestamped pg_dump of the Payload Postgres database.
|
||||
# Usage: npm run backup:db
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
set -euo pipefail
|
||||
|
||||
# Load environment variables
|
||||
if [ -f .env ]; then
|
||||
set -a; source .env; set +a
|
||||
fi
|
||||
|
||||
# Fallback for local development if not in .env
|
||||
DB_NAME="${POSTGRES_DB:-payload}"
|
||||
DB_USER="${POSTGRES_USER:-postgres}"
|
||||
# For production, we need the container name.
|
||||
# We'll use the PROJECT_NAME to find it if possible, otherwise use a default.
|
||||
PROJECT_NAME="${PROJECT_NAME:-mb-grid-solutions-production}"
|
||||
DB_CONTAINER="${DB_CONTAINER:-${PROJECT_NAME}-mb-grid-db-1}"
|
||||
BACKUP_DIR="./backups"
|
||||
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||
BACKUP_FILE="${BACKUP_DIR}/payload_${TIMESTAMP}.sql.gz"
|
||||
|
||||
# Ensure backup directory exists
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# Check if container is running
|
||||
if ! docker ps --format '{{.Names}}' | grep -q "$DB_CONTAINER"; then
|
||||
echo "❌ Database container '$DB_CONTAINER' is not running."
|
||||
echo " Check your docker-compose status."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📦 Backing up Payload database..."
|
||||
echo " Container: $DB_CONTAINER"
|
||||
echo " Database: $DB_NAME"
|
||||
echo " Output: $BACKUP_FILE"
|
||||
|
||||
# Run pg_dump inside the container and compress
|
||||
# We use directus as user for now if we haven't fully switched to postgres user in all environments
|
||||
# But the script should be consistent with the environment.
|
||||
docker exec "$DB_CONTAINER" pg_dump -U "$DB_USER" -d "$DB_NAME" --clean --if-exists | gzip > "$BACKUP_FILE"
|
||||
|
||||
# Show result
|
||||
SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
|
||||
echo ""
|
||||
echo "✅ Backup complete: $BACKUP_FILE ($SIZE)"
|
||||
echo ""
|
||||
|
||||
# Show existing backups
|
||||
echo "📋 Available backups:"
|
||||
ls -lh "$BACKUP_DIR"/*.sql.gz 2>/dev/null | awk '{print " " $NF " (" $5 ")"}'
|
||||
@@ -1,38 +1,40 @@
|
||||
import puppeteer, { HTTPResponse } from 'puppeteer';
|
||||
import axios from 'axios';
|
||||
import * as cheerio from 'cheerio';
|
||||
import puppeteer, { HTTPResponse } from "puppeteer";
|
||||
import axios from "axios";
|
||||
import * as cheerio from "cheerio";
|
||||
|
||||
const targetUrl =
|
||||
process.argv.find((arg) => !arg.startsWith('--') && arg.startsWith('http')) ||
|
||||
process.argv.find((arg) => !arg.startsWith("--") && arg.startsWith("http")) ||
|
||||
process.env.NEXT_PUBLIC_BASE_URL ||
|
||||
'http://localhost:3000';
|
||||
const limit = process.env.ASSET_CHECK_LIMIT ? parseInt(process.env.ASSET_CHECK_LIMIT) : 20;
|
||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'mbgrid';
|
||||
"http://localhost:3000";
|
||||
const limit = process.env.ASSET_CHECK_LIMIT
|
||||
? parseInt(process.env.ASSET_CHECK_LIMIT)
|
||||
: 20;
|
||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || "mbgrid";
|
||||
|
||||
async function main() {
|
||||
console.log(`\n🚀 Starting Strict Asset Integrity Check for: ${targetUrl}`);
|
||||
console.log(`📊 Limit: ${limit} pages\n`);
|
||||
|
||||
// 1. Fetch Sitemap to discover all routes
|
||||
const sitemapUrl = `${targetUrl.replace(/\/$/, '')}/sitemap.xml`;
|
||||
const sitemapUrl = `${targetUrl.replace(/\/$/, "")}/sitemap.xml`;
|
||||
let urls: string[] = [];
|
||||
|
||||
try {
|
||||
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
|
||||
const response = await axios.get(sitemapUrl, {
|
||||
headers: { Cookie: `mb_gatekeeper_session=${gatekeeperPassword}` },
|
||||
headers: { Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}` },
|
||||
});
|
||||
|
||||
const $ = cheerio.load(response.data, { xmlMode: true });
|
||||
urls = $('url loc')
|
||||
urls = $("url loc")
|
||||
.map((i, el) => $(el).text())
|
||||
.get();
|
||||
|
||||
// Normalize to target URL instance
|
||||
const urlPattern = /https?:\/\/[^\/]+/;
|
||||
urls = [...new Set(urls)]
|
||||
.filter((u) => u.startsWith('http'))
|
||||
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, '')))
|
||||
.filter((u) => u.startsWith("http"))
|
||||
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, "")))
|
||||
.sort();
|
||||
|
||||
console.log(`✅ Found ${urls.length} target URLs.`);
|
||||
@@ -42,10 +44,16 @@ async function main() {
|
||||
`⚠️ Too many pages (${urls.length}). Limiting to ${limit} representative pages.`,
|
||||
);
|
||||
// Simplify selection: home pages + a slice of the rest
|
||||
const homeDE = urls.filter((u) => u.endsWith('/de') || u === targetUrl);
|
||||
const homeEN = urls.filter((u) => u.endsWith('/en'));
|
||||
const others = urls.filter((u) => !homeEN.includes(u) && !homeDE.includes(u));
|
||||
urls = [...homeDE, ...homeEN, ...others.slice(0, limit - (homeEN.length + homeDE.length))];
|
||||
const homeDE = urls.filter((u) => u.endsWith("/de") || u === targetUrl);
|
||||
const homeEN = urls.filter((u) => u.endsWith("/en"));
|
||||
const others = urls.filter(
|
||||
(u) => !homeEN.includes(u) && !homeDE.includes(u),
|
||||
);
|
||||
urls = [
|
||||
...homeDE,
|
||||
...homeEN,
|
||||
...others.slice(0, limit - (homeEN.length + homeDE.length)),
|
||||
];
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const errorBody = err instanceof Error ? err.message : String(err);
|
||||
@@ -57,32 +65,44 @@ async function main() {
|
||||
console.log(`\n🕷️ Launching Puppeteer Headless Engine...`);
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || process.env.CHROME_PATH || undefined,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
|
||||
executablePath:
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH ||
|
||||
process.env.CHROME_PATH ||
|
||||
undefined,
|
||||
args: [
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Inject Gatekeeper session bypassing auth screens
|
||||
await page.setCookie({
|
||||
name: 'mb_gatekeeper_session',
|
||||
name: "mintel_gatekeeper_session",
|
||||
value: gatekeeperPassword,
|
||||
domain: new URL(targetUrl).hostname,
|
||||
path: '/',
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secure: targetUrl.startsWith('https://'),
|
||||
secure: targetUrl.startsWith("https://"),
|
||||
});
|
||||
|
||||
let hasBrokenAssets = false;
|
||||
let hasConsoleErrors = false;
|
||||
const brokenAssetsList: Array<{ url: string; status: number; page: string }> = [];
|
||||
const consoleErrorsList: Array<{ type: string; error: string; page: string }> = [];
|
||||
const brokenAssetsList: Array<{ url: string; status: number; page: string }> =
|
||||
[];
|
||||
const consoleErrorsList: Array<{
|
||||
type: string;
|
||||
error: string;
|
||||
page: string;
|
||||
}> = [];
|
||||
|
||||
// Listen for unhandled exceptions natively in the page
|
||||
page.on('pageerror', (err: unknown) => {
|
||||
page.on("pageerror", (err: unknown) => {
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
consoleErrorsList.push({
|
||||
type: 'PAGE_ERROR',
|
||||
type: "PAGE_ERROR",
|
||||
error: errorMessage,
|
||||
page: page.url(),
|
||||
});
|
||||
@@ -90,18 +110,19 @@ async function main() {
|
||||
});
|
||||
|
||||
// Listen for console.error and console.warn messages (like Next.js Image warnings, hydration errors, CSP blocks)
|
||||
page.on('console', (msg: any) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
page.on("console", (msg: any) => {
|
||||
const type = msg.type();
|
||||
if (type === 'error' || type === 'warn') {
|
||||
if (type === "error" || type === "warn") {
|
||||
const text = msg.text();
|
||||
|
||||
// Exclude common browser extension noise or third party tracker warnings
|
||||
if (
|
||||
text.includes('google-analytics') ||
|
||||
text.includes('googletagmanager') ||
|
||||
text.includes('SES Removing unpermitted intrinsics') ||
|
||||
text.includes('Third-party cookie will be blocked') ||
|
||||
text.includes('Fast Refresh')
|
||||
text.includes("google-analytics") ||
|
||||
text.includes("googletagmanager") ||
|
||||
text.includes("SES Removing unpermitted intrinsics") ||
|
||||
text.includes("Third-party cookie will be blocked") ||
|
||||
text.includes("Fast Refresh")
|
||||
)
|
||||
return;
|
||||
|
||||
@@ -115,19 +136,19 @@ async function main() {
|
||||
});
|
||||
|
||||
// Listen to ALL network responses
|
||||
page.on('response', (response: HTTPResponse) => {
|
||||
page.on("response", (response: HTTPResponse) => {
|
||||
const status = response.status();
|
||||
// Catch classic 404s and 500s on ANY fetch/image/script
|
||||
if (
|
||||
status >= 400 &&
|
||||
status !== 999 &&
|
||||
!response.url().includes('google-analytics') &&
|
||||
!response.url().includes('googletagmanager')
|
||||
!response.url().includes("google-analytics") &&
|
||||
!response.url().includes("googletagmanager")
|
||||
) {
|
||||
const type = response.request().resourceType();
|
||||
|
||||
// We explicitly care about images, stylesheets, scripts, and fetch requests (API) getting 404/500s.
|
||||
if (['image', 'script', 'stylesheet', 'fetch', 'xhr'].includes(type)) {
|
||||
if (["image", "script", "stylesheet", "fetch", "xhr"].includes(type)) {
|
||||
brokenAssetsList.push({
|
||||
url: response.url(),
|
||||
status: status,
|
||||
@@ -144,7 +165,7 @@ async function main() {
|
||||
console.log(`[${i + 1}/${urls.length}] Scanning: ${u}`);
|
||||
try {
|
||||
// Wait until network is idle to ensure all Next.js hydration and image lazy-loads trigger
|
||||
await page.goto(u, { waitUntil: 'networkidle0', timeout: 30000 });
|
||||
await page.goto(u, { waitUntil: "networkidle0", timeout: 30000 });
|
||||
|
||||
// Force scroll to bottom to trigger any IntersectionObserver lazy-loaded images
|
||||
await page.evaluate(async () => {
|
||||
@@ -177,7 +198,9 @@ async function main() {
|
||||
|
||||
// 4. Report Results
|
||||
if (hasBrokenAssets && brokenAssetsList.length > 0) {
|
||||
console.error(`\n❌ FATAL: Broken assets (404/500) detected heavily on the site!`);
|
||||
console.error(
|
||||
`\n❌ FATAL: Broken assets (404/500) detected heavily on the site!`,
|
||||
);
|
||||
console.table(brokenAssetsList);
|
||||
}
|
||||
|
||||
@@ -187,7 +210,9 @@ async function main() {
|
||||
}
|
||||
|
||||
if (hasBrokenAssets || hasConsoleErrors) {
|
||||
console.error(`\n🚨 The CI build will now fail to prevent bad code from reaching production.`);
|
||||
console.error(
|
||||
`\n🚨 The CI build will now fail to prevent bad code from reaching production.`,
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log(
|
||||
|
||||
@@ -1,45 +1,49 @@
|
||||
import axios from 'axios';
|
||||
import * as cheerio from 'cheerio';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
import axios from "axios";
|
||||
import * as cheerio from "cheerio";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
const targetUrl = process.argv[2] || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
|
||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'mbgrid';
|
||||
const targetUrl =
|
||||
process.argv[2] ||
|
||||
process.env.NEXT_PUBLIC_BASE_URL ||
|
||||
"http://localhost:3000";
|
||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || "mbgrid";
|
||||
|
||||
async function main() {
|
||||
console.log(`\n🚀 Starting HTML Validation for: ${targetUrl}`);
|
||||
console.log(`📊 Limit: None (Full Sitemap)\n`);
|
||||
|
||||
try {
|
||||
const sitemapUrl = `${targetUrl.replace(/\/$/, '')}/sitemap.xml`;
|
||||
const sitemapUrl = `${targetUrl.replace(/\/$/, "")}/sitemap.xml`;
|
||||
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
|
||||
|
||||
const response = await axios.get(sitemapUrl, {
|
||||
headers: { Cookie: `mb_gatekeeper_session=${gatekeeperPassword}` },
|
||||
headers: { Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}` },
|
||||
validateStatus: (status) => status < 400,
|
||||
});
|
||||
|
||||
const $ = cheerio.load(response.data, { xmlMode: true });
|
||||
let urls = $('url loc')
|
||||
let urls = $("url loc")
|
||||
.map((i, el) => $(el).text())
|
||||
.get();
|
||||
|
||||
const urlPattern = /https?:\/\/[^\/]+/;
|
||||
urls = [...new Set(urls)]
|
||||
.filter((u) => u.startsWith('http'))
|
||||
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, '')))
|
||||
.filter((u) => u.startsWith("http"))
|
||||
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, "")))
|
||||
.sort();
|
||||
|
||||
console.log(`✅ Found ${urls.length} URLs in sitemap.`);
|
||||
|
||||
if (urls.length === 0) {
|
||||
console.error('❌ No URLs found in sitemap. Is the site up?');
|
||||
console.error("❌ No URLs found in sitemap. Is the site up?");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const outputDir = path.join(process.cwd(), '.htmlvalidate-tmp');
|
||||
if (fs.existsSync(outputDir)) fs.rmSync(outputDir, { recursive: true, force: true });
|
||||
const outputDir = path.join(process.cwd(), ".htmlvalidate-tmp");
|
||||
if (fs.existsSync(outputDir))
|
||||
fs.rmSync(outputDir, { recursive: true, force: true });
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
console.log(`📥 Fetching HTML for ${urls.length} pages...`);
|
||||
@@ -47,14 +51,19 @@ async function main() {
|
||||
const u = urls[i];
|
||||
try {
|
||||
const res = await axios.get(u, {
|
||||
headers: { Cookie: `mb_gatekeeper_session=${gatekeeperPassword}` },
|
||||
headers: {
|
||||
Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}`,
|
||||
},
|
||||
validateStatus: (status) => status < 400,
|
||||
});
|
||||
|
||||
// Generate a safe filename that retains URL information
|
||||
const urlStr = new URL(u);
|
||||
const safePath = (urlStr.pathname + urlStr.search).replace(/[^a-zA-Z0-9]/g, '_');
|
||||
const filename = `${safePath || 'index'}.html`;
|
||||
const safePath = (urlStr.pathname + urlStr.search).replace(
|
||||
/[^a-zA-Z0-9]/g,
|
||||
"_",
|
||||
);
|
||||
const filename = `${safePath || "index"}.html`;
|
||||
|
||||
fs.writeFileSync(path.join(outputDir, filename), res.data);
|
||||
} catch (err: unknown) {
|
||||
@@ -66,7 +75,9 @@ async function main() {
|
||||
|
||||
console.log(`\n💻 Executing html-validate...`);
|
||||
try {
|
||||
execSync(`npx html-validate .htmlvalidate-tmp/*.html`, { stdio: 'inherit' });
|
||||
execSync(`npx html-validate .htmlvalidate-tmp/*.html`, {
|
||||
stdio: "inherit",
|
||||
});
|
||||
console.log(`✅ HTML Validation passed perfectly!`);
|
||||
} catch {
|
||||
console.error(`❌ HTML Validation found issues.`);
|
||||
@@ -77,8 +88,9 @@ async function main() {
|
||||
console.error(`\n❌ Error during HTML Validation:`, errorBody);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
const outputDir = path.join(process.cwd(), '.htmlvalidate-tmp');
|
||||
if (fs.existsSync(outputDir)) fs.rmSync(outputDir, { recursive: true, force: true });
|
||||
const outputDir = path.join(process.cwd(), ".htmlvalidate-tmp");
|
||||
if (fs.existsSync(outputDir))
|
||||
fs.rmSync(outputDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
43
scripts/patch_qa.py
Normal file
43
scripts/patch_qa.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import re
|
||||
|
||||
with open('.gitea/workflows/qa.yml', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Replace Registry Auth
|
||||
old_auth = """ - name: 🔐 Registry Auth
|
||||
run: |
|
||||
REGISTRY="${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}"
|
||||
echo "@mintel:registry=https://$REGISTRY" > .npmrc
|
||||
echo "//$REGISTRY/:_authToken=${{ secrets.NPM_TOKEN || secrets.GITEA_PAT || secrets.MINTEL_PRIVATE_TOKEN }}" >> .npmrc"""
|
||||
|
||||
new_auth = """ - name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
|
||||
echo "//${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc"""
|
||||
|
||||
content = content.replace(old_auth, new_auth)
|
||||
|
||||
# Replace Install dependencies (for jobs that don't have the rm -rf)
|
||||
old_install_1 = """ - name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile"""
|
||||
|
||||
new_install_1 = """ - name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile"""
|
||||
|
||||
content = content.replace(old_install_1, new_install_1)
|
||||
|
||||
# Replace Install dependencies (for prepare job)
|
||||
old_install_2 = """ - name: Install dependencies
|
||||
run: |
|
||||
rm -rf .next .turbo node_modules || true
|
||||
pnpm install --no-frozen-lockfile --reporter=append-only"""
|
||||
|
||||
new_install_2 = """ - name: Install dependencies
|
||||
run: |
|
||||
rm -rf .next .turbo node_modules || true
|
||||
pnpm install --frozen-lockfile --reporter=append-only"""
|
||||
|
||||
content = content.replace(old_install_2, new_install_2)
|
||||
|
||||
with open('.gitea/workflows/qa.yml', 'w') as f:
|
||||
f.write(content)
|
||||
20
scripts/revert_auth.py
Normal file
20
scripts/revert_auth.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import re
|
||||
|
||||
with open('.gitea/workflows/qa.yml', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
bad_auth = """ - name: 🔐 Registry Auth
|
||||
run: |
|
||||
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
|
||||
echo "//${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc"""
|
||||
|
||||
good_auth = """ - name: 🔐 Registry Auth
|
||||
run: |
|
||||
REGISTRY="${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}"
|
||||
echo "@mintel:registry=https://$REGISTRY" > .npmrc
|
||||
echo "//$REGISTRY/:_authToken=${{ secrets.NPM_TOKEN || secrets.GITEA_PAT || secrets.MINTEL_PRIVATE_TOKEN }}" >> .npmrc"""
|
||||
|
||||
content = content.replace(bad_auth, good_auth)
|
||||
|
||||
with open('.gitea/workflows/qa.yml', 'w') as f:
|
||||
f.write(content)
|
||||
@@ -1,9 +1,9 @@
|
||||
import axios from 'axios';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { execSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { AxiosError } from 'axios';
|
||||
import axios from "axios";
|
||||
import * as cheerio from "cheerio";
|
||||
import { execSync } from "child_process";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
interface Pa11yConfig {
|
||||
defaults: {
|
||||
@@ -36,9 +36,14 @@ interface Pa11yResult {
|
||||
* 3. Runs pa11y-ci on those URLs
|
||||
*/
|
||||
|
||||
const targetUrl = process.argv[2] || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
|
||||
const limit = process.env.PAGESPEED_LIMIT ? parseInt(process.env.PAGESPEED_LIMIT) : 20;
|
||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'mbgrid';
|
||||
const targetUrl =
|
||||
process.argv[2] ||
|
||||
process.env.NEXT_PUBLIC_BASE_URL ||
|
||||
"http://localhost:3000";
|
||||
const limit = process.env.PAGESPEED_LIMIT
|
||||
? parseInt(process.env.PAGESPEED_LIMIT)
|
||||
: 20;
|
||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || "mbgrid";
|
||||
|
||||
async function main() {
|
||||
console.log(`\n🚀 Starting WCAG Audit for: ${targetUrl}`);
|
||||
@@ -46,32 +51,32 @@ async function main() {
|
||||
|
||||
try {
|
||||
// 1. Fetch Sitemap
|
||||
const sitemapUrl = `${targetUrl.replace(/\/$/, '')}/sitemap.xml`;
|
||||
const sitemapUrl = `${targetUrl.replace(/\/$/, "")}/sitemap.xml`;
|
||||
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
|
||||
|
||||
const response = await axios.get(sitemapUrl, {
|
||||
headers: {
|
||||
Cookie: `mb_gatekeeper_session=${gatekeeperPassword}`,
|
||||
Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}`,
|
||||
},
|
||||
validateStatus: (status) => status < 400,
|
||||
});
|
||||
|
||||
const $ = cheerio.load(response.data, { xmlMode: true });
|
||||
let urls = $('url loc')
|
||||
let urls = $("url loc")
|
||||
.map((i, el) => $(el).text())
|
||||
.get();
|
||||
|
||||
// Cleanup, filter and normalize domains to targetUrl
|
||||
const urlPattern = /https?:\/\/[^\/]+/;
|
||||
urls = [...new Set(urls)]
|
||||
.filter((u) => u.startsWith('http'))
|
||||
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, '')))
|
||||
.filter((u) => u.startsWith("http"))
|
||||
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, "")))
|
||||
.sort();
|
||||
|
||||
console.log(`✅ Found ${urls.length} URLs in sitemap.`);
|
||||
|
||||
if (urls.length === 0) {
|
||||
console.error('❌ No URLs found in sitemap. Is the site up?');
|
||||
console.error("❌ No URLs found in sitemap. Is the site up?");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -79,7 +84,9 @@ async function main() {
|
||||
console.log(
|
||||
`⚠️ Too many pages (${urls.length}). Limiting to ${limit} representative pages.`,
|
||||
);
|
||||
const home = urls.filter((u) => u.endsWith('/de') || u.endsWith('/en') || u === targetUrl);
|
||||
const home = urls.filter(
|
||||
(u) => u.endsWith("/de") || u.endsWith("/en") || u === targetUrl,
|
||||
);
|
||||
const others = urls.filter((u) => !home.includes(u));
|
||||
urls = [...home, ...others.slice(0, limit - home.length)];
|
||||
}
|
||||
@@ -88,10 +95,10 @@ async function main() {
|
||||
urls.forEach((u) => console.log(` - ${u}`));
|
||||
|
||||
// 2. Prepare pa11y-ci config
|
||||
const baseConfigPath = path.join(process.cwd(), '.pa11yci.json');
|
||||
const baseConfigPath = path.join(process.cwd(), ".pa11yci.json");
|
||||
let baseConfig: Pa11yConfig = { defaults: {} };
|
||||
if (fs.existsSync(baseConfigPath)) {
|
||||
baseConfig = JSON.parse(fs.readFileSync(baseConfigPath, 'utf8'));
|
||||
baseConfig = JSON.parse(fs.readFileSync(baseConfigPath, "utf8"));
|
||||
}
|
||||
|
||||
// Extract domain for cookie (not currently used)
|
||||
@@ -105,19 +112,21 @@ async function main() {
|
||||
defaults: {
|
||||
...baseConfig.defaults,
|
||||
threshold: 0, // Force threshold to 0 so all errors are shown in JSON
|
||||
runners: ['axe'],
|
||||
ignore: [...(baseConfig.defaults?.ignore || []), 'color-contrast'],
|
||||
runners: ["axe"],
|
||||
ignore: [...(baseConfig.defaults?.ignore || []), "color-contrast"],
|
||||
chromeLaunchConfig: {
|
||||
...baseConfig.defaults?.chromeLaunchConfig,
|
||||
...(process.env.CHROME_PATH ? { executablePath: process.env.CHROME_PATH } : {}),
|
||||
...(process.env.CHROME_PATH
|
||||
? { executablePath: process.env.CHROME_PATH }
|
||||
: {}),
|
||||
args: [
|
||||
...(baseConfig.defaults?.chromeLaunchConfig?.args || []),
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
],
|
||||
},
|
||||
headers: {
|
||||
Cookie: `mb_gatekeeper_session=${gatekeeperPassword}`,
|
||||
Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}`,
|
||||
},
|
||||
timeout: 60000, // Increase timeout for slower pages
|
||||
},
|
||||
@@ -125,13 +134,13 @@ async function main() {
|
||||
};
|
||||
|
||||
// Create output directory
|
||||
const outputDir = path.join(process.cwd(), '.pa11yci');
|
||||
const outputDir = path.join(process.cwd(), ".pa11yci");
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
const tempConfigPath = path.join(outputDir, 'config.temp.json');
|
||||
const reportPath = path.join(outputDir, 'report.json');
|
||||
const tempConfigPath = path.join(outputDir, "config.temp.json");
|
||||
const reportPath = path.join(outputDir, "report.json");
|
||||
fs.writeFileSync(tempConfigPath, JSON.stringify(tempConfig, null, 2));
|
||||
|
||||
// 3. Execute pa11y-ci
|
||||
@@ -140,8 +149,8 @@ async function main() {
|
||||
|
||||
try {
|
||||
execSync(pa11yCommand, {
|
||||
encoding: 'utf8',
|
||||
stdio: 'inherit',
|
||||
encoding: "utf8",
|
||||
stdio: "inherit",
|
||||
});
|
||||
} catch {
|
||||
// pa11y-ci exits with non-zero if issues are found, which is expected
|
||||
@@ -149,7 +158,7 @@ async function main() {
|
||||
|
||||
// 4. Summarize Results
|
||||
if (fs.existsSync(reportPath)) {
|
||||
const reportData = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
|
||||
const reportData = JSON.parse(fs.readFileSync(reportPath, "utf8"));
|
||||
console.log(`\n📊 WCAG Audit Summary:\n`);
|
||||
|
||||
const summaryTable = Object.keys(reportData.results).map((url) => {
|
||||
@@ -161,31 +170,47 @@ async function main() {
|
||||
|
||||
if (Array.isArray(results)) {
|
||||
// pa11y action execution errors come as objects with a message but no type
|
||||
const actionErrors = results.filter((r: Pa11yResult) => !r.type && r.message).length;
|
||||
errors = results.filter((r: Pa11yResult) => r.type === 'error').length + actionErrors;
|
||||
warnings = results.filter((r: Pa11yResult) => r.type === 'warning').length;
|
||||
notices = results.filter((r: Pa11yResult) => r.type === 'notice').length;
|
||||
const actionErrors = results.filter(
|
||||
(r: Pa11yResult) => !r.type && r.message,
|
||||
).length;
|
||||
errors =
|
||||
results.filter((r: Pa11yResult) => r.type === "error").length +
|
||||
actionErrors;
|
||||
warnings = results.filter(
|
||||
(r: Pa11yResult) => r.type === "warning",
|
||||
).length;
|
||||
notices = results.filter(
|
||||
(r: Pa11yResult) => r.type === "notice",
|
||||
).length;
|
||||
}
|
||||
|
||||
// Clean URL for display
|
||||
const displayUrl = url.replace(targetUrl, '') || '/';
|
||||
const displayUrl = url.replace(targetUrl, "") || "/";
|
||||
|
||||
return {
|
||||
URL: displayUrl.length > 50 ? displayUrl.substring(0, 47) + '...' : displayUrl,
|
||||
URL:
|
||||
displayUrl.length > 50
|
||||
? displayUrl.substring(0, 47) + "..."
|
||||
: displayUrl,
|
||||
Errors: errors,
|
||||
Warnings: warnings,
|
||||
Notices: notices,
|
||||
Status: errors === 0 ? '✅' : '❌',
|
||||
Status: errors === 0 ? "✅" : "❌",
|
||||
};
|
||||
});
|
||||
|
||||
console.table(summaryTable);
|
||||
|
||||
const totalErrors = summaryTable.reduce((acc, curr) => acc + curr.Errors, 0);
|
||||
const totalErrors = summaryTable.reduce(
|
||||
(acc, curr) => acc + curr.Errors,
|
||||
0,
|
||||
);
|
||||
const totalPages = summaryTable.length;
|
||||
const cleanPages = summaryTable.filter((p) => p.Errors === 0).length;
|
||||
|
||||
console.log(`\n📈 Result: ${cleanPages}/${totalPages} pages are error-free.`);
|
||||
console.log(
|
||||
`\n📈 Result: ${cleanPages}/${totalPages} pages are error-free.`,
|
||||
);
|
||||
if (totalErrors > 0) {
|
||||
console.log(` Total Errors discovered: ${totalErrors}`);
|
||||
process.exitCode = 1;
|
||||
@@ -205,7 +230,10 @@ async function main() {
|
||||
process.exit(1);
|
||||
} finally {
|
||||
// Clean up temp config file, keep report
|
||||
const tempConfigPath = path.join(process.cwd(), '.pa11yci/config.temp.json');
|
||||
const tempConfigPath = path.join(
|
||||
process.cwd(),
|
||||
".pa11yci/config.temp.json",
|
||||
);
|
||||
if (fs.existsSync(tempConfigPath)) fs.unlinkSync(tempConfigPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import { Block } from "payload";
|
||||
|
||||
// Define any custom blocks you want here. Leaving empty for now.
|
||||
export const payloadBlocks: Block[] = [];
|
||||
@@ -1,44 +0,0 @@
|
||||
import type { CollectionConfig } from "payload";
|
||||
|
||||
export const FormSubmissions: CollectionConfig = {
|
||||
slug: "form-submissions",
|
||||
admin: {
|
||||
useAsTitle: "name",
|
||||
defaultColumns: ["name", "email", "company", "createdAt"],
|
||||
description: "Captured leads from Contact Form.",
|
||||
},
|
||||
access: {
|
||||
read: ({ req: { user } }) =>
|
||||
Boolean(user) || process.env.NODE_ENV === "development",
|
||||
update: ({ req: { user } }) =>
|
||||
Boolean(user) || process.env.NODE_ENV === "development",
|
||||
delete: ({ req: { user } }) =>
|
||||
Boolean(user) || process.env.NODE_ENV === "development",
|
||||
create: () => false, // Only system creates submissions
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "name",
|
||||
type: "text",
|
||||
required: true,
|
||||
admin: { readOnly: true },
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
type: "email",
|
||||
required: true,
|
||||
admin: { readOnly: true },
|
||||
},
|
||||
{
|
||||
name: "company",
|
||||
type: "text",
|
||||
admin: { readOnly: true },
|
||||
},
|
||||
{
|
||||
name: "message",
|
||||
type: "textarea",
|
||||
required: true,
|
||||
admin: { readOnly: true },
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
import type { CollectionConfig } from "payload";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const filename = fileURLToPath(import.meta.url);
|
||||
const dirname = path.dirname(filename);
|
||||
|
||||
export const Media: CollectionConfig = {
|
||||
slug: "media",
|
||||
admin: {
|
||||
useAsTitle: "filename",
|
||||
defaultColumns: ["filename", "alt", "updatedAt"],
|
||||
},
|
||||
access: {
|
||||
read: () => true, // Publicly readable
|
||||
},
|
||||
upload: {
|
||||
staticDir: path.resolve(dirname, "../../../public/media"),
|
||||
adminThumbnail: "thumbnail",
|
||||
imageSizes: [
|
||||
{
|
||||
name: "thumbnail",
|
||||
width: 400,
|
||||
height: 300,
|
||||
position: "centre",
|
||||
},
|
||||
{
|
||||
name: "card",
|
||||
width: 768,
|
||||
height: undefined,
|
||||
position: "centre",
|
||||
},
|
||||
],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "alt",
|
||||
type: "text",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
import { CollectionConfig } from "payload";
|
||||
import { lexicalEditor, BlocksFeature } from "@payloadcms/richtext-lexical";
|
||||
import { payloadBlocks } from "../blocks/allBlocks";
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: "pages",
|
||||
admin: {
|
||||
useAsTitle: "title",
|
||||
defaultColumns: ["title", "slug", "updatedAt"],
|
||||
},
|
||||
access: {
|
||||
read: () => true, // Publicly readable
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "title",
|
||||
type: "text",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "slug",
|
||||
type: "text",
|
||||
required: true,
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "content",
|
||||
type: "richText",
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
BlocksFeature({
|
||||
blocks: payloadBlocks,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
import type { CollectionConfig } from "payload";
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: "users",
|
||||
admin: {
|
||||
useAsTitle: "email",
|
||||
},
|
||||
auth: true,
|
||||
fields: [
|
||||
// Email added by default
|
||||
],
|
||||
};
|
||||
@@ -1,453 +0,0 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Supported timezones in IANA format.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "supportedTimezones".
|
||||
*/
|
||||
export type SupportedTimezones =
|
||||
| "Pacific/Midway"
|
||||
| "Pacific/Niue"
|
||||
| "Pacific/Honolulu"
|
||||
| "Pacific/Rarotonga"
|
||||
| "America/Anchorage"
|
||||
| "Pacific/Gambier"
|
||||
| "America/Los_Angeles"
|
||||
| "America/Tijuana"
|
||||
| "America/Denver"
|
||||
| "America/Phoenix"
|
||||
| "America/Chicago"
|
||||
| "America/Guatemala"
|
||||
| "America/New_York"
|
||||
| "America/Bogota"
|
||||
| "America/Caracas"
|
||||
| "America/Santiago"
|
||||
| "America/Buenos_Aires"
|
||||
| "America/Sao_Paulo"
|
||||
| "Atlantic/South_Georgia"
|
||||
| "Atlantic/Azores"
|
||||
| "Atlantic/Cape_Verde"
|
||||
| "Europe/London"
|
||||
| "Europe/Berlin"
|
||||
| "Africa/Lagos"
|
||||
| "Europe/Athens"
|
||||
| "Africa/Cairo"
|
||||
| "Europe/Moscow"
|
||||
| "Asia/Riyadh"
|
||||
| "Asia/Dubai"
|
||||
| "Asia/Baku"
|
||||
| "Asia/Karachi"
|
||||
| "Asia/Tashkent"
|
||||
| "Asia/Calcutta"
|
||||
| "Asia/Dhaka"
|
||||
| "Asia/Almaty"
|
||||
| "Asia/Jakarta"
|
||||
| "Asia/Bangkok"
|
||||
| "Asia/Shanghai"
|
||||
| "Asia/Singapore"
|
||||
| "Asia/Tokyo"
|
||||
| "Asia/Seoul"
|
||||
| "Australia/Brisbane"
|
||||
| "Australia/Sydney"
|
||||
| "Pacific/Guam"
|
||||
| "Pacific/Noumea"
|
||||
| "Pacific/Auckland"
|
||||
| "Pacific/Fiji";
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
blocks: {};
|
||||
collections: {
|
||||
users: User;
|
||||
media: Media;
|
||||
"form-submissions": FormSubmission;
|
||||
pages: Page;
|
||||
"payload-kv": PayloadKv;
|
||||
"payload-locked-documents": PayloadLockedDocument;
|
||||
"payload-preferences": PayloadPreference;
|
||||
"payload-migrations": PayloadMigration;
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
media: MediaSelect<false> | MediaSelect<true>;
|
||||
"form-submissions":
|
||||
| FormSubmissionsSelect<false>
|
||||
| FormSubmissionsSelect<true>;
|
||||
pages: PagesSelect<false> | PagesSelect<true>;
|
||||
"payload-kv": PayloadKvSelect<false> | PayloadKvSelect<true>;
|
||||
"payload-locked-documents":
|
||||
| PayloadLockedDocumentsSelect<false>
|
||||
| PayloadLockedDocumentsSelect<true>;
|
||||
"payload-preferences":
|
||||
| PayloadPreferencesSelect<false>
|
||||
| PayloadPreferencesSelect<true>;
|
||||
"payload-migrations":
|
||||
| PayloadMigrationsSelect<false>
|
||||
| PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: number;
|
||||
};
|
||||
fallbackLocale: null;
|
||||
globals: {};
|
||||
globalsSelect: {};
|
||||
locale: null;
|
||||
user: User;
|
||||
jobs: {
|
||||
tasks: unknown;
|
||||
workflows: unknown;
|
||||
};
|
||||
}
|
||||
export interface UserAuthOperations {
|
||||
forgotPassword: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
login: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
registerFirstUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
unlock: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: number;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
resetPasswordToken?: string | null;
|
||||
resetPasswordExpiration?: string | null;
|
||||
salt?: string | null;
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
sessions?:
|
||||
| {
|
||||
id: string;
|
||||
createdAt?: string | null;
|
||||
expiresAt: string;
|
||||
}[]
|
||||
| null;
|
||||
password?: string | null;
|
||||
collection: "users";
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "media".
|
||||
*/
|
||||
export interface Media {
|
||||
id: number;
|
||||
alt: string;
|
||||
prefix?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
thumbnailURL?: string | null;
|
||||
filename?: string | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
focalX?: number | null;
|
||||
focalY?: number | null;
|
||||
sizes?: {
|
||||
thumbnail?: {
|
||||
url?: string | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
filename?: string | null;
|
||||
};
|
||||
card?: {
|
||||
url?: string | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
filename?: string | null;
|
||||
};
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Captured leads from Contact Form.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "form-submissions".
|
||||
*/
|
||||
export interface FormSubmission {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
company?: string | null;
|
||||
message: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "pages".
|
||||
*/
|
||||
export interface Page {
|
||||
id: number;
|
||||
title: string;
|
||||
slug: string;
|
||||
content: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
type: any;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ("ltr" | "rtl") | null;
|
||||
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
|
||||
indent: number;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-kv".
|
||||
*/
|
||||
export interface PayloadKv {
|
||||
id: number;
|
||||
key: string;
|
||||
data:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: number;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: "users";
|
||||
value: number | User;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: "media";
|
||||
value: number | Media;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: "form-submissions";
|
||||
value: number | FormSubmission;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: "pages";
|
||||
value: number | Page;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: "users";
|
||||
value: number | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: number;
|
||||
user: {
|
||||
relationTo: "users";
|
||||
value: number | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: number;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users_select".
|
||||
*/
|
||||
export interface UsersSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
email?: T;
|
||||
resetPasswordToken?: T;
|
||||
resetPasswordExpiration?: T;
|
||||
salt?: T;
|
||||
hash?: T;
|
||||
loginAttempts?: T;
|
||||
lockUntil?: T;
|
||||
sessions?:
|
||||
| T
|
||||
| {
|
||||
id?: T;
|
||||
createdAt?: T;
|
||||
expiresAt?: T;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "media_select".
|
||||
*/
|
||||
export interface MediaSelect<T extends boolean = true> {
|
||||
alt?: T;
|
||||
prefix?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
url?: T;
|
||||
thumbnailURL?: T;
|
||||
filename?: T;
|
||||
mimeType?: T;
|
||||
filesize?: T;
|
||||
width?: T;
|
||||
height?: T;
|
||||
focalX?: T;
|
||||
focalY?: T;
|
||||
sizes?:
|
||||
| T
|
||||
| {
|
||||
thumbnail?:
|
||||
| T
|
||||
| {
|
||||
url?: T;
|
||||
width?: T;
|
||||
height?: T;
|
||||
mimeType?: T;
|
||||
filesize?: T;
|
||||
filename?: T;
|
||||
};
|
||||
card?:
|
||||
| T
|
||||
| {
|
||||
url?: T;
|
||||
width?: T;
|
||||
height?: T;
|
||||
mimeType?: T;
|
||||
filesize?: T;
|
||||
filename?: T;
|
||||
};
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "form-submissions_select".
|
||||
*/
|
||||
export interface FormSubmissionsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
email?: T;
|
||||
company?: T;
|
||||
message?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "pages_select".
|
||||
*/
|
||||
export interface PagesSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
slug?: T;
|
||||
content?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-kv_select".
|
||||
*/
|
||||
export interface PayloadKvSelect<T extends boolean = true> {
|
||||
key?: T;
|
||||
data?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents_select".
|
||||
*/
|
||||
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
|
||||
document?: T;
|
||||
globalSlug?: T;
|
||||
user?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences_select".
|
||||
*/
|
||||
export interface PayloadPreferencesSelect<T extends boolean = true> {
|
||||
user?: T;
|
||||
key?: T;
|
||||
value?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations_select".
|
||||
*/
|
||||
export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
batch?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
*/
|
||||
export interface Auth {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
declare module "payload" {
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import { buildConfig } from "payload";
|
||||
import { postgresAdapter } from "@payloadcms/db-postgres";
|
||||
import { lexicalEditor, BlocksFeature } from "@payloadcms/richtext-lexical";
|
||||
import { nodemailerAdapter } from "@payloadcms/email-nodemailer";
|
||||
import { s3Storage } from "@payloadcms/storage-s3";
|
||||
import sharp from "sharp";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { payloadBlocks } from "./blocks/allBlocks";
|
||||
|
||||
import { Users } from "./collections/Users";
|
||||
import { Media } from "./collections/Media";
|
||||
import { FormSubmissions } from "./collections/FormSubmissions";
|
||||
import { Pages } from "./collections/Pages";
|
||||
|
||||
import { migrations } from "../../migrations/index";
|
||||
|
||||
const filename = fileURLToPath(import.meta.url);
|
||||
const dirname = path.dirname(filename);
|
||||
|
||||
export default buildConfig({
|
||||
admin: {
|
||||
user: Users.slug,
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
meta: {
|
||||
titleSuffix: " – MB Grid Solutions",
|
||||
},
|
||||
},
|
||||
collections: [Users, Media, FormSubmissions, Pages],
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
BlocksFeature({
|
||||
blocks: payloadBlocks,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
secret: process.env.PAYLOAD_SECRET || "fallback-secret-for-dev",
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, "payload-types.ts"),
|
||||
},
|
||||
db: postgresAdapter({
|
||||
pool: {
|
||||
connectionString:
|
||||
process.env.DATABASE_URI ||
|
||||
process.env.POSTGRES_URI ||
|
||||
`postgresql://${process.env.POSTGRES_USER || "postgres"}:${process.env.POSTGRES_PASSWORD || "postgres"}@127.0.0.1:5432/${process.env.POSTGRES_DB || "payload"}`,
|
||||
},
|
||||
prodMigrations: migrations,
|
||||
}),
|
||||
email: nodemailerAdapter({
|
||||
defaultFromAddress:
|
||||
process.env.MAIL_FROM ||
|
||||
process.env.SMTP_FROM ||
|
||||
"info@mb-grid-solutions.com",
|
||||
defaultFromName: "MB Grid Solutions CMS",
|
||||
transportOptions: {
|
||||
host: process.env.MAIL_HOST || process.env.SMTP_HOST || "localhost",
|
||||
port: parseInt(
|
||||
process.env.MAIL_PORT || process.env.SMTP_PORT || "587",
|
||||
),
|
||||
auth: {
|
||||
user:
|
||||
process.env.MAIL_USERNAME ||
|
||||
process.env.MAIL_USER ||
|
||||
process.env.SMTP_USER,
|
||||
pass: process.env.MAIL_PASSWORD || process.env.SMTP_PASS,
|
||||
},
|
||||
secure:
|
||||
process.env.MAIL_SECURE === "true" ||
|
||||
process.env.SMTP_SECURE === "true",
|
||||
},
|
||||
}),
|
||||
sharp,
|
||||
plugins: [
|
||||
...(process.env.S3_ENDPOINT
|
||||
? [
|
||||
s3Storage({
|
||||
collections: {
|
||||
media: {
|
||||
prefix: `${process.env.S3_PREFIX || "mb-grid-solutions"}/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,
|
||||
},
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
});
|
||||
76
test_output.txt
Normal file
76
test_output.txt
Normal file
@@ -0,0 +1,76 @@
|
||||
WARN Issue while reading "/Volumes/Alpha SSD/Coding/mb-grid-solutions.com/.npmrc". Failed to replace env in config: ${NPM_TOKEN}
|
||||
WARN Issue while reading "/Volumes/Alpha SSD/Coding/mb-grid-solutions.com/.npmrc". Failed to replace env in config: ${NPM_TOKEN}
|
||||
|
||||
> mb-grid-solutions.com@1.3.20 test /Volumes/Alpha SSD/Coding/mb-grid-solutions.com
|
||||
> vitest run
|
||||
|
||||
|
||||
RUN v4.0.18 /Volumes/Alpha SSD/Coding/mb-grid-solutions.com
|
||||
|
||||
❯ tests/api-contact.test.ts (0 test)
|
||||
✓ lib/env.test.ts (6 tests) 4ms
|
||||
DOMException [NotSupportedError]: Failed to load iframe page "https://www.openstreetmap.org/export/embed.html?bbox=9.445,48.815,9.465,48.825&layer=mapnik&marker=48.8198,9.4552". Iframe page loading is disabled.
|
||||
at HTMLIFrameElement.#loadPage (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/html-iframe-element/HTMLIFrameElement.js:333:27)
|
||||
at HTMLIFrameElement.[connectedToDocument] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/html-iframe-element/HTMLIFrameElement.js:230:23)
|
||||
at HTMLIFrameElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:794:53)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[appendChild] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:413:45)
|
||||
DOMException [NotSupportedError]: Failed to load iframe page "https://www.openstreetmap.org/export/embed.html?bbox=9.445,48.815,9.465,48.825&layer=mapnik&marker=48.8198,9.4552". Iframe page loading is disabled.
|
||||
at HTMLIFrameElement.#loadPage (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/html-iframe-element/HTMLIFrameElement.js:333:27)
|
||||
at HTMLIFrameElement.[connectedToDocument] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/html-iframe-element/HTMLIFrameElement.js:230:23)
|
||||
at HTMLIFrameElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:794:53)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[appendChild] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:413:45)
|
||||
DOMException [NotSupportedError]: Failed to load iframe page "https://www.openstreetmap.org/export/embed.html?bbox=9.445,48.815,9.465,48.825&layer=mapnik&marker=48.8198,9.4552". Iframe page loading is disabled.
|
||||
at HTMLIFrameElement.#loadPage (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/html-iframe-element/HTMLIFrameElement.js:333:27)
|
||||
at HTMLIFrameElement.[connectedToDocument] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/html-iframe-element/HTMLIFrameElement.js:230:23)
|
||||
at HTMLIFrameElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:794:53)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[connectedToNode] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:786:58)
|
||||
at HTMLDivElement.[appendChild] (file:///Volumes/Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/happy-dom@20.6.1/node_modules/happy-dom/lib/nodes/node/Node.js:413:45)
|
||||
✓ tests/contact.test.tsx (3 tests) 148ms
|
||||
✓ tests/home.test.tsx (3 tests) 123ms
|
||||
|
||||
⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯
|
||||
|
||||
FAIL tests/api-contact.test.ts [ tests/api-contact.test.ts ]
|
||||
Error: Failed to resolve import "@payload-config" from "app/api/contact/route.ts". Does the file exist?
|
||||
Plugin: vite:import-analysis
|
||||
File: /Volumes/Alpha SSD/Coding/mb-grid-solutions.com/app/api/contact/route.ts:3:26
|
||||
1 | import { NextResponse } from "next/server";
|
||||
2 | import { getPayload } from "payload";
|
||||
3 | import configPromise from "@payload-config";
|
||||
| ^
|
||||
4 | import { getServerAppServices } from "@/lib/services/create-services.server";
|
||||
5 | import {
|
||||
❯ TransformPluginContext._formatLog ../../../Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/vite@7.3.1_@types+node@25.2.0_jiti@2.6.1_lightningcss@1.30.2_sass@1.77.4_terser@5.46.0_tsx@4.21.0_yaml@2.8.2/node_modules/vite/dist/node/chunks/config.js:28999:43
|
||||
❯ TransformPluginContext.error ../../../Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/vite@7.3.1_@types+node@25.2.0_jiti@2.6.1_lightningcss@1.30.2_sass@1.77.4_terser@5.46.0_tsx@4.21.0_yaml@2.8.2/node_modules/vite/dist/node/chunks/config.js:28996:14
|
||||
❯ normalizeUrl ../../../Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/vite@7.3.1_@types+node@25.2.0_jiti@2.6.1_lightningcss@1.30.2_sass@1.77.4_terser@5.46.0_tsx@4.21.0_yaml@2.8.2/node_modules/vite/dist/node/chunks/config.js:27119:18
|
||||
❯ ../../../Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/vite@7.3.1_@types+node@25.2.0_jiti@2.6.1_lightningcss@1.30.2_sass@1.77.4_terser@5.46.0_tsx@4.21.0_yaml@2.8.2/node_modules/vite/dist/node/chunks/config.js:27177:32
|
||||
❯ TransformPluginContext.transform ../../../Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/vite@7.3.1_@types+node@25.2.0_jiti@2.6.1_lightningcss@1.30.2_sass@1.77.4_terser@5.46.0_tsx@4.21.0_yaml@2.8.2/node_modules/vite/dist/node/chunks/config.js:27145:4
|
||||
❯ EnvironmentPluginContainer.transform ../../../Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/vite@7.3.1_@types+node@25.2.0_jiti@2.6.1_lightningcss@1.30.2_sass@1.77.4_terser@5.46.0_tsx@4.21.0_yaml@2.8.2/node_modules/vite/dist/node/chunks/config.js:28797:14
|
||||
❯ loadAndTransform ../../../Alpha%20SSD/Coding/mb-grid-solutions.com/node_modules/.pnpm/vite@7.3.1_@types+node@25.2.0_jiti@2.6.1_lightningcss@1.30.2_sass@1.77.4_terser@5.46.0_tsx@4.21.0_yaml@2.8.2/node_modules/vite/dist/node/chunks/config.js:22670:26
|
||||
|
||||
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
|
||||
|
||||
|
||||
Test Files 1 failed | 3 passed (4)
|
||||
Tests 12 passed (12)
|
||||
Start at 14:27:09
|
||||
Duration 1.47s (transform 453ms, setup 660ms, import 1.35s, tests 275ms, environment 1.52s)
|
||||
|
||||
ELIFECYCLE Test failed. See above for more details.
|
||||
@@ -1,11 +1,5 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
// Mock Payload CMS
|
||||
const { mockCreate, mockSendEmail } = vi.hoisted(() => ({
|
||||
mockCreate: vi.fn(),
|
||||
mockSendEmail: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock Nodemailer
|
||||
const { mockSendMail } = vi.hoisted(() => ({
|
||||
mockSendMail: vi.fn().mockResolvedValue({ messageId: "mock-message-id" }),
|
||||
@@ -19,13 +13,6 @@ vi.mock("nodemailer", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("payload", () => ({
|
||||
getPayload: vi.fn().mockResolvedValue({
|
||||
create: mockCreate,
|
||||
sendEmail: mockSendEmail,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock Email Template renders
|
||||
vi.mock("@mintel/mail", () => ({
|
||||
render: vi.fn().mockResolvedValue("<html>Mocked Email HTML</html>"),
|
||||
@@ -92,9 +79,8 @@ describe("Contact API Integration", () => {
|
||||
const data = await response.json();
|
||||
expect(data.error).toBe("message_too_short");
|
||||
|
||||
// Ensure payload and email were NOT called
|
||||
expect(mockCreate).not.toHaveBeenCalled();
|
||||
expect(mockSendEmail).not.toHaveBeenCalled();
|
||||
// Ensure email was NOT called
|
||||
expect(mockSendMail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should catch honeypot submissions", async () => {
|
||||
@@ -113,11 +99,10 @@ describe("Contact API Integration", () => {
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
// But it actually does NOTHING internally
|
||||
expect(mockCreate).not.toHaveBeenCalled();
|
||||
expect(mockSendMail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should successfully save to Payload and send emails", async () => {
|
||||
it("should successfully send emails", async () => {
|
||||
const req = new Request("http://localhost/api/contact", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -139,20 +124,7 @@ describe("Contact API Integration", () => {
|
||||
const data = await response.json();
|
||||
expect(data.message).toBe("Ok");
|
||||
|
||||
// 1. Verify Payload creation
|
||||
expect(mockCreate).toHaveBeenCalledTimes(1);
|
||||
expect(mockCreate).toHaveBeenCalledWith({
|
||||
collection: "form-submissions",
|
||||
data: {
|
||||
name: "Jane Doe",
|
||||
email: "jane@example.com",
|
||||
company: "Jane Tech",
|
||||
message:
|
||||
"Hello, I am interested in exploring your high-voltage grid solutions.",
|
||||
},
|
||||
});
|
||||
|
||||
// 2. Verify Email Sending
|
||||
// 1. Verify Email Sending
|
||||
// Note: sendMail is called twice (Notification + User Confirmation)
|
||||
expect(mockSendMail).toHaveBeenCalledTimes(2);
|
||||
|
||||
@@ -172,8 +144,7 @@ describe("Contact API Integration", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
// 3. Verify notification and analytics
|
||||
expect(mockNotify).toHaveBeenCalledTimes(1);
|
||||
// 2. Verify notifications and analytics
|
||||
expect(mockTrack).toHaveBeenCalledWith("contact-form-success", {
|
||||
has_company: true,
|
||||
});
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"paths": {
|
||||
"@/*": ["./*"],
|
||||
"@payload-config": ["./src/payload/payload.config.ts"]
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
|
||||
5
types/mdx.d.ts
vendored
Normal file
5
types/mdx.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
declare module "*.mdx" {
|
||||
import type { ComponentType } from "react";
|
||||
const component: ComponentType;
|
||||
export default component;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
@@ -16,11 +17,11 @@ export default defineConfig({
|
||||
},
|
||||
globals: true,
|
||||
setupFiles: ['./tests/setup.tsx'],
|
||||
alias: {
|
||||
'next/server': 'next/server.js',
|
||||
'@payload-config': new URL('./tests/__mocks__/payload-config.ts', import.meta.url).pathname,
|
||||
'@': new URL('./', import.meta.url).pathname,
|
||||
},
|
||||
alias: [
|
||||
{ find: 'next/server', replacement: 'next/server.js' },
|
||||
{ find: '@payload-config', replacement: fileURLToPath(new URL('./tests/__mocks__/payload-config.ts', import.meta.url)) },
|
||||
{ find: '@', replacement: fileURLToPath(new URL('./', import.meta.url)) },
|
||||
],
|
||||
exclude: ['**/node_modules/**', '**/.next/**'],
|
||||
server: {
|
||||
deps: {
|
||||
|
||||
Reference in New Issue
Block a user