Compare commits
90 Commits
feature/st
...
v1.0.0-rc.
| Author | SHA1 | Date | |
|---|---|---|---|
| e3f7344daf | |||
| 21a7b0ade2 | |||
| d027fbeac2 | |||
| 8a751998eb | |||
| 48c3e1d013 | |||
| 3df4b44b8d | |||
| 07e0f237b9 | |||
| 57a3944301 | |||
| 5fe0a8d83e | |||
| 8062d33f35 | |||
| ebe67afd73 | |||
| b74f6b6f9e | |||
| 24eea9a2fe | |||
| c70288bba7 | |||
| d438dbdc9d | |||
| e0c4aaf298 | |||
| f44487eeac | |||
| a82b95a28f | |||
| ab688a3dab | |||
| a0ce37708e | |||
| 0379d1f05d | |||
| 50347d049d | |||
| 9678181927 | |||
| 3ffaafefe5 | |||
| e5bf8c861c | |||
| 651e14d665 | |||
| 580cd6789c | |||
| db4cf354ff | |||
| e8957e0672 | |||
| 7ef0bca9f6 | |||
| 198944649a | |||
| 6aa741ab0a | |||
| f69952a5da | |||
| 81af9bf3dd | |||
| f1b617e967 | |||
| d6be9beebf | |||
| 0a797260e3 | |||
| 2a4cc76292 | |||
| f87eb27f41 | |||
| acd86099e5 | |||
| 5ab9791c72 | |||
| 8152ccd5df | |||
| 8eeb571c2d | |||
| b1854d5255 | |||
| 7f4f970a38 | |||
| e5908c757c | |||
| 70efb0c593 | |||
| 479a36f1d0 | |||
| 372a0c5cfa | |||
| 42b06e1ef8 | |||
| b25fdd877a | |||
| dd23310ac4 | |||
| f6f28a4529 | |||
| fc3635db86 | |||
| fc000353a9 | |||
| 73c32c6d31 | |||
| 381e0b121f | |||
| 829c074c7f | |||
| 9189e813b2 | |||
| 249313cc37 | |||
| 8232971419 | |||
| f41260e1db | |||
| 950ef9d463 | |||
| fcb3169d04 | |||
| 9e87720494 | |||
| 6a403f47a0 | |||
| 2d8df53e36 | |||
| 6f49dbc56c | |||
| ad2a477636 | |||
| 77a1067820 | |||
| ea3076b4ec | |||
| 17fe0d7107 | |||
| 38b512973b | |||
| 4f73838c21 | |||
| 2f8ce42409 | |||
| cf7af73b72 | |||
| d526bfe56f | |||
| 4cb7d438a0 | |||
| 03e597442b | |||
| 5f9ee7d976 | |||
| c1304403a1 | |||
| 5036c5fe28 | |||
| 50a524c515 | |||
| 57886a01d6 | |||
| c89bd8e80f | |||
| 9c54322654 | |||
| 8a80eb7b9a | |||
| c1773a7072 | |||
| 33ed13d255 | |||
| 0f5811edb9 |
@@ -1,5 +1,6 @@
|
||||
node_modules
|
||||
.next
|
||||
!.next/cache
|
||||
.git
|
||||
.DS_Store
|
||||
.env
|
||||
|
||||
7
.env
@@ -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
|
||||
|
||||
34
.env.example
@@ -10,6 +10,11 @@
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
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)
|
||||
@@ -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:
|
||||
# ─────────
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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) }}
|
||||
NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${{ 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 NEXT_PUBLIC_UMAMI_SCRIPT_URL="$NEXT_PUBLIC_UMAMI_SCRIPT_URL" \
|
||||
--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) }}
|
||||
NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${{ 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 }}
|
||||
MAIL_PORT: ${{ secrets.MAIL_PORT || vars.MAIL_PORT }}
|
||||
MAIL_USERNAME: ${{ secrets.MAIL_USERNAME || vars.MAIL_USERNAME }}
|
||||
MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }}
|
||||
MAIL_FROM: ${{ secrets.MAIL_FROM || vars.MAIL_FROM }}
|
||||
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 }}
|
||||
DIRECTUS_SECRET: ${{ secrets.DIRECTUS_SECRET }}
|
||||
DIRECTUS_ADMIN_EMAIL: ${{ secrets.DIRECTUS_ADMIN_EMAIL }}
|
||||
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 }}
|
||||
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
|
||||
NEXT_PUBLIC_UMAMI_SCRIPT_URL=$NEXT_PUBLIC_UMAMI_SCRIPT_URL
|
||||
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
@@ -1,2 +1,7 @@
|
||||
node_modules
|
||||
.next
|
||||
.next
|
||||
.DS_Store
|
||||
|
||||
# Directus
|
||||
directus/uploads
|
||||
!directus/extensions/
|
||||
1
.husky/commit-msg
Executable file
@@ -0,0 +1 @@
|
||||
npx --no -- commitlint --edit "$1"
|
||||
1
.husky/pre-commit
Executable file
@@ -0,0 +1 @@
|
||||
npx lint-staged
|
||||
11
.lintstagedrc.js
Normal 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
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100
|
||||
}
|
||||
10
Dockerfile
@@ -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
|
||||
@@ -27,15 +27,19 @@ ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ARG NEXT_PUBLIC_BASE_URL
|
||||
ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID
|
||||
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 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
|
||||
|
||||
21
README.md
@@ -133,7 +133,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 +144,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
|
||||
@@ -252,21 +252,30 @@ 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_BASE_URL` - Application base URL (e.g., `https://klz-cables.com`)
|
||||
- `NEXT_PUBLIC_UMAMI_WEBSITE_ID` - Analytics ID
|
||||
- `NEXT_PUBLIC_UMAMI_SCRIPT_URL` - Analytics script URL
|
||||
- `SENTRY_DSN` - Error tracking DSN
|
||||
- `MAIL_HOST`, `MAIL_PORT`, `MAIL_USERNAME`, `MAIL_PASSWORD`, `MAIL_FROM`, `MAIL_RECIPIENTS` - Email configuration
|
||||
|
||||
### Manual Deployment
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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">→</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">
|
||||
→
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,4 +124,4 @@ export default async function StandardPage({ params: { locale, slug } }: PagePro
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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">→</span>
|
||||
<span className="ml-3 transition-transform group-hover:translate-x-2">
|
||||
→
|
||||
</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>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -2,6 +2,7 @@ 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';
|
||||
@@ -10,6 +11,13 @@ import { SITE_URL } from '@/lib/schema';
|
||||
|
||||
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,29 +28,28 @@ 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 />
|
||||
</NextIntlClientProvider>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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">→</span>
|
||||
<span className="ml-4 transition-transform group-hover:translate-x-2">
|
||||
→
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
9
app/api/health/cms/route.ts
Normal 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 });
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
rules: {
|
||||
'header-max-length': [2, 'always', 500],
|
||||
'subject-case': [0],
|
||||
'subject-full-stop': [0],
|
||||
},
|
||||
};
|
||||
84
components/CMSConnectivityNotice.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
68
components/DatasheetDownload.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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 |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 309 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 283 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 206 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 153 KiB |
@@ -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 |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 309 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 537 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 212 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 153 KiB |
@@ -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 |