Compare commits
55 Commits
v2.1.0-rc.
...
v2.2.2
| Author | SHA1 | Date | |
|---|---|---|---|
| a2cf9791ae | |||
| aa4e3aab4f | |||
| ce719a1d70 | |||
| bd2f92125b | |||
| eebe7972e0 | |||
| a9c7fa7c5e | |||
| 85e7ff71d5 | |||
| 2acb0c1608 | |||
| 082733c4f4 | |||
| af67ae7994 | |||
| 1fd247e358 | |||
| 44401cf546 | |||
| 7f106b1fa7 | |||
| 08425a3a42 | |||
| 62f1e9a89c | |||
| a5718c5013 | |||
| 82bb7240d5 | |||
| 9e7f6ec76f | |||
| b3057d8be0 | |||
| 3b45a967f7 | |||
| cadb104917 | |||
| 0be885428d | |||
| 009f12a3bf | |||
| 8e2a06d6f2 | |||
| 4f2bf3fa51 | |||
| 064ebf45e3 | |||
| e6dfeaffef | |||
| 7cdfe5d7f8 | |||
| 83f4b8eea8 | |||
| 97e76c7cac | |||
| 6caa850045 | |||
| 04ce0ecedd | |||
| 083859d52d | |||
| a13074902b | |||
| 4280f11772 | |||
| 3049c1b6e7 | |||
| 647f9a5f19 | |||
| a2872be02e | |||
| 9c3c7bd34b | |||
| 45602db7ff | |||
| 89405e6e18 | |||
| 57d54231eb | |||
| 5c4225d0a9 | |||
| e1101f2e60 | |||
| 0be6076512 | |||
| 62400943c2 | |||
| 4c60029e21 | |||
| b3c5b911d9 | |||
| 89f00c79a1 | |||
| 98ac3dbd10 | |||
| 0db4c819ff | |||
| 08a3b0be7b | |||
| a953820241 | |||
| fa02ac597f | |||
| 925765233e |
11
.env
11
.env
@@ -7,6 +7,7 @@ SENTRY_DSN=https://c10957d0182245b1a2a806ac2d34a197@errors.infra.mintel.me/1
|
|||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
NEXT_PUBLIC_FEEDBACK_ENABLED=false
|
NEXT_PUBLIC_FEEDBACK_ENABLED=false
|
||||||
NEXT_PUBLIC_RECORD_MODE_ENABLED=false
|
NEXT_PUBLIC_RECORD_MODE_ENABLED=false
|
||||||
|
NPM_TOKEN=263e7f75d8ada27f3a2e71fd6bd9d95298d48a4d
|
||||||
|
|
||||||
# SMTP Configuration
|
# SMTP Configuration
|
||||||
MAIL_HOST=smtp.eu.mailgun.org
|
MAIL_HOST=smtp.eu.mailgun.org
|
||||||
@@ -25,3 +26,13 @@ MAIL_RECIPIENTS=marc@cablecreations.de,info@klz-cables.com
|
|||||||
PAYLOAD_DB_NAME=payload
|
PAYLOAD_DB_NAME=payload
|
||||||
PAYLOAD_DB_USER=payload
|
PAYLOAD_DB_USER=payload
|
||||||
PAYLOAD_DB_PASSWORD=120in09oenaoinsd9iaidon
|
PAYLOAD_DB_PASSWORD=120in09oenaoinsd9iaidon
|
||||||
|
|
||||||
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Hetzner S3 Object Storage
|
||||||
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
|
S3_ENDPOINT=https://fsn1.your-objectstorage.com
|
||||||
|
S3_ACCESS_KEY=ROB3MSWMEIGRL7N94ZKS
|
||||||
|
S3_SECRET_KEY=9QJV3NE8xeLxhyufhNU7lsUB0RffJxPhGuEuFSH3
|
||||||
|
S3_BUCKET=mintel
|
||||||
|
S3_REGION=fsn1
|
||||||
|
S3_PREFIX=klz-cables
|
||||||
@@ -27,14 +27,13 @@ jobs:
|
|||||||
|
|
||||||
- name: 🔐 Configure Private Registry
|
- name: 🔐 Configure Private Registry
|
||||||
run: |
|
run: |
|
||||||
REGISTRY="${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}"
|
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc
|
||||||
echo "@mintel:registry=https://$REGISTRY" > .npmrc
|
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc
|
||||||
echo "//$REGISTRY/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: pnpm install --no-frozen-lockfile
|
||||||
env:
|
env:
|
||||||
NPM_TOKEN: ${{ secrets.REGISTRY_PASS }}
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
- name: 🧪 QA Checks
|
- name: 🧪 QA Checks
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -86,14 +86,16 @@ jobs:
|
|||||||
TRAEFIK_HOST="${SLUG}.branch.mintel.me"
|
TRAEFIK_HOST="${SLUG}.branch.mintel.me"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Standardize Traefik Rule
|
# Standardize Traefik Rule (escaped backticks for Traefik v3)
|
||||||
if [[ "$TRAEFIK_HOST" == *","* ]]; then
|
if [[ "$TRAEFIK_HOST" == *","* ]]; then
|
||||||
TRAEFIK_RULE=$(echo "$TRAEFIK_HOST" | sed 's/,/ /g' | awk '{for(i=1;i<=NF;i++) printf "Host(\"%s\")%s", $i, (i==NF?"":" || ")}')
|
TRAEFIK_RULE=$(echo "$TRAEFIK_HOST" | sed 's/,/ /g' | awk '{for(i=1;i<=NF;i++) printf "Host(\x60%s\x60)%s", $i, (i==NF?"":" || ")}')
|
||||||
PRIMARY_HOST=$(echo "$TRAEFIK_HOST" | cut -d',' -f1 | sed 's/ //g')
|
PRIMARY_HOST=$(echo "$TRAEFIK_HOST" | cut -d',' -f1 | sed 's/ //g')
|
||||||
else
|
else
|
||||||
TRAEFIK_RULE="Host(\"$TRAEFIK_HOST\")"
|
TRAEFIK_RULE='Host(`'"$TRAEFIK_HOST"'`)'
|
||||||
PRIMARY_HOST="$TRAEFIK_HOST"
|
PRIMARY_HOST="$TRAEFIK_HOST"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
GATEKEEPER_HOST="gatekeeper.$PRIMARY_HOST"
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "target=$TARGET"
|
echo "target=$TARGET"
|
||||||
@@ -101,6 +103,7 @@ jobs:
|
|||||||
echo "env_file=$ENV_FILE"
|
echo "env_file=$ENV_FILE"
|
||||||
echo "traefik_host=$PRIMARY_HOST"
|
echo "traefik_host=$PRIMARY_HOST"
|
||||||
echo "traefik_rule=$TRAEFIK_RULE"
|
echo "traefik_rule=$TRAEFIK_RULE"
|
||||||
|
echo "gatekeeper_host=$GATEKEEPER_HOST"
|
||||||
echo "next_public_url=https://$PRIMARY_HOST"
|
echo "next_public_url=https://$PRIMARY_HOST"
|
||||||
if [[ "$TARGET" == "production" ]]; then
|
if [[ "$TARGET" == "production" ]]; then
|
||||||
echo "project_name=klz-cablescom"
|
echo "project_name=klz-cablescom"
|
||||||
@@ -169,18 +172,20 @@ jobs:
|
|||||||
|
|
||||||
- name: 🔐 Registry Auth
|
- name: 🔐 Registry Auth
|
||||||
run: |
|
run: |
|
||||||
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
|
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc
|
||||||
echo "//${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc
|
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: |
|
||||||
|
pnpm store prune
|
||||||
|
pnpm install --no-frozen-lockfile
|
||||||
|
|
||||||
- name: 🔒 Security Audit
|
- name: 🔒 Security Audit
|
||||||
run: pnpm audit --audit-level high
|
run: pnpm audit --audit-level high || echo "⚠️ Audit found vulnerabilities (non-blocking)"
|
||||||
- name: 🧪 QA Checks
|
- name: 🧪 QA Checks
|
||||||
if: github.event.inputs.skip_checks != 'true'
|
if: github.event.inputs.skip_checks != 'true'
|
||||||
env:
|
env:
|
||||||
TURBO_TELEMETRY_DISABLED: "1"
|
TURBO_TELEMETRY_DISABLED: "1"
|
||||||
run: npx turbo run lint check:spell typecheck test --cache-dir=".turbo"
|
run: npx turbo run lint typecheck test --cache-dir=".turbo"
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
# JOB 3: Build & Push
|
# JOB 3: Build & Push
|
||||||
@@ -205,16 +210,16 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
provenance: false
|
provenance: false
|
||||||
platforms: linux/arm64
|
platforms: linux/amd64
|
||||||
build-args: |
|
build-args: |
|
||||||
NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }}
|
NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }}
|
||||||
NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }}
|
NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }}
|
||||||
UMAMI_WEBSITE_ID=${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
|
UMAMI_WEBSITE_ID=${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
|
||||||
UMAMI_API_ENDPOINT=${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
UMAMI_API_ENDPOINT=${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
||||||
NPM_TOKEN=${{ secrets.REGISTRY_PASS }}
|
NPM_TOKEN=${{ secrets.NPM_TOKEN }}
|
||||||
tags: registry.infra.mintel.me/mintel/klz-2026:${{ needs.prepare.outputs.image_tag }}
|
tags: registry.infra.mintel.me/mintel/klz-2026:${{ needs.prepare.outputs.image_tag }}
|
||||||
secrets: |
|
secrets: |
|
||||||
"NPM_TOKEN=${{ secrets.REGISTRY_PASS }}"
|
"NPM_TOKEN=${{ secrets.NPM_TOKEN }}"
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
# JOB 4: Deploy
|
# JOB 4: Deploy
|
||||||
@@ -231,6 +236,7 @@ jobs:
|
|||||||
PROJECT_NAME: ${{ needs.prepare.outputs.project_name }}
|
PROJECT_NAME: ${{ needs.prepare.outputs.project_name }}
|
||||||
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
|
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||||
TRAEFIK_HOST: ${{ needs.prepare.outputs.traefik_host }}
|
TRAEFIK_HOST: ${{ needs.prepare.outputs.traefik_host }}
|
||||||
|
GATEKEEPER_HOST: ${{ needs.prepare.outputs.gatekeeper_host }}
|
||||||
|
|
||||||
# Secrets mapping (Payload CMS)
|
# Secrets mapping (Payload CMS)
|
||||||
PAYLOAD_SECRET: ${{ secrets.PAYLOAD_SECRET || vars.PAYLOAD_SECRET || 'you-need-to-set-a-payload-secret' }}
|
PAYLOAD_SECRET: ${{ secrets.PAYLOAD_SECRET || vars.PAYLOAD_SECRET || 'you-need-to-set-a-payload-secret' }}
|
||||||
@@ -282,7 +288,7 @@ jobs:
|
|||||||
AUTH_MIDDLEWARE_UNPROTECTED="$STD_MW"
|
AUTH_MIDDLEWARE_UNPROTECTED="$STD_MW"
|
||||||
|
|
||||||
# Gatekeeper Origin
|
# Gatekeeper Origin
|
||||||
GATEKEEPER_ORIGIN="$NEXT_PUBLIC_BASE_URL/gatekeeper"
|
GATEKEEPER_ORIGIN="${NEXT_PUBLIC_BASE_URL}/gatekeeper"
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "# Generated by CI - $TARGET"
|
echo "# Generated by CI - $TARGET"
|
||||||
@@ -318,6 +324,7 @@ jobs:
|
|||||||
echo "PROJECT_NAME=$PROJECT_NAME"
|
echo "PROJECT_NAME=$PROJECT_NAME"
|
||||||
printf 'TRAEFIK_HOST_RULE=%s\n' "$TRAEFIK_RULE"
|
printf 'TRAEFIK_HOST_RULE=%s\n' "$TRAEFIK_RULE"
|
||||||
echo "TRAEFIK_HOST=$TRAEFIK_HOST"
|
echo "TRAEFIK_HOST=$TRAEFIK_HOST"
|
||||||
|
echo "GATEKEEPER_HOST=$GATEKEEPER_HOST"
|
||||||
echo "TRAEFIK_ENTRYPOINT=websecure"
|
echo "TRAEFIK_ENTRYPOINT=websecure"
|
||||||
echo "TRAEFIK_TLS=true"
|
echo "TRAEFIK_TLS=true"
|
||||||
echo "TRAEFIK_CERT_RESOLVER=le"
|
echo "TRAEFIK_CERT_RESOLVER=le"
|
||||||
@@ -378,20 +385,29 @@ jobs:
|
|||||||
REMOTE_DB_NAME=$(ssh root@alpha.mintel.me "grep -h '^PAYLOAD_DB_NAME=' $SITE_DIR/.env* 2>/dev/null | tail -1 | cut -d= -f2" || echo "payload")
|
REMOTE_DB_NAME=$(ssh root@alpha.mintel.me "grep -h '^PAYLOAD_DB_NAME=' $SITE_DIR/.env* 2>/dev/null | tail -1 | cut -d= -f2" || echo "payload")
|
||||||
REMOTE_DB_USER="${REMOTE_DB_USER:-payload}"
|
REMOTE_DB_USER="${REMOTE_DB_USER:-payload}"
|
||||||
REMOTE_DB_NAME="${REMOTE_DB_NAME:-payload}"
|
REMOTE_DB_NAME="${REMOTE_DB_NAME:-payload}"
|
||||||
ssh root@alpha.mintel.me "docker exec $DB_CONTAINER psql -U $REMOTE_DB_USER -d $REMOTE_DB_NAME -c \"
|
|
||||||
DO \\\$\\\$ BEGIN
|
# Auto-detect migrations from src/migrations/*.ts
|
||||||
DELETE FROM payload_migrations WHERE batch = -1;
|
BATCH=1
|
||||||
INSERT INTO payload_migrations (name, batch)
|
VALUES=""
|
||||||
SELECT name, batch FROM (VALUES
|
for f in $(ls src/migrations/*.ts 2>/dev/null | sort); do
|
||||||
('20260223_195005_products_collection', 1),
|
NAME=$(basename "$f" .ts)
|
||||||
('20260223_195151_remove_sku_unique', 2),
|
[ -n "$VALUES" ] && VALUES="$VALUES,"
|
||||||
('20260225_003500_add_pages_collection', 3)
|
VALUES="$VALUES ('$NAME', $BATCH)"
|
||||||
) AS v(name, batch)
|
((BATCH++))
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM payload_migrations pm WHERE pm.name = v.name);
|
done
|
||||||
EXCEPTION WHEN undefined_table THEN
|
|
||||||
RAISE NOTICE 'payload_migrations table does not exist yet — skipping sanitization';
|
if [ -n "$VALUES" ]; then
|
||||||
END \\\$\\\$;
|
ssh root@alpha.mintel.me "docker exec $DB_CONTAINER psql -U $REMOTE_DB_USER -d $REMOTE_DB_NAME -c \"
|
||||||
\"" || echo "⚠️ Migration sanitization skipped (table may not exist yet)"
|
DO \\\$\\\$ BEGIN
|
||||||
|
DELETE FROM payload_migrations WHERE batch = -1;
|
||||||
|
INSERT INTO payload_migrations (name, batch)
|
||||||
|
SELECT name, batch FROM (VALUES $VALUES) AS v(name, batch)
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM payload_migrations pm WHERE pm.name = v.name);
|
||||||
|
EXCEPTION WHEN undefined_table THEN
|
||||||
|
RAISE NOTICE 'payload_migrations table does not exist yet — skipping sanitization';
|
||||||
|
END \\\$\\\$;
|
||||||
|
\"" || echo "⚠️ Migration sanitization skipped (table may not exist yet)"
|
||||||
|
fi
|
||||||
|
|
||||||
# Restart app to pick up clean migration state
|
# Restart app to pick up clean migration state
|
||||||
APP_CONTAINER="${{ needs.prepare.outputs.project_name }}-klz-app-1"
|
APP_CONTAINER="${{ needs.prepare.outputs.project_name }}-klz-app-1"
|
||||||
@@ -426,12 +442,28 @@ jobs:
|
|||||||
node-version: 20
|
node-version: 20
|
||||||
- name: 🔐 Registry Auth
|
- name: 🔐 Registry Auth
|
||||||
run: |
|
run: |
|
||||||
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
|
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc
|
||||||
echo "//${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}/:_authToken=${{ secrets.REGISTRY_PASS }}" >> .npmrc
|
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
id: deps
|
id: deps
|
||||||
run: pnpm install --frozen-lockfile
|
run: |
|
||||||
- name: 🔍 Install Chromium (for Asset Scan)
|
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: |
|
run: |
|
||||||
rm -f /etc/apt/apt.conf.d/docker-clean
|
rm -f /etc/apt/apt.conf.d/docker-clean
|
||||||
apt-get update
|
apt-get update
|
||||||
@@ -453,139 +485,54 @@ jobs:
|
|||||||
[ -f /usr/bin/chromium ] && ln -sf /usr/bin/chromium /usr/bin/chromium-browser
|
[ -f /usr/bin/chromium ] && ln -sf /usr/bin/chromium /usr/bin/chromium-browser
|
||||||
|
|
||||||
# ── Critical Smoke Tests (MUST pass) ──────────────────────────────────
|
# ── Critical Smoke Tests (MUST pass) ──────────────────────────────────
|
||||||
|
- name: 🏥 CMS Deep Health Check
|
||||||
|
env:
|
||||||
|
DEPLOY_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||||
|
GK_PASS: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
|
||||||
|
run: |
|
||||||
|
echo "Waiting 10s for app to fully start..."
|
||||||
|
sleep 10
|
||||||
|
echo "Checking basic health..."
|
||||||
|
curl -sf "$DEPLOY_URL/health" || { echo "❌ Basic health check failed"; exit 1; }
|
||||||
|
echo "✅ Basic health OK"
|
||||||
|
echo "Checking CMS DB connectivity..."
|
||||||
|
RESPONSE=$(curl -sf "$DEPLOY_URL/api/health/cms?gk_bypass=$GK_PASS" 2>&1) || {
|
||||||
|
echo "❌ CMS health check failed!"
|
||||||
|
echo "$RESPONSE"
|
||||||
|
echo ""
|
||||||
|
echo "This usually means Payload CMS migrations failed or DB tables are missing."
|
||||||
|
echo "Check: docker logs \$APP_CONTAINER | grep -i error"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo "✅ CMS health: $RESPONSE"
|
||||||
- name: 🚀 OG Image Check
|
- name: 🚀 OG Image Check
|
||||||
if: always() && steps.deps.outcome == 'success'
|
if: always() && steps.deps.outcome == 'success'
|
||||||
env:
|
env:
|
||||||
TEST_URL: ${{ needs.prepare.outputs.next_public_url }}
|
TEST_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||||
run: pnpm run check:og
|
run: pnpm run check:og
|
||||||
- name: 🌐 Full Sitemap HTTP Validation
|
- name: 🌐 Core Smoke Tests (HTTP, API, Locale)
|
||||||
if: always() && steps.deps.outcome == 'success'
|
if: always() && steps.deps.outcome == 'success'
|
||||||
env:
|
uses: https://git.infra.mintel.me/mmintel/at-mintel/.gitea/actions/core-smoke-tests@main
|
||||||
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
|
with:
|
||||||
|
TARGET_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
|
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
|
||||||
run: pnpm run check:http
|
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
||||||
- name: 🌐 Locale & Language Switcher Validation
|
SENTRY_DSN: ${{ secrets.SENTRY_DSN || vars.SENTRY_DSN }}
|
||||||
if: always() && steps.deps.outcome == 'success'
|
|
||||||
env:
|
|
||||||
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
|
|
||||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
|
|
||||||
run: pnpm run check:locale
|
|
||||||
|
|
||||||
# ── Quality Gates (informational, don't block pipeline) ───────────────
|
- name: 📝 E2E Form Submission Test
|
||||||
- name: 🌐 HTML DOM Validation
|
|
||||||
if: always() && steps.deps.outcome == 'success'
|
if: always() && steps.deps.outcome == 'success'
|
||||||
continue-on-error: true
|
|
||||||
env:
|
|
||||||
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
|
|
||||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
|
|
||||||
run: pnpm check:html
|
|
||||||
- name: 🔒 Security Headers Scan
|
|
||||||
if: always() && steps.deps.outcome == 'success'
|
|
||||||
continue-on-error: true
|
|
||||||
env:
|
|
||||||
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
|
|
||||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
|
|
||||||
run: pnpm check:security
|
|
||||||
- name: 🔗 Lychee Deep Link Crawl
|
|
||||||
if: always() && steps.deps.outcome == 'success'
|
|
||||||
continue-on-error: true
|
|
||||||
env:
|
|
||||||
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
|
|
||||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
|
|
||||||
run: pnpm check:links
|
|
||||||
- name: 🖼️ Dynamic Asset & Image Integrity Scan
|
|
||||||
if: always() && steps.deps.outcome == 'success'
|
|
||||||
continue-on-error: true
|
|
||||||
env:
|
env:
|
||||||
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
|
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
|
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
|
||||||
PUPPETEER_EXECUTABLE_PATH: /usr/bin/chromium
|
PUPPETEER_EXECUTABLE_PATH: /usr/bin/chromium
|
||||||
CHROME_PATH: /usr/bin/chromium
|
run: pnpm run check:forms
|
||||||
run: pnpm check:assets
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
# JOB 6: Performance & Accessibility (Lighthouse + WCAG)
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
performance:
|
|
||||||
name: ⚡ Performance & Accessibility
|
|
||||||
needs: [prepare, post_deploy_checks]
|
|
||||||
continue-on-error: true
|
|
||||||
if: needs.post_deploy_checks.result == 'success' && needs.prepare.outputs.target != 'branch'
|
|
||||||
runs-on: docker
|
|
||||||
container:
|
|
||||||
image: catthehacker/ubuntu:act-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Setup pnpm
|
|
||||||
uses: pnpm/action-setup@v3
|
|
||||||
with:
|
|
||||||
version: 10
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
- 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
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install --frozen-lockfile
|
|
||||||
- name: 🔍 Install Chromium (Native & ARM64)
|
|
||||||
run: |
|
|
||||||
rm -f /etc/apt/apt.conf.d/docker-clean
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y gnupg wget ca-certificates
|
|
||||||
|
|
||||||
# Detect OS
|
|
||||||
OS_ID=$(. /etc/os-release && echo $ID)
|
|
||||||
CODENAME=$(. /etc/os-release && echo $VERSION_CODENAME)
|
|
||||||
|
|
||||||
if [ "$OS_ID" = "debian" ]; then
|
|
||||||
echo "🎯 Debian detected - installing native chromium"
|
|
||||||
apt-get install -y chromium
|
|
||||||
else
|
|
||||||
echo "🎯 Ubuntu detected - adding xtradeb PPA"
|
|
||||||
mkdir -p /etc/apt/keyrings
|
|
||||||
KEY_ID="82BB6851C64F6880"
|
|
||||||
|
|
||||||
# Fetch PPA key
|
|
||||||
wget -qO- "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_ID" | gpg --dearmor > /etc/apt/keyrings/xtradeb.gpg
|
|
||||||
|
|
||||||
# Add PPA repository
|
|
||||||
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
|
|
||||||
|
|
||||||
# PRIORITY PINNING: Force PPA over Snap-dummy
|
|
||||||
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
|
|
||||||
|
|
||||||
# Standardize binary paths
|
|
||||||
[ -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
|
|
||||||
- name: ⚡ Lighthouse CI
|
|
||||||
env:
|
|
||||||
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
|
|
||||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
|
|
||||||
CHROME_PATH: /usr/bin/chromium
|
|
||||||
PAGESPEED_LIMIT: 8
|
|
||||||
run: pnpm run pagespeed:test
|
|
||||||
- name: ♿ WCAG Audit
|
|
||||||
env:
|
|
||||||
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
|
|
||||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
|
|
||||||
CHROME_PATH: /usr/bin/chromium
|
|
||||||
PAGESPEED_LIMIT: 8
|
|
||||||
run: pnpm run check:wcag
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
# JOB 7: Notifications
|
# JOB 7: Notifications
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
notifications:
|
notifications:
|
||||||
name: 🔔 Notify
|
name: 🔔 Notify
|
||||||
needs: [prepare, deploy, post_deploy_checks, performance]
|
needs: [prepare, deploy, post_deploy_checks]
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container:
|
container:
|
||||||
@@ -596,7 +543,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
DEPLOY="${{ needs.deploy.result }}"
|
DEPLOY="${{ needs.deploy.result }}"
|
||||||
SMOKE="${{ needs.post_deploy_checks.result }}"
|
SMOKE="${{ needs.post_deploy_checks.result }}"
|
||||||
PERF="${{ needs.performance.result }}"
|
PERF="${{ needs.post_deploy_checks.result }}"
|
||||||
TARGET="${{ needs.prepare.outputs.target }}"
|
TARGET="${{ needs.prepare.outputs.target }}"
|
||||||
VERSION="${{ needs.prepare.outputs.image_tag }}"
|
VERSION="${{ needs.prepare.outputs.image_tag }}"
|
||||||
URL="${{ needs.prepare.outputs.next_public_url }}"
|
URL="${{ needs.prepare.outputs.next_public_url }}"
|
||||||
|
|||||||
17
.gitea/workflows/qa.yml
Normal file
17
.gitea/workflows/qa.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
name: Nightly QA
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 3 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
call-qa-workflow:
|
||||||
|
uses: mmintel/at-mintel/.gitea/workflows/quality-assurance-template.yml@main
|
||||||
|
with:
|
||||||
|
TARGET_URL: 'https://testing.klz-cables.com'
|
||||||
|
PROJECT_NAME: 'klz-2026'
|
||||||
|
secrets:
|
||||||
|
GOTIFY_URL: ${{ secrets.GOTIFY_URL }}
|
||||||
|
GOTIFY_TOKEN: ${{ secrets.GOTIFY_TOKEN }}
|
||||||
|
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
|
||||||
4
.npmrc
4
.npmrc
@@ -1,2 +1,2 @@
|
|||||||
@mintel:registry=https://npm.infra.mintel.me/
|
@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm/
|
||||||
//npm.infra.mintel.me/:_authToken=${NPM_TOKEN}
|
//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${NPM_TOKEN}
|
||||||
|
|||||||
18
Dockerfile
18
Dockerfile
@@ -1,5 +1,5 @@
|
|||||||
# Stage 1: Builder
|
# Stage 1: Builder
|
||||||
FROM registry.infra.mintel.me/mintel/nextjs:v1.7.10 AS base
|
FROM registry.infra.mintel.me/mintel/nextjs:v1.8.20 AS base
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Arguments for build-time configuration
|
# Arguments for build-time configuration
|
||||||
@@ -25,9 +25,10 @@ COPY pnpm-lock.yaml package.json .npmrc* ./
|
|||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
|
||||||
--mount=type=secret,id=NPM_TOKEN \
|
--mount=type=secret,id=NPM_TOKEN \
|
||||||
export NPM_TOKEN=$(cat /run/secrets/NPM_TOKEN) && \
|
export NPM_TOKEN=$(cat /run/secrets/NPM_TOKEN) && \
|
||||||
echo "@mintel:registry=https://npm.infra.mintel.me" > .npmrc && \
|
echo "@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm" > .npmrc && \
|
||||||
echo "//npm.infra.mintel.me/:_authToken=\${NPM_TOKEN}" >> .npmrc && \
|
echo "//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=\${NPM_TOKEN}" >> .npmrc && \
|
||||||
pnpm install --frozen-lockfile && \
|
pnpm store prune && \
|
||||||
|
pnpm install --no-frozen-lockfile && \
|
||||||
rm .npmrc
|
rm .npmrc
|
||||||
|
|
||||||
# Copy source code
|
# Copy source code
|
||||||
@@ -43,10 +44,15 @@ CMD ["pnpm", "dev:local"]
|
|||||||
FROM base AS builder
|
FROM base AS builder
|
||||||
# Limit memory to 1GB to prevent ResourceExhausted in combination with worker limits
|
# Limit memory to 1GB to prevent ResourceExhausted in combination with worker limits
|
||||||
ENV NODE_OPTIONS="--max-old-space-size=1024"
|
ENV NODE_OPTIONS="--max-old-space-size=1024"
|
||||||
|
|
||||||
|
# Force Turbopack (Rust/Rayon) and Node.js to use strictly 3 threads to avoid starving the Gitea Runner VPS CPU
|
||||||
|
ENV RAYON_NUM_THREADS=3
|
||||||
|
ENV UV_THREADPOOL_SIZE=3
|
||||||
|
|
||||||
RUN pnpm build
|
RUN pnpm build
|
||||||
|
|
||||||
# Stage 3: Runner
|
# Stage 2: Runner
|
||||||
FROM registry.infra.mintel.me/mintel/runtime:v1.7.10 AS runner
|
FROM registry.infra.mintel.me/mintel/runtime:v1.8.20 AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Create nextjs user and group (standardized in runtime image but ensuring local ownership)
|
# Create nextjs user and group (standardized in runtime image but ensuring local ownership)
|
||||||
|
|||||||
@@ -462,3 +462,4 @@ Proprietary - KLZ Cables
|
|||||||
**Status**: ✅ **READY FOR DEPLOYMENT**
|
**Status**: ✅ **READY FOR DEPLOYMENT**
|
||||||
**Version**: 1.0.0
|
**Version**: 1.0.0
|
||||||
**Last Updated**: December 27, 2025
|
**Last Updated**: December 27, 2025
|
||||||
|
Trigger rebuilding for x86 architecture.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { notFound } from 'next/navigation';
|
import { notFound, redirect } from 'next/navigation';
|
||||||
import { Container, Badge, Heading } from '@/components/ui';
|
import { Container, Badge, Heading } from '@/components/ui';
|
||||||
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
@@ -21,15 +21,18 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
|||||||
|
|
||||||
if (!pageData) return {};
|
if (!pageData) return {};
|
||||||
|
|
||||||
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
const fileSlug = await mapSlugToFileSlug(pageData.slug || slug, locale);
|
||||||
const deSlug = await mapFileSlugToTranslated(fileSlug, 'de');
|
const deSlug = await mapFileSlugToTranslated(fileSlug, 'de');
|
||||||
const enSlug = await mapFileSlugToTranslated(fileSlug, 'en');
|
const enSlug = await mapFileSlugToTranslated(fileSlug, 'en');
|
||||||
|
|
||||||
|
// Determine correct localized slug based on current locale
|
||||||
|
const currentLocaleSlug = locale === 'de' ? deSlug : enSlug;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: pageData.frontmatter.title,
|
title: pageData.frontmatter.title,
|
||||||
description: pageData.frontmatter.excerpt || '',
|
description: pageData.frontmatter.excerpt || '',
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: `${SITE_URL}/${locale}/${slug}`,
|
canonical: `${SITE_URL}/${locale}/${currentLocaleSlug}`,
|
||||||
languages: {
|
languages: {
|
||||||
de: `${SITE_URL}/de/${deSlug}`,
|
de: `${SITE_URL}/de/${deSlug}`,
|
||||||
en: `${SITE_URL}/en/${enSlug}`,
|
en: `${SITE_URL}/en/${enSlug}`,
|
||||||
@@ -39,7 +42,7 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
|||||||
openGraph: {
|
openGraph: {
|
||||||
title: `${pageData.frontmatter.title} | KLZ Cables`,
|
title: `${pageData.frontmatter.title} | KLZ Cables`,
|
||||||
description: pageData.frontmatter.excerpt || '',
|
description: pageData.frontmatter.excerpt || '',
|
||||||
url: `${SITE_URL}/${locale}/${slug}`,
|
url: `${SITE_URL}/${locale}/${currentLocaleSlug}`,
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: 'summary_large_image',
|
card: 'summary_large_image',
|
||||||
@@ -59,6 +62,13 @@ export default async function StandardPage({ params }: PageProps) {
|
|||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Redirect if accessed via a different locale's slug
|
||||||
|
const fileSlug = await mapSlugToFileSlug(pageData.slug || slug, locale);
|
||||||
|
const correctSlug = await mapFileSlugToTranslated(fileSlug, locale);
|
||||||
|
if (correctSlug && correctSlug !== slug) {
|
||||||
|
redirect(`/${locale}/${correctSlug}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Full-bleed pages render blocks edge-to-edge without the generic article wrapper
|
// Full-bleed pages render blocks edge-to-edge without the generic article wrapper
|
||||||
if (pageData.frontmatter.layout === 'fullBleed') {
|
if (pageData.frontmatter.layout === 'fullBleed') {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { notFound } from 'next/navigation';
|
import { notFound, redirect } from 'next/navigation';
|
||||||
import JsonLd from '@/components/JsonLd';
|
import JsonLd from '@/components/JsonLd';
|
||||||
import { SITE_URL } from '@/lib/schema';
|
import { SITE_URL } from '@/lib/schema';
|
||||||
import { getPostBySlug, getAdjacentPosts, getReadingTime } from '@/lib/blog';
|
import { getPostBySlug, getAdjacentPosts, getReadingTime } from '@/lib/blog';
|
||||||
@@ -32,7 +32,7 @@ export async function generateMetadata({ params }: BlogPostProps): Promise<Metad
|
|||||||
title: post.frontmatter.title,
|
title: post.frontmatter.title,
|
||||||
description: description,
|
description: description,
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: `${SITE_URL}/${locale}/blog/${slug}`,
|
canonical: `${SITE_URL}/${locale}/blog/${post.slug}`,
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: `${post.frontmatter.title} | KLZ Cables`,
|
title: `${post.frontmatter.title} | KLZ Cables`,
|
||||||
@@ -40,7 +40,7 @@ export async function generateMetadata({ params }: BlogPostProps): Promise<Metad
|
|||||||
type: 'article',
|
type: 'article',
|
||||||
publishedTime: post.frontmatter.date,
|
publishedTime: post.frontmatter.date,
|
||||||
authors: ['KLZ Cables'],
|
authors: ['KLZ Cables'],
|
||||||
url: `${SITE_URL}/${locale}/blog/${slug}`,
|
url: `${SITE_URL}/${locale}/blog/${post.slug}`,
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: 'summary_large_image',
|
card: 'summary_large_image',
|
||||||
@@ -54,12 +54,19 @@ export default async function BlogPost({ params }: BlogPostProps) {
|
|||||||
const { locale, slug } = await params;
|
const { locale, slug } = await params;
|
||||||
setRequestLocale(locale);
|
setRequestLocale(locale);
|
||||||
const post = await getPostBySlug(slug, locale);
|
const post = await getPostBySlug(slug, locale);
|
||||||
const { prev, next, isPrevRandom, isNextRandom } = await getAdjacentPosts(slug, locale);
|
|
||||||
|
|
||||||
if (!post) {
|
if (!post) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the user accessed this post using a slug from a different locale
|
||||||
|
// (e.g. via the generic language switcher), redirect them to the correct localized slug URL
|
||||||
|
if (post.slug && post.slug !== slug) {
|
||||||
|
redirect(`/${locale}/blog/${post.slug}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { prev, next, isPrevRandom, isNextRandom } = await getAdjacentPosts(post.slug, locale);
|
||||||
|
|
||||||
// Convert Lexical content into a plain string to estimate reading time roughly
|
// Convert Lexical content into a plain string to estimate reading time roughly
|
||||||
const rawTextContent = JSON.stringify(post.content);
|
const rawTextContent = JSON.stringify(post.content);
|
||||||
|
|
||||||
@@ -88,7 +95,7 @@ export default async function BlogPost({ params }: BlogPostProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-neutral-dark via-neutral-dark/40 to-transparent" />
|
<div className="absolute inset-0 bg-gradient-to-t from-neutral-dark/90 via-neutral-dark/40 to-transparent" />
|
||||||
|
|
||||||
{/* Title overlay on image */}
|
{/* Title overlay on image */}
|
||||||
<div className="absolute inset-0 flex flex-col justify-end pb-16 md:pb-24">
|
<div className="absolute inset-0 flex flex-col justify-end pb-16 md:pb-24">
|
||||||
@@ -105,7 +112,7 @@ export default async function BlogPost({ params }: BlogPostProps) {
|
|||||||
{post.frontmatter.title}
|
{post.frontmatter.title}
|
||||||
</Heading>
|
</Heading>
|
||||||
<div className="flex flex-wrap items-center gap-6 text-white/80 text-sm md:text-base font-medium">
|
<div className="flex flex-wrap items-center gap-6 text-white/80 text-sm md:text-base font-medium">
|
||||||
<time dateTime={post.frontmatter.date}>
|
<time dateTime={post.frontmatter.date} suppressHydrationWarning>
|
||||||
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
|
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
@@ -142,7 +149,7 @@ export default async function BlogPost({ params }: BlogPostProps) {
|
|||||||
{post.frontmatter.title}
|
{post.frontmatter.title}
|
||||||
</Heading>
|
</Heading>
|
||||||
<div className="flex items-center gap-6 text-text-primary/80 font-medium">
|
<div className="flex items-center gap-6 text-text-primary/80 font-medium">
|
||||||
<time dateTime={post.frontmatter.date}>
|
<time dateTime={post.frontmatter.date} suppressHydrationWarning>
|
||||||
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
|
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
|
|||||||
sizes="100vw"
|
sizes="100vw"
|
||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 image-overlay-gradient" />
|
<div className="absolute inset-0 bg-neutral-dark/20" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -84,12 +84,9 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
|
|||||||
{featuredPost &&
|
{featuredPost &&
|
||||||
(new Date(featuredPost.frontmatter.date) > new Date() ||
|
(new Date(featuredPost.frontmatter.date) > new Date() ||
|
||||||
featuredPost.frontmatter.public === false) && (
|
featuredPost.frontmatter.public === false) && (
|
||||||
<Badge
|
<span className="px-2 py-0.5 border border-white/40 text-white/80 rounded uppercase tracking-widest text-[10px] md:text-xs font-bold">
|
||||||
variant="neutral"
|
|
||||||
className="border border-white/30 bg-transparent text-white/80 shadow-none"
|
|
||||||
>
|
|
||||||
Draft Preview
|
Draft Preview
|
||||||
</Badge>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{featuredPost && (
|
{featuredPost && (
|
||||||
@@ -156,66 +153,76 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
|
|||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
{/* Grid for remaining posts */}
|
{/* Grid for remaining posts */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-12">
|
<div className="grid grid-cols-1 gap-12">
|
||||||
{remainingPosts.map((post, idx) => (
|
{remainingPosts.map((post, idx) => (
|
||||||
<Reveal key={post.slug} delay={idx * 100}>
|
<Reveal key={post.slug} delay={idx * 50}>
|
||||||
<Link href={`/${locale}/blog/${post.slug}`} className="group block">
|
<Link
|
||||||
|
href={`/${locale}/blog/${post.slug}`}
|
||||||
|
className="group block focus:outline-none"
|
||||||
|
>
|
||||||
<Card
|
<Card
|
||||||
tag="article"
|
tag="article"
|
||||||
className="h-full flex flex-col border-none shadow-lg hover:shadow-2xl transition-all duration-500 rounded-2xl md:rounded-3xl overflow-hidden"
|
className="relative flex flex-col justify-end border-none shadow-lg hover:shadow-2xl transition-all duration-500 rounded-3xl overflow-hidden min-h-[450px] md:min-h-[500px]"
|
||||||
>
|
>
|
||||||
{post.frontmatter.featuredImage && (
|
{post.frontmatter.featuredImage && (
|
||||||
<div className="relative h-48 md:h-72 overflow-hidden">
|
<>
|
||||||
<Image
|
<Image
|
||||||
src={post.frontmatter.featuredImage.split('?')[0]}
|
src={post.frontmatter.featuredImage.split('?')[0]}
|
||||||
alt={post.frontmatter.title}
|
alt={post.frontmatter.title}
|
||||||
fill
|
fill
|
||||||
className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-110"
|
className="absolute inset-0 w-full h-full object-cover transition-transform duration-1000 group-hover:scale-105"
|
||||||
style={{
|
style={{
|
||||||
objectPosition: `${post.frontmatter.focalX ?? 50}% ${post.frontmatter.focalY ?? 50}%`,
|
objectPosition: `${post.frontmatter.focalX ?? 50}% ${post.frontmatter.focalY ?? 50}%`,
|
||||||
}}
|
}}
|
||||||
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
sizes="(max-width: 768px) 100vw, 100vw"
|
||||||
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 image-overlay-gradient opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
<div className="absolute inset-0 bg-neutral-dark/10 group-hover:bg-neutral-dark/5 transition-colors duration-500" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="relative z-10 w-full p-6 md:p-10 bg-gradient-to-t from-neutral-dark/95 via-neutral-dark/70 to-transparent flex flex-col pt-40">
|
||||||
|
<div className="flex flex-wrap items-center gap-4 mb-4">
|
||||||
{post.frontmatter.category && (
|
{post.frontmatter.category && (
|
||||||
<Badge
|
<Badge variant="accent" className="shadow-md">
|
||||||
variant="accent"
|
|
||||||
className="absolute top-3 left-3 md:top-6 md:left-6 shadow-lg"
|
|
||||||
>
|
|
||||||
{post.frontmatter.category}
|
{post.frontmatter.category}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
{(new Date(post.frontmatter.date) > new Date() ||
|
||||||
|
post.frontmatter.public === false) && (
|
||||||
|
<span className="px-2 py-0.5 border border-white/40 text-white/90 rounded uppercase tracking-widest text-[10px] md:text-xs font-bold bg-neutral-dark/40 shadow-sm">
|
||||||
|
Draft Preview
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<div className="p-5 md:p-10 flex flex-col flex-1">
|
<div className="flex items-center gap-3 text-xs md:text-sm font-bold text-white/80 mb-3 tracking-widest uppercase">
|
||||||
<div className="flex items-center gap-3 text-[10px] md:text-sm font-bold text-primary/70 mb-2 md:mb-4 tracking-widest uppercase">
|
<time dateTime={post.frontmatter.date} suppressHydrationWarning>
|
||||||
<span>
|
|
||||||
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
|
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
})}
|
})}
|
||||||
</span>
|
</time>
|
||||||
{(new Date(post.frontmatter.date) > new Date() ||
|
|
||||||
post.frontmatter.public === false) && (
|
|
||||||
<span className="px-1.5 py-0.5 border border-current rounded-sm text-[9px] md:text-xs">
|
|
||||||
Draft
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg md:text-2xl font-bold text-primary mb-3 md:mb-6 group-hover:text-accent-dark transition-colors line-clamp-3 md:line-clamp-4 leading-tight">
|
|
||||||
|
<h3 className="text-xl md:text-3xl font-bold text-white mb-4 group-hover:text-accent transition-colors drop-shadow-md leading-tight max-w-4xl">
|
||||||
{post.frontmatter.title}
|
{post.frontmatter.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-text-secondary text-sm md:text-lg line-clamp-3 md:line-clamp-4 mb-4 md:mb-8 leading-relaxed">
|
|
||||||
{post.frontmatter.excerpt}
|
{post.frontmatter.excerpt && (
|
||||||
</p>
|
<p className="text-white/90 text-sm md:text-lg line-clamp-3 mb-6 max-w-4xl drop-shadow-sm leading-relaxed">
|
||||||
<div className="mt-auto pt-4 md:pt-8 border-t border-neutral-medium flex items-center justify-between">
|
{post.frontmatter.excerpt}
|
||||||
<span className="text-saturated text-sm md:text-base font-extrabold group-hover:text-accent-dark transition-colors">
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-auto flex items-center justify-between border-t border-white/20 pt-6">
|
||||||
|
<span className="text-accent text-sm md:text-base font-extrabold group-hover:text-white transition-colors">
|
||||||
{t('readMore')}
|
{t('readMore')}
|
||||||
</span>
|
</span>
|
||||||
<div className="w-8 h-8 md:w-10 md:h-10 rounded-full bg-primary-light flex items-center justify-center text-saturated group-hover:bg-accent group-hover:text-primary-dark transition-all duration-300">
|
<div className="w-10 h-10 rounded-full bg-white/10 flex items-center justify-center text-accent group-hover:bg-accent group-hover:text-primary-dark transition-all duration-300 backdrop-blur-sm border border-white/20">
|
||||||
<svg
|
<svg
|
||||||
className="w-4 h-4 md:w-5 md:h-5 transition-transform group-hover:translate-x-1"
|
className="w-5 h-5 transition-transform group-hover:translate-x-1"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
|||||||
@@ -59,6 +59,21 @@ export default async function ContactPage({ params }: ContactPageProps) {
|
|||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
setRequestLocale(locale);
|
setRequestLocale(locale);
|
||||||
const t = await getTranslations({ locale, namespace: 'Contact' });
|
const t = await getTranslations({ locale, namespace: 'Contact' });
|
||||||
|
|
||||||
|
// Get translated slug to redirect if user used incorrect static slug
|
||||||
|
const { headers } = await import('next/headers');
|
||||||
|
const headersList = await headers();
|
||||||
|
const urlPath = headersList.get('x-invoke-path') || '';
|
||||||
|
const currentSlug = urlPath.split('/').pop();
|
||||||
|
|
||||||
|
if (currentSlug) {
|
||||||
|
const contactSlugDe = locale === 'de' ? 'kontakt' : 'contact';
|
||||||
|
if (currentSlug !== contactSlugDe && (currentSlug === 'kontakt' || currentSlug === 'contact')) {
|
||||||
|
const { redirect } = await import('next/navigation');
|
||||||
|
redirect(`/${locale}/${contactSlugDe}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col min-h-screen bg-neutral-light">
|
<div className="flex flex-col min-h-screen bg-neutral-light">
|
||||||
<JsonLd
|
<JsonLd
|
||||||
|
|||||||
@@ -1,66 +1,137 @@
|
|||||||
'use client';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { useTranslations } from 'next-intl';
|
|
||||||
import { Container, Button, Heading } from '@/components/ui';
|
import { Container, Button, Heading } from '@/components/ui';
|
||||||
import Scribble from '@/components/Scribble';
|
import Scribble from '@/components/Scribble';
|
||||||
import { useEffect } from 'react';
|
import { getPayload } from 'payload';
|
||||||
import { useAnalytics } from '@/components/analytics/useAnalytics';
|
import configPromise from '@payload-config';
|
||||||
import { AnalyticsEvents } from '@/components/analytics/analytics-events';
|
import { headers } from 'next/headers';
|
||||||
|
import ClientNotFoundTracker from '@/components/analytics/ClientNotFoundTracker';
|
||||||
|
|
||||||
export default function NotFound() {
|
export default async function NotFound() {
|
||||||
const t = useTranslations('Error.notFound');
|
const t = await getTranslations('Error.notFound');
|
||||||
const { trackEvent } = useAnalytics();
|
|
||||||
|
|
||||||
useEffect(() => {
|
// Try to determine the requested path
|
||||||
const errorUrl = typeof window !== 'undefined' ? window.location.pathname : 'unknown';
|
const headersList = await headers();
|
||||||
trackEvent(AnalyticsEvents.ERROR, {
|
const urlPath = headersList.get('x-invoke-path') || '';
|
||||||
type: '404_not_found',
|
|
||||||
path: errorUrl,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Explicitly send the 404 to Sentry so we have visibility into broken links
|
let suggestedUrl = null;
|
||||||
import('@sentry/nextjs').then((Sentry) => {
|
let suggestedLang = null;
|
||||||
Sentry.withScope((scope) => {
|
|
||||||
scope.setTag('status_code', '404');
|
// If we have a path, try to see if the last segment (slug) exists in ANY locale
|
||||||
scope.setTag('path', errorUrl);
|
if (urlPath) {
|
||||||
Sentry.captureMessage(`Route Not Found: ${errorUrl}`, 'warning');
|
const slug = urlPath.split('/').filter(Boolean).pop();
|
||||||
});
|
if (slug) {
|
||||||
});
|
try {
|
||||||
}, [trackEvent]);
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
|
||||||
|
// Check posts
|
||||||
|
const postRes = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
where: { slug: { equals: slug } },
|
||||||
|
locale: 'all',
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check products
|
||||||
|
const productRes =
|
||||||
|
postRes.docs.length === 0
|
||||||
|
? await payload.find({
|
||||||
|
collection: 'products',
|
||||||
|
where: { slug: { equals: slug } },
|
||||||
|
locale: 'all',
|
||||||
|
limit: 1,
|
||||||
|
})
|
||||||
|
: { docs: [] };
|
||||||
|
|
||||||
|
// Check pages
|
||||||
|
const pageRes =
|
||||||
|
postRes.docs.length === 0 && productRes.docs.length === 0
|
||||||
|
? await payload.find({
|
||||||
|
collection: 'pages',
|
||||||
|
where: { slug: { equals: slug } },
|
||||||
|
locale: 'all',
|
||||||
|
limit: 1,
|
||||||
|
})
|
||||||
|
: { docs: [] };
|
||||||
|
|
||||||
|
const anyDoc = postRes.docs[0] || productRes.docs[0] || pageRes.docs[0];
|
||||||
|
|
||||||
|
if (anyDoc) {
|
||||||
|
// If the doc exists, we can figure out its native locale or
|
||||||
|
// offer the alternative locale (if we are in 'de', offer 'en')
|
||||||
|
const currentLocale = urlPath.startsWith('/en') ? 'en' : 'de';
|
||||||
|
const alternativeLocale = currentLocale === 'de' ? 'en' : 'de';
|
||||||
|
|
||||||
|
suggestedLang = alternativeLocale === 'de' ? 'Deutsch' : 'English';
|
||||||
|
|
||||||
|
// Reconstruct the URL for the alternative locale
|
||||||
|
const pathParts = urlPath.split('/').filter(Boolean);
|
||||||
|
if (pathParts.length > 0 && (pathParts[0] === 'en' || pathParts[0] === 'de')) {
|
||||||
|
pathParts[0] = alternativeLocale;
|
||||||
|
} else {
|
||||||
|
pathParts.unshift(alternativeLocale);
|
||||||
|
}
|
||||||
|
suggestedUrl = '/' + pathParts.join('/');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore Payload errors in 404
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="relative py-24 flex flex-col items-center justify-center text-center min-h-[70vh] overflow-hidden">
|
<>
|
||||||
{/* Industrial Background Element */}
|
<ClientNotFoundTracker path={urlPath} />
|
||||||
<div className="absolute inset-0 -z-10 opacity-[0.03] pointer-events-none flex items-center justify-center">
|
<Container className="relative py-24 flex flex-col items-center justify-center text-center min-h-[70vh] overflow-hidden">
|
||||||
<span className="text-[20rem] font-bold select-none">404</span>
|
{/* Industrial Background Element */}
|
||||||
</div>
|
<div className="absolute inset-0 -z-10 opacity-[0.03] pointer-events-none flex items-center justify-center">
|
||||||
|
<span className="text-[20rem] font-bold select-none">404</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="relative mb-8">
|
<div className="relative mb-8">
|
||||||
<Heading level={1} className="text-6xl md:text-8xl font-bold mb-2">
|
<Heading level={1} className="text-6xl md:text-8xl font-bold mb-2">
|
||||||
404
|
404
|
||||||
|
</Heading>
|
||||||
|
<Scribble
|
||||||
|
variant="circle"
|
||||||
|
className="w-[150%] h-[150%] -top-[25%] -left-[25%] text-accent/40"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Heading level={2} className="text-2xl md:text-3xl font-bold mb-4 text-primary">
|
||||||
|
{t('title')}
|
||||||
</Heading>
|
</Heading>
|
||||||
<Scribble
|
|
||||||
variant="circle"
|
|
||||||
className="w-[150%] h-[150%] -top-[25%] -left-[25%] text-accent/40"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Heading level={2} className="text-2xl md:text-3xl font-bold mb-4 text-primary">
|
<p className="text-text-secondary mb-10 max-w-md text-lg">{t('description')}</p>
|
||||||
{t('title')}
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
<p className="text-white/60 mb-10 max-w-md text-lg">{t('description')}</p>
|
{suggestedUrl && (
|
||||||
|
<div className="mb-12 p-6 bg-accent/10 border border-accent/20 rounded-2xl animate-fade-in shadow-lg relative overflow-hidden group">
|
||||||
|
<div className="absolute inset-0 bg-accent/5 -skew-x-12 translate-x-full group-hover:translate-x-0 transition-transform duration-700" />
|
||||||
|
<div className="relative z-10">
|
||||||
|
<h3 className="text-primary font-bold mb-2 text-lg">
|
||||||
|
Did you mean to visit the {suggestedLang} version?
|
||||||
|
</h3>
|
||||||
|
<p className="text-text-secondary text-sm mb-4">
|
||||||
|
This page exists, but in another language.
|
||||||
|
</p>
|
||||||
|
<Button href={suggestedUrl} variant="accent" size="md" className="w-full sm:w-auto">
|
||||||
|
Go to {suggestedLang} Version
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row gap-4">
|
<div className="flex flex-col sm:flex-row gap-4">
|
||||||
<Button href="/" variant="accent" size="lg">
|
<Button href="/" variant={suggestedUrl ? 'outline' : 'accent'} size="lg">
|
||||||
{t('cta')}
|
{t('cta')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button href="/contact" variant="outline" size="lg">
|
<Button href="/contact" variant={suggestedUrl ? 'ghost' : 'outline'} size="lg">
|
||||||
Contact Support
|
Contact Support
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Decorative Industrial Line */}
|
{/* Decorative Industrial Line */}
|
||||||
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-px h-24 bg-gradient-to-t from-accent/50 to-transparent" />
|
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-px h-24 bg-gradient-to-t from-accent/50 to-transparent" />
|
||||||
</Container>
|
</Container>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { getTranslations, setRequestLocale } from 'next-intl/server';
|
|||||||
import { getProductOGImageMetadata } from '@/lib/metadata';
|
import { getProductOGImageMetadata } from '@/lib/metadata';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound, redirect } from 'next/navigation';
|
||||||
import ProductEngagementTracker from '@/components/analytics/ProductEngagementTracker';
|
import ProductEngagementTracker from '@/components/analytics/ProductEngagementTracker';
|
||||||
import PayloadRichText from '@/components/PayloadRichText';
|
import PayloadRichText from '@/components/PayloadRichText';
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
|
|||||||
title: categoryTitle,
|
title: categoryTitle,
|
||||||
description: categoryDesc,
|
description: categoryDesc,
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: `${SITE_URL}/${locale}/${await mapFileSlugToTranslated('products', locale)}/${productSlug}`,
|
canonical: `${SITE_URL}/${locale}/${await mapFileSlugToTranslated('products', locale)}/${await mapFileSlugToTranslated(fileSlug, locale)}`,
|
||||||
languages: {
|
languages: {
|
||||||
de: `${SITE_URL}/de/${await mapFileSlugToTranslated('products', 'de')}/${await mapFileSlugToTranslated(fileSlug, 'de')}`,
|
de: `${SITE_URL}/de/${await mapFileSlugToTranslated('products', 'de')}/${await mapFileSlugToTranslated(fileSlug, 'de')}`,
|
||||||
en: `${SITE_URL}/en/${await mapFileSlugToTranslated('products', 'en')}/${await mapFileSlugToTranslated(fileSlug, 'en')}`,
|
en: `${SITE_URL}/en/${await mapFileSlugToTranslated('products', 'en')}/${await mapFileSlugToTranslated(fileSlug, 'en')}`,
|
||||||
@@ -75,11 +75,13 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
|
|||||||
const product = await getProductBySlug(productSlug, locale);
|
const product = await getProductBySlug(productSlug, locale);
|
||||||
if (!product) return {};
|
if (!product) return {};
|
||||||
|
|
||||||
|
const currentLocalePath = await getLocalizedPath(locale);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: product.frontmatter.title,
|
title: product.frontmatter.title,
|
||||||
description: product.frontmatter.description,
|
description: product.frontmatter.description,
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: `${SITE_URL}/${locale}/${await mapFileSlugToTranslated('products', locale)}/${slug.join('/')}`,
|
canonical: `${SITE_URL}/${locale}/${currentLocalePath}`,
|
||||||
languages: {
|
languages: {
|
||||||
de: `${SITE_URL}/de/${await getLocalizedPath('de')}`,
|
de: `${SITE_URL}/de/${await getLocalizedPath('de')}`,
|
||||||
en: `${SITE_URL}/en/${await getLocalizedPath('en')}`,
|
en: `${SITE_URL}/en/${await getLocalizedPath('en')}`,
|
||||||
@@ -90,7 +92,7 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
|
|||||||
title: product.frontmatter.title,
|
title: product.frontmatter.title,
|
||||||
description: product.frontmatter.description,
|
description: product.frontmatter.description,
|
||||||
type: 'website',
|
type: 'website',
|
||||||
url: `${SITE_URL}/${locale}/products/${slug.join('/')}`,
|
url: `${SITE_URL}/${locale}/${currentLocalePath}`,
|
||||||
images: getProductOGImageMetadata(productSlug, product.frontmatter.title, locale),
|
images: getProductOGImageMetadata(productSlug, product.frontmatter.title, locale),
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
@@ -114,7 +116,19 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
'high-voltage-cables',
|
'high-voltage-cables',
|
||||||
'solar-cables',
|
'solar-cables',
|
||||||
];
|
];
|
||||||
const fileSlug = await mapSlugToFileSlug(productSlug, locale);
|
|
||||||
|
const fileSlugs = await Promise.all(slug.map((s) => mapSlugToFileSlug(s, locale)));
|
||||||
|
const translatedSlugsForLocale = await Promise.all(
|
||||||
|
fileSlugs.map((fs) => mapFileSlugToTranslated(fs, locale)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// If the requested slugs don't exactly match the translated slugs for the current locale
|
||||||
|
// (i.e. if the user used the static language switcher but kept the original locale's slugs)
|
||||||
|
if (slug.join('/') !== translatedSlugsForLocale.join('/')) {
|
||||||
|
redirect(`/${locale}/${productsSlug}/${translatedSlugsForLocale.join('/')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileSlug = fileSlugs[fileSlugs.length - 1];
|
||||||
|
|
||||||
if (categories.includes(fileSlug)) {
|
if (categories.includes(fileSlug)) {
|
||||||
const allProducts = await getAllProducts(locale);
|
const allProducts = await getAllProducts(locale);
|
||||||
@@ -125,11 +139,26 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
? t(`categories.${categoryKey}.title`)
|
? t(`categories.${categoryKey}.title`)
|
||||||
: fileSlug;
|
: fileSlug;
|
||||||
|
|
||||||
const filteredProducts = allProducts.filter((p) =>
|
const filteredProducts = allProducts.filter((p) => {
|
||||||
p.frontmatter.categories.some(
|
const firstCat = p.frontmatter.categories[0] || '';
|
||||||
(cat) => cat.toLowerCase().replace(/\s+/g, '-') === fileSlug || cat === categoryTitle,
|
const normalizedCat = firstCat.toLowerCase().replace(/\s+/g, '-');
|
||||||
),
|
let pFileSlug = 'low-voltage-cables';
|
||||||
);
|
if (normalizedCat === 'hochspannungskabel' || normalizedCat === 'high-voltage-cables')
|
||||||
|
pFileSlug = 'high-voltage-cables';
|
||||||
|
else if (
|
||||||
|
normalizedCat === 'mittelspannungskabel' ||
|
||||||
|
normalizedCat === 'medium-voltage-cables'
|
||||||
|
)
|
||||||
|
pFileSlug = 'medium-voltage-cables';
|
||||||
|
else if (
|
||||||
|
normalizedCat === 'solarkabel' ||
|
||||||
|
normalizedCat === 'solar-cables' ||
|
||||||
|
normalizedCat === 'solar'
|
||||||
|
)
|
||||||
|
pFileSlug = 'solar-cables';
|
||||||
|
|
||||||
|
return pFileSlug === fileSlug;
|
||||||
|
});
|
||||||
|
|
||||||
const productsWithTranslatedSlugs = await Promise.all(
|
const productsWithTranslatedSlugs = await Promise.all(
|
||||||
filteredProducts.map(async (p) => ({
|
filteredProducts.map(async (p) => ({
|
||||||
@@ -293,6 +322,8 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`[DEBUG PAGE] Slug: ${productSlug}, children count: ${descriptionChildren.length}`);
|
||||||
|
|
||||||
const descriptionContent = {
|
const descriptionContent = {
|
||||||
root: {
|
root: {
|
||||||
...product.content.root,
|
...product.content.root,
|
||||||
@@ -324,29 +355,31 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
categories={product.frontmatter.categories}
|
categories={product.frontmatter.categories}
|
||||||
sku={product.frontmatter.sku}
|
sku={product.frontmatter.sku}
|
||||||
/>
|
/>
|
||||||
<section className="relative pt-40 pb-24 overflow-hidden bg-primary-dark">
|
<section className="relative pt-28 md:pt-40 pb-12 md:pb-24 overflow-hidden bg-primary-dark">
|
||||||
{/* Background Decorative Elements */}
|
{/* Background Decorative Elements */}
|
||||||
<div className="absolute top-0 right-0 w-1/2 h-full bg-gradient-to-l from-accent/5 to-transparent pointer-events-none" />
|
<div className="absolute top-0 right-0 w-1/2 h-full bg-gradient-to-l from-accent/5 to-transparent pointer-events-none" />
|
||||||
<div className="absolute -top-24 -right-24 w-96 h-96 bg-accent/10 rounded-full blur-3xl pointer-events-none" />
|
<div className="absolute -top-24 -right-24 w-96 h-96 bg-accent/10 rounded-full blur-3xl pointer-events-none" />
|
||||||
|
|
||||||
<Container className="relative z-10">
|
<Container className="relative z-10">
|
||||||
<div className="max-w-4xl animate-slide-up">
|
<div className="max-w-4xl animate-slide-up">
|
||||||
<nav className="flex items-center mb-12 text-white/40 text-[10px] font-black uppercase tracking-[0.2em]">
|
<nav className="flex flex-wrap items-center gap-y-1 mb-6 md:mb-12 text-white/40 text-[10px] font-black uppercase tracking-[0.2em]">
|
||||||
<Link
|
<Link
|
||||||
href={`/${locale}/${productsSlug}`}
|
href={`/${locale}/${productsSlug}`}
|
||||||
className="hover:text-accent transition-colors"
|
className="hover:text-accent transition-colors shrink-0"
|
||||||
>
|
>
|
||||||
{t.has('breadcrumb') ? t('breadcrumb') : 'Products'}
|
{t.has('breadcrumb') ? t('breadcrumb') : 'Products'}
|
||||||
</Link>
|
</Link>
|
||||||
<span className="mx-4 opacity-20">/</span>
|
<span className="mx-2 md:mx-4 opacity-20">/</span>
|
||||||
<Link
|
<Link
|
||||||
href={`/${locale}/${productsSlug}/${categorySlug}`}
|
href={`/${locale}/${productsSlug}/${categorySlug}`}
|
||||||
className="hover:text-accent transition-colors"
|
className="hover:text-accent transition-colors shrink-0 max-w-[140px] truncate"
|
||||||
>
|
>
|
||||||
{categoryTitle}
|
{categoryTitle}
|
||||||
</Link>
|
</Link>
|
||||||
<span className="mx-4 opacity-20">/</span>
|
<span className="mx-2 md:mx-4 opacity-20">/</span>
|
||||||
<span className="text-white/90">{product.frontmatter.title}</span>
|
<span className="text-white/90 truncate max-w-[140px] md:max-w-none">
|
||||||
|
{product.frontmatter.title}
|
||||||
|
</span>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-12">
|
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-12">
|
||||||
@@ -357,7 +390,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
{t('englishVersion')}
|
{t('englishVersion')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-wrap gap-3 mb-8">
|
<div className="flex flex-wrap gap-2 mb-4 md:mb-8">
|
||||||
{product.frontmatter.categories.map((cat, idx) => (
|
{product.frontmatter.categories.map((cat, idx) => (
|
||||||
<Badge
|
<Badge
|
||||||
key={idx}
|
key={idx}
|
||||||
@@ -368,10 +401,10 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Heading level={1} className="text-white mb-8 uppercase">
|
<Heading level={1} className="text-white mb-4 md:mb-8 uppercase">
|
||||||
{product.frontmatter.title}
|
{product.frontmatter.title}
|
||||||
</Heading>
|
</Heading>
|
||||||
<p className="text-xl md:text-2xl text-white/60 max-w-2xl leading-relaxed font-medium">
|
<p className="text-base md:text-xl lg:text-2xl text-white/60 max-w-2xl leading-relaxed font-medium">
|
||||||
{product.frontmatter.description}
|
{product.frontmatter.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -385,11 +418,11 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
{/* Large Product Image Section */}
|
{/* Large Product Image Section */}
|
||||||
{product.frontmatter.images && product.frontmatter.images.length > 0 && (
|
{product.frontmatter.images && product.frontmatter.images.length > 0 && (
|
||||||
<div
|
<div
|
||||||
className="relative -mt-32 mb-32 animate-slide-up"
|
className="relative md:-mt-32 mb-8 md:mb-32 animate-slide-up"
|
||||||
style={{ animationDelay: '200ms' }}
|
style={{ animationDelay: '200ms' }}
|
||||||
>
|
>
|
||||||
<div className="bg-white shadow-[0_32px_64px_-12px_rgba(0,0,0,0.1)] rounded-[48px] border border-neutral-dark/5 overflow-hidden p-12 md:p-20 lg:p-24">
|
<div className="bg-white shadow-[0_32px_64px_-12px_rgba(0,0,0,0.1)] rounded-[24px] md:rounded-[48px] border border-neutral-dark/5 overflow-hidden p-6 md:p-20 lg:p-24">
|
||||||
<div className="relative w-full aspect-[21/9]">
|
<div className="relative w-full aspect-[4/3] md:aspect-[21/9]">
|
||||||
<Image
|
<Image
|
||||||
src={product.frontmatter.images[0]}
|
src={product.frontmatter.images[0]}
|
||||||
alt={product.frontmatter.title}
|
alt={product.frontmatter.title}
|
||||||
@@ -424,10 +457,10 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-20">
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-20">
|
||||||
{/* Description Area Next to Sidebar */}
|
{/* Description Area Next to Sidebar */}
|
||||||
<div className="lg:col-span-8">
|
<div className="lg:col-span-8">
|
||||||
<div className="max-w-none prose prose-primary prose-lg md:prose-xl mb-16 pb-16 border-b border-neutral-dark/5">
|
<div className="max-w-none prose prose-primary prose-base md:prose-lg xl:prose-xl mb-8 md:mb-16 pb-8 md:pb-16 border-b border-neutral-dark/5">
|
||||||
{descriptionChildren.length > 0 ? (
|
{descriptionChildren.length > 0 ? (
|
||||||
<PayloadRichText data={descriptionContent} />
|
<PayloadRichText data={descriptionContent} />
|
||||||
) : product.frontmatter.description ? (
|
) : product.frontmatter.description ? (
|
||||||
@@ -435,6 +468,12 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
{product.frontmatter.description}
|
{product.frontmatter.description}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{product.application?.root?.children?.length > 0 && (
|
||||||
|
<div className="mt-12">
|
||||||
|
<PayloadRichText data={product.application} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -443,7 +482,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Full-width Technical Data Below */}
|
{/* Full-width Technical Data Below */}
|
||||||
<div className="mt-16 pt-16 border-t-0">
|
<div className="mt-8 md:mt-16 pt-8 md:pt-16 border-t-0">
|
||||||
<div className="max-w-none prose prose-primary prose-lg md:prose-xl">
|
<div className="max-w-none prose prose-primary prose-lg md:prose-xl">
|
||||||
<PayloadRichText data={technicalContent} />
|
<PayloadRichText data={technicalContent} />
|
||||||
</div>
|
</div>
|
||||||
@@ -501,7 +540,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Related Products Section */}
|
{/* Related Products Section */}
|
||||||
<div className="mt-16 pt-16 border-t border-neutral-dark/5">
|
<div className="mt-10 md:mt-16 pt-10 md:pt-16 border-t border-neutral-dark/5">
|
||||||
<RelatedProducts
|
<RelatedProducts
|
||||||
currentSlug={productSlug}
|
currentSlug={productSlug}
|
||||||
categories={product.frontmatter.categories}
|
categories={product.frontmatter.categories}
|
||||||
|
|||||||
@@ -1,9 +1,41 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
|
import { getPayload } from 'payload';
|
||||||
|
import configPromise from '@payload-config';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deep CMS Health Check
|
||||||
|
* Validates that Payload CMS can actually query the database.
|
||||||
|
* Used by post-deploy smoke tests to catch migration/schema issues.
|
||||||
|
*/
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
// Payload is embedded within the Next.js app, so if this route responds, the CMS is up.
|
const checks: Record<string, string> = {};
|
||||||
// Further DB health checks can be implemented via Payload Local API later.
|
|
||||||
return NextResponse.json({ status: 'ok', message: 'Payload CMS is embedded.' }, { status: 200 });
|
try {
|
||||||
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
checks.init = 'ok';
|
||||||
|
|
||||||
|
// Verify each collection can be queried (catches missing locale tables, broken migrations)
|
||||||
|
const collections = ['posts', 'products', 'pages', 'media'] as const;
|
||||||
|
for (const collection of collections) {
|
||||||
|
try {
|
||||||
|
await payload.find({ collection, limit: 1, locale: 'en' });
|
||||||
|
checks[collection] = 'ok';
|
||||||
|
} catch (e: any) {
|
||||||
|
checks[collection] = `error: ${e.message?.substring(0, 100)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasErrors = Object.values(checks).some(v => v.startsWith('error'));
|
||||||
|
return NextResponse.json(
|
||||||
|
{ status: hasErrors ? 'degraded' : 'ok', checks },
|
||||||
|
{ status: hasErrors ? 503 : 200 },
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ status: 'error', message: e.message?.substring(0, 200), checks },
|
||||||
|
{ status: 503 },
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,9 +58,24 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|||||||
for (const product of productsMetadata) {
|
for (const product of productsMetadata) {
|
||||||
if (!product.frontmatter || !product.slug) continue;
|
if (!product.frontmatter || !product.slug) continue;
|
||||||
|
|
||||||
const category =
|
const firstCat = product.frontmatter.categories[0] || '';
|
||||||
product.frontmatter.categories[0]?.toLowerCase().replace(/\s+/g, '-') || 'other';
|
const normalizedCat = firstCat.toLowerCase().replace(/\s+/g, '-');
|
||||||
const translatedCategory = await mapFileSlugToTranslated(category, locale);
|
let categoryFileSlug = 'low-voltage-cables';
|
||||||
|
if (normalizedCat === 'hochspannungskabel' || normalizedCat === 'high-voltage-cables')
|
||||||
|
categoryFileSlug = 'high-voltage-cables';
|
||||||
|
else if (
|
||||||
|
normalizedCat === 'mittelspannungskabel' ||
|
||||||
|
normalizedCat === 'medium-voltage-cables'
|
||||||
|
)
|
||||||
|
categoryFileSlug = 'medium-voltage-cables';
|
||||||
|
else if (
|
||||||
|
normalizedCat === 'solarkabel' ||
|
||||||
|
normalizedCat === 'solar-cables' ||
|
||||||
|
normalizedCat === 'solar'
|
||||||
|
)
|
||||||
|
categoryFileSlug = 'solar-cables';
|
||||||
|
|
||||||
|
const translatedCategory = await mapFileSlugToTranslated(categoryFileSlug, locale);
|
||||||
const translatedSlug = await mapFileSlugToTranslated(product.slug, locale);
|
const translatedSlug = await mapFileSlugToTranslated(product.slug, locale);
|
||||||
|
|
||||||
sitemapEntries.push({
|
sitemapEntries.push({
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ export default function Footer() {
|
|||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="bg-primary text-white py-24 relative overflow-hidden content-visibility-auto">
|
<footer className="bg-primary text-white py-14 md:py-24 relative overflow-hidden content-visibility-auto">
|
||||||
<div className="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-white/20 to-transparent" />
|
<div className="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-white/20 to-transparent" />
|
||||||
|
|
||||||
<Container>
|
<Container>
|
||||||
<h2 className="sr-only">Footer Navigation</h2>
|
<h2 className="sr-only">Footer Navigation</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-12 gap-16 mb-20">
|
<div className="grid grid-cols-2 md:grid-cols-2 lg:grid-cols-12 gap-10 md:gap-16 mb-12 md:mb-20">
|
||||||
{/* Brand Column */}
|
{/* Brand Column – full width on mobile */}
|
||||||
<div className="lg:col-span-4 space-y-8">
|
<div className="col-span-2 md:col-span-2 lg:col-span-4 space-y-6 md:space-y-8">
|
||||||
<Link
|
<Link
|
||||||
href={`/${locale}`}
|
href={`/${locale}`}
|
||||||
className="inline-block group"
|
className="inline-block group"
|
||||||
@@ -67,9 +67,9 @@ export default function Footer() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Links Columns */}
|
{/* Legal Column */}
|
||||||
<div className="lg:col-span-2">
|
<div className="col-span-1 lg:col-span-2">
|
||||||
<h3 className="text-accent font-bold uppercase tracking-widest text-xs md:text-sm mb-8">
|
<h3 className="text-accent font-bold uppercase tracking-widest text-xs mb-5 md:mb-8">
|
||||||
{t('legal')}
|
{t('legal')}
|
||||||
</h3>
|
</h3>
|
||||||
<ul className="space-y-4 text-white/70 list-none m-0 p-0">
|
<ul className="space-y-4 text-white/70 list-none m-0 p-0">
|
||||||
@@ -121,8 +121,9 @@ export default function Footer() {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="lg:col-span-2">
|
{/* Company Column */}
|
||||||
<h3 className="text-accent font-bold uppercase tracking-widest text-xs md:text-sm mb-8">
|
<div className="col-span-1 lg:col-span-2">
|
||||||
|
<h3 className="text-accent font-bold uppercase tracking-widest text-xs mb-5 md:mb-8">
|
||||||
{t('company')}
|
{t('company')}
|
||||||
</h3>
|
</h3>
|
||||||
<ul className="space-y-4 text-white/70 list-none m-0 p-0">
|
<ul className="space-y-4 text-white/70 list-none m-0 p-0">
|
||||||
@@ -189,9 +190,9 @@ export default function Footer() {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Recent Posts Column */}
|
{/* Recent Posts Column – full width on mobile */}
|
||||||
<div className="lg:col-span-4">
|
<div className="col-span-2 md:col-span-2 lg:col-span-4">
|
||||||
<h3 className="text-accent font-bold uppercase tracking-widest text-xs md:text-sm mb-8">
|
<h3 className="text-accent font-bold uppercase tracking-widest text-xs mb-5 md:mb-8">
|
||||||
{t('recentPosts')}
|
{t('recentPosts')}
|
||||||
</h3>
|
</h3>
|
||||||
<ul className="space-y-6 list-none m-0 p-0">
|
<ul className="space-y-6 list-none m-0 p-0">
|
||||||
@@ -242,7 +243,7 @@ export default function Footer() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-12 border-t border-white/10 flex flex-col md:flex-row justify-between items-center gap-8 text-white/70 text-xs md:text-sm font-medium">
|
<div className="pt-8 md:pt-12 border-t border-white/10 flex flex-row justify-between items-center gap-4 text-white/70 text-xs md:text-sm font-medium">
|
||||||
<p>{t('copyright', { year: currentYear })}</p>
|
<p>{t('copyright', { year: currentYear })}</p>
|
||||||
<div className="flex gap-8">
|
<div className="flex gap-8">
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -141,7 +141,8 @@ export default function Header() {
|
|||||||
{
|
{
|
||||||
'bg-primary/95 backdrop-blur-md md:bg-transparent py-3 md:py-8 shadow-2xl md:shadow-none':
|
'bg-primary/95 backdrop-blur-md md:bg-transparent py-3 md:py-8 shadow-2xl md:shadow-none':
|
||||||
isHomePage && !isScrolled && !isMobileMenuOpen,
|
isHomePage && !isScrolled && !isMobileMenuOpen,
|
||||||
'bg-primary py-3 md:py-4 shadow-2xl': !isHomePage || isScrolled || isMobileMenuOpen,
|
'bg-primary/90 backdrop-blur-md py-3 md:py-4 shadow-2xl':
|
||||||
|
!isHomePage || isScrolled || isMobileMenuOpen,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -152,9 +153,7 @@ export default function Header() {
|
|||||||
<>
|
<>
|
||||||
<header className={headerClass} style={{ animationDuration: '800ms' }}>
|
<header className={headerClass} style={{ animationDuration: '800ms' }}>
|
||||||
<div className="container mx-auto px-4 md:px-12 lg:px-16 max-w-7xl flex items-center justify-between">
|
<div className="container mx-auto px-4 md:px-12 lg:px-16 max-w-7xl flex items-center justify-between">
|
||||||
<div
|
<div className="flex-shrink-0 group touch-target fill-mode-both">
|
||||||
className="flex-shrink-0 group touch-target fill-mode-both"
|
|
||||||
>
|
|
||||||
<Link
|
<Link
|
||||||
href={`/${currentLocale}`}
|
href={`/${currentLocale}`}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -336,115 +335,115 @@ export default function Header() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
{/* Mobile Menu Overlay */}
|
{/* Mobile Menu Overlay */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'fixed inset-0 bg-primary z-[60] lg:hidden transition-all duration-500 ease-in-out flex flex-col',
|
'fixed inset-0 bg-primary/95 backdrop-blur-3xl z-[60] lg:hidden transition-all duration-500 ease-in-out flex flex-col',
|
||||||
isMobileMenuOpen
|
isMobileMenuOpen
|
||||||
? 'opacity-100 translate-y-0'
|
? 'opacity-100 translate-y-0'
|
||||||
: 'opacity-0 -translate-y-full pointer-events-none',
|
: 'opacity-0 -translate-y-full pointer-events-none',
|
||||||
)}
|
)}
|
||||||
id="mobile-menu"
|
id="mobile-menu"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label={t('menu')}
|
aria-label={t('menu')}
|
||||||
ref={mobileMenuRef}
|
ref={mobileMenuRef}
|
||||||
inert={isMobileMenuOpen ? undefined : true}
|
inert={isMobileMenuOpen ? undefined : true}
|
||||||
>
|
>
|
||||||
<nav className="flex-grow flex flex-col justify-center items-center p-8 space-y-8">
|
<nav className="flex-grow flex flex-col justify-center items-center p-8 space-y-8">
|
||||||
{menuItems.map((item, idx) => (
|
{menuItems.map((item, idx) => (
|
||||||
<div
|
|
||||||
key={item.href}
|
|
||||||
className={cn(
|
|
||||||
'transition-all duration-500 transform',
|
|
||||||
isMobileMenuOpen ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8',
|
|
||||||
)}
|
|
||||||
style={{ transitionDelay: `${isMobileMenuOpen ? 200 + idx * 80 : 0}ms` }}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href={`/${currentLocale}${item.href === '/' ? '' : item.href}`}
|
|
||||||
aria-current={
|
|
||||||
(
|
|
||||||
item.href === '/'
|
|
||||||
? pathname === `/${currentLocale}` || pathname === '/'
|
|
||||||
: pathname.startsWith(`/${currentLocale}${item.href}`)
|
|
||||||
)
|
|
||||||
? 'page'
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
setIsMobileMenuOpen(false);
|
|
||||||
trackEvent(AnalyticsEvents.LINK_CLICK, {
|
|
||||||
label: item.label,
|
|
||||||
href: item.href,
|
|
||||||
location: 'mobile_menu',
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className={cn(
|
|
||||||
'text-2xl md:text-3xl font-extrabold text-white hover:text-accent transition-all transform block py-4',
|
|
||||||
(item.href === '/'
|
|
||||||
? pathname === `/${currentLocale}` || pathname === '/'
|
|
||||||
: pathname.startsWith(`/${currentLocale}${item.href}`)) && 'text-accent',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
key={item.href}
|
||||||
className={cn(
|
className={cn(
|
||||||
'pt-8 border-t border-white/10 w-full flex flex-col items-center space-y-8 transition-all duration-500',
|
'transition-all duration-500 transform',
|
||||||
isMobileMenuOpen ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8',
|
isMobileMenuOpen ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8',
|
||||||
)}
|
)}
|
||||||
style={{ transitionDelay: isMobileMenuOpen ? '600ms' : '0ms' }}
|
style={{ transitionDelay: `${isMobileMenuOpen ? 200 + idx * 80 : 0}ms` }}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-8 text-lg md:text-xl font-extrabold tracking-widest uppercase text-white">
|
<Link
|
||||||
<div>
|
href={`/${currentLocale}${item.href === '/' ? '' : item.href}`}
|
||||||
<Link
|
aria-current={
|
||||||
href={getPathForLocale('en')}
|
(
|
||||||
className={`hover:text-accent transition-colors ${currentLocale === 'en' ? 'text-accent' : 'opacity-80'}`}
|
item.href === '/'
|
||||||
>
|
? pathname === `/${currentLocale}` || pathname === '/'
|
||||||
EN
|
: pathname.startsWith(`/${currentLocale}${item.href}`)
|
||||||
</Link>
|
)
|
||||||
</div>
|
? 'page'
|
||||||
<div className="w-px h-6 bg-white/30" />
|
: undefined
|
||||||
<div>
|
}
|
||||||
<Link
|
onClick={() => {
|
||||||
href={getPathForLocale('de')}
|
setIsMobileMenuOpen(false);
|
||||||
className={`hover:text-accent transition-colors ${currentLocale === 'de' ? 'text-accent' : 'opacity-80'}`}
|
trackEvent(AnalyticsEvents.LINK_CLICK, {
|
||||||
>
|
label: item.label,
|
||||||
DE
|
href: item.href,
|
||||||
</Link>
|
location: 'mobile_menu',
|
||||||
</div>
|
});
|
||||||
</div>
|
}}
|
||||||
|
className={cn(
|
||||||
|
'text-2xl md:text-3xl font-extrabold text-white hover:text-accent transition-all transform block py-4',
|
||||||
|
(item.href === '/'
|
||||||
|
? pathname === `/${currentLocale}` || pathname === '/'
|
||||||
|
: pathname.startsWith(`/${currentLocale}${item.href}`)) && 'text-accent',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
<div className="w-full max-w-xs">
|
<div
|
||||||
<Button
|
className={cn(
|
||||||
href={`/${currentLocale}/${currentLocale === 'de' ? 'kontakt' : 'contact'}`}
|
'pt-8 border-t border-white/10 w-full flex flex-col items-center space-y-8 transition-all duration-500',
|
||||||
variant="accent"
|
isMobileMenuOpen ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8',
|
||||||
size="lg"
|
)}
|
||||||
className="w-full py-6 text-lg md:text-xl shadow-2xl hover:scale-105 transition-transform"
|
style={{ transitionDelay: isMobileMenuOpen ? '600ms' : '0ms' }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-8 text-lg md:text-xl font-extrabold tracking-widest uppercase text-white">
|
||||||
|
<div>
|
||||||
|
<Link
|
||||||
|
href={getPathForLocale('en')}
|
||||||
|
className={`hover:text-accent transition-colors ${currentLocale === 'en' ? 'text-accent' : 'opacity-80'}`}
|
||||||
>
|
>
|
||||||
{t('contact')}
|
EN
|
||||||
</Button>
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="w-px h-6 bg-white/30" />
|
||||||
|
<div>
|
||||||
|
<Link
|
||||||
|
href={getPathForLocale('de')}
|
||||||
|
className={`hover:text-accent transition-colors ${currentLocale === 'de' ? 'text-accent' : 'opacity-80'}`}
|
||||||
|
>
|
||||||
|
DE
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom Branding */}
|
<div className="w-full max-w-xs">
|
||||||
<div
|
<Button
|
||||||
className={cn(
|
href={`/${currentLocale}/${currentLocale === 'de' ? 'kontakt' : 'contact'}`}
|
||||||
'p-12 flex justify-center transition-all duration-700',
|
variant="accent"
|
||||||
isMobileMenuOpen ? 'opacity-20 scale-100' : 'opacity-0 scale-75',
|
size="lg"
|
||||||
)}
|
className="w-full py-6 text-lg md:text-xl shadow-2xl hover:scale-105 transition-transform"
|
||||||
style={{ transitionDelay: isMobileMenuOpen ? '800ms' : '0ms' }}
|
>
|
||||||
>
|
{t('contact')}
|
||||||
<Image src="/logo-white.svg" alt={t('home')} width={80} height={80} unoptimized />
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</div>
|
||||||
</div>
|
|
||||||
</header>
|
{/* Bottom Branding */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'p-12 flex justify-center transition-all duration-700',
|
||||||
|
isMobileMenuOpen ? 'opacity-20 scale-100' : 'opacity-0 scale-75',
|
||||||
|
)}
|
||||||
|
style={{ transitionDelay: isMobileMenuOpen ? '800ms' : '0ms' }}
|
||||||
|
>
|
||||||
|
<Image src="/logo-white.svg" alt={t('home')} width={80} height={80} unoptimized />
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,93 +39,99 @@ import CTA from '@/components/home/CTA';
|
|||||||
const jsxConverters: JSXConverters = {
|
const jsxConverters: JSXConverters = {
|
||||||
...defaultJSXConverters,
|
...defaultJSXConverters,
|
||||||
// Let the default converters handle text nodes to preserve valid formatting
|
// Let the default converters handle text nodes to preserve valid formatting
|
||||||
// If the text node contains raw HTML (from messy migrations), render it as HTML instead of escaping it
|
// Use div instead of p for paragraphs to allow nested block elements (like the lists above)
|
||||||
text: ({ node }: any) => {
|
paragraph: ({ node, nodesToJSX }: any) => {
|
||||||
const text = node.text;
|
return (
|
||||||
// Handle markdown-style lists embedded in text nodes from Markdown migration
|
<div className="mb-6 leading-relaxed text-text-secondary">
|
||||||
if (text && text.includes('\n- ')) {
|
{nodesToJSX({ nodes: node.children })}
|
||||||
const parts = text.split('\n- ').filter((p: string) => p.trim() !== '');
|
</div>
|
||||||
// If first part doesn't start with "- ", it's a prefix paragraph
|
);
|
||||||
const startsWithDash = text.trimStart().startsWith('- ');
|
},
|
||||||
const prefix = startsWithDash ? null : parts.shift();
|
// Scale headings to prevent multiple H1s (H1 -> H2, etc) and style natively
|
||||||
return (
|
heading: ({ node, nodesToJSX }: any) => {
|
||||||
<>
|
const children = nodesToJSX({ nodes: node.children });
|
||||||
{prefix && (
|
const tag = node?.tag;
|
||||||
<span dangerouslySetInnerHTML={prefix.includes('<') ? { __html: prefix } : undefined}>
|
if (tag === 'h1')
|
||||||
{!prefix.includes('<') ? prefix : undefined}
|
return (
|
||||||
</span>
|
<h2 className="text-3xl md:text-4xl font-bold mt-12 mb-6 text-text-primary">{children}</h2>
|
||||||
)}
|
);
|
||||||
<ul className="list-disc pl-6 my-4 space-y-2">
|
if (tag === 'h2')
|
||||||
{parts.map((item: string, i: number) => {
|
return (
|
||||||
const cleanItem = item.trim();
|
<h3 className="text-2xl md:text-3xl font-bold mt-10 mb-5 text-text-primary">{children}</h3>
|
||||||
if (cleanItem.includes('<')) {
|
);
|
||||||
return <li key={i} dangerouslySetInnerHTML={{ __html: cleanItem }} />;
|
if (tag === 'h3')
|
||||||
}
|
return (
|
||||||
return <li key={i}>{cleanItem}</li>;
|
<h4 className="text-xl md:text-2xl font-bold mt-8 mb-4 text-text-primary">{children}</h4>
|
||||||
})}
|
);
|
||||||
</ul>
|
if (tag === 'h4')
|
||||||
</>
|
return (
|
||||||
);
|
<h5 className="text-lg md:text-xl font-bold mt-6 mb-4 text-text-primary">{children}</h5>
|
||||||
}
|
);
|
||||||
|
if (tag === 'h5')
|
||||||
if (text && (text.includes('<') || text.includes('data-start'))) {
|
return (
|
||||||
return <span dangerouslySetInnerHTML={{ __html: text }} />;
|
<h6 className="text-base md:text-lg font-bold mt-6 mb-4 text-text-primary">{children}</h6>
|
||||||
}
|
);
|
||||||
|
return <h6 className="text-base font-bold mt-6 mb-4 text-text-primary">{children}</h6>;
|
||||||
// Handle markdown-style links [text](url) from Markdown migration
|
},
|
||||||
if (text && /\[([^\]]+)\]\(([^)]+)\)/.test(text)) {
|
list: ({ node, nodesToJSX }: any) => {
|
||||||
const parts: React.ReactNode[] = [];
|
const children = nodesToJSX({ nodes: node.children });
|
||||||
const remaining = text;
|
if (node?.listType === 'number') {
|
||||||
let key = 0;
|
return (
|
||||||
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
<ol className="list-decimal pl-6 my-6 space-y-2 text-text-secondary marker:text-primary marker:font-bold">
|
||||||
let match;
|
{children}
|
||||||
let lastIndex = 0;
|
</ol>
|
||||||
while ((match = linkRegex.exec(remaining)) !== null) {
|
);
|
||||||
if (match.index > lastIndex) {
|
}
|
||||||
parts.push(<span key={key++}>{remaining.slice(lastIndex, match.index)}</span>);
|
if (node?.listType === 'check') {
|
||||||
}
|
return <ul className="list-none pl-0 my-6 space-y-2 text-text-secondary">{children}</ul>;
|
||||||
parts.push(
|
}
|
||||||
<a
|
return (
|
||||||
key={key++}
|
<ul className="list-disc pl-6 my-6 space-y-2 text-text-secondary marker:text-primary">
|
||||||
href={match[2]}
|
{children}
|
||||||
target="_blank"
|
</ul>
|
||||||
rel="noopener noreferrer"
|
);
|
||||||
className="text-primary underline decoration-primary/30 hover:decoration-primary transition-colors"
|
},
|
||||||
>
|
listitem: ({ node, nodesToJSX }: any) => {
|
||||||
{match[1]}
|
const children = nodesToJSX({ nodes: node.children });
|
||||||
</a>,
|
if (node?.checked != null) {
|
||||||
);
|
return (
|
||||||
lastIndex = match.index + match[0].length;
|
<li className="flex items-center gap-3 mb-2 leading-relaxed">
|
||||||
}
|
<input
|
||||||
if (lastIndex < remaining.length) {
|
type="checkbox"
|
||||||
parts.push(<span key={key++}>{remaining.slice(lastIndex)}</span>);
|
checked={node.checked}
|
||||||
}
|
readOnly
|
||||||
return <>{parts}</>;
|
className="mt-1 w-4 h-4 text-primary focus:ring-primary border-gray-300 rounded"
|
||||||
}
|
/>
|
||||||
|
<span>{children}</span>
|
||||||
// Handle newlines in text nodes — convert to <br> for proper line breaks
|
</li>
|
||||||
if (text && text.includes('\n')) {
|
);
|
||||||
const lines = text.split('\n');
|
}
|
||||||
return (
|
return <li className="mb-2 leading-relaxed">{children}</li>;
|
||||||
<>
|
},
|
||||||
{lines.map((line: string, i: number) => (
|
quote: ({ node, nodesToJSX }: any) => {
|
||||||
<span key={i}>
|
const children = nodesToJSX({ nodes: node.children });
|
||||||
{line}
|
return (
|
||||||
{i < lines.length - 1 && <br />}
|
<blockquote className="border-l-4 border-primary bg-primary/5 rounded-r-2xl pl-6 py-4 my-8 italic text-text-secondary shadow-sm">
|
||||||
</span>
|
{children}
|
||||||
))}
|
</blockquote>
|
||||||
</>
|
);
|
||||||
);
|
},
|
||||||
}
|
link: ({ node, nodesToJSX }: any) => {
|
||||||
|
const children = nodesToJSX({ nodes: node.children });
|
||||||
if (node.format === 1) return <strong>{text}</strong>;
|
// Handling Payload CMS link nodes
|
||||||
if (node.format === 2) return <em>{text}</em>;
|
const href = node?.fields?.url || node?.url || '#';
|
||||||
return <span>{text}</span>;
|
const newTab = node?.fields?.newTab || node?.newTab;
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
target={newTab ? '_blank' : undefined}
|
||||||
|
rel={newTab ? 'noopener noreferrer' : undefined}
|
||||||
|
className="text-primary no-underline hover:underline font-medium transition-colors"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
// Scale headings to prevent multiple H1s (H1 -> H2, etc)
|
|
||||||
h1: ({ children }: any) => <h2 className="text-3xl md:text-4xl font-bold my-6">{children}</h2>,
|
|
||||||
h2: ({ children }: any) => <h3 className="text-2xl md:text-3xl font-bold my-5">{children}</h3>,
|
|
||||||
h3: ({ children }: any) => <h4 className="text-xl md:text-2xl font-bold my-4">{children}</h4>,
|
|
||||||
|
|
||||||
blocks: {
|
blocks: {
|
||||||
// ... preserved existing blocks ...
|
// ... preserved existing blocks ...
|
||||||
@@ -170,10 +176,10 @@ const jsxConverters: JSXConverters = {
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
technicalGrid: ({ node }: any) => (
|
technicalGrid: ({ node }: any) => (
|
||||||
<TechnicalGrid title={node.fields.title} items={node.fields.items} />
|
<TechnicalGrid title={node?.fields?.title} items={node?.fields?.items} />
|
||||||
),
|
),
|
||||||
'block-technicalGrid': ({ node }: any) => {
|
'block-technicalGrid': ({ node }: any) => {
|
||||||
console.log('[PayloadRichText] Rendering block-technicalGrid:', node.fields.title);
|
if (!node?.fields) return null;
|
||||||
return <TechnicalGrid title={node.fields.title} items={node.fields.items} />;
|
return <TechnicalGrid title={node.fields.title} items={node.fields.items} />;
|
||||||
},
|
},
|
||||||
highlightBox: ({ node }: any) => (
|
highlightBox: ({ node }: any) => (
|
||||||
@@ -246,20 +252,23 @@ const jsxConverters: JSXConverters = {
|
|||||||
{node.fields.title}
|
{node.fields.title}
|
||||||
</SplitHeading>
|
</SplitHeading>
|
||||||
),
|
),
|
||||||
productTabs: ({ node }: any) => (
|
productTabs: ({ node }: any) => {
|
||||||
<ProductTabs
|
if (!node?.fields) return null;
|
||||||
technicalData={
|
return (
|
||||||
<ProductTechnicalData
|
<ProductTabs
|
||||||
data={{
|
technicalData={
|
||||||
technicalItems: node.fields.technicalItems,
|
<ProductTechnicalData
|
||||||
voltageTables: node.fields.voltageTables,
|
data={{
|
||||||
}}
|
technicalItems: node.fields.technicalItems,
|
||||||
/>
|
voltageTables: node.fields.voltageTables,
|
||||||
}
|
}}
|
||||||
>
|
/>
|
||||||
<></>
|
}
|
||||||
</ProductTabs>
|
>
|
||||||
),
|
<></>
|
||||||
|
</ProductTabs>
|
||||||
|
);
|
||||||
|
},
|
||||||
'block-productTabs': ({ node }: any) => (
|
'block-productTabs': ({ node }: any) => (
|
||||||
<ProductTabs
|
<ProductTabs
|
||||||
technicalData={
|
technicalData={
|
||||||
@@ -1009,6 +1018,10 @@ export default function PayloadRichText({
|
|||||||
|
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
|
|
||||||
|
if (data.root?.children?.length > 0) {
|
||||||
|
console.log('[PayloadRichText DEBUG] received children', data.root.children.length);
|
||||||
|
}
|
||||||
|
|
||||||
const dynamicConverters: JSXConverters = {
|
const dynamicConverters: JSXConverters = {
|
||||||
...jsxConverters,
|
...jsxConverters,
|
||||||
blocks: {
|
blocks: {
|
||||||
|
|||||||
@@ -38,14 +38,14 @@ export default function ProductTechnicalData({ data }: ProductTechnicalDataProps
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-16">
|
<div className="space-y-8 md:space-y-16">
|
||||||
{technicalItems.length > 0 && (
|
{technicalItems.length > 0 && (
|
||||||
<div className="bg-white p-8 md:p-12 rounded-[32px] shadow-sm border border-neutral-dark/5">
|
<div className="bg-white p-5 md:p-12 rounded-[20px] md:rounded-[32px] shadow-sm border border-neutral-dark/5">
|
||||||
<h3 className="text-2xl font-bold text-primary mb-8 flex items-center gap-3">
|
<h3 className="text-2xl font-bold text-primary mb-8 flex items-center gap-3">
|
||||||
<div className="w-2 h-8 bg-accent rounded-full" />
|
<div className="w-2 h-8 bg-accent rounded-full" />
|
||||||
General Data
|
General Data
|
||||||
</h3>
|
</h3>
|
||||||
<dl className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-12 gap-y-8">
|
<dl className="grid grid-cols-2 sm:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-6 md:gap-x-12 md:gap-y-8">
|
||||||
{technicalItems.map((item, idx) => (
|
{technicalItems.map((item, idx) => (
|
||||||
<div key={idx} className="flex flex-col group">
|
<div key={idx} className="flex flex-col group">
|
||||||
<dt className="text-sm font-bold uppercase tracking-widest text-primary/40 mb-2 group-hover:text-accent transition-colors">
|
<dt className="text-sm font-bold uppercase tracking-widest text-primary/40 mb-2 group-hover:text-accent transition-colors">
|
||||||
@@ -72,7 +72,7 @@ export default function ProductTechnicalData({ data }: ProductTechnicalDataProps
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
className="bg-white p-8 md:p-12 rounded-[32px] shadow-sm border border-neutral-dark/5 overflow-hidden"
|
className="bg-white p-5 md:p-12 rounded-[20px] md:rounded-[32px] shadow-sm border border-neutral-dark/5 overflow-hidden"
|
||||||
>
|
>
|
||||||
<h3 className="text-2xl font-bold text-primary mb-8 flex items-center gap-3">
|
<h3 className="text-2xl font-bold text-primary mb-8 flex items-center gap-3">
|
||||||
<div className="w-2 h-8 bg-accent rounded-full" />
|
<div className="w-2 h-8 bg-accent rounded-full" />
|
||||||
@@ -83,7 +83,7 @@ export default function ProductTechnicalData({ data }: ProductTechnicalDataProps
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{table.metaItems.length > 0 && (
|
{table.metaItems.length > 0 && (
|
||||||
<dl className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-8 mb-12 bg-neutral-light/50 p-8 rounded-2xl border border-neutral-dark/5">
|
<dl className="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 md:gap-8 mb-6 md:mb-12 bg-neutral-light/50 p-4 md:p-8 rounded-xl md:rounded-2xl border border-neutral-dark/5">
|
||||||
{table.metaItems.map((item, mIdx) => (
|
{table.metaItems.map((item, mIdx) => (
|
||||||
<div key={mIdx}>
|
<div key={mIdx}>
|
||||||
<dt className="text-[10px] font-black uppercase tracking-[0.2em] text-primary/40 mb-1">
|
<dt className="text-[10px] font-black uppercase tracking-[0.2em] text-primary/40 mb-1">
|
||||||
@@ -98,9 +98,11 @@ export default function ProductTechnicalData({ data }: ProductTechnicalDataProps
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
{/* Scroll hint gradient on right edge for mobile */}
|
||||||
|
<div className="pointer-events-none absolute right-0 top-0 h-full w-8 bg-gradient-to-l from-white to-transparent z-20 md:hidden" />
|
||||||
<div
|
<div
|
||||||
id={`voltage-table-${idx}`}
|
id={`voltage-table-${idx}`}
|
||||||
className={`overflow-x-auto -mx-8 md:-mx-12 px-8 md:px-12 transition-all duration-500 ease-in-out ${
|
className={`overflow-x-auto -mx-5 md:-mx-12 px-5 md:px-12 transition-all duration-500 ease-in-out ${
|
||||||
!isExpanded && hasManyRows ? 'max-h-[400px] overflow-y-hidden' : 'max-h-[none]'
|
!isExpanded && hasManyRows ? 'max-h-[400px] overflow-y-hidden' : 'max-h-[none]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -41,10 +41,8 @@ export default async function RelatedProducts({
|
|||||||
];
|
];
|
||||||
const catFileSlug =
|
const catFileSlug =
|
||||||
categorySlugs.find((slug) => {
|
categorySlugs.find((slug) => {
|
||||||
const key = slug.replace(/-cables$/, '').replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
|
||||||
const title = t(`categories.${key}.title`);
|
|
||||||
return product.frontmatter.categories.some(
|
return product.frontmatter.categories.some(
|
||||||
(cat) => cat.toLowerCase().replace(/\s+/g, '-') === slug || cat === title,
|
(cat) => cat.toLowerCase().replace(/\s+/g, '-') === slug,
|
||||||
);
|
);
|
||||||
}) || 'low-voltage-cables';
|
}) || 'low-voltage-cables';
|
||||||
|
|
||||||
|
|||||||
26
components/analytics/ClientNotFoundTracker.tsx
Normal file
26
components/analytics/ClientNotFoundTracker.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useAnalytics } from './useAnalytics';
|
||||||
|
import { AnalyticsEvents } from './analytics-events';
|
||||||
|
|
||||||
|
export default function ClientNotFoundTracker({ path }: { path: string }) {
|
||||||
|
const { trackEvent } = useAnalytics();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
trackEvent(AnalyticsEvents.ERROR, {
|
||||||
|
type: '404_not_found',
|
||||||
|
path,
|
||||||
|
});
|
||||||
|
|
||||||
|
import('@sentry/nextjs').then((Sentry) => {
|
||||||
|
Sentry.withScope((scope) => {
|
||||||
|
scope.setTag('status_code', '404');
|
||||||
|
scope.setTag('path', path);
|
||||||
|
Sentry.captureMessage(`Route Not Found: ${path}`, 'warning');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [trackEvent, path]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -23,12 +23,26 @@ export default function Hero({ data }: { data?: any }) {
|
|||||||
className="text-center md:text-left mb-6 md:mb-8 md:max-w-none text-white text-4xl sm:text-5xl md:text-7xl font-extrabold [text-shadow:_-2px_-2px_0_#002b49,_2px_-2px_0_#002b49,_-2px_2px_0_#002b49,_2px_2px_0_#002b49,_-2px_0_0_#002b49,_2px_0_0_#002b49,_0_-2px_0_#002b49,_0_2px_0_#002b49]"
|
className="text-center md:text-left mb-6 md:mb-8 md:max-w-none text-white text-4xl sm:text-5xl md:text-7xl font-extrabold [text-shadow:_-2px_-2px_0_#002b49,_2px_-2px_0_#002b49,_-2px_2px_0_#002b49,_2px_2px_0_#002b49,_-2px_0_0_#002b49,_2px_0_0_#002b49,_0_-2px_0_#002b49,_0_2px_0_#002b49]"
|
||||||
>
|
>
|
||||||
{data?.title ? (
|
{data?.title ? (
|
||||||
<span dangerouslySetInnerHTML={{ __html: data.title.replace(/<green>/g, '<span class="relative inline-block"><span class="relative z-10 text-accent italic inline-block">').replace(/<\/green>/g, '</span><div class="w-[140%] h-[140%] -top-[20%] -left-[20%] text-accent/30 hidden md:block absolute -z-10 animate-in fade-in zoom-in-0 duration-1000 ease-out fill-mode-both" style="animation-delay: 500ms;"><Scribble variant="circle" /></div></span>') }} />
|
<span
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: data.title
|
||||||
|
.replace(
|
||||||
|
/<green>/g,
|
||||||
|
'<span class="relative inline-block"><span class="relative z-10 text-accent italic inline-block">',
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
/<\/green>/g,
|
||||||
|
'</span><div class="w-[140%] h-[140%] -top-[20%] -left-[20%] text-accent/30 hidden md:block absolute -z-10 animate-in fade-in zoom-in-0 duration-1000 ease-out fill-mode-both" style="animation-delay: 500ms;"><Scribble variant="circle" /></div></span>',
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
t.rich('title', {
|
t.rich('title', {
|
||||||
green: (chunks) => (
|
green: (chunks) => (
|
||||||
<span className="relative inline-block">
|
<span className="relative inline-block">
|
||||||
<span className="relative z-10 text-accent italic inline-block">{chunks}</span>
|
<span className="relative z-10 text-accent italic inline-block">
|
||||||
|
{chunks}
|
||||||
|
</span>
|
||||||
<div
|
<div
|
||||||
className="w-[140%] h-[140%] -top-[20%] -left-[20%] text-accent/30 hidden md:block absolute -z-10 animate-in fade-in zoom-in-0 duration-1000 ease-out fill-mode-both"
|
className="w-[140%] h-[140%] -top-[20%] -left-[20%] text-accent/30 hidden md:block absolute -z-10 animate-in fade-in zoom-in-0 duration-1000 ease-out fill-mode-both"
|
||||||
style={{ animationDelay: '500ms' }}
|
style={{ animationDelay: '500ms' }}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Image from 'next/image';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { getAllPosts } from '@/lib/blog';
|
import { getAllPosts } from '@/lib/blog';
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { Section, Container, Heading, Card, Badge } from '../../components/ui';
|
import { Section, Container, Heading } from '../../components/ui';
|
||||||
|
|
||||||
interface RecentPostsProps {
|
interface RecentPostsProps {
|
||||||
locale: string;
|
locale: string;
|
||||||
@@ -13,7 +13,7 @@ interface RecentPostsProps {
|
|||||||
export default async function RecentPosts({ locale, data }: RecentPostsProps) {
|
export default async function RecentPosts({ locale, data }: RecentPostsProps) {
|
||||||
const t = await getTranslations('Blog');
|
const t = await getTranslations('Blog');
|
||||||
const posts = await getAllPosts(locale);
|
const posts = await getAllPosts(locale);
|
||||||
const recentPosts = posts.slice(0, 3);
|
const recentPosts = posts.slice(0, 4);
|
||||||
|
|
||||||
if (recentPosts.length === 0) return null;
|
if (recentPosts.length === 0) return null;
|
||||||
|
|
||||||
@@ -21,9 +21,9 @@ export default async function RecentPosts({ locale, data }: RecentPostsProps) {
|
|||||||
const subtitle = data?.subtitle || t('latestNews');
|
const subtitle = data?.subtitle || t('latestNews');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section className="bg-neutral py-16 md:py-24">
|
<Section className="bg-neutral-light py-0 md:py-0 lg:py-0">
|
||||||
<Container>
|
<Container className="py-12 md:py-16">
|
||||||
<div className="flex flex-col md:flex-row items-start md:items-end justify-between mb-12 md:mb-16 gap-6">
|
<div className="flex flex-col md:flex-row items-start md:items-end justify-between gap-6">
|
||||||
<Heading level={2} subtitle={subtitle} className="mb-0 text-primary">
|
<Heading level={2} subtitle={subtitle} className="mb-0 text-primary">
|
||||||
{title}
|
{title}
|
||||||
</Heading>
|
</Heading>
|
||||||
@@ -35,74 +35,73 @@ export default async function RecentPosts({ locale, data }: RecentPostsProps) {
|
|||||||
<span className="ml-2 transition-transform group-hover:translate-x-2">→</span>
|
<span className="ml-2 transition-transform group-hover:translate-x-2">→</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul className="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-10 list-none p-0 m-0">
|
|
||||||
{recentPosts.map((post, idx) => (
|
|
||||||
<li key={`${post.slug}-${idx}`}>
|
|
||||||
<Link href={`/${locale}/blog/${post.slug}`} className="group block h-full">
|
|
||||||
<Card
|
|
||||||
tag="article"
|
|
||||||
className="h-full flex flex-col border-none shadow-lg hover:shadow-2xl transition-all duration-500 rounded-3xl"
|
|
||||||
>
|
|
||||||
{post.frontmatter.featuredImage && (
|
|
||||||
<div className="relative h-64 overflow-hidden">
|
|
||||||
<Image
|
|
||||||
src={post.frontmatter.featuredImage.split('?')[0]}
|
|
||||||
alt={post.frontmatter.title}
|
|
||||||
fill
|
|
||||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
|
|
||||||
style={{
|
|
||||||
objectPosition: `${post.frontmatter.focalX ?? 50}% ${post.frontmatter.focalY ?? 50}%`,
|
|
||||||
}}
|
|
||||||
sizes="(max-width: 768px) 100vw, 33vw"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 image-overlay-gradient opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
|
||||||
{post.frontmatter.category && (
|
|
||||||
<Badge variant="accent" className="absolute top-4 left-4 shadow-md">
|
|
||||||
{post.frontmatter.category}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="p-6 md:p-8 flex flex-col flex-grow">
|
|
||||||
<div className="text-xs md:text-sm font-medium text-text-light mb-3 md:mb-4 flex items-center gap-2">
|
|
||||||
<span className="w-6 md:w-8 h-px bg-neutral-medium" />
|
|
||||||
<time dateTime={post.frontmatter.date}>
|
|
||||||
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
})}
|
|
||||||
</time>
|
|
||||||
</div>
|
|
||||||
<h3 className="text-lg md:text-xl font-bold text-primary group-hover:text-accent-dark transition-colors line-clamp-2 mb-4 md:mb-6 leading-tight">
|
|
||||||
{post.frontmatter.title}
|
|
||||||
</h3>
|
|
||||||
<div className="mt-auto flex items-center text-primary font-bold group-hover:underline decoration-2 underline-offset-4">
|
|
||||||
{t('readMore')}
|
|
||||||
<svg
|
|
||||||
className="ml-2 w-5 h-5 transition-transform group-hover:translate-x-2"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M17 8l4 4m0 0l-4 4m4-4H3"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
|
<ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 m-0 p-0 list-none">
|
||||||
|
{recentPosts.map((post, idx) => (
|
||||||
|
<li key={`${post.slug}-${idx}`} className="block">
|
||||||
|
<Link
|
||||||
|
href={`/${locale}/blog/${post.slug}`}
|
||||||
|
className="group block relative h-[400px] md:h-[500px] lg:h-[650px] overflow-hidden border-b md:border-b-0 md:border-r border-white/10 last:border-0 focus:outline-none"
|
||||||
|
>
|
||||||
|
{post.frontmatter.featuredImage && (
|
||||||
|
<>
|
||||||
|
<Image
|
||||||
|
src={post.frontmatter.featuredImage.split('?')[0]}
|
||||||
|
alt={post.frontmatter.title}
|
||||||
|
fill
|
||||||
|
className="object-cover transition-transform duration-1000 group-hover:scale-110"
|
||||||
|
style={{
|
||||||
|
objectPosition: `${post.frontmatter.focalX ?? 50}% ${post.frontmatter.focalY ?? 50}%`,
|
||||||
|
}}
|
||||||
|
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 25vw"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-primary-dark/40 group-hover:bg-primary-dark/60 transition-all duration-500" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="absolute inset-0 p-8 md:p-10 flex flex-col justify-end text-white">
|
||||||
|
<div className="mb-4 md:mb-6 transform transition-all duration-500 group-hover:-translate-y-4">
|
||||||
|
<div className="flex flex-wrap items-center gap-2 mb-4">
|
||||||
|
{post.frontmatter.category && (
|
||||||
|
<span className="px-3 py-1 bg-accent text-primary-dark rounded-full text-[10px] md:text-xs font-bold uppercase tracking-wider shadow-sm">
|
||||||
|
{post.frontmatter.category}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<time
|
||||||
|
dateTime={post.frontmatter.date}
|
||||||
|
suppressHydrationWarning
|
||||||
|
className="px-3 py-1 text-white/80 text-[10px] md:text-xs font-bold uppercase tracking-widest border border-white/20 rounded-full bg-white/10 backdrop-blur-md"
|
||||||
|
>
|
||||||
|
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
})}
|
||||||
|
</time>
|
||||||
|
{(new Date(post.frontmatter.date) > new Date() ||
|
||||||
|
post.frontmatter.public === false) && (
|
||||||
|
<span className="px-2 py-0.5 border border-white/40 text-white/90 rounded uppercase tracking-widest text-[10px] md:text-xs font-bold bg-neutral-dark/40 shadow-sm">
|
||||||
|
Draft Preview
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl md:text-2xl font-bold mb-2 md:mb-4 leading-tight drop-shadow-md">
|
||||||
|
{post.frontmatter.title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center text-accent font-bold tracking-wider uppercase text-xs md:text-xs opacity-100 md:opacity-0 group-hover:opacity-100 transition-all duration-500 delay-100">
|
||||||
|
{t('readMore')}{' '}
|
||||||
|
<span className="ml-2 transition-transform group-hover:translate-x-2">
|
||||||
|
→
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ services:
|
|||||||
NEXT_TELEMETRY_DISABLED: "1"
|
NEXT_TELEMETRY_DISABLED: "1"
|
||||||
POSTGRES_URI: postgres://${PAYLOAD_DB_USER:-payload}:${PAYLOAD_DB_PASSWORD:-120in09oenaoinsd9iaidon}@klz-db:5432/${PAYLOAD_DB_NAME:-payload}
|
POSTGRES_URI: postgres://${PAYLOAD_DB_USER:-payload}:${PAYLOAD_DB_PASSWORD:-120in09oenaoinsd9iaidon}@klz-db:5432/${PAYLOAD_DB_NAME:-payload}
|
||||||
PAYLOAD_SECRET: ${PAYLOAD_SECRET:-fallback-secret-for-dev}
|
PAYLOAD_SECRET: ${PAYLOAD_SECRET:-fallback-secret-for-dev}
|
||||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
NODE_OPTIONS: "--max-old-space-size=8192"
|
||||||
UV_THREADPOOL_SIZE: "4"
|
UV_THREADPOOL_SIZE: "4"
|
||||||
NPM_TOKEN: ${NPM_TOKEN:-}
|
NPM_TOKEN: ${NPM_TOKEN:-}
|
||||||
CI: "true"
|
CI: "true"
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ services:
|
|||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}.middlewares=${AUTH_MIDDLEWARE:-klz-ratelimit,klz-forward,klz-compress}"
|
- "traefik.http.routers.${PROJECT_NAME:-klz}.middlewares=${AUTH_MIDDLEWARE:-klz-ratelimit,klz-forward,klz-compress}"
|
||||||
|
|
||||||
# Public Router – paths that bypass Gatekeeper auth (health, SEO, static assets, OG images)
|
# Public Router – paths that bypass Gatekeeper auth (health, SEO, static assets, OG images)
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.rule=(${TRAEFIK_HOST_RULE:-Host(`${TRAEFIK_HOST:-klz-cables.com}`)}) && PathRegexp(`^/(health|uploads|media|robots\\.txt|manifest\\.webmanifest|sitemap(-[0-9]+)?\\.xml|(.*/)?api/og(/.*)?|(.*/)?opengraph-image.*)`)"
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.rule=(${TRAEFIK_HOST_RULE:-Host(`${TRAEFIK_HOST:-klz-cables.com}`)}) && PathRegexp(`^/([a-z]{2}/)?(health|login|gatekeeper|uploads|media|robots\\.txt|manifest\\.webmanifest|sitemap(-[0-9]+)?\\.xml|(.*/)?api/og(/.*)?|(.*/)?opengraph-image.*)`)"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.tls=${TRAEFIK_TLS:-false}"
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-public.tls=${TRAEFIK_TLS:-false}"
|
||||||
@@ -46,9 +46,21 @@ services:
|
|||||||
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-forward.headers.customrequestheaders.X-Forwarded-Proto=https"
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-forward.headers.customrequestheaders.X-Forwarded-Proto=https"
|
||||||
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-forward.headers.customrequestheaders.X-Forwarded-Ssl=on"
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-forward.headers.customrequestheaders.X-Forwarded-Ssl=on"
|
||||||
|
|
||||||
|
# Login redirect – the app's middleware sends users to /login but login lives at /gatekeeper/login
|
||||||
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-loginredirect.redirectregex.regex=^https?://[^/]+/([a-z]{2}/)?login(.*)"
|
||||||
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-loginredirect.redirectregex.replacement=https://${TRAEFIK_HOST:-klz-cables.com}/gatekeeper/login$${2}"
|
||||||
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-loginredirect.redirectregex.permanent=false"
|
||||||
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-loginredir.rule=(${TRAEFIK_HOST_RULE:-Host(`${TRAEFIK_HOST:-klz-cables.com}`)}) && PathRegexp(`^/([a-z]{2}/)?login`)"
|
||||||
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-loginredir.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
||||||
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-loginredir.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
|
||||||
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-loginredir.tls=${TRAEFIK_TLS:-false}"
|
||||||
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-loginredir.middlewares=${PROJECT_NAME:-klz}-loginredirect"
|
||||||
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-loginredir.service=${PROJECT_NAME:-klz}-app-svc"
|
||||||
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-loginredir.priority=2002"
|
||||||
|
|
||||||
klz-gatekeeper:
|
klz-gatekeeper:
|
||||||
profiles: [ "gatekeeper" ]
|
profiles: [ "gatekeeper" ]
|
||||||
image: registry.infra.mintel.me/mintel/gatekeeper:v1.7.12
|
image: registry.infra.mintel.me/mintel/gatekeeper:testing
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
infra:
|
infra:
|
||||||
@@ -66,8 +78,8 @@ services:
|
|||||||
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-auth.forwardauth.authResponseHeaders=X-Auth-User"
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz}-auth.forwardauth.authResponseHeaders=X-Auth-User"
|
||||||
- "traefik.docker.network=infra"
|
- "traefik.docker.network=infra"
|
||||||
|
|
||||||
# Gatekeeper Public Router (Login/Auth UI)
|
# Gatekeeper Public Router (Login/Auth UI) — basePath mode on main domain
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-gatekeeper.rule=(${TRAEFIK_HOST_RULE:-Host(`${TRAEFIK_HOST:-klz-cables.com}`)}) && PathRegexp(`^/(login|gatekeeper)(/.*)?`)"
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-gatekeeper.rule=(${TRAEFIK_HOST_RULE:-Host(`${TRAEFIK_HOST:-klz-cables.com}`)}) && PathPrefix(`/gatekeeper`)"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-gatekeeper.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-gatekeeper.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-gatekeeper.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-gatekeeper.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz}-gatekeeper.tls=${TRAEFIK_TLS:-false}"
|
- "traefik.http.routers.${PROJECT_NAME:-klz}-gatekeeper.tls=${TRAEFIK_TLS:-false}"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// Sentry initialization move to GlitchtipErrorReportingService to allow lazy-loading
|
// Sentry initialization move to GlitchtipErrorReportingService to allow lazy-loading
|
||||||
// for PageSpeed 100 optimizations. This file is now empty to prevent the SDK
|
// for PageSpeed 100 optimizations. This file is now empty to prevent the SDK
|
||||||
// from being included in the initial JS bundle.
|
// from being included in the initial JS bundle.
|
||||||
export {};
|
import * as Sentry from '@sentry/nextjs';
|
||||||
|
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
|
||||||
45
lib/blog.ts
45
lib/blog.ts
@@ -59,18 +59,50 @@ export async function getPostBySlug(slug: string, locale: string): Promise<PostD
|
|||||||
try {
|
try {
|
||||||
const payload = await getPayload({ config: configPromise });
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV === 'development';
|
// First try: Find in the requested locale
|
||||||
const { docs } = await payload.find({
|
let { docs } = await payload.find({
|
||||||
collection: 'posts',
|
collection: 'posts',
|
||||||
where: {
|
where: {
|
||||||
slug: { equals: slug },
|
slug: { equals: slug },
|
||||||
...(!isDev ? { _status: { equals: 'published' } } : {}),
|
...(!config.showDrafts ? { _status: { equals: 'published' } } : {}),
|
||||||
},
|
},
|
||||||
locale: locale as any,
|
locale: locale as any,
|
||||||
draft: isDev,
|
draft: config.showDrafts,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fallback: If not found, try searching across all locales.
|
||||||
|
// This happens when a user uses the static language switcher
|
||||||
|
// e.g. switching from /en/blog/en-slug to /de/blog/en-slug.
|
||||||
|
if (!docs || docs.length === 0) {
|
||||||
|
const { docs: crossLocaleDocs } = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
where: {
|
||||||
|
slug: { equals: slug },
|
||||||
|
...(!config.showDrafts ? { _status: { equals: 'published' } } : {}),
|
||||||
|
},
|
||||||
|
locale: 'all',
|
||||||
|
draft: config.showDrafts,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (crossLocaleDocs && crossLocaleDocs.length > 0) {
|
||||||
|
// Fetch the found document again, but strictly in the requested locale
|
||||||
|
// so we get the correctly translated fields (like the localized slug)
|
||||||
|
const { docs: correctLocaleDocs } = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
where: {
|
||||||
|
id: { equals: crossLocaleDocs[0].id },
|
||||||
|
},
|
||||||
|
locale: locale as any,
|
||||||
|
draft: config.showDrafts,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
docs = correctLocaleDocs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!docs || docs.length === 0) return null;
|
if (!docs || docs.length === 0) return null;
|
||||||
|
|
||||||
const doc = docs[0];
|
const doc = docs[0];
|
||||||
@@ -107,15 +139,14 @@ export async function getPostBySlug(slug: string, locale: string): Promise<PostD
|
|||||||
export async function getAllPosts(locale: string): Promise<PostData[]> {
|
export async function getAllPosts(locale: string): Promise<PostData[]> {
|
||||||
try {
|
try {
|
||||||
const payload = await getPayload({ config: configPromise });
|
const payload = await getPayload({ config: configPromise });
|
||||||
const isDev = process.env.NODE_ENV === 'development';
|
|
||||||
const { docs } = await payload.find({
|
const { docs } = await payload.find({
|
||||||
collection: 'posts',
|
collection: 'posts',
|
||||||
where: {
|
where: {
|
||||||
...(!isDev ? { _status: { equals: 'published' } } : {}),
|
...(!config.showDrafts ? { _status: { equals: 'published' } } : {}),
|
||||||
},
|
},
|
||||||
locale: locale as any,
|
locale: locale as any,
|
||||||
sort: '-date',
|
sort: '-date',
|
||||||
draft: isDev,
|
draft: config.showDrafts,
|
||||||
limit: 100,
|
limit: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ function createConfig() {
|
|||||||
isStaging: target === 'staging',
|
isStaging: target === 'staging',
|
||||||
isTesting: target === 'testing',
|
isTesting: target === 'testing',
|
||||||
isDevelopment: target === 'development',
|
isDevelopment: target === 'development',
|
||||||
|
showDrafts: target === 'development' || target === 'testing' || target === 'staging',
|
||||||
feedbackEnabled: env.NEXT_PUBLIC_FEEDBACK_ENABLED,
|
feedbackEnabled: env.NEXT_PUBLIC_FEEDBACK_ENABLED,
|
||||||
gatekeeperUrl: env.GATEKEEPER_URL,
|
gatekeeperUrl: env.GATEKEEPER_URL,
|
||||||
|
|
||||||
@@ -116,6 +117,9 @@ export const config = {
|
|||||||
get isDevelopment() {
|
get isDevelopment() {
|
||||||
return getConfig().isDevelopment;
|
return getConfig().isDevelopment;
|
||||||
},
|
},
|
||||||
|
get showDrafts() {
|
||||||
|
return getConfig().showDrafts;
|
||||||
|
},
|
||||||
get baseUrl() {
|
get baseUrl() {
|
||||||
return getConfig().baseUrl;
|
return getConfig().baseUrl;
|
||||||
},
|
},
|
||||||
|
|||||||
76
lib/pages.ts
76
lib/pages.ts
@@ -1,5 +1,7 @@
|
|||||||
import { getPayload } from 'payload';
|
import { getPayload } from 'payload';
|
||||||
import configPromise from '@payload-config';
|
import configPromise from '@payload-config';
|
||||||
|
import { mapSlugToFileSlug } from './slugs';
|
||||||
|
import { config } from '@/lib/config';
|
||||||
|
|
||||||
export interface PageFrontmatter {
|
export interface PageFrontmatter {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -44,19 +46,81 @@ function mapDoc(doc: any): PageData {
|
|||||||
export async function getPageBySlug(slug: string, locale: string): Promise<PageData | null> {
|
export async function getPageBySlug(slug: string, locale: string): Promise<PageData | null> {
|
||||||
try {
|
try {
|
||||||
const payload = await getPayload({ config: configPromise });
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
||||||
|
|
||||||
const result = await payload.find({
|
// Try finding exact match first
|
||||||
collection: 'pages' as any,
|
let result = await payload.find({
|
||||||
|
collection: 'pages',
|
||||||
where: {
|
where: {
|
||||||
slug: { equals: slug },
|
and: [
|
||||||
|
{ slug: { equals: fileSlug } },
|
||||||
|
...(!config.showDrafts ? [{ _status: { equals: 'published' } }] : []),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
locale: locale as any,
|
locale: locale as any,
|
||||||
|
depth: 1,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const docs = result.docs as any[];
|
// Fallback: search ALL locales
|
||||||
if (!docs || docs.length === 0) return null;
|
if (result.docs.length === 0) {
|
||||||
return mapDoc(docs[0]);
|
const crossResult = await payload.find({
|
||||||
|
collection: 'pages',
|
||||||
|
where: {
|
||||||
|
and: [
|
||||||
|
{ slug: { equals: fileSlug } },
|
||||||
|
...(!config.showDrafts ? [{ _status: { equals: 'published' } }] : []),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
locale: 'all',
|
||||||
|
depth: 1,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (crossResult.docs.length > 0) {
|
||||||
|
// Fetch missing exact match by internal id
|
||||||
|
result = await payload.find({
|
||||||
|
collection: 'pages',
|
||||||
|
where: {
|
||||||
|
id: { equals: crossResult.docs[0].id },
|
||||||
|
},
|
||||||
|
locale: locale as any,
|
||||||
|
depth: 1,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.docs.length > 0) {
|
||||||
|
const doc = result.docs[0];
|
||||||
|
|
||||||
|
return {
|
||||||
|
slug: doc.slug,
|
||||||
|
frontmatter: {
|
||||||
|
title: doc.title,
|
||||||
|
excerpt: doc.excerpt || '',
|
||||||
|
featuredImage:
|
||||||
|
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
||||||
|
? doc.featuredImage.sizes?.card?.url || doc.featuredImage.url
|
||||||
|
: null,
|
||||||
|
focalX:
|
||||||
|
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
||||||
|
? doc.featuredImage.focalX
|
||||||
|
: 50,
|
||||||
|
focalY:
|
||||||
|
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
||||||
|
? doc.featuredImage.focalY
|
||||||
|
: 50,
|
||||||
|
layout:
|
||||||
|
doc.layout === 'fullBleed' || doc.layout === 'default'
|
||||||
|
? doc.layout
|
||||||
|
: ('default' as const),
|
||||||
|
},
|
||||||
|
content: doc.content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[Payload] getPageBySlug failed for ${slug}:`, error);
|
console.error(`[Payload] getPageBySlug failed for ${slug}:`, error);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { getPayload } from 'payload';
|
import { getPayload } from 'payload';
|
||||||
import configPromise from '@payload-config';
|
import configPromise from '@payload-config';
|
||||||
import { mapSlugToFileSlug } from './slugs';
|
import { mapSlugToFileSlug } from './slugs';
|
||||||
|
import { config } from '@/lib/config';
|
||||||
|
|
||||||
export interface ProductFrontmatter {
|
export interface ProductFrontmatter {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -17,6 +18,7 @@ export interface ProductData {
|
|||||||
slug: string;
|
slug: string;
|
||||||
frontmatter: ProductFrontmatter;
|
frontmatter: ProductFrontmatter;
|
||||||
content: any; // Lexical AST from Payload
|
content: any; // Lexical AST from Payload
|
||||||
|
application?: any; // Lexical AST for Application field
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getProductMetadata(
|
export async function getProductMetadata(
|
||||||
@@ -26,13 +28,12 @@ export async function getProductMetadata(
|
|||||||
const payload = await getPayload({ config: configPromise });
|
const payload = await getPayload({ config: configPromise });
|
||||||
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV === 'development';
|
|
||||||
const result = await payload.find({
|
const result = await payload.find({
|
||||||
collection: 'products',
|
collection: 'products',
|
||||||
where: {
|
where: {
|
||||||
and: [
|
and: [
|
||||||
{ slug: { equals: fileSlug } },
|
{ slug: { equals: fileSlug } },
|
||||||
...(!isDev ? [{ _status: { equals: 'published' } }] : []),
|
...(!config.showDrafts ? [{ _status: { equals: 'published' } }] : []),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
locale: locale as any,
|
locale: locale as any,
|
||||||
@@ -70,13 +71,12 @@ export async function getProductBySlug(slug: string, locale: string): Promise<Pr
|
|||||||
const payload = await getPayload({ config: configPromise });
|
const payload = await getPayload({ config: configPromise });
|
||||||
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV === 'development';
|
|
||||||
const result = await payload.find({
|
const result = await payload.find({
|
||||||
collection: 'products',
|
collection: 'products',
|
||||||
where: {
|
where: {
|
||||||
and: [
|
and: [
|
||||||
{ slug: { equals: fileSlug } },
|
{ slug: { equals: fileSlug } },
|
||||||
...(!isDev ? [{ _status: { equals: 'published' } }] : []),
|
...(!config.showDrafts ? [{ _status: { equals: 'published' } }] : []),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
locale: locale as any,
|
locale: locale as any,
|
||||||
@@ -114,6 +114,7 @@ export async function getProductBySlug(slug: string, locale: string): Promise<Pr
|
|||||||
: 50,
|
: 50,
|
||||||
},
|
},
|
||||||
content: doc.content,
|
content: doc.content,
|
||||||
|
application: doc.application,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,11 +128,10 @@ export async function getProductBySlug(slug: string, locale: string): Promise<Pr
|
|||||||
export async function getAllProductSlugs(locale: string): Promise<string[]> {
|
export async function getAllProductSlugs(locale: string): Promise<string[]> {
|
||||||
try {
|
try {
|
||||||
const payload = await getPayload({ config: configPromise });
|
const payload = await getPayload({ config: configPromise });
|
||||||
const isDev = process.env.NODE_ENV === 'development';
|
|
||||||
const result = await payload.find({
|
const result = await payload.find({
|
||||||
collection: 'products',
|
collection: 'products',
|
||||||
where: {
|
where: {
|
||||||
...(!isDev ? { _status: { equals: 'published' } } : {}),
|
...(!config.showDrafts ? { _status: { equals: 'published' } } : {}),
|
||||||
},
|
},
|
||||||
locale: locale as any,
|
locale: locale as any,
|
||||||
pagination: false,
|
pagination: false,
|
||||||
@@ -157,11 +157,10 @@ export async function getAllProducts(locale: string): Promise<ProductData[]> {
|
|||||||
images: true,
|
images: true,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV === 'development';
|
|
||||||
const result = await payload.find({
|
const result = await payload.find({
|
||||||
collection: 'products',
|
collection: 'products',
|
||||||
where: {
|
where: {
|
||||||
...(!isDev ? { _status: { equals: 'published' } } : {}),
|
...(!config.showDrafts ? { _status: { equals: 'published' } } : {}),
|
||||||
},
|
},
|
||||||
locale: locale as any,
|
locale: locale as any,
|
||||||
depth: 1,
|
depth: 1,
|
||||||
@@ -198,6 +197,7 @@ export async function getAllProducts(locale: string): Promise<ProductData[]> {
|
|||||||
: 50,
|
: 50,
|
||||||
},
|
},
|
||||||
content: null,
|
content: null,
|
||||||
|
application: null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -45,8 +45,10 @@ export default async function middleware(request: NextRequest) {
|
|||||||
if (internalHosts.includes(urlObj.hostname)) {
|
if (internalHosts.includes(urlObj.hostname)) {
|
||||||
const proto = headers.get('x-forwarded-proto') || 'https';
|
const proto = headers.get('x-forwarded-proto') || 'https';
|
||||||
// Prioritize x-forwarded-host (passed by Traefik) over the local Host header
|
// Prioritize x-forwarded-host (passed by Traefik) over the local Host header
|
||||||
const hostHeader =
|
const fallbackHost = process.env.NEXT_PUBLIC_BASE_URL
|
||||||
headers.get('x-forwarded-host') || headers.get('host') || 'testing.klz-cables.com';
|
? new URL(process.env.NEXT_PUBLIC_BASE_URL).host
|
||||||
|
: 'klz-cables.com';
|
||||||
|
const hostHeader = headers.get('x-forwarded-host') || headers.get('host') || fallbackHost;
|
||||||
|
|
||||||
urlObj.protocol = proto;
|
urlObj.protocol = proto;
|
||||||
|
|
||||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/types/routes.d.ts";
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
optimizePackageImports: ['lucide-react', 'framer-motion', '@/components/ui'],
|
optimizePackageImports: ['lucide-react', 'framer-motion', '@/components/ui'],
|
||||||
cpus: 1,
|
cpus: 3,
|
||||||
workerThreads: false,
|
workerThreads: false,
|
||||||
memoryBasedWorkersCount: true,
|
|
||||||
},
|
},
|
||||||
reactStrictMode: false,
|
reactStrictMode: false,
|
||||||
|
swcMinify: true,
|
||||||
productionBrowserSourceMaps: false,
|
productionBrowserSourceMaps: false,
|
||||||
logging: {
|
logging: {
|
||||||
fetches: {
|
fetches: {
|
||||||
@@ -394,6 +394,7 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
images: {
|
images: {
|
||||||
formats: ['image/webp'],
|
formats: ['image/webp'],
|
||||||
|
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{
|
{
|
||||||
protocol: 'https',
|
protocol: 'https',
|
||||||
@@ -403,6 +404,14 @@ const nextConfig = {
|
|||||||
protocol: 'https',
|
protocol: 'https',
|
||||||
hostname: '*.klz-cables.com',
|
hostname: '*.klz-cables.com',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
protocol: 'http',
|
||||||
|
hostname: 'klz-cables.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: 'http',
|
||||||
|
hostname: '*.klz-cables.com',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
protocol: 'http',
|
protocol: 'http',
|
||||||
hostname: 'klz.localhost',
|
hostname: 'klz.localhost',
|
||||||
@@ -428,6 +437,31 @@ const nextConfig = {
|
|||||||
source: '/de/kontakt',
|
source: '/de/kontakt',
|
||||||
destination: '/de/contact',
|
destination: '/de/contact',
|
||||||
},
|
},
|
||||||
|
// Safety rewrites for English locale using German slugs (legacy or content errors)
|
||||||
|
{
|
||||||
|
source: '/en/produkte',
|
||||||
|
destination: '/en/products',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: '/en/produkte/:path*',
|
||||||
|
destination: '/en/products/:path*',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: '/en/kontakt',
|
||||||
|
destination: '/en/contact',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: '/en/impressum',
|
||||||
|
destination: '/en/legal-notice',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: '/en/datenschutz',
|
||||||
|
destination: '/en/privacy-policy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: '/en/agbs',
|
||||||
|
destination: '/en/terms',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
afterFiles: [],
|
afterFiles: [],
|
||||||
fallback: [],
|
fallback: [],
|
||||||
|
|||||||
15
package.json
15
package.json
@@ -4,10 +4,10 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@10.18.3",
|
"packageManager": "pnpm@10.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mintel/mail": "1.8.3",
|
"@mintel/mail": "^1.8.21",
|
||||||
"@mintel/next-config": "1.8.3",
|
"@mintel/next-config": "^1.8.21",
|
||||||
"@mintel/next-feedback": "1.8.10",
|
"@mintel/next-feedback": "^1.8.21",
|
||||||
"@mintel/next-utils": "^1.7.15",
|
"@mintel/next-utils": "^1.8.21",
|
||||||
"@payloadcms/db-postgres": "^3.77.0",
|
"@payloadcms/db-postgres": "^3.77.0",
|
||||||
"@payloadcms/email-nodemailer": "^3.77.0",
|
"@payloadcms/email-nodemailer": "^3.77.0",
|
||||||
"@payloadcms/next": "^3.77.0",
|
"@payloadcms/next": "^3.77.0",
|
||||||
@@ -53,8 +53,8 @@
|
|||||||
"@commitlint/config-conventional": "^20.4.0",
|
"@commitlint/config-conventional": "^20.4.0",
|
||||||
"@cspell/dict-de-de": "^4.1.2",
|
"@cspell/dict-de-de": "^4.1.2",
|
||||||
"@lhci/cli": "^0.15.1",
|
"@lhci/cli": "^0.15.1",
|
||||||
"@mintel/eslint-config": "1.8.3",
|
"@mintel/eslint-config": "1.8.21",
|
||||||
"@mintel/tsconfig": "1.8.3",
|
"@mintel/tsconfig": "^1.8.21",
|
||||||
"@next/bundle-analyzer": "^16.1.6",
|
"@next/bundle-analyzer": "^16.1.6",
|
||||||
"@tailwindcss/cli": "^4.1.18",
|
"@tailwindcss/cli": "^4.1.18",
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
@@ -65,6 +65,7 @@
|
|||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/sharp": "^0.31.1",
|
"@types/sharp": "^0.31.1",
|
||||||
|
"@types/three": "^0.183.1",
|
||||||
"@vitejs/plugin-react": "^5.1.4",
|
"@vitejs/plugin-react": "^5.1.4",
|
||||||
"@vitest/ui": "^4.0.16",
|
"@vitest/ui": "^4.0.16",
|
||||||
"autoprefixer": "^10.4.23",
|
"autoprefixer": "^10.4.23",
|
||||||
@@ -110,6 +111,8 @@
|
|||||||
"check:security": "tsx ./scripts/check-security.ts",
|
"check:security": "tsx ./scripts/check-security.ts",
|
||||||
"check:links": "bash ./scripts/check-links.sh",
|
"check:links": "bash ./scripts/check-links.sh",
|
||||||
"check:assets": "tsx ./scripts/check-broken-assets.ts",
|
"check:assets": "tsx ./scripts/check-broken-assets.ts",
|
||||||
|
"check:forms": "tsx ./scripts/check-forms.ts",
|
||||||
|
"check:apis": "tsx ./scripts/check-apis.ts",
|
||||||
"pdf:datasheets": "tsx ./scripts/generate-pdf-datasheets.ts",
|
"pdf:datasheets": "tsx ./scripts/generate-pdf-datasheets.ts",
|
||||||
"pdf:datasheets:legacy": "tsx ./scripts/generate-pdf-datasheets-pdf-lib.ts",
|
"pdf:datasheets:legacy": "tsx ./scripts/generate-pdf-datasheets-pdf-lib.ts",
|
||||||
"cms:migrate": "payload migrate",
|
"cms:migrate": "payload migrate",
|
||||||
|
|||||||
@@ -45,9 +45,7 @@ export default buildConfig({
|
|||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
titleSuffix: ' – KLZ Cables',
|
titleSuffix: ' – KLZ Cables',
|
||||||
icons: [
|
icons: [{ rel: 'icon', type: 'image/x-icon', url: '/favicon.ico' }],
|
||||||
{ rel: 'icon', type: 'image/x-icon', url: '/favicon.ico' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
localization: {
|
localization: {
|
||||||
@@ -80,18 +78,25 @@ export default buildConfig({
|
|||||||
`postgresql://${process.env.PAYLOAD_DB_USER || 'payload'}:${process.env.PAYLOAD_DB_PASSWORD || '120in09oenaoinsd9iaidon'}@127.0.0.1:54322/${process.env.PAYLOAD_DB_NAME || 'payload'}`,
|
`postgresql://${process.env.PAYLOAD_DB_USER || 'payload'}:${process.env.PAYLOAD_DB_PASSWORD || '120in09oenaoinsd9iaidon'}@127.0.0.1:54322/${process.env.PAYLOAD_DB_NAME || 'payload'}`,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
email: nodemailerAdapter({
|
email: process.env.MAIL_HOST
|
||||||
defaultFromAddress: process.env.MAIL_FROM?.replace(/.*<|>.*/g, '') || 'postmaster@mg.mintel.me',
|
? nodemailerAdapter({
|
||||||
defaultFromName: process.env.MAIL_FROM?.split('<')[0]?.trim() || 'KLZ Cables',
|
defaultFromAddress:
|
||||||
transportOptions: {
|
process.env.MAIL_FROM?.replace(/.*<|>.*/g, '') || 'postmaster@mg.mintel.me',
|
||||||
host: process.env.MAIL_HOST || 'smtp.eu.mailgun.org',
|
defaultFromName: process.env.MAIL_FROM?.split('<')[0]?.trim() || 'KLZ Cables',
|
||||||
port: Number(process.env.MAIL_PORT) || 587,
|
transportOptions: {
|
||||||
auth: {
|
host: process.env.MAIL_HOST || 'smtp.eu.mailgun.org',
|
||||||
user: process.env.MAIL_USERNAME,
|
port: Number(process.env.MAIL_PORT) || 587,
|
||||||
pass: process.env.MAIL_PASSWORD,
|
...(process.env.MAIL_USERNAME
|
||||||
},
|
? {
|
||||||
},
|
auth: {
|
||||||
}),
|
user: process.env.MAIL_USERNAME,
|
||||||
|
pass: process.env.MAIL_PASSWORD,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: undefined,
|
||||||
sharp,
|
sharp,
|
||||||
plugins: [],
|
plugins: [],
|
||||||
});
|
});
|
||||||
|
|||||||
370
pnpm-lock.yaml
generated
370
pnpm-lock.yaml
generated
@@ -13,17 +13,17 @@ importers:
|
|||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@mintel/mail':
|
'@mintel/mail':
|
||||||
specifier: 1.8.3
|
specifier: ^1.8.21
|
||||||
version: 1.8.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 1.8.21(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
'@mintel/next-config':
|
'@mintel/next-config':
|
||||||
specifier: 1.8.3
|
specifier: ^1.8.21
|
||||||
version: 1.8.3(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)(typescript@5.9.3)(webpack@5.105.0(esbuild@0.25.12))
|
version: 1.8.21(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)(typescript@5.9.3)(webpack@5.105.0(esbuild@0.25.12))
|
||||||
'@mintel/next-feedback':
|
'@mintel/next-feedback':
|
||||||
specifier: 1.8.10
|
specifier: ^1.8.21
|
||||||
version: 1.8.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)
|
version: 1.8.21(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)
|
||||||
'@mintel/next-utils':
|
'@mintel/next-utils':
|
||||||
specifier: ^1.7.15
|
specifier: ^1.8.21
|
||||||
version: 1.7.15(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)(typescript@5.9.3)
|
version: 1.8.21(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)(typescript@5.9.3)
|
||||||
'@payloadcms/db-postgres':
|
'@payloadcms/db-postgres':
|
||||||
specifier: ^3.77.0
|
specifier: ^3.77.0
|
||||||
version: 3.77.0(@opentelemetry/api@1.9.0)(payload@3.77.0(graphql@16.12.0)(typescript@5.9.3))
|
version: 3.77.0(@opentelemetry/api@1.9.0)(payload@3.77.0(graphql@16.12.0)(typescript@5.9.3))
|
||||||
@@ -155,11 +155,11 @@ importers:
|
|||||||
specifier: ^0.15.1
|
specifier: ^0.15.1
|
||||||
version: 0.15.1
|
version: 0.15.1
|
||||||
'@mintel/eslint-config':
|
'@mintel/eslint-config':
|
||||||
specifier: 1.8.3
|
specifier: 1.8.21
|
||||||
version: 1.8.3(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
version: 1.8.21(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||||
'@mintel/tsconfig':
|
'@mintel/tsconfig':
|
||||||
specifier: 1.8.3
|
specifier: ^1.8.21
|
||||||
version: 1.8.3
|
version: 1.8.21
|
||||||
'@next/bundle-analyzer':
|
'@next/bundle-analyzer':
|
||||||
specifier: ^16.1.6
|
specifier: ^16.1.6
|
||||||
version: 16.1.6
|
version: 16.1.6
|
||||||
@@ -190,6 +190,9 @@ importers:
|
|||||||
'@types/sharp':
|
'@types/sharp':
|
||||||
specifier: ^0.31.1
|
specifier: ^0.31.1
|
||||||
version: 0.31.1
|
version: 0.31.1
|
||||||
|
'@types/three':
|
||||||
|
specifier: ^0.183.1
|
||||||
|
version: 0.183.1
|
||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^5.1.4
|
specifier: ^5.1.4
|
||||||
version: 5.1.4(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
version: 5.1.4(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||||
@@ -721,6 +724,9 @@ packages:
|
|||||||
'@date-fns/tz@1.2.0':
|
'@date-fns/tz@1.2.0':
|
||||||
resolution: {integrity: sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==}
|
resolution: {integrity: sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==}
|
||||||
|
|
||||||
|
'@dimforge/rapier3d-compat@0.12.0':
|
||||||
|
resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==}
|
||||||
|
|
||||||
'@directus/sdk@21.1.0':
|
'@directus/sdk@21.1.0':
|
||||||
resolution: {integrity: sha512-Ig8zZAQDbc7QMIM54N+x71C04lni9MN9yalNAezjDjFdNknTJzupDY7V5cb+kOJL8GsqDE9Bg8xq8xCmkDVs5A==}
|
resolution: {integrity: sha512-Ig8zZAQDbc7QMIM54N+x71C04lni9MN9yalNAezjDjFdNknTJzupDY7V5cb+kOJL8GsqDE9Bg8xq8xCmkDVs5A==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
@@ -1293,10 +1299,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==}
|
resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
|
'@eslint/eslintrc@3.3.4':
|
||||||
|
resolution: {integrity: sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@eslint/js@9.39.2':
|
'@eslint/js@9.39.2':
|
||||||
resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==}
|
resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
|
'@eslint/js@9.39.3':
|
||||||
|
resolution: {integrity: sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@eslint/object-schema@2.1.7':
|
'@eslint/object-schema@2.1.7':
|
||||||
resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==}
|
resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
@@ -1677,29 +1691,29 @@ packages:
|
|||||||
'@medv/finder@4.0.2':
|
'@medv/finder@4.0.2':
|
||||||
resolution: {integrity: sha512-RraNY9SCcx4KZV0Dh6BEW6XEW2swkqYca74pkFFRw6hHItSHiy+O/xMnpbofjYbzXj0tSpBGthUF1hHTsr3vIQ==}
|
resolution: {integrity: sha512-RraNY9SCcx4KZV0Dh6BEW6XEW2swkqYca74pkFFRw6hHItSHiy+O/xMnpbofjYbzXj0tSpBGthUF1hHTsr3vIQ==}
|
||||||
|
|
||||||
'@mintel/eslint-config@1.8.3':
|
'@mintel/eslint-config@1.8.21':
|
||||||
resolution: {integrity: sha512-Alr0ofai74qhnrhZy2mpNSR4gcqD4DTQLHcHX3pZPpsqu1QfNjhHofU7dyHDx5/SpXoMXsI4hSaLpytKEMNHLg==}
|
resolution: {integrity: sha512-GH5tm1y89AhD+Lxf95BGCOdy7Nv1OPNLWrUpaTR6jsuKfH2dm9fU66LF7sDH5THmrkfAZ8zSzHJsKPjintv3IA==}
|
||||||
|
|
||||||
'@mintel/mail@1.8.3':
|
'@mintel/mail@1.8.21':
|
||||||
resolution: {integrity: sha512-y6MD0Rkn66D6gbMH1/6HNpneCJHUkDxtW7mYnq/ftRZf27ZDuhYqJaQVfXTCXS80oaWTzygc33Lpqz4UpdavQw==}
|
resolution: {integrity: sha512-leZV9gINmxD4eVJ3Ij9KdrQoyib67NVHgL/93J7KcWSUWKbr2HVuKUBpiWImeeEZn3JO0f7JwRbVUzXPBRVeQA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^19.0.0
|
react: ^19.0.0
|
||||||
react-dom: ^19.0.0
|
react-dom: ^19.0.0
|
||||||
|
|
||||||
'@mintel/next-config@1.8.3':
|
'@mintel/next-config@1.8.21':
|
||||||
resolution: {integrity: sha512-2t0ql4FCOqzLc4ImE3D39us5YHeKq9ZHjmWyiz9cwtJm89zGSvB/mzCja2ydrYtTEfLwxpiVKzXrVuSKLjFlow==}
|
resolution: {integrity: sha512-K4jb9Glf84a212BRZ/zmOUueBphmsikvStFCuDc5lxyFT+Hkj4w8ChmtI7gaUxHMrftooduGPXJ1+NFpKkvc/Q==}
|
||||||
|
|
||||||
'@mintel/next-feedback@1.8.10':
|
'@mintel/next-feedback@1.8.21':
|
||||||
resolution: {integrity: sha512-GgCkcgvAibOXli0bfx2/PLJgzPC0sTM67Wddef2eDCiV2Lgy4hZjX9P+PSKP5dwPGooyk29muOtm0HKJlkAXqg==}
|
resolution: {integrity: sha512-n2KzGDbOvAskuzjbt8h5EOMSEnISxHrsXxJwDdMxCXEgmzfJSvWpP2mAqb684dimOwo1UWHE6DMSAFc1FXeYwg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^19.0.0
|
react: ^19.0.0
|
||||||
react-dom: ^19.0.0
|
react-dom: ^19.0.0
|
||||||
|
|
||||||
'@mintel/next-utils@1.7.15':
|
'@mintel/next-utils@1.8.21':
|
||||||
resolution: {integrity: sha512-CqSe3eHamq9zLs+AJxGOPypTLchw/oZ3JcLkor007PcUDMTv/Lspfv5oCaXK2s0FeIOJaa2QwSGPDI1h5/3ZVw==}
|
resolution: {integrity: sha512-sr0yDtySGou+3DNvrqY6HWSHCiVIc8nnoRbckyPMSE21AGxk2aJineXGy9BO9tulSBdhStm2SgdC7McMFTszug==}
|
||||||
|
|
||||||
'@mintel/tsconfig@1.8.3':
|
'@mintel/tsconfig@1.8.21':
|
||||||
resolution: {integrity: sha512-IOtxrTZfPZs4XU4Q068dSUtETFb2drf43mgmaF31j+PtltfPkC7pdJcEjXDg365bZ5EUrgTlt9pYlgGmfrCEVQ==}
|
resolution: {integrity: sha512-ePBfBZiijyXKOS6nLIyxkg7QDZEEC1TugzNhmvwwpc0Yh7BmVHyNpvjg6zKsoGj2rok+9Kc8mLH1WihQIs8SKg==}
|
||||||
|
|
||||||
'@monaco-editor/loader@1.7.0':
|
'@monaco-editor/loader@1.7.0':
|
||||||
resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==}
|
resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==}
|
||||||
@@ -3039,6 +3053,9 @@ packages:
|
|||||||
'@tootallnate/quickjs-emscripten@0.23.0':
|
'@tootallnate/quickjs-emscripten@0.23.0':
|
||||||
resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==}
|
resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==}
|
||||||
|
|
||||||
|
'@tweenjs/tween.js@23.1.3':
|
||||||
|
resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
|
||||||
|
|
||||||
'@tybys/wasm-util@0.10.1':
|
'@tybys/wasm-util@0.10.1':
|
||||||
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
||||||
|
|
||||||
@@ -3152,6 +3169,9 @@ packages:
|
|||||||
'@types/node@22.19.11':
|
'@types/node@22.19.11':
|
||||||
resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==}
|
resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==}
|
||||||
|
|
||||||
|
'@types/node@22.19.13':
|
||||||
|
resolution: {integrity: sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==}
|
||||||
|
|
||||||
'@types/nodemailer@7.0.9':
|
'@types/nodemailer@7.0.9':
|
||||||
resolution: {integrity: sha512-vI8oF1M+8JvQhsId0Pc38BdUP2evenIIys7c7p+9OZXSPOH5c1dyINP1jT8xQ2xPuBUXmIC87s+91IZMDjH8Ow==}
|
resolution: {integrity: sha512-vI8oF1M+8JvQhsId0Pc38BdUP2evenIIys7c7p+9OZXSPOH5c1dyINP1jT8xQ2xPuBUXmIC87s+91IZMDjH8Ow==}
|
||||||
|
|
||||||
@@ -3187,9 +3207,15 @@ packages:
|
|||||||
'@types/sharp@0.31.1':
|
'@types/sharp@0.31.1':
|
||||||
resolution: {integrity: sha512-5nWwamN9ZFHXaYEincMSuza8nNfOof8nmO+mcI+Agx1uMUk4/pQnNIcix+9rLPXzKrm1pS34+6WRDbDV0Jn7ag==}
|
resolution: {integrity: sha512-5nWwamN9ZFHXaYEincMSuza8nNfOof8nmO+mcI+Agx1uMUk4/pQnNIcix+9rLPXzKrm1pS34+6WRDbDV0Jn7ag==}
|
||||||
|
|
||||||
|
'@types/stats.js@0.17.4':
|
||||||
|
resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==}
|
||||||
|
|
||||||
'@types/tedious@4.0.14':
|
'@types/tedious@4.0.14':
|
||||||
resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==}
|
resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==}
|
||||||
|
|
||||||
|
'@types/three@0.183.1':
|
||||||
|
resolution: {integrity: sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==}
|
||||||
|
|
||||||
'@types/trusted-types@2.0.7':
|
'@types/trusted-types@2.0.7':
|
||||||
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||||
|
|
||||||
@@ -3205,6 +3231,9 @@ packages:
|
|||||||
'@types/uuid@10.0.0':
|
'@types/uuid@10.0.0':
|
||||||
resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==}
|
resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==}
|
||||||
|
|
||||||
|
'@types/webxr@0.5.24':
|
||||||
|
resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==}
|
||||||
|
|
||||||
'@types/whatwg-mimetype@3.0.2':
|
'@types/whatwg-mimetype@3.0.2':
|
||||||
resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==}
|
resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==}
|
||||||
|
|
||||||
@@ -3214,63 +3243,63 @@ packages:
|
|||||||
'@types/yauzl@2.10.3':
|
'@types/yauzl@2.10.3':
|
||||||
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
|
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.55.0':
|
'@typescript-eslint/eslint-plugin@8.56.1':
|
||||||
resolution: {integrity: sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==}
|
resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@typescript-eslint/parser': ^8.55.0
|
'@typescript-eslint/parser': ^8.56.1
|
||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||||
typescript: '>=4.8.4 <6.0.0'
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
'@typescript-eslint/parser@8.55.0':
|
'@typescript-eslint/parser@8.56.1':
|
||||||
resolution: {integrity: sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==}
|
resolution: {integrity: sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||||
typescript: '>=4.8.4 <6.0.0'
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
'@typescript-eslint/project-service@8.55.0':
|
'@typescript-eslint/project-service@8.56.1':
|
||||||
resolution: {integrity: sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==}
|
resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=4.8.4 <6.0.0'
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
'@typescript-eslint/scope-manager@8.55.0':
|
'@typescript-eslint/scope-manager@8.56.1':
|
||||||
resolution: {integrity: sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==}
|
resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@typescript-eslint/tsconfig-utils@8.55.0':
|
'@typescript-eslint/tsconfig-utils@8.56.1':
|
||||||
resolution: {integrity: sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==}
|
resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=4.8.4 <6.0.0'
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
'@typescript-eslint/type-utils@8.55.0':
|
'@typescript-eslint/type-utils@8.56.1':
|
||||||
resolution: {integrity: sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==}
|
resolution: {integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||||
typescript: '>=4.8.4 <6.0.0'
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
'@typescript-eslint/types@8.55.0':
|
'@typescript-eslint/types@8.56.1':
|
||||||
resolution: {integrity: sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==}
|
resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@typescript-eslint/typescript-estree@8.55.0':
|
'@typescript-eslint/typescript-estree@8.56.1':
|
||||||
resolution: {integrity: sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==}
|
resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=4.8.4 <6.0.0'
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
'@typescript-eslint/utils@8.55.0':
|
'@typescript-eslint/utils@8.56.1':
|
||||||
resolution: {integrity: sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==}
|
resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||||
typescript: '>=4.8.4 <6.0.0'
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
'@typescript-eslint/visitor-keys@8.55.0':
|
'@typescript-eslint/visitor-keys@8.56.1':
|
||||||
resolution: {integrity: sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==}
|
resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
|
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
|
||||||
@@ -3453,6 +3482,9 @@ packages:
|
|||||||
'@webassemblyjs/wast-printer@1.14.1':
|
'@webassemblyjs/wast-printer@1.14.1':
|
||||||
resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==}
|
resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==}
|
||||||
|
|
||||||
|
'@webgpu/types@0.1.69':
|
||||||
|
resolution: {integrity: sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==}
|
||||||
|
|
||||||
'@xtuc/ieee754@1.2.0':
|
'@xtuc/ieee754@1.2.0':
|
||||||
resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
|
resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
|
||||||
|
|
||||||
@@ -3537,6 +3569,9 @@ packages:
|
|||||||
ajv@6.12.6:
|
ajv@6.12.6:
|
||||||
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
||||||
|
|
||||||
|
ajv@6.14.0:
|
||||||
|
resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
|
||||||
|
|
||||||
ajv@8.17.1:
|
ajv@8.17.1:
|
||||||
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
|
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
|
||||||
|
|
||||||
@@ -3715,6 +3750,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==}
|
resolution: {integrity: sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
|
balanced-match@4.0.4:
|
||||||
|
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
|
||||||
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
bare-events@2.8.2:
|
bare-events@2.8.2:
|
||||||
resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==}
|
resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3804,6 +3843,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==}
|
resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
|
brace-expansion@5.0.4:
|
||||||
|
resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==}
|
||||||
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
braces@3.0.3:
|
braces@3.0.3:
|
||||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -4824,6 +4867,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
|
resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
|
eslint-visitor-keys@5.0.1:
|
||||||
|
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
|
||||||
eslint@9.39.2:
|
eslint@9.39.2:
|
||||||
resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==}
|
resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
@@ -6162,6 +6209,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
meshoptimizer@1.0.1:
|
||||||
|
resolution: {integrity: sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==}
|
||||||
|
|
||||||
metaviewport-parser@0.3.0:
|
metaviewport-parser@0.3.0:
|
||||||
resolution: {integrity: sha512-EoYJ8xfjQ6kpe9VbVHvZTZHiOl4HL1Z18CrZ+qahvLXT7ZO4YTC2JMyt5FaUp9JJp6J4Ybb/z7IsCXZt86/QkQ==}
|
resolution: {integrity: sha512-EoYJ8xfjQ6kpe9VbVHvZTZHiOl4HL1Z18CrZ+qahvLXT7ZO4YTC2JMyt5FaUp9JJp6J4Ybb/z7IsCXZt86/QkQ==}
|
||||||
|
|
||||||
@@ -6282,6 +6332,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==}
|
resolution: {integrity: sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==}
|
||||||
engines: {node: 18 || 20 || >=22}
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
|
minimatch@10.2.4:
|
||||||
|
resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
|
||||||
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
minimist@1.2.8:
|
minimist@1.2.8:
|
||||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||||
|
|
||||||
@@ -6421,6 +6475,10 @@ packages:
|
|||||||
node-addon-api@7.1.1:
|
node-addon-api@7.1.1:
|
||||||
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||||
|
|
||||||
|
node-exports-info@1.6.0:
|
||||||
|
resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
node-fetch@2.7.0:
|
node-fetch@2.7.0:
|
||||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||||
engines: {node: 4.x || >=6.0.0}
|
engines: {node: 4.x || >=6.0.0}
|
||||||
@@ -7157,8 +7215,9 @@ packages:
|
|||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
resolve@2.0.0-next.5:
|
resolve@2.0.0-next.6:
|
||||||
resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==}
|
resolution: {integrity: sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
restore-cursor@2.0.0:
|
restore-cursor@2.0.0:
|
||||||
@@ -7887,11 +7946,11 @@ packages:
|
|||||||
typedarray-to-buffer@3.1.5:
|
typedarray-to-buffer@3.1.5:
|
||||||
resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==}
|
resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==}
|
||||||
|
|
||||||
typescript-eslint@8.55.0:
|
typescript-eslint@8.56.1:
|
||||||
resolution: {integrity: sha512-HE4wj+r5lmDVS9gdaN0/+iqNvPZwGfnJ5lZuz7s5vLlg9ODw0bIiiETaios9LvFI1U94/VBXGm3CB2Y5cNFMpw==}
|
resolution: {integrity: sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||||
typescript: '>=4.8.4 <6.0.0'
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
typescript@5.9.3:
|
typescript@5.9.3:
|
||||||
@@ -8892,6 +8951,8 @@ snapshots:
|
|||||||
|
|
||||||
'@date-fns/tz@1.2.0': {}
|
'@date-fns/tz@1.2.0': {}
|
||||||
|
|
||||||
|
'@dimforge/rapier3d-compat@0.12.0': {}
|
||||||
|
|
||||||
'@directus/sdk@21.1.0': {}
|
'@directus/sdk@21.1.0': {}
|
||||||
|
|
||||||
'@discoveryjs/json-ext@0.5.7': {}
|
'@discoveryjs/json-ext@0.5.7': {}
|
||||||
@@ -9281,8 +9342,24 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@eslint/eslintrc@3.3.4':
|
||||||
|
dependencies:
|
||||||
|
ajv: 6.14.0
|
||||||
|
debug: 4.4.3
|
||||||
|
espree: 10.4.0
|
||||||
|
globals: 14.0.0
|
||||||
|
ignore: 5.3.2
|
||||||
|
import-fresh: 3.3.1
|
||||||
|
js-yaml: 4.1.1
|
||||||
|
minimatch: 10.2.4
|
||||||
|
strip-json-comments: 3.1.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
'@eslint/js@9.39.2': {}
|
'@eslint/js@9.39.2': {}
|
||||||
|
|
||||||
|
'@eslint/js@9.39.3': {}
|
||||||
|
|
||||||
'@eslint/object-schema@2.1.7': {}
|
'@eslint/object-schema@2.1.7': {}
|
||||||
|
|
||||||
'@eslint/plugin-kit@0.4.1':
|
'@eslint/plugin-kit@0.4.1':
|
||||||
@@ -9750,15 +9827,15 @@ snapshots:
|
|||||||
|
|
||||||
'@medv/finder@4.0.2': {}
|
'@medv/finder@4.0.2': {}
|
||||||
|
|
||||||
'@mintel/eslint-config@1.8.3(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
'@mintel/eslint-config@1.8.21(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint/eslintrc': 3.3.3
|
'@eslint/eslintrc': 3.3.4
|
||||||
'@eslint/js': 9.39.2
|
'@eslint/js': 9.39.3
|
||||||
'@next/eslint-plugin-next': 16.1.6
|
'@next/eslint-plugin-next': 16.1.6
|
||||||
eslint-config-next: 16.1.6(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
eslint-config-next: 16.1.6(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||||
eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1))
|
eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1))
|
||||||
eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1))
|
eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1))
|
||||||
typescript-eslint: 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
typescript-eslint: 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@typescript-eslint/parser'
|
- '@typescript-eslint/parser'
|
||||||
- eslint
|
- eslint
|
||||||
@@ -9767,13 +9844,13 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
'@mintel/mail@1.8.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
'@mintel/mail@1.8.21(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@react-email/components': 0.0.33(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
'@react-email/components': 0.0.33(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
react-dom: 19.2.4(react@19.2.4)
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
|
||||||
'@mintel/next-config@1.8.3(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)(typescript@5.9.3)(webpack@5.105.0(esbuild@0.25.12))':
|
'@mintel/next-config@1.8.21(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)(typescript@5.9.3)(webpack@5.105.0(esbuild@0.25.12))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sentry/nextjs': 10.39.0(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(react@19.2.4)(webpack@5.105.0(esbuild@0.25.12))
|
'@sentry/nextjs': 10.39.0(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(react@19.2.4)(webpack@5.105.0(esbuild@0.25.12))
|
||||||
next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)
|
next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)
|
||||||
@@ -9796,7 +9873,7 @@ snapshots:
|
|||||||
- typescript
|
- typescript
|
||||||
- webpack
|
- webpack
|
||||||
|
|
||||||
'@mintel/next-feedback@1.8.10(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)':
|
'@mintel/next-feedback@1.8.21(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@directus/sdk': 21.1.0
|
'@directus/sdk': 21.1.0
|
||||||
'@medv/finder': 4.0.2
|
'@medv/finder': 4.0.2
|
||||||
@@ -9817,7 +9894,7 @@ snapshots:
|
|||||||
- babel-plugin-react-compiler
|
- babel-plugin-react-compiler
|
||||||
- sass
|
- sass
|
||||||
|
|
||||||
'@mintel/next-utils@1.7.15(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)(typescript@5.9.3)':
|
'@mintel/next-utils@1.8.21(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.18)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@directus/sdk': 21.1.0
|
'@directus/sdk': 21.1.0
|
||||||
next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)
|
next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3)
|
||||||
@@ -9835,7 +9912,7 @@ snapshots:
|
|||||||
- sass
|
- sass
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
'@mintel/tsconfig@1.8.3': {}
|
'@mintel/tsconfig@1.8.21': {}
|
||||||
|
|
||||||
'@monaco-editor/loader@1.7.0':
|
'@monaco-editor/loader@1.7.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -11352,6 +11429,8 @@ snapshots:
|
|||||||
|
|
||||||
'@tootallnate/quickjs-emscripten@0.23.0': {}
|
'@tootallnate/quickjs-emscripten@0.23.0': {}
|
||||||
|
|
||||||
|
'@tweenjs/tween.js@23.1.3': {}
|
||||||
|
|
||||||
'@tybys/wasm-util@0.10.1':
|
'@tybys/wasm-util@0.10.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
@@ -11484,6 +11563,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
|
|
||||||
|
'@types/node@22.19.13':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 6.21.0
|
||||||
|
|
||||||
'@types/nodemailer@7.0.9':
|
'@types/nodemailer@7.0.9':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.19.10
|
'@types/node': 22.19.10
|
||||||
@@ -11532,10 +11615,22 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.19.10
|
'@types/node': 22.19.10
|
||||||
|
|
||||||
|
'@types/stats.js@0.17.4': {}
|
||||||
|
|
||||||
'@types/tedious@4.0.14':
|
'@types/tedious@4.0.14':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.19.10
|
'@types/node': 22.19.10
|
||||||
|
|
||||||
|
'@types/three@0.183.1':
|
||||||
|
dependencies:
|
||||||
|
'@dimforge/rapier3d-compat': 0.12.0
|
||||||
|
'@tweenjs/tween.js': 23.1.3
|
||||||
|
'@types/stats.js': 0.17.4
|
||||||
|
'@types/webxr': 0.5.24
|
||||||
|
'@webgpu/types': 0.1.69
|
||||||
|
fflate: 0.8.2
|
||||||
|
meshoptimizer: 1.0.1
|
||||||
|
|
||||||
'@types/trusted-types@2.0.7':
|
'@types/trusted-types@2.0.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -11547,6 +11642,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/uuid@10.0.0': {}
|
'@types/uuid@10.0.0': {}
|
||||||
|
|
||||||
|
'@types/webxr@0.5.24': {}
|
||||||
|
|
||||||
'@types/whatwg-mimetype@3.0.2': {}
|
'@types/whatwg-mimetype@3.0.2': {}
|
||||||
|
|
||||||
'@types/ws@8.18.1':
|
'@types/ws@8.18.1':
|
||||||
@@ -11558,14 +11655,14 @@ snapshots:
|
|||||||
'@types/node': 22.19.11
|
'@types/node': 22.19.11
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.55.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
'@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.12.2
|
'@eslint-community/regexpp': 4.12.2
|
||||||
'@typescript-eslint/parser': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/parser': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||||
'@typescript-eslint/scope-manager': 8.55.0
|
'@typescript-eslint/scope-manager': 8.56.1
|
||||||
'@typescript-eslint/type-utils': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/type-utils': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||||
'@typescript-eslint/utils': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||||
'@typescript-eslint/visitor-keys': 8.55.0
|
'@typescript-eslint/visitor-keys': 8.56.1
|
||||||
eslint: 9.39.2(jiti@2.6.1)
|
eslint: 9.39.2(jiti@2.6.1)
|
||||||
ignore: 7.0.5
|
ignore: 7.0.5
|
||||||
natural-compare: 1.4.0
|
natural-compare: 1.4.0
|
||||||
@@ -11574,41 +11671,41 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
'@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/scope-manager': 8.55.0
|
'@typescript-eslint/scope-manager': 8.56.1
|
||||||
'@typescript-eslint/types': 8.55.0
|
'@typescript-eslint/types': 8.56.1
|
||||||
'@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
|
||||||
'@typescript-eslint/visitor-keys': 8.55.0
|
'@typescript-eslint/visitor-keys': 8.56.1
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
eslint: 9.39.2(jiti@2.6.1)
|
eslint: 9.39.2(jiti@2.6.1)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/project-service@8.55.0(typescript@5.9.3)':
|
'@typescript-eslint/project-service@8.56.1(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.9.3)
|
'@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3)
|
||||||
'@typescript-eslint/types': 8.55.0
|
'@typescript-eslint/types': 8.56.1
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/scope-manager@8.55.0':
|
'@typescript-eslint/scope-manager@8.56.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 8.55.0
|
'@typescript-eslint/types': 8.56.1
|
||||||
'@typescript-eslint/visitor-keys': 8.55.0
|
'@typescript-eslint/visitor-keys': 8.56.1
|
||||||
|
|
||||||
'@typescript-eslint/tsconfig-utils@8.55.0(typescript@5.9.3)':
|
'@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
'@typescript-eslint/type-utils@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
'@typescript-eslint/type-utils@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 8.55.0
|
'@typescript-eslint/types': 8.56.1
|
||||||
'@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
|
||||||
'@typescript-eslint/utils': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
eslint: 9.39.2(jiti@2.6.1)
|
eslint: 9.39.2(jiti@2.6.1)
|
||||||
ts-api-utils: 2.4.0(typescript@5.9.3)
|
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||||
@@ -11616,16 +11713,16 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/types@8.55.0': {}
|
'@typescript-eslint/types@8.56.1': {}
|
||||||
|
|
||||||
'@typescript-eslint/typescript-estree@8.55.0(typescript@5.9.3)':
|
'@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/project-service': 8.55.0(typescript@5.9.3)
|
'@typescript-eslint/project-service': 8.56.1(typescript@5.9.3)
|
||||||
'@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.9.3)
|
'@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3)
|
||||||
'@typescript-eslint/types': 8.55.0
|
'@typescript-eslint/types': 8.56.1
|
||||||
'@typescript-eslint/visitor-keys': 8.55.0
|
'@typescript-eslint/visitor-keys': 8.56.1
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
minimatch: 10.2.2
|
minimatch: 10.2.4
|
||||||
semver: 7.7.4
|
semver: 7.7.4
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
ts-api-utils: 2.4.0(typescript@5.9.3)
|
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||||
@@ -11633,21 +11730,21 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/utils@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
'@typescript-eslint/utils@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
|
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
|
||||||
'@typescript-eslint/scope-manager': 8.55.0
|
'@typescript-eslint/scope-manager': 8.56.1
|
||||||
'@typescript-eslint/types': 8.55.0
|
'@typescript-eslint/types': 8.56.1
|
||||||
'@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
|
||||||
eslint: 9.39.2(jiti@2.6.1)
|
eslint: 9.39.2(jiti@2.6.1)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/visitor-keys@8.55.0':
|
'@typescript-eslint/visitor-keys@8.56.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 8.55.0
|
'@typescript-eslint/types': 8.56.1
|
||||||
eslint-visitor-keys: 4.2.1
|
eslint-visitor-keys: 5.0.1
|
||||||
|
|
||||||
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
|
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
|
||||||
optional: true
|
optional: true
|
||||||
@@ -11846,6 +11943,8 @@ snapshots:
|
|||||||
'@webassemblyjs/ast': 1.14.1
|
'@webassemblyjs/ast': 1.14.1
|
||||||
'@xtuc/long': 4.2.2
|
'@xtuc/long': 4.2.2
|
||||||
|
|
||||||
|
'@webgpu/types@0.1.69': {}
|
||||||
|
|
||||||
'@xtuc/ieee754@1.2.0': {}
|
'@xtuc/ieee754@1.2.0': {}
|
||||||
|
|
||||||
'@xtuc/long@4.2.2': {}
|
'@xtuc/long@4.2.2': {}
|
||||||
@@ -11913,6 +12012,13 @@ snapshots:
|
|||||||
json-schema-traverse: 0.4.1
|
json-schema-traverse: 0.4.1
|
||||||
uri-js: 4.4.1
|
uri-js: 4.4.1
|
||||||
|
|
||||||
|
ajv@6.14.0:
|
||||||
|
dependencies:
|
||||||
|
fast-deep-equal: 3.1.3
|
||||||
|
fast-json-stable-stringify: 2.1.0
|
||||||
|
json-schema-traverse: 0.4.1
|
||||||
|
uri-js: 4.4.1
|
||||||
|
|
||||||
ajv@8.17.1:
|
ajv@8.17.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-deep-equal: 3.1.3
|
fast-deep-equal: 3.1.3
|
||||||
@@ -12103,6 +12209,8 @@ snapshots:
|
|||||||
|
|
||||||
balanced-match@4.0.3: {}
|
balanced-match@4.0.3: {}
|
||||||
|
|
||||||
|
balanced-match@4.0.4: {}
|
||||||
|
|
||||||
bare-events@2.8.2: {}
|
bare-events@2.8.2: {}
|
||||||
|
|
||||||
bare-fs@4.5.3:
|
bare-fs@4.5.3:
|
||||||
@@ -12191,6 +12299,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
balanced-match: 4.0.3
|
balanced-match: 4.0.3
|
||||||
|
|
||||||
|
brace-expansion@5.0.4:
|
||||||
|
dependencies:
|
||||||
|
balanced-match: 4.0.4
|
||||||
|
|
||||||
braces@3.0.3:
|
braces@3.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
fill-range: 7.1.1
|
fill-range: 7.1.1
|
||||||
@@ -13228,18 +13340,18 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
source-map: 0.6.1
|
source-map: 0.6.1
|
||||||
|
|
||||||
eslint-config-next@16.1.6(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
|
eslint-config-next@16.1.6(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/eslint-plugin-next': 16.1.6
|
'@next/eslint-plugin-next': 16.1.6
|
||||||
eslint: 9.39.2(jiti@2.6.1)
|
eslint: 9.39.2(jiti@2.6.1)
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
|
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
|
||||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
|
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
|
||||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1))
|
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1))
|
||||||
eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1))
|
eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1))
|
||||||
eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1))
|
eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1))
|
||||||
globals: 16.4.0
|
globals: 16.4.0
|
||||||
typescript-eslint: 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
typescript-eslint: 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -13267,22 +13379,22 @@ snapshots:
|
|||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
unrs-resolver: 1.11.1
|
unrs-resolver: 1.11.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
|
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
|
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7
|
debug: 3.2.7
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@typescript-eslint/parser': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/parser': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||||
eslint: 9.39.2(jiti@2.6.1)
|
eslint: 9.39.2(jiti@2.6.1)
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
|
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
|
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rtsao/scc': 1.1.0
|
'@rtsao/scc': 1.1.0
|
||||||
array-includes: 3.1.9
|
array-includes: 3.1.9
|
||||||
@@ -13293,11 +13405,11 @@ snapshots:
|
|||||||
doctrine: 2.1.0
|
doctrine: 2.1.0
|
||||||
eslint: 9.39.2(jiti@2.6.1)
|
eslint: 9.39.2(jiti@2.6.1)
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
|
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
|
||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
is-core-module: 2.16.1
|
is-core-module: 2.16.1
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
minimatch: 10.2.2
|
minimatch: 10.2.4
|
||||||
object.fromentries: 2.0.8
|
object.fromentries: 2.0.8
|
||||||
object.groupby: 1.0.3
|
object.groupby: 1.0.3
|
||||||
object.values: 1.2.1
|
object.values: 1.2.1
|
||||||
@@ -13305,7 +13417,7 @@ snapshots:
|
|||||||
string.prototype.trimend: 1.0.9
|
string.prototype.trimend: 1.0.9
|
||||||
tsconfig-paths: 3.15.0
|
tsconfig-paths: 3.15.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@typescript-eslint/parser': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/parser': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- eslint-import-resolver-typescript
|
- eslint-import-resolver-typescript
|
||||||
- eslint-import-resolver-webpack
|
- eslint-import-resolver-webpack
|
||||||
@@ -13325,7 +13437,7 @@ snapshots:
|
|||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
jsx-ast-utils: 3.3.5
|
jsx-ast-utils: 3.3.5
|
||||||
language-tags: 1.0.9
|
language-tags: 1.0.9
|
||||||
minimatch: 10.2.2
|
minimatch: 10.2.4
|
||||||
object.fromentries: 2.0.8
|
object.fromentries: 2.0.8
|
||||||
safe-regex-test: 1.1.0
|
safe-regex-test: 1.1.0
|
||||||
string.prototype.includes: 2.0.1
|
string.prototype.includes: 2.0.1
|
||||||
@@ -13353,12 +13465,12 @@ snapshots:
|
|||||||
estraverse: 5.3.0
|
estraverse: 5.3.0
|
||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
jsx-ast-utils: 3.3.5
|
jsx-ast-utils: 3.3.5
|
||||||
minimatch: 10.2.2
|
minimatch: 10.2.4
|
||||||
object.entries: 1.1.9
|
object.entries: 1.1.9
|
||||||
object.fromentries: 2.0.8
|
object.fromentries: 2.0.8
|
||||||
object.values: 1.2.1
|
object.values: 1.2.1
|
||||||
prop-types: 15.8.1
|
prop-types: 15.8.1
|
||||||
resolve: 2.0.0-next.5
|
resolve: 2.0.0-next.6
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
string.prototype.matchall: 4.0.12
|
string.prototype.matchall: 4.0.12
|
||||||
string.prototype.repeat: 1.0.0
|
string.prototype.repeat: 1.0.0
|
||||||
@@ -13377,6 +13489,8 @@ snapshots:
|
|||||||
|
|
||||||
eslint-visitor-keys@4.2.1: {}
|
eslint-visitor-keys@4.2.1: {}
|
||||||
|
|
||||||
|
eslint-visitor-keys@5.0.1: {}
|
||||||
|
|
||||||
eslint@9.39.2(jiti@2.6.1):
|
eslint@9.39.2(jiti@2.6.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
|
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
|
||||||
@@ -14366,7 +14480,7 @@ snapshots:
|
|||||||
|
|
||||||
jest-worker@27.5.1:
|
jest-worker@27.5.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.19.11
|
'@types/node': 22.19.13
|
||||||
merge-stream: 2.0.0
|
merge-stream: 2.0.0
|
||||||
supports-color: 8.1.1
|
supports-color: 8.1.1
|
||||||
|
|
||||||
@@ -14836,6 +14950,8 @@ snapshots:
|
|||||||
|
|
||||||
merge2@1.4.1: {}
|
merge2@1.4.1: {}
|
||||||
|
|
||||||
|
meshoptimizer@1.0.1: {}
|
||||||
|
|
||||||
metaviewport-parser@0.3.0: {}
|
metaviewport-parser@0.3.0: {}
|
||||||
|
|
||||||
methods@1.1.2: {}
|
methods@1.1.2: {}
|
||||||
@@ -15038,6 +15154,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 5.0.2
|
brace-expansion: 5.0.2
|
||||||
|
|
||||||
|
minimatch@10.2.4:
|
||||||
|
dependencies:
|
||||||
|
brace-expansion: 5.0.4
|
||||||
|
|
||||||
minimist@1.2.8: {}
|
minimist@1.2.8: {}
|
||||||
|
|
||||||
minipass@7.1.2: {}
|
minipass@7.1.2: {}
|
||||||
@@ -15160,6 +15280,13 @@ snapshots:
|
|||||||
|
|
||||||
node-addon-api@7.1.1: {}
|
node-addon-api@7.1.1: {}
|
||||||
|
|
||||||
|
node-exports-info@1.6.0:
|
||||||
|
dependencies:
|
||||||
|
array.prototype.flatmap: 1.3.3
|
||||||
|
es-errors: 1.3.0
|
||||||
|
object.entries: 1.1.9
|
||||||
|
semver: 6.3.1
|
||||||
|
|
||||||
node-fetch@2.7.0:
|
node-fetch@2.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
whatwg-url: 5.0.0
|
whatwg-url: 5.0.0
|
||||||
@@ -16079,9 +16206,12 @@ snapshots:
|
|||||||
path-parse: 1.0.7
|
path-parse: 1.0.7
|
||||||
supports-preserve-symlinks-flag: 1.0.0
|
supports-preserve-symlinks-flag: 1.0.0
|
||||||
|
|
||||||
resolve@2.0.0-next.5:
|
resolve@2.0.0-next.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
es-errors: 1.3.0
|
||||||
is-core-module: 2.16.1
|
is-core-module: 2.16.1
|
||||||
|
node-exports-info: 1.6.0
|
||||||
|
object-keys: 1.1.1
|
||||||
path-parse: 1.0.7
|
path-parse: 1.0.7
|
||||||
supports-preserve-symlinks-flag: 1.0.0
|
supports-preserve-symlinks-flag: 1.0.0
|
||||||
|
|
||||||
@@ -16940,12 +17070,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-typedarray: 1.0.0
|
is-typedarray: 1.0.0
|
||||||
|
|
||||||
typescript-eslint@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
|
typescript-eslint@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/eslint-plugin': 8.55.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||||
'@typescript-eslint/parser': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/parser': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||||
'@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
|
||||||
'@typescript-eslint/utils': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||||
eslint: 9.39.2(jiti@2.6.1)
|
eslint: 9.39.2(jiti@2.6.1)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
|||||||
119
scripts/check-apis.ts
Normal file
119
scripts/check-apis.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import dns from 'dns';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import url from 'url';
|
||||||
|
|
||||||
|
const resolve4 = promisify(dns.resolve4);
|
||||||
|
|
||||||
|
// This script verifies that external logging and analytics APIs are reachable
|
||||||
|
// from the deployment environment (which could be behind corporate firewalls or VPNs).
|
||||||
|
|
||||||
|
const umamiEndpoint = process.env.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me';
|
||||||
|
const sentryDsn = process.env.SENTRY_DSN || '';
|
||||||
|
|
||||||
|
async function checkUmami() {
|
||||||
|
console.log(`\n🔍 Checking Umami Analytics API Availability...`);
|
||||||
|
console.log(` Endpoint: ${umamiEndpoint}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Umami usually exposes a /api/heartbeat or /api/health if we know the route.
|
||||||
|
// Trying root or /api/auth/verify (which will give 401 but proves routing works).
|
||||||
|
// A simple GET to the configured endpoint should return a 200 or 401, not a 5xx/timeout.
|
||||||
|
const response = await axios.get(`${umamiEndpoint.replace(/\/$/, '')}/api/health`, {
|
||||||
|
timeout: 5000,
|
||||||
|
validateStatus: () => true, // Accept any status, we just want to know it's reachable and not 5xx
|
||||||
|
});
|
||||||
|
|
||||||
|
// As long as it's not a 502/503/504 Bad Gateway/Timeout, the service is "up" from our perspective
|
||||||
|
if (response.status >= 500) {
|
||||||
|
throw new Error(`Umami API responded with server error HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` ✅ Umami Analytics is reachable (HTTP ${response.status})`);
|
||||||
|
return true;
|
||||||
|
} catch (err: any) {
|
||||||
|
// If /api/health fails completely, maybe try a DNS check as a fallback
|
||||||
|
try {
|
||||||
|
console.warn(` ⚠️ HTTP check failed, falling back to DNS resolution...`);
|
||||||
|
const umamiHost = new url.URL(umamiEndpoint).hostname;
|
||||||
|
await resolve4(umamiHost);
|
||||||
|
console.log(` ✅ Umami Analytics DNS resolved successfully (${umamiHost})`);
|
||||||
|
return true;
|
||||||
|
} catch (dnsErr: any) {
|
||||||
|
console.error(
|
||||||
|
` ❌ CRITICAL: Umami Analytics is completely unreachable! ${err.message} | DNS: ${dnsErr.message}`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkSentry() {
|
||||||
|
console.log(`\n🔍 Checking Glitchtip/Sentry Error Tracking Availability...`);
|
||||||
|
|
||||||
|
if (!sentryDsn) {
|
||||||
|
console.log(` ℹ️ No SENTRY_DSN provided in environment. Skipping.`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedDsn = new url.URL(sentryDsn);
|
||||||
|
const host = parsedDsn.hostname;
|
||||||
|
console.log(` Host: ${host}`);
|
||||||
|
|
||||||
|
// We do a DNS lookup to ensure the runner can actually resolve the tracking server
|
||||||
|
const addresses = await resolve4(host);
|
||||||
|
|
||||||
|
if (addresses && addresses.length > 0) {
|
||||||
|
console.log(` ✅ Glitchtip/Sentry domain resolved: ${addresses[0]}`);
|
||||||
|
|
||||||
|
// Optional: Quick TCP/HTTP check to the host root (Glitchtip usually runs on 80/443 root)
|
||||||
|
try {
|
||||||
|
const proto = parsedDsn.protocol || 'https:';
|
||||||
|
await axios.get(`${proto}//${host}/api/0/`, {
|
||||||
|
timeout: 5000,
|
||||||
|
validateStatus: () => true,
|
||||||
|
});
|
||||||
|
console.log(` ✅ Glitchtip/Sentry API root responds to HTTP.`);
|
||||||
|
} catch (ignore) {
|
||||||
|
console.log(
|
||||||
|
` ⚠️ Glitchtip/Sentry HTTP ping failed or timed out, but DNS is valid. Proceeding.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
throw new Error('No IP addresses found for DSN host');
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(
|
||||||
|
` ❌ CRITICAL: Glitchtip/Sentry DSN is invalid or hostname is unresolvable! ${err.message}`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('🚀 Starting External API Connectivity Smoke Test...');
|
||||||
|
|
||||||
|
let hasErrors = false;
|
||||||
|
|
||||||
|
const umamiOk = await checkUmami();
|
||||||
|
if (!umamiOk) hasErrors = true;
|
||||||
|
|
||||||
|
const sentryOk = await checkSentry();
|
||||||
|
if (!sentryOk) hasErrors = true;
|
||||||
|
|
||||||
|
if (hasErrors) {
|
||||||
|
console.error(
|
||||||
|
`\n🚨 POST-DEPLOY CHECK FAILED: One or more critical external APIs are unreachable.`,
|
||||||
|
);
|
||||||
|
console.error(` This might mean the deployment environment lacks outbound internet access, `);
|
||||||
|
console.error(` DNS is misconfigured, or the upstream services are down.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n🎉 SUCCESS: All required external APIs are reachable!`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
170
scripts/check-forms.ts
Normal file
170
scripts/check-forms.ts
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import puppeteer, { HTTPResponse } from 'puppeteer';
|
||||||
|
import axios from 'axios';
|
||||||
|
import * as cheerio from 'cheerio';
|
||||||
|
|
||||||
|
const targetUrl = process.argv[2] || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
|
||||||
|
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'klz2026';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log(`\n🚀 Starting E2E Form Submission Check for: ${targetUrl}`);
|
||||||
|
|
||||||
|
// 1. Fetch Sitemap to discover the contact page and a product page
|
||||||
|
const sitemapUrl = `${targetUrl.replace(/\/$/, '')}/sitemap.xml`;
|
||||||
|
let urls: string[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
|
||||||
|
const response = await axios.get(sitemapUrl, {
|
||||||
|
headers: { Cookie: `klz_gatekeeper_session=${gatekeeperPassword}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
const $ = cheerio.load(response.data, { xmlMode: true });
|
||||||
|
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(/\/$/, '')))
|
||||||
|
.sort();
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`❌ Failed to fetch sitemap: ${err.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const contactUrl = urls.find((u) => u.includes('/de/kontakt'));
|
||||||
|
// Ensure we select an actual product page (depth >= 7: http://host/de/produkte/category/product)
|
||||||
|
const productUrl = urls.find(
|
||||||
|
(u) =>
|
||||||
|
u.includes('/de/produkte/') && new URL(u).pathname.split('/').filter(Boolean).length >= 4,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!contactUrl) {
|
||||||
|
console.error(`❌ Could not find contact page in sitemap. Ensure /de/kontakt exists.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!productUrl) {
|
||||||
|
console.error(
|
||||||
|
`❌ Could not find a product page in sitemap. Form testing requires at least one product page.`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Discovered Contact Page: ${contactUrl}`);
|
||||||
|
console.log(`✅ Discovered Product Page: ${productUrl}`);
|
||||||
|
|
||||||
|
// 2. Launch Headless Browser
|
||||||
|
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'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
// 3. Inject Gatekeeper session bypassing auth screens
|
||||||
|
console.log(`\n🛡️ Injecting Gatekeeper Session...`);
|
||||||
|
await page.setCookie({
|
||||||
|
name: 'klz_gatekeeper_session',
|
||||||
|
value: gatekeeperPassword,
|
||||||
|
domain: new URL(targetUrl).hostname,
|
||||||
|
path: '/',
|
||||||
|
httpOnly: true,
|
||||||
|
secure: targetUrl.startsWith('https://'),
|
||||||
|
});
|
||||||
|
|
||||||
|
let hasErrors = false;
|
||||||
|
|
||||||
|
// 4. Test Contact Form
|
||||||
|
try {
|
||||||
|
console.log(`\n🧪 Testing Contact Form on: ${contactUrl}`);
|
||||||
|
await page.goto(contactUrl, { waitUntil: 'networkidle0', timeout: 30000 });
|
||||||
|
|
||||||
|
// Ensure React has hydrated completely
|
||||||
|
await page.waitForNetworkIdle({ idleTime: 1000, timeout: 15000 }).catch(() => {});
|
||||||
|
|
||||||
|
// Ensure form is visible and interactive
|
||||||
|
try {
|
||||||
|
// Find the form input by name
|
||||||
|
await page.waitForSelector('input[name="name"]', { visible: true, timeout: 15000 });
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to find Contact Form input. Page Title:', await page.title());
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill form fields
|
||||||
|
await page.type('input[name="name"]', 'Automated E2E Test');
|
||||||
|
await page.type('input[name="email"]', 'testing@mintel.me');
|
||||||
|
await page.type(
|
||||||
|
'textarea[name="message"]',
|
||||||
|
'This is an automated test verifying the contact form submission.',
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(` Submitting Contact Form...`);
|
||||||
|
|
||||||
|
// Explicitly click submit and wait for navigation/state-change
|
||||||
|
await Promise.all([
|
||||||
|
page.waitForSelector('[role="alert"][aria-live="polite"]', { timeout: 15000 }),
|
||||||
|
page.click('button[type="submit"]'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log(`✅ Contact Form submitted successfully! (Success state verified)`);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`❌ Contact Form Test Failed: ${err.message}`);
|
||||||
|
hasErrors = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Test Product Quote Form
|
||||||
|
try {
|
||||||
|
console.log(`\n🧪 Testing Product Quote Form on: ${productUrl}`);
|
||||||
|
await page.goto(productUrl, { waitUntil: 'networkidle0', timeout: 30000 });
|
||||||
|
|
||||||
|
// Ensure React has hydrated completely
|
||||||
|
await page.waitForNetworkIdle({ idleTime: 1000, timeout: 15000 }).catch(() => {});
|
||||||
|
|
||||||
|
// The product form uses dynamic IDs, so we select by input type in the specific form context
|
||||||
|
try {
|
||||||
|
await page.waitForSelector('form input[type="email"]', { visible: true, timeout: 15000 });
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to find Product Quote Form input. Page Title:', await page.title());
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In RequestQuoteForm, the email input is type="email" and message is a textarea.
|
||||||
|
await page.type('form input[type="email"]', 'testing@mintel.me');
|
||||||
|
await page.type(
|
||||||
|
'form textarea',
|
||||||
|
'Automated request for product quote via E2E testing framework.',
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(` Submitting Product Quote Form...`);
|
||||||
|
|
||||||
|
// Submit and wait for success state
|
||||||
|
await Promise.all([
|
||||||
|
page.waitForSelector('[role="alert"][aria-live="polite"]', { timeout: 15000 }),
|
||||||
|
page.click('form button[type="submit"]'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log(`✅ Product Quote Form submitted successfully! (Success state verified)`);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`❌ Product Quote Form Test Failed: ${err.message}`);
|
||||||
|
hasErrors = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
// 5. Evaluation
|
||||||
|
if (hasErrors) {
|
||||||
|
console.error(`\n🚨 IMPORTANT: Form E2E checks failed. The CI build is failing.`);
|
||||||
|
process.exit(1);
|
||||||
|
} else {
|
||||||
|
console.log(`\n🎉 SUCCESS: All form submissions arrived and handled correctly!`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -50,7 +50,12 @@ async function main() {
|
|||||||
headers: { Cookie: `klz_gatekeeper_session=${gatekeeperPassword}` },
|
headers: { Cookie: `klz_gatekeeper_session=${gatekeeperPassword}` },
|
||||||
validateStatus: (status) => status < 400,
|
validateStatus: (status) => status < 400,
|
||||||
});
|
});
|
||||||
const filename = `page-${i}.html`;
|
|
||||||
|
// 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`;
|
||||||
|
|
||||||
fs.writeFileSync(path.join(outputDir, filename), res.data);
|
fs.writeFileSync(path.join(outputDir, filename), res.data);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(`❌ HTTP Error fetching ${u}: ${err.message}`);
|
console.error(`❌ HTTP Error fetching ${u}: ${err.message}`);
|
||||||
|
|||||||
@@ -53,12 +53,17 @@ TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
|||||||
REMOTE_DB_USER=""
|
REMOTE_DB_USER=""
|
||||||
REMOTE_DB_NAME=""
|
REMOTE_DB_NAME=""
|
||||||
|
|
||||||
# Migration names to insert after restore (keeps Payload from prompting)
|
# Auto-detect migrations from src/migrations/*.ts (no manual maintenance needed)
|
||||||
MIGRATIONS=(
|
MIGRATIONS=()
|
||||||
"20260223_195005_products_collection:1"
|
BATCH=1
|
||||||
"20260223_195151_remove_sku_unique:2"
|
for migration_file in $(ls src/migrations/*.ts 2>/dev/null | sort); do
|
||||||
"20260225_003500_add_pages_collection:3"
|
name=$(basename "$migration_file" .ts)
|
||||||
)
|
MIGRATIONS+=("$name:$BATCH")
|
||||||
|
((BATCH++))
|
||||||
|
done
|
||||||
|
if [ ${#MIGRATIONS[@]} -eq 0 ]; then
|
||||||
|
echo "⚠️ No migration files found in src/migrations/"
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Resolve target environment ─────────────────────────────────────────────
|
# ── Resolve target environment ─────────────────────────────────────────────
|
||||||
resolve_target() {
|
resolve_target() {
|
||||||
@@ -158,6 +163,29 @@ backup_remote_db() {
|
|||||||
REMOTE_BACKUP_FILE="$file"
|
REMOTE_BACKUP_FILE="$file"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Pre-flight: Verify remote containers exist ─────────────────────────────
|
||||||
|
check_remote_containers() {
|
||||||
|
echo "🔍 Checking $TARGET containers..."
|
||||||
|
local missing=0
|
||||||
|
if ! ssh "$SSH_HOST" "docker ps -q -f name=$REMOTE_DB_CONTAINER" | grep -q .; then
|
||||||
|
echo "❌ Database container '$REMOTE_DB_CONTAINER' not found on $SSH_HOST"
|
||||||
|
echo " → Deploy $TARGET first: git push to trigger pipeline, or run:"
|
||||||
|
echo " ssh $SSH_HOST \"cd $REMOTE_SITE_DIR && docker compose -p $REMOTE_PROJECT --env-file .env.\$TARGET up -d\""
|
||||||
|
missing=1
|
||||||
|
fi
|
||||||
|
if ! ssh "$SSH_HOST" "docker ps -q -f name=$REMOTE_APP_CONTAINER" | grep -q .; then
|
||||||
|
echo "❌ App container '$REMOTE_APP_CONTAINER' not found on $SSH_HOST"
|
||||||
|
missing=1
|
||||||
|
fi
|
||||||
|
if [ $missing -eq 1 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "💡 The $TARGET environment hasn't been deployed yet."
|
||||||
|
echo " Push to the '$TARGET' branch or run the pipeline first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ All $TARGET containers running."
|
||||||
|
}
|
||||||
|
|
||||||
# ── PUSH: local → remote ──────────────────────────────────────────────────
|
# ── PUSH: local → remote ──────────────────────────────────────────────────
|
||||||
do_push() {
|
do_push() {
|
||||||
echo ""
|
echo ""
|
||||||
@@ -171,8 +199,9 @@ do_push() {
|
|||||||
echo ""
|
echo ""
|
||||||
[[ ! $REPLY =~ ^[Yy]$ ]] && { echo "Cancelled."; exit 0; }
|
[[ ! $REPLY =~ ^[Yy]$ ]] && { echo "Cancelled."; exit 0; }
|
||||||
|
|
||||||
# 0. Ensure local DB is running
|
# 0. Ensure local DB is running & remote containers exist
|
||||||
ensure_local_db
|
ensure_local_db
|
||||||
|
check_remote_containers
|
||||||
|
|
||||||
# 1. Safety backup of remote
|
# 1. Safety backup of remote
|
||||||
backup_remote_db
|
backup_remote_db
|
||||||
@@ -226,8 +255,9 @@ do_pull() {
|
|||||||
echo ""
|
echo ""
|
||||||
[[ ! $REPLY =~ ^[Yy]$ ]] && { echo "Cancelled."; exit 0; }
|
[[ ! $REPLY =~ ^[Yy]$ ]] && { echo "Cancelled."; exit 0; }
|
||||||
|
|
||||||
# 0. Ensure local DB is running
|
# 0. Ensure local DB is running & remote containers exist
|
||||||
ensure_local_db
|
ensure_local_db
|
||||||
|
check_remote_containers
|
||||||
|
|
||||||
# 1. Safety backup of local
|
# 1. Safety backup of local
|
||||||
backup_local_db
|
backup_local_db
|
||||||
|
|||||||
Reference in New Issue
Block a user