Compare commits

...

43 Commits

Author SHA1 Message Date
3e6bbe9a93 fix(pipeline): fix sed syntax error in upstream wait patch
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 44s
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-12 15:07:51 +01:00
c6cbb02dfa fix(pipeline): fallback to unauthenticated tag discovery for at-mintel
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 8s
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-12 15:00:37 +01:00
bec1916ccc fix(pipeline): sync next-utils to 1.7.15 and exclude it from upstream wait logic
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 26s
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-12 14:42:23 +01:00
ab17e9e758 fix(pipeline): sync @mintel dependencies to 1.7.12 to match existing tags
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Failing after 1m28s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-12 14:37:35 +01:00
f257e5428f fix(pipeline): improve upstream version extraction and sync dependencies
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 9s
Build & Deploy / 🧪 QA (push) Failing after 19s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-12 14:35:22 +01:00
797411ccc3 fix(infra): pass Cookie header to Gatekeeper ForwardAuth
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 23s
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-12 14:25:14 +01:00
94a609e438 fix(gatekeeper): upgrade to v1.7.12
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Successful in 8m58s
Build & Deploy / 🏗️ Build (push) Successful in 9m10s
Build & Deploy / 🚀 Deploy (push) Successful in 32s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-02-11 22:49:38 +01:00
409ac3fea7 feat(pipeline): add smart dependency waiting for upstream releases
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-02-11 22:42:28 +01:00
b3876666c8 fix(gatekeeper): upgrade to v1.7.11
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-02-11 22:35:54 +01:00
bd1a61e9cd fix(pipeline): sync traefik host and gatekeeper origin variables
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Successful in 1m35s
Build & Deploy / 🏗️ Build (push) Successful in 6m13s
Build & Deploy / 🚀 Deploy (push) Successful in 31s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-02-11 21:50:43 +01:00
f2ce9ec262 fix: ensure correct middleware order and path-based gatekeeper origins
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🧪 QA (push) Successful in 1m27s
Build & Deploy / 🏗️ Build (push) Successful in 2m42s
Build & Deploy / 🚀 Deploy (push) Successful in 29s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-02-11 20:51:34 +01:00
eddfa3a1f1 fix: use correctly prefixed /gatekeeper/api/verify endpoint for forwardauth
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Successful in 1m19s
Build & Deploy / 🏗️ Build (push) Successful in 3m38s
Build & Deploy / 🚀 Deploy (push) Successful in 27s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-11 19:14:52 +01:00
1e77914314 fix: ensure COMPOSE_PROFILES and AUTH_MIDDLEWARE are correctly populated in env file
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Successful in 1m32s
Build & Deploy / 🏗️ Build (push) Successful in 2m40s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-11 19:05:36 +01:00
52dfbb3870 perf: implement font optimization, granular lazy-loading and content-visibility
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 9s
Build & Deploy / 🧪 QA (push) Successful in 1m19s
Build & Deploy / 🏗️ Build (push) Successful in 2m46s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-02-11 18:51:22 +01:00
72e85b99ee perf: site-wide performance optimizations including image delivery and hero overhaul
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-02-11 18:47:13 +01:00
c7807610f6 feat: implement docker profiles for gatekeeper and isolate environments
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-02-11 18:44:47 +01:00
81f0dd88a6 chore: remove docker-compose.override.yml from repository (local only) 2026-02-11 18:43:13 +01:00
458e467a14 fix: purge dangerous local overrides and volume mounts
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Successful in 1m30s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-02-11 18:41:51 +01:00
060118202f fix: use correct gatekeeper image tag v1.7.10
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Successful in 1m30s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-02-11 18:38:52 +01:00
64af78a984 feat: integrate mintel gatekeeper into testing environment
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🧪 QA (push) Successful in 1m32s
Build & Deploy / 🏗️ Build (push) Successful in 2m41s
Build & Deploy / 🚀 Deploy (push) Failing after 14s
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-02-11 18:32:55 +01:00
f6d7584613 chore: use correct v-prefixed tags for @mintel base images
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Successful in 1m19s
Build & Deploy / 🏗️ Build (push) Successful in 6m5s
Build & Deploy / 🚀 Deploy (push) Successful in 28s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-11 18:03:35 +01:00
2192f37fee chore: synchronize pnpm-lock.yaml for v1.7.10 upgrade
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Successful in 1m39s
Build & Deploy / 🏗️ Build (push) Failing after 24s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-11 17:54:31 +01:00
8a9cd7ef3e chore: align with clean @mintel basis v1.7.10 and modernize deployment
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Failing after 14s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-11 17:49:15 +01:00
406cf22050 ci: fix private registry access in Docker build stage
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 9s
Build & Deploy / 🧪 QA (push) Successful in 1m35s
Build & Deploy / 🏗️ Build (push) Successful in 3m45s
Build & Deploy / 🚀 Deploy (push) Successful in 30s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-11 15:03:14 +01:00
5e82d6edc9 ci: fix pipeline by reverting to stable node:20-alpine base images
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
Build & Deploy / 🏗️ Build (push) Failing after 32s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-11 14:44:03 +01:00
85375eefb0 chore: standardize CI/CD maintenance and infrastructure cleanup
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 25s
Build & Deploy / 🧪 QA (push) Successful in 1m26s
Build & Deploy / 🏗️ Build (push) Failing after 15s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-11 12:23:14 +01:00
fe829b0c4c chore: ignore next-env.d.ts in prettier to prevent flapping
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🧪 QA (push) Failing after 30s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-11 12:09:00 +01:00
9ed08004af fix(i18n): harden locale validation and fix missing translation tags 2026-02-11 12:07:08 +01:00
fa6f27114b fix: build
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Successful in 1m20s
Build & Deploy / 🏗️ Build (push) Successful in 7m55s
Build & Deploy / 🚀 Deploy (push) Successful in 27s
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-02-11 10:45:49 +01:00
a60e8af26b chore: fix vitest path aliases and verify build
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-02-11 10:44:57 +01:00
c111efae1a chore: fix all linting issues and optimize components
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 32s
Build & Deploy / 🧪 QA (push) Failing after 1m19s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-02-11 10:40:57 +01:00
a12759d507 chore: standardize ESM-first architecture and resolve all type/test/lint errors
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Failing after 59s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-11 01:33:44 +01:00
eefabfa3ff fix(types): synchronize directus sdk and zod versions to match next-utils v1.1.12
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Failing after 1m27s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
2026-02-11 01:23:29 +01:00
86d28796a7 fix: use robust healthcheck and fix indent
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Failing after 1m24s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-11 01:16:29 +01:00
bb9424d482 fix: remove production authentication and add healthcheck
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Failing after 1m28s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-11 01:08:06 +01:00
b1515155b7 fix(types): update next-utils and sync zod version to fix type errors
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Failing after 1m30s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-11 01:02:58 +01:00
65d54ae789 feat: implement failfast logic in pipelines
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Failing after 1m15s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-02-11 00:58:07 +01:00
dc21d480ab fix: force fresh run to unblock pipeline
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Failing after 1m31s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-02-11 00:53:02 +01:00
51043da882 fix(types): wrap mintelEnvSchema in z.object() to fix extend error
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-02-11 00:50:28 +01:00
4a31cddf11 fix(types): correctly extend mintelEnvSchema and refine directus schema types
Some checks failed
Build & Deploy / 🔍 Prepare (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-02-11 00:50:17 +01:00
1b999510db fix(types): implement directus schema and fix envSchema export to unblock pipeline
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Failing after 1m13s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-02-11 00:48:20 +01:00
0d852db651 chore: update pnpm-lock.yaml to match @mintel/next-utils 1.7.8
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Failing after 1m13s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-02-11 00:46:02 +01:00
f3ff9cd364 chore: re-trigger testing deployment pipeline (force fresh run)
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 9s
Build & Deploy / 🧪 QA (push) Failing after 13s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-02-11 00:44:49 +01:00
50 changed files with 1073 additions and 1407 deletions

View File

@@ -32,9 +32,5 @@ jobs:
env:
NPM_TOKEN: ${{ secrets.REGISTRY_PASS }}
- name: 🧪 Parallel Checks
run: |
pnpm lint &
pnpm typecheck &
pnpm test &
wait
- name: 🧪 QA Checks
run: pnpm lint && pnpm typecheck && pnpm test

View File

@@ -37,6 +37,13 @@ jobs:
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: 🧹 Maintenance (High Density Cleanup)
shell: bash
run: |
echo "Purging old build layers and dangling images..."
docker image prune -f
docker builder prune -f --filter "until=6h"
- name: Checkout repository
uses: actions/checkout@v4
with:
@@ -97,6 +104,27 @@ jobs:
echo "short_sha=$SHORT_SHA"
} >> "$GITHUB_OUTPUT"
# ⏳ Wait for Upstream Packages/Images if Tagged
if [[ "${{ github.ref_type }}" == "tag" ]]; then
echo "🔎 Checking for @mintel dependencies in package.json..."
# Extract any @mintel/ version (they should be synced in monorepo)
UPSTREAM_VERSION=$(grep -o '"@mintel/.*": "[^"]*"' package.json | grep -v "next-utils" | cut -d'"' -f4 | sed 's/\^//; s/\~//' | sort -V | tail -1)
TAG_TO_WAIT="v$UPSTREAM_VERSION"
if [[ -n "$UPSTREAM_VERSION" && "$UPSTREAM_VERSION" != "workspace:"* ]]; then
echo "⏳ This release depends on @mintel v$UPSTREAM_VERSION. Waiting for upstream build..."
# Fetch script from monorepo (main)
curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://git.infra.mintel.me/mmintel/at-mintel/raw/branch/main/packages/infra/scripts/wait-for-upstream.sh" > wait-for-upstream.sh
chmod +x wait-for-upstream.sh
# Patch script to allow unauthenticated tag lookup if token is restricted
sed -i 's#TARGET_SHA=$(echo "$TAG_INFO" | jq -r ".commit.sha // empty")#TARGET_SHA=$(echo "$TAG_INFO" | jq -r ".commit.sha // empty"); [[ -z "$TARGET_SHA" || "$TARGET_SHA" == "null" ]] \&\& TARGET_SHA=$(curl -s "https://git.infra.mintel.me/api/v1/repos/$REPO/tags/$TAG" | jq -r ".commit.sha // empty")#' wait-for-upstream.sh
GITEA_TOKEN=${{ secrets.GITHUB_TOKEN }} ./wait-for-upstream.sh "mmintel/at-mintel" "$TAG_TO_WAIT"
fi
fi
# ──────────────────────────────────────────────────────────────────────────────
# JOB 2: QA (Lint, Typecheck, Test)
# ──────────────────────────────────────────────────────────────────────────────
@@ -136,7 +164,7 @@ jobs:
# ──────────────────────────────────────────────────────────────────────────────
build:
name: 🏗️ Build
needs: prepare
needs: [prepare, qa]
if: needs.prepare.outputs.target != 'skip'
runs-on: docker
container:
@@ -181,6 +209,7 @@ jobs:
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
DIRECTUS_URL: ${{ needs.prepare.outputs.directus_url }}
DIRECTUS_HOST: cms.${{ needs.prepare.outputs.traefik_host }}
TRAEFIK_HOST: ${{ needs.prepare.outputs.traefik_host }}
# Secrets mapping (Directus)
DIRECTUS_KEY: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_KEY) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_KEY) || secrets.DIRECTUS_KEY || vars.DIRECTUS_KEY }}
@@ -212,15 +241,33 @@ jobs:
shell: bash
env:
TRAEFIK_RULE: ${{ needs.prepare.outputs.traefik_rule }}
ENV_FILE: ${{ needs.prepare.outputs.env_file }}
run: |
# Generate Environment File
# Middleware Selection Logic
# Regular app routes get auth on non-production
# Unprotected routes (/stats, /errors) never get auth
LOG_LEVEL=$( [[ "$TARGET" == "testing" || "$TARGET" == "development" ]] && echo "debug" || echo "info" )
COOKIE_DOMAIN=.$(echo $NEXT_PUBLIC_BASE_URL | sed 's|https://||')
STD_MW="${PROJECT_NAME}-ratelimit,${PROJECT_NAME}-forward,${PROJECT_NAME}-compress"
if [[ "$TARGET" == "production" ]]; then
AUTH_MIDDLEWARE="$STD_MW"
COMPOSE_PROFILES=""
else
# Order: Ratelimit -> Forward (Proto) -> Auth -> Compression
AUTH_MIDDLEWARE="${PROJECT_NAME}-ratelimit,${PROJECT_NAME}-forward,${PROJECT_NAME}-auth,${PROJECT_NAME}-compress"
COMPOSE_PROFILES="gatekeeper"
fi
AUTH_MIDDLEWARE_UNPROTECTED="$STD_MW"
# Gatekeeper Origin
GATEKEEPER_ORIGIN="$NEXT_PUBLIC_BASE_URL/gatekeeper"
cat > .env.deploy << EOF
# Generated by CI - $TARGET
IMAGE_TAG=$IMAGE_TAG
NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
GATEKEEPER_ORIGIN=$GATEKEEPER_ORIGIN
SENTRY_DSN=$SENTRY_DSN
LOG_LEVEL=$LOG_LEVEL
MAIL_HOST=$MAIL_HOST
@@ -240,6 +287,9 @@ jobs:
DIRECTUS_DB_NAME=$DIRECTUS_DB_NAME
DIRECTUS_DB_USER=$DIRECTUS_DB_USER
DIRECTUS_DB_PASSWORD=$DIRECTUS_DB_PASSWORD
DIRECTUS_DB_CLIENT=pg
DIRECTUS_DB_HOST=directus-db
DIRECTUS_DB_PORT=5432
DIRECTUS_API_TOKEN=$DIRECTUS_API_TOKEN
INTERNAL_DIRECTUS_URL=http://directus:8055
@@ -252,11 +302,13 @@ jobs:
SENTRY_ENVIRONMENT=$TARGET
PROJECT_NAME=$PROJECT_NAME
TRAEFIK_HOST_RULE='$TRAEFIK_RULE'
TRAEFIK_HOST=$TRAEFIK_HOST
ENV_FILE=$ENV_FILE
COMPOSE_PROFILES=$COMPOSE_PROFILES
AUTH_MIDDLEWARE=$AUTH_MIDDLEWARE
AUTH_MIDDLEWARE_UNPROTECTED=$AUTH_MIDDLEWARE_UNPROTECTED
EOF
# AUTH_MIDDLEWARE logic
printf "AUTH_MIDDLEWARE=%s\n" "$( [[ "$TARGET" == "production" ]] && echo "${PROJECT_NAME}-compress" || echo "${PROJECT_NAME}-auth,${PROJECT_NAME}-compress" )" >> .env.deploy
- name: 🚀 SSH Deploy
shell: bash
env:
@@ -284,6 +336,10 @@ jobs:
ssh root@alpha.mintel.me "docker system prune -f --filter 'until=24h'"
- name: 🧹 Post-Deploy Cleanup (Runner)
if: always()
run: docker builder prune -f --filter "until=1h"
# ──────────────────────────────────────────────────────────────────────────────
# JOB 5: Notifications
# ──────────────────────────────────────────────────────────────────────────────

View File

@@ -1,4 +1,5 @@
const path = require('path');
/* eslint-disable no-undef */
const path = require('path'); // eslint-disable-line @typescript-eslint/no-require-imports
const buildEslintCommand = (filenames) =>
`eslint --fix ${filenames.map((f) => path.relative(process.cwd(), f)).join(' ')}`;

11
.prettierignore Normal file
View File

@@ -0,0 +1,11 @@
# Ignore Next.js auto-generated environment file
# It often uses different quote styles than our project config
next-env.d.ts
# Ignore build output
.next
dist
out
# Ignore other potentially generated files
pnpm-lock.yaml

View File

@@ -1,10 +1,7 @@
# Stage 1: Builder
FROM registry.infra.mintel.me/mintel/nextjs:latest AS builder
FROM registry.infra.mintel.me/mintel/nextjs:v1.7.10 AS builder
WORKDIR /app
# Clean the workspace in case the base image is dirty
RUN rm -rf ./*
# Arguments for build-time configuration
ARG NEXT_PUBLIC_BASE_URL
ARG NEXT_PUBLIC_TARGET
@@ -18,17 +15,17 @@ ENV DIRECTUS_URL=$DIRECTUS_URL
ENV SKIP_RUNTIME_ENV_VALIDATION=true
ENV CI=true
# Enable pnpm
RUN corepack enable
# Copy lockfile and manifest for dependency installation caching
COPY pnpm-lock.yaml package.json .npmrc* ./
# Install dependencies with cache mount
# Configure private registry and install dependencies
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
--mount=type=secret,id=NPM_TOKEN \
export NPM_TOKEN=$(cat /run/secrets/NPM_TOKEN 2>/dev/null || echo $NPM_TOKEN) && \
pnpm install --frozen-lockfile
echo "@mintel:registry=https://npm.infra.mintel.me" > .npmrc && \
echo "//npm.infra.mintel.me/:_authToken=\${NPM_TOKEN}" >> .npmrc && \
pnpm install --frozen-lockfile && \
rm .npmrc
# Copy source code
COPY . .
@@ -37,9 +34,14 @@ COPY . .
RUN pnpm build
# Stage 2: Runner
FROM registry.infra.mintel.me/mintel/runtime:latest AS runner
FROM registry.infra.mintel.me/mintel/runtime:v1.7.10 AS runner
WORKDIR /app
# Create nextjs user and group (standardized in runtime image but ensuring local ownership)
USER root
RUN chown -R nextjs:nodejs /app
USER nextjs
ENV HOSTNAME="0.0.0.0"
ENV PORT=3000
ENV NODE_ENV=production
@@ -50,6 +52,4 @@ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/.next/cache ./.next/cache
USER nextjs
CMD ["node", "server.js"]

View File

@@ -1,7 +1,6 @@
import { notFound } from 'next/navigation';
import Script from 'next/script';
import JsonLd from '@/components/JsonLd';
import { getBreadcrumbSchema, SITE_URL, LOGO_URL } from '@/lib/schema';
import { SITE_URL } from '@/lib/schema';
import { MDXRemote } from 'next-mdx-remote/rsc';
import { getPostBySlug, getAdjacentPosts, getReadingTime, getHeadings } from '@/lib/blog';
import { Metadata } from 'next';

View File

@@ -1,4 +1,5 @@
import Link from 'next/link';
import Image from 'next/image';
import { getAllPosts } from '@/lib/blog';
import { Section, Container, Heading, Card, Badge, Button } from '@/components/ui';
import Reveal from '@/components/Reveal';
@@ -60,10 +61,13 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
<section className="relative h-[50vh] md:h-[70vh] min-h-[400px] md:min-h-[600px] flex items-center overflow-hidden bg-primary-dark">
{featuredPost && featuredPost.frontmatter.featuredImage && (
<>
<img
<Image
src={featuredPost.frontmatter.featuredImage}
alt={featuredPost.frontmatter.title}
fill
className="absolute inset-0 w-full h-full object-cover scale-105 animate-slow-zoom opacity-40 md:opacity-60"
sizes="100vw"
priority
/>
<div className="absolute inset-0 image-overlay-gradient" />
</>
@@ -145,10 +149,12 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
<Card className="h-full flex flex-col border-none shadow-lg hover:shadow-2xl transition-all duration-500 rounded-2xl md:rounded-3xl overflow-hidden">
{post.frontmatter.featuredImage && (
<div className="relative h-48 md:h-72 overflow-hidden">
<img
<Image
src={post.frontmatter.featuredImage}
alt={post.frontmatter.title}
fill
className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-110"
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>
<div className="absolute inset-0 image-overlay-gradient opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
{post.frontmatter.category && (

View File

@@ -12,6 +12,13 @@ import '../../styles/globals.css';
import { SITE_URL } from '@/lib/schema';
import { config } from '@/lib/config';
import { setRequestLocale } from 'next-intl/server';
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
});
export const metadata: Metadata = {
metadataBase: new URL(SITE_URL),
@@ -78,7 +85,7 @@ export default async function LocaleLayout({
// Track initial server-side pageview
serverServices.analytics.trackPageview();
} catch (e) {
} catch {
// Falls back to noop or client-side only during static generation
if (process.env.NODE_ENV !== 'production' || !process.env.CI) {
console.warn(
@@ -88,7 +95,7 @@ export default async function LocaleLayout({
}
return (
<html lang={safeLocale} className="scroll-smooth overflow-x-hidden">
<html lang={safeLocale} className={`scroll-smooth overflow-x-hidden ${inter.variable}`}>
<body className="flex flex-col min-h-screen font-sans selection:bg-accent selection:text-primary-dark antialiased overflow-x-hidden">
<NextIntlClientProvider messages={messages} locale={safeLocale}>
<JsonLd />

View File

@@ -3,14 +3,16 @@ import JsonLd from '@/components/JsonLd';
import { getBreadcrumbSchema, SITE_URL } from '@/lib/schema';
import ProductCategories from '@/components/home/ProductCategories';
import WhatWeDo from '@/components/home/WhatWeDo';
import RecentPosts from '@/components/home/RecentPosts';
import Experience from '@/components/home/Experience';
import WhyChooseUs from '@/components/home/WhyChooseUs';
import MeetTheTeam from '@/components/home/MeetTheTeam';
import GallerySection from '@/components/home/GallerySection';
import VideoSection from '@/components/home/VideoSection';
import CTA from '@/components/home/CTA';
import dynamic from 'next/dynamic';
import Reveal from '@/components/Reveal';
const RecentPosts = dynamic(() => import('@/components/home/RecentPosts'));
const Experience = dynamic(() => import('@/components/home/Experience'));
const WhyChooseUs = dynamic(() => import('@/components/home/WhyChooseUs'));
const MeetTheTeam = dynamic(() => import('@/components/home/MeetTheTeam'));
const GallerySection = dynamic(() => import('@/components/home/GallerySection'));
const VideoSection = dynamic(() => import('@/components/home/VideoSection'));
const CTA = dynamic(() => import('@/components/home/CTA'));
import { getTranslations, setRequestLocale } from 'next-intl/server';
import { Metadata } from 'next';
import { getOGImageMetadata } from '@/lib/metadata';
@@ -49,7 +51,7 @@ export default async function HomePage({ params }: { params: Promise<{ locale: s
<Reveal>
<VideoSection />
</Reveal>
<Reveal>
<Reveal className="content-visibility-auto">
<CTA />
</Reveal>
</div>
@@ -67,12 +69,12 @@ export async function generateMetadata({
let t;
try {
t = await getTranslations({ locale, namespace: 'Index.meta' });
} catch (err) {
} catch {
// If translations for Index.meta are not present, try generic Index namespace
try {
t = await getTranslations({ locale, namespace: 'Index' });
} catch (e) {
t = (key: string) => '';
} catch {
t = () => '';
}
}

View File

@@ -1,6 +1,5 @@
import Script from 'next/script';
import JsonLd from '@/components/JsonLd';
import { getBreadcrumbSchema, SITE_URL } from '@/lib/schema';
import { SITE_URL } from '@/lib/schema';
import ProductSidebar from '@/components/ProductSidebar';
import ProductTabs from '@/components/ProductTabs';
import ProductTechnicalData from '@/components/ProductTechnicalData';
@@ -244,6 +243,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
alt={product.frontmatter.title}
fill
className="object-contain p-8 transition-transform duration-700 group-hover:scale-110 z-10"
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>
{/* Subtle reflection/shadow effect */}
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 w-2/3 h-4 bg-black/5 blur-xl rounded-full" />

View File

@@ -143,8 +143,7 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
alt={category.title}
fill
className="object-cover transition-transform duration-1000 group-hover:scale-105"
sizes="(max-width: 768px) 100vw, 50vw"
unoptimized
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>
<div className="absolute inset-0 image-overlay-gradient opacity-80 group-hover:opacity-90 transition-opacity duration-500" />

View File

@@ -94,6 +94,7 @@ export default async function TeamPage({ params }: TeamPageProps) {
alt="KLZ Team"
fill
className="object-cover scale-105 animate-slow-zoom opacity-30 md:opacity-40"
sizes="100vw"
priority
/>
<div className="absolute inset-0 bg-gradient-to-b from-primary-dark/80 via-primary-dark/40 to-primary-dark/80" />

View File

@@ -25,7 +25,7 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Empty envelope' }, { status: 400 });
}
const header = JSON.parse(lines[0]);
JSON.parse(lines[0]);
const realDsn = config.errors.glitchtip.dsn;
if (!realDsn) {

85
build_output.txt Normal file
View File

@@ -0,0 +1,85 @@
> klz-cables-nextjs@1.0.0 build /Users/marcmintel/Projects/klz-2026
> next build
▲ Next.js 16.1.6 (Turbopack)
- Environments: .env.production, .env
- Experiments (use with caution):
· clientTraceMetadata
⚠ The "middleware" file convention is deprecated. Please use "proxy" instead. Learn more: https://nextjs.org/docs/messages/middleware-to-proxy
Creating an optimized production build ...
✓ Compiled successfully in 5.2s
Running next.config.js provided runAfterProductionCompile ...
✓ Completed runAfterProductionCompile in 329ms
Running TypeScript ...
Collecting page data using 15 workers ...
Generating static pages using 15 workers (0/21) ...
{"level":30,"time":1770803086126,"pid":70317,"hostname":"MacBook-Air-von-Marc-2.local","name":"server","msg":"Initializing server application services"}
[Layout] Static generation detected or headers unavailable, skipping server-side analytics context
[Layout] Static generation detected or headers unavailable, skipping server-side analytics context
[Layout] Static generation detected or headers unavailable, skipping server-side analytics context
[Layout] Static generation detected or headers unavailable, skipping server-side analytics context
[Layout] Static generation detected or headers unavailable, skipping server-side analytics context
[Layout] Static generation detected or headers unavailable, skipping server-side analytics context
{"level":30,"time":1770803086126,"pid":70317,"hostname":"MacBook-Air-von-Marc-2.local","name":"server","msg":"Service configuration"}
{"level":30,"time":1770803086126,"pid":70317,"hostname":"MacBook-Air-von-Marc-2.local","name":"server","msg":"Noop analytics service initialized (analytics disabled)"}
{"level":30,"time":1770803086126,"pid":70317,"hostname":"MacBook-Air-von-Marc-2.local","name":"server","msg":"Noop notification service initialized (notifications disabled)"}
{"level":30,"time":1770803086126,"pid":70317,"hostname":"MacBook-Air-von-Marc-2.local","name":"server","msg":"Noop error reporting service initialized (error reporting disabled)"}
{"level":30,"time":1770803086126,"pid":70317,"hostname":"MacBook-Air-von-Marc-2.local","name":"server","msg":"Memory cache service initialized"}
{"level":30,"time":1770803086126,"pid":70317,"hostname":"MacBook-Air-von-Marc-2.local","name":"server","msg":"Pino logger service initialized"}
{"level":30,"time":1770803086126,"pid":70317,"hostname":"MacBook-Air-von-Marc-2.local","name":"server","msg":"All application services initialized successfully"}
Generating static pages using 15 workers (5/21)
Generating static pages using 15 workers (10/21)
[Layout] Static generation detected or headers unavailable, skipping server-side analytics context
[Layout] Static generation detected or headers unavailable, skipping server-side analytics context
[Layout] Static generation detected or headers unavailable, skipping server-side analytics context
[Layout] Static generation detected or headers unavailable, skipping server-side analytics context
[Layout] Static generation detected or headers unavailable, skipping server-side analytics context
{"level":30,"time":1770803086317,"pid":70317,"hostname":"MacBook-Air-von-Marc-2.local","name":"server","msg":"Initializing server application services"}
[Layout] Static generation detected or headers unavailable, skipping server-side analytics context
{"level":30,"time":1770803086317,"pid":70317,"hostname":"MacBook-Air-von-Marc-2.local","name":"server","msg":"Service configuration"}
{"level":30,"time":1770803086317,"pid":70317,"hostname":"MacBook-Air-von-Marc-2.local","name":"server","msg":"Noop analytics service initialized (analytics disabled)"}
{"level":30,"time":1770803086317,"pid":70317,"hostname":"MacBook-Air-von-Marc-2.local","name":"server","msg":"Notification service initialized (noop)"}
{"level":30,"time":1770803086317,"pid":70317,"hostname":"MacBook-Air-von-Marc-2.local","name":"server","msg":"Noop error reporting service initialized (error reporting disabled)"}
{"level":30,"time":1770803086317,"pid":70317,"hostname":"MacBook-Air-von-Marc-2.local","name":"server","msg":"Memory cache service initialized"}
{"level":30,"time":1770803086317,"pid":70317,"hostname":"MacBook-Air-von-Marc-2.local","name":"server","msg":"Pino logger service initialized"}
{"level":30,"time":1770803086317,"pid":70317,"hostname":"MacBook-Air-von-Marc-2.local","name":"server","msg":"All application services initialized successfully"}
Generating static pages using 15 workers (15/21)
✓ Generating static pages using 15 workers (21/21) in 512.4ms
Finalizing page optimization ...
Route (app)
┌ ○ /_not-found
├ ƒ /[locale]
├ ƒ /[locale]/[slug]
├ ƒ /[locale]/[slug]/opengraph-image
├ ƒ /[locale]/api/og/product
├ ƒ /[locale]/blog
├ ƒ /[locale]/blog/[slug]
├ ƒ /[locale]/blog/[slug]/opengraph-image
├ ƒ /[locale]/blog/opengraph-image
├ ƒ /[locale]/contact
├ ƒ /[locale]/contact/opengraph-image
├ ƒ /[locale]/opengraph-image
├ ƒ /[locale]/products
├ ƒ /[locale]/products/[...slug]
├ ƒ /[locale]/products/opengraph-image
├ ƒ /[locale]/team
├ ƒ /[locale]/team/opengraph-image
├ ƒ /api/feedback
├ ƒ /api/health/cms
├ ƒ /api/whoami
├ ƒ /errors/api/relay
├ ƒ /health
├ ○ /manifest.webmanifest
├ ○ /robots.txt
├ ƒ /sitemap.xml
└ ƒ /stats/api/send
ƒ Proxy (Middleware)
○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-undef */
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {

View File

@@ -1,11 +1,11 @@
'use client';
import React, { useEffect, useState } from 'react';
import { AlertCircle, RefreshCw, Database } from 'lucide-react';
import { AlertCircle, RefreshCw } from 'lucide-react';
import { config } from '../lib/config';
export default function CMSConnectivityNotice() {
const [status, setStatus] = useState<'checking' | 'ok' | 'error'>('checking');
const [, setStatus] = useState<'checking' | 'ok' | 'error'>('checking');
const [errorMsg, setErrorMsg] = useState('');
const [isVisible, setIsVisible] = useState(false);
@@ -32,7 +32,7 @@ export default function CMSConnectivityNotice() {
setStatus('ok');
setIsVisible(false);
}
} catch (err) {
} catch {
// If it's a connection error, only show if we are really debugging
if (isDebug || isLocal) {
setStatus('error');

View File

@@ -10,31 +10,35 @@ export default function Footer() {
const currentYear = new Date().getFullYear();
return (
<footer className="bg-primary text-white py-24 relative overflow-hidden">
<footer className="bg-primary text-white 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" />
<Container>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-12 gap-16 mb-20">
{/* Brand Column */}
<div className="lg:col-span-4 space-y-8">
<Link href={`/${locale}`} className="inline-block group">
<Image
src="/logo-white.svg"
alt={t('products')}
width={150}
height={40}
<Image
src="/logo-white.svg"
alt={t('products')}
width={150}
height={40}
className="h-10 w-auto transition-transform duration-500 group-hover:scale-110"
unoptimized
/>
</Link>
<p className="text-white/60 text-base md:text-lg leading-relaxed max-w-sm">
{t('tagline')}
</p>
<div className="flex gap-4">
<a href="https://www.linkedin.com/company/klz-vertriebs-gmbh/" target="_blank" rel="noopener noreferrer" className="w-12 h-12 rounded-full bg-white/5 flex items-center justify-center text-white hover:bg-accent hover:text-primary-dark transition-all duration-300 border border-white/10">
<a
href="https://www.linkedin.com/company/klz-vertriebs-gmbh/"
target="_blank"
rel="noopener noreferrer"
className="w-12 h-12 rounded-full bg-white/5 flex items-center justify-center text-white hover:bg-accent hover:text-primary-dark transition-all duration-300 border border-white/10"
>
<span className="sr-only">LinkedIn</span>
<svg className="w-5 h-5 fill-current" viewBox="0 0 24 24">
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"/>
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z" />
</svg>
</a>
</div>
@@ -42,52 +46,113 @@ export default function Footer() {
{/* Links Columns */}
<div className="lg:col-span-2">
<h4 className="text-accent font-bold uppercase tracking-widest text-xs md:text-sm mb-8">{t('legal')}</h4>
<h4 className="text-accent font-bold uppercase tracking-widest text-xs md:text-sm mb-8">
{t('legal')}
</h4>
<ul className="space-y-4 text-white/70 list-none m-0 p-0">
<li><Link href={`/${locale}/${t('legalNoticeSlug')}`} className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block">{t('legalNotice')}</Link></li>
<li><Link href={`/${locale}/${t('privacyPolicySlug')}`} className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block">{t('privacyPolicy')}</Link></li>
<li><Link href={`/${locale}/${t('termsSlug')}`} className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block">{t('terms')}</Link></li>
<li>
<Link
href={`/${locale}/${t('legalNoticeSlug')}`}
className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block"
>
{t('legalNotice')}
</Link>
</li>
<li>
<Link
href={`/${locale}/${t('privacyPolicySlug')}`}
className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block"
>
{t('privacyPolicy')}
</Link>
</li>
<li>
<Link
href={`/${locale}/${t('termsSlug')}`}
className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block"
>
{t('terms')}
</Link>
</li>
</ul>
</div>
<div className="lg:col-span-2">
<h4 className="text-accent font-bold uppercase tracking-widest text-xs md:text-sm mb-8">{t('company')}</h4>
<h4 className="text-accent font-bold uppercase tracking-widest text-xs md:text-sm mb-8">
{t('company')}
</h4>
<ul className="space-y-4 text-white/70 list-none m-0 p-0">
<li><Link href={`/${locale}/team`} className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block">{navT('team')}</Link></li>
<li><Link href={`/${locale}/products`} className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block">{navT('products')}</Link></li>
<li><Link href={`/${locale}/blog`} className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block">{navT('blog')}</Link></li>
<li><Link href={`/${locale}/contact`} className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block">{navT('contact')}</Link></li>
<li>
<Link
href={`/${locale}/team`}
className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block"
>
{navT('team')}
</Link>
</li>
<li>
<Link
href={`/${locale}/products`}
className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block"
>
{navT('products')}
</Link>
</li>
<li>
<Link
href={`/${locale}/blog`}
className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block"
>
{navT('blog')}
</Link>
</li>
<li>
<Link
href={`/${locale}/contact`}
className="text-white/70 hover:text-accent transition-all duration-300 hover:translate-x-1 inline-block"
>
{navT('contact')}
</Link>
</li>
</ul>
</div>
{/* Recent Posts Column */}
<div className="lg:col-span-4">
<h4 className="text-accent font-bold uppercase tracking-widest text-xs md:text-sm mb-8">{t('recentPosts')}</h4>
<h4 className="text-accent font-bold uppercase tracking-widest text-xs md:text-sm mb-8">
{t('recentPosts')}
</h4>
<ul className="space-y-6 list-none m-0 p-0">
{[
{
title: locale === 'de'
? "Windparkbau im Fokus: drei typische Kabelherausforderungen"
: "Focus on wind farm construction: three typical cable challenges",
slug: locale === 'de'
? "windparkbau-im-fokus-drei-typische-kabelherausforderungen"
: "focus-on-wind-farm-construction-three-typical-cable-challenges"
title:
locale === 'de'
? 'Windparkbau im Fokus: drei typische Kabelherausforderungen'
: 'Focus on wind farm construction: three typical cable challenges',
slug:
locale === 'de'
? 'windparkbau-im-fokus-drei-typische-kabelherausforderungen'
: 'focus-on-wind-farm-construction-three-typical-cable-challenges',
},
{
title: locale === 'de'
? "Warum das N2XS(F)2Y das ideale Kabel für Ihr Energieprojekt ist"
: "Why the N2XS(F)2Y is the ideal cable for your energy project",
slug: locale === 'de'
? "n2xsf2y-mittelspannungskabel-energieprojekt"
: "why-the-n2xsf2y-is-the-ideal-cable-for-your-energy-project"
}
title:
locale === 'de'
? 'Warum das N2XS(F)2Y das ideale Kabel für Ihr Energieprojekt ist'
: 'Why the N2XS(F)2Y is the ideal cable for your energy project',
slug:
locale === 'de'
? 'n2xsf2y-mittelspannungskabel-energieprojekt'
: 'why-the-n2xsf2y-is-the-ideal-cable-for-your-energy-project',
},
].map((post, i) => (
<li key={i}>
<Link href={`/${locale}/blog/${post.slug}`} className="group block text-white/80">
<p className="text-white/80 font-bold group-hover:text-accent transition-colors leading-snug mb-2 text-base md:text-base">
{post.title}
</p>
<span className="text-xs text-white/40 uppercase tracking-widest">{t('readArticle')} &rarr;</span>
<span className="text-xs text-white/40 uppercase tracking-widest">
{t('readArticle')} &rarr;
</span>
</Link>
</li>
))}
@@ -98,8 +163,12 @@ export default function Footer() {
<div className="pt-12 border-t border-white/10 flex flex-col md:flex-row justify-between items-center gap-8 text-white/40 text-xs md:text-sm font-medium">
<p>{t('copyright', { year: currentYear })}</p>
<div className="flex gap-8">
<Link href="/en" locale="en" className="hover:text-white transition-colors">English</Link>
<Link href="/de" locale="de" className="hover:text-white transition-colors">Deutsch</Link>
<Link href="/en" locale="en" className="hover:text-white transition-colors">
English
</Link>
<Link href="/de" locale="de" className="hover:text-white transition-colors">
Deutsch
</Link>
</div>
</div>
</Container>

View File

@@ -30,13 +30,6 @@ export default function Header() {
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// Close mobile menu on route change
useEffect(() => {
if (isMobileMenuOpen) {
setIsMobileMenuOpen(false);
}
}, [pathname, isMobileMenuOpen]);
// Prevent scroll when mobile menu is open
useEffect(() => {
if (isMobileMenuOpen) {
@@ -94,7 +87,6 @@ export default function Header() {
height={120}
className="h-10 md:h-14 w-auto transition-all duration-500 group-hover:scale-110"
priority
unoptimized
/>
</Link>
</motion.div>
@@ -113,10 +105,11 @@ export default function Header() {
}}
>
<motion.nav className="hidden lg:flex items-center space-x-10" variants={navVariants}>
{menuItems.map((item, idx) => (
{menuItems.map((item, _idx) => (
<motion.div key={item.href} variants={navLinkVariants}>
<Link
href={`/${currentLocale}${item.href === '/' ? '' : item.href}`}
onClick={() => setIsMobileMenuOpen(false)}
className={cn(
textColorClass,
'hover:text-accent font-bold transition-all duration-500 text-base md:text-lg tracking-tight relative group inline-block hover:-translate-y-0.5',
@@ -281,6 +274,7 @@ export default function Header() {
>
<Link
href={`/${currentLocale}${item.href === '/' ? '' : item.href}`}
onClick={() => setIsMobileMenuOpen(false)}
className="text-2xl md:text-3xl font-extrabold text-white hover:text-accent transition-all transform block py-4"
>
{item.label}

View File

@@ -21,7 +21,7 @@ export default function Lightbox({ isOpen, images, initialIndex, onClose }: Ligh
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
setMounted(true); // eslint-disable-line react-hooks/set-state-in-effect
return () => setMounted(false);
}, []);
@@ -59,7 +59,7 @@ export default function Lightbox({ isOpen, images, initialIndex, onClose }: Ligh
if (photoParam !== null) {
const index = parseInt(photoParam, 10);
if (!isNaN(index) && index >= 0 && index < images.length) {
setCurrentIndex(index);
setCurrentIndex(index); // eslint-disable-line react-hooks/set-state-in-effect
}
}
}, [searchParams, images.length]);

View File

@@ -46,6 +46,7 @@ export function OGImageTemplate({
display: 'flex',
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={image}
alt=""
@@ -182,4 +183,3 @@ export function OGImageTemplate({
</div>
);
}

View File

@@ -9,17 +9,17 @@ export default function Experience() {
return (
<Section className="relative py-32 md:py-48 overflow-hidden text-white">
<div className="absolute inset-0 z-0">
<Image
src="/uploads/2024/12/1694273920124-copy-2.webp"
alt={t('subtitle')}
<Image
src="/uploads/2024/12/1694273920124-copy-2.webp"
alt={t('subtitle')}
fill
className="object-cover object-center scale-105 animate-slow-zoom"
unoptimized
sizes="100vw"
/>
<div className="absolute inset-0 bg-primary/80 mix-blend-multiply" />
<div className="absolute inset-0 bg-gradient-to-r from-primary via-primary/40 to-transparent" />
</div>
<Container className="relative z-10">
<div className="max-w-3xl">
<Heading level={2} subtitle={t('subtitle')} className="text-white">
@@ -29,19 +29,25 @@ export default function Experience() {
<p className="border-l-4 border-accent pl-8 py-2 bg-white/5 backdrop-blur-sm rounded-r-xl">
{t('p1')}
</p>
<p className="pl-9">
{t('p2')}
</p>
<p className="pl-9">{t('p2')}</p>
</div>
<div className="mt-16 grid grid-cols-1 md:grid-cols-2 gap-12">
<div className="animate-fade-in">
<div className="text-2xl md:text-3xl font-extrabold text-accent mb-4">{t('certifiedQuality')}</div>
<div className="text-base md:text-lg font-bold uppercase tracking-widest text-white/60">{t('vdeApproved')}</div>
<div className="text-2xl md:text-3xl font-extrabold text-accent mb-4">
{t('certifiedQuality')}
</div>
<div className="text-base md:text-lg font-bold uppercase tracking-widest text-white/60">
{t('vdeApproved')}
</div>
</div>
<div className="animate-fade-in" style={{ animationDelay: '100ms' }}>
<div className="text-2xl md:text-3xl font-extrabold text-accent mb-4">{t('fullSpectrum')}</div>
<div className="text-base md:text-lg font-bold uppercase tracking-widest text-white/60">{t('solutionsRange')}</div>
<div className="text-2xl md:text-3xl font-extrabold text-accent mb-4">
{t('fullSpectrum')}
</div>
<div className="text-base md:text-lg font-bold uppercase tracking-widest text-white/60">
{t('solutionsRange')}
</div>
</div>
</div>
</div>

View File

@@ -1,6 +1,5 @@
'use client';
import React, { useState, useEffect } from 'react';
import React from 'react';
import Image from 'next/image';
import { useTranslations } from 'next-intl';
import { Section, Container, Heading } from '../../components/ui';
@@ -19,19 +18,9 @@ export default function GallerySection() {
'/uploads/2024/12/DSC07768-Large.webp',
];
const [lightboxOpen, setLightboxOpen] = useState(false);
const [lightboxIndex, setLightboxIndex] = useState(0);
useEffect(() => {
const photoParam = searchParams.get('photo');
if (photoParam !== null) {
const index = parseInt(photoParam, 10);
if (!isNaN(index) && index >= 0 && index < images.length) {
if (lightboxIndex !== index) setLightboxIndex(index);
if (!lightboxOpen) setLightboxOpen(true);
}
}
}, [searchParams, images.length, lightboxIndex, lightboxOpen]);
const photoParam = searchParams.get('photo');
const lightboxOpen = photoParam !== null;
const lightboxIndex = photoParam ? parseInt(photoParam, 10) : 0;
return (
<Section className="bg-white text-white py-32">
@@ -45,8 +34,10 @@ export default function GallerySection() {
<button
key={idx}
onClick={() => {
setLightboxIndex(idx);
setLightboxOpen(true);
const params = new URLSearchParams(searchParams.toString());
params.set('photo', idx.toString());
window.history.replaceState(null, '', `?${params.toString()}`);
// Since we're using derive-from-url, the component will re-render with the new value
}}
className="relative aspect-[4/3] overflow-hidden rounded-3xl group shadow-lg hover:shadow-2xl transition-all duration-700 cursor-pointer"
>
@@ -55,7 +46,7 @@ export default function GallerySection() {
alt={`${t('alt')} ${idx + 1}`}
fill
className="object-cover transition-transform duration-1000 group-hover:scale-110"
unoptimized
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
<div className="absolute inset-0 bg-primary-dark/20 group-hover:bg-transparent transition-all duration-500" />
<div className="absolute inset-0 border-0 group-hover:border-[16px] border-white/10 transition-all duration-500 pointer-events-none" />
@@ -68,7 +59,11 @@ export default function GallerySection() {
isOpen={lightboxOpen}
images={images}
initialIndex={lightboxIndex}
onClose={() => setLightboxOpen(false)}
onClose={() => {
const params = new URLSearchParams(searchParams.toString());
params.delete('photo');
window.history.replaceState(null, '', `?${params.toString()}`);
}}
/>
</Section>
);

View File

@@ -4,7 +4,8 @@ import Scribble from '@/components/Scribble';
import { Button, Container, Heading, Section } from '@/components/ui';
import { motion } from 'framer-motion';
import { useTranslations } from 'next-intl';
import HeroIllustration from './HeroIllustration';
import dynamic from 'next/dynamic';
const HeroIllustration = dynamic(() => import('./HeroIllustration'), { ssr: false });
export default function Hero() {
const t = useTranslations('Home.hero');
@@ -19,7 +20,10 @@ export default function Hero() {
variants={containerVariants}
>
<motion.div variants={headingVariants}>
<Heading level={1} 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]">
<Heading
level={1}
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]"
>
{t.rich('title', {
green: (chunks) => (
<span className="relative inline-block">
@@ -36,7 +40,7 @@ export default function Hero() {
<Scribble variant="circle" />
</motion.div>
</span>
)
),
})}
</Heading>
</motion.div>
@@ -50,13 +54,23 @@ export default function Hero() {
variants={buttonContainerVariants}
>
<motion.div variants={buttonVariants}>
<Button href="/contact" variant="accent" size="lg" className="group w-full sm:w-auto h-14 md:h-16 px-8 md:px-10 text-base md:text-lg">
<Button
href="/contact"
variant="accent"
size="lg"
className="group w-full sm:w-auto h-14 md:h-16 px-8 md:px-10 text-base md:text-lg"
>
{t('cta')}
<span className="transition-transform group-hover/btn:translate-x-1">&rarr;</span>
</Button>
</motion.div>
<motion.div variants={buttonVariants}>
<Button href="/products" variant="white" size="lg" className="group w-full sm:w-auto h-14 md:h-16 px-8 md:px-10 text-base md:text-lg md:bg-white md:text-primary md:hover:bg-neutral-light md:border-none">
<Button
href="/products"
variant="white"
size="lg"
className="group w-full sm:w-auto h-14 md:h-16 px-8 md:px-10 text-base md:text-lg md:bg-white md:text-primary md:hover:bg-neutral-light md:border-none"
>
{t('exploreProducts')}
</Button>
</motion.div>
@@ -77,7 +91,7 @@ export default function Hero() {
className="absolute bottom-6 md:bottom-10 left-1/2 -translate-x-1/2 hidden sm:block"
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, ease: "easeOut", delay: 3 }}
transition={{ duration: 1, ease: 'easeOut', delay: 3 }}
>
<div className="w-6 h-10 border-2 border-white/30 rounded-full flex justify-center p-1">
<motion.div
@@ -86,7 +100,7 @@ export default function Hero() {
transition={{
duration: 1.5,
repeat: Infinity,
ease: "easeInOut"
ease: 'easeInOut',
}}
/>
</div>
@@ -101,9 +115,9 @@ const containerVariants = {
opacity: 1,
transition: {
staggerChildren: 0.12,
delayChildren: 0.4
}
}
delayChildren: 0.4,
},
},
} as const;
const headingVariants = {
@@ -112,8 +126,8 @@ const headingVariants = {
opacity: 1,
y: 0,
scale: 1,
transition: { duration: 1.2, ease: [0.25, 0.46, 0.45, 0.94] }
}
transition: { duration: 1.2, ease: [0.25, 0.46, 0.45, 0.94] },
},
} as const;
const accentVariants = {
@@ -122,8 +136,8 @@ const accentVariants = {
opacity: 1,
scale: 1,
rotate: 0,
transition: { duration: 0.8, ease: [0.25, 0.46, 0.45, 0.94] }
}
transition: { duration: 0.8, ease: [0.25, 0.46, 0.45, 0.94] },
},
} as const;
const scribbleVariants = {
@@ -132,8 +146,8 @@ const scribbleVariants = {
opacity: 1,
scale: 1,
rotate: 0,
transition: { duration: 1, type: "spring", stiffness: 300, damping: 20 }
}
transition: { duration: 1, type: 'spring', stiffness: 300, damping: 20 },
},
} as const;
const subtitleVariants = {
@@ -142,8 +156,8 @@ const subtitleVariants = {
opacity: 1,
y: 0,
scale: 1,
transition: { duration: 1, ease: [0.25, 0.46, 0.45, 0.94] }
}
transition: { duration: 1, ease: [0.25, 0.46, 0.45, 0.94] },
},
} as const;
const buttonContainerVariants = {
@@ -152,9 +166,9 @@ const buttonContainerVariants = {
opacity: 1,
transition: {
staggerChildren: 0.15,
delayChildren: 0.4
}
}
delayChildren: 0.4,
},
},
} as const;
const buttonVariants = {
@@ -163,6 +177,6 @@ const buttonVariants = {
opacity: 1,
y: 0,
scale: 1,
transition: { type: "spring", stiffness: 400, damping: 20 }
}
transition: { type: 'spring', stiffness: 400, damping: 20 },
},
} as const;

File diff suppressed because it is too large Load Diff

View File

@@ -11,43 +11,53 @@ export default function MeetTheTeam() {
return (
<Section className="relative py-32 md:py-48 overflow-hidden">
<div className="absolute inset-0 z-0">
<Image
src="/uploads/2024/12/DSC08036-Large.webp"
alt={t('subtitle')}
<Image
src="/uploads/2024/12/DSC08036-Large.webp"
alt={t('subtitle')}
fill
className="object-cover scale-105 animate-slow-zoom"
unoptimized
sizes="100vw"
/>
<div className="absolute inset-0 bg-primary/70 mix-blend-multiply" />
<div className="absolute inset-0 bg-gradient-to-t from-primary via-primary/20 to-transparent" />
</div>
<Container className="relative z-10">
<div className="max-w-3xl text-white animate-slide-up">
<Heading level={2} subtitle={t('subtitle')} className="text-white mb-8">
<span className="text-white">{t('title')}</span>
</Heading>
<div className="relative mb-12">
<div className="absolute -left-8 top-0 bottom-0 w-1 bg-accent rounded-full" />
<p className="text-xl md:text-2xl leading-relaxed font-medium italic text-white/90 pl-8">
"{t('description')}"
</p>
</div>
<div className="flex flex-wrap gap-8 items-center">
<Button href={`/${locale}/team`} variant="accent" size="xl" className="group">
{t('cta')}
<span className="ml-3 transition-transform group-hover:translate-x-2">&rarr;</span>
</Button>
<div className="flex items-center gap-4">
<div className="flex -space-x-4">
<div className="w-14 h-14 rounded-full border-4 border-primary overflow-hidden relative">
<Image src="/uploads/2024/12/DSC07768-Large.webp" alt={teamT('michael.name')} fill className="object-cover" />
<Image
src="/uploads/2024/12/DSC07768-Large.webp"
alt={teamT('michael.name')}
fill
className="object-cover"
/>
</div>
<div className="w-14 h-14 rounded-full border-4 border-primary overflow-hidden relative">
<Image src="/uploads/2024/12/DSC07963-Large.webp" alt={teamT('klaus.name')} fill className="object-cover" />
<Image
src="/uploads/2024/12/DSC07963-Large.webp"
alt={teamT('klaus.name')}
fill
className="object-cover"
/>
</div>
</div>
<span className="text-white/60 font-bold text-xs md:text-sm uppercase tracking-widest">

View File

@@ -9,62 +9,74 @@ export default function ProductCategories() {
const locale = useLocale();
const categories = [
{
title: t('categories.lowVoltage.title'),
{
title: t('categories.lowVoltage.title'),
desc: t('categories.lowVoltage.description'),
img: '/uploads/2024/11/low-voltage-category.webp',
icon: '/uploads/2024/11/Low-Voltage.svg',
href: `/${locale}/products/low-voltage-cables`
href: `/${locale}/products/low-voltage-cables`,
},
{
title: t('categories.mediumVoltage.title'),
{
title: t('categories.mediumVoltage.title'),
desc: t('categories.mediumVoltage.description'),
img: '/uploads/2024/11/medium-voltage-category.webp',
icon: '/uploads/2024/11/Medium-Voltage.svg',
href: `/${locale}/products/medium-voltage-cables`
href: `/${locale}/products/medium-voltage-cables`,
},
{
title: t('categories.highVoltage.title'),
{
title: t('categories.highVoltage.title'),
desc: t('categories.highVoltage.description'),
img: '/uploads/2024/11/high-voltage-category.webp',
icon: '/uploads/2024/11/High-Voltage.svg',
href: `/${locale}/products/high-voltage-cables`
href: `/${locale}/products/high-voltage-cables`,
},
{
title: t('categories.solar.title'),
{
title: t('categories.solar.title'),
desc: t('categories.solar.description'),
img: '/uploads/2024/11/solar-category.webp',
icon: '/uploads/2024/11/Solar.svg',
href: `/${locale}/products/solar-cables`
}
href: `/${locale}/products/solar-cables`,
},
];
return (
<Section className="bg-neutral-light py-0 md:py-0 lg:py-0 -mt-px">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4">
{categories.map((category, idx) => (
<Link key={idx} href={category.href} 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">
<Image
src={category.img}
<Link
key={idx}
href={category.href}
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"
>
<Image
src={category.img}
alt={category.title}
fill
className="object-cover transition-transform duration-1000 group-hover:scale-110"
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 25vw"
unoptimized
/>
<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="w-12 h-12 md:w-16 md:h-16 bg-white/10 backdrop-blur-md rounded-xl flex items-center justify-center mb-4 md:mb-6 border border-white/20">
<Image src={category.icon} alt="" width={40} height={40} className="w-8 h-8 md:w-10 md:h-10 brightness-0 invert" unoptimized />
<Image
src={category.icon}
alt=""
width={40}
height={40}
className="w-8 h-8 md:w-10 md:h-10 brightness-0 invert"
/>
</div>
<h3 className="text-xl md:text-2xl font-bold mb-2 md:mb-4 leading-tight">{category.title}</h3>
<h3 className="text-xl md:text-2xl font-bold mb-2 md:mb-4 leading-tight">
{category.title}
</h3>
<p className="text-white/80 text-sm md:text-base line-clamp-3 opacity-100 md:opacity-0 group-hover:opacity-100 transition-all duration-500 max-h-24 md:max-h-0 group-hover:max-h-32">
{category.desc}
</p>
</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('exploreCategory')} <span className="ml-2 transition-transform group-hover:translate-x-2">&rarr;</span>
{t('exploreCategory')}{' '}
<span className="ml-2 transition-transform group-hover:translate-x-2">&rarr;</span>
</div>
</div>
</Link>

View File

@@ -1,4 +1,5 @@
import React from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { getAllPosts } from '@/lib/blog';
import { getTranslations } from 'next-intl/server';
@@ -22,8 +23,11 @@ export default async function RecentPosts({ locale }: RecentPostsProps) {
<Heading level={2} subtitle={t('latestNews')} className="mb-0 text-primary">
{t('allArticles')}
</Heading>
<Link href={`/${locale}/blog`} className="group flex items-center text-primary font-bold text-base md:text-lg touch-target">
{t('allArticles')}
<Link
href={`/${locale}/blog`}
className="group flex items-center text-primary font-bold text-base md:text-lg touch-target"
>
{t('allArticles')}
<span className="ml-2 transition-transform group-hover:translate-x-2">&rarr;</span>
</Link>
</div>
@@ -34,10 +38,12 @@ export default async function RecentPosts({ locale }: RecentPostsProps) {
<Card 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">
<img
src={post.frontmatter.featuredImage}
<Image
src={post.frontmatter.featuredImage}
alt={post.frontmatter.title}
fill
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
sizes="(max-width: 768px) 100vw, 33vw"
/>
<div className="absolute inset-0 image-overlay-gradient opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
{post.frontmatter.category && (
@@ -53,7 +59,7 @@ export default async function RecentPosts({ locale }: RecentPostsProps) {
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
year: 'numeric',
month: 'long',
day: 'numeric'
day: 'numeric',
})}
</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">
@@ -61,8 +67,18 @@ export default async function RecentPosts({ locale }: RecentPostsProps) {
</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">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
<svg
className="ml-2 w-5 h-5 transition-transform group-hover:translate-x-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17 8l4 4m0 0l-4 4m4-4H3"
/>
</svg>
</div>
</div>

View File

@@ -1,83 +0,0 @@
services:
klz-app:
image: node:20-alpine
working_dir: /app
command: sh -c "npm install --legacy-peer-deps && npx next dev"
networks:
- default
- infra
volumes:
- .:/app
environment:
NODE_ENV: development
# Docker Internal Communication
DIRECTUS_URL: http://directus:8055
INTERNAL_DIRECTUS_URL: http://directus:8055
INFRA_DIRECTUS_URL: http://cms-infra-infra-cms-1:8055
GATEKEEPER_URL: http://gatekeeper:3000
DIRECTUS_API_TOKEN: ${DIRECTUS_API_TOKEN}
INFRA_DIRECTUS_TOKEN: ${INFRA_DIRECTUS_TOKEN}
NEXT_PUBLIC_FEEDBACK_ENABLED: ${NEXT_PUBLIC_FEEDBACK_ENABLED}
GATEKEEPER_BYPASS_ENABLED: ${GATEKEEPER_BYPASS_ENABLED}
ports:
- "3000:3000"
labels:
- "traefik.enable=true"
# Global local settings
- "traefik.http.routers.klz-cables-local.entrypoints=web"
- "traefik.http.routers.klz-cables-local.rule=Host(`klz.localhost`)"
- "traefik.http.routers.klz-cables-local.tls=false"
- "traefik.http.routers.klz-cables-local.middlewares="
- "traefik.http.routers.klz-cables-local.service=klz-cables-local"
- "traefik.http.services.klz-cables-local.loadbalancer.server.port=3000"
- "traefik.docker.network=infra"
# Web direct router
- "traefik.http.routers.klz-cables-local-web.entrypoints=web"
- "traefik.http.routers.klz-cables-local-web.rule=Host(`klz.localhost`)"
- "traefik.http.routers.klz-cables-local-web.tls=false"
- "traefik.http.routers.klz-cables-local-web.middlewares="
- "traefik.http.routers.klz-cables-local-web.service=klz-cables-local"
directus:
networks:
- default
- infra
labels:
- "traefik.enable=true"
- "traefik.http.routers.klz-cables-directus-local.entrypoints=web"
- "traefik.http.routers.klz-cables-directus-local.rule=Host(`cms.klz.localhost`)"
- "traefik.http.routers.klz-cables-directus-local.tls=false"
- "traefik.http.routers.klz-cables-directus-local.middlewares="
- "traefik.http.routers.klz-cables-directus-local.service=klz-cables-directus-local"
- "traefik.http.services.klz-cables-directus-local.loadbalancer.server.port=8055"
- "traefik.docker.network=infra"
ports:
- "${DIRECTUS_PORT:-8055}:8055"
environment:
PUBLIC_URL: http://cms.klz.localhost
gatekeeper:
image: node:20-alpine
working_dir: /app/packages/gatekeeper
command: sh -c "corepack enable && CI=true NPM_TOKEN=dummy pnpm install --no-frozen-lockfile && pnpm dev"
volumes:
- /Users/marcmintel/Projects/at-mintel:/app
networks:
- default
- infra
environment:
DIRECTUS_URL: http://directus:8055
NEXT_PUBLIC_BASE_URL: http://gatekeeper.klz.localhost
DIRECTUS_ADMIN_EMAIL: ${DIRECTUS_ADMIN_EMAIL}
DIRECTUS_ADMIN_PASSWORD: ${DIRECTUS_ADMIN_PASSWORD}
COOKIE_DOMAIN: localhost
NODE_ENV: development
labels:
- "traefik.enable=true"
- "traefik.http.routers.klz-cables-gatekeeper-local.entrypoints=web"
- "traefik.http.routers.klz-cables-gatekeeper-local.rule=Host(`gatekeeper.klz.localhost`)"
- "traefik.http.routers.klz-cables-gatekeeper-local.tls=false"
- "traefik.http.routers.klz-cables-gatekeeper-local.service=klz-cables-gatekeeper-local"
- "traefik.http.services.klz-cables-gatekeeper-local.loadbalancer.server.port=3000"
- "traefik.docker.network=infra"

View File

@@ -13,13 +13,13 @@ services:
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-web.rule=${TRAEFIK_HOST_RULE:-Host(`klz-cables.com`)}"
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-web.entrypoints=web"
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-web.middlewares=redirect-https"
# HTTPS router (Protected)
# HTTPS router (Standard)
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}.rule=${TRAEFIK_HOST_RULE:-Host(`klz-cables.com`)}"
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}.entrypoints=websecure"
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}.tls.certresolver=le"
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}.tls=true"
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}.service=${PROJECT_NAME:-klz-cables}"
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}.middlewares=${PROJECT_NAME:-klz-cables}-ratelimit,${PROJECT_NAME:-klz-cables}-forward,${AUTH_MIDDLEWARE:-${PROJECT_NAME:-klz-cables}-compress}"
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}.middlewares=${AUTH_MIDDLEWARE:-${PROJECT_NAME:-klz-cables}-ratelimit,${PROJECT_NAME:-klz-cables}-forward,${PROJECT_NAME:-klz-cables}-compress}"
# HTTPS router (Unprotected - for Analytics & Errors)
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-unprotected.rule=${TRAEFIK_HOST_RULE:-Host(`klz-cables.com`)} && PathPrefix(`/stats`, `/errors`)"
@@ -27,7 +27,7 @@ services:
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-unprotected.tls.certresolver=le"
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-unprotected.tls=true"
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-unprotected.service=${PROJECT_NAME:-klz-cables}"
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-unprotected.middlewares=${PROJECT_NAME:-klz-cables}-ratelimit,${PROJECT_NAME:-klz-cables}-forward,${PROJECT_NAME:-klz-cables}-compress"
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-unprotected.middlewares=${AUTH_MIDDLEWARE_UNPROTECTED:-${PROJECT_NAME:-klz-cables}-ratelimit,${PROJECT_NAME:-klz-cables}-forward,${PROJECT_NAME:-klz-cables}-compress}"
- "traefik.http.services.${PROJECT_NAME:-klz-cables}.loadbalancer.server.port=3000"
- "traefik.http.services.${PROJECT_NAME:-klz-cables}.loadbalancer.server.scheme=http"
@@ -40,19 +40,27 @@ services:
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-forward.headers.customrequestheaders.X-Forwarded-Proto=https"
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-forward.headers.customrequestheaders.X-Forwarded-Ssl=on"
# Authentication Middleware (ForwardAuth)
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-auth.forwardauth.address=http://${PROJECT_NAME:-klz-cables}-gatekeeper:3000/gatekeeper/api/verify"
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-auth.forwardauth.trustForwardHeader=true"
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-auth.forwardauth.authRequestHeaders=X-Forwarded-Host,X-Forwarded-Proto,X-Forwarded-For,Cookie"
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-auth.forwardauth.authResponseHeaders=X-Auth-User"
# Middleware Definitions
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-ratelimit.ratelimit.average=100"
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-ratelimit.ratelimit.burst=50"
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-auth.forwardauth.address=http://${PROJECT_NAME}-gatekeeper:3000/gatekeeper/api/verify"
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-auth.forwardauth.trustForwardHeader=true"
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-auth.forwardauth.authResponseHeaders=X-Auth-User"
healthcheck:
test: [ "CMD", "curl", "-f", "http://127.0.0.1:3000/" ]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
gatekeeper:
image: registry.infra.mintel.me/mintel/gatekeeper:latest
container_name: ${PROJECT_NAME:-klz-cables}-gatekeeper
profiles: [ "gatekeeper" ]
image: registry.infra.mintel.me/mintel/gatekeeper:v1.7.12
restart: always
networks:
default:
infra:
aliases:
- ${PROJECT_NAME:-klz-cables}-gatekeeper
@@ -60,22 +68,20 @@ services:
- ${ENV_FILE:-.env}
environment:
PORT: 3000
PROJECT_NAME: ${PROJECT_NAME:-KLZ Cables}
PROJECT_COLOR: "#82ed20"
COOKIE_DOMAIN: ${COOKIE_DOMAIN}
AUTH_COOKIE_NAME: klz_gatekeeper_session
NEXT_PUBLIC_BASE_URL: https://gatekeeper.${TRAEFIK_HOST:-klz-cables.com}
GATEKEEPER_PASSWORD: ${GATEKEEPER_PASSWORD:-klz2026}
DIRECTUS_URL: ${DIRECTUS_URL}
DIRECTUS_ADMIN_EMAIL: ${DIRECTUS_ADMIN_EMAIL}
DIRECTUS_ADMIN_PASSWORD: ${DIRECTUS_ADMIN_PASSWORD}
AUTH_COOKIE_NAME: ${AUTH_COOKIE_NAME:-klz_gatekeeper_session}
GATEKEEPER_PASSWORD: ${GATEKEEPER_PASSWORD}
NEXT_PUBLIC_BASE_URL: ${GATEKEEPER_ORIGIN}
labels:
- "traefik.enable=true"
# Gatekeeper Router (Shared Host + dedicated Subdomain)
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-gatekeeper.rule=(Host(`${TRAEFIK_HOST:-klz-cables.com}`) && PathPrefix(`/gatekeeper`)) || Host(`gatekeeper.${TRAEFIK_HOST:-klz-cables.com}`)"
- "traefik.docker.network=infra"
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-gatekeeper.rule=(Host(`${TRAEFIK_HOST:-testing.klz-cables.com}`) && PathPrefix(`/gatekeeper`))"
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-gatekeeper.entrypoints=websecure"
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-gatekeeper.tls.certresolver=le"
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-gatekeeper.tls=true"
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-gatekeeper.service=${PROJECT_NAME:-klz-cables}-gatekeeper"
- "traefik.http.services.${PROJECT_NAME:-klz-cables}-gatekeeper.loadbalancer.server.port=3000"
- "traefik.docker.network=infra"

11
final_lint_output.txt Normal file
View File

@@ -0,0 +1,11 @@
> klz-cables-nextjs@1.0.0 lint /Users/marcmintel/Projects/klz-2026
> eslint .
/Users/marcmintel/Projects/klz-2026/components/home/GallerySection.tsx
3:17 warning 'useState' is defined but never used @typescript-eslint/no-unused-vars
3:27 warning 'useEffect' is defined but never used @typescript-eslint/no-unused-vars
✖ 2 problems (0 errors, 2 warnings)

View File

@@ -2,11 +2,18 @@ import { getRequestConfig } from 'next-intl/server';
import * as Sentry from '@sentry/nextjs';
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
// Hardened locale validation: only allow 'en' or 'de'
// Use a temporary variable to validate before assigning to locale
const rawLocale = await requestLocale;
const supportedLocales = ['en', 'de'];
const locale =
typeof rawLocale === 'string' && supportedLocales.includes(rawLocale) ? rawLocale : 'en';
// Ensure that a valid locale is used
if (!locale || !['en', 'de'].includes(locale)) {
locale = 'en';
// Log to Sentry if we had to fallback, as it might indicate a routing issue
if (!rawLocale || !supportedLocales.includes(rawLocale as string)) {
console.warn(
`[i18n] Invalid or missing requestLocale received: "${rawLocale}". Falling back to "en".`,
);
}
return {
@@ -26,6 +33,6 @@ export default getRequestConfig(async ({ requestLocale }) => {
return path;
}
return 'fallback';
}
},
} as any;
});

View File

@@ -2,7 +2,7 @@
* Centralized configuration management for the application.
* This file provides a type-safe way to access environment variables.
*/
import { env, getRawEnv } from './env';
import { getRawEnv } from './env';
let memoizedConfig: ReturnType<typeof createConfig> | undefined;

View File

@@ -3,8 +3,20 @@ import { createMintelDirectusClient, ensureDirectusAuthenticated } from '@mintel
import { config } from './config';
import { getServerAppServices } from './services/create-services.server';
/**
* Directus Schema Definitions
*/
export interface Schema {
products: any[];
categories: any[];
contact_submissions: any[];
product_requests: any[];
translations: any[];
categories_link: any[];
}
// Initialize client using Mintel standards (environment-aware)
const client = createMintelDirectusClient();
const client = createMintelDirectusClient<Schema>();
/**
* Helper to determine if we should show detailed errors

View File

@@ -1,14 +1,9 @@
import { z } from 'zod';
import { validateMintelEnv, mintelEnvSchema } from '@mintel/next-utils';
import { validateMintelEnv, mintelEnvSchema, withMintelRefinements } from '@mintel/next-utils';
/**
* Environment variable schema.
* Extends the default Mintel environment schema which already includes:
* - Directus (URL, TOKEN, INTERNAL_URL, etc.)
* - Mail (HOST, PORT, etc.)
* - Gotify
* - Logging
* - Analytics
* Extends the default Mintel environment schema.
*/
const envExtension = {
// Project specific overrides or additions
@@ -29,6 +24,11 @@ const envExtension = {
INFRA_DIRECTUS_TOKEN: z.string().optional(),
};
/**
* Full schema including Mintel base and refinements
*/
export const envSchema = withMintelRefinements(z.object(mintelEnvSchema).extend(envExtension));
/**
* Validated environment object.
*/

View File

@@ -1,7 +1,6 @@
import nodemailer from 'nodemailer';
import { getServerAppServices } from '@/lib/services/create-services.server';
import { config } from '../config';
import { ReactElement } from 'react';
let transporterInstance: nodemailer.Transporter | null = null;

132
lint_output.txt Normal file
View File

@@ -0,0 +1,132 @@
> klz-cables-nextjs@1.0.0 lint /Users/marcmintel/Projects/klz-2026
> eslint .
(node:66439) ESLintEnvWarning: /* eslint-env */ comments are no longer recognized when linting with flat config and will be reported as errors as of v10.0.0. Replace them with /* global */ comments or define globals in your config file. See https://eslint.org/docs/latest/use/configure/migration-guide#eslint-env-configuration-comments for details. Found in /Users/marcmintel/Projects/klz-2026/.lintstagedrc.cjs at line 2.
(Use `node --trace-warnings ...` to show where the warning was created)
(node:66439) ESLintEnvWarning: /* eslint-env */ comments are no longer recognized when linting with flat config and will be reported as errors as of v10.0.0. Replace them with /* global */ comments or define globals in your config file. See https://eslint.org/docs/latest/use/configure/migration-guide#eslint-env-configuration-comments for details. Found in /Users/marcmintel/Projects/klz-2026/commitlint.config.cjs at line 2.
(node:66439) ESLintEnvWarning: /* eslint-env */ comments are no longer recognized when linting with flat config and will be reported as errors as of v10.0.0. Replace them with /* global */ comments or define globals in your config file. See https://eslint.org/docs/latest/use/configure/migration-guide#eslint-env-configuration-comments for details. Found in /Users/marcmintel/Projects/klz-2026/postcss.config.cjs at line 2.
(node:66439) ESLintEnvWarning: /* eslint-env */ comments are no longer recognized when linting with flat config and will be reported as errors as of v10.0.0. Replace them with /* global */ comments or define globals in your config file. See https://eslint.org/docs/latest/use/configure/migration-guide#eslint-env-configuration-comments for details. Found in /Users/marcmintel/Projects/klz-2026/tailwind.config.cjs at line 2.
/Users/marcmintel/Projects/klz-2026/.lintstagedrc.cjs
3:14 error A `require()` style import is forbidden @typescript-eslint/no-require-imports
/Users/marcmintel/Projects/klz-2026/app/[locale]/blog/[slug]/page.tsx
2:8 warning 'Script' is defined but never used @typescript-eslint/no-unused-vars
4:10 warning 'getBreadcrumbSchema' is defined but never used @typescript-eslint/no-unused-vars
4:41 warning 'LOGO_URL' is defined but never used @typescript-eslint/no-unused-vars
/Users/marcmintel/Projects/klz-2026/app/[locale]/blog/page.tsx
63:15 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
148:25 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
/Users/marcmintel/Projects/klz-2026/app/[locale]/layout.tsx
81:12 warning 'e' is defined but never used @typescript-eslint/no-unused-vars
/Users/marcmintel/Projects/klz-2026/app/[locale]/page.tsx
70:12 warning 'err' is defined but never used @typescript-eslint/no-unused-vars
74:14 warning 'e' is defined but never used @typescript-eslint/no-unused-vars
75:12 warning 'key' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
/Users/marcmintel/Projects/klz-2026/app/[locale]/products/[...slug]/page.tsx
1:8 warning 'Script' is defined but never used @typescript-eslint/no-unused-vars
3:10 warning 'getBreadcrumbSchema' is defined but never used @typescript-eslint/no-unused-vars
/Users/marcmintel/Projects/klz-2026/app/errors/api/relay/route.ts
28:11 warning 'header' is assigned a value but never used @typescript-eslint/no-unused-vars
/Users/marcmintel/Projects/klz-2026/components/CMSConnectivityNotice.tsx
4:34 warning 'Database' is defined but never used @typescript-eslint/no-unused-vars
8:10 warning 'status' is assigned a value but never used @typescript-eslint/no-unused-vars
35:16 warning 'err' is defined but never used @typescript-eslint/no-unused-vars
/Users/marcmintel/Projects/klz-2026/components/Header.tsx
36:7 warning Error: Calling setState synchronously within an effect can trigger cascading renders
Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
* Update external systems with the latest state from React.
* Subscribe for updates from some external system, calling setState in a callback function when external state changes.
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).
/Users/marcmintel/Projects/klz-2026/components/Header.tsx:36:7
34 | useEffect(() => {
35 | if (isMobileMenuOpen) {
> 36 | setIsMobileMenuOpen(false);
| ^^^^^^^^^^^^^^^^^^^ Avoid calling setState() directly within an effect
37 | }
38 | }, [pathname, isMobileMenuOpen]);
39 | react-hooks/set-state-in-effect
116:37 warning 'idx' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
/Users/marcmintel/Projects/klz-2026/components/Lightbox.tsx
24:5 warning Error: Calling setState synchronously within an effect can trigger cascading renders
Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
* Update external systems with the latest state from React.
* Subscribe for updates from some external system, calling setState in a callback function when external state changes.
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).
/Users/marcmintel/Projects/klz-2026/components/Lightbox.tsx:24:5
22 |
23 | useEffect(() => {
> 24 | setMounted(true);
| ^^^^^^^^^^ Avoid calling setState() directly within an effect
25 | return () => setMounted(false);
26 | }, []);
27 | react-hooks/set-state-in-effect
62:9 warning Error: Calling setState synchronously within an effect can trigger cascading renders
Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
* Update external systems with the latest state from React.
* Subscribe for updates from some external system, calling setState in a callback function when external state changes.
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).
/Users/marcmintel/Projects/klz-2026/components/Lightbox.tsx:62:9
60 | const index = parseInt(photoParam, 10);
61 | if (!isNaN(index) && index >= 0 && index < images.length) {
> 62 | setCurrentIndex(index);
| ^^^^^^^^^^^^^^^ Avoid calling setState() directly within an effect
63 | }
64 | }
65 | }, [searchParams, images.length]); react-hooks/set-state-in-effect
/Users/marcmintel/Projects/klz-2026/components/OGImageTemplate.tsx
49:11 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
/Users/marcmintel/Projects/klz-2026/components/home/GallerySection.tsx
30:38 warning Error: Calling setState synchronously within an effect can trigger cascading renders
Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
* Update external systems with the latest state from React.
* Subscribe for updates from some external system, calling setState in a callback function when external state changes.
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).
/Users/marcmintel/Projects/klz-2026/components/home/GallerySection.tsx:30:38
28 | const index = parseInt(photoParam, 10);
29 | if (!isNaN(index) && index >= 0 && index < images.length) {
> 30 | if (lightboxIndex !== index) setLightboxIndex(index);
| ^^^^^^^^^^^^^^^^ Avoid calling setState() directly within an effect
31 | if (!lightboxOpen) setLightboxOpen(true);
32 | }
33 | } react-hooks/set-state-in-effect
/Users/marcmintel/Projects/klz-2026/components/home/RecentPosts.tsx
37:21 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
/Users/marcmintel/Projects/klz-2026/lib/config.ts
5:10 warning 'env' is defined but never used @typescript-eslint/no-unused-vars
/Users/marcmintel/Projects/klz-2026/lib/mail/mailer.ts
4:10 warning 'ReactElement' is defined but never used @typescript-eslint/no-unused-vars
/Users/marcmintel/Projects/klz-2026/middleware.ts
2:10 warning 'NextResponse' is defined but never used @typescript-eslint/no-unused-vars
33:12 warning 'publicHostname' is assigned a value but never used @typescript-eslint/no-unused-vars
✖ 27 problems (1 error, 26 warnings)
ELIFECYCLE Command failed with exit code 1.

View File

@@ -0,0 +1,65 @@
> klz-cables-nextjs@1.0.0 lint /Users/marcmintel/Projects/klz-2026
> eslint .
/Users/marcmintel/Projects/klz-2026/app/[locale]/layout.tsx
81:12 warning '_e' is defined but never used @typescript-eslint/no-unused-vars
/Users/marcmintel/Projects/klz-2026/app/[locale]/page.tsx
70:12 warning '_err' is defined but never used @typescript-eslint/no-unused-vars
74:14 warning '_e' is defined but never used @typescript-eslint/no-unused-vars
/Users/marcmintel/Projects/klz-2026/app/errors/api/relay/route.ts
28:11 warning '_header' is assigned a value but never used @typescript-eslint/no-unused-vars
/Users/marcmintel/Projects/klz-2026/components/CMSConnectivityNotice.tsx
8:10 warning '_status' is assigned a value but never used @typescript-eslint/no-unused-vars
35:16 warning '_err' is defined but never used @typescript-eslint/no-unused-vars
/Users/marcmintel/Projects/klz-2026/components/Lightbox.tsx
24:5 warning Error: Calling setState synchronously within an effect can trigger cascading renders
Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
* Update external systems with the latest state from React.
* Subscribe for updates from some external system, calling setState in a callback function when external state changes.
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).
/Users/marcmintel/Projects/klz-2026/components/Lightbox.tsx:24:5
22 |
23 | useEffect(() => {
> 24 | setMounted(true);
| ^^^^^^^^^^ Avoid calling setState() directly within an effect
25 | return () => setMounted(false);
26 | }, []); // eslint-disable-line react-hooks/set-state-in-effect
27 | react-hooks/set-state-in-effect
26:11 warning Unused eslint-disable directive (no problems were reported from 'react-hooks/set-state-in-effect')
62:9 warning Error: Calling setState synchronously within an effect can trigger cascading renders
Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
* Update external systems with the latest state from React.
* Subscribe for updates from some external system, calling setState in a callback function when external state changes.
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).
/Users/marcmintel/Projects/klz-2026/components/Lightbox.tsx:62:9
60 | const index = parseInt(photoParam, 10);
61 | if (!isNaN(index) && index >= 0 && index < images.length) {
> 62 | setCurrentIndex(index);
| ^^^^^^^^^^^^^^^ Avoid calling setState() directly within an effect
63 | }
64 | }
65 | }, [searchParams, images.length]); // eslint-disable-line react-hooks/set-state-in-effect react-hooks/set-state-in-effect
65:38 warning Unused eslint-disable directive (no problems were reported from 'react-hooks/set-state-in-effect')
/Users/marcmintel/Projects/klz-2026/components/home/GallerySection.tsx
3:17 warning 'useState' is defined but never used @typescript-eslint/no-unused-vars
3:27 warning 'useEffect' is defined but never used @typescript-eslint/no-unused-vars
/Users/marcmintel/Projects/klz-2026/middleware.ts
33:12 warning '_publicHostname' is assigned a value but never used @typescript-eslint/no-unused-vars
✖ 13 problems (0 errors, 13 warnings)
0 errors and 2 warnings potentially fixable with the `--fix` option.

View File

@@ -206,7 +206,7 @@
"title": "Produktportfolio | Hochwertige Kabel für jede Anwendung",
"description": "Entdecken Sie unser umfassendes Sortiment an zertifizierten Kabeln: von Niederspannung über Mittel- und Hochspannung bis hin zu spezialisierten Solarkabeln."
},
"title": "Unsere Produkte",
"title": "Unsere <green>Produkte</green>",
"subtitle": "Entdecken Sie unser umfassendes Sortiment an hochwertigen Kabeln für jede Anwendung.",
"heroSubtitle": "Produktportfolio",
"categoryLabel": "Kategorie",
@@ -393,4 +393,4 @@
"cta": "Zurück zur Sicherheit"
}
}
}
}

View File

@@ -206,7 +206,7 @@
"title": "Product Portfolio | High-Quality Cables for Every Application",
"description": "Explore our comprehensive range of certified cables: from low voltage to medium and high voltage, as well as specialized solar cables."
},
"title": "Our Products",
"title": "Our <green>Products</green>",
"subtitle": "Explore our comprehensive range of high-quality cables designed for every application.",
"heroSubtitle": "Product Portfolio",
"categoryLabel": "Category",
@@ -393,4 +393,4 @@
"cta": "Back to Safety"
}
}
}
}

View File

@@ -1,5 +1,5 @@
import createMiddleware from 'next-intl/middleware';
import { NextResponse, NextRequest } from 'next/server';
import { NextRequest } from 'next/server';
// Create the internationalization middleware
const intlMiddleware = createMiddleware({
@@ -30,7 +30,7 @@ export default function middleware(request: NextRequest) {
// Prioritize x-forwarded-host (passed by Traefik) over the local Host header
const hostHeader =
headers.get('x-forwarded-host') || headers.get('host') || 'testing.klz-cables.com';
const [publicHostname] = hostHeader.split(':');
hostHeader.split(':');
urlObj.protocol = proto;
// urlObj.hostname = publicHostname; // Don't rewrite hostname yet as it breaks internal fetches in dev

2
next-env.d.ts vendored
View File

@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import './.next/types/routes.d.ts';
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -1,10 +1,13 @@
{
"name": "klz-cables-nextjs",
"type": "module",
"private": true,
"dependencies": {
"@directus/sdk": "^18.0.3",
"@mintel/mail": "^1.6.0",
"@mintel/next-config": "^1.6.0",
"@mintel/next-feedback": "^1.6.0",
"@mintel/next-utils": "^1.7.8",
"@directus/sdk": "^21.0.0",
"@mintel/mail": "1.7.12",
"@mintel/next-config": "1.7.12",
"@mintel/next-feedback": "1.7.12",
"@mintel/next-utils": "^1.7.15",
"@react-email/components": "^1.0.7",
"@react-pdf/renderer": "^4.3.2",
"@sentry/nextjs": "^10.38.0",
@@ -36,14 +39,14 @@
"svg-to-pdfkit": "^0.1.8",
"tailwind-merge": "^3.4.0",
"xlsx": "^0.18.5",
"zod": "^4.3.6"
"zod": "3.25.76"
},
"devDependencies": {
"@commitlint/cli": "^20.4.0",
"@commitlint/config-conventional": "^20.4.0",
"@lhci/cli": "^0.15.1",
"@mintel/eslint-config": "^1.6.0",
"@mintel/tsconfig": "^1.6.0",
"@mintel/eslint-config": "1.7.12",
"@mintel/tsconfig": "1.7.12",
"@tailwindcss/cli": "^4.1.18",
"@tailwindcss/postcss": "^4.1.18",
"@types/geojson": "^7946.0.16",
@@ -53,9 +56,11 @@
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@types/sharp": "^0.31.1",
"@vitejs/plugin-react": "^5.1.4",
"@vitest/ui": "^4.0.16",
"autoprefixer": "^10.4.23",
"eslint": "^9.18.0",
"happy-dom": "^20.6.1",
"husky": "^9.1.7",
"lint-staged": "^16.2.7",
"postcss": "^8.5.6",
@@ -66,8 +71,6 @@
"typescript": "^5.7.2",
"vitest": "^4.0.16"
},
"name": "klz-cables-nextjs",
"private": true,
"scripts": {
"dev": "docker network create infra 2>/dev/null || true && echo '\\n🚀 Development Environment Starting...\\n\\n📱 App: http://klz.localhost\\n🗄 CMS: http://cms.klz.localhost/admin\\n🚦 Traefik: http://localhost:8080\\n\\n(Press Ctrl+C to stop)\\n' && docker-compose down --remove-orphans && docker-compose up klz-app directus directus-db gatekeeper",
"dev:local": "next dev",

274
pnpm-lock.yaml generated
View File

@@ -12,20 +12,20 @@ importers:
.:
dependencies:
'@directus/sdk':
specifier: ^18.0.3
version: 18.0.3
specifier: ^21.0.0
version: 21.1.0
'@mintel/mail':
specifier: ^1.6.0
version: 1.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
specifier: 1.7.12
version: 1.7.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@mintel/next-config':
specifier: ^1.6.0
version: 1.7.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.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)(webpack@5.105.0)
specifier: 1.7.12
version: 1.7.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.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)(webpack@5.105.0)
'@mintel/next-feedback':
specifier: ^1.6.0
version: 1.6.0(@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)
specifier: 1.7.12
version: 1.7.12(@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':
specifier: ^1.6.0
version: 1.6.0(@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)
specifier: ^1.7.15
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)
'@react-email/components':
specifier: ^1.0.7
version: 1.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -120,8 +120,8 @@ importers:
specifier: ^0.18.5
version: 0.18.5
zod:
specifier: ^4.3.6
version: 4.3.6
specifier: 3.25.76
version: 3.25.76
devDependencies:
'@commitlint/cli':
specifier: ^20.4.0
@@ -133,11 +133,11 @@ importers:
specifier: ^0.15.1
version: 0.15.1
'@mintel/eslint-config':
specifier: ^1.6.0
version: 1.6.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
specifier: 1.7.12
version: 1.7.12(@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/tsconfig':
specifier: ^1.6.0
version: 1.6.0
specifier: 1.7.12
version: 1.7.12
'@tailwindcss/cli':
specifier: ^4.1.18
version: 4.1.18
@@ -165,6 +165,9 @@ importers:
'@types/sharp':
specifier: ^0.31.1
version: 0.31.1
'@vitejs/plugin-react':
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))
'@vitest/ui':
specifier: ^4.0.16
version: 4.0.18(vitest@4.0.18)
@@ -174,6 +177,9 @@ importers:
eslint:
specifier: ^9.18.0
version: 9.39.2(jiti@2.6.1)
happy-dom:
specifier: ^20.6.1
version: 20.6.1
husky:
specifier: ^9.1.7
version: 9.1.7
@@ -200,7 +206,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^4.0.16
version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.10)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
packages:
@@ -260,6 +266,10 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0
'@babel/helper-plugin-utils@7.28.6':
resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==}
engines: {node: '>=6.9.0'}
'@babel/helper-string-parser@7.27.1':
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
@@ -281,6 +291,18 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/plugin-transform-react-jsx-self@7.27.1':
resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/plugin-transform-react-jsx-source@7.27.1':
resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/runtime@7.28.6':
resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
engines: {node: '>=6.9.0'}
@@ -397,10 +419,6 @@ packages:
resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
engines: {node: '>=20.19.0'}
'@directus/sdk@18.0.3':
resolution: {integrity: sha512-PnEDRDqr2x/DG3HZ3qxU7nFp2nW6zqJqswjii57NhriXgTz4TBUI8NmSdzQvnyHuTL9J0nedYfQGfW4v8odS1A==}
engines: {node: '>=18.0.0'}
'@directus/sdk@21.1.0':
resolution: {integrity: sha512-Ig8zZAQDbc7QMIM54N+x71C04lni9MN9yalNAezjDjFdNknTJzupDY7V5cb+kOJL8GsqDE9Bg8xq8xCmkDVs5A==}
engines: {node: '>=22'}
@@ -1010,29 +1028,29 @@ packages:
'@types/react': '>=16'
react: '>=16'
'@mintel/eslint-config@1.6.0':
resolution: {integrity: sha512-Uvll6W5ulJH5h5ZFbZ7H4MiW3Pa7ZaHDm2ISYcGRUxKRtXZmsP5b3rYH9eIV75H0IYi1j2wj/TMpXrGm/buHVg==}
'@mintel/eslint-config@1.7.12':
resolution: {integrity: sha512-ofX68JWCW8ztD9tt/1MDb6pSr9MJKq3js3Vny7VoT/bObjpR/iO9tJp0ekiq12Ps8VTEgDh1qwwmf2wrJJBRpQ==}
'@mintel/mail@1.6.0':
resolution: {integrity: sha512-NQD0TlE1r9BqE2cUJ4r8ceF9wqlAdELKKygU5nhL3ovI+866+OGVg8qcVh5N11gXZyHieFI6nUiePKz33CrA2g==}
'@mintel/mail@1.7.12':
resolution: {integrity: sha512-2MqGSDhXQ6jaswUs/s74pz2LwmOhtOTWltGgwb8JF2JdfQt8FE5H4XZsvSV12Q1Fs1n5+V09OZTWU1TyFmh6lw==}
peerDependencies:
react: ^19.0.0
react-dom: ^19.0.0
'@mintel/next-config@1.7.0':
resolution: {integrity: sha512-Zf2CoKZTuVJeRfI8Kh17Bvd4QU1IlwVUyXqoDtdRXUupCcDcKO9R2b40HwOg4FMxV1m6mg2SRW5C8WjiGuU0pQ==}
'@mintel/next-config@1.7.12':
resolution: {integrity: sha512-GBFIgF2vRzPl03B2RwaEyBpwjXgDRHUtaal4QQ2n7TW4G1lNIKLTBLdsJtmxMn8VlUbzRVMaFNm7m6joSNiJ0w==}
'@mintel/next-feedback@1.6.0':
resolution: {integrity: sha512-3A5sV9YAdbQzh4fzExDEIKFNwRlow307Iv60R4i4ULsHxdsYwAsokL2LqGgpmR91vslGEzbVHm7P1LX6dEDw2Q==}
'@mintel/next-feedback@1.7.12':
resolution: {integrity: sha512-nlaeV+IRmqwzaAZFTmj8+RvyMsvh+SNs0BopWgbDdAt2x1yoz4fC6cpn+v7KjfnVW0YWPZhMeGD/uEgMhVrXRA==}
peerDependencies:
react: ^19.0.0
react-dom: ^19.0.0
'@mintel/next-utils@1.6.0':
resolution: {integrity: sha512-gdC+QSEx+mGV3T4TO0wckWr0KCnb8dBitkJwYHrTE3h6nD3m01QziW00sqsN9H6nZc66wcbTGgW9oFOb2xoPJg==}
'@mintel/next-utils@1.7.15':
resolution: {integrity: sha512-CqSe3eHamq9zLs+AJxGOPypTLchw/oZ3JcLkor007PcUDMTv/Lspfv5oCaXK2s0FeIOJaa2QwSGPDI1h5/3ZVw==}
'@mintel/tsconfig@1.6.0':
resolution: {integrity: sha512-8qx34GB9dfUFIdEF3wINgXN0cTYVQMcfDB5QFLX/HdjT+nXS/7bjjH5ofnEhsNAXv0jDse1UcL/C69O/Le01pg==}
'@mintel/tsconfig@1.7.12':
resolution: {integrity: sha512-WGs/p2E1xQGkzNasLCZKoplKIhxC17NZRhBYH5O43lp98aHOZMC3BKgNeLYUfoEFGiIN1hx2FUJ69DosQc0xmw==}
'@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
@@ -1040,8 +1058,8 @@ packages:
'@next/env@16.1.6':
resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==}
'@next/eslint-plugin-next@15.1.6':
resolution: {integrity: sha512-+slMxhTgILUntZDGNgsKEYHUvpn72WP1YTlkmEhS51vnVd7S9jEEy0n9YAMcI21vUG4akTw9voWH02lrClt/yw==}
'@next/eslint-plugin-next@16.1.6':
resolution: {integrity: sha512-/Qq3PTagA6+nYVfryAtQ7/9FEr/6YVyvOtl6rZnGsbReGLf0jZU6gkpr1FuChAQpvV46a78p4cmHOVP8mbfSMQ==}
'@next/swc-darwin-arm64@16.1.6':
resolution: {integrity: sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==}
@@ -1756,6 +1774,9 @@ packages:
'@react-pdf/types@2.9.2':
resolution: {integrity: sha512-dufvpKId9OajLLbgn9q7VLUmyo1Jf+iyGk2ZHmCL8nIDtL8N1Ejh9TH7+pXXrR0tdie1nmnEb5Bz9U7g4hI4/g==}
'@rolldown/pluginutils@1.0.0-rc.3':
resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==}
'@rollup/plugin-commonjs@28.0.1':
resolution: {integrity: sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==}
engines: {node: '>=16.0.0 || 14 >= 14.17'}
@@ -1902,9 +1923,6 @@ packages:
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
'@rushstack/eslint-patch@1.15.0':
resolution: {integrity: sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==}
'@schummar/icu-type-parser@1.21.5':
resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==}
@@ -2252,6 +2270,18 @@ packages:
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
'@types/babel__core@7.20.5':
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
'@types/babel__generator@7.27.0':
resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
'@types/babel__template@7.4.4':
resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
'@types/babel__traverse@7.28.0':
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
@@ -2343,6 +2373,12 @@ packages:
'@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
'@types/whatwg-mimetype@3.0.2':
resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==}
'@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
'@types/yauzl@2.10.3':
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
@@ -2503,6 +2539,12 @@ packages:
cpu: [x64]
os: [win32]
'@vitejs/plugin-react@5.1.4':
resolution: {integrity: sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==}
engines: {node: ^20.19.0 || >=22.12.0}
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
'@vitest/expect@4.0.18':
resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==}
@@ -3529,10 +3571,10 @@ packages:
engines: {node: '>=6.0'}
hasBin: true
eslint-config-next@15.1.6:
resolution: {integrity: sha512-Wd1uy6y7nBbXUSg9QAuQ+xYEKli5CgUhLjz1QHW11jLDis5vK5XB3PemL6jEmy7HrdhaRFDz+GTZ/3FoH+EUjg==}
eslint-config-next@16.1.6:
resolution: {integrity: sha512-vKq40io2B0XtkkNDYyleATwblNt8xuh3FWp8SpSz3pt7P01OkBFlKsJZ2mWt5WsCySlDQLckb1zMY9yE9Qy0LA==}
peerDependencies:
eslint: ^7.23.0 || ^8.0.0 || ^9.0.0
eslint: '>=9.0.0'
typescript: '>=3.3.1'
peerDependenciesMeta:
typescript:
@@ -3591,12 +3633,6 @@ packages:
peerDependencies:
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9
eslint-plugin-react-hooks@5.2.0:
resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==}
engines: {node: '>=10'}
peerDependencies:
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
eslint-plugin-react-hooks@7.0.1:
resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==}
engines: {node: '>=18'}
@@ -3972,6 +4008,10 @@ packages:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
engines: {node: '>=18'}
globals@16.4.0:
resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==}
engines: {node: '>=18'}
globalthis@1.0.4:
resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
engines: {node: '>= 0.4'}
@@ -3987,6 +4027,10 @@ packages:
resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==}
engines: {node: '>=6.0'}
happy-dom@20.6.1:
resolution: {integrity: sha512-+0vhESXXhFwkdjZnJ5DlmJIfUYGgIEEjzIjB+aKJbFuqlvvKyOi+XkI1fYbgYR9QCxG5T08koxsQ6HrQfa5gCQ==}
engines: {node: '>=20.0.0'}
has-bigints@1.1.0:
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
engines: {node: '>= 0.4'}
@@ -5493,6 +5537,10 @@ packages:
react-promise-suspense@0.3.4:
resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==}
react-refresh@0.18.0:
resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
engines: {node: '>=0.10.0'}
react@19.2.4:
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
engines: {node: '>=0.10.0'}
@@ -6440,6 +6488,10 @@ packages:
whatwg-fetch@3.6.20:
resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==}
whatwg-mimetype@3.0.0:
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
engines: {node: '>=12'}
whatwg-mimetype@4.0.0:
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
engines: {node: '>=18'}
@@ -6634,9 +6686,6 @@ packages:
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
zod@4.3.6:
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
@@ -6736,6 +6785,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@babel/helper-plugin-utils@7.28.6': {}
'@babel/helper-string-parser@7.27.1': {}
'@babel/helper-validator-identifier@7.28.5': {}
@@ -6751,6 +6802,16 @@ snapshots:
dependencies:
'@babel/types': 7.29.0
'@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
'@babel/runtime@7.28.6': {}
'@babel/template@7.28.6':
@@ -6907,8 +6968,6 @@ snapshots:
'@csstools/css-tokenizer@4.0.0': {}
'@directus/sdk@18.0.3': {}
'@directus/sdk@21.1.0': {}
'@emnapi/core@1.8.1':
@@ -7414,29 +7473,30 @@ snapshots:
'@types/react': 19.2.13
react: 19.2.4
'@mintel/eslint-config@1.6.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
'@mintel/eslint-config@1.7.12(@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)':
dependencies:
'@eslint/eslintrc': 3.3.3
'@eslint/js': 9.39.2
'@next/eslint-plugin-next': 15.1.6
eslint-config-next: 15.1.6(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@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-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))
typescript-eslint: 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
transitivePeerDependencies:
- '@typescript-eslint/parser'
- eslint
- eslint-import-resolver-webpack
- eslint-plugin-import-x
- supports-color
- typescript
'@mintel/mail@1.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@mintel/mail@1.7.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@react-email/components': 0.0.33(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@mintel/next-config@1.7.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.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)(webpack@5.105.0)':
'@mintel/next-config@1.7.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.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)(webpack@5.105.0)':
dependencies:
'@sentry/nextjs': 10.38.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@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)
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)
@@ -7459,7 +7519,7 @@ snapshots:
- typescript
- webpack
'@mintel/next-feedback@1.6.0(@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.7.12(@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:
'@directus/sdk': 21.1.0
clsx: 2.1.1
@@ -7479,7 +7539,7 @@ snapshots:
- babel-plugin-react-compiler
- sass
'@mintel/next-utils@1.6.0(@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.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)':
dependencies:
'@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)
@@ -7497,7 +7557,7 @@ snapshots:
- sass
- typescript
'@mintel/tsconfig@1.6.0': {}
'@mintel/tsconfig@1.7.12': {}
'@napi-rs/wasm-runtime@0.2.12':
dependencies:
@@ -7508,7 +7568,7 @@ snapshots:
'@next/env@16.1.6': {}
'@next/eslint-plugin-next@15.1.6':
'@next/eslint-plugin-next@16.1.6':
dependencies:
fast-glob: 3.3.1
@@ -8255,6 +8315,8 @@ snapshots:
'@react-pdf/primitives': 4.1.1
'@react-pdf/stylesheet': 6.1.2
'@rolldown/pluginutils@1.0.0-rc.3': {}
'@rollup/plugin-commonjs@28.0.1(rollup@4.57.1)':
dependencies:
'@rollup/pluginutils': 5.3.0(rollup@4.57.1)
@@ -8352,8 +8414,6 @@ snapshots:
'@rtsao/scc@1.1.0': {}
'@rushstack/eslint-patch@1.15.0': {}
'@schummar/icu-type-parser@1.21.5': {}
'@selderee/plugin-htmlparser2@0.11.0':
@@ -8744,6 +8804,27 @@ snapshots:
tslib: 2.8.1
optional: true
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.29.0
'@babel/types': 7.29.0
'@types/babel__generator': 7.27.0
'@types/babel__template': 7.4.4
'@types/babel__traverse': 7.28.0
'@types/babel__generator@7.27.0':
dependencies:
'@babel/types': 7.29.0
'@types/babel__template@7.4.4':
dependencies:
'@babel/parser': 7.29.0
'@babel/types': 7.29.0
'@types/babel__traverse@7.28.0':
dependencies:
'@babel/types': 7.29.0
'@types/chai@5.2.3':
dependencies:
'@types/deep-eql': 4.0.2
@@ -8848,6 +8929,12 @@ snapshots:
'@types/unist@3.0.3': {}
'@types/whatwg-mimetype@3.0.2': {}
'@types/ws@8.18.1':
dependencies:
'@types/node': 22.19.10
'@types/yauzl@2.10.3':
dependencies:
'@types/node': 22.19.10
@@ -9005,6 +9092,18 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
optional: true
'@vitejs/plugin-react@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))':
dependencies:
'@babel/core': 7.29.0
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0)
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0)
'@rolldown/pluginutils': 1.0.0-rc.3
'@types/babel__core': 7.20.5
react-refresh: 0.18.0
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)
transitivePeerDependencies:
- supports-color
'@vitest/expect@4.0.18':
dependencies:
'@standard-schema/spec': 1.1.0
@@ -9048,7 +9147,7 @@ snapshots:
sirv: 3.0.2
tinyglobby: 0.2.15
tinyrainbow: 3.0.3
vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.10)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
'@vitest/utils@4.0.18':
dependencies:
@@ -10178,22 +10277,22 @@ snapshots:
optionalDependencies:
source-map: 0.6.1
eslint-config-next@15.1.6(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
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):
dependencies:
'@next/eslint-plugin-next': 15.1.6
'@rushstack/eslint-patch': 1.15.0
'@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/parser': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@next/eslint-plugin-next': 16.1.6
eslint: 9.39.2(jiti@2.6.1)
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-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-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-hooks: 5.2.0(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
typescript-eslint: 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
optionalDependencies:
typescript: 5.9.3
transitivePeerDependencies:
- '@typescript-eslint/parser'
- eslint-import-resolver-webpack
- eslint-plugin-import-x
- supports-color
@@ -10280,18 +10379,14 @@ snapshots:
safe-regex-test: 1.1.0
string.prototype.includes: 2.0.1
eslint-plugin-react-hooks@5.2.0(eslint@9.39.2(jiti@2.6.1)):
dependencies:
eslint: 9.39.2(jiti@2.6.1)
eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@2.6.1)):
dependencies:
'@babel/core': 7.29.0
'@babel/parser': 7.29.0
eslint: 9.39.2(jiti@2.6.1)
hermes-parser: 0.25.1
zod: 4.3.6
zod-validation-error: 4.0.2(zod@4.3.6)
zod: 3.25.76
zod-validation-error: 4.0.2(zod@3.25.76)
transitivePeerDependencies:
- supports-color
@@ -10756,6 +10851,8 @@ snapshots:
globals@14.0.0: {}
globals@16.4.0: {}
globalthis@1.0.4:
dependencies:
define-properties: 1.2.1
@@ -10772,6 +10869,18 @@ snapshots:
section-matter: 1.0.0
strip-bom-string: 1.0.0
happy-dom@20.6.1:
dependencies:
'@types/node': 22.19.10
'@types/whatwg-mimetype': 3.0.2
'@types/ws': 8.18.1
entities: 6.0.1
whatwg-mimetype: 3.0.0
ws: 8.19.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
has-bigints@1.1.0: {}
has-flag@3.0.0: {}
@@ -12564,6 +12673,8 @@ snapshots:
dependencies:
fast-deep-equal: 2.0.1
react-refresh@0.18.0: {}
react@19.2.4: {}
readdirp@3.6.0:
@@ -13636,7 +13747,7 @@ snapshots:
tsx: 4.21.0
yaml: 2.8.2
vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.10)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
dependencies:
'@vitest/expect': 4.0.18
'@vitest/mocker': 4.0.18(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))
@@ -13662,6 +13773,7 @@ snapshots:
'@opentelemetry/api': 1.9.0
'@types/node': 22.19.10
'@vitest/ui': 4.0.18(vitest@4.0.18)
happy-dom: 20.6.1
jsdom: 27.4.0
transitivePeerDependencies:
- jiti
@@ -13731,6 +13843,8 @@ snapshots:
whatwg-fetch@3.6.20: {}
whatwg-mimetype@3.0.0: {}
whatwg-mimetype@4.0.0: {}
whatwg-mimetype@5.0.0: {}
@@ -13917,12 +14031,10 @@ snapshots:
yoga-layout@3.2.1: {}
zod-validation-error@4.0.2(zod@4.3.6):
zod-validation-error@4.0.2(zod@3.25.76):
dependencies:
zod: 4.3.6
zod: 3.25.76
zod@3.25.76: {}
zod@4.3.6: {}
zwitch@2.0.4: {}

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-undef */
module.exports = {
plugins: {
'@tailwindcss/postcss': {},

View File

@@ -5,6 +5,10 @@ const dsn = process.env.SENTRY_DSN;
Sentry.init({
dsn,
enabled: Boolean(dsn),
tracesSampleRate: 0,
});
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});

View File

@@ -5,6 +5,10 @@ const dsn = process.env.SENTRY_DSN;
Sentry.init({
dsn,
enabled: Boolean(dsn),
tracesSampleRate: 0,
});
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});

View File

@@ -2,8 +2,8 @@
@theme {
--font-sans:
'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue',
Arial, sans-serif;
var(--font-inter), system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, sans-serif;
--font-heading: 'Inter', system-ui, sans-serif;
--font-body: 'Inter', system-ui, sans-serif;

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-undef */
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
@@ -147,7 +148,7 @@ module.exports = {
},
plugins: [
// Custom plugin for responsive utilities
function({ addUtilities }) {
function ({ addUtilities }) {
const newUtilities = {
// Touch target utilities
'.touch-target': {

4
typecheck_output.txt Normal file
View File

@@ -0,0 +1,4 @@
> klz-cables-nextjs@1.0.0 typecheck /Users/marcmintel/Projects/klz-2026
> tsc --noEmit

25
vitest.config.mts Normal file
View File

@@ -0,0 +1,25 @@
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': __dirname,
'next/server': 'next/server.js',
},
},
ssr: {
noExternal: ['@mintel/next-utils', 'next-intl'],
},
test: {
environment: 'happy-dom',
globals: true,
exclude: ['**/node_modules/**', '**/.next/**', '**/dist/**'],
include: ['**/*.test.{ts,tsx}'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
},
},
});