Compare commits

...

94 Commits

Author SHA1 Message Date
698141f70b fix: eslint and tests
All checks were successful
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 7s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m31s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 3m53s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 31s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Successful in 5m24s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-06 22:46:53 +01:00
e179e8162c refactor: Standardize Umami analytics environment variables to non-public names with fallbacks to NEXT_PUBLIC_ prefixed versions.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 7s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Failing after 1m31s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 3m51s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-06 22:35:49 +01:00
259d712105 fix: Update Gitea workflow to use environment-specific mail and Directus secrets, and refactor Directus branding script for improved CSS management and button styling.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 8s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Has been cancelled
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been cancelled
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been cancelled
Build & Deploy KLZ Cables / 🔔 Notifications (push) Has been cancelled
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Has been cancelled
2026-02-06 21:43:53 +01:00
0178e828d6 chore: ensure traefik network labels are applied
All checks were successful
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 8s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Has been skipped
Build & Deploy KLZ Cables / 🏗️ Build App (push) Has been skipped
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 1s
2026-02-06 19:19:40 +01:00
e3f7344daf chore: traefik labels
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 10s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 26s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Failing after 1m34s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-06 18:36:34 +01:00
21a7b0ade2 feat: Implement replyTo for contact form emails and refine success/error message layout. 2026-02-06 18:24:58 +01:00
d027fbeac2 chore: Standardize error message extraction in contact form actions and mailer, and add Directus restart to the Directus sync script.
All checks were successful
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 14s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Has been skipped
Build & Deploy KLZ Cables / 🏗️ Build App (push) Has been skipped
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-06 17:55:43 +01:00
8a751998eb feat: Add COOKIE_DOMAIN to .env and improve project name fallback logic in sync-directus script
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 11s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m40s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Failing after 17m34s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been cancelled
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been cancelled
Build & Deploy KLZ Cables / 🔔 Notifications (push) Has been cancelled
2026-02-06 17:37:53 +01:00
48c3e1d013 chore: rename scripts 2026-02-06 17:19:57 +01:00
3df4b44b8d fix: center success and error messages within the RequestQuoteForm component.
All checks were successful
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 7s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m31s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 3m53s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 34s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Successful in 9m46s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-06 17:08:39 +01:00
07e0f237b9 feat: Introduce metadata-only retrieval functions for posts, products, and pages to optimize sitemap generation.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 35s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m30s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 4m39s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 46s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Has been cancelled
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been cancelled
2026-02-06 17:01:25 +01:00
57a3944301 feat: Update gatekeeper image to latest, add new environment variables, and allow gatekeeper's own paths to prevent redirect loops.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 9s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 18s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m45s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 27s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Failing after 4m9s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 1s
2026-02-06 15:26:21 +01:00
5fe0a8d83e chore: Hardcode 'compress' Traefik middleware for Directus, removing the dynamic AUTH_MIDDLEWARE variable.
All checks were successful
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 7s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Has been skipped
Build & Deploy KLZ Cables / 🏗️ Build App (push) Has been skipped
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Has been skipped
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-06 14:31:39 +01:00
8062d33f35 fix: Validate server-only environment variables exclusively on the server to prevent browser validation errors.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 12s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m39s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 29s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 3m56s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 47s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Has been cancelled
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been cancelled
2026-02-06 14:19:29 +01:00
ebe67afd73 feat: Broaden middleware's internal URL correction to include hosts like klz-app and localhost, and update Varnish's health check URL to /health.
All checks were successful
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 7s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m31s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 23s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 5m7s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 50s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Successful in 9m7s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-06 13:23:26 +01:00
b74f6b6f9e fix(middleware): strip port 3000 from reconstructed URL to prevent hydration mismatch
All checks were successful
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 7s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m30s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 21s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 4m38s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 50s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Successful in 7m12s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-06 12:35:33 +01:00
24eea9a2fe fix(middleware): resolve 0.0.0.0 hydration issues and add header logging
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 7s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m29s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 20s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 4m32s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 48s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Has been cancelled
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been cancelled
2026-02-06 12:22:32 +01:00
c70288bba7 feat: Enhance Directus URL resolution for internal and proxy paths, and adjust Traefik host variable interpolation.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 39s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m33s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 22s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 4m47s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 48s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Failing after 3m50s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-06 11:29:10 +01:00
d438dbdc9d feat: Add Varnish backend health check to deploy workflow and set APP_VERSION in Varnish service.
All checks were successful
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 10s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m36s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 21s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 4m41s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 45s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Successful in 4m18s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 1s
2026-02-06 00:21:39 +01:00
e0c4aaf298 feat: Add PROJECT_NAME and COOKIE_DOMAIN environment variables to the deploy workflow.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 7s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m32s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 24s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 4m33s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 44s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Failing after 3m56s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-05 23:58:03 +01:00
f44487eeac feat: Add configurable cookie domain to gatekeeper and enhance Varnish backend configuration with health probes and increased timeouts.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 11s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m41s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 28s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 3m56s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 46s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Failing after 3m7s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 3s
2026-02-05 23:29:34 +01:00
a82b95a28f feat: Adjust backend timeouts, add /cms to cache bypass, and include X-Backend-IP in responses.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 9s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m33s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 20s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 4m29s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 45s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Failing after 2m7s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-05 22:41:26 +01:00
ab688a3dab refactor: rename app service to klz-app and establish a new internal Docker network for service communication.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 34s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m31s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 20s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 9m37s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 1m41s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Failing after 3m6s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-05 21:54:46 +01:00
a0ce37708e fix(ci): fix shell compatibility and scp order in deployment workflow
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 6s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m29s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 23s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 4m34s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 44s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Failing after 2m21s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-05 19:25:53 +01:00
0379d1f05d fix: restore critical build/deploy fixes and standardize destructive UI state
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 9s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m35s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 21s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 4m38s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Failing after 12s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-05 14:16:28 +01:00
50347d049d style: add danger button variant and fix contact form error state
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 6s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 2m3s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 33s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 3m59s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Failing after 9s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 3s
2026-02-05 14:08:06 +01:00
9678181927 fix: copy varnish config and ensure server directories in deployment
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 11s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m45s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 33s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 3m54s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Failing after 9s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 1s
2026-02-05 14:03:36 +01:00
3ffaafefe5 build: use --legacy-peer-deps in CI workflow for peer dependency resolution
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 7s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m29s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 21s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 4m37s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Failing after 36s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-05 13:40:21 +01:00
e5bf8c861c build: fix peer dependency conflict in Dockerfile
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 10s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Failing after 13s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 21s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 11m56s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-05 13:24:28 +01:00
651e14d665 build: update @mintel/mail to 1.2.3
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 7s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Failing after 12s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 20s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Failing after 35s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-05 13:12:54 +01:00
580cd6789c feat: Allow skipping MAIL_HOST environment variable validation during build processes using SKIP_RUNTIME_ENV_VALIDATION.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 8s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m35s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 20s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Failing after 5m36s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-05 12:48:56 +01:00
db4cf354ff feat: Add conditional MAIL_HOST validation, lazy-load mailer, and update Gitea workflow to use vars for mail and Sentry environment variables.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 14s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m44s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Failing after 1m54s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 31s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-05 12:39:51 +01:00
e8957e0672 feat: Add Varnish caching service and configuration, adjusting Traefik routing and implementing a rate limit middleware.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 7s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m34s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 21s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Failing after 4m59s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-05 01:54:17 +01:00
7ef0bca9f6 feat: Implement dual email sending for contact form submissions, including a customer confirmation and internal notification, by refactoring email rendering to accept pre-rendered HTML. 2026-02-05 01:54:01 +01:00
198944649a feat: Add new 1x1200/35 product configuration to data files and datasheets, including scripts to manage its technical and ampacity values.
All checks were successful
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 7s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m37s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 21s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 4m7s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 44s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Successful in 1m18s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-04 17:52:21 +01:00
6aa741ab0a refactor: remove npm cache restoration steps and update Gotify failure notification conditions
All checks were successful
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 8s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m43s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 22s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 4m21s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 44s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Successful in 6m6s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-02 18:43:21 +01:00
f69952a5da refactor: Enable pino-pretty transport exclusively for development environments.
All checks were successful
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 7s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 4m6s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 22s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 11m52s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 42s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Successful in 4m40s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 1s
2026-02-02 17:43:54 +01:00
81af9bf3dd feat: Add Gotify notification service and integrate it with error reporting and contact form actions, making error reporting methods asynchronous.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 7s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 4m3s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 20s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been cancelled
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been cancelled
Build & Deploy KLZ Cables / 🔔 Notifications (push) Has been cancelled
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Has been cancelled
2026-02-02 17:38:15 +01:00
f1b617e967 feat: add contact form submission status translations 2026-02-02 17:34:18 +01:00
d6be9beebf fix: directus
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 8s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 6m36s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 23s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 11m59s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 43s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Failing after 7m22s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-02 16:46:26 +01:00
0a797260e3 feat: Introduce NEXT_PUBLIC_TARGET build argument and abstract server-side error reporting to a dedicated service.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 7s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 4m2s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 11m56s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 43s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Failing after 2m7s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 1s
2026-02-02 15:27:04 +01:00
2a4cc76292 feat: Disable Lighthouse CI report uploading and generate a local summary table of PageSpeed scores instead. 2026-02-02 15:20:33 +01:00
f87eb27f41 ci: tighten docker maintenance window and fix yaml syntax
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 19s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m37s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 22s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 4m23s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 43s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Failing after 2m23s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-02 14:57:14 +01:00
acd86099e5 ci: add proactive docker cleanup and tighten prune filters
Some checks failed
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Has been cancelled
Build & Deploy KLZ Cables / 🏗️ Build App (push) Has been cancelled
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Has been cancelled
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been cancelled
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been cancelled
Build & Deploy KLZ Cables / 🔔 Notifications (push) Has been cancelled
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Has been cancelled
2026-02-02 14:49:15 +01:00
5ab9791c72 ci: use shallow clones to resolve 'No space left on device' errors
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 7s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Failing after 10s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Failing after 9s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been cancelled
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been cancelled
Build & Deploy KLZ Cables / 🔔 Notifications (push) Has been cancelled
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Has been cancelled
2026-02-02 14:45:15 +01:00
8152ccd5df ci: standardize runner containers and harden script output logic
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Failing after 57s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Has been skipped
Build & Deploy KLZ Cables / 🏗️ Build App (push) Has been skipped
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Has been skipped
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-02 14:35:49 +01:00
8eeb571c2d feat: Add deployment target configuration for environment-specific settings, Sentry integration, and CMS notice logic.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Failing after 56s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Has been skipped
Build & Deploy KLZ Cables / 🏗️ Build App (push) Has been skipped
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Has been skipped
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-02 14:31:03 +01:00
b1854d5255 ci: optimize builds with parallelism, change detection, and registry caching
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 22s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m32s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Failing after 5m24s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Failing after 12s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 1s
2026-02-02 14:18:01 +01:00
7f4f970a38 feat: Configure deploy workflow concurrency to dynamically group by environment and enable in-progress cancellation. 2026-02-02 14:17:04 +01:00
e5908c757c ci: ensure docker cli is available in build job
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 27s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m32s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 5m7s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 43s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Failing after 7m52s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-02 14:11:09 +01:00
70efb0c593 ci: update Gitea deployment workflow configuration
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 25s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Failing after 34s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m34s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-02 14:04:05 +01:00
479a36f1d0 feat: Improve CMS health check error reporting and limit connectivity notice display to developer and debug environments.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 22s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m32s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been cancelled
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been cancelled
Build & Deploy KLZ Cables / 🔔 Notifications (push) Has been cancelled
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Has been cancelled
2026-02-02 13:42:20 +01:00
372a0c5cfa feat: Add new logo and configure application icons in metadata and manifest.
All checks were successful
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m33s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 2m59s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 45s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Successful in 4m56s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-02 13:08:19 +01:00
42b06e1ef8 refactor: Replace hardcoded domain with SITE_URL constant across metadata and schema definitions for improved configurability.
All checks were successful
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 20s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m30s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 3m14s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 42s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Successful in 5m0s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-02 12:10:09 +01:00
b25fdd877a fix(ci): use correct GPG key ID and multi-method fetch for Chromium PPA on Ubuntu Noble
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m31s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 2m47s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 39s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Failing after 2m36s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-02 01:21:16 +01:00
dd23310ac4 fix(ci): prioritize PPA chromium over snap wrapper and pass explicit chrome path to lhci
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m30s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 2m52s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 40s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Failing after 1m6s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-01 22:50:33 +01:00
f6f28a4529 fix(ci): robust chromium install for arm64 with os detection and gpg retries
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m32s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 2m50s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 40s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Failing after 2m32s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-01 22:30:22 +01:00
fc3635db86 fix(ci): replace deprecated apt-key with gpg dearmor and use robust keyserver fetch
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 20s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m31s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 2m51s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 42s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Failing after 1m5s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-01 21:41:21 +01:00
fc000353a9 feat: Add support for an internal Directus URL for server-side communication and enhance the health check with schema validation for the products collection.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m33s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 2m53s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 40s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Failing after 1m5s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 1s
2026-02-01 21:22:30 +01:00
73c32c6d31 fix(ci): robust chromium installation to avoid snap issues on arm64
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m30s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 2m51s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 39s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Failing after 1m6s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-01 19:46:41 +01:00
381e0b121f fix(ci): install chromium instead of google-chrome-stable for arm64 support
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m29s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 3m22s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 39s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Failing after 2m28s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-01 18:13:51 +01:00
829c074c7f fix(ci): use bash and explicit if conditions to fix skip logic
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 20s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m34s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 7m23s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 40s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Failing after 1m10s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-01 17:58:18 +01:00
9189e813b2 fix(ci): skip build and deploy jobs when target is skip (chore commits) 2026-02-01 17:53:11 +01:00
249313cc37 chore: Configure Husky, Commitlint, and Lint-staged to enforce Git commit message conventions and run pre-commit checks.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Has been skipped
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Failing after 6s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 1s
2026-02-01 17:41:00 +01:00
8232971419 ci: Update Gitea deploy workflow.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m33s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been cancelled
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been cancelled
Build & Deploy KLZ Cables / 🔔 Notifications (push) Has been cancelled
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Has been cancelled
2026-02-01 17:37:38 +01:00
f41260e1db feat: implement automated Lighthouse CI testing for sitemap URLs with dedicated configuration and scripts. 2026-02-01 17:35:31 +01:00
950ef9d463 refactor: Replace custom X-Auth-Redirect header with standard HTTP redirect for unauthorized access.
All checks were successful
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 20s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m25s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 2m47s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 30s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-01 16:52:57 +01:00
fcb3169d04 feat: Implement a gatekeeper service for access control and add CMS health monitoring with a connectivity notice.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m24s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Failing after 3m8s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-01 16:27:52 +01:00
9e87720494 chore: update .env.example with current environment variable examples.
All checks were successful
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 20s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m24s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 2m58s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 29s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-01 15:16:46 +01:00
6a403f47a0 feat: Add script and package.json commands to synchronize Directus data and uploads between local and staging environments.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 20s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m23s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 5m32s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Failing after 8s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-01 14:55:38 +01:00
2d8df53e36 refactor: Replace hardcoded Traefik service names with a dynamic project name variable.
All checks were successful
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m25s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 2m52s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 29s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-01 14:26:44 +01:00
6f49dbc56c ci: Update Gitea deploy workflow configuration.
All checks were successful
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 20s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m24s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 2m55s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 30s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 1s
2026-02-01 14:17:09 +01:00
ad2a477636 perf: Add Docker build cache mounts for npm ci and npm run build commands.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m24s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 5m48s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Failing after 12s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 3s
2026-02-01 14:00:44 +01:00
77a1067820 build: include .next/cache in Docker build context.
All checks were successful
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m22s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 5m42s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 44s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-01 13:43:42 +01:00
ea3076b4ec fix: Remove quotes from environment variables in Traefik host rules within docker-compose.yml.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m28s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been cancelled
Build & Deploy KLZ Cables / 🔔 Notifications (push) Has been cancelled
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Has been cancelled
2026-02-01 13:38:35 +01:00
17fe0d7107 fix: Update Traefik host rules in docker-compose.yml to use properly quoted environment variables.
All checks were successful
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 2m32s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 4m26s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 49s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 1s
2026-02-01 13:26:25 +01:00
38b512973b ci: update Gitea deploy workflow configuration.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m17s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 4m52s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Has been cancelled
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been cancelled
2026-02-01 13:18:06 +01:00
4f73838c21 ci: update Gitea deploy workflow configuration.
Some checks failed
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Has been cancelled
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Has been cancelled
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been cancelled
Build & Deploy KLZ Cables / 🔔 Notifications (push) Has been cancelled
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Has been cancelled
2026-02-01 13:15:34 +01:00
2f8ce42409 refactor: Remove automatic CMS bootstrapping and wait commands from the dev script.
All checks were successful
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m17s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 3m28s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 43s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 1s
2026-02-01 13:04:11 +01:00
cf7af73b72 feat: Automate Directus CMS bootstrapping in the dev script and update gitignore rules.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 20s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m15s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 3m32s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Failing after 1m0s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-01 12:47:46 +01:00
d526bfe56f chore: Remove various unused uploaded files from Directus. 2026-02-01 12:47:30 +01:00
4cb7d438a0 feat: make Directus CMS URL configurable via environment variable with a default.
All checks were successful
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m17s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 3m42s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 56s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-01 11:17:24 +01:00
03e597442b feat: Centralize OG image font loading and sizing, simplify product page OG generation, and refine template styling.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m36s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Failing after 1m31s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 3s
2026-02-01 11:05:37 +01:00
5f9ee7d976 chore: resolve merge conflict in deploy workflow and sync docker-compose
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 24s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Failing after 1m16s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Has been skipped
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-01 02:18:25 +01:00
c1304403a1 ci cd
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 22s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Failing after 1m34s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Has been skipped
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 1s
2026-01-31 22:32:32 +01:00
5036c5fe28 deploy
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 4m13s
2026-01-31 21:10:12 +01:00
50a524c515 gitea
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 3m54s
2026-01-31 19:21:53 +01:00
57886a01d6 traefik
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m58s
2026-01-31 18:36:34 +01:00
c89bd8e80f staging deploy
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m54s
2026-01-31 18:07:28 +01:00
9c54322654 datasheet download
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 4m1s
2026-01-31 10:40:17 +01:00
8a80eb7b9a og
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m55s
2026-01-31 10:35:07 +01:00
c1773a7072 datasheets as downloads
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m59s
2026-01-31 10:29:39 +01:00
33ed13d255 og
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 3m28s
2026-01-31 10:25:25 +01:00
0f5811edb9 og
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 1m47s
2026-01-31 10:21:24 +01:00
928 changed files with 11425 additions and 8837 deletions

View File

@@ -1,5 +1,6 @@
node_modules
.next
!.next/cache
.git
.DS_Store
.env

9
.env
View File

@@ -1,7 +1,7 @@
# Application
NODE_ENV=production
NEXT_PUBLIC_BASE_URL=https://klz-cables.com
UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js
UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
NEXT_PUBLIC_UMAMI_WEBSITE_ID=59a7db94-0100-4c7e-98ef-99f45b17f9c3
SENTRY_DSN=https://c10957d0182245b1a2a806ac2d34a197@errors.infra.mintel.me/1
LOG_LEVEL=info
@@ -28,4 +28,9 @@ DIRECTUS_ADMIN_EMAIL=marc@mintel.me
DIRECTUS_ADMIN_PASSWORD=Tim300493.
DIRECTUS_DB_NAME=directus
DIRECTUS_DB_USER=directus
DIRECTUS_DB_PASSWORD=directus
# Local Development
PROJECT_NAME=klz-cables
TRAEFIK_HOST=klz.localhost
DIRECTUS_HOST=cms.klz.localhost
GATEKEEPER_PASSWORD=klz2026
COOKIE_DOMAIN=localhost

View File

@@ -10,13 +10,18 @@
# ────────────────────────────────────────────────────────────────────────────
NODE_ENV=development
NEXT_PUBLIC_BASE_URL=http://localhost:3000
# TARGET is used to differentiate between environments (testing, staging, production)
# NEXT_PUBLIC_TARGET makes this information available to the frontend
NEXT_PUBLIC_TARGET=development
# TARGET is used server-side
TARGET=development
# ────────────────────────────────────────────────────────────────────────────
# Analytics (Umami)
# ────────────────────────────────────────────────────────────────────────────
# Optional: Leave empty to disable analytics
NEXT_PUBLIC_UMAMI_WEBSITE_ID=
NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js
UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
# ────────────────────────────────────────────────────────────────────────────
# Error Tracking (GlitchTip/Sentry)
@@ -39,24 +44,23 @@ MAIL_RECIPIENTS=info@klz-cables.com
# Logging
# ────────────────────────────────────────────────────────────────────────────
LOG_LEVEL=info
GATEKEEPER_PASSWORD=klz2026
SENTRY_DSN=
# For Directus Error Tracking
# SENTRY_ENVIRONMENT is set automatically by CI
# ────────────────────────────────────────────────────────────────────────────
# Varnish Cache (Docker only)
# Deployment Configuration (CI/CD only)
# ────────────────────────────────────────────────────────────────────────────
VARNISH_CACHE_SIZE=256m
# These are typically set by the CI/CD workflow
IMAGE_TAG=latest
TRAEFIK_HOST=klz-cables.com
ENV_FILE=.env
# ────────────────────────────────────────────────────────────────────────────
# Strapi CMS
# Varnish Configuration
# ────────────────────────────────────────────────────────────────────────────
STRAPI_DATABASE_NAME=strapi
STRAPI_DATABASE_USERNAME=strapi
STRAPI_DATABASE_PASSWORD=strapi
STRAPI_URL=http://localhost:1337
APP_KEYS=toBeModified1,toBeModified2
API_TOKEN_SALT=tobemodified
ADMIN_JWT_SECRET=tobemodified
TRANSFER_TOKEN_SALT=tobemodified
JWT_SECRET=tobemodified
VARNISH_CACHE_SIZE=256M
# ============================================================================
# IMPORTANT NOTES
@@ -74,7 +78,11 @@ JWT_SECRET=tobemodified
# ──────────────────
# 1. Build-time: Only NEXT_PUBLIC_* vars are needed (via --build-arg)
# 2. Runtime: All vars are loaded from .env file on the server
# 3. The .env file should exist at: /home/deploy/sites/klz-cables.com/.env
# 3. Branch Deployments:
# - main branch uses .env.prod
# - staging branch uses .env.staging
# - CI/CD supports STAGING_ prefix for all secrets to override defaults
# - TRAEFIK_HOST is automatically derived from NEXT_PUBLIC_BASE_URL
#
# Security:
# ─────────

View File

@@ -13,7 +13,7 @@ NEXT_PUBLIC_BASE_URL=https://klz-cables.com
# Analytics (Umami)
NEXT_PUBLIC_UMAMI_WEBSITE_ID=
NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js
UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
# Error Tracking (GlitchTip/Sentry)
SENTRY_DSN=

View File

@@ -1,12 +1,16 @@
{
"extends": ["next/core-web-vitals", "next/typescript"],
"extends": ["next/core-web-vitals", "next/typescript", "prettier"],
"rules": {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_"
}
],
"@typescript-eslint/no-require-imports": "off",
"prefer-const": "warn",
"react/no-unescaped-entities": "off",
"@next/next/no-img-element": "warn"
}
}

32
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,32 @@
name: CI - Lint, Typecheck & Test
on:
push:
branches-ignore:
- main
pull_request:
jobs:
quality-assurance:
runs-on: docker
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: 🔍 Lint
run: npm run lint
- name: 🏗️ Typecheck
run: npm run typecheck
- name: 🧪 Test
run: npm run test

View File

@@ -2,195 +2,499 @@ name: Build & Deploy KLZ Cables
on:
push:
branches: [main]
branches:
- main
tags:
- 'v*'
workflow_dispatch:
inputs:
skip_long_checks:
description: 'Skip tests? (true/false)'
required: false
default: 'false'
concurrency:
group: ${{ github.workflow }}-${{ (github.ref_type == 'tag' && !contains(github.ref_name, '-')) && 'prod' || (github.ref_type == 'tag' && 'staging' || 'testing') }}
cancel-in-progress: true
jobs:
build-and-deploy:
# ──────────────────────────────────────────────────────────────────────────────
# JOB 1: Prepare & Determine Environment
# ──────────────────────────────────────────────────────────────────────────────
prepare:
name: 🔍 Prepare Environment
runs-on: docker
outputs:
target: ${{ steps.determine.outputs.target }}
image_tag: ${{ steps.determine.outputs.image_tag }}
env_file: ${{ steps.determine.outputs.env_file }}
traefik_host: ${{ steps.determine.outputs.traefik_host }}
next_public_base_url: ${{ steps.determine.outputs.next_public_base_url }}
directus_url: ${{ steps.determine.outputs.directus_url }}
directus_host: ${{ steps.determine.outputs.directus_host }}
project_name: ${{ steps.determine.outputs.project_name }}
is_prod: ${{ steps.determine.outputs.is_prod }}
gotify_title: ${{ steps.determine.outputs.gotify_title }}
gotify_priority: ${{ steps.determine.outputs.gotify_priority }}
short_sha: ${{ steps.determine.outputs.short_sha }}
commit_msg: ${{ steps.determine.outputs.commit_msg }}
container:
image: catthehacker/ubuntu:act-latest
steps:
# ═══════════════════════════════════════════════════════════════════════════════
# LOGGING: Workflow Start - Full Transparency
# ═══════════════════════════════════════════════════════════════════════════════
- name: 📋 Log Workflow Start
- name: 🧹 Maintenance (High Density Cleanup)
shell: bash
run: |
echo "🚀 Starting deployment for ${{ github.repository }} (${{ github.ref }})"
echo " • Commit: ${{ github.sha }}"
echo " • Timestamp: $(date -u +'%Y-%m-%d %H:%M:%S UTC')"
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:
fetch-depth: 2
# ═══════════════════════════════════════════════════════════════════════════════
# LOGGING: Registry Login Phase
# ═══════════════════════════════════════════════════════════════════════════════
- name: 🔐 Login to private registry
- name: 🔍 Environment & Version ermitteln
id: determine
shell: bash
run: |
TAG="${{ github.ref_name }}"
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-9)
IMAGE_TAG="sha-${SHORT_SHA}"
COMMIT_MSG=$(git log -1 --pretty=%s || echo "No commit message available")
if [[ "${{ github.ref_type }}" == "branch" && "$TAG" == "main" ]]; then
if [[ "$COMMIT_MSG" =~ ^chore: ]]; then
TARGET="skip"
GOTIFY_TITLE=" Skip Deploy (Chore)"
GOTIFY_PRIORITY=2
else
TARGET="testing"
IMAGE_TAG="main-${SHORT_SHA}"
ENV_FILE=".env.testing"
TRAEFIK_HOST="testing.klz-cables.com"
NEXT_PUBLIC_BASE_URL="https://testing.klz-cables.com"
DIRECTUS_URL="https://cms.testing.klz-cables.com"
DIRECTUS_HOST="cms.testing.klz-cables.com"
PROJECT_NAME="klz-cables-testing"
IS_PROD="false"
GOTIFY_TITLE="🧪 Testing-Deploy"
GOTIFY_PRIORITY=4
fi
elif [[ "${{ github.ref_type }}" == "tag" ]]; then
if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
TARGET="production"
IMAGE_TAG="$TAG"
ENV_FILE=".env.prod"
TRAEFIK_HOST="klz-cables.com, www.klz-cables.com"
NEXT_PUBLIC_BASE_URL="https://klz-cables.com"
DIRECTUS_URL="https://cms.klz-cables.com"
DIRECTUS_HOST="cms.klz-cables.com"
PROJECT_NAME="klz-cables-prod"
IS_PROD="true"
GOTIFY_TITLE="🚀 Production-Release"
GOTIFY_PRIORITY=6
elif [[ "$TAG" =~ -rc || "$TAG" =~ -beta || "$TAG" =~ -alpha ]]; then
TARGET="staging"
IMAGE_TAG="$TAG"
ENV_FILE=".env.staging"
TRAEFIK_HOST="staging.klz-cables.com"
NEXT_PUBLIC_BASE_URL="https://staging.klz-cables.com"
DIRECTUS_URL="https://cms.staging.klz-cables.com"
DIRECTUS_HOST="cms.staging.klz-cables.com"
PROJECT_NAME="klz-cables-staging"
IS_PROD="false"
GOTIFY_TITLE="🧪 Staging-Deploy (Pre-Release)"
GOTIFY_PRIORITY=5
else
TARGET="skip"
GOTIFY_TITLE="❓ Unbekannter Tag"
GOTIFY_PRIORITY=3
fi
else
TARGET="skip"
fi
{
echo "target=$TARGET"
echo "image_tag=$IMAGE_TAG"
echo "env_file=$ENV_FILE"
echo "traefik_host=$TRAEFIK_HOST"
echo "next_public_base_url=$NEXT_PUBLIC_BASE_URL"
echo "directus_url=$DIRECTUS_URL"
echo "directus_host=$DIRECTUS_HOST"
echo "project_name=$PROJECT_NAME"
echo "is_prod=$IS_PROD"
echo "gotify_title=$GOTIFY_TITLE"
echo "gotify_priority=$GOTIFY_PRIORITY"
echo "short_sha=$SHORT_SHA"
echo "commit_msg=$COMMIT_MSG"
} >> "$GITHUB_OUTPUT"
# ──────────────────────────────────────────────────────────────────────────────
# JOB 2: Quality Assurance (Lint & Test)
# ──────────────────────────────────────────────────────────────────────────────
qa:
name: 🧪 Quality Assurance
needs: prepare
if: needs.prepare.outputs.target != 'skip'
runs-on: docker
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: 🧪 Run Checks in Parallel
if: github.event.inputs.skip_long_checks != 'true'
run: |
npm run lint &
LINT_PID=$!
npm run typecheck &
TYPE_PID=$!
npm run test &
TEST_PID=$!
# Wait for all and fail if any fail
wait $LINT_PID || exit 1
wait $TYPE_PID || exit 1
wait $TEST_PID || exit 1
# ──────────────────────────────────────────────────────────────────────────────
# JOB 3: Build & Push Docker Image
# ──────────────────────────────────────────────────────────────────────────────
build-app:
name: 🏗️ Build App
needs: prepare
if: ${{ needs.prepare.outputs.target != 'skip' }}
runs-on: docker
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: 🐳 Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 🔐 Registry Login
run: |
echo "🔐 Authenticating with registry.infra.mintel.me..."
echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
# ═══════════════════════════════════════════════════════════════════════════════
# LOGGING: Build Phase
# ═══════════════════════════════════════════════════════════════════════════════
- name: 🏗️ Build Docker image
- name: 🏗️ App bauen & pushen
env:
IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }}
TARGET: ${{ needs.prepare.outputs.target }}
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_base_url }}
NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ needs.prepare.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.TESTING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID) }}
UMAMI_API_ENDPOINT: ${{ needs.prepare.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.TESTING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL) }}
DIRECTUS_URL: ${{ needs.prepare.outputs.directus_url }}
run: |
echo "🏗️ Building Docker image (linux/arm64)..."
docker buildx build \
--pull \
--platform linux/arm64 \
--build-arg NEXT_PUBLIC_BASE_URL="${{ secrets.NEXT_PUBLIC_BASE_URL }}" \
--build-arg NEXT_PUBLIC_UMAMI_WEBSITE_ID="${{ secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}" \
--build-arg NEXT_PUBLIC_UMAMI_SCRIPT_URL="${{ secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL }}" \
-t registry.infra.mintel.me/mintel/klz-cables.com:latest \
--build-arg NEXT_PUBLIC_BASE_URL="$NEXT_PUBLIC_BASE_URL" \
--build-arg NEXT_PUBLIC_UMAMI_WEBSITE_ID="$NEXT_PUBLIC_UMAMI_WEBSITE_ID" \
--build-arg UMAMI_API_ENDPOINT="$UMAMI_API_ENDPOINT" \
--build-arg NEXT_PUBLIC_TARGET="$TARGET" \
--build-arg DIRECTUS_URL="$DIRECTUS_URL" \
-t registry.infra.mintel.me/mintel/klz-cables.com:$IMAGE_TAG \
--cache-from type=registry,ref=registry.infra.mintel.me/mintel/klz-cables.com:buildcache \
--cache-to type=registry,ref=registry.infra.mintel.me/mintel/klz-cables.com:buildcache,mode=max \
--push .
# ═══════════════════════════════════════════════════════════════════════════════
# LOGGING: Deployment Phase
# ═══════════════════════════════════════════════════════════════════════════════
- name: 🚀 Deploy to production server
# ──────────────────────────────────────────────────────────────────────────────
# JOB 4: Deploy via SSH
# ──────────────────────────────────────────────────────────────────────────────
deploy:
name: 🚀 Deploy
needs: [prepare, build-app, qa]
if: ${{ needs.prepare.outputs.target != 'skip' }}
runs-on: docker
container:
image: catthehacker/ubuntu:act-latest
env:
TARGET: ${{ needs.prepare.outputs.target }}
IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }}
ENV_FILE: ${{ needs.prepare.outputs.env_file }}
TRAEFIK_HOST: ${{ needs.prepare.outputs.traefik_host }}
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_base_url }}
NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ needs.prepare.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.TESTING_NEXT_PUBLIC_UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID) }}
UMAMI_API_ENDPOINT: ${{ needs.prepare.outputs.target == 'production' && secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.TESTING_NEXT_PUBLIC_UMAMI_SCRIPT_URL || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL) }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN || vars.SENTRY_DSN || (needs.prepare.outputs.target == 'production' && secrets.SENTRY_DSN || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_SENTRY_DSN || secrets.TESTING_SENTRY_DSN || secrets.SENTRY_DSN)) }}
MAIL_HOST: ${{ secrets.MAIL_HOST || vars.MAIL_HOST || (needs.prepare.outputs.target == 'production' && (secrets.MAIL_HOST || vars.MAIL_HOST) || (needs.prepare.outputs.target == 'staging' && (secrets.STAGING_MAIL_HOST || vars.STAGING_MAIL_HOST) || (secrets.TESTING_MAIL_HOST || vars.TESTING_MAIL_HOST) || (secrets.MAIL_HOST || vars.MAIL_HOST))) }}
MAIL_PORT: ${{ secrets.MAIL_PORT || vars.MAIL_PORT || (needs.prepare.outputs.target == 'production' && (secrets.MAIL_PORT || vars.MAIL_PORT) || (needs.prepare.outputs.target == 'staging' && (secrets.STAGING_MAIL_PORT || vars.STAGING_MAIL_PORT) || (secrets.TESTING_MAIL_PORT || vars.TESTING_MAIL_PORT) || (secrets.MAIL_PORT || vars.MAIL_PORT))) }}
MAIL_USERNAME: ${{ secrets.MAIL_USERNAME || vars.MAIL_USERNAME || (needs.prepare.outputs.target == 'production' && (secrets.MAIL_USERNAME || vars.MAIL_USERNAME) || (needs.prepare.outputs.target == 'staging' && (secrets.STAGING_MAIL_USERNAME || vars.STAGING_MAIL_USERNAME) || (secrets.TESTING_MAIL_USERNAME || vars.TESTING_MAIL_USERNAME) || (secrets.MAIL_USERNAME || vars.MAIL_USERNAME))) }}
MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD || (needs.prepare.outputs.target == 'production' && secrets.MAIL_PASSWORD || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_MAIL_PASSWORD || secrets.TESTING_MAIL_PASSWORD || secrets.MAIL_PASSWORD)) }}
MAIL_FROM: ${{ secrets.MAIL_FROM || vars.MAIL_FROM || (needs.prepare.outputs.target == 'production' && (secrets.MAIL_FROM || vars.MAIL_FROM) || (needs.prepare.outputs.target == 'staging' && (secrets.STAGING_MAIL_FROM || vars.STAGING_MAIL_FROM) || (secrets.TESTING_MAIL_FROM || vars.TESTING_MAIL_FROM) || (secrets.MAIL_FROM || vars.MAIL_FROM))) }}
MAIL_RECIPIENTS: ${{ secrets.MAIL_RECIPIENTS || vars.MAIL_RECIPIENTS || (needs.prepare.outputs.target == 'production' && (secrets.MAIL_RECIPIENTS || vars.MAIL_RECIPIENTS) || (needs.prepare.outputs.target == 'staging' && (secrets.STAGING_MAIL_RECIPIENTS || vars.STAGING_MAIL_RECIPIENTS) || (secrets.TESTING_MAIL_RECIPIENTS || vars.TESTING_MAIL_RECIPIENTS) || (secrets.MAIL_RECIPIENTS || vars.MAIL_RECIPIENTS))) }}
DIRECTUS_URL: ${{ needs.prepare.outputs.directus_url }}
DIRECTUS_HOST: ${{ needs.prepare.outputs.directus_host }}
PROJECT_NAME: ${{ needs.prepare.outputs.project_name }}
DIRECTUS_KEY: ${{ secrets.DIRECTUS_KEY || (needs.prepare.outputs.target == 'production' && secrets.DIRECTUS_KEY || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_KEY || secrets.TESTING_DIRECTUS_KEY || secrets.DIRECTUS_KEY)) }}
DIRECTUS_SECRET: ${{ secrets.DIRECTUS_SECRET || (needs.prepare.outputs.target == 'production' && secrets.DIRECTUS_SECRET || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_SECRET || secrets.TESTING_DIRECTUS_SECRET || secrets.DIRECTUS_SECRET)) }}
DIRECTUS_ADMIN_EMAIL: ${{ secrets.DIRECTUS_ADMIN_EMAIL || (needs.prepare.outputs.target == 'production' && secrets.DIRECTUS_ADMIN_EMAIL || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_ADMIN_EMAIL || secrets.TESTING_DIRECTUS_ADMIN_EMAIL || secrets.DIRECTUS_ADMIN_EMAIL)) }}
DIRECTUS_ADMIN_PASSWORD: ${{ secrets.DIRECTUS_ADMIN_PASSWORD || (needs.prepare.outputs.target == 'production' && secrets.DIRECTUS_ADMIN_PASSWORD || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_ADMIN_PASSWORD || secrets.TESTING_DIRECTUS_ADMIN_PASSWORD || secrets.DIRECTUS_ADMIN_PASSWORD)) }}
DIRECTUS_DB_NAME: ${{ secrets.DIRECTUS_DB_NAME || 'directus' }}
DIRECTUS_DB_USER: ${{ secrets.DIRECTUS_DB_USER || 'directus' }}
DIRECTUS_DB_PASSWORD: ${{ secrets.DIRECTUS_DB_PASSWORD || (needs.prepare.outputs.target == 'production' && secrets.DIRECTUS_DB_PASSWORD || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_DB_PASSWORD || secrets.TESTING_DIRECTUS_DB_PASSWORD || secrets.DIRECTUS_DB_PASSWORD)) }}
DIRECTUS_API_TOKEN: ${{ secrets.DIRECTUS_API_TOKEN || (needs.prepare.outputs.target == 'production' && secrets.DIRECTUS_API_TOKEN || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_API_TOKEN || secrets.TESTING_DIRECTUS_API_TOKEN || secrets.DIRECTUS_API_TOKEN)) }}
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: 🚀 Deploy to ${{ env.TARGET }}
shell: bash
run: |
echo "🚀 Deploying to alpha.mintel.me..."
# Setup SSH
echo "Deploying $TARGET → $IMAGE_TAG"
mkdir -p ~/.ssh
echo "${{ secrets.ALPHA_SSH_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H alpha.mintel.me >> ~/.ssh/known_hosts 2>/dev/null
# Create .env file content
cat > /tmp/klz-cables.env << EOF
# ============================================================================
# KLZ Cables - Production Environment Configuration
# ============================================================================
# Auto-generated by CI/CD workflow
# DO NOT EDIT MANUALLY - Changes will be overwritten on next deployment
# ============================================================================
# Application
# Generated by CI - $TARGET - $(date -u)
NODE_ENV=production
NEXT_PUBLIC_BASE_URL=${{ secrets.NEXT_PUBLIC_BASE_URL }}
# Analytics (Umami)
NEXT_PUBLIC_UMAMI_WEBSITE_ID=${{ secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}
NEXT_PUBLIC_UMAMI_SCRIPT_URL=${{ secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL }}
# Error Tracking (GlitchTip/Sentry)
SENTRY_DSN=${{ secrets.SENTRY_DSN }}
# Email Configuration (Mailgun)
MAIL_HOST=${{ secrets.MAIL_HOST }}
MAIL_PORT=${{ secrets.MAIL_PORT }}
MAIL_USERNAME=${{ secrets.MAIL_USERNAME }}
MAIL_PASSWORD=${{ secrets.MAIL_PASSWORD }}
MAIL_FROM=${{ secrets.MAIL_FROM }}
MAIL_RECIPIENTS=${{ secrets.MAIL_RECIPIENTS }}
NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
NEXT_PUBLIC_TARGET=$TARGET
NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID
UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
SENTRY_DSN=$SENTRY_DSN
LOG_LEVEL=$( [[ "$TARGET" == "testing" || "$TARGET" == "development" ]] && echo "debug" || echo "info" )
MAIL_HOST=$MAIL_HOST
MAIL_PORT=$MAIL_PORT
MAIL_USERNAME=$MAIL_USERNAME
MAIL_PASSWORD=$MAIL_PASSWORD
MAIL_FROM=$MAIL_FROM
MAIL_RECIPIENTS=$MAIL_RECIPIENTS
# Directus
DIRECTUS_URL=https://cms.klz-cables.com
DIRECTUS_KEY=${{ secrets.DIRECTUS_KEY }}
DIRECTUS_SECRET=${{ secrets.DIRECTUS_SECRET }}
DIRECTUS_ADMIN_EMAIL=${{ secrets.DIRECTUS_ADMIN_EMAIL }}
DIRECTUS_ADMIN_PASSWORD=${{ secrets.DIRECTUS_ADMIN_PASSWORD }}
DIRECTUS_DB_NAME=directus
DIRECTUS_DB_USER=directus
DIRECTUS_DB_PASSWORD=${{ secrets.DIRECTUS_DB_PASSWORD }}
DIRECTUS_URL=$DIRECTUS_URL
DIRECTUS_HOST=$DIRECTUS_HOST
DIRECTUS_KEY=$DIRECTUS_KEY
DIRECTUS_SECRET=$DIRECTUS_SECRET
DIRECTUS_ADMIN_EMAIL=$DIRECTUS_ADMIN_EMAIL
DIRECTUS_ADMIN_PASSWORD=$DIRECTUS_ADMIN_PASSWORD
DIRECTUS_DB_NAME=$DIRECTUS_DB_NAME
DIRECTUS_DB_USER=$DIRECTUS_DB_USER
DIRECTUS_DB_PASSWORD=$DIRECTUS_DB_PASSWORD
DIRECTUS_API_TOKEN=$DIRECTUS_API_TOKEN
INTERNAL_DIRECTUS_URL=http://directus:8055
GATEKEEPER_PASSWORD=$GATEKEEPER_PASSWORD
TARGET=$TARGET
SENTRY_ENVIRONMENT=$TARGET
IMAGE_TAG=$IMAGE_TAG
TRAEFIK_HOST=$TRAEFIK_HOST
ENV_FILE=$ENV_FILE
AUTH_MIDDLEWARE=$( [[ "$TARGET" == "production" ]] && echo "compress" || echo "${PROJECT_NAME}-auth,compress" )
PROJECT_NAME=$PROJECT_NAME
COOKIE_DOMAIN=.$(echo $NEXT_PUBLIC_BASE_URL | sed 's|https://||')
EOF
# Upload .env and docker-compose.yml
scp -o StrictHostKeyChecking=accept-new /tmp/klz-cables.env root@alpha.mintel.me:/home/deploy/sites/klz-cables.com/.env
# 1. Cleanup and Create Directories on server BEFORE SCP
ssh -o StrictHostKeyChecking=accept-new root@alpha.mintel.me bash << 'EOF'
set -e
mkdir -p /home/deploy/sites/klz-cables.com/varnish
mkdir -p /home/deploy/sites/klz-cables.com/directus/uploads /home/deploy/sites/klz-cables.com/directus/extensions
if [ -d "/home/deploy/sites/klz-cables.com/varnish/default.vcl" ]; then
echo "🧹 Removing directory 'varnish/default.vcl' created by Docker..."
rm -rf /home/deploy/sites/klz-cables.com/varnish/default.vcl
fi
chown -R deploy:deploy /home/deploy/sites/klz-cables.com/directus /home/deploy/sites/klz-cables.com/varnish
EOF
# 2. Transfer files
scp -o StrictHostKeyChecking=accept-new /tmp/klz-cables.env root@alpha.mintel.me:/home/deploy/sites/klz-cables.com/$ENV_FILE
scp -o StrictHostKeyChecking=accept-new docker-compose.yml root@alpha.mintel.me:/home/deploy/sites/klz-cables.com/docker-compose.yml
ssh -o StrictHostKeyChecking=accept-new root@alpha.mintel.me bash << EOF
set -e
cd /home/deploy/sites/klz-cables.com
chmod 600 .env
chown deploy:deploy .env
echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
docker pull registry.infra.mintel.me/mintel/klz-cables.com:latest
docker-compose down
echo "🚀 Starting containers..."
docker-compose up -d
echo "⏳ Giving the app a few seconds to warm up..."
sleep 10
echo "🔍 Checking container status..."
docker-compose ps
if ! docker-compose ps | grep -q "Up"; then
echo "❌ Container failed to start"
docker-compose logs --tail=100
exit 1
fi
scp -r -o StrictHostKeyChecking=accept-new varnish root@alpha.mintel.me:/home/deploy/sites/klz-cables.com/
echo "✅ Deployment complete!"
EOF
rm -f /tmp/klz-cables.env
ssh -o StrictHostKeyChecking=accept-new root@alpha.mintel.me IMAGE_TAG="$IMAGE_TAG" ENV_FILE="$ENV_FILE" PROJECT_NAME="$PROJECT_NAME" bash << 'EOF'
set -e
cd /home/deploy/sites/klz-cables.com
chmod 600 "$ENV_FILE"
chown deploy:deploy "$ENV_FILE"
# ═══════════════════════════════════════════════════════════════════════════════
# LOGGING: Workflow Summary
# ═══════════════════════════════════════════════════════════════════════════════
- name: 📊 Workflow Summary
if: always()
run: |
echo "📊 Status: ${{ job.status }}"
echo "🎯 Target: alpha.mintel.me"
# ═══════════════════════════════════════════════════════════════════════════════
# NOTIFICATION: Gotify
# ═══════════════════════════════════════════════════════════════════════════════
- name: 🔔 Gotify Notification (Success)
if: success()
run: |
echo "Sending success notification to Gotify..."
RESPONSE=$(curl -k -s -w "\n%{http_code}" -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
-F "title=✅ Deployment Success: ${{ github.repository }}" \
-F "message=The deployment of ${{ github.repository }} (branch: ${{ github.ref }}) was successful.
echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin
echo "→ Pulling image: $IMAGE_TAG"
docker compose -p "$PROJECT_NAME" --env-file "$ENV_FILE" pull
echo "→ Starting containers..."
docker compose -p "$PROJECT_NAME" --env-file "$ENV_FILE" up -d --remove-orphans
docker system prune -f --filter "until=24h"
echo "→ Waiting 15s for warmup..."
sleep 15
echo "→ Container status:"
docker compose -p "$PROJECT_NAME" --env-file "$ENV_FILE" ps
if ! docker compose -p "$PROJECT_NAME" --env-file "$ENV_FILE" ps | grep -q "Up"; then
echo "❌ Fehler: Container nicht Up!"
docker compose -p "$PROJECT_NAME" --env-file "$ENV_FILE" logs --tail=150
exit 1
fi
Commit: ${{ github.sha }}
Actor: ${{ github.actor }}
Run ID: ${{ github.run_id }}" \
-F "priority=5")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
echo "HTTP Status: $HTTP_CODE"
echo "Response Body: $BODY"
if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 300 ]; then
echo "Failed to send Gotify notification"
exit 0 # Don't fail the workflow because of notification failure
fi
echo "→ Verifying Varnish Backend Health..."
docker compose -p "$PROJECT_NAME" --env-file "$ENV_FILE" exec -T varnish varnishadm backend.list
if ! docker compose -p "$PROJECT_NAME" --env-file "$ENV_FILE" exec -T varnish varnishadm backend.list | grep -q "healthy"; then
echo "❌ Fehler: Varnish Backend ist SICK!"
docker compose -p "$PROJECT_NAME" --env-file "$ENV_FILE" logs varnish
exit 1
fi
echo "✅ Varnish Backend ist Healthy."
- name: 🔔 Gotify Notification (Failure)
if: failure()
# ──────────────────────────────────────────────────────────────────────────────
# JOB 5: PageSpeed Test
# ──────────────────────────────────────────────────────────────────────────────
pagespeed:
name: ⚡ PageSpeed
needs: [prepare, deploy]
if: |
always() &&
needs.prepare.outputs.target != 'skip' &&
needs.deploy.result == 'success' &&
github.event.inputs.skip_long_checks != 'true'
runs-on: docker
container:
image: catthehacker/ubuntu:act-latest
# outputs:
# report_url: ${{ steps.save.outputs.report_url }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: 🔍 Install Chromium (Native & ARM64)
run: |
echo "Sending failure notification to Gotify..."
RESPONSE=$(curl -k -s -w "\n%{http_code}" -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
-F "title=❌ Deployment Failed: ${{ github.repository }}" \
-F "message=The deployment of ${{ github.repository }} (branch: ${{ github.ref }}) failed!
apt-get update
apt-get install -y gnupg wget ca-certificates
# Detect OS
OS_ID=$(. /etc/os-release && echo $ID)
CODENAME=$(. /etc/os-release && echo $VERSION_CODENAME)
if [ "$OS_ID" = "debian" ]; then
echo "🎯 Debian detected - installing native chromium"
apt-get install -y chromium
else
echo "🎯 Ubuntu detected - adding xtradeb PPA"
mkdir -p /etc/apt/keyrings
KEY_ID="82BB6851C64F6880"
Commit: ${{ github.sha }}
Actor: ${{ github.actor }}
Run ID: ${{ github.run_id }}
# Multi-method Key Fetch
SUCCESS=false
echo "Fetching key $KEY_ID..."
Please check the logs for details." \
-F "priority=8")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
echo "HTTP Status: $HTTP_CODE"
echo "Response Body: $BODY"
if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 300 ]; then
echo "Failed to send Gotify notification"
exit 0 # Don't fail the workflow because of notification failure
# Method 1: gpg --recv-keys (standard)
for server in "hkp://keyserver.ubuntu.com:80" "hkp://keyserver.ubuntu.com:11371"; do
if gpg --no-default-keyring --keyring /tmp/xtradeb.gpg --keyserver "$server" --recv-keys "$KEY_ID"; then
gpg --no-default-keyring --keyring /tmp/xtradeb.gpg --export > /etc/apt/keyrings/xtradeb.gpg
SUCCESS=true && break
fi
done
# Method 2: Direct wget (fallback)
if [ "$SUCCESS" = false ]; then
wget -qO- "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_ID" | gpg --dearmor > /etc/apt/keyrings/xtradeb.gpg && SUCCESS=true
fi
if [ "$SUCCESS" = true ]; then
echo "deb [signed-by=/etc/apt/keyrings/xtradeb.gpg] http://ppa.launchpad.net/xtradeb/apps/ubuntu $CODENAME main" > /etc/apt/sources.list.d/xtradeb-ppa.list
else
echo "⚠️ GPG fetch failed, using legacy apt-key as last resort..."
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys "$KEY_ID" || true
echo "deb http://ppa.launchpad.net/xtradeb/apps/ubuntu $CODENAME main" > /etc/apt/sources.list.d/xtradeb-ppa.list
fi
# PRIORITY PINNING: Force PPA over Snap-dummy
printf "Package: *\nPin: release o=LP-PPA-xtradeb-apps\nPin-Priority: 1001\n" > /etc/apt/preferences.d/xtradeb
apt-get update
apt-get install -y --allow-downgrades chromium || apt-get install -y chromium-browser
fi
# Force clean paths (remove existing dead links/files if they are snap wrappers)
rm -f /usr/bin/google-chrome /usr/bin/chromium-browser
[ -f /usr/bin/chromium ] && ln -sf /usr/bin/chromium /usr/bin/google-chrome
[ -f /usr/bin/chromium ] && ln -sf /usr/bin/chromium /usr/bin/chromium-browser
echo "✅ Binary check:"
ls -l /usr/bin/chromium* /usr/bin/google-chrome || true
continue-on-error: true
- name: 🧪 Run PageSpeed (Lighthouse)
env:
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_base_url }}
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
PAGESPEED_LIMIT: 8
PUPPETEER_EXECUTABLE_PATH: /usr/bin/chromium
CHROME_PATH: /usr/bin/chromium
run: npm run pagespeed:test
# ──────────────────────────────────────────────────────────────────────────────
# JOB 6: Notifications
# ──────────────────────────────────────────────────────────────────────────────
notifications:
name: 🔔 Notifications
needs: [prepare, qa, build-app, deploy, pagespeed]
if: always()
runs-on: docker
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: 📊 Deployment Summary
run: |
echo "┌──────────────────────────────┐"
echo "│ Deployment Summary │"
echo "├──────────────────────────────┤"
echo "│ Status: ${{ needs.deploy.result }} │"
echo "│ Umgebung: ${{ needs.prepare.outputs.target || 'skipped' }} │"
echo "│ Version: ${{ needs.prepare.outputs.image_tag }} │"
echo "│ Commit: ${{ needs.prepare.outputs.short_sha }} │"
echo "│ Message: ${{ needs.prepare.outputs.commit_msg }} │"
echo "└──────────────────────────────┘"
- name: 🔔 Gotify - Success
if: needs.deploy.result == 'success'
run: |
curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
-F "title=${{ needs.prepare.outputs.gotify_title }}" \
-F "message=Erfolgreich deployt auf **${{ needs.prepare.outputs.target }}**\n\nVersion: **${{ needs.prepare.outputs.image_tag }}**\nCommit: ${{ needs.prepare.outputs.short_sha }} (${{ needs.prepare.outputs.commit_msg }})\nVon: ${{ github.actor }}\nRun: ${{ github.run_id }}" \
-F "priority=4" || true
- name: 🔔 Gotify - Failure
if: |
needs.prepare.result == 'failure' ||
needs.qa.result == 'failure' ||
needs.build-app.result == 'failure' ||
needs.deploy.result == 'failure' ||
needs.pagespeed.result == 'failure'
run: |
curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
-F "title=❌ Deployment FEHLGESCHLAGEN ${{ needs.prepare.outputs.target || 'unknown' }}" \
-F "message=**Fehler beim Deploy auf ${{ needs.prepare.outputs.target }}**\n\nVersion: ${{ needs.prepare.outputs.image_tag || '?' }}\nCommit: ${{ needs.prepare.outputs.short_sha || '?' }}\nVon: ${{ github.actor }}\nRun: ${{ github.run_id }}\n\nBitte Logs prüfen!" \
-F "priority=8" || true

7
.gitignore vendored
View File

@@ -1,2 +1,7 @@
node_modules
.next
.next
.DS_Store
# Directus
directus/uploads
!directus/extensions/

1
.husky/commit-msg Executable file
View File

@@ -0,0 +1 @@
npx --no -- commitlint --edit "$1"

1
.husky/pre-commit Executable file
View File

@@ -0,0 +1 @@
npx lint-staged

11
.lintstagedrc.js Normal file
View File

@@ -0,0 +1,11 @@
const path = require('path');
const buildEslintCommand = (filenames) =>
`next lint --fix --file ${filenames
.map((f) => path.relative(process.cwd(), f))
.join(' --file ')}`;
module.exports = {
'*.{js,jsx,ts,tsx}': [buildEslintCommand, 'prettier --write'],
'*.{json,md,css,scss}': ['prettier --write'],
};

7
.prettierrc Normal file
View File

@@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 100
}

View File

@@ -8,7 +8,7 @@ WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json package-lock.json* ./
RUN npm ci
RUN --mount=type=cache,target=/root/.npm npm ci --legacy-peer-deps
# Rebuild the source code only when needed
@@ -25,17 +25,24 @@ ENV NEXT_TELEMETRY_DISABLED=1
# Build-time environment variables for Next.js
# These are baked into the client bundle during build
ARG NEXT_PUBLIC_BASE_URL
ARG NEXT_PUBLIC_BASE_URL
ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID
ARG UMAMI_API_ENDPOINT
ARG UMAMI_SCRIPT_URL
ARG NEXT_PUBLIC_UMAMI_SCRIPT_URL
ARG NEXT_PUBLIC_TARGET
ARG DIRECTUS_URL
ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
ENV NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID
ENV NEXT_PUBLIC_UMAMI_SCRIPT_URL=$NEXT_PUBLIC_UMAMI_SCRIPT_URL
ENV UMAMI_API_ENDPOINT=${UMAMI_API_ENDPOINT:-${UMAMI_SCRIPT_URL:-$NEXT_PUBLIC_UMAMI_SCRIPT_URL}}
ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET
ENV DIRECTUS_URL=$DIRECTUS_URL
# Validate environment variables during build
RUN npx tsx scripts/validate-env.ts
RUN SKIP_RUNTIME_ENV_VALIDATION=true npx tsx scripts/validate-env.ts
RUN npm run build
RUN --mount=type=cache,target=/app/.next/cache npm run build
# Production image, copy all the files and run next
FROM base AS runner

View File

@@ -5,11 +5,13 @@ A complete WordPress to Next.js static site migration for KLZ Cables, transformi
## 🚀 Quick Start
### Prerequisites
- Node.js 18+
- Node.js 18+
- npm or yarn
### Installation
```bash
````bash
# Install dependencies
npm install --legacy-peer-deps
@@ -42,11 +44,12 @@ npm run cms:logs
# Stop the CMS
npm run cms:stop
```
````
Once running, you can access the Strapi admin panel at `http://localhost:1337/admin`.
### 🔄 Data & Migration
To sync data or migrate existing content:
```bash
@@ -61,6 +64,7 @@ npm run cms:migrate
```
### Environment Variables
```bash
# .env
SITE_URL=https://klz-cables.com
@@ -69,8 +73,8 @@ TURNSTILE_SITE_KEY=your_turnstile_key
TURNSTILE_SECRET_KEY=your_turnstile_secret
# Umami
NEXT_PUBLIC_UMAMI_WEBSITE_ID=your_umami_website_id
NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js
UMAMI_WEBSITE_ID=your_umami_website_id
UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
# GlitchTip (Sentry compatible)
SENTRY_DSN=https://PUBLIC_KEY@errors.infra.mintel.me/PROJECT_ID
@@ -81,6 +85,7 @@ NEXT_PUBLIC_SENTRY_DSN=https://PUBLIC_KEY@errors.infra.mintel.me/PROJECT_ID
## 📊 Project Overview
### Migration Statistics
- **Content Exported**: 141 items
- 18 pages (9 EN + 9 DE)
- 59 posts (29 EN + 30 DE)
@@ -91,6 +96,7 @@ NEXT_PUBLIC_SENTRY_DSN=https://PUBLIC_KEY@errors.infra.mintel.me/PROJECT_ID
- **Translation Pairs**: 16
### Performance Benefits
- **Before**: Dynamic WordPress with database queries
- **After**: Static HTML with CDN delivery
- **Load Time**: <100ms (vs 500ms+)
@@ -99,6 +105,7 @@ NEXT_PUBLIC_SENTRY_DSN=https://PUBLIC_KEY@errors.infra.mintel.me/PROJECT_ID
## 🏗️ Architecture
### Tech Stack
- **Framework**: Next.js 14 (App Router)
- **Language**: TypeScript
- **Styling**: SCSS
@@ -109,6 +116,7 @@ NEXT_PUBLIC_SENTRY_DSN=https://PUBLIC_KEY@errors.infra.mintel.me/PROJECT_ID
- **CAPTCHA**: Cloudflare Turnstile
### Project Structure
```
app/
├── layout.tsx # Root layout
@@ -133,7 +141,7 @@ app/
├── api/
│ └── contact/route.ts # Contact API
├── sitemap.ts # Sitemap generator
── robots.ts # Robots.txt generator
── robots.ts # Robots.txt generator
lib/
├── data.ts # Data access
@@ -144,7 +152,7 @@ components/
├── LocaleSwitcher.tsx # Language switcher
├── ContactForm.tsx # Contact form
├── CookieConsent.tsx # GDPR banner
── SEO.tsx # SEO utilities
── SEO.tsx # SEO utilities
data/
├── raw/ # WordPress export
@@ -163,6 +171,7 @@ scripts/
## 🎯 Features
### ✅ Implemented
- **Multi-language**: EN/DE with `/de/` prefix routing
- **Contact Forms**: Resend integration with validation
- **GDPR Compliance**: Cookie consent banner
@@ -175,12 +184,14 @@ scripts/
- **Asset Management**: WordPress → local path mapping
### 🔄 In Progress
- Analytics integration (consent-based)
- Turnstile CAPTCHA
- Build testing
- Deployment configuration
### 📝 Remaining
- Performance optimization
- Final QA testing
- Documentation updates
@@ -188,6 +199,7 @@ scripts/
## 📝 Content Management
### Data Export
```bash
# Export from WordPress
npm run data:export
@@ -203,6 +215,7 @@ npm run data:improve-mapping
```
### Adding New Content
1. Export new content from WordPress
2. Process the data
3. Rebuild the site
@@ -210,17 +223,20 @@ npm run data:improve-mapping
## 🎨 Design System
### Colors
- Primary: `#0066cc` (KLZ Blue)
- Secondary: `#00a896` (Teal)
- Text: `#1a1a1a`
- Background: `#f8f9fa`
### Typography
- Font: Inter
- Base: 16px
- Scale: 1.25 (Major Third)
### Layout
- Max width: 1200px
- Responsive grid
- Mobile-first
@@ -228,6 +244,7 @@ npm run data:improve-mapping
## 🔧 API Endpoints
### Contact Form
```
POST /api/contact
{
@@ -239,11 +256,13 @@ POST /api/contact
```
### Sitemap
```
GET /sitemap.xml
```
### Robots
```
GET /robots.txt
```
@@ -252,21 +271,32 @@ GET /robots.txt
### Automatic Deployment (Current Setup)
The project uses **Gitea Actions** for CI/CD. Every push to `main` triggers:
The project uses **Gitea Actions** for CI/CD. Every push to `main` or `staging` triggers:
1. **Build**: Docker image built for `linux/arm64`
2. **Push**: Image pushed to `registry.infra.mintel.me`
3. **Deploy**: SSH to production server, pull and restart containers
1. **Build**: Docker image built for `linux/arm64` with branch-specific build args
2. **Push**: Image pushed to `registry.infra.mintel.me` with commit SHA tag
3. **Deploy**: SSH to production server, pull and restart containers using branch-specific `.env` files
**Workflow**: `.gitea/workflows/deploy.yml`
**Branch Deployments**:
- `main` branch: Deploys to production using `.env.prod`
- `staging` branch: Deploys to staging using `.env.staging`
**Environment Overrides**:
The CI/CD workflow supports `STAGING_`-prefixed secrets (e.g., `STAGING_NEXT_PUBLIC_BASE_URL`) to override default secrets when deploying the `staging` branch.
**Required Secrets** (configure in Gitea repository settings):
- `REGISTRY_USER` - Docker registry username
- `REGISTRY_PASS` - Docker registry password
- `ALPHA_SSH_KEY` - SSH private key for deployment
- `NEXT_PUBLIC_UMAMI_WEBSITE_ID` - Analytics ID
- `NEXT_PUBLIC_UMAMI_SCRIPT_URL` - Analytics script URL
- `NEXT_PUBLIC_BASE_URL` - Application base URL (e.g., `https://klz-cables.com`)
- `UMAMI_WEBSITE_ID` - Analytics ID
- `UMAMI_API_ENDPOINT` - Analytics API endpoint (formerly NEXT_PUBLIC_UMAMI_SCRIPT_URL)
- `SENTRY_DSN` - Error tracking DSN
- `MAIL_HOST`, `MAIL_PORT`, `MAIL_USERNAME`, `MAIL_PASSWORD`, `MAIL_FROM`, `MAIL_RECIPIENTS` - Email configuration
### Manual Deployment
@@ -284,6 +314,7 @@ docker image prune -f
```
Or use the convenience script:
```bash
bash scripts/deploy-webhook.sh
```
@@ -295,11 +326,13 @@ Client → Traefik (TLS) → Next.js App
```
**Domains**:
- `klz-cables.com` - Production
- `www.klz-cables.com` - Production (www)
- `staging.klz-cables.com` - Staging
**Services**:
- `app`: Next.js application (port 3000)
- `traefik`: Reverse proxy (external)
@@ -308,25 +341,30 @@ For detailed deployment documentation, see [`docs/DEPLOYMENT.md`](docs/DEPLOYMEN
## 📈 Performance
### Build Time
- **Target**: < 2 minutes
- **Current**: ~1-2 minutes
### Page Load
- **Target**: < 100ms
- **Current**: Static HTML from CDN
### Bundle Size
- **Target**: < 100KB gzipped
- **Current**: Optimized with code splitting
## 🔒 Security
### Environment Variables
- Never commit `.env` file
- Rotate keys regularly
- Use secrets in deployment platform
### Form Security
- Email validation
- Rate limiting (recommended)
- Turnstile CAPTCHA (pending)
@@ -334,6 +372,7 @@ For detailed deployment documentation, see [`docs/DEPLOYMENT.md`](docs/DEPLOYMEN
## 🎓 WordPress Specifics
### WPBakery Shortcodes Removed
- `[vc_row]`, `[vc_column]`, `[vc_column_text]`
- `[nectar_*]` (Salient theme)
- `[image_with_animation]`
@@ -341,13 +380,16 @@ For detailed deployment documentation, see [`docs/DEPLOYMENT.md`](docs/DEPLOYMEN
- `[divider]`
### HTML Sanitization
- Removes inline event handlers
- Strips scripts
- Normalizes classes
- Preserves structure
### Asset Mapping
WordPress URLs → Local paths:
```
https://klz-cables.com/wp-content/uploads/... → /media/...
```
@@ -355,11 +397,13 @@ https://klz-cables.com/wp-content/uploads/... → /media/...
## 📚 Documentation
### Internal
- `PROJECT_STRUCTURE.md` - Detailed structure
- `IMPLEMENTATION_SUMMARY.md` - Progress tracking
- `FINAL_SUMMARY.md` - Complete overview
### External
- [Next.js Docs](https://nextjs.org/docs)
- [WordPress REST API](https://developer.wordpress.org/rest-api/)
- [Resend Docs](https://resend.com/docs)
@@ -370,17 +414,20 @@ https://klz-cables.com/wp-content/uploads/... → /media/...
### Common Issues
**TypeScript Errors**
- The TypeScript errors shown in the editor are expected
- They occur because modules reference each other
- The build process resolves these correctly
- Run `npm run build` to verify
**Build Failures**
- Check environment variables
- Verify data files exist
- Clear `.next` cache: `rm -rf .next`
**Missing Modules**
- Run `npm install --legacy-peer-deps`
- Check `package.json` dependencies
@@ -395,11 +442,12 @@ https://klz-cables.com/wp-content/uploads/... → /media/...
**i18n**: Multi-language support
**SEO**: Metadata and sitemaps
**Compatibility**: WPBakery content handled
**Media**: All images downloaded
**Media**: All images downloaded
## 📞 Support
For issues or questions:
1. Check the documentation
2. Review the troubleshooting section
3. Check environment variables

View File

@@ -1,6 +1,7 @@
import { ImageResponse } from 'next/og';
import { getPageBySlug } from '@/lib/pages';
import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const runtime = 'nodejs';
@@ -8,11 +9,11 @@ export default async function Image({ params: { locale, slug } }: { params: { lo
const pageData = await getPageBySlug(slug, locale);
if (!pageData) {
return new ImageResponse(
<div style={{ display: 'flex', width: '100%', height: '100%', backgroundColor: '#001a4d' }} />
);
return new Response('Page not found', { status: 404 });
}
const fonts = await getOgFonts();
return new ImageResponse(
(
<OGImageTemplate
@@ -22,8 +23,9 @@ export default async function Image({ params: { locale, slug } }: { params: { lo
/>
),
{
width: 1200,
height: 630,
...OG_IMAGE_SIZE,
fonts,
}
);
}

View File

@@ -6,6 +6,7 @@ import { Metadata } from 'next';
import { getPageBySlug, getAllPages } from '@/lib/pages';
import { mdxComponents } from '@/components/blog/MDXComponents';
import { getOGImageMetadata } from '@/lib/metadata';
import { SITE_URL } from '@/lib/schema';
interface PageProps {
params: {
@@ -30,7 +31,7 @@ export async function generateStaticParams() {
export async function generateMetadata({ params: { locale, slug } }: PageProps): Promise<Metadata> {
const pageData = await getPageBySlug(slug, locale);
if (!pageData) return {};
return {
@@ -39,15 +40,15 @@ export async function generateMetadata({ params: { locale, slug } }: PageProps):
alternates: {
canonical: `/${locale}/${slug}`,
languages: {
'de': `/de/${slug}`,
'en': `/en/${slug}`,
de: `/de/${slug}`,
en: `/en/${slug}`,
'x-default': `/en/${slug}`,
},
},
openGraph: {
title: `${pageData.frontmatter.title} | KLZ Cables`,
description: pageData.frontmatter.excerpt || '',
url: `https://klz-cables.com/${locale}/${slug}`,
url: `${SITE_URL}/${locale}/${slug}`,
images: getOGImageMetadata(slug, pageData.frontmatter.title, locale),
},
twitter: {
@@ -75,7 +76,9 @@ export default async function StandardPage({ params: { locale, slug } }: PagePro
</div>
<Container className="relative z-10">
<div className="max-w-4xl animate-slide-up">
<Badge variant="accent" className="mb-4 md:mb-6">{t('badge')}</Badge>
<Badge variant="accent" className="mb-4 md:mb-6">
{t('badge')}
</Badge>
<Heading level={1} className="text-white mb-0">
{pageData.frontmatter.title}
</Heading>
@@ -106,9 +109,14 @@ export default async function StandardPage({ params: { locale, slug } }: PagePro
<div className="relative z-10 max-w-2xl">
<h3 className="text-2xl md:text-3xl font-bold mb-4">{t('needHelp')}</h3>
<p className="text-lg text-white/70 mb-8">{t('supportTeamAvailable')}</p>
<a href={`/${locale}/contact`} className="inline-flex items-center px-8 py-4 bg-accent text-primary-dark font-bold rounded-full hover:bg-white transition-all duration-300 group/link">
{t('contactUs')}
<span className="ml-2 transition-transform group-hover/link:translate-x-1">&rarr;</span>
<a
href={`/${locale}/contact`}
className="inline-flex items-center px-8 py-4 bg-accent text-primary-dark font-bold rounded-full hover:bg-white transition-all duration-300 group/link"
>
{t('contactUs')}
<span className="ml-2 transition-transform group-hover/link:translate-x-1">
&rarr;
</span>
</a>
</div>
</div>
@@ -116,4 +124,4 @@ export default async function StandardPage({ params: { locale, slug } }: PagePro
</div>
</div>
);
}
}

View File

@@ -3,6 +3,7 @@ import { getProductBySlug } from '@/lib/mdx';
import { getTranslations } from 'next-intl/server';
import { OGImageTemplate } from '@/components/OGImageTemplate';
import { NextRequest } from 'next/server';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const runtime = 'nodejs';
@@ -10,7 +11,7 @@ export async function GET(
request: NextRequest,
{ params }: { params: { locale: string } }
) {
const { searchParams } = new URL(request.url);
const { searchParams, origin } = new URL(request.url);
const slug = searchParams.get('slug');
const locale = params.locale || 'en';
@@ -18,6 +19,7 @@ export async function GET(
return new Response('Missing slug', { status: 400 });
}
const fonts = await getOgFonts();
const t = await getTranslations({ locale, namespace: 'Products' });
// Check if it's a category page
@@ -36,8 +38,8 @@ export async function GET(
/>
),
{
width: 1200,
height: 630,
...OG_IMAGE_SIZE,
fonts,
}
);
}
@@ -45,16 +47,13 @@ export async function GET(
const product = await getProductBySlug(slug, locale);
if (!product) {
return new ImageResponse(
<div style={{ display: 'flex', width: '100%', height: '100%', backgroundColor: '#001a4d' }} />
);
return new Response('Product not found', { status: 404 });
}
const { origin } = new URL(request.url);
const featuredImage = product.frontmatter.images?.[0]
? (product.frontmatter.images[0].startsWith('http')
? product.frontmatter.images[0]
: `${origin}${product.frontmatter.images[0]}`)
? product.frontmatter.images[0]
: `${origin}${product.frontmatter.images[0]}`)
: undefined;
return new ImageResponse(
@@ -67,8 +66,9 @@ export async function GET(
/>
),
{
width: 1200,
height: 630,
...OG_IMAGE_SIZE,
fonts,
}
);
}

View File

@@ -1,36 +1,44 @@
import { ImageResponse } from 'next/og';
import { getPostBySlug } from '@/lib/blog';
import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
import { SITE_URL } from '@/lib/schema';
export const runtime = 'nodejs';
export default async function Image({ params: { locale, slug } }: { params: { locale: string, slug: string } }) {
export default async function Image({
params: { locale, slug },
}: {
params: { locale: string; slug: string };
}) {
const post = await getPostBySlug(slug, locale);
if (!post) {
return new ImageResponse(
<div style={{ display: 'flex', width: '100%', height: '100%', backgroundColor: '#001a4d' }} />
);
return new Response('Post not found', { status: 404 });
}
const fonts = await getOgFonts();
// We don't have request.url here, but we can assume the domain from SITE_URL or config
// For local images during dev, relative paths in <img> might not work in Satori
// but if we are in nodejs runtime, we could potentially read from disk.
// For now, let's just make sure it's absolute.
const featuredImage = post.frontmatter.featuredImage
? (post.frontmatter.featuredImage.startsWith('http')
? post.frontmatter.featuredImage
: `https://klz-cables.com${post.frontmatter.featuredImage}`)
? post.frontmatter.featuredImage.startsWith('http')
? post.frontmatter.featuredImage
: `${SITE_URL}${post.frontmatter.featuredImage}`
: undefined;
return new ImageResponse(
(
<OGImageTemplate
title={post.frontmatter.title}
description={post.frontmatter.excerpt}
label={post.frontmatter.category || 'Blog'}
image={featuredImage}
/>
),
<OGImageTemplate
title={post.frontmatter.title}
description={post.frontmatter.excerpt}
label={post.frontmatter.category || 'Blog'}
image={featuredImage}
/>,
{
width: 1200,
height: 630,
}
...OG_IMAGE_SIZE,
fonts,
},
);
}

View File

@@ -20,9 +20,11 @@ interface BlogPostProps {
};
}
export async function generateMetadata({ params: { locale, slug } }: BlogPostProps): Promise<Metadata> {
export async function generateMetadata({
params: { locale, slug },
}: BlogPostProps): Promise<Metadata> {
const post = await getPostBySlug(slug, locale);
if (!post) return {};
const description = post.frontmatter.excerpt || '';
@@ -32,8 +34,8 @@ export async function generateMetadata({ params: { locale, slug } }: BlogPostPro
alternates: {
canonical: `/${locale}/blog/${slug}`,
languages: {
'de': `/de/blog/${slug}`,
'en': `/en/blog/${slug}`,
de: `/de/blog/${slug}`,
en: `/en/blog/${slug}`,
'x-default': `/en/blog/${slug}`,
},
},
@@ -43,7 +45,7 @@ export async function generateMetadata({ params: { locale, slug } }: BlogPostPro
type: 'article',
publishedTime: post.frontmatter.date,
authors: ['KLZ Cables'],
url: `https://klz-cables.com/${locale}/blog/${slug}`,
url: `${SITE_URL}/${locale}/blog/${slug}`,
images: getOGImageMetadata(`blog/${slug}`, post.frontmatter.title, locale),
},
twitter: {
@@ -66,16 +68,15 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
return (
<article className="bg-white min-h-screen font-sans selection:bg-primary/10 selection:text-primary">
{/* Featured Image Header */}
{post.frontmatter.featuredImage ? (
<div className="relative w-full h-[70vh] min-h-[500px] overflow-hidden group">
<div
<div
className="absolute inset-0 bg-cover bg-center transition-transform duration-[3s] ease-out scale-110 group-hover:scale-100"
style={{ backgroundImage: `url(${post.frontmatter.featuredImage})` }}
/>
<div className="absolute inset-0 bg-gradient-to-t from-neutral-dark via-neutral-dark/40 to-transparent" />
{/* Title overlay on image */}
<div className="absolute inset-0 flex flex-col justify-end pb-16 md:pb-24">
<div className="container mx-auto px-4">
@@ -87,7 +88,10 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
</span>
</div>
)}
<Heading level={1} className="text-white mb-8 drop-shadow-2xl animate-slight-fade-in-from-bottom [animation-delay:200ms]">
<Heading
level={1}
className="text-white mb-8 drop-shadow-2xl animate-slight-fade-in-from-bottom [animation-delay:200ms]"
>
{post.frontmatter.title}
</Heading>
<div className="flex flex-wrap items-center gap-6 text-white/80 text-sm md:text-base font-medium animate-slight-fade-in-from-bottom [animation-delay:400ms]">
@@ -95,7 +99,7 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
year: 'numeric',
month: 'long',
day: 'numeric'
day: 'numeric',
})}
</time>
<span className="w-1 h-1 bg-white/30 rounded-full" />
@@ -123,7 +127,7 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
year: 'numeric',
month: 'long',
day: 'numeric'
day: 'numeric',
})}
</time>
<span className="w-1 h-1 bg-neutral-300 rounded-full" />
@@ -168,8 +172,18 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
href={`/${locale}/blog`}
className="inline-flex items-center gap-3 text-text-secondary hover:text-primary font-bold text-sm uppercase tracking-widest transition-all group"
>
<svg className="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="M15 19l-7-7 7-7" />
<svg
className="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="M15 19l-7-7 7-7"
/>
</svg>
{locale === 'de' ? 'Zurück zur Übersicht' : 'Back to Overview'}
</Link>
@@ -188,57 +202,63 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
{/* Structured Data */}
<JsonLd
id={`jsonld-${slug}`}
data={{
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.frontmatter.title,
datePublished: post.frontmatter.date,
dateModified: post.frontmatter.date,
image: post.frontmatter.featuredImage ? `https://klz-cables.com${post.frontmatter.featuredImage}` : undefined,
author: {
'@type': 'Organization',
name: 'KLZ Cables',
url: 'https://klz-cables.com',
logo: 'https://klz-cables.com/logo-blue.svg'
},
publisher: {
'@type': 'Organization',
name: 'KLZ Cables',
logo: {
'@type': 'ImageObject',
url: 'https://klz-cables.com/logo-blue.svg',
data={
{
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.frontmatter.title,
datePublished: post.frontmatter.date,
dateModified: post.frontmatter.date,
image: post.frontmatter.featuredImage
? `${SITE_URL}${post.frontmatter.featuredImage}`
: undefined,
author: {
'@type': 'Organization',
name: 'KLZ Cables',
url: SITE_URL,
logo: `${SITE_URL}/logo-blue.svg`,
},
},
description: post.frontmatter.excerpt,
mainEntityOfPage: {
'@type': 'WebPage',
'@id': `https://klz-cables.com/${locale}/blog/${slug}`,
},
articleSection: post.frontmatter.category,
wordCount: post.content.split(/\s+/).length,
timeRequired: `PT${getReadingTime(post.content)}M`
} as any}
publisher: {
'@type': 'Organization',
name: 'KLZ Cables',
logo: {
'@type': 'ImageObject',
url: `${SITE_URL}/logo-blue.svg`,
},
},
description: post.frontmatter.excerpt,
mainEntityOfPage: {
'@type': 'WebPage',
'@id': `${SITE_URL}/${locale}/blog/${slug}`,
},
articleSection: post.frontmatter.category,
wordCount: post.content.split(/\s+/).length,
timeRequired: `PT${getReadingTime(post.content)}M`,
} as any
}
/>
<JsonLd
id={`breadcrumb-${slug}`}
data={{
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: 'Blog',
item: `https://klz-cables.com/${locale}/blog`,
},
{
'@type': 'ListItem',
position: 2,
name: post.frontmatter.title,
item: `https://klz-cables.com/${locale}/blog/${slug}`,
},
],
} as any}
data={
{
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: 'Blog',
item: `${SITE_URL}/${locale}/blog`,
},
{
'@type': 'ListItem',
position: 2,
name: post.frontmatter.title,
item: `${SITE_URL}/${locale}/blog/${slug}`,
},
],
} as any
}
/>
</article>
);

View File

@@ -1,25 +1,25 @@
import { ImageResponse } from 'next/og';
import { getTranslations } from 'next-intl/server';
import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const runtime = 'nodejs';
export default async function Image({ params: { locale } }: { params: { locale: string } }) {
const t = await getTranslations({ locale, namespace: 'Blog.meta' });
const title = t('title');
const description = t('description');
const fonts = await getOgFonts();
return new ImageResponse(
(
<OGImageTemplate
title={title}
description={description}
title={t('title')}
description={t('description')}
label="Blog"
/>
),
{
width: 1200,
height: 630,
...OG_IMAGE_SIZE,
fonts,
}
);
}

View File

@@ -4,6 +4,7 @@ import { Section, Container, Heading, Card, Badge, Button } from '@/components/u
import Reveal from '@/components/Reveal';
import { getTranslations } from 'next-intl/server';
import { getOGImageMetadata } from '@/lib/metadata';
import { SITE_URL } from '@/lib/schema';
interface BlogIndexProps {
params: {
@@ -19,15 +20,15 @@ export async function generateMetadata({ params: { locale } }: BlogIndexProps) {
alternates: {
canonical: `/${locale}/blog`,
languages: {
'de': '/de/blog',
'en': '/en/blog',
de: '/de/blog',
en: '/en/blog',
'x-default': '/en/blog',
},
},
openGraph: {
title: `${t('title')} | KLZ Cables`,
description: t('description'),
url: `https://klz-cables.com/${locale}/blog`,
url: `${SITE_URL}/${locale}/blog`,
images: getOGImageMetadata('blog', t('title'), locale),
},
twitter: {
@@ -41,10 +42,10 @@ export async function generateMetadata({ params: { locale } }: BlogIndexProps) {
export default async function BlogIndex({ params: { locale } }: BlogIndexProps) {
const t = await getTranslations('Blog');
const posts = await getAllPosts(locale);
// Sort posts by date descending
const sortedPosts = [...posts].sort((a, b) =>
new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime()
const sortedPosts = [...posts].sort(
(a, b) => new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime(),
);
const featuredPost = sortedPosts[0];
@@ -65,10 +66,12 @@ export default async function BlogIndex({ params: { locale } }: BlogIndexProps)
<div className="absolute inset-0 image-overlay-gradient" />
</>
)}
<Container className="relative z-10">
<div className="max-w-4xl animate-slide-up">
<Badge variant="saturated" className="mb-4 md:mb-6">{t('featuredPost')}</Badge>
<Badge variant="saturated" className="mb-4 md:mb-6">
{t('featuredPost')}
</Badge>
{featuredPost && (
<>
<Heading level={1} className="text-white mb-4 md:mb-8">
@@ -77,9 +80,16 @@ export default async function BlogIndex({ params: { locale } }: BlogIndexProps)
<p className="text-base md:text-xl text-white/80 mb-6 md:mb-10 line-clamp-2 md:line-clamp-2 max-w-2xl">
{featuredPost.frontmatter.excerpt}
</p>
<Button href={`/${locale}/blog/${featuredPost.slug}`} variant="accent" size="lg" className="group w-full md:w-auto md:h-16 md:px-10 md:text-xl">
<Button
href={`/${locale}/blog/${featuredPost.slug}`}
variant="accent"
size="lg"
className="group w-full md:w-auto md:h-16 md:px-10 md:text-xl"
>
{t('readFullArticle')}
<span className="ml-3 transition-transform group-hover:translate-x-2">&rarr;</span>
<span className="ml-3 transition-transform group-hover:translate-x-2">
&rarr;
</span>
</Button>
</>
)}
@@ -97,10 +107,30 @@ export default async function BlogIndex({ params: { locale } }: BlogIndexProps)
</Heading>
<div className="flex flex-wrap gap-2 md:gap-4">
{/* Category filters could go here */}
<Badge variant="primary" className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4">{t('categories.all')}</Badge>
<Badge variant="neutral" className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4">{t('categories.industry')}</Badge>
<Badge variant="neutral" className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4">{t('categories.technical')}</Badge>
<Badge variant="neutral" className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4">{t('categories.sustainability')}</Badge>
<Badge
variant="primary"
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
>
{t('categories.all')}
</Badge>
<Badge
variant="neutral"
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
>
{t('categories.industry')}
</Badge>
<Badge
variant="neutral"
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
>
{t('categories.technical')}
</Badge>
<Badge
variant="neutral"
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
>
{t('categories.sustainability')}
</Badge>
</div>
</div>
</Reveal>
@@ -120,7 +150,10 @@ export default async function BlogIndex({ params: { locale } }: BlogIndexProps)
/>
<div className="absolute inset-0 image-overlay-gradient opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
{post.frontmatter.category && (
<Badge variant="accent" className="absolute top-3 left-3 md:top-6 md:left-6 shadow-lg">
<Badge
variant="accent"
className="absolute top-3 left-3 md:top-6 md:left-6 shadow-lg"
>
{post.frontmatter.category}
</Badge>
)}
@@ -131,7 +164,7 @@ export default async function BlogIndex({ params: { locale } }: BlogIndexProps)
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
year: 'numeric',
month: 'long',
day: 'numeric'
day: 'numeric',
})}
</div>
<h3 className="text-lg md:text-2xl font-bold text-primary mb-3 md:mb-6 group-hover:text-accent-dark transition-colors line-clamp-2 leading-tight">
@@ -145,8 +178,18 @@ export default async function BlogIndex({ params: { locale } }: BlogIndexProps)
{t('readMore')}
</span>
<div className="w-8 h-8 md:w-10 md:h-10 rounded-full bg-primary-light flex items-center justify-center text-saturated group-hover:bg-accent group-hover:text-primary-dark transition-all duration-300">
<svg className="w-4 h-4 md:w-5 md:h-5 transition-transform group-hover:translate-x-1" 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="w-4 h-4 md:w-5 md:h-5 transition-transform group-hover:translate-x-1"
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>
@@ -156,13 +199,21 @@ export default async function BlogIndex({ params: { locale } }: BlogIndexProps)
</Reveal>
))}
</div>
{/* Pagination Placeholder */}
<div className="mt-12 md:mt-24 flex justify-center gap-2 md:gap-4">
<Button variant="outline" size="sm" className="md:h-11 md:px-6 md:text-base" disabled>{t('prev')}</Button>
<Button variant="primary" size="sm" className="md:h-11 md:px-6 md:text-base">1</Button>
<Button variant="outline" size="sm" className="md:h-11 md:px-6 md:text-base">2</Button>
<Button variant="outline" size="sm" className="md:h-11 md:px-6 md:text-base">{t('next')}</Button>
<Button variant="outline" size="sm" className="md:h-11 md:px-6 md:text-base" disabled>
{t('prev')}
</Button>
<Button variant="primary" size="sm" className="md:h-11 md:px-6 md:text-base">
1
</Button>
<Button variant="outline" size="sm" className="md:h-11 md:px-6 md:text-base">
2
</Button>
<Button variant="outline" size="sm" className="md:h-11 md:px-6 md:text-base">
{t('next')}
</Button>
</div>
</Container>
</Section>

View File

@@ -1,11 +1,14 @@
import { ImageResponse } from 'next/og';
import { getTranslations } from 'next-intl/server';
import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const runtime = 'nodejs';
export default async function Image({ params: { locale } }: { params: { locale: string } }) {
const t = await getTranslations({ locale, namespace: 'Contact' });
const fonts = await getOgFonts();
const title = t('meta.title') || t('title');
const description = t('meta.description') || t('subtitle');
@@ -18,8 +21,8 @@ export default async function Image({ params: { locale } }: { params: { locale:
/>
),
{
width: 1200,
height: 630,
...OG_IMAGE_SIZE,
fonts,
}
);
}

View File

@@ -23,7 +23,9 @@ interface ContactPageProps {
};
}
export async function generateMetadata({ params: { locale } }: ContactPageProps): Promise<Metadata> {
export async function generateMetadata({
params: { locale },
}: ContactPageProps): Promise<Metadata> {
const t = await getTranslations({ locale, namespace: 'Contact' });
const title = t('meta.title') || t('title');
const description = t('meta.description') || t('subtitle');
@@ -31,7 +33,7 @@ export async function generateMetadata({ params: { locale } }: ContactPageProps)
title,
description,
alternates: {
canonical: `https://klz-cables.com/${locale}/contact`,
canonical: `${SITE_URL}/${locale}/contact`,
languages: {
'de-DE': '/de/contact',
'en-US': '/en/contact',
@@ -40,7 +42,7 @@ export async function generateMetadata({ params: { locale } }: ContactPageProps)
openGraph: {
title: `${title} | KLZ Cables`,
description,
url: `https://klz-cables.com/${locale}/contact`,
url: `${SITE_URL}/${locale}/contact`,
siteName: 'KLZ Cables',
images: getOGImageMetadata('contact', title, locale),
locale: `${locale.toUpperCase()}_DE`,
@@ -78,7 +80,7 @@ export default async function ContactPage({ params }: ContactPageProps) {
'@type': 'ListItem',
position: 1,
name: t('title'),
item: `https://klz-cables.com/${locale}/contact`,
item: `${SITE_URL}/${locale}/contact`,
},
],
}}
@@ -89,9 +91,9 @@ export default async function ContactPage({ params }: ContactPageProps) {
'@context': 'https://schema.org',
'@type': 'LocalBusiness',
name: 'KLZ Cables',
image: 'https://klz-cables.com/logo.png',
'@id': 'https://klz-cables.com',
url: 'https://klz-cables.com',
image: `${SITE_URL}/logo.png`,
'@id': SITE_URL,
url: SITE_URL,
address: {
'@type': 'PostalAddress',
streetAddress: 'Raiffeisenstraße 22',
@@ -107,20 +109,12 @@ export default async function ContactPage({ params }: ContactPageProps) {
openingHoursSpecification: [
{
'@type': 'OpeningHoursSpecification',
dayOfWeek: [
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday'
],
dayOfWeek: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
opens: '08:00',
closes: '17:00'
}
closes: '17:00',
},
],
sameAs: [
'https://www.linkedin.com/company/klz-cables'
]
sameAs: ['https://www.linkedin.com/company/klz-cables'],
}}
/>
{/* Hero Section */}
@@ -154,36 +148,71 @@ export default async function ContactPage({ params }: ContactPageProps) {
<div className="space-y-4 md:space-y-8">
<div className="flex items-start gap-4 md:gap-6 group">
<div className="w-10 h-10 md:w-14 md:h-14 rounded-xl md:rounded-2xl bg-saturated/10 flex items-center justify-center text-saturated group-hover:bg-accent group-hover:text-primary-dark transition-all duration-300 shadow-sm flex-shrink-0">
<svg className="w-5 h-5 md:w-7 md:h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
<svg
className="w-5 h-5 md:w-7 md:h-7"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</div>
<div>
<h4 className="text-base md:text-xl font-bold text-primary mb-1 md:mb-2">{t('info.office')}</h4>
<h4 className="text-base md:text-xl font-bold text-primary mb-1 md:mb-2">
{t('info.office')}
</h4>
<p className="text-sm md:text-lg text-text-secondary leading-relaxed whitespace-pre-line">
{t('info.address')}
</p>
</div>
</div>
<div className="flex items-start gap-4 md:gap-6 group">
<div className="w-10 h-10 md:w-14 md:h-14 rounded-xl md:rounded-2xl bg-saturated/10 flex items-center justify-center text-saturated group-hover:bg-accent group-hover:text-primary-dark transition-all duration-300 shadow-sm flex-shrink-0">
<svg className="w-5 h-5 md:w-7 md:h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
<svg
className="w-5 h-5 md:w-7 md:h-7"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
</div>
<div>
<h4 className="text-base md:text-xl font-bold text-primary mb-1 md:mb-2">{t('info.email')}</h4>
<a href="mailto:info@klz-cables.com" className="text-sm md:text-lg text-text-secondary hover:text-primary transition-colors font-medium touch-target">info@klz-cables.com</a>
<h4 className="text-base md:text-xl font-bold text-primary mb-1 md:mb-2">
{t('info.email')}
</h4>
<a
href="mailto:info@klz-cables.com"
className="text-sm md:text-lg text-text-secondary hover:text-primary transition-colors font-medium touch-target"
>
info@klz-cables.com
</a>
</div>
</div>
</div>
</div>
<div className="p-6 md:p-10 bg-white rounded-2xl md:rounded-3xl border border-neutral-medium shadow-sm animate-fade-in">
<Heading level={4} className="mb-4 md:mb-6">{t('hours.title')}</Heading>
<Heading level={4} className="mb-4 md:mb-6">
{t('hours.title')}
</Heading>
<ul className="space-y-2 md:space-y-4 list-none m-0 p-0">
<li className="flex justify-between items-center pb-2 md:pb-4 border-b border-neutral-medium text-sm md:text-base">
<span className="font-bold text-primary">{t('hours.weekdays')}</span>
@@ -199,24 +228,28 @@ export default async function ContactPage({ params }: ContactPageProps) {
{/* Contact Form */}
<div className="lg:col-span-7">
<Suspense fallback={<div className="animate-pulse bg-neutral-medium h-96 rounded-2xl md:rounded-3xl"></div>}>
<Suspense
fallback={
<div className="animate-pulse bg-neutral-medium h-96 rounded-2xl md:rounded-3xl"></div>
}
>
<ContactForm />
</Suspense>
</div>
</div>
</Container>
</Section>
{/* Map Section */}
<section className="h-[300px] md:h-[500px] bg-neutral-medium relative overflow-hidden grayscale hover:grayscale-0 transition-all duration-1000">
<Suspense fallback={<div className="h-full w-full bg-neutral-medium animate-pulse flex items-center justify-center">
<div className="text-primary font-medium">Loading Map...</div>
</div>}>
<LeafletMap
address={t('info.address')}
lat={48.8144}
lng={9.4144}
/>
<Suspense
fallback={
<div className="h-full w-full bg-neutral-medium animate-pulse flex items-center justify-center">
<div className="text-primary font-medium">Loading Map...</div>
</div>
}
>
<LeafletMap address={t('info.address')} lat={48.8144} lng={9.4144} />
</Suspense>
</section>
</div>

View File

@@ -2,14 +2,23 @@ import Footer from '@/components/Footer';
import Header from '@/components/Header';
import JsonLd from '@/components/JsonLd';
import AnalyticsProvider from '@/components/analytics/AnalyticsProvider';
import CMSConnectivityNotice from '@/components/CMSConnectivityNotice';
import { Metadata, Viewport } from 'next';
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
import '../../styles/globals.css';
import { SITE_URL } from '@/lib/schema';
import { config } from '@/lib/config';
export const metadata: Metadata = {
metadataBase: new URL(SITE_URL),
icons: {
icon: [
{ url: '/favicon.ico', sizes: 'any' },
{ url: '/logo-blue.svg', type: 'image/svg+xml' },
],
apple: [{ url: '/apple-touch-icon.png', sizes: '180x180', type: 'image/png' }],
},
};
export const viewport: Viewport = {
@@ -20,31 +29,30 @@ export const viewport: Viewport = {
viewportFit: 'cover',
themeColor: '#001a4d',
};
export default async function LocaleLayout({
children,
params: {locale}
params: { locale },
}: {
children: React.ReactNode;
params: {locale: string};
params: { locale: string };
}) {
// Providing all messages to the client
// side is the easiest way to get started
const messages = await getMessages();
return (
<html lang={locale} className="scroll-smooth overflow-x-hidden">
<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={locale}>
<JsonLd />
<Header />
<main className="flex-grow animate-fade-in overflow-visible">
{children}
</main>
<main className="flex-grow animate-fade-in overflow-visible">{children}</main>
<Footer />
<CMSConnectivityNotice />
{/* Sends pageviews for client-side navigations */}
<AnalyticsProvider />
<AnalyticsProvider websiteId={config.analytics.umami.websiteId} />
</NextIntlClientProvider>
</body>
</html>

View File

@@ -1,11 +1,13 @@
import { ImageResponse } from 'next/og';
import { getTranslations } from 'next-intl/server';
import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const runtime = 'nodejs';
export default async function Image({ params: { locale } }: { params: { locale: string } }) {
const t = await getTranslations({ locale, namespace: 'Index.meta' });
const fonts = await getOgFonts();
return new ImageResponse(
(
@@ -16,8 +18,9 @@ export default async function Image({ params: { locale } }: { params: { locale:
/>
),
{
width: 1200,
height: 630,
...OG_IMAGE_SIZE,
fonts,
}
);
}

View File

@@ -20,25 +20,45 @@ export default function HomePage({ params: { locale } }: { params: { locale: str
<div className="flex flex-col min-h-screen">
<JsonLd
id="breadcrumb-home"
data={getBreadcrumbSchema([
{ name: 'Home', item: `/${locale}` },
])}
data={getBreadcrumbSchema([{ name: 'Home', item: `/${locale}` }])}
/>
<Hero />
<Reveal><ProductCategories /></Reveal>
<Reveal><WhatWeDo /></Reveal>
<Reveal><RecentPosts locale={locale} /></Reveal>
<Reveal><Experience /></Reveal>
<Reveal><WhyChooseUs /></Reveal>
<Reveal><MeetTheTeam /></Reveal>
<Reveal><GallerySection /></Reveal>
<Reveal><VideoSection /></Reveal>
<Reveal><CTA /></Reveal>
<Reveal>
<ProductCategories />
</Reveal>
<Reveal>
<WhatWeDo />
</Reveal>
<Reveal>
<RecentPosts locale={locale} />
</Reveal>
<Reveal>
<Experience />
</Reveal>
<Reveal>
<WhyChooseUs />
</Reveal>
<Reveal>
<MeetTheTeam />
</Reveal>
<Reveal>
<GallerySection />
</Reveal>
<Reveal>
<VideoSection />
</Reveal>
<Reveal>
<CTA />
</Reveal>
</div>
);
}
export async function generateMetadata({ params: { locale } }: { params: { locale: string } }): Promise<Metadata> {
export async function generateMetadata({
params: { locale },
}: {
params: { locale: string };
}): Promise<Metadata> {
// Use translations for meta where available (namespace: Index.meta)
// Fallback to a sensible default if translation keys are missing.
let t;
@@ -62,15 +82,15 @@ export async function generateMetadata({ params: { locale } }: { params: { local
alternates: {
canonical: `/${locale}`,
languages: {
'de': '/de',
'en': '/en',
de: '/de',
en: '/en',
'x-default': '/en',
},
},
openGraph: {
title: `${title} | KLZ Cables`,
description,
url: `https://klz-cables.com/${locale}`,
url: `${SITE_URL}/${locale}`,
images: getOGImageMetadata('', title, locale),
},
twitter: {

View File

@@ -5,6 +5,7 @@ import ProductSidebar from '@/components/ProductSidebar';
import ProductTabs from '@/components/ProductTabs';
import ProductTechnicalData from '@/components/ProductTechnicalData';
import RelatedProducts from '@/components/RelatedProducts';
import DatasheetDownload from '@/components/DatasheetDownload';
import { Badge, Container, Heading, Section } from '@/components/ui';
import { getDatasheetPath } from '@/lib/datasheets';
import { getAllProducts, getProductBySlug } from '@/lib/mdx';
@@ -30,12 +31,23 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
const t = await getTranslations('Products');
// Check if it's a category page
const categories = ['low-voltage-cables', 'medium-voltage-cables', 'high-voltage-cables', 'solar-cables'];
const categories = [
'low-voltage-cables',
'medium-voltage-cables',
'high-voltage-cables',
'solar-cables',
];
const fileSlug = await mapSlugToFileSlug(productSlug, locale);
if (categories.includes(fileSlug)) {
const categoryKey = fileSlug.replace(/-cables$/, '').replace(/-([a-z])/g, (g) => g[1].toUpperCase());
const categoryTitle = t.has(`categories.${categoryKey}.title`) ? t(`categories.${categoryKey}.title`) : fileSlug;
const categoryDesc = t.has(`categories.${categoryKey}.description`) ? t(`categories.${categoryKey}.description`) : '';
const categoryKey = fileSlug
.replace(/-cables$/, '')
.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
const categoryTitle = t.has(`categories.${categoryKey}.title`)
? t(`categories.${categoryKey}.title`)
: fileSlug;
const categoryDesc = t.has(`categories.${categoryKey}.description`)
? t(`categories.${categoryKey}.description`)
: '';
return {
title: categoryTitle,
@@ -43,15 +55,15 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
alternates: {
canonical: `/${locale}/products/${productSlug}`,
languages: {
'de': `/de/products/${await mapFileSlugToTranslated(productSlug, 'de')}`,
'en': `/en/products/${await mapFileSlugToTranslated(productSlug, 'en')}`,
de: `/de/products/${await mapFileSlugToTranslated(productSlug, 'de')}`,
en: `/en/products/${await mapFileSlugToTranslated(productSlug, 'en')}`,
'x-default': `/en/products/${await mapFileSlugToTranslated(productSlug, 'en')}`,
},
},
openGraph: {
title: `${categoryTitle} | KLZ Cables`,
description: categoryDesc,
url: `https://klz-cables.com/${locale}/products/${productSlug}`,
url: `${SITE_URL}/${locale}/products/${productSlug}`,
images: getProductOGImageMetadata(fileSlug, categoryTitle, locale),
},
twitter: {
@@ -71,8 +83,8 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
alternates: {
canonical: `/${locale}/products/${slug.join('/')}`,
languages: {
'de': `/de/products/${await mapFileSlugToTranslated(slug[0], 'de')}/${await mapFileSlugToTranslated(productSlug, 'de')}`,
'en': `/en/products/${await mapFileSlugToTranslated(slug[0], 'en')}/${await mapFileSlugToTranslated(productSlug, 'en')}`,
de: `/de/products/${await mapFileSlugToTranslated(slug[0], 'de')}/${await mapFileSlugToTranslated(productSlug, 'de')}`,
en: `/en/products/${await mapFileSlugToTranslated(slug[0], 'en')}/${await mapFileSlugToTranslated(productSlug, 'en')}`,
'x-default': `/en/products/${await mapFileSlugToTranslated(slug[0], 'en')}/${await mapFileSlugToTranslated(productSlug, 'en')}`,
},
},
@@ -80,7 +92,7 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
title: `${product.frontmatter.title} | KLZ Cables`,
description: product.frontmatter.description,
type: 'website',
url: `https://klz-cables.com/${locale}/products/${slug.join('/')}`,
url: `${SITE_URL}/${locale}/products/${slug.join('/')}`,
images: getProductOGImageMetadata(productSlug, product.frontmatter.title, locale),
},
twitter: {
@@ -94,20 +106,36 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
const components = {
ProductTechnicalData,
ProductTabs,
p: (props: any) => <p {...props} className="text-lg md:text-xl text-text-secondary leading-relaxed mb-8 font-medium" />,
p: (props: any) => (
<p
{...props}
className="text-lg md:text-xl text-text-secondary leading-relaxed mb-8 font-medium"
/>
),
h2: (props: any) => (
<div className="relative mb-16">
<h2 {...props} className="text-3xl md:text-4xl font-black text-primary tracking-tighter uppercase mb-6" />
<h2
{...props}
className="text-3xl md:text-4xl font-black text-primary tracking-tighter uppercase mb-6"
/>
<div className="w-20 h-1.5 bg-accent rounded-full" />
</div>
),
h3: (props: any) => <h3 {...props} className="text-2xl md:text-3xl font-black text-primary mb-10 tracking-tight uppercase" />,
h3: (props: any) => (
<h3
{...props}
className="text-2xl md:text-3xl font-black text-primary mb-10 tracking-tight uppercase"
/>
),
ul: (props: any) => <ul {...props} className="list-none pl-0 mb-10" />,
section: (props: any) => <div {...props} className="block" />,
li: (props: any) => (
<li className="flex items-start gap-4 group mb-4 last:mb-0">
<div className="mt-2.5 w-2 h-2 rounded-full bg-accent flex-shrink-0 group-hover:scale-125 transition-transform" />
<span {...props} className="text-lg md:text-xl text-text-secondary leading-relaxed font-medium" />
<span
{...props}
className="text-lg md:text-xl text-text-secondary leading-relaxed font-medium"
/>
</li>
),
strong: (props: any) => <strong {...props} className="font-black text-primary" />,
@@ -116,13 +144,26 @@ const components = {
<table {...props} className="min-w-full divide-y divide-neutral-dark/10" />
</div>
),
th: (props: any) => <th {...props} className="px-8 py-6 bg-neutral-light/50 text-left text-[10px] font-black uppercase tracking-[0.25em] text-primary/60" />,
td: (props: any) => <td {...props} className="px-8 py-6 text-text-secondary border-t border-neutral-dark/5 text-lg md:text-xl font-medium" />,
th: (props: any) => (
<th
{...props}
className="px-8 py-6 bg-neutral-light/50 text-left text-[10px] font-black uppercase tracking-[0.25em] text-primary/60"
/>
),
td: (props: any) => (
<td
{...props}
className="px-8 py-6 text-text-secondary border-t border-neutral-dark/5 text-lg md:text-xl font-medium"
/>
),
hr: () => <hr className="my-24 border-t-2 border-neutral-dark/5" />,
blockquote: (props: any) => (
<div className="my-20 p-10 md:p-16 bg-primary-dark rounded-[40px] relative overflow-hidden group">
<div className="absolute top-0 right-0 w-64 h-64 bg-accent/10 rounded-full -translate-y-1/2 translate-x-1/2 blur-3xl group-hover:bg-accent/20 transition-colors duration-700" />
<div className="relative z-10 italic text-2xl md:text-3xl text-white/90 leading-relaxed font-black tracking-tight" {...props} />
<div
className="relative z-10 italic text-2xl md:text-3xl text-white/90 leading-relaxed font-black tracking-tight"
{...props}
/>
</div>
),
};
@@ -133,28 +174,36 @@ export default async function ProductPage({ params }: ProductPageProps) {
const t = await getTranslations('Products');
// Check if it's a category page
const categories = ['low-voltage-cables', 'medium-voltage-cables', 'high-voltage-cables', 'solar-cables'];
const categories = [
'low-voltage-cables',
'medium-voltage-cables',
'high-voltage-cables',
'solar-cables',
];
const fileSlug = await mapSlugToFileSlug(productSlug, locale);
if (categories.includes(fileSlug)) {
const allProducts = await getAllProducts(locale);
const categoryKey = fileSlug.replace(/-cables$/, '').replace(/-([a-z])/g, (g) => g[1].toUpperCase());
const categoryTitle = t.has(`categories.${categoryKey}.title`) ? t(`categories.${categoryKey}.title`) : fileSlug;
const categoryKey = fileSlug
.replace(/-cables$/, '')
.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
const categoryTitle = t.has(`categories.${categoryKey}.title`)
? t(`categories.${categoryKey}.title`)
: fileSlug;
// Filter products for this category
const filteredProducts = allProducts.filter(p =>
p.frontmatter.categories.some(cat =>
cat.toLowerCase().replace(/\s+/g, '-') === fileSlug ||
cat === categoryTitle
)
const filteredProducts = allProducts.filter((p) =>
p.frontmatter.categories.some(
(cat) => cat.toLowerCase().replace(/\s+/g, '-') === fileSlug || cat === categoryTitle,
),
);
// Get translated product slugs
const productsWithTranslatedSlugs = await Promise.all(
filteredProducts.map(async (p) => ({
...p,
translatedSlug: await mapFileSlugToTranslated(p.slug, locale)
}))
translatedSlug: await mapFileSlugToTranslated(p.slug, locale),
})),
);
return (
@@ -163,7 +212,9 @@ export default async function ProductPage({ params }: ProductPageProps) {
<Container className="relative z-10">
<div className="max-w-4xl animate-slide-up">
<nav className="flex items-center mb-8 text-white/40 text-sm font-bold uppercase tracking-widest">
<Link href={`/${locale}/products`} className="hover:text-accent transition-colors">{t('title')}</Link>
<Link href={`/${locale}/products`} className="hover:text-accent transition-colors">
{t('title')}
</Link>
<span className="mx-3 opacity-30">/</span>
<span className="text-white/90">{categoryTitle}</span>
</nav>
@@ -201,7 +252,10 @@ export default async function ProductPage({ params }: ProductPageProps) {
<div className="p-8 md:p-10">
<div className="flex flex-wrap gap-2 mb-4">
{product.frontmatter.categories.map((cat, i) => (
<span key={i} className="text-[10px] font-bold uppercase tracking-widest text-primary/40">
<span
key={i}
className="text-[10px] font-bold uppercase tracking-widest text-primary/40"
>
{cat}
</span>
))}
@@ -216,8 +270,18 @@ export default async function ProductPage({ params }: ProductPageProps) {
<span className="border-b-2 border-primary/10 group-hover:border-accent-dark transition-colors pb-1">
{t('details')}
</span>
<svg className="w-5 h-5 ml-3 transition-transform group-hover:translate-x-1" 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="w-5 h-5 ml-3 transition-transform group-hover:translate-x-1"
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>
@@ -237,7 +301,9 @@ export default async function ProductPage({ params }: ProductPageProps) {
}
// Extract technical data for schema
const technicalDataMatch = product.content.match(/technicalData=\{<ProductTechnicalData data=\{(.*?)\}\s*\/>\}/s);
const technicalDataMatch = product.content.match(
/technicalData=\{<ProductTechnicalData data=\{(.*?)\}\s*\/>\}/s,
);
let technicalItems = [];
if (technicalDataMatch) {
try {
@@ -252,11 +318,15 @@ export default async function ProductPage({ params }: ProductPageProps) {
const isFallback = (product.frontmatter as any).isFallback;
const categorySlug = slug[0];
const categoryFileSlug = await mapSlugToFileSlug(categorySlug, locale);
const categoryKey = categoryFileSlug.replace(/-cables$/, '').replace(/-([a-z])/g, (g) => g[1].toUpperCase());
const categoryTitle = t.has(`categories.${categoryKey}.title`) ? t(`categories.${categoryKey}.title`) : categoryFileSlug;
const categoryKey = categoryFileSlug
.replace(/-cables$/, '')
.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
const categoryTitle = t.has(`categories.${categoryKey}.title`)
? t(`categories.${categoryKey}.title`)
: categoryFileSlug;
const sidebar = (
<ProductSidebar
<ProductSidebar
productName={product.frontmatter.title}
productImage={product.frontmatter.images?.[0]}
datasheetPath={datasheetPath}
@@ -286,17 +356,24 @@ export default async function ProductPage({ params }: ProductPageProps) {
{/* Background Decorative Elements */}
<div className="absolute top-0 right-0 w-1/2 h-full bg-gradient-to-l from-accent/5 to-transparent pointer-events-none" />
<div className="absolute -top-24 -right-24 w-96 h-96 bg-accent/10 rounded-full blur-3xl pointer-events-none" />
<Container className="relative z-10">
<div className="max-w-4xl animate-slide-up">
<nav className="flex items-center mb-12 text-white/40 text-[10px] font-black uppercase tracking-[0.2em]">
<Link href={`/${locale}/products`} className="hover:text-accent transition-colors">{t('title')}</Link>
<Link href={`/${locale}/products`} className="hover:text-accent transition-colors">
{t('title')}
</Link>
<span className="mx-4 opacity-20">/</span>
<Link href={`/${locale}/products/${categorySlug}`} className="hover:text-accent transition-colors">{categoryTitle}</Link>
<Link
href={`/${locale}/products/${categorySlug}`}
className="hover:text-accent transition-colors"
>
{categoryTitle}
</Link>
<span className="mx-4 opacity-20">/</span>
<span className="text-white/90">{product.frontmatter.title}</span>
</nav>
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-12">
<div className="flex-1">
{isFallback && (
@@ -307,7 +384,11 @@ export default async function ProductPage({ params }: ProductPageProps) {
)}
<div className="flex flex-wrap gap-3 mb-8">
{product.frontmatter.categories.map((cat, idx) => (
<Badge key={idx} variant="accent" className="bg-white/5 text-white/80 border-white/10 backdrop-blur-md px-5 py-2 text-[10px] font-black tracking-[0.15em]">
<Badge
key={idx}
variant="accent"
className="bg-white/5 text-white/80 border-white/10 backdrop-blur-md px-5 py-2 text-[10px] font-black tracking-[0.15em]"
>
{cat}
</Badge>
))}
@@ -328,11 +409,14 @@ export default async function ProductPage({ params }: ProductPageProps) {
<Container className="relative">
{/* Large Product Image Section */}
{product.frontmatter.images && product.frontmatter.images.length > 0 && (
<div className="relative -mt-32 mb-32 animate-slide-up" style={{ animationDelay: '200ms' }}>
<div
className="relative -mt-32 mb-32 animate-slide-up"
style={{ animationDelay: '200ms' }}
>
<div className="bg-white shadow-[0_32px_64px_-12px_rgba(0,0,0,0.1)] rounded-[48px] border border-neutral-dark/5 overflow-hidden p-12 md:p-20 lg:p-24">
<div className="relative w-full aspect-[21/9]">
<Image
src={product.frontmatter.images[0]}
<Image
src={product.frontmatter.images[0]}
alt={product.frontmatter.title}
fill
className="object-contain transition-transform duration-1000 hover:scale-105"
@@ -341,12 +425,20 @@ export default async function ProductPage({ params }: ProductPageProps) {
{/* Subtle reflection/shadow effect */}
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-3/4 h-12 bg-black/5 blur-3xl rounded-[100%]" />
</div>
{product.frontmatter.images.length > 1 && (
<div className="flex justify-center gap-8 mt-20">
{product.frontmatter.images.slice(0, 5).map((img, idx) => (
<div key={idx} className="relative w-24 h-24 md:w-32 md:h-32 border-2 border-neutral-dark/5 rounded-3xl overflow-hidden bg-neutral-light/30 hover:border-accent transition-all duration-500 cursor-pointer group p-4">
<Image src={img} alt="" fill className="object-contain p-4 transition-transform duration-700 group-hover:scale-110" />
<div
key={idx}
className="relative w-24 h-24 md:w-32 md:h-32 border-2 border-neutral-dark/5 rounded-3xl overflow-hidden bg-neutral-light/30 hover:border-accent transition-all duration-500 cursor-pointer group p-4"
>
<Image
src={img}
alt=""
fill
className="object-contain p-4 transition-transform duration-700 group-hover:scale-110"
/>
</div>
))}
</div>
@@ -359,51 +451,68 @@ export default async function ProductPage({ params }: ProductPageProps) {
<div className="w-full">
{/* Main Content Area */}
<div className="max-w-none">
<MDXRemote source={processedContent} components={productComponents} />
<MDXRemote source={processedContent} components={productComponents} />
</div>
{/* Datasheet Download Section - Only for Medium Voltage for now */}
{categoryFileSlug === 'medium-voltage-cables' && datasheetPath && (
<div className="mt-24 pt-24 border-t-2 border-neutral-dark/5">
<div className="mb-12">
<h2 className="text-3xl md:text-4xl font-black text-primary tracking-tighter uppercase mb-4">
{t('downloadDatasheet')}
</h2>
<div className="h-1.5 w-24 bg-accent rounded-full" />
</div>
<DatasheetDownload datasheetPath={datasheetPath} />
</div>
)}
{/* Structured Data */}
<JsonLd
id={`jsonld-${product.slug}`}
data={{
'@context': 'https://schema.org',
'@type': 'Product',
name: product.frontmatter.title,
description: product.frontmatter.description,
sku: product.frontmatter.sku || product.slug.toUpperCase(),
image: product.frontmatter.images?.[0] ? `https://klz-cables.com${product.frontmatter.images[0]}` : undefined,
brand: {
'@type': 'Brand',
name: 'KLZ Cables',
},
offers: {
'@type': 'Offer',
availability: 'https://schema.org/InStock',
priceCurrency: 'EUR',
url: `https://klz-cables.com/${locale}/products/${slug.join('/')}`,
itemCondition: 'https://schema.org/NewCondition',
},
additionalProperty: technicalItems.map((item: any) => ({
'@type': 'PropertyValue',
name: item.label,
value: item.value,
})),
category: product.frontmatter.categories.join(', '),
mainEntityOfPage: {
'@type': 'WebPage',
'@id': `https://klz-cables.com/${locale}/products/${slug.join('/')}`,
},
} as any}
data={
{
'@context': 'https://schema.org',
'@type': 'Product',
name: product.frontmatter.title,
description: product.frontmatter.description,
sku: product.frontmatter.sku || product.slug.toUpperCase(),
image: product.frontmatter.images?.[0]
? `${SITE_URL}${product.frontmatter.images[0]}`
: undefined,
brand: {
'@type': 'Brand',
name: 'KLZ Cables',
},
offers: {
'@type': 'Offer',
availability: 'https://schema.org/InStock',
priceCurrency: 'EUR',
url: `${SITE_URL}/${locale}/products/${slug.join('/')}`,
itemCondition: 'https://schema.org/NewCondition',
},
additionalProperty: technicalItems.map((item: any) => ({
'@type': 'PropertyValue',
name: item.label,
value: item.value,
})),
category: product.frontmatter.categories.join(', '),
mainEntityOfPage: {
'@type': 'WebPage',
'@id': `${SITE_URL}/${locale}/products/${slug.join('/')}`,
},
} as any
}
/>
</div>
</div>
{/* Related Products Section */}
<div className="mt-16 pt-16 border-t border-neutral-dark/5">
<RelatedProducts
currentSlug={productSlug}
categories={product.frontmatter.categories}
locale={locale}
<RelatedProducts
currentSlug={productSlug}
categories={product.frontmatter.categories}
locale={locale}
/>
</div>
</Container>

View File

@@ -1,83 +1,29 @@
import { ImageResponse } from 'next/og';
import { getProductBySlug } from '@/lib/mdx';
import { getTranslations } from 'next-intl/server';
import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const runtime = 'nodejs';
export default async function Image({ params: { locale, slug } }: { params: { locale: string, slug?: string[] } }) {
const t = await getTranslations('Products');
export default async function Image({ params: { locale } }: { params: { locale: string } }) {
const t = await getTranslations({ locale, namespace: 'Products' });
const fonts = await getOgFonts();
// If no slug, it's the main products page
if (!slug || slug.length === 0) {
const title = t('meta.title') || t('title');
const description = t('meta.description') || t('subtitle');
return new ImageResponse(
(
<OGImageTemplate
title={title}
description={description}
label="Products"
/>
),
{
width: 1200,
height: 630,
}
);
}
const productSlug = slug[slug.length - 1];
// Check if it's a category page
const categories = ['low-voltage-cables', 'medium-voltage-cables', 'high-voltage-cables', 'solar-cables'];
if (categories.includes(productSlug)) {
const categoryKey = productSlug.replace(/-cables$/, '').replace(/-([a-z])/g, (g) => g[1].toUpperCase());
const categoryTitle = t.has(`categories.${categoryKey}.title`) ? t(`categories.${categoryKey}.title`) : productSlug;
const categoryDesc = t.has(`categories.${categoryKey}.description`) ? t(`categories.${categoryKey}.description`) : '';
return new ImageResponse(
(
<OGImageTemplate
title={categoryTitle}
description={categoryDesc}
label="Product Category"
/>
),
{
width: 1200,
height: 630,
}
);
}
const product = await getProductBySlug(productSlug, locale);
if (!product) {
return new ImageResponse(
<div style={{ display: 'flex', width: '100%', height: '100%', backgroundColor: '#001a4d' }} />
);
}
const featuredImage = product.frontmatter.images?.[0]
? (product.frontmatter.images[0].startsWith('http')
? product.frontmatter.images[0]
: `https://klz-cables.com${product.frontmatter.images[0]}`)
: undefined;
const title = t('meta.title') || t('title');
const description = t('meta.description') || t('subtitle');
return new ImageResponse(
(
<OGImageTemplate
title={product.frontmatter.title}
description={product.frontmatter.description}
label={product.frontmatter.categories?.[0] || 'Product'}
image={featuredImage}
title={title}
description={description}
label="Products"
/>
),
{
width: 1200,
height: 630,
...OG_IMAGE_SIZE,
fonts,
}
);
}

View File

@@ -7,6 +7,7 @@ import Image from 'next/image';
import Link from 'next/link';
import { mapFileSlugToTranslated } from '@/lib/slugs';
import { getOGImageMetadata } from '@/lib/metadata';
import { SITE_URL } from '@/lib/schema';
interface ProductsPageProps {
params: {
@@ -14,7 +15,9 @@ interface ProductsPageProps {
};
}
export async function generateMetadata({ params: { locale } }: ProductsPageProps): Promise<Metadata> {
export async function generateMetadata({
params: { locale },
}: ProductsPageProps): Promise<Metadata> {
const t = await getTranslations({ locale, namespace: 'Products' });
const title = t('meta.title') || t('title');
const description = t('meta.description') || t('subtitle');
@@ -24,15 +27,15 @@ export async function generateMetadata({ params: { locale } }: ProductsPageProps
alternates: {
canonical: `/${locale}/products`,
languages: {
'de': '/de/products',
'en': '/en/products',
de: '/de/products',
en: '/en/products',
'x-default': '/en/products',
},
},
openGraph: {
title: `${title} | KLZ Cables`,
description,
url: `https://klz-cables.com/${locale}/products`,
url: `${SITE_URL}/${locale}/products`,
images: getOGImageMetadata('products', title, locale),
},
twitter: {
@@ -58,29 +61,29 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
desc: t('categories.lowVoltage.description'),
img: '/uploads/2024/11/low-voltage-category.webp',
icon: '/uploads/2024/11/Low-Voltage.svg',
href: `/${params.locale}/products/${lowVoltageSlug}`
href: `/${params.locale}/products/${lowVoltageSlug}`,
},
{
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: `/${params.locale}/products/${mediumVoltageSlug}`
href: `/${params.locale}/products/${mediumVoltageSlug}`,
},
{
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: `/${params.locale}/products/${highVoltageSlug}`
href: `/${params.locale}/products/${highVoltageSlug}`,
},
{
title: t('categories.solar.title'),
desc: t('categories.solar.description'),
img: '/uploads/2024/11/solar-category.webp',
icon: '/uploads/2024/11/Solar.svg',
href: `/${params.locale}/products/${solarSlug}`
}
href: `/${params.locale}/products/${solarSlug}`,
},
];
return (
@@ -89,7 +92,10 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
<section className="relative min-h-[50vh] md:min-h-[70vh] flex items-center pt-32 pb-20 md:pt-40 md:pb-32 overflow-hidden bg-primary-dark">
<Container className="relative z-10">
<div className="max-w-4xl animate-slide-up">
<Badge variant="saturated" className="mb-4 md:mb-8 shadow-lg px-3 py-1 md:px-4 md:py-1.5">
<Badge
variant="saturated"
className="mb-4 md:mb-8 shadow-lg px-3 py-1 md:px-4 md:py-1.5"
>
{t('heroSubtitle')}
</Badge>
<Heading level={1} className="text-white mb-4 md:mb-8">
@@ -97,16 +103,24 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
green: (chunks) => (
<span className="relative inline-block">
<span className="relative z-10 text-accent italic">{chunks}</span>
<Scribble variant="circle" className="w-[140%] h-[140%] -top-[20%] -left-[20%] text-accent/30 hidden md:block" />
<Scribble
variant="circle"
className="w-[140%] h-[140%] -top-[20%] -left-[20%] text-accent/30 hidden md:block"
/>
</span>
)
),
})}
</Heading>
<p className="text-lg md:text-xl text-white/70 leading-relaxed max-w-2xl mb-8 md:mb-12 line-clamp-2 md:line-clamp-none">
{t('subtitle')}
</p>
<div className="flex flex-wrap gap-4 md:gap-6">
<Button href="#categories" variant="accent" size="lg" className="group w-full md:w-auto">
<Button
href="#categories"
variant="accent"
size="lg"
className="group w-full md:w-auto"
>
{t('viewProducts')}
<span className="ml-3 transition-transform group-hover:translate-y-1"></span>
</Button>
@@ -123,8 +137,8 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
<Link key={idx} href={category.href} className="group block">
<Card className="h-full border-none shadow-sm hover:shadow-2xl transition-all duration-500 rounded-[24px] md:rounded-[48px] overflow-hidden bg-white active:scale-[0.98]">
<div className="relative h-[200px] md:h-[400px] overflow-hidden">
<Image
src={category.img}
<Image
src={category.img}
alt={category.title}
fill
className="object-cover transition-transform duration-1000 group-hover:scale-105"
@@ -132,13 +146,22 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
unoptimized
/>
<div className="absolute inset-0 image-overlay-gradient opacity-80 group-hover:opacity-90 transition-opacity duration-500" />
<div className="absolute top-3 right-3 md:top-8 md:right-8 w-10 h-10 md:w-20 md:h-20 bg-white/10 backdrop-blur-md rounded-xl md:rounded-[24px] flex items-center justify-center border border-white/20 shadow-2xl transition-all duration-500 group-hover:scale-110 group-hover:bg-white/20">
<Image src={category.icon} alt="" width={24} height={24} className="w-6 h-6 md:w-12 md:h-12 brightness-0 invert opacity-80" />
<Image
src={category.icon}
alt=""
width={24}
height={24}
className="w-6 h-6 md:w-12 md:h-12 brightness-0 invert opacity-80"
/>
</div>
<div className="absolute bottom-4 left-4 md:bottom-10 md:left-10 right-4 md:right-10">
<Badge variant="accent" className="mb-2 md:mb-4 shadow-lg bg-accent text-primary-dark border-none text-[10px] md:text-xs">
<Badge
variant="accent"
className="mb-2 md:mb-4 shadow-lg bg-accent text-primary-dark border-none text-[10px] md:text-xs"
>
{t('categoryLabel')}
</Badge>
<h2 className="text-xl md:text-4xl font-bold text-white leading-tight transition-transform duration-500 group-hover:translate-x-1">
@@ -155,8 +178,18 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
{t('viewProducts')}
</span>
<div className="ml-3 md:ml-4 w-8 h-8 md:w-10 md:h-10 rounded-full bg-primary-light flex items-center justify-center text-saturated group-hover:bg-accent group-hover:text-primary-dark transition-all duration-300 shadow-sm">
<svg className="w-4 h-4 md:w-5 md:h-5 transition-transform group-hover:translate-x-1" 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="w-4 h-4 md:w-5 md:h-5 transition-transform group-hover:translate-x-1"
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>
@@ -168,7 +201,7 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
</div>
</Container>
</Section>
{/* Technical Support CTA */}
<Reveal>
<Section className="bg-white py-12 md:py-28">
@@ -177,14 +210,23 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
<div className="absolute top-0 right-0 w-1/2 h-full bg-accent/5 -skew-x-12 translate-x-1/4" />
<div className="relative z-10 flex flex-col lg:flex-row items-center justify-between gap-6 md:gap-12">
<div className="max-w-2xl text-center lg:text-left">
<h2 className="text-2xl md:text-5xl lg:text-6xl font-bold text-white mb-4 md:mb-8 tracking-tight">{t('cta.title')}</h2>
<h2 className="text-2xl md:text-5xl lg:text-6xl font-bold text-white mb-4 md:mb-8 tracking-tight">
{t('cta.title')}
</h2>
<p className="text-base md:text-xl text-white/70 leading-relaxed">
{t('cta.description')}
</p>
</div>
<Button href={`/${params.locale}/contact`} variant="accent" size="lg" className="group whitespace-nowrap w-full md:w-auto md:h-16 md:px-10 md:text-xl">
<Button
href={`/${params.locale}/contact`}
variant="accent"
size="lg"
className="group whitespace-nowrap w-full md:w-auto md:h-16 md:px-10 md:text-xl"
>
{t('cta.button')}
<span className="ml-4 transition-transform group-hover:translate-x-2">&rarr;</span>
<span className="ml-4 transition-transform group-hover:translate-x-2">
&rarr;
</span>
</Button>
</div>
</div>

View File

@@ -1,11 +1,14 @@
import { ImageResponse } from 'next/og';
import { getTranslations } from 'next-intl/server';
import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const runtime = 'nodejs';
export default async function Image({ params: { locale } }: { params: { locale: string } }) {
const t = await getTranslations({ locale, namespace: 'Team' });
const fonts = await getOgFonts();
const title = t('meta.title') || t('hero.subtitle');
const description = t('meta.description') || t('hero.title');
@@ -15,12 +18,12 @@ export default async function Image({ params: { locale } }: { params: { locale:
title={title}
description={description}
label="Our Team"
image="https://klz-cables.com/uploads/2024/12/DSC07655-Large.webp"
/>
),
{
width: 1200,
height: 630,
...OG_IMAGE_SIZE,
fonts,
}
);
}

View File

@@ -24,15 +24,15 @@ export async function generateMetadata({ params: { locale } }: TeamPageProps): P
alternates: {
canonical: `/${locale}/team`,
languages: {
'de': '/de/team',
'en': '/en/team',
de: '/de/team',
en: '/en/team',
'x-default': '/en/team',
},
},
openGraph: {
title: `${title} | KLZ Cables`,
description,
url: `https://klz-cables.com/${locale}/team`,
url: `${SITE_URL}/${locale}/team`,
images: getOGImageMetadata('team', title, locale),
},
twitter: {
@@ -50,9 +50,7 @@ export default async function TeamPage({ params: { locale } }: TeamPageProps) {
<div className="flex flex-col min-h-screen bg-neutral-light">
<JsonLd
id="breadcrumb-team"
data={getBreadcrumbSchema([
{ name: t('hero.subtitle'), item: `/team` },
])}
data={getBreadcrumbSchema([{ name: t('hero.subtitle'), item: `/team` }])}
/>
<JsonLd
id="person-michael"
@@ -65,10 +63,8 @@ export default async function TeamPage({ params: { locale } }: TeamPageProps) {
'@type': 'Organization',
name: 'KLZ Cables',
},
sameAs: [
'https://www.linkedin.com/in/michael-bodemer-33b493122/'
],
image: `${SITE_URL}/uploads/2024/12/DSC07768-Large.webp`
sameAs: ['https://www.linkedin.com/in/michael-bodemer-33b493122/'],
image: `${SITE_URL}/uploads/2024/12/DSC07768-Large.webp`,
}}
/>
<JsonLd
@@ -82,10 +78,8 @@ export default async function TeamPage({ params: { locale } }: TeamPageProps) {
'@type': 'Organization',
name: 'KLZ Cables',
},
sameAs: [
'https://www.linkedin.com/in/klaus-mintel-b80a8b193/'
],
image: `${SITE_URL}/uploads/2024/12/DSC07963-Large.webp`
sameAs: ['https://www.linkedin.com/in/klaus-mintel-b80a8b193/'],
image: `${SITE_URL}/uploads/2024/12/DSC07963-Large.webp`,
}}
/>
{/* Hero Section */}
@@ -101,9 +95,11 @@ export default async function TeamPage({ params: { locale } }: TeamPageProps) {
/>
<div className="absolute inset-0 bg-gradient-to-b from-primary-dark/80 via-primary-dark/40 to-primary-dark/80" />
</div>
<Container className="relative z-10 text-center text-white max-w-5xl">
<Badge variant="saturated" className="mb-4 md:mb-8 shadow-lg">{t('hero.badge')}</Badge>
<Badge variant="saturated" className="mb-4 md:mb-8 shadow-lg">
{t('hero.badge')}
</Badge>
<Heading level={1} className="text-white mb-4 md:mb-8">
{t('hero.subtitle')}
</Heading>
@@ -120,7 +116,9 @@ export default async function TeamPage({ params: { locale } }: TeamPageProps) {
<Reveal className="w-full lg:w-1/2 p-6 md:p-24 lg:p-32 flex flex-col justify-center bg-primary-dark text-white relative order-2 lg:order-1">
<div className="absolute top-0 right-0 w-32 h-full bg-accent/5 -skew-x-12 translate-x-1/2" />
<div className="relative z-10">
<Badge variant="accent" className="mb-4 md:mb-8">{t('michael.role')}</Badge>
<Badge variant="accent" className="mb-4 md:mb-8">
{t('michael.role')}
</Badge>
<Heading level={2} className="text-white mb-6 md:mb-10 text-3xl md:text-5xl">
<span className="text-white">{t('michael.name')}</span>
</Heading>
@@ -133,9 +131,9 @@ export default async function TeamPage({ params: { locale } }: TeamPageProps) {
<p className="text-base md:text-xl leading-relaxed text-white/70 mb-6 md:mb-12 max-w-xl">
{t('michael.description')}
</p>
<Button
href="https://www.linkedin.com/in/michael-bodemer-33b493122/"
variant="accent"
<Button
href="https://www.linkedin.com/in/michael-bodemer-33b493122/"
variant="accent"
size="lg"
className="group w-full md:w-auto md:h-16 md:px-10 md:text-xl active:scale-95 transition-transform"
>
@@ -173,26 +171,36 @@ export default async function TeamPage({ params: { locale } }: TeamPageProps) {
<Container className="relative z-10">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 md:gap-16 items-center">
<div className="lg:col-span-6">
<Heading level={2} subtitle={t('legacy.subtitle')} className="text-white mb-6 md:mb-10">
<Heading
level={2}
subtitle={t('legacy.subtitle')}
className="text-white mb-6 md:mb-10"
>
<span className="text-white">{t('legacy.title')}</span>
</Heading>
<div className="space-y-4 md:space-y-8 text-base md:text-2xl text-white/80 leading-relaxed font-medium">
<p className="border-l-4 border-accent pl-5 md:pl-8 py-2 bg-white/5 backdrop-blur-sm rounded-r-xl md:rounded-r-2xl">
{t('legacy.p1')}
</p>
<p className="pl-6 md:pl-9 line-clamp-3 md:line-clamp-none">
{t('legacy.p2')}
</p>
<p className="pl-6 md:pl-9 line-clamp-3 md:line-clamp-none">{t('legacy.p2')}</p>
</div>
</div>
<div className="lg:col-span-6 grid grid-cols-2 md:grid-cols-2 gap-3 md:gap-6">
<div className="p-4 md:p-8 bg-white/5 backdrop-blur-md border border-white/10 rounded-2xl md:rounded-[32px] hover:bg-white/10 transition-colors">
<div className="text-xl md:text-4xl font-extrabold text-accent mb-1 md:mb-2">{t('legacy.expertise')}</div>
<div className="text-[10px] md:text-sm font-bold uppercase tracking-widest text-white/50">{t('legacy.expertiseDesc')}</div>
<div className="text-xl md:text-4xl font-extrabold text-accent mb-1 md:mb-2">
{t('legacy.expertise')}
</div>
<div className="text-[10px] md:text-sm font-bold uppercase tracking-widest text-white/50">
{t('legacy.expertiseDesc')}
</div>
</div>
<div className="p-4 md:p-8 bg-white/5 backdrop-blur-md border border-white/10 rounded-2xl md:rounded-[32px] hover:bg-white/10 transition-colors">
<div className="text-xl md:text-4xl font-extrabold text-accent mb-1 md:mb-2">{t('legacy.network')}</div>
<div className="text-[10px] md:text-sm font-bold uppercase tracking-widest text-white/50">{t('legacy.networkDesc')}</div>
<div className="text-xl md:text-4xl font-extrabold text-accent mb-1 md:mb-2">
{t('legacy.network')}
</div>
<div className="text-[10px] md:text-sm font-bold uppercase tracking-widest text-white/50">
{t('legacy.networkDesc')}
</div>
</div>
</div>
</div>
@@ -216,7 +224,9 @@ export default async function TeamPage({ params: { locale } }: TeamPageProps) {
<Reveal className="w-full lg:w-1/2 p-6 md:p-24 lg:p-32 flex flex-col justify-center bg-neutral-light text-saturated relative order-2">
<div className="absolute top-0 left-0 w-32 h-full bg-saturated/5 skew-x-12 -translate-x-1/2" />
<div className="relative z-10">
<Badge variant="saturated" className="mb-4 md:mb-8">{t('klaus.role')}</Badge>
<Badge variant="saturated" className="mb-4 md:mb-8">
{t('klaus.role')}
</Badge>
<Heading level={2} className="text-saturated mb-6 md:mb-10 text-3xl md:text-6xl">
{t('klaus.name')}
</Heading>
@@ -229,9 +239,9 @@ export default async function TeamPage({ params: { locale } }: TeamPageProps) {
<p className="text-base md:text-xl leading-relaxed text-text-secondary mb-6 md:mb-12 max-w-xl">
{t('klaus.description')}
</p>
<Button
href="https://www.linkedin.com/in/klaus-mintel-b80a8b193/"
variant="saturated"
<Button
href="https://www.linkedin.com/in/klaus-mintel-b80a8b193/"
variant="saturated"
size="lg"
className="group w-full md:w-auto md:h-16 md:px-10 md:text-xl active:scale-95 transition-transform"
>
@@ -255,11 +265,14 @@ export default async function TeamPage({ params: { locale } }: TeamPageProps) {
<p className="text-base md:text-xl text-text-secondary leading-relaxed">
{t('manifesto.tagline')}
</p>
{/* Mobile-only progress indicator */}
<div className="flex lg:hidden mt-8 gap-2">
{[0, 1, 2, 3, 4, 5].map((i) => (
<div key={i} className="h-1.5 flex-1 bg-neutral-medium rounded-full overflow-hidden">
<div
key={i}
className="h-1.5 flex-1 bg-neutral-medium rounded-full overflow-hidden"
>
<div className="h-full bg-accent w-0 group-active:w-full transition-all duration-500" />
</div>
))}
@@ -268,12 +281,21 @@ export default async function TeamPage({ params: { locale } }: TeamPageProps) {
</div>
<div className="sticky-narrative-content grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-10">
{[0, 1, 2, 3, 4, 5].map((idx) => (
<div key={idx} className="p-6 md:p-10 bg-neutral-light rounded-2xl md:rounded-[40px] border border-transparent hover:border-accent hover:bg-white hover:shadow-2xl transition-all duration-500 group active:scale-[0.98] touch-target-none">
<div
key={idx}
className="p-6 md:p-10 bg-neutral-light rounded-2xl md:rounded-[40px] border border-transparent hover:border-accent hover:bg-white hover:shadow-2xl transition-all duration-500 group active:scale-[0.98] touch-target-none"
>
<div className="w-10 h-10 md:w-16 md:h-16 bg-white rounded-xl md:rounded-2xl flex items-center justify-center mb-4 md:mb-8 shadow-sm group-hover:bg-accent transition-colors duration-500">
<span className="text-primary font-extrabold text-lg md:text-2xl group-hover:text-primary-dark">0{idx + 1}</span>
<span className="text-primary font-extrabold text-lg md:text-2xl group-hover:text-primary-dark">
0{idx + 1}
</span>
</div>
<h3 className="text-lg md:text-2xl font-bold mb-2 md:mb-4 text-primary">{t(`manifesto.items.${idx}.title`)}</h3>
<p className="text-sm md:text-lg text-text-secondary leading-relaxed">{t(`manifesto.items.${idx}.description`)}</p>
<h3 className="text-lg md:text-2xl font-bold mb-2 md:mb-4 text-primary">
{t(`manifesto.items.${idx}.title`)}
</h3>
<p className="text-sm md:text-lg text-text-secondary leading-relaxed">
{t(`manifesto.items.${idx}.description`)}
</p>
</div>
))}
</div>

View File

@@ -1,72 +1,131 @@
"use server";
'use server';
import client, { ensureAuthenticated } from "@/lib/directus";
import { createItem } from "@directus/sdk";
import { sendEmail } from "@/lib/mail/mailer";
import ContactEmail from "@/components/emails/ContactEmail";
import React from "react";
import { getServerAppServices } from "@/lib/services/create-services.server";
import client, { ensureAuthenticated } from '@/lib/directus';
import { createItem } from '@directus/sdk';
import { sendEmail } from '@/lib/mail/mailer';
import { render, ContactFormNotification, ConfirmationMessage } from '@mintel/mail';
import React from 'react';
import { getServerAppServices } from '@/lib/services/create-services.server';
export async function sendContactFormAction(formData: FormData) {
const services = getServerAppServices();
const logger = services.logger.child({ action: 'sendContactFormAction' });
const name = formData.get("name") as string;
const email = formData.get("email") as string;
const message = formData.get("message") as string;
const productName = formData.get("productName") as string | null;
const name = formData.get('name') as string;
const email = formData.get('email') as string;
const message = formData.get('message') as string;
const productName = formData.get('productName') as string | null;
if (!name || !email || !message) {
logger.warn('Missing required fields in contact form', { name: !!name, email: !!email, message: !!message });
return { success: false, error: "Missing required fields" };
logger.warn('Missing required fields in contact form', {
name: !!name,
email: !!email,
message: !!message,
});
return { success: false, error: 'Missing required fields' };
}
// 1. Save to Directus
try {
await ensureAuthenticated();
if (productName) {
await client.request(createItem('product_requests', {
product_name: productName,
email,
message
}));
await client.request(
createItem('product_requests', {
product_name: productName,
email,
message,
}),
);
logger.info('Product request stored in Directus');
} else {
await client.request(createItem('contact_submissions', {
name,
email,
message
}));
await client.request(
createItem('contact_submissions', {
name,
email,
message,
}),
);
logger.info('Contact submission stored in Directus');
}
} catch (error) {
logger.error('Failed to store submission in Directus', { error });
// We continue anyway to try sending the email, but maybe we should report this
services.errors.captureException(error, { action: 'directus_store_submission' });
}
// 2. Send Email
logger.info('Sending contact form email', { email, productName });
// 2. Send Emails
logger.info('Sending branded emails', { email, productName });
const subject = productName
const notificationSubject = productName
? `Product Inquiry: ${productName}`
: "New Contact Form Submission";
: 'New Contact Form Submission';
const confirmationSubject = 'Thank you for your inquiry';
const result = await sendEmail({
subject,
template: React.createElement(ContactEmail, {
name,
email,
message,
productName: productName || undefined,
subject,
}),
});
try {
// 2a. Send notification to Mintel/Client
const notificationHtml = await render(
React.createElement(ContactFormNotification, {
name,
email,
message,
productName: productName || undefined,
}),
);
if (result.success) {
logger.info('Contact form email sent successfully', { messageId: result.messageId });
} else {
logger.error('Failed to send contact form email', { error: result.error });
services.errors.captureException(result.error, { action: 'sendContactFormAction', email });
const notificationResult = await sendEmail({
replyTo: email,
subject: notificationSubject,
html: notificationHtml,
});
if (notificationResult.success) {
logger.info('Notification email sent successfully', {
messageId: notificationResult.messageId,
});
}
// 2b. Send confirmation to Customer (branded as KLZ Cables)
const confirmationHtml = await render(
React.createElement(ConfirmationMessage, {
name,
clientName: 'KLZ Cables',
// brandColor: '#82ed20', // Optional: could be KLZ specific
}),
);
const confirmationResult = await sendEmail({
to: email,
subject: confirmationSubject,
html: confirmationHtml,
});
if (confirmationResult.success) {
logger.info('Confirmation email sent successfully', {
messageId: confirmationResult.messageId,
});
}
// Notify via Gotify (Internal)
await services.notifications.notify({
title: `📩 ${notificationSubject}`,
message: `New message from ${name} (${email}):\n\n${message}`,
priority: 5,
});
return { success: true };
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
logger.error('Failed to send branded emails', {
error: errorMsg,
stack: error instanceof Error ? error.stack : undefined,
});
services.errors.captureException(error, { action: 'sendContactFormAction', email });
await services.notifications.notify({
title: '🚨 Contact Form Error',
message: `Failed to send emails for ${name} (${email}). Error: ${errorMsg}`,
priority: 8,
});
return { success: false, error: errorMsg };
}
return result;
}

View File

@@ -0,0 +1,9 @@
import { checkHealth } from '@/lib/directus';
import { NextResponse } from 'next/server';
export const dynamic = 'force-dynamic';
export async function GET() {
const health = await checkHealth();
return NextResponse.json(health, { status: health.status === 'ok' ? 200 : 503 });
}

View File

@@ -15,6 +15,11 @@ export default function manifest(): MetadataRoute.Manifest {
sizes: 'any',
type: 'image/x-icon',
},
{
src: '/logo.png',
sizes: '512x512',
type: 'image/png',
},
{
src: '/apple-touch-icon.png',
sizes: '180x180',

View File

@@ -1,6 +1,8 @@
import { config } from '@/lib/config';
import { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
const baseUrl = config.baseUrl || 'https://klz-cables.com';
return {
rules: [
{
@@ -11,8 +13,8 @@ export default function robots(): MetadataRoute.Robots {
{
userAgent: ['GPTBot', 'ClaudeBot', 'PerplexityBot', 'Google-Extended', 'OAI-SearchBot'],
allow: '/',
}
},
],
sitemap: 'https://klz-cables.com/sitemap.xml',
sitemap: `${baseUrl}/sitemap.xml`,
};
}

View File

@@ -1,10 +1,13 @@
import { config } from '@/lib/config';
import { MetadataRoute } from 'next';
import { getAllProducts } from '@/lib/mdx';
import { getAllPosts } from '@/lib/blog';
import { getAllPages } from '@/lib/pages';
import { getAllProductsMetadata } from '@/lib/mdx';
import { getAllPostsMetadata } from '@/lib/blog';
import { getAllPagesMetadata } from '@/lib/pages';
export const revalidate = 3600; // Revalidate every hour
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://klz-cables.com';
const baseUrl = config.baseUrl || 'https://klz-cables.com';
const locales = ['de', 'en'];
const routes = [
@@ -33,12 +36,12 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
}
// Products
const products = await getAllProducts(locale);
for (const product of products) {
// We need to find the category for the product to build the URL
// In this project, products are under /products/[category]/[slug]
// The category is in product.frontmatter.categories
const category = product.frontmatter.categories[0]?.toLowerCase().replace(/\s+/g, '-') || 'other';
const productsMetadata = await getAllProductsMetadata(locale);
for (const product of productsMetadata) {
if (!product.frontmatter || !product.slug) continue;
const category =
product.frontmatter.categories[0]?.toLowerCase().replace(/\s+/g, '-') || 'other';
sitemapEntries.push({
url: `${baseUrl}/${locale}/products/${category}/${product.slug}`,
lastModified: new Date(),
@@ -48,8 +51,10 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
}
// Blog posts
const posts = await getAllPosts(locale);
for (const post of posts) {
const postsMetadata = await getAllPostsMetadata(locale);
for (const post of postsMetadata) {
if (!post.frontmatter || !post.slug) continue;
sitemapEntries.push({
url: `${baseUrl}/${locale}/blog/${post.slug}`,
lastModified: new Date(post.frontmatter.date),
@@ -59,8 +64,10 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
}
// Static pages
const pages = await getAllPages(locale);
for (const page of pages) {
const pagesMetadata = await getAllPagesMetadata(locale);
for (const page of pagesMetadata) {
if (!page.slug) continue;
sitemapEntries.push({
url: `${baseUrl}/${locale}/${page.slug}`,
lastModified: new Date(),

8
commitlint.config.js Normal file
View File

@@ -0,0 +1,8 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'header-max-length': [2, 'always', 500],
'subject-case': [0],
'subject-full-stop': [0],
},
};

View File

@@ -0,0 +1,84 @@
'use client';
import React, { useEffect, useState } from 'react';
import { AlertCircle, RefreshCw, Database } from 'lucide-react';
import { config } from '../lib/config';
export default function CMSConnectivityNotice() {
const [status, setStatus] = useState<'checking' | 'ok' | 'error'>('checking');
const [errorMsg, setErrorMsg] = useState('');
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
// Only show if we've detected an issue AND we are in a context where we want to see it
const checkCMS = async () => {
const isDebug = new URLSearchParams(window.location.search).has('cms_debug');
const isLocal = config.isDevelopment;
const isTesting = config.isTesting;
// Only proceed with check if it's developer context (Local or Testing)
// Staging and Production should NEVER see this unless forced with ?cms_debug
if (!isLocal && !isTesting && !isDebug) return;
try {
const response = await fetch('/api/health/cms');
const data = await response.json();
if (data.status !== 'ok') {
setStatus('error');
setErrorMsg(data.message);
setIsVisible(true);
} else {
setStatus('ok');
setIsVisible(false);
}
} catch (err) {
// If it's a connection error, only show if we are really debugging
if (isDebug || isLocal) {
setStatus('error');
setErrorMsg('Could not connect to CMS health endpoint');
setIsVisible(true);
}
}
};
checkCMS();
}, []);
if (!isVisible) return null;
return (
<div className="fixed bottom-4 right-4 z-[9999] animate-slide-up">
<div className="bg-red-500/90 backdrop-blur-md border border-red-400 text-white p-4 rounded-2xl shadow-2xl max-w-sm">
<div className="flex items-start gap-3">
<div className="bg-white/20 p-2 rounded-lg">
<AlertCircle className="w-5 h-5" />
</div>
<div className="flex-1">
<h4 className="font-bold text-sm mb-1">CMS Issue Detected</h4>
<p className="text-xs opacity-90 leading-relaxed mb-3">
{errorMsg === 'relation "products" does not exist'
? 'The database schema is missing. Please sync your local data to this environment.'
: errorMsg || 'The application cannot connect to the Directus CMS.'}
</p>
<div className="flex gap-2">
<button
onClick={() => window.location.reload()}
className="bg-white text-red-600 text-[10px] font-bold uppercase tracking-wider px-3 py-1.5 rounded-lg flex items-center gap-2 hover:bg-neutral-100 transition-colors"
>
<RefreshCw className="w-3 h-3" />
Retry
</button>
<button
onClick={() => setIsVisible(false)}
className="bg-black/20 text-white text-[10px] font-bold uppercase tracking-wider px-3 py-1.5 rounded-lg hover:bg-black/30 transition-colors"
>
Dismiss
</button>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -17,10 +17,10 @@ export default function ContactForm() {
const formData = new FormData(e.currentTarget);
const email = formData.get('email') as string;
try {
const result = await sendContactFormAction(formData);
if (result.success) {
if (result?.success) {
trackEvent('contact_form_submission', {
form_type: 'general',
email,
@@ -41,7 +41,12 @@ export default function ContactForm() {
return (
<Card className="p-6 md:p-12 rounded-2xl md:rounded-[40px] border-none shadow-2xl text-center">
<div className="w-20 h-20 bg-accent rounded-full flex items-center justify-center mx-auto mb-6 shadow-lg shadow-accent/20">
<svg className="w-10 h-10 text-primary-dark" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg
className="w-10 h-10 text-primary-dark"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
</div>
@@ -49,7 +54,8 @@ export default function ContactForm() {
{t('form.successTitle') || 'Message Sent!'}
</Heading>
<p className="text-text-secondary text-lg mb-8">
{t('form.successDesc') || 'Thank you for your message. We will get back to you as soon as possible.'}
{t('form.successDesc') ||
'Thank you for your message. We will get back to you as soon as possible.'}
</p>
<Button onClick={() => setStatus('idle')} variant="saturated">
{t('form.sendAnother') || 'Send another message'}
@@ -62,7 +68,13 @@ export default function ContactForm() {
return (
<Card className="p-6 md:p-12 rounded-2xl md:rounded-[40px] border-destructive/20 shadow-2xl text-center bg-destructive/5 animate-slide-up">
<div className="w-20 h-20 bg-destructive rounded-full flex items-center justify-center mx-auto mb-6 shadow-lg shadow-destructive/20">
<svg className="w-10 h-10 text-destructive-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<svg
className="w-10 h-10 text-destructive-foreground"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth="2"
>
<circle cx="12" cy="12" r="10" />
<line x1="15" y1="9" x2="9" y2="15" strokeLinecap="round" strokeLinejoin="round" />
<line x1="9" y1="9" x2="15" y2="15" strokeLinecap="round" strokeLinejoin="round" />
@@ -74,7 +86,12 @@ export default function ContactForm() {
<p className="text-destructive/80 text-lg mb-8 leading-relaxed font-medium">
{t('form.error') || 'Something went wrong. Please check your input and try again.'}
</p>
<Button onClick={() => setStatus('idle')} variant="saturated" size="lg" className="w-full bg-destructive hover:bg-destructive/90 text-destructive-foreground border-destructive shadow-lg shadow-destructive/20">
<Button
onClick={() => setStatus('idle')}
variant="destructive"
size="lg"
className="w-full"
>
{t('form.tryAgain') || 'Try Again'}
</Button>
</Card>
@@ -89,9 +106,9 @@ export default function ContactForm() {
<form onSubmit={handleSubmit} className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-8">
<div className="space-y-1 md:space-y-2">
<Label htmlFor="name">{t('form.name')}</Label>
<Input
type="text"
id="name"
<Input
type="text"
id="name"
name="name"
autoComplete="name"
enterKeyHint="next"
@@ -101,9 +118,9 @@ export default function ContactForm() {
</div>
<div className="space-y-1 md:space-y-2">
<Label htmlFor="email">{t('form.email')}</Label>
<Input
type="email"
id="email"
<Input
type="email"
id="email"
name="email"
autoComplete="email"
inputMode="email"
@@ -114,32 +131,50 @@ export default function ContactForm() {
</div>
<div className="md:col-span-2 space-y-1 md:space-y-2">
<Label htmlFor="message">{t('form.message')}</Label>
<Textarea
id="message"
<Textarea
id="message"
name="message"
rows={4}
rows={4}
enterKeyHint="send"
placeholder={t('form.messagePlaceholder')}
required
/>
</div>
<div className="md:col-span-2 pt-2 md:pt-4">
<Button
type="submit"
variant="saturated"
size="lg"
<Button
type="submit"
variant="saturated"
size="lg"
disabled={status === 'submitting'}
className="w-full shadow-xl shadow-saturated/20 md:h-16 md:px-10 md:text-xl active:scale-[0.98] transition-transform"
>
{status === 'submitting' ? (
<span className="flex items-center gap-2">
<svg className="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
<svg
className="animate-spin h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
{t('form.submitting') || 'Sending...'}
</span>
) : t('form.submit')}
) : (
t('form.submit')
)}
</Button>
</div>
</form>

View File

@@ -0,0 +1,68 @@
'use client';
import { cn } from '@/components/ui/utils';
import { useTranslations } from 'next-intl';
interface DatasheetDownloadProps {
datasheetPath: string;
className?: string;
}
export default function DatasheetDownload({ datasheetPath, className }: DatasheetDownloadProps) {
const t = useTranslations('Products');
return (
<div className={cn("mt-8 animate-slight-fade-in-from-bottom", className)}>
<a
href={datasheetPath}
target="_blank"
rel="noopener noreferrer"
className="group relative block w-full overflow-hidden rounded-[32px] bg-primary-dark p-1 transition-all duration-500 hover:shadow-[0_20px_50px_rgba(0,0,0,0.2)] hover:-translate-y-1"
>
{/* Animated Background Gradient */}
<div className="absolute inset-0 bg-gradient-to-r from-accent via-saturated to-accent opacity-20 group-hover:opacity-40 transition-opacity duration-500 animate-gradient-x" />
{/* Inner Content */}
<div className="relative flex items-center gap-6 rounded-[31px] bg-primary-dark/90 backdrop-blur-xl p-6 md:p-8 border border-white/10">
{/* Icon Container */}
<div className="relative flex h-16 w-16 flex-shrink-0 items-center justify-center rounded-2xl bg-white/5 border border-white/10 group-hover:bg-accent group-hover:border-white/20 transition-all duration-500">
<div className="absolute inset-0 rounded-2xl bg-accent/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
<svg
className="relative h-8 w-8 text-white transition-transform duration-500 group-hover:scale-110 group-hover:rotate-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
</div>
{/* Text Content */}
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-accent">PDF Datasheet</span>
</div>
<h3 className="text-xl md:text-2xl font-black text-white uppercase tracking-tighter leading-none group-hover:text-accent transition-colors duration-300">
{t('downloadDatasheet')}
</h3>
<p className="mt-2 text-sm font-medium text-white/60 leading-relaxed">
{t('downloadDatasheetDesc')}
</p>
</div>
{/* Arrow Icon */}
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-white/5 text-white/20 group-hover:bg-accent group-hover:text-white group-hover:translate-x-1 transition-all duration-500">
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</a>
</div>
);
}

View File

@@ -29,7 +29,7 @@ export function OGImageTemplate({
backgroundColor: mode === 'light' ? '#ffffff' : primaryBlue,
padding: '80px',
position: 'relative',
fontFamily: 'Inter, sans-serif',
fontFamily: 'Inter',
};
return (
@@ -39,7 +39,10 @@ export function OGImageTemplate({
<div
style={{
position: 'absolute',
inset: 0,
top: 0,
left: 0,
right: 0,
bottom: 0,
display: 'flex',
}}
>
@@ -57,23 +60,26 @@ export function OGImageTemplate({
<div
style={{
position: 'absolute',
inset: 0,
background: 'linear-gradient(to right, rgba(0,26,77,0.9) 0%, rgba(0,26,77,0.4) 100%)',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'linear-gradient(to right, rgba(0,26,77,0.95), rgba(0,26,77,0.6))',
}}
/>
</div>
)}
{/* Decorative Scribble Circle (Simplified for Satori) */}
{/* Decorative Brand Accent (Top Right) */}
<div
style={{
position: 'absolute',
top: '-100px',
right: '-100px',
top: '-150px',
right: '-150px',
width: '600px',
height: '600px',
borderRadius: '50%',
background: `radial-gradient(circle, ${accentGreen}1a 0%, transparent 70%)`,
borderRadius: '300px',
backgroundColor: `${accentGreen}15`,
display: 'flex',
}}
/>
@@ -84,11 +90,11 @@ export function OGImageTemplate({
<div
style={{
fontSize: '24px',
fontWeight: 'bold',
fontWeight: 700,
color: accentGreen,
textTransform: 'uppercase',
letterSpacing: '0.2em',
marginBottom: '24px',
letterSpacing: '0.3em',
marginBottom: '32px',
display: 'flex',
}}
>
@@ -99,13 +105,14 @@ export function OGImageTemplate({
{/* Title */}
<div
style={{
fontSize: '72px',
fontWeight: '900',
fontSize: title.length > 40 ? '64px' : '82px',
fontWeight: 700,
color: 'white',
lineHeight: '1.1',
maxWidth: '900px',
marginBottom: '32px',
lineHeight: '1.05',
maxWidth: '950px',
marginBottom: '40px',
display: 'flex',
letterSpacing: '-0.02em',
}}
>
{title}
@@ -116,13 +123,14 @@ export function OGImageTemplate({
<div
style={{
fontSize: '32px',
color: 'rgba(255,255,255,0.8)',
maxWidth: '800px',
color: 'rgba(255,255,255,0.7)',
maxWidth: '850px',
lineHeight: '1.4',
display: 'flex',
fontWeight: 400,
}}
>
{description}
{description.length > 160 ? description.substring(0, 157) + '...' : description}
</div>
)}
</div>
@@ -139,33 +147,34 @@ export function OGImageTemplate({
>
<div
style={{
width: '120px',
height: '8px',
width: '80px',
height: '6px',
backgroundColor: accentGreen,
borderRadius: '4px',
borderRadius: '3px',
marginRight: '24px',
}}
/>
<div
style={{
fontSize: '24px',
fontWeight: 'bold',
fontWeight: 700,
color: 'white',
textTransform: 'uppercase',
letterSpacing: '0.1em',
letterSpacing: '0.15em',
display: 'flex',
}}
>
KLZ Cables
</div>
</div>
{/* Saturated Blue Accent */}
{/* Saturated Blue Brand Strip */}
<div
style={{
position: 'absolute',
top: 0,
right: 0,
width: '10px',
width: '12px',
height: '100%',
backgroundColor: saturatedBlue,
}}
@@ -173,3 +182,4 @@ export function OGImageTemplate({
</div>
);
}

View File

@@ -3,6 +3,7 @@
import Image from 'next/image';
import { useTranslations } from 'next-intl';
import RequestQuoteForm from '@/components/RequestQuoteForm';
import DatasheetDownload from '@/components/DatasheetDownload';
import Scribble from '@/components/Scribble';
import { cn } from '@/components/ui/utils';
@@ -64,33 +65,7 @@ export default function ProductSidebar({ productName, productImage, datasheetPat
{/* Datasheet Download */}
{datasheetPath && (
<a
href={datasheetPath}
target="_blank"
rel="noopener noreferrer"
className="block bg-white rounded-2xl border border-neutral-medium overflow-hidden group transition-all duration-500 hover:shadow-xl hover:border-saturated/30 hover:-translate-y-0.5"
>
<div className="p-4 flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-neutral-medium/20 flex items-center justify-center flex-shrink-0 group-hover:bg-saturated group-hover:text-white transition-all duration-500 text-saturated border border-transparent group-hover:border-white/20">
<svg className="w-6 h-6 transition-transform duration-500 group-hover:scale-110" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<div className="flex-1 min-w-0">
<h3 className="text-sm md:text-base font-heading font-black text-neutral-dark m-0 uppercase tracking-tighter leading-tight group-hover:text-saturated transition-colors duration-300">
{t('downloadDatasheet')}
</h3>
<p className="text-text-secondary text-[10px] md:text-xs m-0 mt-0.5 font-semibold leading-tight truncate uppercase tracking-widest opacity-60">
{t('downloadDatasheetDesc')}
</p>
</div>
<div className="text-neutral-dark/20 group-hover:text-saturated transition-all duration-500 transform group-hover:translate-x-1">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</a>
<DatasheetDownload datasheetPath={datasheetPath} className="mt-0" />
)}
</div>
);

View File

@@ -49,14 +49,28 @@ export default function RequestQuoteForm({ productName }: RequestQuoteFormProps)
if (status === 'success') {
return (
<div className="bg-accent/5 border border-accent/20 text-primary-dark p-4 rounded-xl text-center animate-fade-in !mt-0">
<div className="w-10 h-10 bg-accent rounded-full flex items-center justify-center mx-auto mb-3 shadow-lg shadow-accent/20">
<svg className="w-5 h-5 text-primary-dark" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
<div className="bg-accent/5 border border-accent/20 text-primary-dark p-4 rounded-xl text-center animate-fade-in !mt-0 w-full">
<div className="flex justify-center mb-3">
<div className="w-10 h-10 bg-accent rounded-full flex items-center justify-center shadow-lg shadow-accent/20">
<svg
className="w-5 h-5 text-primary-dark"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
</div>
<h3 className="text-base font-extrabold mb-1 tracking-tight !mt-0">{t('successTitle')}</h3>
<p className="text-text-secondary text-xs leading-tight mb-4 !mt-0">
<h3 className="text-base font-extrabold mb-1 tracking-tight !mt-0 text-center w-full">
{t('successTitle')}
</h3>
<p className="text-text-secondary text-xs leading-tight mb-4 !mt-0 text-center w-full">
{t('successDesc', { productName })}
</p>
<button
@@ -73,26 +87,36 @@ export default function RequestQuoteForm({ productName }: RequestQuoteFormProps)
if (status === 'error') {
return (
<div className="bg-destructive/5 border border-destructive/20 text-destructive p-4 rounded-xl text-center animate-fade-in !mt-0">
<div className="w-10 h-10 bg-destructive rounded-full flex items-center justify-center mx-auto mb-3 shadow-lg shadow-destructive/20">
<svg className="w-5 h-5 text-destructive-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="3">
<circle cx="12" cy="12" r="10" />
<line x1="15" y1="9" x2="9" y2="15" />
<line x1="9" y1="9" x2="15" y2="15" />
</svg>
<div className="bg-destructive/5 border border-destructive/20 text-destructive p-4 rounded-xl text-center animate-fade-in !mt-0 w-full">
<div className="flex justify-center mb-3">
<div className="w-10 h-10 bg-destructive rounded-full flex items-center justify-center shadow-lg shadow-destructive/20">
<svg
className="w-5 h-5 text-destructive-foreground"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth="3"
>
<circle cx="12" cy="12" r="10" />
<line x1="15" y1="9" x2="9" y2="15" />
<line x1="9" y1="9" x2="15" y2="15" />
</svg>
</div>
</div>
<h3 className="text-base font-extrabold mb-1 tracking-tight !mt-0 text-destructive">{t('errorTitle') || 'Submission Failed'}</h3>
<p className="text-destructive/80 text-xs leading-tight mb-4 !mt-0">
<h3 className="text-base font-extrabold mb-1 tracking-tight !mt-0 text-destructive text-center w-full">
{t('errorTitle') || 'Submission Failed'}
</h3>
<p className="text-destructive/80 text-xs leading-tight mb-4 !mt-0 text-center w-full">
{t('errorDesc') || 'Something went wrong. Please try again.'}
</p>
<button
<Button
onClick={() => setStatus('idle')}
className="inline-flex items-center text-[9px] font-bold uppercase tracking-[0.2em] text-destructive hover:text-destructive/80 transition-colors group"
variant="destructive"
size="sm"
className="w-full"
>
<span className="border-b-2 border-destructive/10 group-hover:border-destructive/30 transition-colors pb-1">
{t('tryAgain') || 'Try Again'}
</span>
</button>
{t('tryAgain') || 'Try Again'}
</Button>
</div>
);
}
@@ -133,22 +157,48 @@ export default function RequestQuoteForm({ productName }: RequestQuoteFormProps)
>
{status === 'submitting' ? (
<>
<svg className="animate-spin h-3 w-3 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
<svg
className="animate-spin h-3 w-3 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<span className="text-xs">{t('submitting')}</span>
</>
) : (
<>
<span className="text-xs">{t('submit')}</span>
<svg className="w-3 h-3 transition-transform group-hover:translate-x-1" 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="w-3 h-3 transition-transform group-hover:translate-x-1"
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>
</>
)}
</Button>
<p className="text-[7px] text-center text-text-secondary/40 uppercase tracking-[0.15em] font-medium px-2 !mt-1 !mb-0">
{t('privacyNote')}
</p>

View File

@@ -3,7 +3,6 @@
import { useEffect } from 'react';
import { usePathname, useSearchParams } from 'next/navigation';
import { getAppServices } from '@/lib/services/create-services';
import Script from 'next/script';
/**
* AnalyticsProvider Component
@@ -11,49 +10,35 @@ import Script from 'next/script';
* Automatically tracks pageviews on client-side route changes.
* This component should be placed inside your layout to handle navigation events.
*
* @param {Object} props - Component props
* @param {string} [props.websiteId] - The Umami website ID (passed from server config)
*
* @example
* ```tsx
* // In your layout.tsx
* <NextIntlClientProvider messages={messages} locale={locale}>
* <UmamiScript />
* <Header />
* <main>{children}</main>
* <Footer />
* <AnalyticsProvider />
* </NextIntlClientProvider>
* const { websiteId } = config.analytics.umami;
* <AnalyticsProvider websiteId={websiteId} />
* ```
*/
export default function AnalyticsProvider() {
export default function AnalyticsProvider({ websiteId }: { websiteId?: string }) {
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
if (!pathname) return;
const services = getAppServices();
const url = `${pathname}${searchParams?.size ? `?${searchParams.toString()}` : ''}`;
// Track pageview with the full URL
services.analytics.trackPageview(url);
if (process.env.NODE_ENV === 'development') {
console.log('[Umami] Tracked pageview:', url);
}
}, [pathname, searchParams]);
const websiteId = process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID;
if (!websiteId) return null;
return (
<Script
id="umami-analytics"
src="/stats/script.js"
data-website-id={websiteId}
data-host-url="/stats"
strategy="afterInteractive"
data-domains="klz-cables.com"
defer
/>
);
return null;
}

View File

@@ -3,24 +3,43 @@ import Link from 'next/link';
import { cn } from './utils';
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'accent' | 'saturated' | 'outline' | 'ghost' | 'white';
variant?:
| 'primary'
| 'secondary'
| 'accent'
| 'saturated'
| 'outline'
| 'ghost'
| 'white'
| 'destructive';
size?: 'sm' | 'md' | 'lg' | 'xl';
href?: string;
className?: string;
children?: React.ReactNode;
}
export function Button({ className, variant = 'primary', size = 'md', href, ...props }: ButtonProps) {
const baseStyles = 'inline-flex items-center justify-center rounded-full font-semibold transition-all duration-500 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none active:scale-95 hover:-translate-y-1 hover:scale-[1.02] relative overflow-hidden group/btn isolate';
export function Button({
className,
variant = 'primary',
size = 'md',
href,
...props
}: ButtonProps) {
const baseStyles =
'inline-flex items-center justify-center rounded-full font-semibold transition-all duration-500 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none active:scale-95 hover:-translate-y-1 hover:scale-[1.02] relative overflow-hidden group/btn isolate';
const variants = {
primary: 'bg-primary text-white shadow-md hover:shadow-primary/30 hover:shadow-2xl',
secondary: 'bg-secondary text-white shadow-md hover:shadow-secondary/30 hover:shadow-2xl',
accent: 'bg-accent text-primary-dark shadow-md hover:shadow-accent/30 hover:shadow-2xl',
saturated: 'bg-saturated text-white shadow-md hover:shadow-primary/30 hover:shadow-2xl',
outline: 'border-2 border-primary bg-transparent text-primary hover:text-white hover:shadow-primary/20 hover:shadow-xl',
outline:
'border-2 border-primary bg-transparent text-primary hover:text-white hover:shadow-primary/20 hover:shadow-xl',
ghost: 'text-primary hover:shadow-lg',
white: 'bg-white text-primary shadow-md hover:shadow-primary/30 hover:shadow-2xl hover:text-white',
white:
'bg-white text-primary shadow-md hover:shadow-primary/30 hover:shadow-2xl hover:text-white',
destructive:
'bg-destructive text-destructive-foreground shadow-md hover:shadow-destructive/30 hover:shadow-2xl',
};
const sizes = {
@@ -40,20 +59,25 @@ export function Button({ className, variant = 'primary', size = 'md', href, ...p
outline: 'bg-primary',
ghost: 'bg-primary-light/10',
white: 'bg-primary-light',
destructive: 'bg-destructive/90',
};
const content = (
<>
<span className={cn(
"relative z-10 flex items-center justify-center gap-2 transition-colors duration-500",
variant === 'white' ? "group-hover/btn:text-primary-dark" : ""
)}>
<span
className={cn(
'relative z-10 flex items-center justify-center gap-2 transition-colors duration-500',
variant === 'white' ? 'group-hover/btn:text-primary-dark' : '',
)}
>
{props.children}
</span>
<span className={cn(
"absolute inset-0 translate-y-[101%] group-hover/btn:translate-y-0 transition-transform duration-500 ease-out",
overlayColors[variant]
)} />
<span
className={cn(
'absolute inset-0 translate-y-[101%] group-hover/btn:translate-y-0 transition-transform duration-500 ease-out',
overlayColors[variant],
)}
/>
</>
);

4
cookies.txt Normal file
View File

@@ -0,0 +1,4 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

Binary file not shown.

View File

@@ -398,6 +398,24 @@ locale: de
"55",
"4195"
]
},
{
"configuration": "1x1200/35",
"cells": [
"Al",
"RM",
"0,95",
"48,5",
"0,0247",
"3,4",
"Auf Anfrage",
"Auf Anfrage",
"113",
"2,4",
"885",
"59",
"4800"
]
}
]
},
@@ -737,6 +755,24 @@ locale: de
"60",
"4634"
]
},
{
"configuration": "1x1200/35",
"cells": [
"Al",
"RM",
"1,05",
"52,3",
"0,0247",
"5,5",
"Auf Anfrage",
"Auf Anfrage",
"113",
"2,4",
"990",
"66",
"5200"
]
}
]
},
@@ -1076,6 +1112,24 @@ locale: de
"65",
"5093"
]
},
{
"configuration": "1x1200/35",
"cells": [
"Al",
"RM",
"1,15",
"57,5",
"0,0247",
"8,0",
"Auf Anfrage",
"Auf Anfrage",
"113",
"2,4",
"1065",
"71",
"5900"
]
}
]
}

View File

@@ -398,6 +398,24 @@ locale: en
"55",
"4195"
]
},
{
"configuration": "1x1200/35",
"cells": [
"Al",
"RM",
"0.95",
"48.5",
"0.0247",
"3.4",
"On Request",
"On Request",
"113",
"2.4",
"885",
"59",
"4800"
]
}
]
},
@@ -737,6 +755,24 @@ locale: en
"60",
"4634"
]
},
{
"configuration": "1x1200/35",
"cells": [
"Al",
"RM",
"1.05",
"52.3",
"0.0247",
"5.5",
"On Request",
"On Request",
"113",
"2.4",
"990",
"66",
"5200"
]
}
]
},
@@ -1076,6 +1112,24 @@ locale: en
"65",
"5093"
]
},
{
"configuration": "1x1200/35",
"cells": [
"Al",
"RM",
"1.15",
"57.5",
"0.0247",
"8",
"On Request",
"On Request",
"113",
"2.4",
"1065",
"71",
"5900"
]
}
]
}

View File

@@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:serif="http://www.serif.com/" width="100%" height="100%" viewBox="0 0 295 99" version="1.1" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,0.000798697,0)">
<path d="M83.219,92.879C83.219,93.629 82.973,94.043 81.992,94.043C81.008,94.043 80.82,93.629 80.82,92.91L80.82,89.969C80.82,89.25 81.008,88.836 81.992,88.836C83.043,88.836 83.219,89.25 83.219,89.988L84.578,89.988C84.578,88.305 83.82,87.637 81.992,87.637C80.16,87.637 79.461,88.297 79.461,89.898L79.461,92.98C79.461,94.543 80.191,95.242 81.992,95.242C83.793,95.242 84.578,94.543 84.578,92.879L83.219,92.879Z" style="fill:#0117bf;fill-rule:nonzero;"></path>
<path d="M90.543,87.656L89.195,87.656L87.102,95.223L88.496,95.223L88.891,93.883L90.828,93.883L91.211,95.223L92.609,95.223L90.543,87.656ZM89.227,92.555L89.855,89.754L90.484,92.555L89.227,92.555Z" style="fill:#0117bf;fill-rule:nonzero;"></path>
<path d="M95.336,95.223L97.836,95.223C99.668,95.223 100.523,94.574 100.523,92.871C100.523,91.828 99.922,91.148 99.137,90.98C99.734,90.578 99.824,90.117 99.824,89.652C99.824,88.473 98.957,87.648 97.59,87.648L95.336,87.648L95.336,95.223ZM96.688,91.809L97.836,91.809C98.82,91.809 99.066,92.152 99.066,92.898C99.066,93.617 98.91,93.992 97.855,93.992L96.688,93.992L96.688,91.809ZM97.59,88.809C98.258,88.809 98.426,89.289 98.426,89.672C98.426,90.156 98.16,90.559 97.602,90.559L96.695,90.559L96.695,88.809L97.59,88.809Z" style="fill:#0117bf;fill-rule:nonzero;"></path>
<path d="M107.906,93.98L104.98,93.98L104.98,87.648L103.613,87.648L103.613,95.223L107.906,95.223L107.906,93.98Z" style="fill:#0117bf;fill-rule:nonzero;"></path>
<path d="M110.879,87.648L110.879,95.23L115.375,95.23L115.375,93.992L112.238,93.992L112.238,91.996L114.793,91.996L114.793,90.773L112.238,90.773L112.238,88.828L115.238,88.828L115.238,87.648L110.879,87.648Z" style="fill:#0117bf;fill-rule:nonzero;"></path>
<path d="M121.684,89.625L123.051,89.625C122.926,88.109 122.02,87.605 120.652,87.605C119.098,87.605 118.23,88.344 118.23,89.762C118.23,91.324 119.137,91.75 119.992,91.855C120.797,91.965 121.863,91.965 121.863,92.859C121.863,93.715 121.488,94.062 120.672,94.062C119.805,94.062 119.551,93.746 119.52,93.164L118.152,93.164C118.152,94.387 118.754,95.301 120.641,95.301C122.461,95.301 123.219,94.562 123.219,92.812C123.219,91.297 122.383,90.941 121.508,90.805C120.355,90.629 119.598,90.707 119.598,89.754C119.598,89.035 119.902,88.797 120.652,88.797C121.309,88.797 121.645,88.984 121.684,89.625Z" style="fill:#0117bf;fill-rule:nonzero;"></path>
<path d="M135.348,87.648L130.91,87.648L130.91,95.23L132.258,95.23L132.258,92.004L134.875,92.004L134.875,90.773L132.258,90.773L132.258,88.887L135.348,88.887L135.348,87.648Z" style="fill:#0117bf;fill-rule:nonzero;"></path>
<path d="M140.82,95.289C142.621,95.289 143.406,94.594 143.406,93.027L143.406,89.82C143.406,88.219 142.648,87.559 140.82,87.559C138.988,87.559 138.289,88.219 138.289,89.82L138.289,93.027C138.289,94.594 139.02,95.289 140.82,95.289ZM140.82,94.09C139.836,94.09 139.648,93.676 139.648,92.961L139.648,89.891C139.648,89.199 139.836,88.758 140.82,88.758C141.871,88.758 142.051,89.199 142.051,89.891L142.051,92.961C142.051,93.676 141.805,94.09 140.82,94.09Z" style="fill:#0117bf;fill-rule:nonzero;"></path>
<path d="M151.703,95.223L150.039,92.34C150.957,92.043 151.348,91.434 151.348,90.312L151.348,89.918C151.348,88.316 150.492,87.648 148.664,87.648L146.754,87.648L146.754,95.223L148.113,95.223L148.113,92.555L148.613,92.555L150.121,95.223L151.703,95.223ZM148.102,91.305L148.102,88.895L148.684,88.895C149.734,88.895 149.922,89.27 149.922,89.988L149.922,90.242C149.922,90.961 149.648,91.305 148.664,91.305L148.102,91.305Z" style="fill:#0117bf;fill-rule:nonzero;"></path>
<path d="M161.707,87.656L160.359,87.656L158.262,95.223L159.66,95.223L160.055,93.883L161.992,93.883L162.375,95.223L163.773,95.223L161.707,87.656ZM160.387,92.555L161.016,89.754L161.648,92.555L160.387,92.555Z" style="fill:#0117bf;fill-rule:nonzero;"></path>
<path d="M173.254,92.145L174.543,92.145L174.543,92.879C174.543,93.629 174.195,94.043 173.215,94.043C172.23,94.043 172.043,93.629 172.043,92.91L172.043,89.938C172.043,89.25 172.23,88.809 173.215,88.809C174.266,88.809 174.441,89.16 174.441,89.871L175.801,89.871C175.801,88.246 175.043,87.605 173.215,87.605C171.383,87.605 170.684,88.324 170.684,89.871L170.684,92.91C170.684,94.543 171.414,95.262 173.215,95.262C175.012,95.262 175.801,94.543 175.801,92.879L175.801,90.914L173.254,90.914L173.254,92.145Z" style="fill:#0117bf;fill-rule:nonzero;"></path>
<path d="M184.02,95.223L182.355,92.34C183.27,92.043 183.664,91.434 183.664,90.312L183.664,89.918C183.664,88.316 182.809,87.648 180.98,87.648L179.07,87.648L179.07,95.223L180.426,95.223L180.426,92.555L180.93,92.555L182.434,95.223L184.02,95.223ZM180.418,91.305L180.418,88.895L181,88.895C182.051,88.895 182.238,89.27 182.238,89.988L182.238,90.242C182.238,90.961 181.961,91.305 180.98,91.305L180.418,91.305Z" style="fill:#0117bf;fill-rule:nonzero;"></path>
<path d="M186.965,87.648L186.965,95.23L191.461,95.23L191.461,93.992L188.32,93.992L188.32,91.996L190.879,91.996L190.879,90.773L188.32,90.773L188.32,88.828L191.32,88.828L191.32,87.648L186.965,87.648Z" style="fill:#0117bf;fill-rule:nonzero;"></path>
<path d="M194.562,87.648L194.562,95.23L199.059,95.23L199.059,93.992L195.918,93.992L195.918,91.996L198.477,91.996L198.477,90.773L195.918,90.773L195.918,88.828L198.922,88.828L198.922,87.648L194.562,87.648Z" style="fill:#0117bf;fill-rule:nonzero;"></path>
<path d="M206.922,87.656L205.574,87.656L205.574,89.445L205.723,92.645L203.496,87.656L202.148,87.656L202.148,95.223L203.496,95.223L203.496,93.293L203.379,90.422L205.602,95.223L206.922,95.223L206.922,87.656Z" style="fill:#0117bf;fill-rule:nonzero;"></path>
<path d="M210.34,87.648L210.34,95.23L214.836,95.23L214.836,93.992L211.695,93.992L211.695,91.996L214.254,91.996L214.254,90.773L211.695,90.773L211.695,88.828L214.695,88.828L214.695,87.648L210.34,87.648Z" style="fill:#0117bf;fill-rule:nonzero;"></path>
<path d="M222.887,95.223L221.223,92.34C222.137,92.043 222.531,91.434 222.531,90.312L222.531,89.918C222.531,88.316 221.676,87.648 219.848,87.648L217.938,87.648L217.938,95.223L219.293,95.223L219.293,92.555L219.797,92.555L221.301,95.223L222.887,95.223ZM219.285,91.305L219.285,88.895L219.867,88.895C220.918,88.895 221.105,89.27 221.105,89.988L221.105,90.242C221.105,90.961 220.828,91.305 219.844,91.305L219.285,91.305Z" style="fill:#0117bf;fill-rule:nonzero;"></path>
<path d="M233.93,87.676L229.445,87.676L229.445,88.887L231.02,88.887L231.02,95.223L232.367,95.223L232.367,88.887L233.93,88.887L233.93,87.676Z" style="fill:#0117bf;fill-rule:nonzero;"></path>
<path d="M238.922,95.289C240.723,95.289 241.508,94.594 241.508,93.027L241.508,89.82C241.508,88.219 240.75,87.559 238.922,87.559C237.094,87.559 236.395,88.219 236.395,89.82L236.395,93.027C236.395,94.594 237.121,95.289 238.922,95.289ZM238.922,94.09C237.938,94.09 237.75,93.676 237.75,92.961L237.75,89.891C237.75,89.199 237.938,88.758 238.922,88.758C239.973,88.758 240.152,89.199 240.152,89.891L240.152,92.961C240.152,93.676 239.906,94.09 238.922,94.09Z" style="fill:#0117bf;fill-rule:nonzero;"></path>
<path d="M247.867,93.43L249.375,90.383L249.215,92.547L249.215,95.223L250.574,95.223L250.574,87.648L249.266,87.648L247.711,91L246.164,87.648L244.859,87.648L244.859,95.223L246.215,95.223L246.215,92.547L246.059,90.383L247.562,93.43L247.867,93.43Z" style="fill:#0117bf;fill-rule:nonzero;"></path>
<path d="M256.41,95.289C258.211,95.289 259,94.594 259,93.027L259,89.82C259,88.219 258.242,87.559 256.41,87.559C254.582,87.559 253.883,88.219 253.883,89.82L253.883,93.027C253.883,94.594 254.609,95.289 256.41,95.289ZM256.41,94.09C255.426,94.09 255.238,93.676 255.238,92.961L255.238,89.891C255.238,89.199 255.426,88.758 256.41,88.758C257.465,88.758 257.64,89.199 257.64,89.891L257.64,92.961C257.64,93.676 257.394,94.09 256.41,94.09Z" style="fill:#0117bf;fill-rule:nonzero;"></path>
<path d="M267.297,95.223L265.633,92.34C266.547,92.043 266.941,91.434 266.941,90.312L266.941,89.918C266.941,88.316 266.086,87.648 264.254,87.648L262.348,87.648L262.348,95.223L263.703,95.223L263.703,92.555L264.207,92.555L265.711,95.223L267.297,95.223ZM263.695,91.305L263.695,88.895L264.273,88.895C265.328,88.895 265.516,89.27 265.516,89.988L265.516,90.242C265.516,90.961 265.238,91.305 264.254,91.305L263.695,91.305Z" style="fill:#0117bf;fill-rule:nonzero;"></path>
<path d="M275.188,95.223L273.527,92.34C274.441,92.043 274.836,91.434 274.836,90.312L274.836,89.918C274.836,88.316 273.977,87.648 272.148,87.648L270.238,87.648L270.238,95.223L271.598,95.223L271.598,92.555L272.098,92.555L273.605,95.223L275.188,95.223ZM271.586,91.305L271.586,88.895L272.168,88.895C273.223,88.895 273.406,89.27 273.406,89.988L273.406,90.242C273.406,90.961 273.133,91.305 272.148,91.305L271.586,91.305Z" style="fill:#0117bf;fill-rule:nonzero;"></path>
<path d="M280.555,95.289C282.355,95.289 283.141,94.594 283.141,93.027L283.141,89.82C283.141,88.219 282.383,87.559 280.555,87.559C278.723,87.559 278.023,88.219 278.023,89.82L278.023,93.027C278.023,94.594 278.754,95.289 280.555,95.289ZM280.555,94.09C279.57,94.09 279.383,93.676 279.383,92.961L279.383,89.891C279.383,89.199 279.57,88.758 280.555,88.758C281.605,88.758 281.785,89.199 281.785,89.891L281.785,92.961C281.785,93.676 281.539,94.09 280.555,94.09Z" style="fill:#0117bf;fill-rule:nonzero;"></path>
<path d="M290.141,91.141L291.25,95.23L292.688,95.23L294.586,87.648L293.188,87.648L292.441,90.262L292,93.352L290.66,87.895L289.617,87.895L288.379,93.352L287.836,90.262L287.09,87.648L285.691,87.648L287.699,95.23L289.125,95.23L290.141,91.141Z" style="fill:#0117bf;fill-rule:nonzero;"></path>
<path d="M90.383,76.133C90.336,76.137 90.293,76.141 90.25,76.141L80.816,76.141C80.773,76.141 80.73,76.137 80.684,76.133C80.641,76.129 80.598,76.121 80.555,76.113C80.508,76.105 80.465,76.094 80.426,76.082C80.383,76.066 80.34,76.055 80.297,76.035C80.258,76.02 80.219,76 80.18,75.98C80.141,75.957 80.102,75.934 80.066,75.91C80.027,75.887 79.992,75.859 79.957,75.832C79.922,75.805 79.891,75.773 79.859,75.742C79.828,75.711 79.797,75.68 79.77,75.645C79.742,75.609 79.715,75.574 79.691,75.535C79.668,75.5 79.645,75.461 79.621,75.422C79.602,75.383 79.582,75.344 79.566,75.301C79.547,75.262 79.535,75.219 79.52,75.176C79.508,75.137 79.496,75.094 79.488,75.047C79.48,75.004 79.473,74.961 79.469,74.918C79.465,74.871 79.461,74.828 79.461,74.785L79.461,17.875C79.461,17.828 79.465,17.785 79.469,17.742C79.473,17.695 79.48,17.652 79.488,17.609C79.496,17.566 79.508,17.523 79.52,17.48C79.535,17.438 79.547,17.395 79.566,17.355C79.582,17.312 79.602,17.273 79.621,17.234C79.645,17.195 79.668,17.156 79.691,17.121C79.715,17.082 79.742,17.047 79.77,17.012C79.797,16.98 79.828,16.945 79.859,16.914C79.891,16.883 79.922,16.855 79.957,16.824C79.992,16.797 80.027,16.77 80.066,16.746C80.102,16.723 80.141,16.699 80.18,16.68C80.219,16.656 80.258,16.637 80.297,16.621C80.34,16.605 80.383,16.59 80.426,16.578C80.465,16.562 80.508,16.555 80.555,16.543C80.598,16.535 80.641,16.527 80.684,16.523C80.73,16.52 80.773,16.52 80.816,16.52L90.25,16.52C90.293,16.52 90.336,16.52 90.383,16.523C90.426,16.527 90.469,16.535 90.512,16.543C90.555,16.555 90.598,16.562 90.641,16.578C90.684,16.59 90.727,16.605 90.766,16.621C90.809,16.637 90.848,16.656 90.887,16.68C90.926,16.699 90.965,16.723 91,16.746C91.039,16.77 91.074,16.797 91.109,16.824C91.141,16.855 91.176,16.883 91.207,16.914C91.238,16.945 91.27,16.98 91.297,17.012C91.324,17.047 91.352,17.082 91.375,17.121C91.398,17.156 91.422,17.195 91.441,17.234C91.465,17.273 91.484,17.312 91.5,17.355C91.516,17.395 91.531,17.438 91.547,17.48C91.559,17.523 91.57,17.566 91.578,17.609C91.586,17.652 91.594,17.695 91.598,17.742C91.602,17.785 91.602,17.828 91.602,17.875L91.602,44.699L129.363,16.785C129.477,16.695 129.604,16.633 129.742,16.586C129.882,16.539 130.022,16.52 130.168,16.52L143.746,16.52C143.852,16.52 143.953,16.531 144.059,16.555C144.16,16.578 144.258,16.613 144.355,16.664C144.449,16.711 144.535,16.77 144.617,16.836C144.699,16.906 144.77,16.98 144.832,17.066C144.859,17.102 144.883,17.137 144.906,17.176C144.93,17.215 144.949,17.254 144.969,17.293C144.988,17.332 145.004,17.375 145.02,17.418C145.035,17.457 145.047,17.5 145.059,17.543C145.07,17.586 145.078,17.629 145.086,17.676C145.09,17.719 145.098,17.762 145.098,17.805C145.102,17.852 145.102,17.895 145.098,17.941C145.098,17.984 145.09,18.027 145.086,18.07C145.078,18.117 145.07,18.16 145.059,18.203C145.047,18.246 145.035,18.289 145.02,18.328C145.004,18.371 144.988,18.414 144.969,18.453C144.949,18.492 144.93,18.531 144.906,18.57C144.883,18.609 144.859,18.645 144.832,18.68C144.805,18.715 144.777,18.75 144.75,18.781C144.719,18.816 144.688,18.848 144.656,18.879C144.621,18.906 144.586,18.934 144.551,18.961L91.602,58.23L91.602,74.785C91.602,74.828 91.602,74.871 91.598,74.918C91.594,74.961 91.586,75.004 91.578,75.047C91.57,75.094 91.559,75.137 91.547,75.176C91.531,75.219 91.516,75.262 91.5,75.301C91.484,75.344 91.465,75.383 91.441,75.422C91.422,75.461 91.398,75.5 91.375,75.535C91.352,75.574 91.324,75.609 91.297,75.645C91.27,75.68 91.238,75.711 91.207,75.742C91.176,75.773 91.141,75.805 91.109,75.832C91.074,75.859 91.039,75.887 91,75.91C90.965,75.934 90.926,75.957 90.887,75.98C90.848,76 90.809,76.02 90.766,76.035C90.727,76.055 90.684,76.066 90.641,76.082C90.598,76.094 90.555,76.105 90.512,76.113C90.469,76.121 90.426,76.129 90.383,76.133ZM88.93,57.234C88.906,57.34 88.895,57.441 88.895,57.547L88.895,73.43L82.172,73.43L82.172,19.227L88.895,19.227L88.895,47.387C88.895,47.531 88.914,47.672 88.961,47.809C89.008,47.949 89.074,48.074 89.16,48.191C89.211,48.262 89.27,48.328 89.336,48.387C89.402,48.449 89.473,48.5 89.551,48.547C89.625,48.594 89.707,48.629 89.789,48.66C89.875,48.691 89.961,48.711 90.047,48.727C90.137,48.738 90.223,48.742 90.312,48.738C90.402,48.734 90.488,48.723 90.574,48.699C90.66,48.68 90.746,48.648 90.824,48.613C90.906,48.574 90.98,48.527 91.055,48.477L130.613,19.227L139.645,19.227L89.441,56.461C89.355,56.523 89.281,56.594 89.211,56.676C89.145,56.758 89.086,56.844 89.039,56.938C88.992,57.035 88.953,57.133 88.93,57.234Z" style="fill:#0117bf;"></path>
<path d="M148.246,74.535C148.262,74.617 148.27,74.699 148.27,74.785C148.27,74.828 148.27,74.871 148.262,74.918C148.258,74.961 148.254,75.004 148.246,75.047C148.234,75.094 148.227,75.137 148.211,75.176C148.199,75.219 148.184,75.262 148.168,75.301C148.148,75.344 148.133,75.383 148.109,75.422C148.09,75.461 148.066,75.5 148.043,75.535C148.016,75.574 147.992,75.609 147.961,75.645C147.934,75.68 147.906,75.711 147.875,75.742C147.844,75.773 147.809,75.805 147.773,75.832C147.742,75.859 147.707,75.887 147.668,75.91C147.633,75.934 147.594,75.957 147.555,75.98C147.516,76 147.477,76.02 147.434,76.035C147.395,76.055 147.352,76.066 147.309,76.082C147.266,76.094 147.223,76.105 147.18,76.113C147.137,76.121 147.094,76.129 147.047,76.133C147.004,76.137 146.961,76.141 146.914,76.141L134.719,76.141C134.625,76.141 134.531,76.129 134.441,76.109C134.348,76.09 134.258,76.062 134.172,76.023C134.086,75.984 134.004,75.938 133.926,75.883C133.852,75.828 133.781,75.766 133.719,75.695L110.465,50.086C110.438,50.055 110.41,50.02 110.383,49.988C110.355,49.953 110.332,49.914 110.309,49.879C110.285,49.84 110.266,49.801 110.246,49.762C110.227,49.719 110.211,49.68 110.195,49.637C110.18,49.598 110.168,49.555 110.156,49.512C110.145,49.469 110.137,49.426 110.129,49.379C110.121,49.336 110.117,49.293 110.113,49.246L110.113,49.113C110.117,49.07 110.121,49.027 110.125,48.984C110.133,48.938 110.141,48.895 110.152,48.852C110.164,48.809 110.176,48.766 110.191,48.723C110.203,48.684 110.223,48.641 110.238,48.602C110.258,48.562 110.281,48.523 110.301,48.484C110.324,48.445 110.348,48.41 110.375,48.371C110.402,48.336 110.43,48.301 110.461,48.27C110.488,48.238 110.52,48.203 110.551,48.176C110.586,48.145 110.621,48.117 110.656,48.09L117.809,42.723C117.844,42.699 117.879,42.676 117.914,42.652C117.949,42.633 117.984,42.613 118.023,42.594C118.059,42.574 118.098,42.559 118.137,42.543C118.176,42.527 118.215,42.516 118.254,42.504C118.293,42.492 118.336,42.484 118.375,42.477C118.418,42.469 118.457,42.461 118.5,42.457C118.543,42.457 118.582,42.453 118.625,42.453C118.668,42.453 118.707,42.457 118.75,42.461C118.789,42.465 118.832,42.469 118.871,42.477C118.914,42.484 118.953,42.492 118.992,42.504C119.035,42.516 119.074,42.531 119.113,42.547C119.152,42.559 119.188,42.578 119.227,42.594C119.266,42.613 119.301,42.633 119.336,42.656C119.371,42.68 119.406,42.703 119.438,42.727C119.473,42.75 119.504,42.777 119.535,42.805C119.566,42.836 119.594,42.863 119.621,42.895L147.918,73.871C147.973,73.934 148.023,74 148.066,74.07C148.109,74.141 148.148,74.215 148.18,74.293C148.211,74.371 148.23,74.453 148.246,74.535ZM135.32,73.43L113.473,49.363L118.453,45.629L143.844,73.43L135.32,73.43Z" style="fill:#0117bf;"></path>
<path d="M171.984,16.52C171.938,16.52 171.895,16.52 171.852,16.523C171.805,16.527 171.762,16.535 171.719,16.543C171.676,16.555 171.633,16.562 171.59,16.578C171.547,16.59 171.504,16.605 171.465,16.621C171.426,16.637 171.383,16.656 171.344,16.68C171.305,16.699 171.27,16.723 171.23,16.746C171.195,16.77 171.156,16.797 171.125,16.824C171.09,16.855 171.055,16.883 171.023,16.914C170.992,16.945 170.965,16.98 170.938,17.012C170.906,17.047 170.883,17.082 170.855,17.121C170.832,17.156 170.809,17.195 170.789,17.234C170.766,17.273 170.75,17.312 170.73,17.355C170.715,17.395 170.699,17.438 170.688,17.48C170.676,17.523 170.664,17.566 170.652,17.609C170.645,17.652 170.641,17.695 170.637,17.742C170.629,17.785 170.629,17.828 170.629,17.875L170.629,74.785C170.629,74.828 170.629,74.871 170.637,74.918C170.641,74.961 170.645,75.004 170.652,75.047C170.664,75.094 170.676,75.137 170.688,75.176C170.699,75.219 170.715,75.262 170.73,75.301C170.75,75.344 170.766,75.383 170.789,75.422C170.809,75.461 170.832,75.5 170.855,75.535C170.883,75.574 170.906,75.609 170.938,75.645C170.965,75.68 170.992,75.711 171.023,75.742C171.055,75.773 171.09,75.805 171.125,75.832C171.156,75.859 171.195,75.887 171.23,75.91C171.27,75.934 171.305,75.957 171.344,75.98C171.383,76 171.426,76.02 171.465,76.035C171.504,76.055 171.547,76.066 171.59,76.082C171.633,76.094 171.676,76.105 171.719,76.113C171.762,76.121 171.805,76.129 171.852,76.133C171.895,76.137 171.938,76.141 171.984,76.141L212.391,76.141C212.434,76.141 212.48,76.137 212.523,76.133C212.566,76.129 212.609,76.121 212.656,76.113C212.699,76.105 212.742,76.094 212.785,76.082C212.828,76.066 212.867,76.055 212.91,76.035C212.949,76.02 212.988,76 213.027,75.98C213.066,75.957 213.105,75.934 213.145,75.91C213.18,75.887 213.215,75.859 213.25,75.832C213.285,75.805 213.316,75.773 213.348,75.742C213.379,75.711 213.41,75.68 213.438,75.645C213.465,75.609 213.492,75.574 213.516,75.535C213.543,75.5 213.566,75.461 213.586,75.422C213.605,75.383 213.625,75.344 213.641,75.301C213.66,75.262 213.672,75.219 213.688,75.176C213.699,75.137 213.711,75.094 213.719,75.047C213.727,75.004 213.734,74.961 213.738,74.918C213.742,74.871 213.746,74.828 213.746,74.785L213.746,66.328C213.746,66.285 213.742,66.238 213.738,66.195C213.734,66.152 213.727,66.109 213.719,66.062C213.711,66.02 213.699,65.977 213.688,65.934C213.672,65.895 213.66,65.852 213.641,65.809C213.625,65.77 213.605,65.73 213.586,65.691C213.566,65.652 213.543,65.613 213.516,65.574C213.492,65.539 213.465,65.504 213.438,65.469C213.41,65.434 213.379,65.402 213.348,65.372C213.316,65.34 213.285,65.309 213.25,65.281C213.215,65.254 213.18,65.227 213.145,65.204C213.105,65.177 213.066,65.156 213.027,65.134C212.988,65.113 212.949,65.094 212.91,65.079C212.867,65.059 212.828,65.047 212.785,65.031C212.742,65.02 212.699,65.009 212.656,65C212.609,64.992 212.566,64.984 212.523,64.981C212.48,64.977 212.434,64.973 212.391,64.973L182.77,64.973L182.77,17.875C182.77,17.828 182.766,17.785 182.762,17.742C182.758,17.695 182.754,17.652 182.742,17.609C182.734,17.566 182.723,17.523 182.711,17.48C182.699,17.438 182.684,17.395 182.668,17.355C182.648,17.312 182.629,17.273 182.609,17.234C182.59,17.195 182.566,17.156 182.543,17.121C182.516,17.082 182.488,17.047 182.461,17.012C182.434,16.98 182.402,16.945 182.371,16.914C182.34,16.883 182.309,16.855 182.273,16.824C182.238,16.797 182.203,16.77 182.168,16.746C182.129,16.723 182.094,16.699 182.055,16.68C182.016,16.656 181.973,16.637 181.934,16.621C181.891,16.605 181.852,16.59 181.809,16.578C181.766,16.562 181.723,16.555 181.68,16.543C181.637,16.535 181.59,16.527 181.547,16.523C181.504,16.52 181.457,16.52 181.414,16.52L171.984,16.52ZM173.34,19.227L173.34,73.43L211.035,73.43L211.035,67.684L181.414,67.684C181.371,67.684 181.324,67.68 181.281,67.676C181.238,67.672 181.195,67.668 181.148,67.656C181.105,67.648 181.062,67.637 181.02,67.625C180.977,67.613 180.938,67.598 180.895,67.582C180.855,67.562 180.816,67.543 180.777,67.523C180.738,67.504 180.699,67.48 180.66,67.457C180.625,67.43 180.59,67.406 180.555,67.375C180.52,67.348 180.488,67.316 180.457,67.285C180.426,67.254 180.395,67.223 180.367,67.188C180.34,67.152 180.312,67.117 180.289,67.082C180.262,67.043 180.238,67.008 180.219,66.969C180.199,66.93 180.18,66.887 180.164,66.848C180.145,66.805 180.129,66.766 180.117,66.723C180.105,66.68 180.094,66.637 180.086,66.594C180.078,66.551 180.07,66.504 180.066,66.461C180.062,66.418 180.059,66.375 180.059,66.328L180.059,19.227L173.34,19.227Z" style="fill:#0117bf;"></path>
<path d="M294.578,66.195C294.582,66.238 294.586,66.285 294.586,66.328L294.586,74.785C294.586,74.828 294.582,74.871 294.578,74.918C294.574,74.961 294.57,75.004 294.559,75.047C294.551,75.094 294.539,75.137 294.527,75.176C294.516,75.219 294.5,75.262 294.484,75.301C294.465,75.344 294.445,75.383 294.426,75.422C294.406,75.461 294.383,75.5 294.359,75.535C294.332,75.574 294.305,75.609 294.277,75.645C294.25,75.68 294.219,75.711 294.188,75.742C294.156,75.773 294.125,75.805 294.09,75.832C294.055,75.859 294.02,75.887 293.984,75.91C293.945,75.934 293.91,75.957 293.871,75.98C293.832,76 293.789,76.02 293.75,76.035C293.707,76.055 293.668,76.066 293.625,76.082C293.582,76.094 293.539,76.105 293.496,76.113C293.453,76.121 293.406,76.129 293.363,76.133C293.32,76.137 293.273,76.141 293.23,76.141L237.457,76.141C237.414,76.141 237.371,76.137 237.324,76.133C237.281,76.129 237.238,76.121 237.195,76.113C237.148,76.105 237.105,76.094 237.062,76.082C237.023,76.066 236.98,76.055 236.941,76.035C236.898,76.02 236.859,76 236.82,75.98C236.781,75.957 236.742,75.934 236.707,75.91C236.668,75.887 236.633,75.859 236.598,75.832C236.562,75.805 236.531,75.773 236.5,75.742C236.469,75.711 236.438,75.68 236.41,75.645C236.383,75.609 236.355,75.574 236.332,75.535C236.305,75.5 236.285,75.461 236.262,75.422C236.242,75.383 236.223,75.344 236.207,75.301C236.188,75.262 236.176,75.219 236.16,75.176C236.148,75.137 236.137,75.094 236.129,75.047C236.121,75.004 236.113,74.961 236.109,74.918C236.105,74.871 236.102,74.828 236.102,74.785L236.102,66.328C236.102,66.23 236.113,66.137 236.133,66.043C236.156,65.945 236.184,65.855 236.227,65.766C236.266,65.68 236.312,65.594 236.371,65.52C236.43,65.441 236.496,65.372 236.57,65.305L292.344,16.852C292.402,16.797 292.469,16.75 292.539,16.707C292.609,16.668 292.68,16.633 292.758,16.605C292.832,16.574 292.91,16.555 292.988,16.539C293.07,16.523 293.148,16.52 293.23,16.52C293.273,16.52 293.32,16.52 293.363,16.523C293.406,16.527 293.453,16.535 293.496,16.543C293.539,16.555 293.582,16.562 293.625,16.578C293.668,16.59 293.707,16.605 293.75,16.621C293.789,16.637 293.832,16.656 293.871,16.68C293.91,16.699 293.945,16.723 293.984,16.746C294.02,16.77 294.055,16.797 294.09,16.824C294.125,16.855 294.156,16.883 294.188,16.914C294.219,16.945 294.25,16.98 294.277,17.012C294.305,17.047 294.332,17.082 294.359,17.121C294.383,17.156 294.406,17.195 294.426,17.234C294.445,17.273 294.465,17.312 294.484,17.355C294.5,17.395 294.516,17.438 294.527,17.48C294.539,17.523 294.551,17.566 294.559,17.609C294.57,17.652 294.574,17.695 294.578,17.742C294.582,17.785 294.586,17.828 294.586,17.875L294.586,28.766C294.586,28.863 294.574,28.961 294.555,29.055C294.535,29.148 294.504,29.238 294.465,29.328C294.426,29.418 294.375,29.5 294.316,29.578C294.262,29.652 294.195,29.727 294.121,29.789L253.906,64.899L293.234,64.973C293.277,64.973 293.324,64.977 293.367,64.981C293.41,64.984 293.453,64.992 293.496,65C293.543,65.009 293.582,65.02 293.625,65.031C293.668,65.047 293.711,65.062 293.75,65.079C293.793,65.094 293.832,65.113 293.871,65.134C293.91,65.156 293.949,65.18 293.984,65.204C294.023,65.227 294.059,65.254 294.09,65.281C294.125,65.309 294.16,65.34 294.191,65.372C294.223,65.402 294.25,65.439 294.277,65.469C294.309,65.504 294.332,65.539 294.359,65.578C294.383,65.613 294.406,65.652 294.426,65.691C294.445,65.73 294.465,65.77 294.484,65.812C294.5,65.852 294.516,65.895 294.527,65.938C294.539,65.977 294.551,66.02 294.559,66.066C294.57,66.109 294.574,66.152 294.578,66.195ZM250.301,67.602L291.875,67.68L291.875,73.43L238.812,73.43L238.812,66.945L291.875,20.844L291.875,28.152L249.414,65.227C249.34,65.289 249.273,65.359 249.219,65.439C249.16,65.516 249.109,65.598 249.07,65.688C249.031,65.773 249,65.863 248.98,65.957C248.957,66.055 248.949,66.148 248.949,66.246C248.949,66.289 248.949,66.332 248.953,66.379C248.961,66.422 248.965,66.465 248.973,66.508C248.984,66.555 248.992,66.598 249.008,66.637C249.02,66.68 249.035,66.723 249.051,66.762C249.066,66.805 249.086,66.844 249.105,66.883C249.129,66.922 249.152,66.961 249.176,67C249.199,67.035 249.227,67.07 249.254,67.105C249.281,67.141 249.312,67.172 249.344,67.203C249.375,67.234 249.406,67.266 249.441,67.293C249.477,67.32 249.512,67.348 249.551,67.371C249.586,67.398 249.625,67.422 249.664,67.441C249.703,67.461 249.742,67.48 249.781,67.5C249.824,67.516 249.867,67.531 249.906,67.543C249.949,67.555 249.992,67.566 250.035,67.574C250.082,67.586 250.125,67.59 250.168,67.594C250.211,67.602 250.258,67.602 250.301,67.602Z" style="fill:#0117bf;"></path>
<path d="M238.059,16.523C238.102,16.52 238.145,16.52 238.191,16.52L281.281,16.52C281.383,16.52 281.484,16.531 281.582,16.551C281.684,16.574 281.781,16.609 281.871,16.656C281.965,16.699 282.051,16.754 282.133,16.82C282.211,16.883 282.281,16.957 282.348,17.039C282.375,17.074 282.398,17.109 282.422,17.145C282.445,17.184 282.469,17.223 282.488,17.262C282.508,17.301 282.527,17.34 282.543,17.383C282.559,17.426 282.574,17.465 282.586,17.508C282.598,17.551 282.605,17.594 282.613,17.641C282.621,17.684 282.629,17.727 282.629,17.77C282.633,17.816 282.637,17.859 282.633,17.902C282.633,17.949 282.629,17.992 282.625,18.035C282.621,18.082 282.613,18.125 282.602,18.168C282.594,18.211 282.582,18.254 282.566,18.297C282.555,18.336 282.539,18.379 282.52,18.418C282.5,18.461 282.48,18.5 282.461,18.539C282.438,18.578 282.414,18.613 282.387,18.652C282.363,18.688 282.336,18.723 282.309,18.758C282.277,18.789 282.246,18.82 282.215,18.852C282.184,18.883 282.148,18.914 282.117,18.941L271.223,27.477C271.102,27.57 270.969,27.641 270.828,27.691C270.684,27.738 270.535,27.766 270.387,27.766L238.191,27.766C238.145,27.766 238.102,27.762 238.059,27.758C238.012,27.754 237.969,27.746 237.926,27.738C237.883,27.73 237.84,27.719 237.797,27.707C237.754,27.695 237.711,27.68 237.672,27.66C237.629,27.645 237.59,27.625 237.551,27.605C237.512,27.582 237.473,27.562 237.438,27.535C237.398,27.512 237.363,27.484 237.328,27.457C237.297,27.43 237.262,27.398 237.23,27.367C237.199,27.336 237.172,27.305 237.141,27.27C237.113,27.234 237.09,27.199 237.062,27.164C237.039,27.125 237.016,27.086 236.996,27.047C236.973,27.008 236.953,26.969 236.938,26.93C236.922,26.887 236.906,26.844 236.895,26.805C236.879,26.762 236.871,26.719 236.859,26.676C236.852,26.629 236.844,26.586 236.84,26.543C236.836,26.5 236.836,26.453 236.836,26.41L236.836,17.875C236.836,17.828 236.836,17.785 236.84,17.742C236.844,17.695 236.852,17.652 236.859,17.609C236.871,17.566 236.879,17.523 236.895,17.48C236.906,17.438 236.922,17.395 236.938,17.355C236.953,17.312 236.973,17.273 236.996,17.234C237.016,17.195 237.039,17.156 237.062,17.121C237.09,17.082 237.113,17.047 237.141,17.012C237.172,16.98 237.199,16.945 237.23,16.914C237.262,16.883 237.297,16.855 237.328,16.824C237.363,16.797 237.398,16.77 237.438,16.746C237.473,16.723 237.512,16.699 237.551,16.68C237.59,16.656 237.629,16.637 237.672,16.621C237.711,16.605 237.754,16.59 237.797,16.578C237.84,16.562 237.883,16.555 237.926,16.543C237.969,16.535 238.012,16.527 238.059,16.523ZM277.352,19.227L269.918,25.055L239.543,25.055L239.543,19.227L277.352,19.227Z" style="fill:#0117bf;"></path>
<path d="M24.406,28.266L16.988,0.547C16.988,0.328 16.77,0.109 16.441,0.109L15.023,0C14.586,0 14.258,0.328 14.367,0.762L19.059,27.5C19.059,27.719 19.277,27.828 19.496,27.938L21.57,28.59C21.789,28.59 21.898,28.699 22.008,28.918C22.66,28.484 23.426,28.266 24.188,28.266L24.406,28.266Z" style="fill:#0117bf;"></path>
<path d="M26.688,32.547C26.695,32.465 26.699,32.383 26.699,32.301C26.699,32.219 26.695,32.137 26.688,32.055C26.68,31.973 26.668,31.895 26.652,31.812C26.633,31.73 26.613,31.652 26.59,31.574C26.566,31.496 26.539,31.418 26.508,31.34C26.477,31.266 26.441,31.191 26.402,31.117C26.363,31.047 26.32,30.977 26.277,30.906C26.23,30.84 26.18,30.773 26.129,30.711C26.078,30.648 26.023,30.586 25.965,30.527C25.906,30.469 25.844,30.414 25.781,30.363C25.719,30.309 25.652,30.262 25.582,30.215C25.516,30.168 25.445,30.125 25.371,30.09C25.301,30.051 25.227,30.016 25.148,29.984C25.074,29.953 24.996,29.926 24.918,29.898C24.84,29.875 24.758,29.855 24.68,29.84C24.598,29.824 24.516,29.812 24.434,29.805C24.352,29.797 24.27,29.793 24.188,29.793C24.105,29.793 24.023,29.797 23.945,29.805C23.859,29.812 23.781,29.824 23.699,29.84C23.617,29.855 23.539,29.875 23.461,29.898C23.383,29.926 23.305,29.953 23.23,29.984C23.152,30.016 23.078,30.051 23.008,30.09C22.934,30.125 22.863,30.168 22.793,30.215C22.727,30.262 22.66,30.309 22.598,30.363C22.535,30.414 22.473,30.469 22.414,30.527C22.355,30.586 22.301,30.648 22.25,30.711C22.195,30.773 22.148,30.84 22.102,30.906C22.055,30.977 22.016,31.047 21.977,31.117C21.938,31.191 21.902,31.266 21.871,31.34C21.84,31.418 21.812,31.496 21.789,31.574C21.762,31.652 21.742,31.73 21.727,31.812C21.711,31.895 21.699,31.973 21.691,32.055C21.684,32.137 21.68,32.219 21.68,32.301C21.68,32.383 21.684,32.465 21.691,32.547C21.699,32.629 21.711,32.711 21.727,32.793C21.742,32.871 21.762,32.953 21.789,33.031C21.812,33.109 21.84,33.188 21.871,33.262C21.902,33.34 21.938,33.414 21.977,33.484C22.016,33.559 22.055,33.629 22.102,33.695C22.148,33.766 22.195,33.832 22.25,33.895C22.301,33.957 22.355,34.02 22.414,34.078C22.473,34.137 22.535,34.191 22.598,34.242C22.66,34.293 22.727,34.344 22.793,34.391C22.863,34.434 22.934,34.477 23.008,34.516C23.078,34.555 23.152,34.59 23.23,34.621C23.305,34.652 23.383,34.68 23.461,34.703C23.539,34.727 23.617,34.746 23.699,34.766C23.781,34.781 23.859,34.793 23.945,34.801C24.023,34.809 24.105,34.812 24.188,34.812C24.27,34.812 24.352,34.809 24.434,34.801C24.516,34.793 24.598,34.781 24.68,34.766C24.758,34.746 24.84,34.727 24.918,34.703C24.996,34.68 25.074,34.652 25.148,34.621C25.227,34.59 25.301,34.555 25.371,34.516C25.445,34.477 25.516,34.434 25.582,34.391C25.652,34.344 25.719,34.293 25.781,34.242C25.844,34.191 25.906,34.137 25.965,34.078C26.023,34.02 26.078,33.957 26.129,33.895C26.18,33.832 26.23,33.766 26.277,33.695C26.32,33.629 26.363,33.559 26.402,33.484C26.441,33.414 26.477,33.34 26.508,33.262C26.539,33.188 26.566,33.109 26.59,33.031C26.613,32.953 26.633,32.871 26.652,32.793C26.668,32.711 26.68,32.629 26.688,32.547Z" style="fill:#0117bf;"></path>
<path d="M55.945,41.688L56.711,40.488C56.926,40.16 56.816,39.723 56.383,39.504L30.957,30.23L30.738,30.23C30.52,30.23 30.41,30.336 30.301,30.445L28.664,31.977C28.555,32.082 28.336,32.191 28.227,32.191L28.117,32.191L28.117,32.41C28.117,33.176 27.898,33.938 27.465,34.594L55.289,42.016L55.398,42.016C55.617,42.016 55.836,41.906 55.945,41.688Z" style="fill:#0117bf;"></path>
<path d="M1.707,56.527L21.68,39.941L22.551,39.176C22.66,39.066 22.77,38.742 22.66,38.523L22.117,36.34C22.117,36.121 22.117,35.902 22.223,35.793C22.008,35.684 21.898,35.574 21.68,35.465C21.133,35.141 20.805,34.594 20.477,34.047L0.18,54.348C-0.038,54.562 -0.038,54.891 0.07,55.109L0.727,56.309C0.835,56.527 1.055,56.637 1.273,56.637C1.492,56.637 1.598,56.637 1.707,56.527Z" style="fill:#0117bf;"></path>
<path d="M25.824,35.902L28.008,98.215L20.371,98.215L22.332,41.25L23.535,40.27C24.188,39.723 24.406,38.957 24.188,38.195L23.754,36.449L23.973,36.23L24.188,36.23C24.844,36.23 25.391,36.121 25.824,35.902Z" style="fill:#0117bf;"></path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:serif="http://www.serif.com/" width="100%" height="100%" viewBox="0 0 46 46" version="1.1" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;">
<rect id="High-Voltage" serif:id="High Voltage" x="0.419" y="0.459" width="44.943" height="44.943" style="fill:none;"></rect>
<clipPath id="_clip1">
<rect x="0.419" y="0.459" width="44.943" height="44.943"></rect>
</clipPath>
<g clip-path="url(#_clip1)">
<path d="M44.612,22.931c0,-11.988 -9.733,-21.722 -21.721,-21.722c-11.988,0 -21.722,9.734 -21.722,21.722c0,11.988 9.734,21.721 21.722,21.721c11.988,0 21.721,-9.733 21.721,-21.721Z" style="fill:none;stroke:currentColor;stroke-width:1.5px;"></path>
<g>
<path d="M24.642,22.947c-0,-1.031 -0.837,-1.867 -1.867,-1.867c-1.031,-0 -1.868,0.836 -1.868,1.867c0,1.03 0.837,1.867 1.868,1.867c1.03,0 1.867,-0.837 1.867,-1.867Z" style="fill:none;stroke:currentColor;stroke-width:0.66px;"></path>
<path d="M28.617,22.947c0,-1.031 -0.836,-1.867 -1.867,-1.867c-1.031,-0 -1.867,0.836 -1.867,1.867c-0,1.03 0.836,1.867 1.867,1.867c1.031,0 1.867,-0.837 1.867,-1.867Z" style="fill:none;stroke:currentColor;stroke-width:0.66px;"></path>
<path d="M32.593,22.947c-0,-1.031 -0.837,-1.867 -1.867,-1.867c-1.031,-0 -1.868,0.836 -1.868,1.867c0,1.03 0.837,1.867 1.868,1.867c1.03,0 1.867,-0.837 1.867,-1.867Z" style="fill:none;stroke:currentColor;stroke-width:0.66px;"></path>
<path d="M20.666,22.947c0,-1.031 -0.836,-1.867 -1.867,-1.867c-1.03,-0 -1.867,0.836 -1.867,1.867c-0,1.03 0.837,1.867 1.867,1.867c1.031,0 1.867,-0.837 1.867,-1.867Z" style="fill:none;stroke:currentColor;stroke-width:0.66px;"></path>
<path d="M16.691,22.947c0,-1.031 -0.837,-1.867 -1.867,-1.867c-1.031,-0 -1.868,0.836 -1.868,1.867c0,1.03 0.837,1.867 1.868,1.867c1.03,0 1.867,-0.837 1.867,-1.867Z" style="fill:none;stroke:currentColor;stroke-width:0.66px;"></path>
<path d="M26.528,19.662c0,-1.031 -0.836,-1.868 -1.867,-1.868c-1.031,0 -1.867,0.837 -1.867,1.868c-0,1.03 0.836,1.867 1.867,1.867c1.031,-0 1.867,-0.837 1.867,-1.867Z" style="fill:none;stroke:currentColor;stroke-width:0.66px;"></path>
<path d="M30.354,19.662c0,-1.031 -0.836,-1.868 -1.867,-1.868c-1.031,0 -1.867,0.837 -1.867,1.868c-0,1.03 0.836,1.867 1.867,1.867c1.031,-0 1.867,-0.837 1.867,-1.867Z" style="fill:none;stroke:currentColor;stroke-width:0.66px;"></path>
<path d="M22.702,19.662c0,-1.031 -0.836,-1.868 -1.867,-1.868c-1.031,0 -1.867,0.837 -1.867,1.868c-0,1.03 0.836,1.867 1.867,1.867c1.031,-0 1.867,-0.837 1.867,-1.867Z" style="fill:none;stroke:currentColor;stroke-width:0.66px;"></path>
<path d="M18.877,19.662c-0,-1.031 -0.837,-1.868 -1.868,-1.868c-1.03,0 -1.867,0.837 -1.867,1.868c-0,1.03 0.837,1.867 1.867,1.867c1.031,-0 1.868,-0.837 1.868,-1.867Z" style="fill:none;stroke:currentColor;stroke-width:0.66px;"></path>
<path d="M28.597,16.426c-0,-1.03 -0.837,-1.867 -1.868,-1.867c-1.03,0 -1.867,0.837 -1.867,1.867c0,1.031 0.837,1.868 1.867,1.868c1.031,-0 1.868,-0.837 1.868,-1.868Z" style="fill:none;stroke:currentColor;stroke-width:0.66px;"></path>
<path d="M24.771,16.426c-0,-1.03 -0.837,-1.867 -1.867,-1.867c-1.031,0 -1.868,0.837 -1.868,1.867c0,1.031 0.837,1.868 1.868,1.868c1.03,-0 1.867,-0.837 1.867,-1.868Z" style="fill:none;stroke:currentColor;stroke-width:0.66px;"></path>
<path d="M20.945,16.426c-0,-1.03 -0.837,-1.867 -1.867,-1.867c-1.031,0 -1.868,0.837 -1.868,1.867c0,1.031 0.837,1.868 1.868,1.868c1.03,-0 1.867,-0.837 1.867,-1.868Z" style="fill:none;stroke:currentColor;stroke-width:0.66px;"></path>
<path d="M28.639,29.537c0,-1.031 -0.836,-1.867 -1.867,-1.867c-1.031,-0 -1.867,0.836 -1.867,1.867c-0,1.03 0.836,1.867 1.867,1.867c1.031,0 1.867,-0.837 1.867,-1.867Z" style="fill:none;stroke:currentColor;stroke-width:0.66px;"></path>
<path d="M24.813,29.537c0,-1.031 -0.836,-1.867 -1.867,-1.867c-1.031,-0 -1.867,0.836 -1.867,1.867c-0,1.03 0.836,1.867 1.867,1.867c1.031,0 1.867,-0.837 1.867,-1.867Z" style="fill:none;stroke:currentColor;stroke-width:0.66px;"></path>
<path d="M20.987,29.537c0,-1.031 -0.836,-1.867 -1.867,-1.867c-1.031,-0 -1.867,0.836 -1.867,1.867c-0,1.03 0.836,1.867 1.867,1.867c1.031,0 1.867,-0.837 1.867,-1.867Z" style="fill:none;stroke:currentColor;stroke-width:0.66px;"></path>
<path d="M26.741,26.232c-0,-1.03 -0.837,-1.867 -1.868,-1.867c-1.03,0 -1.867,0.837 -1.867,1.867c-0,1.031 0.837,1.867 1.867,1.867c1.031,0 1.868,-0.836 1.868,-1.867Z" style="fill:none;stroke:currentColor;stroke-width:0.66px;"></path>
<path d="M30.779,26.232c-0,-1.03 -0.837,-1.867 -1.868,-1.867c-1.03,0 -1.867,0.837 -1.867,1.867c-0,1.031 0.837,1.867 1.867,1.867c1.031,0 1.868,-0.836 1.868,-1.867Z" style="fill:none;stroke:currentColor;stroke-width:0.66px;"></path>
<path d="M22.702,26.232c0,-1.03 -0.836,-1.867 -1.867,-1.867c-1.031,0 -1.867,0.837 -1.867,1.867c-0,1.031 0.836,1.867 1.867,1.867c1.031,0 1.867,-0.836 1.867,-1.867Z" style="fill:none;stroke:currentColor;stroke-width:0.66px;"></path>
<path d="M18.664,26.232c0,-1.03 -0.836,-1.867 -1.867,-1.867c-1.031,0 -1.867,0.837 -1.867,1.867c-0,1.031 0.836,1.867 1.867,1.867c1.031,0 1.867,-0.836 1.867,-1.867Z" style="fill:none;stroke:currentColor;stroke-width:0.66px;"></path>
</g>
<path d="M35.253,22.931c-0,-6.823 -5.539,-12.362 -12.362,-12.362c-6.823,-0 -12.362,5.539 -12.362,12.362c0,6.823 5.539,12.362 12.362,12.362c6.823,0 12.362,-5.539 12.362,-12.362Z" style="fill:none;stroke:currentColor;stroke-width:1.5px;"></path>
<path d="M7.792,21.245c0.931,0 1.686,0.756 1.686,1.686c-0,0.93 -0.755,1.686 -1.686,1.686c-0.93,-0 -1.685,-0.756 -1.685,-1.686c-0,-0.93 0.755,-1.686 1.685,-1.686Zm0.078,3.965c0.899,-0.24 1.824,0.294 2.065,1.193c0.241,0.898 -0.293,1.823 -1.192,2.064c-0.899,0.241 -1.824,-0.293 -2.064,-1.192c-0.241,-0.899 0.293,-1.824 1.191,-2.065Zm1.102,3.81c0.806,-0.465 1.838,-0.188 2.303,0.618c0.465,0.805 0.189,1.837 -0.617,2.302c-0.806,0.465 -1.838,0.189 -2.303,-0.617c-0.465,-0.806 -0.189,-1.837 0.617,-2.303Zm2.051,3.395c0.657,-0.657 1.726,-0.657 2.383,0c0.658,0.658 0.658,1.727 0.001,2.384c-0.658,0.658 -1.726,0.658 -2.384,0c-0.658,-0.658 -0.658,-1.726 -0,-2.384Zm2.859,2.749c0.465,-0.806 1.497,-1.082 2.302,-0.617c0.806,0.465 1.083,1.497 0.618,2.303c-0.466,0.805 -1.497,1.082 -2.303,0.617c-0.806,-0.466 -1.083,-1.497 -0.617,-2.303Zm3.473,1.915c0.241,-0.899 1.166,-1.433 2.064,-1.192c0.899,0.241 1.433,1.166 1.192,2.065c-0.24,0.898 -1.165,1.432 -2.064,1.191c-0.899,-0.24 -1.433,-1.165 -1.192,-2.064Zm3.85,0.951c0,-0.931 0.756,-1.686 1.686,-1.686c0.93,0 1.686,0.755 1.686,1.686c-0,0.93 -0.756,1.685 -1.686,1.685c-0.93,0 -1.686,-0.755 -1.686,-1.685Zm3.965,-0.078c-0.24,-0.899 0.294,-1.824 1.193,-2.065c0.898,-0.241 1.823,0.293 2.064,1.192c0.241,0.899 -0.293,1.824 -1.192,2.064c-0.899,0.241 -1.824,-0.293 -2.065,-1.191Zm3.81,-1.102c-0.465,-0.806 -0.188,-1.838 0.618,-2.303c0.805,-0.465 1.837,-0.189 2.302,0.617c0.465,0.806 0.189,1.837 -0.617,2.303c-0.806,0.465 -1.837,0.188 -2.303,-0.617Zm3.395,-2.051c-0.657,-0.657 -0.657,-1.726 0,-2.384c0.658,-0.657 1.727,-0.657 2.384,0c0.658,0.658 0.658,1.726 0,2.384c-0.658,0.658 -1.726,0.658 -2.384,0Zm2.749,-2.859c-0.806,-0.465 -1.082,-1.497 -0.617,-2.302c0.465,-0.806 1.497,-1.083 2.303,-0.618c0.805,0.466 1.082,1.497 0.617,2.303c-0.466,0.806 -1.497,1.082 -2.303,0.617Zm1.915,-3.473c-0.899,-0.241 -1.433,-1.166 -1.192,-2.064c0.241,-0.899 1.166,-1.433 2.065,-1.193c0.898,0.241 1.432,1.166 1.191,2.065c-0.24,0.899 -1.165,1.433 -2.064,1.192Zm0.951,-3.85c-0.931,-0 -1.686,-0.756 -1.686,-1.686c0,-0.93 0.755,-1.686 1.686,-1.686c0.93,0 1.685,0.756 1.685,1.686c0,0.93 -0.755,1.686 -1.685,1.686Zm-0.078,-3.966c-0.899,0.241 -1.824,-0.293 -2.065,-1.192c-0.241,-0.898 0.293,-1.823 1.192,-2.064c0.899,-0.241 1.824,0.293 2.064,1.192c0.241,0.899 -0.293,1.824 -1.191,2.064Zm-1.102,-3.809c-0.806,0.465 -1.838,0.188 -2.303,-0.618c-0.465,-0.805 -0.189,-1.837 0.617,-2.302c0.806,-0.465 1.837,-0.189 2.303,0.617c0.465,0.806 0.188,1.837 -0.617,2.303Zm-2.051,-3.395c-0.657,0.657 -1.726,0.657 -2.384,-0.001c-0.657,-0.657 -0.657,-1.726 0,-2.383c0.658,-0.658 1.726,-0.658 2.384,-0c0.658,0.658 0.658,1.726 0,2.384Zm-2.859,-2.749c-0.465,0.806 -1.497,1.082 -2.302,0.617c-0.806,-0.465 -1.083,-1.497 -0.618,-2.303c0.466,-0.805 1.497,-1.082 2.303,-0.617c0.806,0.465 1.082,1.497 0.617,2.303Zm-3.473,-1.915c-0.241,0.899 -1.166,1.433 -2.064,1.192c-0.899,-0.241 -1.433,-1.166 -1.193,-2.065c0.241,-0.898 1.166,-1.432 2.065,-1.191c0.899,0.24 1.433,1.165 1.192,2.064Zm-3.85,-0.951c-0,0.931 -0.756,1.686 -1.686,1.686c-0.93,-0 -1.686,-0.755 -1.686,-1.686c0,-0.93 0.756,-1.685 1.686,-1.685c0.93,-0 1.686,0.755 1.686,1.685Zm-3.966,0.078c0.241,0.899 -0.293,1.824 -1.192,2.065c-0.898,0.241 -1.823,-0.293 -2.064,-1.192c-0.241,-0.899 0.293,-1.824 1.192,-2.064c0.899,-0.241 1.824,0.293 2.064,1.191Zm-3.809,1.102c0.465,0.806 0.188,1.838 -0.618,2.303c-0.805,0.465 -1.837,0.189 -2.302,-0.617c-0.466,-0.806 -0.189,-1.838 0.617,-2.303c0.806,-0.465 1.837,-0.188 2.303,0.617Zm-3.395,2.051c0.657,0.657 0.657,1.726 -0.001,2.383c-0.657,0.658 -1.726,0.658 -2.383,0.001c-0.658,-0.658 -0.658,-1.726 -0,-2.384c0.658,-0.658 1.726,-0.658 2.384,-0Zm-2.749,2.859c0.806,0.465 1.082,1.497 0.617,2.302c-0.465,0.806 -1.497,1.083 -2.303,0.618c-0.806,-0.466 -1.082,-1.497 -0.617,-2.303c0.465,-0.806 1.497,-1.082 2.303,-0.617Zm-1.915,3.473c0.899,0.241 1.433,1.166 1.192,2.064c-0.241,0.899 -1.166,1.433 -2.065,1.192c-0.898,-0.24 -1.432,-1.165 -1.191,-2.064c0.24,-0.899 1.165,-1.433 2.064,-1.192Z" style="fill:none;stroke:currentColor;stroke-width:0.54px;"></path>
<path d="M41.098,22.931c0,-9.799 -8.158,-17.755 -18.207,-17.755c-10.049,0 -18.208,7.956 -18.208,17.755c0,9.799 8.159,17.755 18.208,17.755c10.049,-0 18.207,-7.956 18.207,-17.755Z" style="fill:none;stroke:currentColor;stroke-width:1.5px;"></path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

View File

@@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:serif="http://www.serif.com/" width="100%" height="100%" viewBox="0 0 295 99" version="1.1" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,0.000798697,0)">
<path d="M83.219,92.879C83.219,93.629 82.973,94.043 81.992,94.043C81.008,94.043 80.82,93.629 80.82,92.91L80.82,89.969C80.82,89.25 81.008,88.836 81.992,88.836C83.043,88.836 83.219,89.25 83.219,89.988L84.578,89.988C84.578,88.305 83.82,87.637 81.992,87.637C80.16,87.637 79.461,88.297 79.461,89.898L79.461,92.98C79.461,94.543 80.191,95.242 81.992,95.242C83.793,95.242 84.578,94.543 84.578,92.879L83.219,92.879Z" style="fill:white;fill-rule:nonzero;"></path>
<path d="M90.543,87.656L89.195,87.656L87.102,95.223L88.496,95.223L88.891,93.883L90.828,93.883L91.211,95.223L92.609,95.223L90.543,87.656ZM89.227,92.555L89.855,89.754L90.484,92.555L89.227,92.555Z" style="fill:white;fill-rule:nonzero;"></path>
<path d="M95.336,95.223L97.836,95.223C99.668,95.223 100.523,94.574 100.523,92.871C100.523,91.828 99.922,91.148 99.137,90.98C99.734,90.578 99.824,90.117 99.824,89.652C99.824,88.473 98.957,87.648 97.59,87.648L95.336,87.648L95.336,95.223ZM96.688,91.809L97.836,91.809C98.82,91.809 99.066,92.152 99.066,92.898C99.066,93.617 98.91,93.992 97.855,93.992L96.688,93.992L96.688,91.809ZM97.59,88.809C98.258,88.809 98.426,89.289 98.426,89.672C98.426,90.156 98.16,90.559 97.602,90.559L96.695,90.559L96.695,88.809L97.59,88.809Z" style="fill:white;fill-rule:nonzero;"></path>
<path d="M107.906,93.98L104.98,93.98L104.98,87.648L103.613,87.648L103.613,95.223L107.906,95.223L107.906,93.98Z" style="fill:white;fill-rule:nonzero;"></path>
<path d="M110.879,87.648L110.879,95.23L115.375,95.23L115.375,93.992L112.238,93.992L112.238,91.996L114.793,91.996L114.793,90.773L112.238,90.773L112.238,88.828L115.238,88.828L115.238,87.648L110.879,87.648Z" style="fill:white;fill-rule:nonzero;"></path>
<path d="M121.684,89.625L123.051,89.625C122.926,88.109 122.02,87.605 120.652,87.605C119.098,87.605 118.23,88.344 118.23,89.762C118.23,91.324 119.137,91.75 119.992,91.855C120.797,91.965 121.863,91.965 121.863,92.859C121.863,93.715 121.488,94.062 120.672,94.062C119.805,94.062 119.551,93.746 119.52,93.164L118.152,93.164C118.152,94.387 118.754,95.301 120.641,95.301C122.461,95.301 123.219,94.562 123.219,92.812C123.219,91.297 122.383,90.941 121.508,90.805C120.355,90.629 119.598,90.707 119.598,89.754C119.598,89.035 119.902,88.797 120.652,88.797C121.309,88.797 121.645,88.984 121.684,89.625Z" style="fill:white;fill-rule:nonzero;"></path>
<path d="M135.348,87.648L130.91,87.648L130.91,95.23L132.258,95.23L132.258,92.004L134.875,92.004L134.875,90.773L132.258,90.773L132.258,88.887L135.348,88.887L135.348,87.648Z" style="fill:white;fill-rule:nonzero;"></path>
<path d="M140.82,95.289C142.621,95.289 143.406,94.594 143.406,93.027L143.406,89.82C143.406,88.219 142.648,87.559 140.82,87.559C138.988,87.559 138.289,88.219 138.289,89.82L138.289,93.027C138.289,94.594 139.02,95.289 140.82,95.289ZM140.82,94.09C139.836,94.09 139.648,93.676 139.648,92.961L139.648,89.891C139.648,89.199 139.836,88.758 140.82,88.758C141.871,88.758 142.051,89.199 142.051,89.891L142.051,92.961C142.051,93.676 141.805,94.09 140.82,94.09Z" style="fill:white;fill-rule:nonzero;"></path>
<path d="M151.703,95.223L150.039,92.34C150.957,92.043 151.348,91.434 151.348,90.312L151.348,89.918C151.348,88.316 150.492,87.648 148.664,87.648L146.754,87.648L146.754,95.223L148.113,95.223L148.113,92.555L148.613,92.555L150.121,95.223L151.703,95.223ZM148.102,91.305L148.102,88.895L148.684,88.895C149.734,88.895 149.922,89.27 149.922,89.988L149.922,90.242C149.922,90.961 149.648,91.305 148.664,91.305L148.102,91.305Z" style="fill:white;fill-rule:nonzero;"></path>
<path d="M161.707,87.656L160.359,87.656L158.262,95.223L159.66,95.223L160.055,93.883L161.992,93.883L162.375,95.223L163.773,95.223L161.707,87.656ZM160.387,92.555L161.016,89.754L161.648,92.555L160.387,92.555Z" style="fill:white;fill-rule:nonzero;"></path>
<path d="M173.254,92.145L174.543,92.145L174.543,92.879C174.543,93.629 174.195,94.043 173.215,94.043C172.23,94.043 172.043,93.629 172.043,92.91L172.043,89.938C172.043,89.25 172.23,88.809 173.215,88.809C174.266,88.809 174.441,89.16 174.441,89.871L175.801,89.871C175.801,88.246 175.043,87.605 173.215,87.605C171.383,87.605 170.684,88.324 170.684,89.871L170.684,92.91C170.684,94.543 171.414,95.262 173.215,95.262C175.012,95.262 175.801,94.543 175.801,92.879L175.801,90.914L173.254,90.914L173.254,92.145Z" style="fill:white;fill-rule:nonzero;"></path>
<path d="M184.02,95.223L182.355,92.34C183.27,92.043 183.664,91.434 183.664,90.312L183.664,89.918C183.664,88.316 182.809,87.648 180.98,87.648L179.07,87.648L179.07,95.223L180.426,95.223L180.426,92.555L180.93,92.555L182.434,95.223L184.02,95.223ZM180.418,91.305L180.418,88.895L181,88.895C182.051,88.895 182.238,89.27 182.238,89.988L182.238,90.242C182.238,90.961 181.961,91.305 180.98,91.305L180.418,91.305Z" style="fill:white;fill-rule:nonzero;"></path>
<path d="M186.965,87.648L186.965,95.23L191.461,95.23L191.461,93.992L188.32,93.992L188.32,91.996L190.879,91.996L190.879,90.773L188.32,90.773L188.32,88.828L191.32,88.828L191.32,87.648L186.965,87.648Z" style="fill:white;fill-rule:nonzero;"></path>
<path d="M194.562,87.648L194.562,95.23L199.059,95.23L199.059,93.992L195.918,93.992L195.918,91.996L198.477,91.996L198.477,90.773L195.918,90.773L195.918,88.828L198.922,88.828L198.922,87.648L194.562,87.648Z" style="fill:white;fill-rule:nonzero;"></path>
<path d="M206.922,87.656L205.574,87.656L205.574,89.445L205.723,92.645L203.496,87.656L202.148,87.656L202.148,95.223L203.496,95.223L203.496,93.293L203.379,90.422L205.602,95.223L206.922,95.223L206.922,87.656Z" style="fill:white;fill-rule:nonzero;"></path>
<path d="M210.34,87.648L210.34,95.23L214.836,95.23L214.836,93.992L211.695,93.992L211.695,91.996L214.254,91.996L214.254,90.773L211.695,90.773L211.695,88.828L214.695,88.828L214.695,87.648L210.34,87.648Z" style="fill:white;fill-rule:nonzero;"></path>
<path d="M222.887,95.223L221.223,92.34C222.137,92.043 222.531,91.434 222.531,90.312L222.531,89.918C222.531,88.316 221.676,87.648 219.848,87.648L217.938,87.648L217.938,95.223L219.293,95.223L219.293,92.555L219.797,92.555L221.301,95.223L222.887,95.223ZM219.285,91.305L219.285,88.895L219.867,88.895C220.918,88.895 221.105,89.27 221.105,89.988L221.105,90.242C221.105,90.961 220.828,91.305 219.844,91.305L219.285,91.305Z" style="fill:white;fill-rule:nonzero;"></path>
<path d="M233.93,87.676L229.445,87.676L229.445,88.887L231.02,88.887L231.02,95.223L232.367,95.223L232.367,88.887L233.93,88.887L233.93,87.676Z" style="fill:white;fill-rule:nonzero;"></path>
<path d="M238.922,95.289C240.723,95.289 241.508,94.594 241.508,93.027L241.508,89.82C241.508,88.219 240.75,87.559 238.922,87.559C237.094,87.559 236.395,88.219 236.395,89.82L236.395,93.027C236.395,94.594 237.121,95.289 238.922,95.289ZM238.922,94.09C237.938,94.09 237.75,93.676 237.75,92.961L237.75,89.891C237.75,89.199 237.938,88.758 238.922,88.758C239.973,88.758 240.152,89.199 240.152,89.891L240.152,92.961C240.152,93.676 239.906,94.09 238.922,94.09Z" style="fill:white;fill-rule:nonzero;"></path>
<path d="M247.867,93.43L249.375,90.383L249.215,92.547L249.215,95.223L250.574,95.223L250.574,87.648L249.266,87.648L247.711,91L246.164,87.648L244.859,87.648L244.859,95.223L246.215,95.223L246.215,92.547L246.059,90.383L247.562,93.43L247.867,93.43Z" style="fill:white;fill-rule:nonzero;"></path>
<path d="M256.41,95.289C258.211,95.289 259,94.594 259,93.027L259,89.82C259,88.219 258.242,87.559 256.41,87.559C254.582,87.559 253.883,88.219 253.883,89.82L253.883,93.027C253.883,94.594 254.609,95.289 256.41,95.289ZM256.41,94.09C255.426,94.09 255.238,93.676 255.238,92.961L255.238,89.891C255.238,89.199 255.426,88.758 256.41,88.758C257.465,88.758 257.64,89.199 257.64,89.891L257.64,92.961C257.64,93.676 257.394,94.09 256.41,94.09Z" style="fill:white;fill-rule:nonzero;"></path>
<path d="M267.297,95.223L265.633,92.34C266.547,92.043 266.941,91.434 266.941,90.312L266.941,89.918C266.941,88.316 266.086,87.648 264.254,87.648L262.348,87.648L262.348,95.223L263.703,95.223L263.703,92.555L264.207,92.555L265.711,95.223L267.297,95.223ZM263.695,91.305L263.695,88.895L264.273,88.895C265.328,88.895 265.516,89.27 265.516,89.988L265.516,90.242C265.516,90.961 265.238,91.305 264.254,91.305L263.695,91.305Z" style="fill:white;fill-rule:nonzero;"></path>
<path d="M275.188,95.223L273.527,92.34C274.441,92.043 274.836,91.434 274.836,90.312L274.836,89.918C274.836,88.316 273.977,87.648 272.148,87.648L270.238,87.648L270.238,95.223L271.598,95.223L271.598,92.555L272.098,92.555L273.605,95.223L275.188,95.223ZM271.586,91.305L271.586,88.895L272.168,88.895C273.223,88.895 273.406,89.27 273.406,89.988L273.406,90.242C273.406,90.961 273.133,91.305 272.148,91.305L271.586,91.305Z" style="fill:white;fill-rule:nonzero;"></path>
<path d="M280.555,95.289C282.355,95.289 283.141,94.594 283.141,93.027L283.141,89.82C283.141,88.219 282.383,87.559 280.555,87.559C278.723,87.559 278.023,88.219 278.023,89.82L278.023,93.027C278.023,94.594 278.754,95.289 280.555,95.289ZM280.555,94.09C279.57,94.09 279.383,93.676 279.383,92.961L279.383,89.891C279.383,89.199 279.57,88.758 280.555,88.758C281.605,88.758 281.785,89.199 281.785,89.891L281.785,92.961C281.785,93.676 281.539,94.09 280.555,94.09Z" style="fill:white;fill-rule:nonzero;"></path>
<path d="M290.141,91.141L291.25,95.23L292.688,95.23L294.586,87.648L293.188,87.648L292.441,90.262L292,93.352L290.66,87.895L289.617,87.895L288.379,93.352L287.836,90.262L287.09,87.648L285.691,87.648L287.699,95.23L289.125,95.23L290.141,91.141Z" style="fill:white;fill-rule:nonzero;"></path>
<path d="M90.383,76.133C90.336,76.137 90.293,76.141 90.25,76.141L80.816,76.141C80.773,76.141 80.73,76.137 80.684,76.133C80.641,76.129 80.598,76.121 80.555,76.113C80.508,76.105 80.465,76.094 80.426,76.082C80.383,76.066 80.34,76.055 80.297,76.035C80.258,76.02 80.219,76 80.18,75.98C80.141,75.957 80.102,75.934 80.066,75.91C80.027,75.887 79.992,75.859 79.957,75.832C79.922,75.805 79.891,75.773 79.859,75.742C79.828,75.711 79.797,75.68 79.77,75.645C79.742,75.609 79.715,75.574 79.691,75.535C79.668,75.5 79.645,75.461 79.621,75.422C79.602,75.383 79.582,75.344 79.566,75.301C79.547,75.262 79.535,75.219 79.52,75.176C79.508,75.137 79.496,75.094 79.488,75.047C79.48,75.004 79.473,74.961 79.469,74.918C79.465,74.871 79.461,74.828 79.461,74.785L79.461,17.875C79.461,17.828 79.465,17.785 79.469,17.742C79.473,17.695 79.48,17.652 79.488,17.609C79.496,17.566 79.508,17.523 79.52,17.48C79.535,17.438 79.547,17.395 79.566,17.355C79.582,17.312 79.602,17.273 79.621,17.234C79.645,17.195 79.668,17.156 79.691,17.121C79.715,17.082 79.742,17.047 79.77,17.012C79.797,16.98 79.828,16.945 79.859,16.914C79.891,16.883 79.922,16.855 79.957,16.824C79.992,16.797 80.027,16.77 80.066,16.746C80.102,16.723 80.141,16.699 80.18,16.68C80.219,16.656 80.258,16.637 80.297,16.621C80.34,16.605 80.383,16.59 80.426,16.578C80.465,16.562 80.508,16.555 80.555,16.543C80.598,16.535 80.641,16.527 80.684,16.523C80.73,16.52 80.773,16.52 80.816,16.52L90.25,16.52C90.293,16.52 90.336,16.52 90.383,16.523C90.426,16.527 90.469,16.535 90.512,16.543C90.555,16.555 90.598,16.562 90.641,16.578C90.684,16.59 90.727,16.605 90.766,16.621C90.809,16.637 90.848,16.656 90.887,16.68C90.926,16.699 90.965,16.723 91,16.746C91.039,16.77 91.074,16.797 91.109,16.824C91.141,16.855 91.176,16.883 91.207,16.914C91.238,16.945 91.27,16.98 91.297,17.012C91.324,17.047 91.352,17.082 91.375,17.121C91.398,17.156 91.422,17.195 91.441,17.234C91.465,17.273 91.484,17.312 91.5,17.355C91.516,17.395 91.531,17.438 91.547,17.48C91.559,17.523 91.57,17.566 91.578,17.609C91.586,17.652 91.594,17.695 91.598,17.742C91.602,17.785 91.602,17.828 91.602,17.875L91.602,44.699L129.363,16.785C129.477,16.695 129.604,16.633 129.742,16.586C129.882,16.539 130.022,16.52 130.168,16.52L143.746,16.52C143.852,16.52 143.953,16.531 144.059,16.555C144.16,16.578 144.258,16.613 144.355,16.664C144.449,16.711 144.535,16.77 144.617,16.836C144.699,16.906 144.77,16.98 144.832,17.066C144.859,17.102 144.883,17.137 144.906,17.176C144.93,17.215 144.949,17.254 144.969,17.293C144.988,17.332 145.004,17.375 145.02,17.418C145.035,17.457 145.047,17.5 145.059,17.543C145.07,17.586 145.078,17.629 145.086,17.676C145.09,17.719 145.098,17.762 145.098,17.805C145.102,17.852 145.102,17.895 145.098,17.941C145.098,17.984 145.09,18.027 145.086,18.07C145.078,18.117 145.07,18.16 145.059,18.203C145.047,18.246 145.035,18.289 145.02,18.328C145.004,18.371 144.988,18.414 144.969,18.453C144.949,18.492 144.93,18.531 144.906,18.57C144.883,18.609 144.859,18.645 144.832,18.68C144.805,18.715 144.777,18.75 144.75,18.781C144.719,18.816 144.688,18.848 144.656,18.879C144.621,18.906 144.586,18.934 144.551,18.961L91.602,58.23L91.602,74.785C91.602,74.828 91.602,74.871 91.598,74.918C91.594,74.961 91.586,75.004 91.578,75.047C91.57,75.094 91.559,75.137 91.547,75.176C91.531,75.219 91.516,75.262 91.5,75.301C91.484,75.344 91.465,75.383 91.441,75.422C91.422,75.461 91.398,75.5 91.375,75.535C91.352,75.574 91.324,75.609 91.297,75.645C91.27,75.68 91.238,75.711 91.207,75.742C91.176,75.773 91.141,75.805 91.109,75.832C91.074,75.859 91.039,75.887 91,75.91C90.965,75.934 90.926,75.957 90.887,75.98C90.848,76 90.809,76.02 90.766,76.035C90.727,76.055 90.684,76.066 90.641,76.082C90.598,76.094 90.555,76.105 90.512,76.113C90.469,76.121 90.426,76.129 90.383,76.133ZM88.93,57.234C88.906,57.34 88.895,57.441 88.895,57.547L88.895,73.43L82.172,73.43L82.172,19.227L88.895,19.227L88.895,47.387C88.895,47.531 88.914,47.672 88.961,47.809C89.008,47.949 89.074,48.074 89.16,48.191C89.211,48.262 89.27,48.328 89.336,48.387C89.402,48.449 89.473,48.5 89.551,48.547C89.625,48.594 89.707,48.629 89.789,48.66C89.875,48.691 89.961,48.711 90.047,48.727C90.137,48.738 90.223,48.742 90.312,48.738C90.402,48.734 90.488,48.723 90.574,48.699C90.66,48.68 90.746,48.648 90.824,48.613C90.906,48.574 90.98,48.527 91.055,48.477L130.613,19.227L139.645,19.227L89.441,56.461C89.355,56.523 89.281,56.594 89.211,56.676C89.145,56.758 89.086,56.844 89.039,56.938C88.992,57.035 88.953,57.133 88.93,57.234Z" style="fill:white;"></path>
<path d="M148.246,74.535C148.262,74.617 148.27,74.699 148.27,74.785C148.27,74.828 148.27,74.871 148.262,74.918C148.258,74.961 148.254,75.004 148.246,75.047C148.234,75.094 148.227,75.137 148.211,75.176C148.199,75.219 148.184,75.262 148.168,75.301C148.148,75.344 148.133,75.383 148.109,75.422C148.09,75.461 148.066,75.5 148.043,75.535C148.016,75.574 147.992,75.609 147.961,75.645C147.934,75.68 147.906,75.711 147.875,75.742C147.844,75.773 147.809,75.805 147.773,75.832C147.742,75.859 147.707,75.887 147.668,75.91C147.633,75.934 147.594,75.957 147.555,75.98C147.516,76 147.477,76.02 147.434,76.035C147.395,76.055 147.352,76.066 147.309,76.082C147.266,76.094 147.223,76.105 147.18,76.113C147.137,76.121 147.094,76.129 147.047,76.133C147.004,76.137 146.961,76.141 146.914,76.141L134.719,76.141C134.625,76.141 134.531,76.129 134.441,76.109C134.348,76.09 134.258,76.062 134.172,76.023C134.086,75.984 134.004,75.938 133.926,75.883C133.852,75.828 133.781,75.766 133.719,75.695L110.465,50.086C110.438,50.055 110.41,50.02 110.383,49.988C110.355,49.953 110.332,49.914 110.309,49.879C110.285,49.84 110.266,49.801 110.246,49.762C110.227,49.719 110.211,49.68 110.195,49.637C110.18,49.598 110.168,49.555 110.156,49.512C110.145,49.469 110.137,49.426 110.129,49.379C110.121,49.336 110.117,49.293 110.113,49.246L110.113,49.113C110.117,49.07 110.121,49.027 110.125,48.984C110.133,48.938 110.141,48.895 110.152,48.852C110.164,48.809 110.176,48.766 110.191,48.723C110.203,48.684 110.223,48.641 110.238,48.602C110.258,48.562 110.281,48.523 110.301,48.484C110.324,48.445 110.348,48.41 110.375,48.371C110.402,48.336 110.43,48.301 110.461,48.27C110.488,48.238 110.52,48.203 110.551,48.176C110.586,48.145 110.621,48.117 110.656,48.09L117.809,42.723C117.844,42.699 117.879,42.676 117.914,42.652C117.949,42.633 117.984,42.613 118.023,42.594C118.059,42.574 118.098,42.559 118.137,42.543C118.176,42.527 118.215,42.516 118.254,42.504C118.293,42.492 118.336,42.484 118.375,42.477C118.418,42.469 118.457,42.461 118.5,42.457C118.543,42.457 118.582,42.453 118.625,42.453C118.668,42.453 118.707,42.457 118.75,42.461C118.789,42.465 118.832,42.469 118.871,42.477C118.914,42.484 118.953,42.492 118.992,42.504C119.035,42.516 119.074,42.531 119.113,42.547C119.152,42.559 119.188,42.578 119.227,42.594C119.266,42.613 119.301,42.633 119.336,42.656C119.371,42.68 119.406,42.703 119.438,42.727C119.473,42.75 119.504,42.777 119.535,42.805C119.566,42.836 119.594,42.863 119.621,42.895L147.918,73.871C147.973,73.934 148.023,74 148.066,74.07C148.109,74.141 148.148,74.215 148.18,74.293C148.211,74.371 148.23,74.453 148.246,74.535ZM135.32,73.43L113.473,49.363L118.453,45.629L143.844,73.43L135.32,73.43Z" style="fill:white;"></path>
<path d="M171.984,16.52C171.938,16.52 171.895,16.52 171.852,16.523C171.805,16.527 171.762,16.535 171.719,16.543C171.676,16.555 171.633,16.562 171.59,16.578C171.547,16.59 171.504,16.605 171.465,16.621C171.426,16.637 171.383,16.656 171.344,16.68C171.305,16.699 171.27,16.723 171.23,16.746C171.195,16.77 171.156,16.797 171.125,16.824C171.09,16.855 171.055,16.883 171.023,16.914C170.992,16.945 170.965,16.98 170.938,17.012C170.906,17.047 170.883,17.082 170.855,17.121C170.832,17.156 170.809,17.195 170.789,17.234C170.766,17.273 170.75,17.312 170.73,17.355C170.715,17.395 170.699,17.438 170.688,17.48C170.676,17.523 170.664,17.566 170.652,17.609C170.645,17.652 170.641,17.695 170.637,17.742C170.629,17.785 170.629,17.828 170.629,17.875L170.629,74.785C170.629,74.828 170.629,74.871 170.637,74.918C170.641,74.961 170.645,75.004 170.652,75.047C170.664,75.094 170.676,75.137 170.688,75.176C170.699,75.219 170.715,75.262 170.73,75.301C170.75,75.344 170.766,75.383 170.789,75.422C170.809,75.461 170.832,75.5 170.855,75.535C170.883,75.574 170.906,75.609 170.938,75.645C170.965,75.68 170.992,75.711 171.023,75.742C171.055,75.773 171.09,75.805 171.125,75.832C171.156,75.859 171.195,75.887 171.23,75.91C171.27,75.934 171.305,75.957 171.344,75.98C171.383,76 171.426,76.02 171.465,76.035C171.504,76.055 171.547,76.066 171.59,76.082C171.633,76.094 171.676,76.105 171.719,76.113C171.762,76.121 171.805,76.129 171.852,76.133C171.895,76.137 171.938,76.141 171.984,76.141L212.391,76.141C212.434,76.141 212.48,76.137 212.523,76.133C212.566,76.129 212.609,76.121 212.656,76.113C212.699,76.105 212.742,76.094 212.785,76.082C212.828,76.066 212.867,76.055 212.91,76.035C212.949,76.02 212.988,76 213.027,75.98C213.066,75.957 213.105,75.934 213.145,75.91C213.18,75.887 213.215,75.859 213.25,75.832C213.285,75.805 213.316,75.773 213.348,75.742C213.379,75.711 213.41,75.68 213.438,75.645C213.465,75.609 213.492,75.574 213.516,75.535C213.543,75.5 213.566,75.461 213.586,75.422C213.605,75.383 213.625,75.344 213.641,75.301C213.66,75.262 213.672,75.219 213.688,75.176C213.699,75.137 213.711,75.094 213.719,75.047C213.727,75.004 213.734,74.961 213.738,74.918C213.742,74.871 213.746,74.828 213.746,74.785L213.746,66.328C213.746,66.285 213.742,66.238 213.738,66.195C213.734,66.152 213.727,66.109 213.719,66.062C213.711,66.02 213.699,65.977 213.688,65.934C213.672,65.895 213.66,65.852 213.641,65.809C213.625,65.77 213.605,65.73 213.586,65.691C213.566,65.652 213.543,65.613 213.516,65.574C213.492,65.539 213.465,65.504 213.438,65.469C213.41,65.434 213.379,65.402 213.348,65.372C213.316,65.34 213.285,65.309 213.25,65.281C213.215,65.254 213.18,65.227 213.145,65.204C213.105,65.177 213.066,65.156 213.027,65.134C212.988,65.113 212.949,65.094 212.91,65.079C212.867,65.059 212.828,65.047 212.785,65.031C212.742,65.02 212.699,65.009 212.656,65C212.609,64.992 212.566,64.984 212.523,64.981C212.48,64.977 212.434,64.973 212.391,64.973L182.77,64.973L182.77,17.875C182.77,17.828 182.766,17.785 182.762,17.742C182.758,17.695 182.754,17.652 182.742,17.609C182.734,17.566 182.723,17.523 182.711,17.48C182.699,17.438 182.684,17.395 182.668,17.355C182.648,17.312 182.629,17.273 182.609,17.234C182.59,17.195 182.566,17.156 182.543,17.121C182.516,17.082 182.488,17.047 182.461,17.012C182.434,16.98 182.402,16.945 182.371,16.914C182.34,16.883 182.309,16.855 182.273,16.824C182.238,16.797 182.203,16.77 182.168,16.746C182.129,16.723 182.094,16.699 182.055,16.68C182.016,16.656 181.973,16.637 181.934,16.621C181.891,16.605 181.852,16.59 181.809,16.578C181.766,16.562 181.723,16.555 181.68,16.543C181.637,16.535 181.59,16.527 181.547,16.523C181.504,16.52 181.457,16.52 181.414,16.52L171.984,16.52ZM173.34,19.227L173.34,73.43L211.035,73.43L211.035,67.684L181.414,67.684C181.371,67.684 181.324,67.68 181.281,67.676C181.238,67.672 181.195,67.668 181.148,67.656C181.105,67.648 181.062,67.637 181.02,67.625C180.977,67.613 180.938,67.598 180.895,67.582C180.855,67.562 180.816,67.543 180.777,67.523C180.738,67.504 180.699,67.48 180.66,67.457C180.625,67.43 180.59,67.406 180.555,67.375C180.52,67.348 180.488,67.316 180.457,67.285C180.426,67.254 180.395,67.223 180.367,67.188C180.34,67.152 180.312,67.117 180.289,67.082C180.262,67.043 180.238,67.008 180.219,66.969C180.199,66.93 180.18,66.887 180.164,66.848C180.145,66.805 180.129,66.766 180.117,66.723C180.105,66.68 180.094,66.637 180.086,66.594C180.078,66.551 180.07,66.504 180.066,66.461C180.062,66.418 180.059,66.375 180.059,66.328L180.059,19.227L173.34,19.227Z" style="fill:white;"></path>
<path d="M294.578,66.195C294.582,66.238 294.586,66.285 294.586,66.328L294.586,74.785C294.586,74.828 294.582,74.871 294.578,74.918C294.574,74.961 294.57,75.004 294.559,75.047C294.551,75.094 294.539,75.137 294.527,75.176C294.516,75.219 294.5,75.262 294.484,75.301C294.465,75.344 294.445,75.383 294.426,75.422C294.406,75.461 294.383,75.5 294.359,75.535C294.332,75.574 294.305,75.609 294.277,75.645C294.25,75.68 294.219,75.711 294.188,75.742C294.156,75.773 294.125,75.805 294.09,75.832C294.055,75.859 294.02,75.887 293.984,75.91C293.945,75.934 293.91,75.957 293.871,75.98C293.832,76 293.789,76.02 293.75,76.035C293.707,76.055 293.668,76.066 293.625,76.082C293.582,76.094 293.539,76.105 293.496,76.113C293.453,76.121 293.406,76.129 293.363,76.133C293.32,76.137 293.273,76.141 293.23,76.141L237.457,76.141C237.414,76.141 237.371,76.137 237.324,76.133C237.281,76.129 237.238,76.121 237.195,76.113C237.148,76.105 237.105,76.094 237.062,76.082C237.023,76.066 236.98,76.055 236.941,76.035C236.898,76.02 236.859,76 236.82,75.98C236.781,75.957 236.742,75.934 236.707,75.91C236.668,75.887 236.633,75.859 236.598,75.832C236.562,75.805 236.531,75.773 236.5,75.742C236.469,75.711 236.438,75.68 236.41,75.645C236.383,75.609 236.355,75.574 236.332,75.535C236.305,75.5 236.285,75.461 236.262,75.422C236.242,75.383 236.223,75.344 236.207,75.301C236.188,75.262 236.176,75.219 236.16,75.176C236.148,75.137 236.137,75.094 236.129,75.047C236.121,75.004 236.113,74.961 236.109,74.918C236.105,74.871 236.102,74.828 236.102,74.785L236.102,66.328C236.102,66.23 236.113,66.137 236.133,66.043C236.156,65.945 236.184,65.855 236.227,65.766C236.266,65.68 236.312,65.594 236.371,65.52C236.43,65.441 236.496,65.372 236.57,65.305L292.344,16.852C292.402,16.797 292.469,16.75 292.539,16.707C292.609,16.668 292.68,16.633 292.758,16.605C292.832,16.574 292.91,16.555 292.988,16.539C293.07,16.523 293.148,16.52 293.23,16.52C293.273,16.52 293.32,16.52 293.363,16.523C293.406,16.527 293.453,16.535 293.496,16.543C293.539,16.555 293.582,16.562 293.625,16.578C293.668,16.59 293.707,16.605 293.75,16.621C293.789,16.637 293.832,16.656 293.871,16.68C293.91,16.699 293.945,16.723 293.984,16.746C294.02,16.77 294.055,16.797 294.09,16.824C294.125,16.855 294.156,16.883 294.188,16.914C294.219,16.945 294.25,16.98 294.277,17.012C294.305,17.047 294.332,17.082 294.359,17.121C294.383,17.156 294.406,17.195 294.426,17.234C294.445,17.273 294.465,17.312 294.484,17.355C294.5,17.395 294.516,17.438 294.527,17.48C294.539,17.523 294.551,17.566 294.559,17.609C294.57,17.652 294.574,17.695 294.578,17.742C294.582,17.785 294.586,17.828 294.586,17.875L294.586,28.766C294.586,28.863 294.574,28.961 294.555,29.055C294.535,29.148 294.504,29.238 294.465,29.328C294.426,29.418 294.375,29.5 294.316,29.578C294.262,29.652 294.195,29.727 294.121,29.789L253.906,64.899L293.234,64.973C293.277,64.973 293.324,64.977 293.367,64.981C293.41,64.984 293.453,64.992 293.496,65C293.543,65.009 293.582,65.02 293.625,65.031C293.668,65.047 293.711,65.062 293.75,65.079C293.793,65.094 293.832,65.113 293.871,65.134C293.91,65.156 293.949,65.18 293.984,65.204C294.023,65.227 294.059,65.254 294.09,65.281C294.125,65.309 294.16,65.34 294.191,65.372C294.223,65.402 294.25,65.439 294.277,65.469C294.309,65.504 294.332,65.539 294.359,65.578C294.383,65.613 294.406,65.652 294.426,65.691C294.445,65.73 294.465,65.77 294.484,65.812C294.5,65.852 294.516,65.895 294.527,65.938C294.539,65.977 294.551,66.02 294.559,66.066C294.57,66.109 294.574,66.152 294.578,66.195ZM250.301,67.602L291.875,67.68L291.875,73.43L238.812,73.43L238.812,66.945L291.875,20.844L291.875,28.152L249.414,65.227C249.34,65.289 249.273,65.359 249.219,65.439C249.16,65.516 249.109,65.598 249.07,65.688C249.031,65.773 249,65.863 248.98,65.957C248.957,66.055 248.949,66.148 248.949,66.246C248.949,66.289 248.949,66.332 248.953,66.379C248.961,66.422 248.965,66.465 248.973,66.508C248.984,66.555 248.992,66.598 249.008,66.637C249.02,66.68 249.035,66.723 249.051,66.762C249.066,66.805 249.086,66.844 249.105,66.883C249.129,66.922 249.152,66.961 249.176,67C249.199,67.035 249.227,67.07 249.254,67.105C249.281,67.141 249.312,67.172 249.344,67.203C249.375,67.234 249.406,67.266 249.441,67.293C249.477,67.32 249.512,67.348 249.551,67.371C249.586,67.398 249.625,67.422 249.664,67.441C249.703,67.461 249.742,67.48 249.781,67.5C249.824,67.516 249.867,67.531 249.906,67.543C249.949,67.555 249.992,67.566 250.035,67.574C250.082,67.586 250.125,67.59 250.168,67.594C250.211,67.602 250.258,67.602 250.301,67.602Z" style="fill:white;"></path>
<path d="M238.059,16.523C238.102,16.52 238.145,16.52 238.191,16.52L281.281,16.52C281.383,16.52 281.484,16.531 281.582,16.551C281.684,16.574 281.781,16.609 281.871,16.656C281.965,16.699 282.051,16.754 282.133,16.82C282.211,16.883 282.281,16.957 282.348,17.039C282.375,17.074 282.398,17.109 282.422,17.145C282.445,17.184 282.469,17.223 282.488,17.262C282.508,17.301 282.527,17.34 282.543,17.383C282.559,17.426 282.574,17.465 282.586,17.508C282.598,17.551 282.605,17.594 282.613,17.641C282.621,17.684 282.629,17.727 282.629,17.77C282.633,17.816 282.637,17.859 282.633,17.902C282.633,17.949 282.629,17.992 282.625,18.035C282.621,18.082 282.613,18.125 282.602,18.168C282.594,18.211 282.582,18.254 282.566,18.297C282.555,18.336 282.539,18.379 282.52,18.418C282.5,18.461 282.48,18.5 282.461,18.539C282.438,18.578 282.414,18.613 282.387,18.652C282.363,18.688 282.336,18.723 282.309,18.758C282.277,18.789 282.246,18.82 282.215,18.852C282.184,18.883 282.148,18.914 282.117,18.941L271.223,27.477C271.102,27.57 270.969,27.641 270.828,27.691C270.684,27.738 270.535,27.766 270.387,27.766L238.191,27.766C238.145,27.766 238.102,27.762 238.059,27.758C238.012,27.754 237.969,27.746 237.926,27.738C237.883,27.73 237.84,27.719 237.797,27.707C237.754,27.695 237.711,27.68 237.672,27.66C237.629,27.645 237.59,27.625 237.551,27.605C237.512,27.582 237.473,27.562 237.438,27.535C237.398,27.512 237.363,27.484 237.328,27.457C237.297,27.43 237.262,27.398 237.23,27.367C237.199,27.336 237.172,27.305 237.141,27.27C237.113,27.234 237.09,27.199 237.062,27.164C237.039,27.125 237.016,27.086 236.996,27.047C236.973,27.008 236.953,26.969 236.938,26.93C236.922,26.887 236.906,26.844 236.895,26.805C236.879,26.762 236.871,26.719 236.859,26.676C236.852,26.629 236.844,26.586 236.84,26.543C236.836,26.5 236.836,26.453 236.836,26.41L236.836,17.875C236.836,17.828 236.836,17.785 236.84,17.742C236.844,17.695 236.852,17.652 236.859,17.609C236.871,17.566 236.879,17.523 236.895,17.48C236.906,17.438 236.922,17.395 236.938,17.355C236.953,17.312 236.973,17.273 236.996,17.234C237.016,17.195 237.039,17.156 237.062,17.121C237.09,17.082 237.113,17.047 237.141,17.012C237.172,16.98 237.199,16.945 237.23,16.914C237.262,16.883 237.297,16.855 237.328,16.824C237.363,16.797 237.398,16.77 237.438,16.746C237.473,16.723 237.512,16.699 237.551,16.68C237.59,16.656 237.629,16.637 237.672,16.621C237.711,16.605 237.754,16.59 237.797,16.578C237.84,16.562 237.883,16.555 237.926,16.543C237.969,16.535 238.012,16.527 238.059,16.523ZM277.352,19.227L269.918,25.055L239.543,25.055L239.543,19.227L277.352,19.227Z" style="fill:white;"></path>
<path d="M24.406,28.266L16.988,0.547C16.988,0.328 16.77,0.109 16.441,0.109L15.023,0C14.586,0 14.258,0.328 14.367,0.762L19.059,27.5C19.059,27.719 19.277,27.828 19.496,27.938L21.57,28.59C21.789,28.59 21.898,28.699 22.008,28.918C22.66,28.484 23.426,28.266 24.188,28.266L24.406,28.266Z" style="fill:white;"></path>
<path d="M26.688,32.547C26.695,32.465 26.699,32.383 26.699,32.301C26.699,32.219 26.695,32.137 26.688,32.055C26.68,31.973 26.668,31.895 26.652,31.812C26.633,31.73 26.613,31.652 26.59,31.574C26.566,31.496 26.539,31.418 26.508,31.34C26.477,31.266 26.441,31.191 26.402,31.117C26.363,31.047 26.32,30.977 26.277,30.906C26.23,30.84 26.18,30.773 26.129,30.711C26.078,30.648 26.023,30.586 25.965,30.527C25.906,30.469 25.844,30.414 25.781,30.363C25.719,30.309 25.652,30.262 25.582,30.215C25.516,30.168 25.445,30.125 25.371,30.09C25.301,30.051 25.227,30.016 25.148,29.984C25.074,29.953 24.996,29.926 24.918,29.898C24.84,29.875 24.758,29.855 24.68,29.84C24.598,29.824 24.516,29.812 24.434,29.805C24.352,29.797 24.27,29.793 24.188,29.793C24.105,29.793 24.023,29.797 23.945,29.805C23.859,29.812 23.781,29.824 23.699,29.84C23.617,29.855 23.539,29.875 23.461,29.898C23.383,29.926 23.305,29.953 23.23,29.984C23.152,30.016 23.078,30.051 23.008,30.09C22.934,30.125 22.863,30.168 22.793,30.215C22.727,30.262 22.66,30.309 22.598,30.363C22.535,30.414 22.473,30.469 22.414,30.527C22.355,30.586 22.301,30.648 22.25,30.711C22.195,30.773 22.148,30.84 22.102,30.906C22.055,30.977 22.016,31.047 21.977,31.117C21.938,31.191 21.902,31.266 21.871,31.34C21.84,31.418 21.812,31.496 21.789,31.574C21.762,31.652 21.742,31.73 21.727,31.812C21.711,31.895 21.699,31.973 21.691,32.055C21.684,32.137 21.68,32.219 21.68,32.301C21.68,32.383 21.684,32.465 21.691,32.547C21.699,32.629 21.711,32.711 21.727,32.793C21.742,32.871 21.762,32.953 21.789,33.031C21.812,33.109 21.84,33.188 21.871,33.262C21.902,33.34 21.938,33.414 21.977,33.484C22.016,33.559 22.055,33.629 22.102,33.695C22.148,33.766 22.195,33.832 22.25,33.895C22.301,33.957 22.355,34.02 22.414,34.078C22.473,34.137 22.535,34.191 22.598,34.242C22.66,34.293 22.727,34.344 22.793,34.391C22.863,34.434 22.934,34.477 23.008,34.516C23.078,34.555 23.152,34.59 23.23,34.621C23.305,34.652 23.383,34.68 23.461,34.703C23.539,34.727 23.617,34.746 23.699,34.766C23.781,34.781 23.859,34.793 23.945,34.801C24.023,34.809 24.105,34.812 24.188,34.812C24.27,34.812 24.352,34.809 24.434,34.801C24.516,34.793 24.598,34.781 24.68,34.766C24.758,34.746 24.84,34.727 24.918,34.703C24.996,34.68 25.074,34.652 25.148,34.621C25.227,34.59 25.301,34.555 25.371,34.516C25.445,34.477 25.516,34.434 25.582,34.391C25.652,34.344 25.719,34.293 25.781,34.242C25.844,34.191 25.906,34.137 25.965,34.078C26.023,34.02 26.078,33.957 26.129,33.895C26.18,33.832 26.23,33.766 26.277,33.695C26.32,33.629 26.363,33.559 26.402,33.484C26.441,33.414 26.477,33.34 26.508,33.262C26.539,33.188 26.566,33.109 26.59,33.031C26.613,32.953 26.633,32.871 26.652,32.793C26.668,32.711 26.68,32.629 26.688,32.547Z" style="fill:white;"></path>
<path d="M55.945,41.688L56.711,40.488C56.926,40.16 56.816,39.723 56.383,39.504L30.957,30.23L30.738,30.23C30.52,30.23 30.41,30.336 30.301,30.445L28.664,31.977C28.555,32.082 28.336,32.191 28.227,32.191L28.117,32.191L28.117,32.41C28.117,33.176 27.898,33.938 27.465,34.594L55.289,42.016L55.398,42.016C55.617,42.016 55.836,41.906 55.945,41.688Z" style="fill:white;"></path>
<path d="M1.707,56.527L21.68,39.941L22.551,39.176C22.66,39.066 22.77,38.742 22.66,38.523L22.117,36.34C22.117,36.121 22.117,35.902 22.223,35.793C22.008,35.684 21.898,35.574 21.68,35.465C21.133,35.141 20.805,34.594 20.477,34.047L0.18,54.348C-0.038,54.562 -0.038,54.891 0.07,55.109L0.727,56.309C0.835,56.527 1.055,56.637 1.273,56.637C1.492,56.637 1.598,56.637 1.707,56.527Z" style="fill:white;"></path>
<path d="M25.824,35.902L28.008,98.215L20.371,98.215L22.332,41.25L23.535,40.27C24.188,39.723 24.406,38.957 24.188,38.195L23.754,36.449L23.973,36.23L24.188,36.23C24.844,36.23 25.391,36.121 25.824,35.902Z" style="fill:white;"></path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 537 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Some files were not shown because too many files have changed in this diff Show More