Merge branch 'feature/annotator-integration' into main
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 55s
Build & Deploy / 🧪 QA (push) Successful in 1m30s
Build & Deploy / 🏗️ Build (push) Successful in 3m19s
Build & Deploy / 🚀 Deploy (push) Successful in 36s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 59s
Build & Deploy / 🔔 Notify (push) Successful in 2s
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 55s
Build & Deploy / 🧪 QA (push) Successful in 1m30s
Build & Deploy / 🏗️ Build (push) Successful in 3m19s
Build & Deploy / 🚀 Deploy (push) Successful in 36s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 59s
Build & Deploy / 🔔 Notify (push) Successful in 2s
This commit is contained in:
@@ -13,6 +13,10 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
default: 'false'
|
default: 'false'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
PUPPETEER_SKIP_DOWNLOAD: "true"
|
PUPPETEER_SKIP_DOWNLOAD: "true"
|
||||||
COREPACK_NPM_REGISTRY: "https://registry.npmmirror.com"
|
COREPACK_NPM_REGISTRY: "https://registry.npmmirror.com"
|
||||||
@@ -89,12 +93,12 @@ jobs:
|
|||||||
DOMAIN="e-tib.com"
|
DOMAIN="e-tib.com"
|
||||||
PRJ="etib"
|
PRJ="etib"
|
||||||
|
|
||||||
if [[ "${{ github.ref_type }}" == "branch" && "$REF" == "main" ]]; then
|
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
|
||||||
TARGET="testing"
|
TARGET="testing"
|
||||||
IMAGE_TAG="main-${SHORT_SHA}"
|
IMAGE_TAG="main-${SHORT_SHA}"
|
||||||
ENV_FILE=".env.testing"
|
ENV_FILE=".env.testing"
|
||||||
TRAEFIK_HOST="test.${DOMAIN}"
|
TRAEFIK_HOST="test.${DOMAIN}"
|
||||||
elif [[ "${{ github.ref_type }}" == "tag" ]]; then
|
elif [[ "${{ github.ref }}" == refs/tags/* ]]; then
|
||||||
if [[ "$REF" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
if [[ "$REF" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
TARGET="production"
|
TARGET="production"
|
||||||
IMAGE_TAG="$REF"
|
IMAGE_TAG="$REF"
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
{"files":{".turbo/turbo-lint.log":{"size":11972,"mtime_nanos":1779970374061729177,"mode":420,"is_dir":false}},"order":[".turbo/turbo-lint.log"]}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"hash":"0864e4a10f76dcaf","duration":6769,"sha":"68c4a056b70ff215c9ef3252506875dfd3ba4eee","dirty_hash":"29197232f806b237725188c5bfe8d43cb2f12f8bb9e0df915eecf0896e2c04a3"}
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
{"files":{".turbo/turbo-test.log":{"size":2776,"mtime_nanos":1781469605328811961,"mode":420,"is_dir":false}},"order":[".turbo/turbo-test.log"]}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"hash":"41698960411f6bf6","duration":4168,"sha":"a3c732dd133b1b8261b19cf64b3bf684e269e839","dirty_hash":"1ef3b75af417f41468b9558671572e75e0fc055f9dbe6e805cfa6f59b1cddf70"}
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
{"files":{".turbo/turbo-typecheck.log":{"size":85,"mtime_nanos":1779970373499305257,"mode":420,"is_dir":false}},"order":[".turbo/turbo-typecheck.log"]}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"hash":"7668a73054ba889d","duration":6206,"sha":"68c4a056b70ff215c9ef3252506875dfd3ba4eee","dirty_hash":"29197232f806b237725188c5bfe8d43cb2f12f8bb9e0df915eecf0896e2c04a3"}
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
{"files":{".turbo/turbo-typecheck.log":{"size":90,"mtime_nanos":1781782738306875884,"mode":420,"is_dir":false}},"order":[".turbo/turbo-typecheck.log"]}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"hash":"7d27d7d8e3c30797","duration":14130,"sha":"d6eb1c0e7fe99bc7006a396c8eea0228c4723b31","dirty_hash":"b247ccedfcf352a6efb73993a3482b827d5b44922d26bf137f251d0d69a200b6"}
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
{"files":{".turbo/turbo-test.log":{"size":2150,"mtime_nanos":1779970370282819916,"mode":420,"is_dir":false}},"order":[".turbo/turbo-test.log"]}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"hash":"9c3fa4d9e1993aff","duration":2989,"sha":"68c4a056b70ff215c9ef3252506875dfd3ba4eee","dirty_hash":"29197232f806b237725188c5bfe8d43cb2f12f8bb9e0df915eecf0896e2c04a3"}
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
{"files":{".turbo/turbo-lint.log":{"size":12987,"mtime_nanos":1781469609469080321,"mode":420,"is_dir":false}},"order":[".turbo/turbo-lint.log"]}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"hash":"bb46018d4a7bc516","duration":8307,"sha":"a3c732dd133b1b8261b19cf64b3bf684e269e839","dirty_hash":"1ef3b75af417f41468b9558671572e75e0fc055f9dbe6e805cfa6f59b1cddf70"}
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
{"files":{".turbo/turbo-lint.log":{"size":15001,"mtime_nanos":1781782739446599688,"mode":420,"is_dir":false}},"order":[".turbo/turbo-lint.log"]}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"hash":"c3b4dbd60e2d475a","duration":15270,"sha":"d6eb1c0e7fe99bc7006a396c8eea0228c4723b31","dirty_hash":"b247ccedfcf352a6efb73993a3482b827d5b44922d26bf137f251d0d69a200b6"}
|
|
||||||
Binary file not shown.
1
.turbo/cache/32905076da5f4894-manifest.json
vendored
1
.turbo/cache/32905076da5f4894-manifest.json
vendored
@@ -1 +0,0 @@
|
|||||||
{"files":{".turbo/turbo-typecheck.log":{"size":92,"mtime_nanos":1779097674383861303,"mode":420,"is_dir":false}},"order":[".turbo/turbo-typecheck.log"]}
|
|
||||||
1
.turbo/cache/32905076da5f4894-meta.json
vendored
1
.turbo/cache/32905076da5f4894-meta.json
vendored
@@ -1 +0,0 @@
|
|||||||
{"hash":"32905076da5f4894","duration":4492,"sha":"35d9650626b56a727e9c1981308145cb7b2f28aa","dirty_hash":"58f8ebc92a0452cbb0a09c5ebe800385fcc42ff82fee098bb7d6c8828bc526f0"}
|
|
||||||
BIN
.turbo/cache/32905076da5f4894.tar.zst
vendored
BIN
.turbo/cache/32905076da5f4894.tar.zst
vendored
Binary file not shown.
1
.turbo/cache/7e2629942c881f80-manifest.json
vendored
1
.turbo/cache/7e2629942c881f80-manifest.json
vendored
@@ -1 +0,0 @@
|
|||||||
{"files":{".turbo/turbo-test.log":{"size":818,"mtime_nanos":1779097671747078359,"mode":420,"is_dir":false}},"order":[".turbo/turbo-test.log"]}
|
|
||||||
1
.turbo/cache/7e2629942c881f80-meta.json
vendored
1
.turbo/cache/7e2629942c881f80-meta.json
vendored
@@ -1 +0,0 @@
|
|||||||
{"hash":"7e2629942c881f80","duration":1855,"sha":"35d9650626b56a727e9c1981308145cb7b2f28aa","dirty_hash":"58f8ebc92a0452cbb0a09c5ebe800385fcc42ff82fee098bb7d6c8828bc526f0"}
|
|
||||||
BIN
.turbo/cache/7e2629942c881f80.tar.zst
vendored
BIN
.turbo/cache/7e2629942c881f80.tar.zst
vendored
Binary file not shown.
1
.turbo/cache/85340b254e0f10f1-manifest.json
vendored
1
.turbo/cache/85340b254e0f10f1-manifest.json
vendored
@@ -1 +0,0 @@
|
|||||||
{"files":{".turbo/turbo-lint.log":{"size":10746,"mtime_nanos":1779097675143794164,"mode":420,"is_dir":false}},"order":[".turbo/turbo-lint.log"]}
|
|
||||||
1
.turbo/cache/85340b254e0f10f1-meta.json
vendored
1
.turbo/cache/85340b254e0f10f1-meta.json
vendored
@@ -1 +0,0 @@
|
|||||||
{"hash":"85340b254e0f10f1","duration":5252,"sha":"35d9650626b56a727e9c1981308145cb7b2f28aa","dirty_hash":"58f8ebc92a0452cbb0a09c5ebe800385fcc42ff82fee098bb7d6c8828bc526f0"}
|
|
||||||
BIN
.turbo/cache/85340b254e0f10f1.tar.zst
vendored
BIN
.turbo/cache/85340b254e0f10f1.tar.zst
vendored
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
{"files":{".turbo/turbo-test.log":{"size":3396,"mtime_nanos":1781782731048784172,"mode":420,"is_dir":false}},"order":[".turbo/turbo-test.log"]}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"hash":"ea9126c55a797120","duration":6873,"sha":"d6eb1c0e7fe99bc7006a396c8eea0228c4723b31","dirty_hash":"b247ccedfcf352a6efb73993a3482b827d5b44922d26bf137f251d0d69a200b6"}
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
{"files":{".turbo/turbo-typecheck.log":{"size":85,"mtime_nanos":1781469608726900998,"mode":420,"is_dir":false}},"order":[".turbo/turbo-typecheck.log"]}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"hash":"eb4eeeb717c1af0c","duration":7565,"sha":"a3c732dd133b1b8261b19cf64b3bf684e269e839","dirty_hash":"1ef3b75af417f41468b9558671572e75e0fc055f9dbe6e805cfa6f59b1cddf70"}
|
|
||||||
Binary file not shown.
@@ -1,5 +0,0 @@
|
|||||||
|
|
||||||
> e-tib-nextjs@2.2.12 lint /Volumes/Alpha SSD/Coding/e-tib.com
|
|
||||||
> eslint .
|
|
||||||
|
|
||||||
ELIFECYCLE Command failed.
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
|
|
||||||
> e-tib-nextjs@2.2.12 test /Volumes/Alpha SSD/Coding/e-tib.com
|
|
||||||
> vitest run --passWithNoTests
|
|
||||||
|
|
||||||
|
|
||||||
[1m[46m RUN [49m[22m [36mv4.1.2 [39m[90m/Volumes/Alpha SSD/Coding/e-tib.com[39m
|
|
||||||
|
|
||||||
(node:64961) [DEP0205] DeprecationWarning: `module.register()` is deprecated. Use `module.registerHooks()` instead.
|
|
||||||
(Use `node --trace-deprecation ...` to show where the warning was created)
|
|
||||||
[32m✓[39m tests/mdx-links.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 29[2mms[22m[39m
|
|
||||||
[32m✓[39m tests/homepage-and-competence-content.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 14[2mms[22m[39m
|
|
||||||
[31m❯[39m tests/legal-pages.test.ts [2m([22m[2m2 tests[22m[2m | [22m[31m1 failed[39m[2m)[22m[32m 9[2mms[22m[39m
|
|
||||||
[32m✓[39m should contain highly professional B2B sections in German AGB[32m 5[2mms[22m[39m
|
|
||||||
[31m [31m×[31m should contain professional corresponding B2B sections in English Terms[39m[32m 2[2mms[22m[39m
|
|
||||||
[32m✓[39m tests/task-12-links.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 8[2mms[22m[39m
|
|
||||||
[32m✓[39m tests/messen-content.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 8[2mms[22m[39m
|
|
||||||
[32m✓[39m lib/mail/mailer.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 7[2mms[22m[39m
|
|
||||||
[90mstderr[2m | components/layout/LanguageSwitcher.test.tsx[2m > [22m[2mLanguageSwitcher TDD[2m > [22m[2mtranslates /en/trade-fairs to /de/messen when switching to German
|
|
||||||
[22m[39mReact does not recognize the `transitionMessage` prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase `transitionmessage` instead. If you accidentally passed it from a parent component, remove it from the DOM element.
|
|
||||||
|
|
||||||
[32m✓[39m components/layout/LanguageSwitcher.test.tsx [2m([22m[2m5 tests[22m[2m)[22m[32m 25[2mms[22m[39m
|
|
||||||
[32m✓[39m components/search/AISearchResults.test.tsx [2m([22m[2m1 test[22m[2m)[22m[32m 52[2mms[22m[39m
|
|
||||||
[90mstderr[2m | components/blocks/ReferencesSlider.test.tsx[2m > [22m[2mReferencesSlider TDD[2m > [22m[2mrenders reference tiles correctly
|
|
||||||
[22m[39mReceived `true` for a non-boolean attribute `fill`.
|
|
||||||
|
|
||||||
If you want to write it to the DOM, pass a string instead: fill="true" or fill={value.toString()}.
|
|
||||||
|
|
||||||
[32m✓[39m components/blocks/ReferencesSlider.test.tsx [2m([22m[2m2 tests[22m[2m)[22m[32m 43[2mms[22m[39m
|
|
||||||
[32m✓[39m lib/env.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 4[2mms[22m[39m
|
|
||||||
[90mstdout[2m | app/[locale]/referenzen/[slug]/page.test.tsx
|
|
||||||
[22m[39m[Config] Initializing Toggles: { feedbackEnabled: [33mfalse[39m }
|
|
||||||
|
|
||||||
[32m✓[39m app/[locale]/referenzen/[slug]/page.test.tsx [2m([22m[2m1 test[22m[2m)[22m[32m 26[2mms[22m[39m
|
|
||||||
|
|
||||||
[31m⎯⎯⎯⎯⎯⎯⎯[39m[1m[41m Failed Tests 1 [49m[22m[31m⎯⎯⎯⎯⎯⎯⎯[39m
|
|
||||||
|
|
||||||
[41m[1m FAIL [22m[49m tests/legal-pages.test.ts[2m > [22mLegal Pages Content Validation[2m > [22mshould contain professional corresponding B2B sections in English Terms
|
|
||||||
[31m[1mError[22m: ENOENT: no such file or directory, open '/Volumes/Alpha SSD/Coding/e-tib.com/content/en/terms.mdx'[39m
|
|
||||||
[36m [2m❯[22m tests/legal-pages.test.ts:[2m25:24[22m[39m
|
|
||||||
[90m 23|[39m
|
|
||||||
[90m 24|[39m it('should contain professional corresponding B2B sections in Englis…
|
|
||||||
[90m 25|[39m [35mconst[39m content [33m=[39m fs[33m.[39m[34mreadFileSync[39m(enTermsPath[33m,[39m [32m'utf8'[39m)[33m;[39m
|
|
||||||
[90m |[39m [31m^[39m
|
|
||||||
[90m 26|[39m
|
|
||||||
[90m 27|[39m [34mexpect[39m(content)[33m.[39m[34mtoContain[39m([32m'title: "Terms and Conditions"'[39m)[33m;[39m
|
|
||||||
|
|
||||||
[31m[2m⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯[22m[39m
|
|
||||||
|
|
||||||
|
|
||||||
[2m Test Files [22m [1m[31m1 failed[39m[22m[2m | [22m[1m[32m10 passed[39m[22m[90m (11)[39m
|
|
||||||
[2m Tests [22m [1m[31m1 failed[39m[22m[2m | [22m[1m[32m26 passed[39m[22m[90m (27)[39m
|
|
||||||
[2m Start at [22m 14:54:33
|
|
||||||
[2m Duration [22m 5.94s[2m (transform 5.27s, setup 0ms, import 9.43s, tests 222ms, environment 13.80s)[22m
|
|
||||||
|
|
||||||
ELIFECYCLE Test failed. See above for more details.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
|
|
||||||
> e-tib-nextjs@2.2.12 typecheck /Volumes/Alpha SSD/Coding/e-tib.com
|
|
||||||
> tsc --noEmit
|
|
||||||
|
|
||||||
ELIFECYCLE Command failed.
|
|
||||||
@@ -17,7 +17,7 @@ ENV SKIP_RUNTIME_ENV_VALIDATION=true
|
|||||||
ENV CI=true
|
ENV CI=true
|
||||||
|
|
||||||
# Copy lockfile and manifest for dependency installation caching
|
# Copy lockfile and manifest for dependency installation caching
|
||||||
COPY pnpm-lock.yaml package.json .npmrc* ./
|
COPY pnpm-lock.yaml package.json .npmrc* *.tgz ./
|
||||||
COPY patches* ./patches/
|
COPY patches* ./patches/
|
||||||
|
|
||||||
ARG NPM_DOMAIN=git.infra.mintel.me
|
ARG NPM_DOMAIN=git.infra.mintel.me
|
||||||
|
|||||||
13
app/[locale]/errors/api/relay/route.ts
Normal file
13
app/[locale]/errors/api/relay/route.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
try {
|
||||||
|
const rawText = await req.text();
|
||||||
|
// Sentry sends NDJSON (Newline Delimited JSON). Split by newline to parse safely.
|
||||||
|
const items = rawText.split('\n').filter(Boolean).map(line => JSON.parse(line));
|
||||||
|
console.log("CLIENT ERROR INTERCEPTED:", JSON.stringify(items[0], null, 2));
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Failed to parse relay body (NDJSON)", e);
|
||||||
|
}
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import { getMessages } from 'next-intl/server';
|
|||||||
import '../../styles/globals.css';
|
import '../../styles/globals.css';
|
||||||
import { SITE_URL } from '@/lib/schema';
|
import { SITE_URL } from '@/lib/schema';
|
||||||
import FeedbackClientWrapper from '@/components/FeedbackClientWrapper';
|
import FeedbackClientWrapper from '@/components/FeedbackClientWrapper';
|
||||||
|
import AnnotatorClientWrapper from '@/components/AnnotatorClientWrapper';
|
||||||
import { setRequestLocale } from 'next-intl/server';
|
import { setRequestLocale } from 'next-intl/server';
|
||||||
import { Inter } from 'next/font/google';
|
import { Inter } from 'next/font/google';
|
||||||
import { mapFileSlugToTranslated } from '@/lib/slugs';
|
import { mapFileSlugToTranslated } from '@/lib/slugs';
|
||||||
@@ -37,7 +38,7 @@ export async function generateMetadata(props: {
|
|||||||
template: '%s | E-TIB',
|
template: '%s | E-TIB',
|
||||||
default: 'E-TIB | Die Experten für Kabelnetzbau',
|
default: 'E-TIB | Die Experten für Kabelnetzbau',
|
||||||
},
|
},
|
||||||
description: 'Ihr Partner für Kabelleitungstiefbau, Horizontalspülbohrungen, Planung und Vermessung in Guben und überregional.',
|
description: 'Ihr Partner für Kabelleitungsnetzbau, Horizontalspülbohrungen, Planung und Vermessung in Guben und überregional.',
|
||||||
metadataBase: new URL(baseUrl),
|
metadataBase: new URL(baseUrl),
|
||||||
manifest: '/manifest.webmanifest',
|
manifest: '/manifest.webmanifest',
|
||||||
alternates: {
|
alternates: {
|
||||||
@@ -214,6 +215,7 @@ export default async function Layout(props: {
|
|||||||
<Footer companyInfo={companyInfo} />
|
<Footer companyInfo={companyInfo} />
|
||||||
<JsonLd />
|
<JsonLd />
|
||||||
<AnalyticsShell />
|
<AnalyticsShell />
|
||||||
|
{process.env.TARGET !== 'production' && <AnnotatorClientWrapper />}
|
||||||
{feedbackEnabled && <FeedbackClientWrapper feedbackEnabled={feedbackEnabled} />}
|
{feedbackEnabled && <FeedbackClientWrapper feedbackEnabled={feedbackEnabled} />}
|
||||||
</TransitionProvider>
|
</TransitionProvider>
|
||||||
</NextIntlClientProvider>
|
</NextIntlClientProvider>
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
import { describe, it, expect, vi } from 'vitest';
|
|
||||||
import ReferenceDetail from './page';
|
|
||||||
|
|
||||||
// Mock next/navigation
|
|
||||||
vi.mock('next/navigation', () => ({
|
|
||||||
notFound: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock next-intl/server
|
|
||||||
vi.mock('next-intl/server', () => ({
|
|
||||||
setRequestLocale: vi.fn(),
|
|
||||||
getTranslations: vi.fn().mockResolvedValue(() => 'translated text'),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock @/lib/references
|
|
||||||
vi.mock('@/lib/references', () => ({
|
|
||||||
getReferenceBySlug: vi.fn().mockResolvedValue({
|
|
||||||
slug: 'test-slug',
|
|
||||||
frontmatter: {
|
|
||||||
title: 'Test Title',
|
|
||||||
location: 'Test Location',
|
|
||||||
category: 'Test Category',
|
|
||||||
},
|
|
||||||
content: 'Test content',
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('ReferenceDetail Page TDD', () => {
|
|
||||||
it('renders reference detail correctly', async () => {
|
|
||||||
const props = {
|
|
||||||
params: Promise.resolve({ locale: 'de', slug: 'test-slug' })
|
|
||||||
};
|
|
||||||
|
|
||||||
// We expect the execution to fail if getTranslations is not defined/imported inside page.tsx
|
|
||||||
const component = await ReferenceDetail(props);
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
import { notFound } from 'next/navigation';
|
|
||||||
import { Container, Badge, Heading } from '@/components/ui';
|
|
||||||
import { setRequestLocale, getTranslations } from 'next-intl/server';
|
|
||||||
import { Metadata } from 'next';
|
|
||||||
import { getReferenceBySlug, getAllReferences } from '@/lib/references';
|
|
||||||
import { MDXRemote } from 'next-mdx-remote/rsc';
|
|
||||||
import { SITE_URL } from '@/lib/schema';
|
|
||||||
import TrackedLink from '@/components/analytics/TrackedLink';
|
|
||||||
import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
|
|
||||||
import { MapPin, Calendar, Briefcase, ChevronLeft } from 'lucide-react';
|
|
||||||
import Image from 'next/image';
|
|
||||||
|
|
||||||
interface PageProps {
|
|
||||||
params: Promise<{
|
|
||||||
locale: string;
|
|
||||||
slug: string;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
|
||||||
const { locale, slug } = await params;
|
|
||||||
if (locale !== 'de' && locale !== 'en') return {};
|
|
||||||
|
|
||||||
const reference = await getReferenceBySlug(slug, locale);
|
|
||||||
if (!reference) return {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: `${reference.frontmatter.title} | Referenzen | E-TIB Gruppe`,
|
|
||||||
description: `Details zum Projekt ${reference.frontmatter.title} in ${reference.frontmatter.location}.`,
|
|
||||||
alternates: {
|
|
||||||
canonical: `${SITE_URL}/${locale}/referenzen/${slug}`,
|
|
||||||
},
|
|
||||||
openGraph: {
|
|
||||||
title: `${reference.frontmatter.title} | E-TIB`,
|
|
||||||
description: `Details zum Projekt ${reference.frontmatter.title} in ${reference.frontmatter.location}.`,
|
|
||||||
url: `${SITE_URL}/${locale}/referenzen/${slug}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function generateStaticParams(props: any) {
|
|
||||||
const params = await props.params;
|
|
||||||
const locale = params?.locale || 'de'; // fallback to 'de' if undefined
|
|
||||||
const references = await getAllReferences(locale);
|
|
||||||
return references.map((ref) => ({
|
|
||||||
slug: ref.slug,
|
|
||||||
locale
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function ReferenceDetail(props: { params: Promise<{ locale: string; slug: string }> }) {
|
|
||||||
const { locale, slug } = await props.params;
|
|
||||||
|
|
||||||
if (locale !== 'de' && locale !== 'en') {
|
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
setRequestLocale(locale);
|
|
||||||
const reference = await getReferenceBySlug(slug, locale);
|
|
||||||
const t = await getTranslations('ReferenceDetail');
|
|
||||||
|
|
||||||
if (!reference) {
|
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
// MDX components mapping for industrial typography
|
|
||||||
const mdxComponents = {
|
|
||||||
Heading,
|
|
||||||
h1: (props: any) => <Heading level={1} size="2" className="hidden" {...props} />, // Hidden because Hero handles H1
|
|
||||||
h2: (props: any) => <Heading level={2} size="3" className="mt-16 mb-6 border-b border-neutral-100 pb-4 text-neutral-dark" {...props} />,
|
|
||||||
h3: (props: any) => <Heading level={3} size="4" className="mt-12 mb-4 text-primary" {...props} />,
|
|
||||||
h4: (props: any) => <Heading level={4} size="5" className="mt-8 mb-4 uppercase tracking-widest text-neutral-500" {...props} />,
|
|
||||||
p: (props: any) => <p className="text-base md:text-lg text-text-secondary leading-[1.8] mb-6 font-medium max-w-3xl" {...props} />,
|
|
||||||
ul: (props: any) => <ul className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6 my-12" {...props} />,
|
|
||||||
ol: (props: any) => <ol className="list-decimal pl-6 mb-8 space-y-3 text-base md:text-lg text-text-secondary font-medium max-w-3xl" {...props} />,
|
|
||||||
li: (props: any) => {
|
|
||||||
// Smart Metric Card Logic
|
|
||||||
let text = '';
|
|
||||||
if (typeof props.children === 'string') {
|
|
||||||
text = props.children;
|
|
||||||
} else if (Array.isArray(props.children)) {
|
|
||||||
text = props.children.map((c: any) => typeof c === 'string' ? c : '').join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
const match = text.match(/^([\d.,]+)\s*(m|Satz|Stück|km|kV|Fasern)\b(.*)/);
|
|
||||||
|
|
||||||
if (match && typeof props.children === 'string') {
|
|
||||||
const [, value, unit, rest] = match;
|
|
||||||
return (
|
|
||||||
<li className="bg-white p-6 md:p-8 rounded-3xl border border-neutral-100 flex flex-col justify-center hover:shadow-[0_20px_40px_-15px_rgba(0,0,0,0.05)] hover:border-primary/30 transition-all duration-500 group hover:-translate-y-1 relative overflow-hidden">
|
|
||||||
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl from-neutral-50 to-transparent rounded-bl-full -z-10 group-hover:from-primary/5 transition-colors duration-500" />
|
|
||||||
<div className="flex items-baseline gap-2 mb-3">
|
|
||||||
<span className="text-4xl md:text-5xl font-bold font-heading text-primary tracking-tight">{value}</span>
|
|
||||||
<span className="text-lg md:text-xl font-bold text-neutral-400">{unit}</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-neutral-600 font-semibold text-sm md:text-base leading-snug">{rest.replace(/^[\s-]*\s/, '')}</span>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li className="bg-neutral-50 p-6 rounded-3xl border border-neutral-100 flex items-start gap-4 hover:shadow-md transition-all duration-300 group hover:-translate-y-1">
|
|
||||||
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center shrink-0 mt-0.5 group-hover:bg-primary/20 transition-colors">
|
|
||||||
<svg className="w-4 h-4 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<span className="text-neutral-700 font-semibold leading-relaxed">{props.children}</span>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
a: (props: any) => <a className="text-primary hover:text-primary-dark underline decoration-primary/30 underline-offset-4 hover:decoration-primary transition-all font-bold" {...props} />,
|
|
||||||
strong: (props: any) => <strong className="font-bold text-neutral-900" {...props} />,
|
|
||||||
blockquote: (props: any) => (
|
|
||||||
<blockquote className="border-l-4 border-primary pl-6 py-3 my-10 italic bg-neutral-50 rounded-r-2xl text-neutral-700 font-medium max-w-3xl shadow-sm" {...props} />
|
|
||||||
),
|
|
||||||
hr: (props: any) => <hr className="my-16 border-t-2 border-neutral-100 max-w-3xl" {...props} />,
|
|
||||||
img: (props: any) => <img className="rounded-2xl shadow-2xl my-12 max-w-full h-auto border border-neutral-100" {...props} />,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col min-h-screen bg-white">
|
|
||||||
{/* Hero Section */}
|
|
||||||
<section className="bg-[#050B14] text-white pt-28 pb-10 md:pt-52 md:pb-24 min-h-[30vh] md:min-h-[45vh] flex flex-col justify-end relative overflow-hidden">
|
|
||||||
{reference.frontmatter.featuredImage && (
|
|
||||||
<div className="absolute inset-0 z-0">
|
|
||||||
<Image
|
|
||||||
src={reference.frontmatter.featuredImage}
|
|
||||||
alt={reference.frontmatter.title}
|
|
||||||
fill
|
|
||||||
className="object-cover opacity-30 mix-blend-luminosity scale-105"
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-[#050B14] via-[#050B14]/70 to-transparent" />
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-r from-[#050B14] via-[#050B14]/40 to-transparent" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="absolute inset-0 opacity-20 z-0 pointer-events-none">
|
|
||||||
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-primary/30 via-transparent to-transparent" />
|
|
||||||
</div>
|
|
||||||
<Container className="relative z-10">
|
|
||||||
<div className="mb-12">
|
|
||||||
<TrackedLink
|
|
||||||
href={`/${locale}/referenzen`}
|
|
||||||
className="inline-flex items-center gap-2 text-white/60 hover:text-white transition-colors uppercase tracking-widest text-xs font-bold"
|
|
||||||
eventProperties={{ location: 'reference_back_btn' }}
|
|
||||||
>
|
|
||||||
<ChevronLeft className="w-4 h-4" />
|
|
||||||
{t('backToOverview')}
|
|
||||||
</TrackedLink>
|
|
||||||
</div>
|
|
||||||
<div className="max-w-5xl">
|
|
||||||
<Badge variant="accent" className="mb-6">
|
|
||||||
{t('projectReference')}
|
|
||||||
</Badge>
|
|
||||||
<Heading level={1} variant="white" className="mb-10 leading-[1.1] md:text-6xl lg:text-7xl drop-shadow-lg">
|
|
||||||
{reference.frontmatter.title}
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 md:gap-6 mt-12 md:mt-16">
|
|
||||||
<div className="flex items-start gap-4 p-6 md:p-8 bg-white/5 rounded-3xl border border-white/10 backdrop-blur-md transition-all hover:bg-white/10">
|
|
||||||
<MapPin className="w-8 h-8 text-primary shrink-0 opacity-80" />
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-white/50 mb-1.5 uppercase tracking-widest font-bold">{t('location')}</p>
|
|
||||||
<p className="font-semibold text-lg md:text-xl text-white/90">{reference.frontmatter.location}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start gap-4 p-6 md:p-8 bg-white/5 rounded-3xl border border-white/10 backdrop-blur-md transition-all hover:bg-white/10">
|
|
||||||
<Briefcase className="w-8 h-8 text-primary shrink-0 opacity-80" />
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-white/50 mb-1.5 uppercase tracking-widest font-bold">{t('client')}</p>
|
|
||||||
<p className="font-semibold text-lg md:text-xl text-white/90 line-clamp-2">{reference.frontmatter.client}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start gap-4 p-6 md:p-8 bg-white/5 rounded-3xl border border-white/10 backdrop-blur-md transition-all hover:bg-white/10">
|
|
||||||
<Calendar className="w-8 h-8 text-primary shrink-0 opacity-80" />
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-white/50 mb-1.5 uppercase tracking-widest font-bold">{t('period')}</p>
|
|
||||||
<p className="font-semibold text-lg md:text-xl text-white/90">{reference.frontmatter.dateString || new Date(reference.frontmatter.date).getFullYear()}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Main Content Area */}
|
|
||||||
<Container className="py-16 md:py-24">
|
|
||||||
<div className="max-w-4xl mx-auto">
|
|
||||||
<h2 className="text-3xl font-bold text-neutral-dark mb-8">{t('scopeTitle')}</h2>
|
|
||||||
|
|
||||||
<div className="w-full">
|
|
||||||
<MDXRemote source={reference.content} components={mdxComponents} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-20 flex justify-center">
|
|
||||||
<TrackedLink
|
|
||||||
href={`/${locale}/referenzen`}
|
|
||||||
className={getButtonClasses('primary', 'lg')}
|
|
||||||
eventProperties={{ location: 'reference_bottom_back_btn' }}
|
|
||||||
>
|
|
||||||
<span className="relative z-10 flex items-center justify-center gap-2">
|
|
||||||
{t('viewAll')}
|
|
||||||
</span>
|
|
||||||
<ButtonOverlay variant="primary" />
|
|
||||||
</TrackedLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -11,6 +11,14 @@ import { InteractiveGermanyMap } from '@/components/blocks/InteractiveGermanyMap
|
|||||||
import { HeroSection } from '@/components/blocks/HeroSection';
|
import { HeroSection } from '@/components/blocks/HeroSection';
|
||||||
import { defaultLocations, minorLocations } from '@/lib/map-data';
|
import { defaultLocations, minorLocations } from '@/lib/map-data';
|
||||||
|
|
||||||
|
import { MDXRemote } from 'next-mdx-remote/rsc';
|
||||||
|
|
||||||
|
const mdxComponents = {
|
||||||
|
ul: (props: any) => <ul className="list-disc pl-5 mb-6 text-neutral-600 text-sm space-y-2" {...props} />,
|
||||||
|
li: (props: any) => <li className="pl-1" {...props} />,
|
||||||
|
p: (props: any) => <p className="text-neutral-600 text-sm mb-4 leading-relaxed" {...props} />,
|
||||||
|
};
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
locale: string;
|
locale: string;
|
||||||
@@ -90,11 +98,9 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
|
|||||||
<Container className="relative z-20 mt-16 md:mt-24">
|
<Container className="relative z-20 mt-16 md:mt-24">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
|
||||||
{references.map((ref) => (
|
{references.map((ref) => (
|
||||||
<TrackedLink
|
<div
|
||||||
key={ref.slug}
|
key={ref.slug}
|
||||||
href={`/${locale}/referenzen/${ref.slug}`}
|
className="flex flex-col bg-white border border-neutral-100 rounded-3xl overflow-hidden shadow-sm block"
|
||||||
eventProperties={{ location: 'reference_list_card', slug: ref.slug }}
|
|
||||||
className="group flex flex-col bg-white border border-neutral-100 rounded-3xl overflow-hidden shadow-sm hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-1 cursor-pointer block"
|
|
||||||
>
|
>
|
||||||
<div className="flex flex-col h-full relative">
|
<div className="flex flex-col h-full relative">
|
||||||
{/* Image Section */}
|
{/* Image Section */}
|
||||||
@@ -111,7 +117,7 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
|
|||||||
<Image src="/assets/logo.png" alt="E-TIB Logo" width={80} height={80} className="opacity-20 grayscale" />
|
<Image src="/assets/logo.png" alt="E-TIB Logo" width={80} height={80} className="opacity-20 grayscale" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="absolute inset-0 bg-black/5 transition-colors duration-500" />
|
<div className="absolute inset-0 bg-black/5" />
|
||||||
|
|
||||||
{/* Location Badge */}
|
{/* Location Badge */}
|
||||||
<div className="absolute top-4 left-4 z-10">
|
<div className="absolute top-4 left-4 z-10">
|
||||||
@@ -124,10 +130,17 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
|
|||||||
|
|
||||||
{/* Content Section */}
|
{/* Content Section */}
|
||||||
<div className="flex flex-col flex-grow p-6 md:p-8 bg-white">
|
<div className="flex flex-col flex-grow p-6 md:p-8 bg-white">
|
||||||
<h3 className="text-xl md:text-2xl font-bold font-heading text-neutral-dark mb-6 transition-colors line-clamp-3 leading-[1.2] group-hover:text-primary">
|
<h3 className="text-xl md:text-2xl font-bold font-heading text-neutral-dark mb-6 leading-[1.2]">
|
||||||
{ref.frontmatter.title}
|
{ref.frontmatter.title}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
{/* Project Details / Content */}
|
||||||
|
{ref.content && (
|
||||||
|
<div className="flex-grow mb-6">
|
||||||
|
<MDXRemote source={ref.content} components={mdxComponents} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Technical Meta Data */}
|
{/* Technical Meta Data */}
|
||||||
<div className="flex flex-col gap-3 mt-auto border-t border-neutral-100 pt-6 relative z-10">
|
<div className="flex flex-col gap-3 mt-auto border-t border-neutral-100 pt-6 relative z-10">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
@@ -147,7 +160,7 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TrackedLink>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
'use server';
|
|
||||||
|
|
||||||
import { getServerAppServices } from '@/lib/services/create-services.server';
|
|
||||||
|
|
||||||
export async function requestBrochureAction(formData: FormData) {
|
|
||||||
const services = getServerAppServices();
|
|
||||||
const logger = services.logger.child({ action: 'requestBrochureAction' });
|
|
||||||
|
|
||||||
const { headers } = await import('next/headers');
|
|
||||||
const requestHeaders = await headers();
|
|
||||||
|
|
||||||
if ('setServerContext' in services.analytics) {
|
|
||||||
(services.analytics as any).setServerContext({
|
|
||||||
userAgent: requestHeaders.get('user-agent') || undefined,
|
|
||||||
language: requestHeaders.get('accept-language')?.split(',')[0] || undefined,
|
|
||||||
referrer: requestHeaders.get('referer') || undefined,
|
|
||||||
ip: requestHeaders.get('x-forwarded-for')?.split(',')[0] || undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
services.analytics.track('brochure-request-attempt');
|
|
||||||
|
|
||||||
const email = formData.get('email') as string;
|
|
||||||
const locale = (formData.get('locale') as string) || 'en';
|
|
||||||
|
|
||||||
// Anti-spam Honeypot Check
|
|
||||||
const honeypot = formData.get('company_website') as string;
|
|
||||||
if (honeypot) {
|
|
||||||
logger.warn('Spam detected via honeypot in brochure request', { email });
|
|
||||||
// Silently succeed to fool the bot without doing actual work
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!email) {
|
|
||||||
logger.warn('Missing email in brochure request');
|
|
||||||
return { success: false, error: 'Missing email address' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Basic email validation
|
|
||||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
||||||
return { success: false, error: 'Invalid email address' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Save to CMS - Removed along with Payload CMS
|
|
||||||
|
|
||||||
// 2. Notify via Gotify
|
|
||||||
try {
|
|
||||||
await services.notifications.notify({
|
|
||||||
title: '📑 Brochure Download Request',
|
|
||||||
message: `New brochure download request from ${email} (${locale})`,
|
|
||||||
priority: 3,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to send notification', { error });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Send Brochure via Email
|
|
||||||
const brochureUrl = `https://e-tib.com/brochure/etib-product-catalog-${locale}.pdf`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { sendEmail } = await import('@/lib/mail/mailer');
|
|
||||||
const { render } = await import('@mintel/mail');
|
|
||||||
const React = await import('react');
|
|
||||||
const { BrochureDeliveryEmail } = await import('@/components/emails/BrochureDeliveryEmail');
|
|
||||||
|
|
||||||
const html = await render(
|
|
||||||
React.createElement(BrochureDeliveryEmail, {
|
|
||||||
_email: email,
|
|
||||||
brochureUrl,
|
|
||||||
locale: locale as 'en' | 'de',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const emailResult = await sendEmail({
|
|
||||||
to: email,
|
|
||||||
subject: locale === 'de' ? 'Ihr E-TIB Kabelkatalog' : 'Your E-TIB Cable Catalog',
|
|
||||||
html,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (emailResult.success) {
|
|
||||||
logger.info('Brochure email sent successfully', { email });
|
|
||||||
} else {
|
|
||||||
logger.error('Failed to send brochure email', { error: emailResult.error, email });
|
|
||||||
services.errors.captureException(new Error(`Brochure email failed: ${emailResult.error}`), {
|
|
||||||
action: 'requestBrochureAction_email',
|
|
||||||
email,
|
|
||||||
});
|
|
||||||
return { success: false, error: 'Failed to send email. Please try again later.' };
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Exception while sending brochure email', { error });
|
|
||||||
return { success: false, error: 'Failed to send email. Please try again later.' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Track success
|
|
||||||
services.analytics.track('brochure-request-success', {
|
|
||||||
locale,
|
|
||||||
delivery_method: 'email',
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
36
app/actions/getAnnotatorAssets.ts
Normal file
36
app/actions/getAnnotatorAssets.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
'use server';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export async function getAnnotatorAssets() {
|
||||||
|
// Try /app/public first (docker), then process.cwd()/public
|
||||||
|
let publicDir = '/app/public';
|
||||||
|
if (!fs.existsSync(publicDir)) {
|
||||||
|
publicDir = path.join(process.cwd(), 'public');
|
||||||
|
}
|
||||||
|
|
||||||
|
const walk = async (dir: string): Promise<string[]> => {
|
||||||
|
let results: string[] = [];
|
||||||
|
if (!fs.existsSync(dir)) return results;
|
||||||
|
|
||||||
|
const list = await fs.promises.readdir(dir);
|
||||||
|
for (const file of list) {
|
||||||
|
const filePath = path.join(dir, file);
|
||||||
|
const stat = await fs.promises.stat(filePath);
|
||||||
|
if (stat && stat.isDirectory()) {
|
||||||
|
const nested = await walk(filePath);
|
||||||
|
results = results.concat(nested);
|
||||||
|
} else {
|
||||||
|
if (filePath.match(/\.(png|jpg|jpeg|webp|mp4|svg)$/i)) {
|
||||||
|
results.push(filePath.replace(publicDir, ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
const results = await walk(publicDir);
|
||||||
|
|
||||||
|
// Console logs removed for absolute silence & speed
|
||||||
|
return results;
|
||||||
|
}
|
||||||
52
app/actions/submitAnnotations.ts
Normal file
52
app/actions/submitAnnotations.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { sendEmail } from '@/lib/mail/mailer';
|
||||||
|
import { getServerAppServices } from '@/lib/services/create-services.server';
|
||||||
|
|
||||||
|
export async function submitAnnotations(annotations: any[]) {
|
||||||
|
const services = getServerAppServices();
|
||||||
|
const logger = services.logger.child({ action: 'submitAnnotations' });
|
||||||
|
|
||||||
|
if (!annotations || annotations.length === 0) {
|
||||||
|
return { success: false, error: 'Keine Korrekturen vorhanden.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const jsonContent = JSON.stringify(annotations, null, 2);
|
||||||
|
const htmlContent = `
|
||||||
|
<h2>Neue Website-Korrekturen (${annotations.length})</h2>
|
||||||
|
<p>Es wurden neue Korrekturen über den Annotator eingereicht.</p>
|
||||||
|
<ul>
|
||||||
|
${annotations.map(ann => `
|
||||||
|
<li>
|
||||||
|
<b>${new URL(ann.url).pathname}</b> [${ann.type}]<br />
|
||||||
|
Element: <code>${ann.selector}</code><br />
|
||||||
|
<i>${ann.type === 'image' ? 'Neues Bild: ' : 'Text: '}${ann.newContent}</i>
|
||||||
|
</li>
|
||||||
|
`).join('')}
|
||||||
|
</ul>
|
||||||
|
<p><i>Die genauen Daten (inkl. Scroll-Positionen, Viewport und Zeitstempel) befinden sich hier:</i></p>
|
||||||
|
<pre style="background: #f4f4f4; padding: 10px; border-radius: 5px; font-size: 12px; overflow-x: auto;">
|
||||||
|
${jsonContent}
|
||||||
|
</pre>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const notificationResult = await sendEmail({
|
||||||
|
replyTo: 'info@e-tib.com', // Falls Rückfragen nötig
|
||||||
|
subject: `[Korrekturen] ${annotations.length} neue Anmerkungen für E-TIB`,
|
||||||
|
html: htmlContent
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!notificationResult.success) {
|
||||||
|
logger.error('Annotation email FAILED', { error: notificationResult.error });
|
||||||
|
return { success: false, error: notificationResult.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Annotation email sent successfully', { count: annotations.length });
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
|
logger.error('Failed to send annotations', { error: errorMsg });
|
||||||
|
return { success: false, error: errorMsg };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export default function TestPage() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>TEST PAGE WORKS</h1>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
31
components/AnnotatorClientWrapper.tsx
Normal file
31
components/AnnotatorClientWrapper.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { getAnnotatorAssets } from '@/app/actions/getAnnotatorAssets';
|
||||||
|
import { submitAnnotations } from '@/app/actions/submitAnnotations';
|
||||||
|
|
||||||
|
const Annotator = dynamic(() => import('@/components/annotator-local/Annotator').then(mod => mod.Annotator), { ssr: false });
|
||||||
|
|
||||||
|
export default function AnnotatorClientWrapper() {
|
||||||
|
const [assets, setAssets] = useState<string[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getAnnotatorAssets()
|
||||||
|
.then(fetchedAssets => {
|
||||||
|
if (Array.isArray(fetchedAssets)) {
|
||||||
|
setAssets(fetchedAssets);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.error('[AnnotatorClientWrapper] Fetch failed:', err));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSubmit = async (annotations: any[]) => {
|
||||||
|
const result = await submitAnnotations(annotations);
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Annotator assets={assets} onSubmit={handleSubmit} />;
|
||||||
|
}
|
||||||
626
components/annotator-local/Annotator.tsx
Normal file
626
components/annotator-local/Annotator.tsx
Normal file
@@ -0,0 +1,626 @@
|
|||||||
|
import { useState, useEffect, useMemo } from "react";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import { MessageSquare, X, Check, Image as ImageIcon, Type, StickyNote, Download, Trash2, List } from "lucide-react";
|
||||||
|
import { clsx, type ClassValue } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { finder } from "@medv/finder";
|
||||||
|
|
||||||
|
function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Annotation {
|
||||||
|
id: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
selector: string;
|
||||||
|
type: "text" | "image" | "note";
|
||||||
|
originalContent: string;
|
||||||
|
newContent: string;
|
||||||
|
url: string;
|
||||||
|
elementRect: Record<string, number> | null;
|
||||||
|
scrollX: number;
|
||||||
|
scrollY: number;
|
||||||
|
viewportWidth: number;
|
||||||
|
viewportHeight: number;
|
||||||
|
userAgent: string;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnnotatorProps {
|
||||||
|
assets?: string[]; // Array of image URLs for the media browser
|
||||||
|
onSubmit?: (annotations: Annotation[]) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Annotator({ assets = [], onSubmit }: AnnotatorProps) {
|
||||||
|
const [isActive, setIsActive] = useState(false);
|
||||||
|
const [hoveredElement, setHoveredElement] = useState<HTMLElement | null>(null);
|
||||||
|
const [selectedElement, setSelectedElement] = useState<HTMLElement | null>(null);
|
||||||
|
const [annotations, setAnnotations] = useState<Annotation[]>(() => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const saved = localStorage.getItem("mintel-annotations");
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(saved);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to parse saved annotations", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
localStorage.setItem("mintel-annotations", JSON.stringify(annotations));
|
||||||
|
}
|
||||||
|
}, [annotations]);
|
||||||
|
|
||||||
|
// Modal state
|
||||||
|
const [currentType, setCurrentType] = useState<"text" | "image" | "note">("text");
|
||||||
|
const [currentText, setCurrentText] = useState("");
|
||||||
|
const [selectedAsset, setSelectedAsset] = useState<string | null>(null);
|
||||||
|
const [showList, setShowList] = useState(false);
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Disable overlays if needed (e.g. inside iframes)
|
||||||
|
const isExcluded = useMemo(() => {
|
||||||
|
if (typeof window === "undefined") return false;
|
||||||
|
return window.self !== window.top;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getSelector = (el: HTMLElement): string => {
|
||||||
|
try {
|
||||||
|
return finder(el, {
|
||||||
|
root: document.body,
|
||||||
|
className: (name) =>
|
||||||
|
!name.startsWith("annotator-") &&
|
||||||
|
!name.includes("[") &&
|
||||||
|
!name.includes("/") &&
|
||||||
|
!name.match(/^[a-z]-[0-9]/),
|
||||||
|
idName: (name) => !name.startsWith("__next") && !name.includes(":"),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isActive) {
|
||||||
|
setHoveredElement(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
if (selectedElement) return;
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (target.closest(".annotator-ui-ignore")) {
|
||||||
|
setHoveredElement(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setHoveredElement(target);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = (e: MouseEvent) => {
|
||||||
|
if (selectedElement) return;
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (target.closest(".annotator-ui-ignore")) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
setSelectedElement(target);
|
||||||
|
setHoveredElement(null);
|
||||||
|
|
||||||
|
// Auto-detect type
|
||||||
|
if (target.tagName.toLowerCase() === "img") {
|
||||||
|
setCurrentType("image");
|
||||||
|
} else {
|
||||||
|
setCurrentType("text");
|
||||||
|
setCurrentText(target.innerText || "");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("mousemove", handleMouseMove);
|
||||||
|
window.addEventListener("click", handleClick, true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
window.removeEventListener("click", handleClick, true);
|
||||||
|
};
|
||||||
|
}, [isActive, selectedElement]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
if (selectedElement) {
|
||||||
|
setSelectedElement(null);
|
||||||
|
} else if (isActive) {
|
||||||
|
setIsActive(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [isActive, selectedElement]);
|
||||||
|
|
||||||
|
const saveAnnotation = () => {
|
||||||
|
if (!selectedElement) return;
|
||||||
|
|
||||||
|
const rect = selectedElement.getBoundingClientRect();
|
||||||
|
const newContent = currentType === "image" ? selectedAsset || "" : currentText;
|
||||||
|
|
||||||
|
if (!newContent) return;
|
||||||
|
|
||||||
|
const annotation: Annotation = {
|
||||||
|
id: Math.random().toString(36).substring(2, 9),
|
||||||
|
x: rect.left + rect.width / 2 + window.scrollX,
|
||||||
|
y: rect.top + rect.height / 2 + window.scrollY,
|
||||||
|
selector: getSelector(selectedElement),
|
||||||
|
type: currentType,
|
||||||
|
originalContent: currentType === "image" ? (selectedElement as HTMLImageElement).src || "" : selectedElement.innerText,
|
||||||
|
newContent,
|
||||||
|
url: window.location.href,
|
||||||
|
elementRect: rect ? {
|
||||||
|
x: rect.x, y: rect.y, width: rect.width, height: rect.height,
|
||||||
|
top: rect.top, right: rect.right, bottom: rect.bottom, left: rect.left
|
||||||
|
} : null,
|
||||||
|
scrollX: window.scrollX,
|
||||||
|
scrollY: window.scrollY,
|
||||||
|
viewportWidth: window.innerWidth,
|
||||||
|
viewportHeight: window.innerHeight,
|
||||||
|
userAgent: navigator.userAgent,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setAnnotations([...annotations, annotation]);
|
||||||
|
setSelectedElement(null);
|
||||||
|
setCurrentText("");
|
||||||
|
setSelectedAsset(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportJSON = () => {
|
||||||
|
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(annotations, null, 2));
|
||||||
|
const downloadAnchorNode = document.createElement("a");
|
||||||
|
downloadAnchorNode.setAttribute("href", dataStr);
|
||||||
|
downloadAnchorNode.setAttribute("download", "corrections.json");
|
||||||
|
document.body.appendChild(downloadAnchorNode);
|
||||||
|
downloadAnchorNode.click();
|
||||||
|
downloadAnchorNode.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!onSubmit || annotations.length === 0) return;
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
await onSubmit(annotations);
|
||||||
|
setAnnotations([]);
|
||||||
|
setShowList(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Submission failed", error);
|
||||||
|
alert("Fehler beim Senden der Korrekturen. Bitte JSON Export nutzen.");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteAnnotation = (id: string) => {
|
||||||
|
setAnnotations(annotations.filter((a) => a.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const [updateTick, setUpdateTick] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isActive && !selectedElement) return;
|
||||||
|
|
||||||
|
let rafId: number;
|
||||||
|
const handleScroll = () => {
|
||||||
|
cancelAnimationFrame(rafId);
|
||||||
|
rafId = requestAnimationFrame(() => setUpdateTick(t => t + 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use capture phase to catch scrolls on any nested container
|
||||||
|
window.addEventListener("scroll", handleScroll, true);
|
||||||
|
window.addEventListener("resize", handleScroll);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("scroll", handleScroll, true);
|
||||||
|
window.removeEventListener("resize", handleScroll);
|
||||||
|
cancelAnimationFrame(rafId);
|
||||||
|
};
|
||||||
|
}, [isActive, selectedElement]);
|
||||||
|
|
||||||
|
const hoveredRect = useMemo(() => hoveredElement?.getBoundingClientRect(), [hoveredElement, updateTick]);
|
||||||
|
const selectedRect = useMemo(() => selectedElement?.getBoundingClientRect(), [selectedElement, updateTick]);
|
||||||
|
|
||||||
|
if (isExcluded) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="annotator-ui-ignore">
|
||||||
|
{/* 1. Global Toolbar */}
|
||||||
|
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-[9999] w-max max-w-[95vw]">
|
||||||
|
<div className="bg-black/80 backdrop-blur-xl border border-white/10 p-2 rounded-2xl shadow-2xl flex items-center gap-1 sm:gap-2 overflow-x-auto hide-scrollbar" style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsActive(!isActive)}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 px-4 py-2 rounded-xl transition-all font-medium",
|
||||||
|
isActive
|
||||||
|
? "bg-blue-500 text-white shadow-lg shadow-blue-500/20"
|
||||||
|
: "text-white/70 hover:text-white hover:bg-white/10"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isActive ? <X size={18} /> : <MessageSquare size={18} />}
|
||||||
|
{isActive ? "Modus beenden" : "Korrekturen erfassen"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="w-px h-6 bg-white/10 mx-1" />
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setShowList(!showList)}
|
||||||
|
className="p-2 text-white/70 hover:text-white hover:bg-white/10 rounded-xl relative"
|
||||||
|
>
|
||||||
|
<List size={20} />
|
||||||
|
{annotations.length > 0 && (
|
||||||
|
<span className="absolute -top-1 -right-1 w-5 h-5 bg-blue-500 text-[10px] flex items-center justify-center rounded-full text-white font-bold border-2 border-[#1a1a1a]">
|
||||||
|
{annotations.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{annotations.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="w-px h-6 bg-white/10 mx-1" />
|
||||||
|
<button
|
||||||
|
onClick={exportJSON}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 rounded-xl text-white/70 hover:text-white hover:bg-white/10 transition-all font-medium"
|
||||||
|
>
|
||||||
|
<Download size={18} />
|
||||||
|
JSON Export
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 2. Highlights */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{isActive && (
|
||||||
|
<div className="fixed inset-0 pointer-events-none z-[9998]">
|
||||||
|
{hoveredRect && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="absolute border-2 border-blue-400 bg-blue-400/10 rounded-sm transition-all duration-200"
|
||||||
|
style={{
|
||||||
|
top: hoveredRect.top,
|
||||||
|
left: hoveredRect.left,
|
||||||
|
width: hoveredRect.width,
|
||||||
|
height: hoveredRect.height,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedRect && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0.9, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
className="absolute border-2 border-yellow-400 bg-yellow-400/20 rounded-sm"
|
||||||
|
style={{
|
||||||
|
top: selectedRect.top,
|
||||||
|
left: selectedRect.left,
|
||||||
|
width: selectedRect.width,
|
||||||
|
height: selectedRect.height,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* 3. Action Modal */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{selectedElement && (
|
||||||
|
<div className="fixed inset-0 flex items-center justify-center z-[10000] bg-black/40 backdrop-blur-sm">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||||
|
className="bg-[#1c1c1e] border border-white/10 rounded-3xl p-4 sm:p-6 w-[95vw] md:w-[800px] shadow-2xl flex flex-col max-h-[85vh]"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h3 className="text-white font-bold text-lg">Änderung erfassen</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedElement(null)}
|
||||||
|
className="text-white/40 hover:text-white"
|
||||||
|
>
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 mb-6 shrink-0">
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentType("text")}
|
||||||
|
className={cn(
|
||||||
|
"flex-1 py-3 px-4 rounded-xl text-sm font-medium transition-all flex items-center justify-center gap-2",
|
||||||
|
currentType === "text"
|
||||||
|
? "bg-white text-black shadow-lg"
|
||||||
|
: "bg-white/5 text-white/40 hover:bg-white/10"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Type size={16} /> Text
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentType("image")}
|
||||||
|
className={cn(
|
||||||
|
"flex-1 py-3 px-4 rounded-xl text-sm font-medium transition-all flex items-center justify-center gap-2",
|
||||||
|
currentType === "image"
|
||||||
|
? "bg-white text-black shadow-lg"
|
||||||
|
: "bg-white/5 text-white/40 hover:bg-white/10"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ImageIcon size={16} /> Bild
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentType("note")}
|
||||||
|
className={cn(
|
||||||
|
"flex-1 py-3 px-4 rounded-xl text-sm font-medium transition-all flex items-center justify-center gap-2",
|
||||||
|
currentType === "note"
|
||||||
|
? "bg-white text-black shadow-lg"
|
||||||
|
: "bg-white/5 text-white/40 hover:bg-white/10"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<StickyNote size={16} /> Notiz
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto min-h-[200px] mb-6 hide-scrollbar" style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}>
|
||||||
|
{currentType === "text" && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-bold uppercase tracking-wider text-white/40">Neuer Text</label>
|
||||||
|
<textarea
|
||||||
|
autoFocus
|
||||||
|
value={currentText}
|
||||||
|
onChange={(e) => setCurrentText(e.target.value)}
|
||||||
|
className="w-full h-32 bg-black/40 border border-white/10 rounded-2xl p-4 text-white placeholder:text-white/20 focus:outline-none focus:border-blue-500/50 transition-colors resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentType === "note" && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-bold uppercase tracking-wider text-white/40">Anmerkung</label>
|
||||||
|
<textarea
|
||||||
|
autoFocus
|
||||||
|
value={currentText}
|
||||||
|
onChange={(e) => setCurrentText(e.target.value)}
|
||||||
|
placeholder="Was soll hier gemacht werden?"
|
||||||
|
className="w-full h-32 bg-black/40 border border-white/10 rounded-2xl p-4 text-white placeholder:text-white/20 focus:outline-none focus:border-blue-500/50 transition-colors resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentType === "image" && (
|
||||||
|
<div className="space-y-2 h-full">
|
||||||
|
<label className="text-xs font-bold uppercase tracking-wider text-white/40">Neues Bild wählen</label>
|
||||||
|
{assets.length === 0 ? (
|
||||||
|
<div className="text-sm text-white/40 p-4 bg-white/5 rounded-xl border border-white/5 text-center">
|
||||||
|
Keine Assets übergeben. Bitte das prop <code>assets</code> befüllen.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<style>{`.hide-scrollbar::-webkit-scrollbar { display: none; }`}</style>
|
||||||
|
<div className="mb-3">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Suchen nach Dateinamen..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-3 text-sm text-white placeholder:text-white/40 focus:outline-none focus:border-blue-500/50 transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3 h-[400px] overflow-y-auto hide-scrollbar"
|
||||||
|
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
|
||||||
|
>
|
||||||
|
{assets.filter(asset => asset.toLowerCase().includes(searchTerm.toLowerCase())).map((asset) => {
|
||||||
|
const filename = asset.split('/').pop() || asset;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={asset}
|
||||||
|
title={asset}
|
||||||
|
onClick={() => setSelectedAsset(asset)}
|
||||||
|
className={cn(
|
||||||
|
"relative flex flex-col items-center bg-black/20 rounded-xl overflow-hidden border-2 transition-all p-2 gap-2 w-full",
|
||||||
|
selectedAsset === asset
|
||||||
|
? "border-blue-500 shadow-[0_0_0_4px_rgba(59,130,246,0.3)] bg-blue-500/10"
|
||||||
|
: "border-transparent hover:border-white/20 hover:bg-white/5"
|
||||||
|
)}
|
||||||
|
style={{ height: '130px' }}
|
||||||
|
>
|
||||||
|
<div className="w-full flex-1 rounded-lg overflow-hidden bg-black/40 flex items-center justify-center relative">
|
||||||
|
{asset.match(/\.(mp4|webm)$/i) ? (
|
||||||
|
<video src={asset} className="w-full h-full object-cover opacity-80" />
|
||||||
|
) : (
|
||||||
|
<img src={asset} loading="lazy" alt={filename} className="w-full h-full object-contain" />
|
||||||
|
)}
|
||||||
|
{selectedAsset === asset && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center bg-blue-500/20 backdrop-blur-[2px]">
|
||||||
|
<div className="bg-blue-500 text-white rounded-full p-1.5 shadow-lg">
|
||||||
|
<Check size={18} strokeWidth={3} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] font-medium text-white/70 truncate w-full text-center shrink-0" title={filename}>
|
||||||
|
{filename}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
disabled={(!currentText && currentType !== "image") || (currentType === "image" && !selectedAsset)}
|
||||||
|
onClick={saveAnnotation}
|
||||||
|
className="w-full bg-blue-500 hover:bg-blue-400 disabled:opacity-50 disabled:cursor-not-allowed text-white font-bold py-4 rounded-2xl flex items-center justify-center gap-2 transition-all shadow-lg shadow-blue-500/20 shrink-0"
|
||||||
|
>
|
||||||
|
<Check size={20} />
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* 4. Annotations List Sidebar */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showList && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
onClick={() => setShowList(false)}
|
||||||
|
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[10001]"
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
initial={{ x: "100%" }}
|
||||||
|
animate={{ x: 0 }}
|
||||||
|
exit={{ x: "100%" }}
|
||||||
|
transition={{ type: "spring", damping: 25, stiffness: 200 }}
|
||||||
|
className="fixed top-0 right-0 h-full w-full sm:w-[400px] bg-[#1c1c1e] border-l border-white/10 z-[10002] shadow-2xl flex flex-col"
|
||||||
|
>
|
||||||
|
<div className="p-8 border-b border-white/10 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold text-white mb-1">Korrekturen</h2>
|
||||||
|
<p className="text-white/40 text-sm">{annotations.length} erfasst</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowList(false)}
|
||||||
|
className="p-2 text-white/40 hover:text-white bg-white/5 rounded-xl transition-colors"
|
||||||
|
>
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto p-6 space-y-4">
|
||||||
|
{annotations.length === 0 ? (
|
||||||
|
<div className="h-full flex flex-col items-center justify-center text-center px-8 opacity-40">
|
||||||
|
<List size={48} className="mb-4" />
|
||||||
|
<p>Noch keine Korrekturen vorhanden.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
annotations.map((ann) => (
|
||||||
|
<div
|
||||||
|
key={ann.id}
|
||||||
|
className="bg-white/5 border border-white/5 rounded-2xl p-4 flex flex-col gap-3 group relative"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => deleteAnnotation(ann.id)}
|
||||||
|
className="absolute top-4 right-4 text-white/20 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className={cn(
|
||||||
|
"px-2 py-1 rounded-md text-[10px] font-bold uppercase tracking-wider flex items-center gap-1",
|
||||||
|
ann.type === "text" ? "bg-blue-500/20 text-blue-400" :
|
||||||
|
ann.type === "image" ? "bg-purple-500/20 text-purple-400" :
|
||||||
|
"bg-orange-500/20 text-orange-400"
|
||||||
|
)}>
|
||||||
|
{ann.type === "text" && <Type size={12} />}
|
||||||
|
{ann.type === "image" && <ImageIcon size={12} />}
|
||||||
|
{ann.type === "note" && <StickyNote size={12} />}
|
||||||
|
{ann.type}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ann.type === "image" ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<img src={ann.newContent} alt="New" className="w-16 h-16 object-cover rounded-lg bg-white/10" />
|
||||||
|
<div className="text-xs text-white/60 flex-1 truncate">{ann.newContent}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-white text-sm whitespace-pre-wrap">{ann.newContent}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="text-[10px] text-white/30 truncate mt-2 font-mono flex items-center gap-2">
|
||||||
|
<span>{ann.selector}</span>
|
||||||
|
{ann.url && <span className="truncate max-w-[100px] text-blue-400" title={ann.url}>{new URL(ann.url).pathname}</span>}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap items-center gap-2 mt-2 pt-2 border-t border-white/5">
|
||||||
|
{ann.timestamp && (
|
||||||
|
<span className="text-[9px] px-1.5 py-0.5 rounded bg-white/5 text-white/40">
|
||||||
|
{new Date(ann.timestamp).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{ann.viewportWidth && (
|
||||||
|
<span className="text-[9px] px-1.5 py-0.5 rounded bg-white/5 text-white/40">
|
||||||
|
{ann.viewportWidth}x{ann.viewportHeight}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{ann.scrollX !== undefined && (
|
||||||
|
<span className="text-[9px] px-1.5 py-0.5 rounded bg-white/5 text-white/40">
|
||||||
|
Scroll: {Math.round(ann.scrollX)},{Math.round(ann.scrollY)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{annotations.length > 0 && (
|
||||||
|
<div className="p-6 border-t border-white/10 flex flex-col gap-3">
|
||||||
|
{onSubmit && (
|
||||||
|
<button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="w-full bg-blue-500 hover:bg-blue-400 text-white font-bold py-4 rounded-2xl flex items-center justify-center gap-2 transition-all shadow-lg shadow-blue-500/20 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Check size={20} />
|
||||||
|
)}
|
||||||
|
{isSubmitting ? "Wird gesendet..." : "Korrekturen an Entwickler senden"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={exportJSON}
|
||||||
|
className={cn(
|
||||||
|
"w-full font-bold py-3 rounded-2xl flex items-center justify-center gap-2 transition-all",
|
||||||
|
onSubmit ? "bg-white/5 text-white/70 hover:bg-white/10 hover:text-white" : "bg-blue-500 hover:bg-blue-400 text-white shadow-lg shadow-blue-500/20 py-4"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Download size={onSubmit ? 18 : 20} />
|
||||||
|
JSON {onSubmit && "lokal "}Exportieren
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (confirm("Möchtest du wirklich alle erfassten Korrekturen löschen?")) {
|
||||||
|
setAnnotations([]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-full bg-red-500/10 text-red-500 hover:bg-red-500/20 font-bold py-3 rounded-xl flex items-center justify-center gap-2 transition-all mt-2"
|
||||||
|
>
|
||||||
|
<Trash2 size={18} />
|
||||||
|
Alle löschen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
components/annotator-local/index.ts
Normal file
1
components/annotator-local/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { Annotator } from "./Annotator";
|
||||||
@@ -140,7 +140,7 @@ export function CertificatesBlock({ badge, title, description, certificates = de
|
|||||||
variants={containerVariants}
|
variants={containerVariants}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
whileInView="show"
|
whileInView="show"
|
||||||
viewport={{ once: true, margin: "-50px" }}
|
viewport={{ once: true, amount: 0.1 }}
|
||||||
>
|
>
|
||||||
{certificates.map((cert, index) => {
|
{certificates.map((cert, index) => {
|
||||||
const isIso = cert.type === 'iso';
|
const isIso = cert.type === 'iso';
|
||||||
|
|||||||
@@ -81,12 +81,12 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
|
|||||||
return (
|
return (
|
||||||
<section className="py-12 md:py-24 bg-neutral text-neutral-dark relative overflow-hidden">
|
<section className="py-12 md:py-24 bg-neutral text-neutral-dark relative overflow-hidden">
|
||||||
<Container className="relative z-10 px-0 md:px-12 lg:px-16">
|
<Container className="relative z-10 px-0 md:px-12 lg:px-16">
|
||||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-end mb-12 px-4 md:px-0">
|
<div className="flex flex-row justify-between items-end mb-12 px-4 md:px-0">
|
||||||
<div className="max-w-2xl text-left">
|
<div className="max-w-2xl text-left">
|
||||||
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
|
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
|
||||||
<h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-4">{title}</h3>
|
<h3 className="font-heading text-3xl md:text-5xl font-extrabold text-neutral-dark mb-4">{title}</h3>
|
||||||
</div>
|
</div>
|
||||||
<Button href={ctaHref} variant="ghost" className="mt-6 md:mt-0 bg-primary/5 hover:bg-primary/10 mx-4 md:mx-0">
|
<Button href={ctaHref} variant="ghost" className="bg-primary/5 hover:bg-primary/10 mx-4 md:mx-0 whitespace-nowrap">
|
||||||
{ctaLabel}
|
{ctaLabel}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 ml-2 transform group-hover:translate-x-1 transition-transform" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 ml-2 transform group-hover:translate-x-1 transition-transform" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef, useCallback, useMemo } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { MapPin, Factory, Zap, CheckCircle2, ArrowUpRight } from 'lucide-react';
|
import { MapPin, CheckCircle2, ArrowUpRight } from 'lucide-react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useRouter, usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder';
|
import { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder';
|
||||||
import { Location, defaultLocations, minorLocations, projectLocations } from '@/lib/map-data';
|
import { Location, projectLocations } from '@/lib/map-data';
|
||||||
import { standorteLocations } from '@/lib/standorte-data';
|
import { standorteLocations } from '@/lib/standorte-data';
|
||||||
import { useLocale, useTranslations } from 'next-intl';
|
import { useLocale, useTranslations } from 'next-intl';
|
||||||
|
|
||||||
@@ -28,6 +28,59 @@ interface InteractiveGermanyMapProps {
|
|||||||
hideStandorte?: boolean;
|
hideStandorte?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MinorNode = React.memo(({ loc, isActive, onEnter, onLeave }: { loc: Location, isActive: boolean, onEnter: (loc: Location) => void, onLeave: () => void }) => (
|
||||||
|
<div
|
||||||
|
className={`absolute group/minor cursor-pointer w-8 h-8 md:w-5 md:h-5 flex items-center justify-center ${isActive ? 'z-[60]' : 'z-10 hover:z-30'}`}
|
||||||
|
style={{
|
||||||
|
left: `${loc.x}%`,
|
||||||
|
top: `${loc.y}%`,
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => onEnter(loc)}
|
||||||
|
onMouseLeave={onLeave}
|
||||||
|
onTouchStart={() => onEnter(loc)}
|
||||||
|
onClick={() => onEnter(loc)}
|
||||||
|
>
|
||||||
|
<div className={`w-1.5 h-1.5 rounded-full transition-all duration-300 ${isActive ? 'bg-white scale-150 shadow-[0_0_10px_rgba(130,237,32,1)]' : 'bg-primary/80 group-hover/minor:bg-white group-hover/minor:scale-150'}`} />
|
||||||
|
<div className="absolute inset-0 m-auto w-2 h-2 bg-primary rounded-full opacity-0 group-hover/minor:animate-ping" />
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
MinorNode.displayName = 'MinorNode';
|
||||||
|
|
||||||
|
const MajorNode = React.memo(({ loc, isActive, idx, onEnter, onLeave }: { loc: Location, isActive: boolean, idx: number, onEnter: (loc: Location) => void, onLeave: () => void }) => {
|
||||||
|
const isHQ = loc.type === 'hq';
|
||||||
|
const isBranch = loc.type === 'branch';
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`absolute transform -translate-x-1/2 -translate-y-1/2 group/pin cursor-pointer w-16 h-16 flex items-center justify-center ${isActive ? 'z-[60]' : 'z-20 hover:z-40'}`}
|
||||||
|
style={{ left: `${loc.x}%`, top: `${loc.y}%` }}
|
||||||
|
onMouseEnter={() => onEnter(loc)}
|
||||||
|
onMouseLeave={onLeave}
|
||||||
|
onTouchStart={() => onEnter(loc)}
|
||||||
|
onClick={() => onEnter(loc)}
|
||||||
|
>
|
||||||
|
{(isHQ || isBranch) && (
|
||||||
|
<div className="absolute inset-0 m-auto w-6 h-6 rounded-full animate-ping bg-primary/50 scale-150" />
|
||||||
|
)}
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ type: 'spring', delay: idx * 0.1 }}
|
||||||
|
className={`relative flex items-center justify-center rounded-full shadow-[0_0_20px_rgba(130,237,32,0.6)] transition-transform duration-300 ${
|
||||||
|
isActive ? 'scale-125 z-30 ring-4 ring-primary/40' : 'scale-100 group-hover/pin:scale-110'
|
||||||
|
} ${
|
||||||
|
isHQ || isBranch
|
||||||
|
? 'w-7 h-7 bg-primary text-[#050B14]'
|
||||||
|
: 'w-4 h-4 bg-white ring-[3px] ring-primary'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{(isHQ || isBranch) && <MapPin className="w-4 h-4" />}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
MajorNode.displayName = 'MajorNode';
|
||||||
|
|
||||||
export function InteractiveGermanyMap({
|
export function InteractiveGermanyMap({
|
||||||
badge,
|
badge,
|
||||||
title,
|
title,
|
||||||
@@ -40,22 +93,21 @@ export function InteractiveGermanyMap({
|
|||||||
const [activeLocation, setActiveLocation] = useState<Location | null>(null);
|
const [activeLocation, setActiveLocation] = useState<Location | null>(null);
|
||||||
const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const handleMouseEnter = (loc: Location) => {
|
const handleMouseEnter = useCallback((loc: Location) => {
|
||||||
if (hoverTimeoutRef.current) {
|
if (hoverTimeoutRef.current) {
|
||||||
clearTimeout(hoverTimeoutRef.current);
|
clearTimeout(hoverTimeoutRef.current);
|
||||||
hoverTimeoutRef.current = null;
|
hoverTimeoutRef.current = null;
|
||||||
}
|
}
|
||||||
setActiveLocation(loc);
|
setActiveLocation(loc);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
const handleMouseLeave = useCallback(() => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
setActiveLocation(null);
|
setActiveLocation(null);
|
||||||
}, 200);
|
}, 200);
|
||||||
hoverTimeoutRef.current = timeout;
|
hoverTimeoutRef.current = timeout;
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
const t = useTranslations('InteractiveGermanyMap');
|
const t = useTranslations('InteractiveGermanyMap');
|
||||||
@@ -67,14 +119,11 @@ export function InteractiveGermanyMap({
|
|||||||
];
|
];
|
||||||
|
|
||||||
const finalBadge = badge || tStandard('badge');
|
const finalBadge = badge || tStandard('badge');
|
||||||
// the map is mostly used with specific props, so we leave title & description to fallbacks if not provided
|
|
||||||
const finalTitle = title || (locale === 'en' ? <>Nationwide<br/>in operation for you.</> : <>Deutschlandweit<br/>für Sie im Einsatz.</>);
|
const finalTitle = title || (locale === 'en' ? <>Nationwide<br/>in operation for you.</> : <>Deutschlandweit<br/>für Sie im Einsatz.</>);
|
||||||
const finalDescription = description || (locale === 'en' ? 'From our strategic locations in Guben, Kirchheilingen and Bülstedt, we control and implement complex infrastructure projects nationwide.' : 'Von unseren strategischen Standorten in Guben, Kirchheilingen und Bülstedt steuern und realisieren wir komplexe Infrastrukturprojekte im gesamten Bundesgebiet.');
|
const finalDescription = description || (locale === 'en' ? 'From our strategic locations in Guben, Kirchheilingen and Bülstedt, we control and implement complex infrastructure projects nationwide.' : 'Von unseren strategischen Standorten in Guben, Kirchheilingen und Bülstedt steuern und realisieren wir komplexe Infrastrukturprojekte im gesamten Bundesgebiet.');
|
||||||
|
|
||||||
const finalLocations = React.useMemo(() => {
|
const finalLocations = useMemo(() => {
|
||||||
if (hideStandorte) return locations;
|
if (hideStandorte) return locations;
|
||||||
|
|
||||||
// Ensure the 3 standorte are always included, without duplicating if already present by ID
|
|
||||||
const base = [...locations];
|
const base = [...locations];
|
||||||
standorteLocations.forEach(sl => {
|
standorteLocations.forEach(sl => {
|
||||||
if (!base.some(l => l.id === sl.id)) {
|
if (!base.some(l => l.id === sl.id)) {
|
||||||
@@ -84,19 +133,28 @@ export function InteractiveGermanyMap({
|
|||||||
return base;
|
return base;
|
||||||
}, [locations, hideStandorte]);
|
}, [locations, hideStandorte]);
|
||||||
|
|
||||||
|
const mapBackground = useMemo(() => (
|
||||||
|
<div className="absolute inset-0 opacity-[0.25] mix-blend-screen drop-shadow-2xl brightness-200 pointer-events-none transform-gpu">
|
||||||
|
<Image
|
||||||
|
src="/germany-map.svg"
|
||||||
|
alt={locale === 'en' ? "Map of Germany" : "Deutschlandkarte"}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
), [locale]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={isHero ? `hero-map-${pathname}` : undefined} className={isHero ? "relative w-full" : "relative w-full max-w-7xl mx-auto py-12 md:py-24 px-4 sm:px-6 mt-16 md:mt-20"}>
|
<div key={isHero ? `hero-map-${pathname}` : undefined} className={isHero ? "relative w-full" : "relative w-full max-w-7xl mx-auto py-12 md:py-24 px-4 sm:px-6 mt-16 md:mt-20"}>
|
||||||
<div className={`bg-[#050B14] ${isHero ? 'pt-32 pb-16 md:pt-40 md:pb-24' : 'rounded-[2.5rem] md:rounded-[3.5rem] shadow-2xl border border-white/5'} overflow-visible relative group/map`}>
|
<div className={`bg-[#050B14] ${isHero ? 'pt-32 pb-16 md:pt-40 md:pb-24' : 'rounded-[2.5rem] md:rounded-[3.5rem] shadow-2xl border border-white/5'} overflow-visible relative group/map`}>
|
||||||
{/* Animated Border Glow */}
|
|
||||||
{!isHero && <AnimatedGlossyBorder opacity={0.7} className="z-30" />}
|
{!isHero && <AnimatedGlossyBorder opacity={0.7} className="z-30" />}
|
||||||
|
|
||||||
{/* Background Effects */}
|
|
||||||
<div className={`absolute inset-0 bg-gradient-to-br from-[#050B14] via-[#0A1322] to-[#050B14] ${!isHero && 'rounded-[2.5rem] md:rounded-[3.5rem]'} overflow-hidden`} />
|
<div className={`absolute inset-0 bg-gradient-to-br from-[#050B14] via-[#0A1322] to-[#050B14] ${!isHero && 'rounded-[2.5rem] md:rounded-[3.5rem]'} overflow-hidden`} />
|
||||||
<div className="absolute top-0 right-0 w-[800px] h-[800px] bg-primary/5 rounded-full blur-[120px] pointer-events-none translate-x-1/3 -translate-y-1/3 overflow-hidden" />
|
<div className="absolute top-0 right-0 w-[800px] h-[800px] bg-primary/5 rounded-full blur-[120px] pointer-events-none translate-x-1/3 -translate-y-1/3 overflow-hidden" />
|
||||||
|
|
||||||
<div className={`relative z-10 grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-12 gap-12 items-center min-h-[600px] ${isHero ? 'max-w-7xl mx-auto px-4 sm:px-6' : 'p-8 md:p-12 lg:p-16'}`}>
|
<div className={`relative z-10 grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-12 gap-12 items-center min-h-[600px] ${isHero ? 'max-w-7xl mx-auto px-4 sm:px-6' : 'p-8 md:p-12 lg:p-16'}`}>
|
||||||
|
|
||||||
{/* Content Left */}
|
|
||||||
<div className="xl:col-span-5 text-white z-20">
|
<div className="xl:col-span-5 text-white z-20">
|
||||||
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-primary/10 border border-primary/20 text-primary text-xs font-bold uppercase tracking-wider mb-8">
|
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-primary/10 border border-primary/20 text-primary text-xs font-bold uppercase tracking-wider mb-8">
|
||||||
<MapPin className="w-4 h-4" />
|
<MapPin className="w-4 h-4" />
|
||||||
@@ -117,7 +175,6 @@ export function InteractiveGermanyMap({
|
|||||||
{finalDescription}
|
{finalDescription}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Industrial Stats Grid */}
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{finalStats.map((stat, i) => {
|
{finalStats.map((stat, i) => {
|
||||||
const StatCard = () => (
|
const StatCard = () => (
|
||||||
@@ -148,85 +205,32 @@ export function InteractiveGermanyMap({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Map Right */}
|
|
||||||
<div className="xl:col-span-7 relative flex items-center justify-center min-h-[500px] lg:min-h-[700px] z-10 pointer-events-none">
|
<div className="xl:col-span-7 relative flex items-center justify-center min-h-[500px] lg:min-h-[700px] z-10 pointer-events-none">
|
||||||
{/* Map Container - Enforce strict aspect ratio matching the SVG (1024x1024) */}
|
|
||||||
<div className="relative w-full max-w-[600px] aspect-square group pointer-events-auto">
|
<div className="relative w-full max-w-[600px] aspect-square group pointer-events-auto">
|
||||||
|
|
||||||
{/* Map SVG */}
|
{mapBackground}
|
||||||
<div className="absolute inset-0 opacity-[0.25] mix-blend-screen drop-shadow-2xl brightness-200 pointer-events-none">
|
|
||||||
<Image
|
{finalLocations.filter(l => l.type === 'minor_node').map((loc, idx) => (
|
||||||
src="/germany-map.svg"
|
<MinorNode
|
||||||
alt={locale === 'en' ? "Map of Germany" : "Deutschlandkarte"}
|
key={`minor-${loc.id || idx}`}
|
||||||
fill
|
loc={loc}
|
||||||
className="object-cover"
|
isActive={activeLocation === loc}
|
||||||
priority
|
onEnter={handleMouseEnter}
|
||||||
|
onLeave={handleMouseLeave}
|
||||||
/>
|
/>
|
||||||
</div>
|
))}
|
||||||
|
|
||||||
{/* Minor Location Markers (The 100+ generic projects) */}
|
{finalLocations.filter(l => l.type !== 'minor_node').map((loc, idx) => (
|
||||||
{finalLocations.filter(l => l.type === 'minor_node').map((loc, idx) => {
|
<MajorNode
|
||||||
const isActive = activeLocation === loc;
|
key={loc.id}
|
||||||
return (
|
loc={loc}
|
||||||
<div
|
idx={idx}
|
||||||
key={`minor-${idx}`}
|
isActive={activeLocation?.id === loc.id}
|
||||||
className={`absolute group/minor cursor-pointer w-8 h-8 md:w-5 md:h-5 flex items-center justify-center ${isActive ? 'z-[60]' : 'z-10 hover:z-30'}`}
|
onEnter={handleMouseEnter}
|
||||||
style={{
|
onLeave={handleMouseLeave}
|
||||||
left: `${loc.x}%`,
|
/>
|
||||||
top: `${loc.y}%`,
|
))}
|
||||||
transform: 'translate(-50%, -50%)',
|
|
||||||
}}
|
|
||||||
onMouseEnter={() => handleMouseEnter(loc)}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
onTouchStart={() => handleMouseEnter(loc)}
|
|
||||||
onClick={() => handleMouseEnter(loc)}
|
|
||||||
>
|
|
||||||
<div className={`w-1.5 h-1.5 rounded-full transition-all duration-300 ${isActive ? 'bg-white scale-150 shadow-[0_0_10px_rgba(130,237,32,1)]' : 'bg-primary/80 group-hover/minor:bg-white group-hover/minor:scale-150'}`} />
|
|
||||||
<div className="absolute inset-0 m-auto w-2 h-2 bg-primary rounded-full opacity-0 group-hover/minor:animate-ping" />
|
|
||||||
</div>
|
|
||||||
)})}
|
|
||||||
|
|
||||||
{/* Major Location Markers (HQ, Branch, Project) */}
|
|
||||||
{finalLocations.filter(l => l.type !== 'minor_node').map((loc, idx) => {
|
|
||||||
const isHQ = loc.type === 'hq';
|
|
||||||
const isBranch = loc.type === 'branch';
|
|
||||||
const isActive = activeLocation?.id === loc.id;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={loc.id}
|
|
||||||
className={`absolute transform -translate-x-1/2 -translate-y-1/2 group/pin cursor-pointer w-16 h-16 flex items-center justify-center ${isActive ? 'z-[60]' : 'z-20 hover:z-40'}`}
|
|
||||||
style={{ left: `${loc.x}%`, top: `${loc.y}%` }}
|
|
||||||
onMouseEnter={() => handleMouseEnter(loc)}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
onTouchStart={() => handleMouseEnter(loc)}
|
|
||||||
onClick={() => handleMouseEnter(loc)}
|
|
||||||
>
|
|
||||||
{/* Ping Animation for HQ / Branch */}
|
|
||||||
{(isHQ || isBranch) && (
|
|
||||||
<div className="absolute inset-0 m-auto w-6 h-6 rounded-full animate-ping bg-primary/50 scale-150" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Elegant Marker Dot */}
|
|
||||||
<motion.div
|
|
||||||
initial={{ scale: 0 }}
|
|
||||||
animate={{ scale: 1 }}
|
|
||||||
transition={{ type: 'spring', delay: idx * 0.1 }}
|
|
||||||
className={`relative flex items-center justify-center rounded-full shadow-[0_0_20px_rgba(130,237,32,0.6)] transition-transform duration-300 ${
|
|
||||||
isActive ? 'scale-125 z-30 ring-4 ring-primary/40' : 'scale-100 group-hover/pin:scale-110'
|
|
||||||
} ${
|
|
||||||
isHQ || isBranch
|
|
||||||
? 'w-7 h-7 bg-primary text-[#050B14]'
|
|
||||||
: 'w-4 h-4 bg-white ring-[3px] ring-primary'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{(isHQ || isBranch) && <MapPin className="w-4 h-4" />}
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* Global Tooltip */}
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{activeLocation && (
|
{activeLocation && (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -235,17 +239,16 @@ export function InteractiveGermanyMap({
|
|||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
className="absolute z-[9999] pointer-events-auto"
|
className="absolute z-[9999] pointer-events-auto bottom-[-60px] left-1/2 -translate-x-1/2 w-[calc(100vw-2rem)] max-w-[320px] md:w-max md:bottom-auto md:left-[var(--md-left)] md:top-[var(--md-top)] md:[transform:var(--md-transform)]"
|
||||||
style={{
|
style={{
|
||||||
left: `${activeLocation.x}%`,
|
'--md-left': `${activeLocation.x}%`,
|
||||||
top: `${activeLocation.y}%`,
|
'--md-top': `${activeLocation.y}%`,
|
||||||
transform: activeLocation.x < 50 ? 'translate(20px, -50%)' : 'translate(calc(-100% - 20px), -50%)',
|
'--md-transform': activeLocation.x < 50 ? 'translate(20px, -50%)' : 'translate(calc(-100% - 20px), -50%)',
|
||||||
}}
|
} as React.CSSProperties}
|
||||||
onMouseEnter={() => handleMouseEnter(activeLocation)}
|
onMouseEnter={() => handleMouseEnter(activeLocation)}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
>
|
>
|
||||||
<div className="bg-[#050B14]/95 backdrop-blur-xl border border-white/10 rounded-2xl shadow-2xl min-w-[200px] w-max max-w-[300px] text-white overflow-hidden">
|
<div className="bg-[#050B14]/95 backdrop-blur-xl border border-white/10 rounded-2xl shadow-2xl w-full text-white overflow-hidden">
|
||||||
|
|
||||||
{activeLocation.featuredImage && (
|
{activeLocation.featuredImage && (
|
||||||
<div className="relative w-full h-32 bg-neutral-900">
|
<div className="relative w-full h-32 bg-neutral-900">
|
||||||
<Image
|
<Image
|
||||||
|
|||||||
@@ -97,12 +97,12 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
|
|||||||
<section id="referenzen" className="py-16 md:py-24 lg:py-32 bg-neutral-dark text-white relative overflow-hidden">
|
<section id="referenzen" className="py-16 md:py-24 lg:py-32 bg-neutral-dark text-white relative overflow-hidden">
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-neutral-dark via-neutral-900 to-neutral-dark z-0" />
|
<div className="absolute inset-0 bg-gradient-to-b from-neutral-dark via-neutral-900 to-neutral-dark z-0" />
|
||||||
|
|
||||||
<div className="container relative z-10 mb-12 flex flex-col md:flex-row justify-between items-end gap-6">
|
<div className="container relative z-10 mb-12 flex flex-col md:flex-row justify-between items-start md:items-end gap-6">
|
||||||
<div>
|
<div className="text-left">
|
||||||
<h2 className="text-primary-light font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
|
<h2 className="text-primary-light font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
|
||||||
<h3 className="font-heading text-3xl md:text-5xl font-extrabold">{title}</h3>
|
<h3 className="font-heading text-3xl md:text-5xl font-extrabold">{title}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center md:text-left max-w-3xl mx-auto md:mx-0 mb-16">
|
<div className="text-left max-w-3xl mx-auto md:mx-0 mb-8 md:mb-16">
|
||||||
{description}
|
{description}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -131,16 +131,8 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
|
|||||||
transition={{ delay: i * 0.1, duration: 0.6, ease: "easeOut" }}
|
transition={{ delay: i * 0.1, duration: 0.6, ease: "easeOut" }}
|
||||||
className="flex-shrink-0 w-[320px] md:w-[480px] snap-center group pointer-events-auto"
|
className="flex-shrink-0 w-[320px] md:w-[480px] snap-center group pointer-events-auto"
|
||||||
>
|
>
|
||||||
<Link
|
<div
|
||||||
href={`/${locale}/referenzen/${ref.slug}`}
|
|
||||||
data-testid="reference-tile"
|
data-testid="reference-tile"
|
||||||
draggable={false}
|
|
||||||
onClick={(e) => {
|
|
||||||
if (dragDistanceRef.current > 15) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
dragDistanceRef.current = 0;
|
|
||||||
}}
|
|
||||||
className="block relative select-none aspect-[16/10] bg-neutral-800 rounded-2xl overflow-hidden mb-5 border border-white/5 shadow-2xl"
|
className="block relative select-none aspect-[16/10] bg-neutral-800 rounded-2xl overflow-hidden mb-5 border border-white/5 shadow-2xl"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
@@ -161,7 +153,7 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
|
|||||||
</span>
|
</span>
|
||||||
<h4 className="font-heading text-xl md:text-2xl font-bold leading-tight break-words">{ref.title}</h4>
|
<h4 className="font-heading text-xl md:text-2xl font-bold leading-tight break-words">{ref.title}</h4>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ export function ServiceDetailGrid({
|
|||||||
{/* On a white card, a very subtle primary/grey shine works best */}
|
{/* On a white card, a very subtle primary/grey shine works best */}
|
||||||
<HoverShineOverlay shineColor="via-primary/5" />
|
<HoverShineOverlay shineColor="via-primary/5" />
|
||||||
|
|
||||||
<div className="relative z-10 flex flex-col md:flex-row justify-between items-start mb-12 gap-6">
|
<div className="relative z-10 flex flex-row justify-between items-start mb-12 gap-6">
|
||||||
<div className="pt-2 flex-1 min-w-0 pr-4">
|
<div className="pt-2 flex-1 min-w-0 pr-4">
|
||||||
<h4 className="text-lg md:text-2xl font-extrabold text-neutral-dark uppercase tracking-wider text-balance break-words" lang="de">{panel.title}</h4>
|
<h4 className="text-lg md:text-2xl font-extrabold text-neutral-dark uppercase tracking-wider text-balance break-words" lang="de">{panel.title}</h4>
|
||||||
<div className="h-1 w-12 bg-primary mt-6 transition-all duration-700 group-hover:w-24 rounded-full"></div>
|
<div className="h-1 w-12 bg-primary mt-6 transition-all duration-700 group-hover:w-24 rounded-full"></div>
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
|
|||||||
variants={containerVariants}
|
variants={containerVariants}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
whileInView="visible"
|
whileInView="visible"
|
||||||
viewport={{ once: true, margin: "-50px" }}
|
viewport={{ once: true, amount: 0.1 }}
|
||||||
>
|
>
|
||||||
{companiesData.map((company: any, index: number) => {
|
{companiesData.map((company: any, index: number) => {
|
||||||
const isCurrent = company.title.toUpperCase() === 'E-TIB GMBH';
|
const isCurrent = company.title.toUpperCase() === 'E-TIB GMBH';
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
import {
|
|
||||||
Body,
|
|
||||||
Container,
|
|
||||||
Head,
|
|
||||||
Heading,
|
|
||||||
Hr,
|
|
||||||
Html,
|
|
||||||
Preview,
|
|
||||||
Section,
|
|
||||||
Text,
|
|
||||||
Button,
|
|
||||||
} from '@react-email/components';
|
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
interface BrochureDeliveryEmailProps {
|
|
||||||
_email: string;
|
|
||||||
brochureUrl: string;
|
|
||||||
locale: 'en' | 'de';
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BrochureDeliveryEmail = ({
|
|
||||||
_email,
|
|
||||||
brochureUrl,
|
|
||||||
locale = 'en',
|
|
||||||
}: BrochureDeliveryEmailProps) => {
|
|
||||||
const t =
|
|
||||||
locale === 'de'
|
|
||||||
? {
|
|
||||||
subject: 'Ihr E-TIB Kabelkatalog',
|
|
||||||
greeting: 'Vielen Dank für Ihr Interesse an E-TIB.',
|
|
||||||
body: 'Anbei erhalten Sie den Link zu unserem aktuellen Produktkatalog. Dieser enthält alle wichtigen technischen Spezifikationen und detaillierten Produktdaten.',
|
|
||||||
button: 'Katalog herunterladen',
|
|
||||||
footer: 'Diese E-Mail wurde von e-tib.com gesendet.',
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
subject: 'Your E-TIB Cable Catalog',
|
|
||||||
greeting: 'Thank you for your interest in E-TIB.',
|
|
||||||
body: 'Below you will find the link to our current product catalog. It contains all key technical specifications and detailed product data.',
|
|
||||||
button: 'Download Catalog',
|
|
||||||
footer: 'This email was sent from e-tib.com.',
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Html>
|
|
||||||
<Head />
|
|
||||||
<Preview>{t.subject}</Preview>
|
|
||||||
<Body style={main}>
|
|
||||||
<Container style={container}>
|
|
||||||
<Section style={headerSection}>
|
|
||||||
<Heading style={h1}>{t.subject}</Heading>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section style={section}>
|
|
||||||
<Text style={text}>
|
|
||||||
<strong>{t.greeting}</strong>
|
|
||||||
</Text>
|
|
||||||
<Text style={text}>{t.body}</Text>
|
|
||||||
|
|
||||||
<Section style={buttonContainer}>
|
|
||||||
<Button style={button} href={brochureUrl}>
|
|
||||||
{t.button}
|
|
||||||
</Button>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Hr style={hr} />
|
|
||||||
</Section>
|
|
||||||
<Text style={footer}>{t.footer}</Text>
|
|
||||||
</Container>
|
|
||||||
</Body>
|
|
||||||
</Html>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BrochureDeliveryEmail;
|
|
||||||
|
|
||||||
const main = {
|
|
||||||
backgroundColor: '#f6f9fc',
|
|
||||||
fontFamily:
|
|
||||||
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
|
|
||||||
};
|
|
||||||
|
|
||||||
const container = {
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
margin: '0 auto',
|
|
||||||
padding: '0 0 48px',
|
|
||||||
marginBottom: '64px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
overflow: 'hidden',
|
|
||||||
border: '1px solid #e6ebf1',
|
|
||||||
};
|
|
||||||
|
|
||||||
const headerSection = {
|
|
||||||
backgroundColor: '#111111',
|
|
||||||
padding: '32px 48px',
|
|
||||||
borderBottom: '4px solid #117c61',
|
|
||||||
};
|
|
||||||
|
|
||||||
const h1 = {
|
|
||||||
color: '#ffffff',
|
|
||||||
fontSize: '24px',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
margin: '0',
|
|
||||||
};
|
|
||||||
|
|
||||||
const section = {
|
|
||||||
padding: '32px 48px 0',
|
|
||||||
};
|
|
||||||
|
|
||||||
const text = {
|
|
||||||
color: '#333',
|
|
||||||
fontSize: '16px',
|
|
||||||
lineHeight: '24px',
|
|
||||||
textAlign: 'left' as const,
|
|
||||||
};
|
|
||||||
|
|
||||||
const buttonContainer = {
|
|
||||||
textAlign: 'center' as const,
|
|
||||||
marginTop: '32px',
|
|
||||||
marginBottom: '32px',
|
|
||||||
};
|
|
||||||
|
|
||||||
const button = {
|
|
||||||
backgroundColor: '#117c61',
|
|
||||||
borderRadius: '4px',
|
|
||||||
color: '#ffffff',
|
|
||||||
fontSize: '16px',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
textDecoration: 'none',
|
|
||||||
textAlign: 'center' as const,
|
|
||||||
display: 'inline-block',
|
|
||||||
padding: '16px 32px',
|
|
||||||
};
|
|
||||||
|
|
||||||
const hr = {
|
|
||||||
borderColor: '#e6ebf1',
|
|
||||||
margin: '20px 0',
|
|
||||||
};
|
|
||||||
|
|
||||||
const footer = {
|
|
||||||
color: '#8898aa',
|
|
||||||
fontSize: '12px',
|
|
||||||
lineHeight: '16px',
|
|
||||||
textAlign: 'center' as const,
|
|
||||||
marginTop: '20px',
|
|
||||||
};
|
|
||||||
@@ -19,7 +19,7 @@ export function Footer({ companyInfo }: FooterProps) {
|
|||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="bg-[#050B14] text-neutral-light pt-40 pb-10 md:pt-40 md:pb-16 relative overflow-hidden border-t border-white/5">
|
<footer className="bg-[#050B14] text-neutral-light pt-24 pb-28 md:pt-32 md:pb-16 relative overflow-hidden border-t border-white/5">
|
||||||
{/* Subtle background tech grid */}
|
{/* Subtle background tech grid */}
|
||||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#8080800a_1px,transparent_1px),linear-gradient(to_bottom,#8080800a_1px,transparent_1px)] bg-[size:32px_32px] [mask-image:radial-gradient(ellipse_80%_50%_at_50%_0%,#000_70%,transparent_100%)] pointer-events-none" />
|
<div className="absolute inset-0 bg-[linear-gradient(to_right,#8080800a_1px,transparent_1px),linear-gradient(to_bottom,#8080800a_1px,transparent_1px)] bg-[size:32px_32px] [mask-image:radial-gradient(ellipse_80%_50%_at_50%_0%,#000_70%,transparent_100%)] pointer-events-none" />
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ export function Footer({ companyInfo }: FooterProps) {
|
|||||||
<p className="text-neutral-400 text-base leading-relaxed max-w-sm">
|
<p className="text-neutral-400 text-base leading-relaxed max-w-sm">
|
||||||
{locale === 'de'
|
{locale === 'de'
|
||||||
? 'Die Experten für Kabelnetzbau, Horizontalspülbohrungen und umfassende Infrastrukturprojekte. Qualität und Zuverlässigkeit aus Guben.'
|
? 'Die Experten für Kabelnetzbau, Horizontalspülbohrungen und umfassende Infrastrukturprojekte. Qualität und Zuverlässigkeit aus Guben.'
|
||||||
: 'Experts in cable construction, horizontal directional drilling, and comprehensive infrastructure projects. Quality and reliability from Guben.'}
|
: 'Experts in cable network construction, horizontal directional drilling, and comprehensive infrastructure projects. Quality and reliability from Guben.'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export function MobileBottomNav({ navLinks, currentLocale }: MobileBottomNavProp
|
|||||||
}, [currentLocale, navLinks]);
|
}, [currentLocale, navLinks]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="md:hidden fixed bottom-0 left-0 right-0 z-50 px-2 pb-6 pt-2 bg-gradient-to-t from-white via-white to-white/90 backdrop-blur-xl border-t border-neutral-200/50 shadow-[0_-8px_32px_rgba(0,0,0,0.05)] safe-area-bottom">
|
<div className="md:hidden fixed bottom-0 left-0 right-0 z-50 px-2 pb-2 pt-2 bg-gradient-to-t from-white via-white to-white/90 backdrop-blur-xl border-t border-neutral-200/50 shadow-[0_-8px_32px_rgba(0,0,0,0.05)] safe-area-bottom">
|
||||||
<nav className="flex justify-around items-center max-w-md mx-auto">
|
<nav className="flex justify-around items-center max-w-md mx-auto">
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
// Determine if active
|
// Determine if active
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
router.push(href);
|
router.push(href);
|
||||||
|
|
||||||
// Fallback: If for some reason the route doesn't change after 5 seconds, open the shutter anyway
|
// Fallback: If for some reason the route doesn't change after 60 seconds (useful for slow dev servers), open the shutter anyway
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsTransitioning((current) => {
|
setIsTransitioning((current) => {
|
||||||
if (current) {
|
if (current) {
|
||||||
@@ -69,7 +69,7 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
return current;
|
return current;
|
||||||
});
|
});
|
||||||
}, 5000);
|
}, 60000);
|
||||||
}, 700);
|
}, 700);
|
||||||
}, [isTransitioning, router]);
|
}, [isTransitioning, router]);
|
||||||
|
|
||||||
|
|||||||
BIN
contact_sheet.jpg
Normal file
BIN
contact_sheet.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: "Kabelleitungstiefbau im Bereich Telekommunikation"
|
title: "Kabelleitungsnetzbau im Bereich Telekommunikation"
|
||||||
date: "2024-03-20"
|
date: "2024-03-20"
|
||||||
excerpt: "Zukunftssichere Infrastruktur durch kompetenten Kabelleitungstiefbau für Glasfaser- und Telekommunikationsnetze (FTTX)."
|
excerpt: "Zukunftssichere Infrastruktur durch kompetenten Kabelleitungsnetzbau für Glasfaser- und Telekommunikationsnetze (FTTX)."
|
||||||
layout: "fullBleed"
|
layout: "fullBleed"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -117,12 +117,12 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
|
|||||||
ctaHref="/de/kompetenzen"
|
ctaHref="/de/kompetenzen"
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
title: "Kabelleitungstiefbau",
|
title: "Kabelleitungsnetzbau",
|
||||||
description: "Kabelleitungstiefbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen bis 110 kV",
|
description: "Kabelleitungsnetzbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen bis 110 kV",
|
||||||
tag: "Energie",
|
tag: "Energie",
|
||||||
size: "large",
|
size: "large",
|
||||||
href: "/de/kabelnetzbau",
|
href: "/de/kabeltiefbau",
|
||||||
image: { url: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-54.jpg", alt: "Kabelleitungstiefbau" }
|
image: { url: "/assets/photos/DSC01123.JPG", alt: "Kabelleitungsnetzbau" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Bohrtechnik",
|
title: "Bohrtechnik",
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
---
|
---
|
||||||
title: "Kabelleitungstiefbau"
|
title: "Kabelleitungsnetzbau"
|
||||||
date: "2024-03-20"
|
date: "2024-03-20"
|
||||||
excerpt: "Professioneller Kabelleitungstiefbau (Hoch-, Mittel- und Niederspannung) und Kabelmontagen bis 110 kV. Wir garantieren höchste Sicherheitsstandards und lückenlose Dokumentation für Ihr Projekt."
|
excerpt: "Professioneller Kabelleitungsnetzbau (Hoch-, Mittel- und Niederspannung) und Kabelmontagen bis 110 kV. Wir garantieren höchste Sicherheitsstandards und lückenlose Dokumentation für Ihr Projekt."
|
||||||
layout: "fullBleed"
|
layout: "fullBleed"
|
||||||
---
|
---
|
||||||
|
|
||||||
<HeroSection
|
<HeroSection
|
||||||
badge="Kernkompetenz"
|
badge="Kernkompetenz"
|
||||||
title="Kabelleitungstiefbau"
|
title="Kabelleitungsnetzbau"
|
||||||
subtitle="Komplette Infrastruktur-Lösungen für Hoch-, Mittel- und Niederspannungsnetze sowie Kabelmontagen bis 110 kV"
|
subtitle="Komplette Infrastruktur-Lösungen für Hoch-, Mittel- und Niederspannungsnetze sowie Kabelmontagen bis 110 kV"
|
||||||
backgroundImage={{ url: '/assets/photos/DSC01123.JPG' }}
|
backgroundImage={{ url: '/assets/photos/DSC01123.JPG' }}
|
||||||
alignment="center"
|
alignment="center"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ layout: "fullBleed"
|
|||||||
<HomeHero
|
<HomeHero
|
||||||
badge="Unsere Leistungen"
|
badge="Unsere Leistungen"
|
||||||
title="Alles aus einer Hand"
|
title="Alles aus einer Hand"
|
||||||
description="Als Full-Service-Partner im Bereich Kabelleitungstiefbau decken wir das gesamte Spektrum moderner Infrastrukturprojekte ab."
|
description="Als Full-Service-Partner im Bereich Kabelleitungsnetzbau decken wir das gesamte Spektrum moderner Infrastrukturprojekte ab."
|
||||||
videoUrl="/assets/videos/web/hero-bohrung.mp4"
|
videoUrl="/assets/videos/web/hero-bohrung.mp4"
|
||||||
linkText="Kontakt aufnehmen"
|
linkText="Kontakt aufnehmen"
|
||||||
linkHref="/de/kontakt"
|
linkHref="/de/kontakt"
|
||||||
@@ -23,12 +23,12 @@ layout: "fullBleed"
|
|||||||
ctaHref="/de/kontakt"
|
ctaHref="/de/kontakt"
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
title: "Kabelleitungstiefbau",
|
title: "Kabelleitungsnetzbau",
|
||||||
description: "Klassischer Grabenbau, professionelle Verlegung und Kabelmontage (Hoch-, Mittel- und Niederspannung) bis 110 kV.",
|
description: "Klassischer Grabenbau, professionelle Verlegung und Kabelmontage (Hoch-, Mittel- und Niederspannung) bis 110 kV.",
|
||||||
tag: "Energie",
|
tag: "Energie",
|
||||||
size: "large",
|
size: "large",
|
||||||
href: "/de/kabelnetzbau",
|
href: "/de/kabeltiefbau",
|
||||||
image: { url: "/assets/photos/DSC01123.JPG", alt: "Kabelleitungstiefbau" }
|
image: { url: "/assets/photos/DSC01123.JPG", alt: "Kabelleitungsnetzbau" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Bohrtechnik",
|
title: "Bohrtechnik",
|
||||||
@@ -36,7 +36,7 @@ layout: "fullBleed"
|
|||||||
tag: "Innovation",
|
tag: "Innovation",
|
||||||
size: "medium",
|
size: "medium",
|
||||||
href: "/de/bohrtechnik",
|
href: "/de/bohrtechnik",
|
||||||
image: { url: "/assets/photos/DSC08653.JPG", alt: "Bohrtechnik" }
|
image: { url: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-47.jpg", alt: "Bohrtechnik" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Glasfaser",
|
title: "Glasfaser",
|
||||||
@@ -70,7 +70,7 @@ layout: "fullBleed"
|
|||||||
panels={[
|
panels={[
|
||||||
{
|
{
|
||||||
id: "energy",
|
id: "energy",
|
||||||
title: "Kabelleitungstiefbau (Hoch-/Mittel- und Niederspannung)",
|
title: "Kabelleitungsnetzbau (Hoch-/Mittel- und Niederspannung)",
|
||||||
icon: "energy",
|
icon: "energy",
|
||||||
lists: [
|
lists: [
|
||||||
[
|
[
|
||||||
@@ -86,7 +86,7 @@ layout: "fullBleed"
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "fiber",
|
id: "fiber",
|
||||||
title: "Kabelleitungstiefbau (Telekommunikation)",
|
title: "Kabelleitungsnetzbau (Telekommunikation)",
|
||||||
icon: "fiber",
|
icon: "fiber",
|
||||||
lists: [
|
lists: [
|
||||||
[
|
[
|
||||||
@@ -143,7 +143,7 @@ layout: "fullBleed"
|
|||||||
<JsonLd data={[
|
<JsonLd data={[
|
||||||
{
|
{
|
||||||
"@type": "Service",
|
"@type": "Service",
|
||||||
"name": "Kabelleitungstiefbau",
|
"name": "Kabelleitungsnetzbau",
|
||||||
"description": "Klassischer Grabenbau, professionelle Verlegung und Kabelmontage (Hoch-, Mittel- und Niederspannung) bis 110 kV.",
|
"description": "Klassischer Grabenbau, professionelle Verlegung und Kabelmontage (Hoch-, Mittel- und Niederspannung) bis 110 kV.",
|
||||||
"provider": { "@type": "Organization", "name": "E-TIB GmbH" }
|
"provider": { "@type": "Organization", "name": "E-TIB GmbH" }
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ layout: "fullBleed"
|
|||||||
|
|
||||||
<div className="container px-4 max-w-7xl mx-auto py-16">
|
<div className="container px-4 max-w-7xl mx-auto py-16">
|
||||||
<blockquote>
|
<blockquote>
|
||||||
Wir bieten unseren Kunden alles aus einer Hand – von der Grobplanung Ihrer Projektidee über den Kabelleitungstiefbau bis zum Bau Ihres erneuerbaren Energieprojektes.
|
Wir bieten unseren Kunden alles aus einer Hand – von der Grobplanung Ihrer Projektidee über den Kabelleitungsnetzbau bis zum Bau Ihres erneuerbaren Energieprojektes.
|
||||||
</blockquote>
|
</blockquote>
|
||||||
|
|
||||||
## Fundament für den Projekterfolg
|
## Fundament für den Projekterfolg
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ client: "Energiekontor AG"
|
|||||||
date: "2026-06-01"
|
date: "2026-06-01"
|
||||||
dateString: "01/2025 bis 06/2026"
|
dateString: "01/2025 bis 06/2026"
|
||||||
location: "18334 Dettmannsdorf"
|
location: "18334 Dettmannsdorf"
|
||||||
featuredImage: "/assets/photos/etib_ingenieure_tiefbau_solar_pv_planung_vermessung_gis-256.jpg"
|
featuredImage: "/assets/photos/DJI_0301.JPG"
|
||||||
---
|
---
|
||||||
|
|
||||||
- 12.300 m Kabelgraben ausheben und verfüllen
|
- 12.300 m Kabelgraben ausheben und verfüllen
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ client: "wpd GmbH"
|
|||||||
date: "2026-03-01"
|
date: "2026-03-01"
|
||||||
dateString: "seit 03/2026"
|
dateString: "seit 03/2026"
|
||||||
location: "24808 Jevenstedt"
|
location: "24808 Jevenstedt"
|
||||||
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-15.jpg"
|
featuredImage: "/assets/photos/DJI_0037.JPG"
|
||||||
---
|
---
|
||||||
|
|
||||||
- 500 m Kabelgraben ausheben und verfüllen
|
- 500 m Kabelgraben ausheben und verfüllen
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ client: "E.DIS Netz GmbH"
|
|||||||
date: "2025-06-01"
|
date: "2025-06-01"
|
||||||
dateString: "01/2025 bis 06/2025"
|
dateString: "01/2025 bis 06/2025"
|
||||||
location: "14669 Ketzin"
|
location: "14669 Ketzin"
|
||||||
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-14.jpg"
|
featuredImage: "/assets/photos/DSC02643.JPG"
|
||||||
---
|
---
|
||||||
|
|
||||||
- 1.600 m Kabelgraben ausheben und verfüllen
|
- 1.600 m Kabelgraben ausheben und verfüllen
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ client: "GP Joule Anlagenbau GmbH & Co. KG"
|
|||||||
date: "2026-03-01"
|
date: "2026-03-01"
|
||||||
dateString: "02/2025 bis 03/2026"
|
dateString: "02/2025 bis 03/2026"
|
||||||
location: "48346 Ostbevern"
|
location: "48346 Ostbevern"
|
||||||
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg"
|
featuredImage: "/assets/photos/DJI_0048.JPG"
|
||||||
---
|
---
|
||||||
|
|
||||||
- 4.100 m Kabelgraben ausheben und verfüllen
|
- 4.100 m Kabelgraben ausheben und verfüllen
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ client: "NaturStromAnlagen GmbH"
|
|||||||
date: "2025-02-01"
|
date: "2025-02-01"
|
||||||
dateString: "04/2024 bis 02/2025"
|
dateString: "04/2024 bis 02/2025"
|
||||||
location: "15326 Petershagen"
|
location: "15326 Petershagen"
|
||||||
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-12.jpg"
|
featuredImage: "/assets/photos/Screenshot_2025-09-30_085736.png"
|
||||||
---
|
---
|
||||||
|
|
||||||
- 12.000 m Kabelgraben ausheben und verfüllen
|
- 12.000 m Kabelgraben ausheben und verfüllen
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ client: "Trianel GmbH"
|
|||||||
date: "2025-08-01"
|
date: "2025-08-01"
|
||||||
dateString: "02/2025 bis 08/2025"
|
dateString: "02/2025 bis 08/2025"
|
||||||
location: "15345 Prötzel"
|
location: "15345 Prötzel"
|
||||||
featuredImage: "/assets/photos/etib_ingenieure_tiefbau_solar_pv_planung_vermessung_gis-294.jpg"
|
featuredImage: "/assets/photos/DJI_0243.JPG"
|
||||||
---
|
---
|
||||||
|
|
||||||
- 2.600 m Kabelgraben ausheben und verfüllen
|
- 2.600 m Kabelgraben ausheben und verfüllen
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
---
|
---
|
||||||
title: "Home - Experts for Cable Construction & Civil Engineering"
|
title: "Home - Experts for cable network construction & Civil Engineering"
|
||||||
description: "E-TIB GmbH is your reliable partner for complex cable routes, horizontal directional drilling, surveying, and telecommunications infrastructure in Germany."
|
description: "E-TIB GmbH is your reliable partner for complex cable routes, horizontal directional drilling, surveying, and telecommunications infrastructure in Germany."
|
||||||
---
|
---
|
||||||
|
|
||||||
<HomeHero
|
<HomeHero
|
||||||
|
|
||||||
title={"The Experts for\nCable Construction"}
|
title={"The Experts for\ncable network construction"}
|
||||||
description="We realize complex supply lines for a modern society. Reliable, innovative and with the highest precision in cable construction and drilling technology."
|
description="We realize complex supply lines for a modern society. Reliable, innovative and with the highest precision in cable network construction and drilling technology."
|
||||||
videoUrl="/assets/videos/web/hero-kabelpflug.mp4"
|
videoUrl="/assets/videos/web/hero-kabelpflug.mp4"
|
||||||
linkText="Our Services"
|
linkText="Our Services"
|
||||||
linkHref="/en/competencies"
|
linkHref="/en/competencies"
|
||||||
@@ -121,8 +121,8 @@ description: "E-TIB GmbH is your reliable partner for complex cable routes, hori
|
|||||||
description: "Cable civil engineering (high, medium, low voltage) and cable assembly up to 110 kV",
|
description: "Cable civil engineering (high, medium, low voltage) and cable assembly up to 110 kV",
|
||||||
tag: "Energy",
|
tag: "Energy",
|
||||||
size: "large",
|
size: "large",
|
||||||
href: "/en/cable-civil-engineering",
|
href: "/en/kabeltiefbau",
|
||||||
image: { url: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-54.jpg", alt: "Cable Civil Engineering" }
|
image: { url: "/assets/photos/DSC01123.JPG", alt: "Cable Network Construction" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Drilling Technology",
|
title: "Drilling Technology",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: "Competencies - Cable Civil Engineering & Horizontal Directional Drilling"
|
title: "Competencies - Cable Civil Engineering & Horizontal Directional Drilling"
|
||||||
date: "2024-03-20"
|
date: "2024-03-20"
|
||||||
excerpt: "From cable construction to electrical installation: We provide holistic solutions for modern network infrastructures."
|
excerpt: "From cable network construction to electrical installation: We provide holistic solutions for modern network infrastructures."
|
||||||
layout: "fullBleed"
|
layout: "fullBleed"
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -27,8 +27,8 @@ layout: "fullBleed"
|
|||||||
description: "Classic trench construction, professional laying and cable assembly (high, medium, and low voltage) up to 110 kV.",
|
description: "Classic trench construction, professional laying and cable assembly (high, medium, and low voltage) up to 110 kV.",
|
||||||
tag: "Energy",
|
tag: "Energy",
|
||||||
size: "large",
|
size: "large",
|
||||||
href: "/en/cable-civil-engineering",
|
href: "/en/kabeltiefbau",
|
||||||
image: { url: "/assets/photos/DSC01123.JPG", alt: "Cable Civil Engineering" }
|
image: { url: "/assets/photos/DSC01123.JPG", alt: "Cable Network Construction" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Drilling Technology",
|
title: "Drilling Technology",
|
||||||
@@ -36,7 +36,7 @@ layout: "fullBleed"
|
|||||||
tag: "Innovation",
|
tag: "Innovation",
|
||||||
size: "medium",
|
size: "medium",
|
||||||
href: "/en/drilling-technology",
|
href: "/en/drilling-technology",
|
||||||
image: { url: "/assets/photos/DSC08653.JPG", alt: "Drilling Technology" }
|
image: { url: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-47.jpg", alt: "Drilling Technology" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Fiber Optics",
|
title: "Fiber Optics",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ client: "Energiekontor AG"
|
|||||||
date: "2026-06-01"
|
date: "2026-06-01"
|
||||||
dateString: "01/2025 to 06/2026"
|
dateString: "01/2025 to 06/2026"
|
||||||
location: "18334 Dettmannsdorf"
|
location: "18334 Dettmannsdorf"
|
||||||
featuredImage: "/assets/photos/etib_ingenieure_tiefbau_solar_pv_planung_vermessung_gis-256.jpg"
|
featuredImage: "/assets/photos/DJI_0301.JPG"
|
||||||
---
|
---
|
||||||
|
|
||||||
- Excavation and backfilling of 12,300 m cable trench
|
- Excavation and backfilling of 12,300 m cable trench
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ client: "wpd GmbH"
|
|||||||
date: "2026-03-01"
|
date: "2026-03-01"
|
||||||
dateString: "since 03/2026"
|
dateString: "since 03/2026"
|
||||||
location: "24808 Jevenstedt"
|
location: "24808 Jevenstedt"
|
||||||
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-15.jpg"
|
featuredImage: "/assets/photos/DJI_0037.JPG"
|
||||||
---
|
---
|
||||||
|
|
||||||
- Excavation and backfilling of 500 m cable trench
|
- Excavation and backfilling of 500 m cable trench
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ client: "E.DIS Netz GmbH"
|
|||||||
date: "2025-06-01"
|
date: "2025-06-01"
|
||||||
dateString: "01/2025 to 06/2025"
|
dateString: "01/2025 to 06/2025"
|
||||||
location: "14669 Ketzin"
|
location: "14669 Ketzin"
|
||||||
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-14.jpg"
|
featuredImage: "/assets/photos/DSC02643.JPG"
|
||||||
---
|
---
|
||||||
|
|
||||||
- Excavation and backfilling of 1,600 m cable trench
|
- Excavation and backfilling of 1,600 m cable trench
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ client: "GP Joule Anlagenbau GmbH & Co. KG"
|
|||||||
date: "2026-03-01"
|
date: "2026-03-01"
|
||||||
dateString: "02/2025 to 03/2026"
|
dateString: "02/2025 to 03/2026"
|
||||||
location: "48346 Ostbevern"
|
location: "48346 Ostbevern"
|
||||||
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg"
|
featuredImage: "/assets/photos/DJI_0048.JPG"
|
||||||
---
|
---
|
||||||
|
|
||||||
- Excavation and backfilling of 4,100 m cable trench
|
- Excavation and backfilling of 4,100 m cable trench
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ client: "NaturStromAnlagen GmbH"
|
|||||||
date: "2025-02-01"
|
date: "2025-02-01"
|
||||||
dateString: "04/2024 to 02/2025"
|
dateString: "04/2024 to 02/2025"
|
||||||
location: "15326 Petershagen"
|
location: "15326 Petershagen"
|
||||||
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-12.jpg"
|
featuredImage: "/assets/photos/Screenshot_2025-09-30_085736.png"
|
||||||
---
|
---
|
||||||
|
|
||||||
- Excavation and backfilling of 12,000 m cable trench
|
- Excavation and backfilling of 12,000 m cable trench
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ client: "Trianel GmbH"
|
|||||||
date: "2025-08-01"
|
date: "2025-08-01"
|
||||||
dateString: "02/2025 to 08/2025"
|
dateString: "02/2025 to 08/2025"
|
||||||
location: "15345 Prötzel"
|
location: "15345 Prötzel"
|
||||||
featuredImage: "/assets/photos/etib_ingenieure_tiefbau_solar_pv_planung_vermessung_gis-294.jpg"
|
featuredImage: "/assets/photos/DJI_0243.JPG"
|
||||||
---
|
---
|
||||||
|
|
||||||
- Excavation and backfilling of 2,600 m cable trench
|
- Excavation and backfilling of 2,600 m cable trench
|
||||||
|
|||||||
@@ -1,388 +0,0 @@
|
|||||||
/**
|
|
||||||
* Excel Products Module
|
|
||||||
*
|
|
||||||
* Provides typed access to technical product data from Excel source files.
|
|
||||||
* Reuses the parsing logic from the PDF datasheet generator.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import * as XLSX from 'xlsx';
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
const EXCEL_SOURCE_FILES = [
|
|
||||||
path.join(process.cwd(), 'data/excel/high-voltage.xlsx'),
|
|
||||||
path.join(process.cwd(), 'data/excel/medium-voltage-KM.xlsx'),
|
|
||||||
path.join(process.cwd(), 'data/excel/low-voltage-KM.xlsx'),
|
|
||||||
path.join(process.cwd(), 'data/excel/solar-cables.xlsx'),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Types
|
|
||||||
export type ExcelRow = Record<string, string | number | boolean | Date>;
|
|
||||||
|
|
||||||
export interface ExcelMatch {
|
|
||||||
rows: ExcelRow[];
|
|
||||||
units: Record<string, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TechnicalData {
|
|
||||||
configurations: string[];
|
|
||||||
attributes: Array<{
|
|
||||||
name: string;
|
|
||||||
options: string[];
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProductLookupParams {
|
|
||||||
name?: string;
|
|
||||||
slug?: string;
|
|
||||||
sku?: string;
|
|
||||||
translationKey?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache singleton
|
|
||||||
let EXCEL_INDEX: Map<string, ExcelMatch> | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize Excel key to match product identifiers
|
|
||||||
* Examples:
|
|
||||||
* - "NA2XS(FL)2Y" -> "NA2XSFL2Y"
|
|
||||||
* - "na2xsfl2y-3" -> "NA2XSFL2Y"
|
|
||||||
*/
|
|
||||||
function normalizeExcelKey(value: string): string {
|
|
||||||
return String(value || '')
|
|
||||||
.toUpperCase()
|
|
||||||
.replace(/-\d+$/g, '')
|
|
||||||
.replace(/[^A-Z0-9]+/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize value (strip HTML, trim whitespace)
|
|
||||||
*/
|
|
||||||
function normalizeValue(value: string): string {
|
|
||||||
if (!value) return '';
|
|
||||||
return String(value)
|
|
||||||
.replace(/<[^>]*>/g, '')
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if value looks numeric
|
|
||||||
*/
|
|
||||||
function looksNumeric(value: string): boolean {
|
|
||||||
const v = normalizeValue(value).replace(/,/g, '.');
|
|
||||||
return /^-?\d+(?:\.\d+)?$/.test(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load Excel rows from a file using xlsx library
|
|
||||||
*/
|
|
||||||
function loadExcelRows(filePath: string): ExcelRow[] {
|
|
||||||
if (!fs.existsSync(filePath)) {
|
|
||||||
console.warn(`[excel-products] File not found: ${filePath}`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const workbook = XLSX.readFile(filePath, {
|
|
||||||
cellDates: false,
|
|
||||||
cellNF: false,
|
|
||||||
cellText: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get the first sheet
|
|
||||||
const sheetName = workbook.SheetNames[0];
|
|
||||||
const worksheet = workbook.Sheets[sheetName];
|
|
||||||
|
|
||||||
// Convert to JSON
|
|
||||||
const rows = XLSX.utils.sheet_to_json(worksheet, {
|
|
||||||
defval: '',
|
|
||||||
raw: false
|
|
||||||
}) as ExcelRow[];
|
|
||||||
|
|
||||||
return rows;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[excel-products] Error reading ${filePath}:`, error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build the Excel index from all source files
|
|
||||||
*/
|
|
||||||
export function getExcelIndex(): Map<string, ExcelMatch> {
|
|
||||||
if (EXCEL_INDEX) return EXCEL_INDEX;
|
|
||||||
|
|
||||||
const idx = new Map<string, ExcelMatch>();
|
|
||||||
|
|
||||||
for (const file of EXCEL_SOURCE_FILES) {
|
|
||||||
const rows = loadExcelRows(file);
|
|
||||||
if (rows.length === 0) continue;
|
|
||||||
|
|
||||||
// Find units row (if present)
|
|
||||||
const unitsRow = rows.find(r => r && r['Part Number'] === 'Units') || null;
|
|
||||||
const units: Record<string, string> = {};
|
|
||||||
|
|
||||||
if (unitsRow) {
|
|
||||||
for (const [k, v] of Object.entries(unitsRow)) {
|
|
||||||
if (k === 'Part Number') continue;
|
|
||||||
const unit = normalizeValue(String(v ?? ''));
|
|
||||||
if (unit) units[k] = unit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Index rows by Part Number
|
|
||||||
for (const r of rows) {
|
|
||||||
const pn = r?.['Part Number'];
|
|
||||||
if (!pn || pn === 'Units') continue;
|
|
||||||
|
|
||||||
const key = normalizeExcelKey(String(pn));
|
|
||||||
if (!key) continue;
|
|
||||||
|
|
||||||
const cur = idx.get(key);
|
|
||||||
if (!cur) {
|
|
||||||
idx.set(key, { rows: [r], units });
|
|
||||||
} else {
|
|
||||||
cur.rows.push(r);
|
|
||||||
// Keep the most comprehensive units
|
|
||||||
if (Object.keys(cur.units).length < Object.keys(units).length) {
|
|
||||||
cur.units = units;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EXCEL_INDEX = idx;
|
|
||||||
return idx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find Excel match for a product using various identifiers
|
|
||||||
*/
|
|
||||||
function findExcelForProduct(params: ProductLookupParams): ExcelMatch | null {
|
|
||||||
const idx = getExcelIndex();
|
|
||||||
|
|
||||||
const candidates = [
|
|
||||||
params.name,
|
|
||||||
params.slug ? params.slug.replace(/-\d+$/g, '') : '',
|
|
||||||
params.sku,
|
|
||||||
params.translationKey,
|
|
||||||
].filter(Boolean) as string[];
|
|
||||||
|
|
||||||
for (const c of candidates) {
|
|
||||||
const key = normalizeExcelKey(c);
|
|
||||||
const match = idx.get(key);
|
|
||||||
if (match && match.rows.length) return match;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Guess column key based on patterns
|
|
||||||
*/
|
|
||||||
function guessColumnKey(row: ExcelRow, patterns: RegExp[]): string | null {
|
|
||||||
const keys = Object.keys(row || {});
|
|
||||||
|
|
||||||
for (const re of patterns) {
|
|
||||||
const k = keys.find(x => {
|
|
||||||
const key = String(x);
|
|
||||||
|
|
||||||
// Specific exclusions to prevent wrong matches
|
|
||||||
if (re.test('conductor') && /ross section conductor/i.test(key)) return false;
|
|
||||||
if (re.test('insulation thickness') && /Diameter over insulation/i.test(key)) return false;
|
|
||||||
if (re.test('conductor') && !/^conductor$/i.test(key)) return false;
|
|
||||||
if (re.test('insulation') && !/^insulation$/i.test(key)) return false;
|
|
||||||
if (re.test('sheath') && !/^sheath$/i.test(key)) return false;
|
|
||||||
if (re.test('norm') && !/^norm$/i.test(key)) return false;
|
|
||||||
|
|
||||||
return re.test(key);
|
|
||||||
});
|
|
||||||
if (k) return k;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get unique non-empty values from an array
|
|
||||||
*/
|
|
||||||
function getUniqueNonEmpty(options: string[]): string[] {
|
|
||||||
const uniq: string[] = [];
|
|
||||||
const seen = new Set<string>();
|
|
||||||
for (const v of options.map(normalizeValue).filter(Boolean)) {
|
|
||||||
const k = v.toLowerCase();
|
|
||||||
if (seen.has(k)) continue;
|
|
||||||
seen.add(k);
|
|
||||||
uniq.push(v);
|
|
||||||
}
|
|
||||||
return uniq;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get technical data for a product from Excel files
|
|
||||||
*/
|
|
||||||
export function getExcelTechnicalDataForProduct(params: ProductLookupParams): TechnicalData | null {
|
|
||||||
const match = findExcelForProduct(params);
|
|
||||||
if (!match || match.rows.length === 0) return null;
|
|
||||||
|
|
||||||
const rows = match.rows;
|
|
||||||
const sample = rows[0];
|
|
||||||
|
|
||||||
// Find cross-section column
|
|
||||||
const csKey = guessColumnKey(sample, [
|
|
||||||
/number of cores and cross-section/i,
|
|
||||||
/cross.?section/i,
|
|
||||||
/ross section conductor/i,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!csKey) return null;
|
|
||||||
|
|
||||||
// Extract configurations
|
|
||||||
const voltageKey = guessColumnKey(sample, [/rated voltage/i, /voltage rating/i, /spannungs/i, /nennspannung/i]);
|
|
||||||
|
|
||||||
const configurations = rows
|
|
||||||
.map(r => {
|
|
||||||
const cs = normalizeValue(String(r?.[csKey] ?? ''));
|
|
||||||
const v = voltageKey ? normalizeValue(String(r?.[voltageKey] ?? '')) : '';
|
|
||||||
if (!cs) return '';
|
|
||||||
if (!v) return cs;
|
|
||||||
const vHasUnit = /\bkv\b/i.test(v);
|
|
||||||
const vText = vHasUnit ? v : `${v} kV`;
|
|
||||||
return `${cs} - ${vText}`;
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
if (configurations.length === 0) return null;
|
|
||||||
|
|
||||||
// Extract technical attributes
|
|
||||||
const attributes: Array<{ name: string; options: string[] }> = [];
|
|
||||||
|
|
||||||
// Key technical columns
|
|
||||||
const outerKey = guessColumnKey(sample, [/outer diameter\b/i, /outer diameter.*approx/i, /outer diameter of cable/i, /außen/i]);
|
|
||||||
const weightKey = guessColumnKey(sample, [/weight\b/i, /gewicht/i, /cable weight/i]);
|
|
||||||
const dcResKey = guessColumnKey(sample, [/dc resistance/i, /resistance conductor/i, /leiterwiderstand/i]);
|
|
||||||
const ratedVoltKey = voltageKey;
|
|
||||||
const testVoltKey = guessColumnKey(sample, [/test voltage/i, /prüfspannung/i]);
|
|
||||||
const tempRangeKey = guessColumnKey(sample, [/operating temperature range/i, /temperature range/i, /temperaturbereich/i]);
|
|
||||||
const conductorKey = guessColumnKey(sample, [/^conductor$/i]);
|
|
||||||
const insulationKey = guessColumnKey(sample, [/^insulation$/i]);
|
|
||||||
const sheathKey = guessColumnKey(sample, [/^sheath$/i]);
|
|
||||||
const normKey = guessColumnKey(sample, [/^norm$/i, /^standard$/i]);
|
|
||||||
const cprKey = guessColumnKey(sample, [/cpr class/i]);
|
|
||||||
const packagingKey = guessColumnKey(sample, [/^packaging$/i]);
|
|
||||||
const shapeKey = guessColumnKey(sample, [/shape of conductor/i]);
|
|
||||||
const flameKey = guessColumnKey(sample, [/flame retardant/i]);
|
|
||||||
const diamCondKey = guessColumnKey(sample, [/diameter conductor/i]);
|
|
||||||
const diamInsKey = guessColumnKey(sample, [/diameter over insulation/i]);
|
|
||||||
const diamScreenKey = guessColumnKey(sample, [/diameter over screen/i]);
|
|
||||||
const metalScreenKey = guessColumnKey(sample, [/metallic screen/i]);
|
|
||||||
const capacitanceKey = guessColumnKey(sample, [/capacitance/i]);
|
|
||||||
const reactanceKey = guessColumnKey(sample, [/reactance/i]);
|
|
||||||
const electricalStressKey = guessColumnKey(sample, [/electrical stress/i]);
|
|
||||||
const pullingForceKey = guessColumnKey(sample, [/max\. pulling force/i, /pulling force/i]);
|
|
||||||
const heatingTrefoilKey = guessColumnKey(sample, [/heating time constant.*trefoil/i]);
|
|
||||||
const heatingFlatKey = guessColumnKey(sample, [/heating time constant.*flat/i]);
|
|
||||||
const currentAirTrefoilKey = guessColumnKey(sample, [/current ratings in air.*trefoil/i]);
|
|
||||||
const currentAirFlatKey = guessColumnKey(sample, [/current ratings in air.*flat/i]);
|
|
||||||
const currentGroundTrefoilKey = guessColumnKey(sample, [/current ratings in ground.*trefoil/i]);
|
|
||||||
const currentGroundFlatKey = guessColumnKey(sample, [/current ratings in ground.*flat/i]);
|
|
||||||
const scCurrentCondKey = guessColumnKey(sample, [/conductor shortcircuit current/i]);
|
|
||||||
const scCurrentScreenKey = guessColumnKey(sample, [/screen shortcircuit current/i]);
|
|
||||||
const minLayKey = guessColumnKey(sample, [/minimal temperature for laying/i]);
|
|
||||||
const minStoreKey = guessColumnKey(sample, [/minimal storage temperature/i]);
|
|
||||||
const maxOpKey = guessColumnKey(sample, [/maximal operating conductor temperature/i, /max\. operating/i]);
|
|
||||||
const maxScKey = guessColumnKey(sample, [/maximal short-circuit temperature/i, /short\s*circuit\s*temperature/i]);
|
|
||||||
const insThkKey = guessColumnKey(sample, [/nominal insulation thickness/i, /insulation thickness/i]);
|
|
||||||
const sheathThkKey = guessColumnKey(sample, [/nominal sheath thickness/i, /minimum sheath thickness/i]);
|
|
||||||
const maxResKey = guessColumnKey(sample, [/maximum resistance of conductor/i]);
|
|
||||||
const bendKey = guessColumnKey(sample, [/bending radius/i, /min\. bending radius/i]);
|
|
||||||
|
|
||||||
// Helper to add attribute
|
|
||||||
const addAttr = (name: string, key: string | null, unit?: string) => {
|
|
||||||
if (!key) return;
|
|
||||||
const options = rows
|
|
||||||
.map(r => normalizeValue(String(r?.[key] ?? '')))
|
|
||||||
.map(v => (unit && v && looksNumeric(v) ? `${v} ${unit}` : v))
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
if (options.length === 0) return;
|
|
||||||
|
|
||||||
const uniqueOptions = getUniqueNonEmpty(options);
|
|
||||||
attributes.push({ name, options: uniqueOptions });
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add attributes
|
|
||||||
addAttr('Outer diameter', outerKey, 'mm');
|
|
||||||
addAttr('Weight', weightKey, 'kg/km');
|
|
||||||
addAttr('DC resistance at 20 °C', dcResKey, 'Ω/km');
|
|
||||||
addAttr('Rated voltage', ratedVoltKey, '');
|
|
||||||
addAttr('Test voltage', testVoltKey, '');
|
|
||||||
addAttr('Operating temperature range', tempRangeKey, '');
|
|
||||||
addAttr('Minimal temperature for laying', minLayKey, '');
|
|
||||||
addAttr('Minimal storage temperature', minStoreKey, '');
|
|
||||||
addAttr('Maximal operating conductor temperature', maxOpKey, '');
|
|
||||||
addAttr('Maximal short-circuit temperature', maxScKey, '');
|
|
||||||
addAttr('Nominal insulation thickness', insThkKey, 'mm');
|
|
||||||
addAttr('Nominal sheath thickness', sheathThkKey, 'mm');
|
|
||||||
addAttr('Maximum resistance of conductor', maxResKey, 'Ω/km');
|
|
||||||
addAttr('Conductor', conductorKey, '');
|
|
||||||
addAttr('Insulation', insulationKey, '');
|
|
||||||
addAttr('Sheath', sheathKey, '');
|
|
||||||
addAttr('Standard', normKey, '');
|
|
||||||
addAttr('Conductor diameter', diamCondKey, 'mm');
|
|
||||||
addAttr('Insulation diameter', diamInsKey, 'mm');
|
|
||||||
addAttr('Screen diameter', diamScreenKey, 'mm');
|
|
||||||
addAttr('Metallic screen', metalScreenKey, '');
|
|
||||||
addAttr('Max. pulling force', pullingForceKey, '');
|
|
||||||
addAttr('Electrical stress conductor', electricalStressKey, '');
|
|
||||||
addAttr('Electrical stress insulation', electricalStressKey, '');
|
|
||||||
addAttr('Reactance', reactanceKey, '');
|
|
||||||
addAttr('Heating time constant trefoil', heatingTrefoilKey, 's');
|
|
||||||
addAttr('Heating time constant flat', heatingFlatKey, 's');
|
|
||||||
addAttr('Flame retardant', flameKey, '');
|
|
||||||
addAttr('CPR class', cprKey, '');
|
|
||||||
addAttr('Packaging', packagingKey, '');
|
|
||||||
addAttr('Bending radius', bendKey, 'mm');
|
|
||||||
addAttr('Shape of conductor', shapeKey, '');
|
|
||||||
addAttr('Capacitance', capacitanceKey, 'μF/km');
|
|
||||||
addAttr('Current ratings in air, trefoil', currentAirTrefoilKey, 'A');
|
|
||||||
addAttr('Current ratings in air, flat', currentAirFlatKey, 'A');
|
|
||||||
addAttr('Current ratings in ground, trefoil', currentGroundTrefoilKey, 'A');
|
|
||||||
addAttr('Current ratings in ground, flat', currentGroundFlatKey, 'A');
|
|
||||||
addAttr('Conductor shortcircuit current', scCurrentCondKey, 'kA');
|
|
||||||
addAttr('Screen shortcircuit current', scCurrentScreenKey, 'kA');
|
|
||||||
|
|
||||||
return {
|
|
||||||
configurations,
|
|
||||||
attributes,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get raw Excel rows for a product (for detailed inspection)
|
|
||||||
*/
|
|
||||||
export function getExcelRowsForProduct(params: ProductLookupParams): ExcelRow[] {
|
|
||||||
const match = findExcelForProduct(params);
|
|
||||||
return match?.rows || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the Excel index cache (useful for development)
|
|
||||||
*/
|
|
||||||
export function clearExcelCache(): void {
|
|
||||||
EXCEL_INDEX = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preload Excel data on module initialization
|
|
||||||
* This ensures the cache is built during build time
|
|
||||||
*/
|
|
||||||
export function preloadExcelData(): void {
|
|
||||||
getExcelIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preload when imported
|
|
||||||
if (require.main === module) {
|
|
||||||
preloadExcelData();
|
|
||||||
}
|
|
||||||
1436
lib/pdf-brochure.tsx
1436
lib/pdf-brochure.tsx
File diff suppressed because it is too large
Load Diff
329
lib/pdf-page.tsx
329
lib/pdf-page.tsx
@@ -1,329 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { Document, Page, View, Text, StyleSheet, Font, Link } from '@react-pdf/renderer';
|
|
||||||
|
|
||||||
// Register fonts (using system fonts for now, can be customized)
|
|
||||||
Font.register({
|
|
||||||
family: 'Helvetica',
|
|
||||||
fonts: [
|
|
||||||
{ src: '/fonts/Helvetica.ttf', fontWeight: 400 },
|
|
||||||
{ src: '/fonts/Helvetica-Bold.ttf', fontWeight: 700 },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Brand Tokens (matching datasheet) ──────────────────────────────────
|
|
||||||
const C = {
|
|
||||||
navy: '#001a4d',
|
|
||||||
navyDeep: '#000d26',
|
|
||||||
green: '#4da612',
|
|
||||||
greenLight: '#e8f5d8',
|
|
||||||
white: '#FFFFFF',
|
|
||||||
offWhite: '#f8f9fa',
|
|
||||||
gray100: '#f3f4f6',
|
|
||||||
gray200: '#e5e7eb',
|
|
||||||
gray300: '#d1d5db',
|
|
||||||
gray400: '#9ca3af',
|
|
||||||
gray600: '#4b5563',
|
|
||||||
gray900: '#111827',
|
|
||||||
};
|
|
||||||
|
|
||||||
const MARGIN = 56;
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
page: {
|
|
||||||
color: C.gray900,
|
|
||||||
lineHeight: 1.5,
|
|
||||||
backgroundColor: C.white,
|
|
||||||
paddingTop: 0,
|
|
||||||
paddingBottom: 80,
|
|
||||||
fontFamily: 'Helvetica',
|
|
||||||
},
|
|
||||||
|
|
||||||
// Hero-style header
|
|
||||||
hero: {
|
|
||||||
backgroundColor: C.white,
|
|
||||||
paddingTop: 24,
|
|
||||||
paddingBottom: 0,
|
|
||||||
paddingHorizontal: MARGIN,
|
|
||||||
marginBottom: 20,
|
|
||||||
position: 'relative',
|
|
||||||
borderBottomWidth: 0,
|
|
||||||
},
|
|
||||||
|
|
||||||
header: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 16,
|
|
||||||
},
|
|
||||||
|
|
||||||
logoText: {
|
|
||||||
fontSize: 22,
|
|
||||||
fontWeight: 700,
|
|
||||||
color: C.navyDeep,
|
|
||||||
letterSpacing: 2,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
},
|
|
||||||
|
|
||||||
docTitle: {
|
|
||||||
fontSize: 8,
|
|
||||||
fontWeight: 700,
|
|
||||||
color: C.green,
|
|
||||||
letterSpacing: 2,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
},
|
|
||||||
|
|
||||||
// Content Area
|
|
||||||
content: {
|
|
||||||
paddingHorizontal: MARGIN,
|
|
||||||
},
|
|
||||||
|
|
||||||
pageTitle: {
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: 700,
|
|
||||||
color: C.navyDeep,
|
|
||||||
marginBottom: 8,
|
|
||||||
marginTop: 10,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: -0.5,
|
|
||||||
},
|
|
||||||
|
|
||||||
accentBar: {
|
|
||||||
width: 30,
|
|
||||||
height: 2,
|
|
||||||
backgroundColor: C.green,
|
|
||||||
marginBottom: 20,
|
|
||||||
borderRadius: 1,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Lexical Elements
|
|
||||||
paragraph: {
|
|
||||||
fontSize: 10,
|
|
||||||
color: C.gray600,
|
|
||||||
lineHeight: 1.7,
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
heading1: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: 700,
|
|
||||||
color: C.navyDeep,
|
|
||||||
marginTop: 20,
|
|
||||||
marginBottom: 10,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
},
|
|
||||||
heading2: {
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 700,
|
|
||||||
color: C.navyDeep,
|
|
||||||
marginTop: 16,
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
heading3: {
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: 700,
|
|
||||||
color: C.navyDeep,
|
|
||||||
marginTop: 12,
|
|
||||||
marginBottom: 6,
|
|
||||||
},
|
|
||||||
list: {
|
|
||||||
marginBottom: 12,
|
|
||||||
marginLeft: 8,
|
|
||||||
},
|
|
||||||
listItem: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
listItemBullet: {
|
|
||||||
width: 12,
|
|
||||||
fontSize: 10,
|
|
||||||
color: C.green,
|
|
||||||
fontWeight: 700,
|
|
||||||
},
|
|
||||||
listItemContent: {
|
|
||||||
flex: 1,
|
|
||||||
fontSize: 10,
|
|
||||||
color: C.gray600,
|
|
||||||
lineHeight: 1.7,
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
color: C.green,
|
|
||||||
textDecoration: 'none',
|
|
||||||
},
|
|
||||||
textBold: {
|
|
||||||
fontWeight: 700,
|
|
||||||
fontFamily: 'Helvetica-Bold',
|
|
||||||
color: C.navyDeep,
|
|
||||||
},
|
|
||||||
textItalic: {
|
|
||||||
fontStyle: 'italic',
|
|
||||||
},
|
|
||||||
|
|
||||||
// Footer — matches brochure style
|
|
||||||
footer: {
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 28,
|
|
||||||
left: MARGIN,
|
|
||||||
right: MARGIN,
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingTop: 12,
|
|
||||||
borderTopWidth: 2,
|
|
||||||
borderTopColor: C.green,
|
|
||||||
},
|
|
||||||
|
|
||||||
footerText: {
|
|
||||||
fontSize: 7,
|
|
||||||
color: C.gray400,
|
|
||||||
fontWeight: 400,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: 0.8,
|
|
||||||
},
|
|
||||||
|
|
||||||
footerBrand: {
|
|
||||||
fontSize: 9,
|
|
||||||
fontWeight: 700,
|
|
||||||
color: C.navyDeep,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: 1.5,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Lexical to React-PDF Renderer ────────────────────────────────
|
|
||||||
|
|
||||||
const renderLexicalNode = (node: any, idx: number): React.ReactNode => {
|
|
||||||
if (!node) return null;
|
|
||||||
|
|
||||||
switch (node.type) {
|
|
||||||
case 'text': {
|
|
||||||
const format = node.format || 0;
|
|
||||||
const isBold = (format & 1) !== 0;
|
|
||||||
const isItalic = (format & 2) !== 0;
|
|
||||||
|
|
||||||
let elementStyle: any = {};
|
|
||||||
if (isBold) elementStyle = { ...elementStyle, ...styles.textBold };
|
|
||||||
if (isItalic) elementStyle = { ...elementStyle, ...styles.textItalic };
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Text key={idx} style={elementStyle}>
|
|
||||||
{node.text}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'paragraph': {
|
|
||||||
return (
|
|
||||||
<Text key={idx} style={styles.paragraph}>
|
|
||||||
{node.children?.map((child: any, i: number) => renderLexicalNode(child, i))}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'heading': {
|
|
||||||
let hStyle = styles.heading3;
|
|
||||||
if (node.tag === 'h1') hStyle = styles.heading1;
|
|
||||||
if (node.tag === 'h2') hStyle = styles.heading2;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Text key={idx} style={hStyle}>
|
|
||||||
{node.children?.map((child: any, i: number) => renderLexicalNode(child, i))}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'list': {
|
|
||||||
return (
|
|
||||||
<View key={idx} style={styles.list}>
|
|
||||||
{node.children?.map((child: any, i: number) => {
|
|
||||||
if (child.type === 'listitem') {
|
|
||||||
return (
|
|
||||||
<View key={i} style={styles.listItem}>
|
|
||||||
<Text style={styles.listItemBullet}>
|
|
||||||
{node.listType === 'number' ? `${i + 1}.` : '•'}
|
|
||||||
</Text>
|
|
||||||
<Text style={styles.listItemContent}>
|
|
||||||
{child.children?.map((c: any, ci: number) => renderLexicalNode(c, ci))}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return renderLexicalNode(child, i);
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'link': {
|
|
||||||
const href = node.fields?.url || node.url || '#';
|
|
||||||
return (
|
|
||||||
<Link key={idx} src={href} style={styles.link}>
|
|
||||||
{node.children?.map((child: any, i: number) => renderLexicalNode(child, i))}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'linebreak': {
|
|
||||||
return <Text key={idx}>{'\n'}</Text>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore payload blocks recursively to avoid crashing
|
|
||||||
case 'block':
|
|
||||||
return null;
|
|
||||||
|
|
||||||
default:
|
|
||||||
if (node.children) {
|
|
||||||
return (
|
|
||||||
<Text key={idx}>
|
|
||||||
{node.children.map((child: any, i: number) => renderLexicalNode(child, i))}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
interface PDFPageProps {
|
|
||||||
page: any;
|
|
||||||
locale?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PDFPage: React.FC<PDFPageProps> = ({ page, locale = 'de' }) => {
|
|
||||||
const dateStr = new Date().toLocaleDateString(locale === 'en' ? 'en-US' : 'de-DE', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Document>
|
|
||||||
<Page size="A4" style={styles.page}>
|
|
||||||
{/* Hero Header */}
|
|
||||||
<View style={styles.hero} fixed>
|
|
||||||
<View style={styles.header}>
|
|
||||||
<View>
|
|
||||||
<Text style={styles.logoText}>KLZ</Text>
|
|
||||||
</View>
|
|
||||||
<Text style={styles.docTitle}>{locale === 'en' ? 'Document' : 'Dokument'}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.content}>
|
|
||||||
<Text style={styles.pageTitle}>{page.title}</Text>
|
|
||||||
<View style={styles.accentBar} />
|
|
||||||
|
|
||||||
<View>
|
|
||||||
{page.content?.root?.children?.map((node: any, i: number) =>
|
|
||||||
renderLexicalNode(node, i),
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Minimal footer */}
|
|
||||||
<View style={styles.footer} fixed>
|
|
||||||
<Text style={styles.footerBrand}>KLZ CABLES</Text>
|
|
||||||
<Text style={styles.footerText}>{dateStr}</Text>
|
|
||||||
</View>
|
|
||||||
</Page>
|
|
||||||
</Document>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -42,7 +42,7 @@ export const getLocalBusinessSchema = () => ({
|
|||||||
url: SITE_URL,
|
url: SITE_URL,
|
||||||
logo: LOGO_URL,
|
logo: LOGO_URL,
|
||||||
image: LOGO_URL,
|
image: LOGO_URL,
|
||||||
description: 'Ihr Partner für Kabelleitungstiefbau, Horizontalspülbohrungen, Planung und Vermessung in Guben und überregional.',
|
description: 'Ihr Partner für Kabelleitungsnetzbau, Horizontalspülbohrungen, Planung und Vermessung in Guben und überregional.',
|
||||||
telephone: '+4935616857733',
|
telephone: '+4935616857733',
|
||||||
email: 'info@e-tib.com',
|
email: 'info@e-tib.com',
|
||||||
address: {
|
address: {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export const standorteData: StandortData[] = [
|
|||||||
shortName: 'Guben (Hauptsitz)',
|
shortName: 'Guben (Hauptsitz)',
|
||||||
type: 'hq',
|
type: 'hq',
|
||||||
description: {
|
description: {
|
||||||
de: 'Unser Hauptsitz in Guben bildet das strategische und operative Zentrum der E-TIB Gruppe. Von hier aus steuern wir bundesweit Großprojekte im Kabelleitungstiefbau und koordinieren unseren hochmodernen Maschinenpark.',
|
de: 'Unser Hauptsitz in Guben bildet das strategische und operative Zentrum der E-TIB Gruppe. Von hier aus steuern wir bundesweit Großprojekte im Kabelleitungsnetzbau und koordinieren unseren hochmodernen Maschinenpark.',
|
||||||
en: 'Our headquarters in Guben forms the strategic and operational center of the E-TIB Group. From here, we manage large-scale civil engineering projects nationwide and coordinate our state-of-the-art machinery fleet.'
|
en: 'Our headquarters in Guben forms the strategic and operational center of the E-TIB Group. From here, we manage large-scale civil engineering projects nationwide and coordinate our state-of-the-art machinery fleet.'
|
||||||
},
|
},
|
||||||
address: {
|
address: {
|
||||||
@@ -56,13 +56,13 @@ export const standorteData: StandortData[] = [
|
|||||||
keyFeatures: {
|
keyFeatures: {
|
||||||
de: [
|
de: [
|
||||||
'Zentrale strategische Steuerung',
|
'Zentrale strategische Steuerung',
|
||||||
'Kabelleitungstiefbau & Infrastruktur',
|
'Kabelleitungsnetzbau & Infrastruktur',
|
||||||
'Koordination Großmaschinenpark',
|
'Koordination Großmaschinenpark',
|
||||||
'Zentrales Projektmanagement'
|
'Zentrales Projektmanagement'
|
||||||
],
|
],
|
||||||
en: [
|
en: [
|
||||||
'Central strategic management',
|
'Central strategic management',
|
||||||
'Underground cable engineering & infrastructure',
|
'Cable network engineering & infrastructure',
|
||||||
'Coordination of heavy machinery',
|
'Coordination of heavy machinery',
|
||||||
'Central project management'
|
'Central project management'
|
||||||
]
|
]
|
||||||
|
|||||||
31
make_contact_sheet.py
Normal file
31
make_contact_sheet.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import os
|
||||||
|
import math
|
||||||
|
from PIL import Image, ImageDraw
|
||||||
|
|
||||||
|
folder = 'public/assets/photos'
|
||||||
|
files = [f for f in os.listdir(folder) if f.lower().endswith(('.jpg', '.jpeg', '.png'))]
|
||||||
|
files.sort()
|
||||||
|
|
||||||
|
thumb_w, thumb_h = 300, 300
|
||||||
|
cols = 8
|
||||||
|
rows = math.ceil(len(files) / cols)
|
||||||
|
|
||||||
|
contact_sheet = Image.new('RGB', (cols * thumb_w, rows * thumb_h), (255, 255, 255))
|
||||||
|
draw = ImageDraw.Draw(contact_sheet)
|
||||||
|
|
||||||
|
for i, f in enumerate(files):
|
||||||
|
path = os.path.join(folder, f)
|
||||||
|
try:
|
||||||
|
img = Image.open(path)
|
||||||
|
img.thumbnail((thumb_w, thumb_h - 40))
|
||||||
|
x = (i % cols) * thumb_w
|
||||||
|
y = (i // cols) * thumb_h
|
||||||
|
img_x = x + (thumb_w - img.width) // 2
|
||||||
|
img_y = y + (thumb_h - 40 - img.height) // 2
|
||||||
|
contact_sheet.paste(img, (img_x, img_y))
|
||||||
|
draw.text((x + 10, y + thumb_h - 30), f, fill=(0,0,0))
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
contact_sheet.save('contact_sheet.jpg')
|
||||||
|
print("Saved contact_sheet.jpg")
|
||||||
@@ -20,11 +20,11 @@
|
|||||||
"categories": {}
|
"categories": {}
|
||||||
},
|
},
|
||||||
"Index": {
|
"Index": {
|
||||||
"title": "E-TIB GmbH – Experts in Underground Cable Engineering & Infrastructure",
|
"title": "E-TIB GmbH – Experts in Cable network engineering & Infrastructure",
|
||||||
"description": "Your partner for underground cable engineering, horizontal drilling, and fiber optic installation in Guben and nationwide.",
|
"description": "Your partner for Cable network engineering, horizontal drilling, and fiber optic installation in Guben and nationwide.",
|
||||||
"meta": {
|
"meta": {
|
||||||
"title": "E-TIB GmbH | Specialist for Underground Cable Engineering & Grid Expansion",
|
"title": "E-TIB GmbH | Specialist for Cable network engineering & Grid Expansion",
|
||||||
"description": "E-TIB GmbH from Guben is your expert for underground cable engineering, directional drilling, plowing works, and electrical installation up to 110 kV."
|
"description": "E-TIB GmbH from Guben is your expert for Cable network engineering, directional drilling, plowing works, and electrical installation up to 110 kV."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Navigation": {
|
"Navigation": {
|
||||||
@@ -49,12 +49,12 @@
|
|||||||
"career": "Career",
|
"career": "Career",
|
||||||
"contact": "Contact",
|
"contact": "Contact",
|
||||||
"copyright": "Copyright © {year} E-TIB GmbH. All rights reserved.",
|
"copyright": "Copyright © {year} E-TIB GmbH. All rights reserved.",
|
||||||
"tagline": "Experts for underground cable engineering and the execution of electrical infrastructure projects. Quality and reliability from Guben."
|
"tagline": "Experts for Cable network engineering and the execution of electrical infrastructure projects. Quality and reliability from Guben."
|
||||||
},
|
},
|
||||||
"Team": {
|
"Team": {
|
||||||
"meta": {
|
"meta": {
|
||||||
"title": "About Us | The E-TIB Group",
|
"title": "About Us | The E-TIB Group",
|
||||||
"description": "Get to know the E-TIB Group: Specialists for underground cable engineering, drilling technology, and engineering services."
|
"description": "Get to know the E-TIB Group: Specialists for Cable network engineering, drilling technology, and engineering services."
|
||||||
},
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
"title": "Infrastructure for the Future",
|
"title": "Infrastructure for the Future",
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
"subtitle": "Specialized units for complex projects",
|
"subtitle": "Specialized units for complex projects",
|
||||||
"etib": {
|
"etib": {
|
||||||
"title": "E‑TIB GmbH",
|
"title": "E‑TIB GmbH",
|
||||||
"desc": "Execution of electrical infrastructure projects and underground cable engineering."
|
"desc": "Execution of electrical infrastructure projects and Cable network engineering."
|
||||||
},
|
},
|
||||||
"bohrtechnik": {
|
"bohrtechnik": {
|
||||||
"title": "E‑TIB Bohrtechnik GmbH",
|
"title": "E‑TIB Bohrtechnik GmbH",
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
"Contact": {
|
"Contact": {
|
||||||
"meta": {
|
"meta": {
|
||||||
"title": "Contact | E-TIB GmbH Guben",
|
"title": "Contact | E-TIB GmbH Guben",
|
||||||
"description": "Get in touch with E-TIB GmbH in Guben. Your partner for underground cable engineering and grid infrastructure."
|
"description": "Get in touch with E-TIB GmbH in Guben. Your partner for Cable network engineering and grid infrastructure."
|
||||||
},
|
},
|
||||||
"title": "Contact Us",
|
"title": "Contact Us",
|
||||||
"subtitle": "Do you have questions about our services or a specific project? We are here for you.",
|
"subtitle": "Do you have questions about our services or a specific project? We are here for you.",
|
||||||
@@ -135,7 +135,7 @@
|
|||||||
"subtitle": "Comprehensive solutions for the expansion of modern energy and data networks.",
|
"subtitle": "Comprehensive solutions for the expansion of modern energy and data networks.",
|
||||||
"badge": "Service Spectrum",
|
"badge": "Service Spectrum",
|
||||||
"items": {
|
"items": {
|
||||||
"kabelbau": "Underground Cable Engineering",
|
"kabelbau": "Cable network engineering",
|
||||||
"pflugarbeiten": "Cable Plowing",
|
"pflugarbeiten": "Cable Plowing",
|
||||||
"bohrungen": "Horizontal Directional Drilling",
|
"bohrungen": "Horizontal Directional Drilling",
|
||||||
"elektromontage": "Electrical Installations up to 110 kV",
|
"elektromontage": "Electrical Installations up to 110 kV",
|
||||||
@@ -149,7 +149,7 @@
|
|||||||
},
|
},
|
||||||
"Home": {
|
"Home": {
|
||||||
"hero": {
|
"hero": {
|
||||||
"title": "THE EXPERTS FOR <green>UNDERGROUND CABLE ENGINEERING</green>",
|
"title": "THE EXPERTS FOR <green>Cable network engineering</green>",
|
||||||
"subtitle": "We help expanding the energy cable networks for a green future.",
|
"subtitle": "We help expanding the energy cable networks for a green future.",
|
||||||
"cta": "Request now",
|
"cta": "Request now",
|
||||||
"searchPlaceholder": "Describe project or search for cables...",
|
"searchPlaceholder": "Describe project or search for cables...",
|
||||||
@@ -207,7 +207,7 @@
|
|||||||
"Blog": {
|
"Blog": {
|
||||||
"meta": {
|
"meta": {
|
||||||
"title": "Blog & News",
|
"title": "Blog & News",
|
||||||
"description": "Latest news and insights from the world of underground cable engineering and infrastructure."
|
"description": "Latest news and insights from the world of Cable network engineering and infrastructure."
|
||||||
},
|
},
|
||||||
"featuredPost": "Featured Post",
|
"featuredPost": "Featured Post",
|
||||||
"readFullArticle": "Read Full Article",
|
"readFullArticle": "Read Full Article",
|
||||||
@@ -226,7 +226,7 @@
|
|||||||
"ReferencesSlider": {
|
"ReferencesSlider": {
|
||||||
"badge": "Selected Projects",
|
"badge": "Selected Projects",
|
||||||
"title": "References & Successes",
|
"title": "References & Successes",
|
||||||
"description": "A selection of our successfully completed projects in underground cable engineering, drilling technology, and grid infrastructure.",
|
"description": "A selection of our successfully completed projects in Cable network engineering, drilling technology, and grid infrastructure.",
|
||||||
"ctaLabel": "View All References"
|
"ctaLabel": "View All References"
|
||||||
},
|
},
|
||||||
"CompanyTimeline": {
|
"CompanyTimeline": {
|
||||||
|
|||||||
@@ -4,13 +4,13 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@10.18.3",
|
"packageManager": "pnpm@10.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@medv/finder": "^4.0.2",
|
||||||
"@mintel/mail": "1.9.5",
|
"@mintel/mail": "1.9.5",
|
||||||
"@mintel/next-config": "1.9.5",
|
"@mintel/next-config": "1.9.5",
|
||||||
"@mintel/next-feedback": "1.9.5",
|
"@mintel/next-feedback": "1.9.5",
|
||||||
"@mintel/next-utils": "1.9.5",
|
"@mintel/next-utils": "1.9.5",
|
||||||
"@next/mdx": "^16.1.6",
|
"@next/mdx": "^16.1.6",
|
||||||
"@react-email/components": "1.0.8",
|
"@react-email/components": "1.0.8",
|
||||||
"@react-pdf/renderer": "^4.3.2",
|
|
||||||
"@react-three/drei": "^10.7.7",
|
"@react-three/drei": "^10.7.7",
|
||||||
"@react-three/fiber": "^9.5.0",
|
"@react-three/fiber": "^9.5.0",
|
||||||
"@sentry/nextjs": "^10.39.0",
|
"@sentry/nextjs": "^10.39.0",
|
||||||
@@ -30,7 +30,6 @@
|
|||||||
"next-intl": "^4.8.2",
|
"next-intl": "^4.8.2",
|
||||||
"next-mdx-remote": "^5.0.0",
|
"next-mdx-remote": "^5.0.0",
|
||||||
"nodemailer": "^8.0.7",
|
"nodemailer": "^8.0.7",
|
||||||
"pdf-lib": "^1.17.1",
|
|
||||||
"pino": "^10.3.0",
|
"pino": "^10.3.0",
|
||||||
"pino-pretty": "^13.1.3",
|
"pino-pretty": "^13.1.3",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
@@ -123,10 +122,6 @@
|
|||||||
"check:assets": "tsx ./scripts/check-broken-assets.ts",
|
"check:assets": "tsx ./scripts/check-broken-assets.ts",
|
||||||
"check:forms": "tsx ./scripts/check-forms.ts",
|
"check:forms": "tsx ./scripts/check-forms.ts",
|
||||||
"check:apis": "tsx ./scripts/check-apis.ts",
|
"check:apis": "tsx ./scripts/check-apis.ts",
|
||||||
"pdf:datasheets": "tsx ./scripts/generate-pdf-datasheets.tsx",
|
|
||||||
"pdf:datasheets:legacy": "tsx ./scripts/generate-pdf-datasheets-pdf-lib.ts",
|
|
||||||
"excel:datasheets": "tsx ./scripts/generate-excel-datasheets.ts",
|
|
||||||
"brochure:generate": "tsx ./scripts/generate-brochure.ts",
|
|
||||||
"assets:push:staging": "bash ./scripts/assets-sync.sh local staging",
|
"assets:push:staging": "bash ./scripts/assets-sync.sh local staging",
|
||||||
"assets:push:prod": "bash ./scripts/assets-sync.sh local prod",
|
"assets:push:prod": "bash ./scripts/assets-sync.sh local prod",
|
||||||
"assets:pull:testing": "bash ./scripts/assets-sync.sh testing local",
|
"assets:pull:testing": "bash ./scripts/assets-sync.sh testing local",
|
||||||
@@ -143,7 +138,7 @@
|
|||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"preinstall": "npx only-allow pnpm"
|
"preinstall": "npx only-allow pnpm"
|
||||||
},
|
},
|
||||||
"version": "2.2.13-rc.2",
|
"version": "2.2.13-rc.46",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"@parcel/watcher",
|
"@parcel/watcher",
|
||||||
|
|||||||
309
pnpm-lock.yaml
generated
309
pnpm-lock.yaml
generated
@@ -12,6 +12,9 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@medv/finder':
|
||||||
|
specifier: ^4.0.2
|
||||||
|
version: 4.0.2
|
||||||
'@mintel/mail':
|
'@mintel/mail':
|
||||||
specifier: 1.9.5
|
specifier: 1.9.5
|
||||||
version: 1.9.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 1.9.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
@@ -30,9 +33,6 @@ importers:
|
|||||||
'@react-email/components':
|
'@react-email/components':
|
||||||
specifier: 1.0.8
|
specifier: 1.0.8
|
||||||
version: 1.0.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 1.0.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
'@react-pdf/renderer':
|
|
||||||
specifier: ^4.3.2
|
|
||||||
version: 4.3.2(react@19.2.4)
|
|
||||||
'@react-three/drei':
|
'@react-three/drei':
|
||||||
specifier: ^10.7.7
|
specifier: ^10.7.7
|
||||||
version: 10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2))(@types/react@19.2.14)(@types/three@0.183.1)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2)
|
version: 10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2))(@types/react@19.2.14)(@types/three@0.183.1)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2)
|
||||||
@@ -90,9 +90,6 @@ importers:
|
|||||||
nodemailer:
|
nodemailer:
|
||||||
specifier: ^8.0.7
|
specifier: ^8.0.7
|
||||||
version: 8.0.7
|
version: 8.0.7
|
||||||
pdf-lib:
|
|
||||||
specifier: ^1.17.1
|
|
||||||
version: 1.17.1
|
|
||||||
pino:
|
pino:
|
||||||
specifier: ^10.3.0
|
specifier: ^10.3.0
|
||||||
version: 10.3.1
|
version: 10.3.1
|
||||||
@@ -1894,12 +1891,6 @@ packages:
|
|||||||
'@paulirish/trace_engine@0.0.53':
|
'@paulirish/trace_engine@0.0.53':
|
||||||
resolution: {integrity: sha512-PUl/vlfo08Oj804VI5nDPeSk9vyslnBlVzDDwFt8SUVxY8+KdGMkra/vrXjEEHe8gb7+RqVTfOIlGw0nyrEelA==}
|
resolution: {integrity: sha512-PUl/vlfo08Oj804VI5nDPeSk9vyslnBlVzDDwFt8SUVxY8+KdGMkra/vrXjEEHe8gb7+RqVTfOIlGw0nyrEelA==}
|
||||||
|
|
||||||
'@pdf-lib/standard-fonts@1.0.0':
|
|
||||||
resolution: {integrity: sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==}
|
|
||||||
|
|
||||||
'@pdf-lib/upng@1.0.1':
|
|
||||||
resolution: {integrity: sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==}
|
|
||||||
|
|
||||||
'@pinojs/redact@0.4.0':
|
'@pinojs/redact@0.4.0':
|
||||||
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
|
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
|
||||||
|
|
||||||
@@ -2218,49 +2209,6 @@ packages:
|
|||||||
react: ^18.0.0
|
react: ^18.0.0
|
||||||
react-dom: ^18.0.0
|
react-dom: ^18.0.0
|
||||||
|
|
||||||
'@react-pdf/fns@3.1.2':
|
|
||||||
resolution: {integrity: sha512-qTKGUf0iAMGg2+OsUcp9ffKnKi41RukM/zYIWMDJ4hRVYSr89Q7e3wSDW/Koqx3ea3Uy/z3h2y3wPX6Bdfxk6g==}
|
|
||||||
|
|
||||||
'@react-pdf/font@4.0.4':
|
|
||||||
resolution: {integrity: sha512-8YtgGtL511txIEc9AjiilpZ7yjid8uCd8OGUl6jaL3LIHnrToUupSN4IzsMQpVTCMYiDLFnDNQzpZsOYtRS/Pg==}
|
|
||||||
|
|
||||||
'@react-pdf/image@3.0.4':
|
|
||||||
resolution: {integrity: sha512-z0ogVQE0bKqgXQ5smgzIU857rLV7bMgVdrYsu3UfXDDLSzI7QPvzf6MFTFllX6Dx2rcsF13E01dqKPtJEM799g==}
|
|
||||||
|
|
||||||
'@react-pdf/layout@4.4.2':
|
|
||||||
resolution: {integrity: sha512-gNu2oh8MiGR+NJZYTJ4c4q0nWCESBI6rKFiodVhE7OeVAjtzZzd6l65wsN7HXdWJqOZD3ttD97iE+tf5SOd/Yg==}
|
|
||||||
|
|
||||||
'@react-pdf/pdfkit@4.1.0':
|
|
||||||
resolution: {integrity: sha512-Wm/IOAv0h/U5Ra94c/PltFJGcpTUd/fwVMVeFD6X9tTTPCttIwg0teRG1Lqq617J8K4W7jpL/B0HTH0mjp3QpQ==}
|
|
||||||
|
|
||||||
'@react-pdf/png-js@3.0.0':
|
|
||||||
resolution: {integrity: sha512-eSJnEItZ37WPt6Qv5pncQDxLJRK15eaRwPT+gZoujP548CodenOVp49GST8XJvKMFt9YqIBzGBV/j9AgrOQzVA==}
|
|
||||||
|
|
||||||
'@react-pdf/primitives@4.1.1':
|
|
||||||
resolution: {integrity: sha512-IuhxYls1luJb7NUWy6q5avb1XrNaVj9bTNI40U9qGRuS6n7Hje/8H8Qi99Z9UKFV74bBP3DOf3L1wV2qZVgVrQ==}
|
|
||||||
|
|
||||||
'@react-pdf/reconciler@2.0.0':
|
|
||||||
resolution: {integrity: sha512-7zaPRujpbHSmCpIrZ+b9HSTJHthcVZzX0Wx7RzvQGsGBUbHP4p6s5itXrAIOuQuPvDepoHGNOvf6xUuMVvdoyw==}
|
|
||||||
peerDependencies:
|
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
|
||||||
|
|
||||||
'@react-pdf/render@4.3.2':
|
|
||||||
resolution: {integrity: sha512-el5KYM1sH/PKcO4tRCIm8/AIEmhtraaONbwCrBhFdehoGv6JtgnXiMxHGAvZbI5kEg051GbyP+XIU6f6YbOu6Q==}
|
|
||||||
|
|
||||||
'@react-pdf/renderer@4.3.2':
|
|
||||||
resolution: {integrity: sha512-EhPkj35gO9rXIyyx29W3j3axemvVY5RigMmlK4/6Ku0pXB8z9PEE/sz4ZBOShu2uot6V4xiCR3aG+t9IjJJlBQ==}
|
|
||||||
peerDependencies:
|
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
|
||||||
|
|
||||||
'@react-pdf/stylesheet@6.1.2':
|
|
||||||
resolution: {integrity: sha512-E3ftGRYUQGKiN3JOgtGsLDo0hGekA6dmkmi/MYACytmPTKxQRBSO3126MebmCq+t1rgU9uRlREIEawJ+8nzSbw==}
|
|
||||||
|
|
||||||
'@react-pdf/textkit@6.1.0':
|
|
||||||
resolution: {integrity: sha512-sFlzDC9CDFrJsnL3B/+NHrk9+Advqk7iJZIStiYQDdskbow8GF/AGYrpIk+vWSnh35YxaGbHkqXq53XOxnyrjQ==}
|
|
||||||
|
|
||||||
'@react-pdf/types@2.9.2':
|
|
||||||
resolution: {integrity: sha512-dufvpKId9OajLLbgn9q7VLUmyo1Jf+iyGk2ZHmCL8nIDtL8N1Ejh9TH7+pXXrR0tdie1nmnEb5Bz9U7g4hI4/g==}
|
|
||||||
|
|
||||||
'@react-three/drei@10.7.7':
|
'@react-three/drei@10.7.7':
|
||||||
resolution: {integrity: sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==}
|
resolution: {integrity: sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3435,9 +3383,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==}
|
resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==}
|
||||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
|
|
||||||
abs-svg-path@0.1.1:
|
|
||||||
resolution: {integrity: sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==}
|
|
||||||
|
|
||||||
accepts@1.3.8:
|
accepts@1.3.8:
|
||||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -3773,9 +3718,6 @@ packages:
|
|||||||
brotli@1.3.3:
|
brotli@1.3.3:
|
||||||
resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==}
|
resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==}
|
||||||
|
|
||||||
browserify-zlib@0.2.0:
|
|
||||||
resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==}
|
|
||||||
|
|
||||||
browserslist@4.28.2:
|
browserslist@4.28.2:
|
||||||
resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
|
resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
|
||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
@@ -3970,9 +3912,6 @@ packages:
|
|||||||
color-name@1.1.4:
|
color-name@1.1.4:
|
||||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
|
|
||||||
color-string@1.9.1:
|
|
||||||
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
|
|
||||||
|
|
||||||
colorette@2.0.20:
|
colorette@2.0.20:
|
||||||
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
||||||
|
|
||||||
@@ -4109,9 +4048,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
crypto-js@4.2.0:
|
|
||||||
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
|
|
||||||
|
|
||||||
crypto-random-string@2.0.0:
|
crypto-random-string@2.0.0:
|
||||||
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
|
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -4427,9 +4363,6 @@ packages:
|
|||||||
electron-to-chromium@1.5.330:
|
electron-to-chromium@1.5.330:
|
||||||
resolution: {integrity: sha512-jFNydB5kFtYUobh4IkWUnXeyDbjf/r9gcUEXe1xcrcUxIGfTdzPXA+ld6zBRbwvgIGVzDll/LTIiDztEtckSnA==}
|
resolution: {integrity: sha512-jFNydB5kFtYUobh4IkWUnXeyDbjf/r9gcUEXe1xcrcUxIGfTdzPXA+ld6zBRbwvgIGVzDll/LTIiDztEtckSnA==}
|
||||||
|
|
||||||
emoji-regex-xs@1.0.0:
|
|
||||||
resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==}
|
|
||||||
|
|
||||||
emoji-regex@10.6.0:
|
emoji-regex@10.6.0:
|
||||||
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
|
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
|
||||||
|
|
||||||
@@ -5167,12 +5100,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==}
|
resolution: {integrity: sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==}
|
||||||
engines: {node: '>= 6.0.0'}
|
engines: {node: '>= 6.0.0'}
|
||||||
|
|
||||||
hsl-to-hex@1.0.0:
|
|
||||||
resolution: {integrity: sha512-K6GVpucS5wFf44X0h2bLVRDsycgJmf9FF2elg+CrqD8GcFU8c6vYhgXn8NjUkFCwj+xDFb70qgLbTUm6sxwPmA==}
|
|
||||||
|
|
||||||
hsl-to-rgb-for-reals@1.1.1:
|
|
||||||
resolution: {integrity: sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==}
|
|
||||||
|
|
||||||
html-encoding-sniffer@6.0.0:
|
html-encoding-sniffer@6.0.0:
|
||||||
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
|
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
|
||||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||||
@@ -5254,9 +5181,6 @@ packages:
|
|||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
hyphen@1.14.1:
|
|
||||||
resolution: {integrity: sha512-kvL8xYl5QMTh+LwohVN72ciOxC0OEV79IPdJSTwEXok9y9QHebXGdFgrED4sWfiax/ODx++CAMk3hMy4XPJPOw==}
|
|
||||||
|
|
||||||
i18next-fs-backend@2.6.3:
|
i18next-fs-backend@2.6.3:
|
||||||
resolution: {integrity: sha512-/5aW996nbolGh/qVU9urjAJ2QiXpiyGuhG8x/ZvvZQ3bMlrK+ouUdD6whS4e+sU9CCdUIzl7x/heEjsBh/3xtw==}
|
resolution: {integrity: sha512-/5aW996nbolGh/qVU9urjAJ2QiXpiyGuhG8x/ZvvZQ3bMlrK+ouUdD6whS4e+sU9CCdUIzl7x/heEjsBh/3xtw==}
|
||||||
|
|
||||||
@@ -5390,9 +5314,6 @@ packages:
|
|||||||
is-arrayish@0.2.1:
|
is-arrayish@0.2.1:
|
||||||
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
|
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
|
||||||
|
|
||||||
is-arrayish@0.3.4:
|
|
||||||
resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==}
|
|
||||||
|
|
||||||
is-async-function@2.1.1:
|
is-async-function@2.1.1:
|
||||||
resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==}
|
resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -5551,9 +5472,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
|
resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
is-url@1.2.4:
|
|
||||||
resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
|
|
||||||
|
|
||||||
is-weakmap@2.0.2:
|
is-weakmap@2.0.2:
|
||||||
resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
|
resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -5595,9 +5513,6 @@ packages:
|
|||||||
jackspeak@3.4.3:
|
jackspeak@3.4.3:
|
||||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||||
|
|
||||||
jay-peg@1.1.1:
|
|
||||||
resolution: {integrity: sha512-D62KEuBxz/ip2gQKOEhk/mx14o7eiFRaU+VNNSP4MOiIkwb/D6B3G1Mfas7C/Fit8EsSV2/IWjZElx/Gs6A4ww==}
|
|
||||||
|
|
||||||
jest-worker@27.5.1:
|
jest-worker@27.5.1:
|
||||||
resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
|
resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
|
||||||
engines: {node: '>= 10.13.0'}
|
engines: {node: '>= 10.13.0'}
|
||||||
@@ -6031,9 +5946,6 @@ packages:
|
|||||||
mdn-data@2.27.1:
|
mdn-data@2.27.1:
|
||||||
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
|
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
|
||||||
|
|
||||||
media-engine@1.0.3:
|
|
||||||
resolution: {integrity: sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==}
|
|
||||||
|
|
||||||
media-typer@0.3.0:
|
media-typer@0.3.0:
|
||||||
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -6380,9 +6292,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
normalize-svg-path@1.1.0:
|
|
||||||
resolution: {integrity: sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==}
|
|
||||||
|
|
||||||
npm-run-path@4.0.1:
|
npm-run-path@4.0.1:
|
||||||
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
|
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -6529,9 +6438,6 @@ packages:
|
|||||||
pako@0.2.9:
|
pako@0.2.9:
|
||||||
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
|
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
|
||||||
|
|
||||||
pako@1.0.11:
|
|
||||||
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
|
||||||
|
|
||||||
parent-module@1.0.1:
|
parent-module@1.0.1:
|
||||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -6550,9 +6456,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
|
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
parse-svg-path@0.1.2:
|
|
||||||
resolution: {integrity: sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==}
|
|
||||||
|
|
||||||
parse5-htmlparser2-tree-adapter@7.1.0:
|
parse5-htmlparser2-tree-adapter@7.1.0:
|
||||||
resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==}
|
resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==}
|
||||||
|
|
||||||
@@ -6604,9 +6507,6 @@ packages:
|
|||||||
pause-stream@0.0.11:
|
pause-stream@0.0.11:
|
||||||
resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==}
|
resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==}
|
||||||
|
|
||||||
pdf-lib@1.17.1:
|
|
||||||
resolution: {integrity: sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==}
|
|
||||||
|
|
||||||
pdf-parse@2.4.5:
|
pdf-parse@2.4.5:
|
||||||
resolution: {integrity: sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg==}
|
resolution: {integrity: sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg==}
|
||||||
engines: {node: '>=20.16.0 <21 || >=22.3.0'}
|
engines: {node: '>=20.16.0 <21 || >=22.3.0'}
|
||||||
@@ -6823,9 +6723,6 @@ packages:
|
|||||||
queue-microtask@1.2.3:
|
queue-microtask@1.2.3:
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||||
|
|
||||||
queue@6.0.2:
|
|
||||||
resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==}
|
|
||||||
|
|
||||||
quick-format-unescaped@4.0.4:
|
quick-format-unescaped@4.0.4:
|
||||||
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
||||||
|
|
||||||
@@ -7119,9 +7016,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
||||||
engines: {node: '>=v12.22.7'}
|
engines: {node: '>=v12.22.7'}
|
||||||
|
|
||||||
scheduler@0.25.0-rc-603e6108-20241029:
|
|
||||||
resolution: {integrity: sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==}
|
|
||||||
|
|
||||||
scheduler@0.27.0:
|
scheduler@0.27.0:
|
||||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||||
|
|
||||||
@@ -7219,9 +7113,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
simple-swizzle@0.2.4:
|
|
||||||
resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==}
|
|
||||||
|
|
||||||
sirv@2.0.4:
|
sirv@2.0.4:
|
||||||
resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
|
resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
@@ -7398,9 +7289,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
|
resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
string_decoder@1.3.0:
|
|
||||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
|
||||||
|
|
||||||
stringify-entities@4.0.4:
|
stringify-entities@4.0.4:
|
||||||
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
||||||
|
|
||||||
@@ -7486,9 +7374,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: '>=17.0'
|
react: '>=17.0'
|
||||||
|
|
||||||
svg-arc-to-cubic-bezier@3.2.0:
|
|
||||||
resolution: {integrity: sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==}
|
|
||||||
|
|
||||||
svg-to-pdfkit@0.1.8:
|
svg-to-pdfkit@0.1.8:
|
||||||
resolution: {integrity: sha512-QItiGZBy5TstGy+q8mjQTMGRlDDOARXLxH+sgVm1n/LYeo0zFcQlcCh8m4zi8QxctrxB9Kue/lStc/RD5iLadQ==}
|
resolution: {integrity: sha512-QItiGZBy5TstGy+q8mjQTMGRlDDOARXLxH+sgVm1n/LYeo0zFcQlcCh8m4zi8QxctrxB9Kue/lStc/RD5iLadQ==}
|
||||||
|
|
||||||
@@ -7872,10 +7757,6 @@ packages:
|
|||||||
victory-vendor@37.3.6:
|
victory-vendor@37.3.6:
|
||||||
resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==}
|
resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==}
|
||||||
|
|
||||||
vite-compatible-readable-stream@3.6.1:
|
|
||||||
resolution: {integrity: sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==}
|
|
||||||
engines: {node: '>= 6'}
|
|
||||||
|
|
||||||
vite@8.0.3:
|
vite@8.0.3:
|
||||||
resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==}
|
resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -8214,9 +8095,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
|
resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
yoga-layout@3.2.1:
|
|
||||||
resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==}
|
|
||||||
|
|
||||||
zod-validation-error@4.0.2:
|
zod-validation-error@4.0.2:
|
||||||
resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
|
resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
@@ -9860,14 +9738,6 @@ snapshots:
|
|||||||
legacy-javascript: 0.0.1
|
legacy-javascript: 0.0.1
|
||||||
third-party-web: 0.29.2
|
third-party-web: 0.29.2
|
||||||
|
|
||||||
'@pdf-lib/standard-fonts@1.0.0':
|
|
||||||
dependencies:
|
|
||||||
pako: 1.0.11
|
|
||||||
|
|
||||||
'@pdf-lib/upng@1.0.1':
|
|
||||||
dependencies:
|
|
||||||
pako: 1.0.11
|
|
||||||
|
|
||||||
'@pinojs/redact@0.4.0': {}
|
'@pinojs/redact@0.4.0': {}
|
||||||
|
|
||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
@@ -10147,107 +10017,6 @@ snapshots:
|
|||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
react-dom: 19.2.4(react@19.2.4)
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
|
||||||
'@react-pdf/fns@3.1.2': {}
|
|
||||||
|
|
||||||
'@react-pdf/font@4.0.4':
|
|
||||||
dependencies:
|
|
||||||
'@react-pdf/pdfkit': 4.1.0
|
|
||||||
'@react-pdf/types': 2.9.2
|
|
||||||
fontkit: 2.0.4
|
|
||||||
is-url: 1.2.4
|
|
||||||
|
|
||||||
'@react-pdf/image@3.0.4':
|
|
||||||
dependencies:
|
|
||||||
'@react-pdf/png-js': 3.0.0
|
|
||||||
jay-peg: 1.1.1
|
|
||||||
|
|
||||||
'@react-pdf/layout@4.4.2':
|
|
||||||
dependencies:
|
|
||||||
'@react-pdf/fns': 3.1.2
|
|
||||||
'@react-pdf/image': 3.0.4
|
|
||||||
'@react-pdf/primitives': 4.1.1
|
|
||||||
'@react-pdf/stylesheet': 6.1.2
|
|
||||||
'@react-pdf/textkit': 6.1.0
|
|
||||||
'@react-pdf/types': 2.9.2
|
|
||||||
emoji-regex-xs: 1.0.0
|
|
||||||
queue: 6.0.2
|
|
||||||
yoga-layout: 3.2.1
|
|
||||||
|
|
||||||
'@react-pdf/pdfkit@4.1.0':
|
|
||||||
dependencies:
|
|
||||||
'@babel/runtime': 7.29.2
|
|
||||||
'@react-pdf/png-js': 3.0.0
|
|
||||||
browserify-zlib: 0.2.0
|
|
||||||
crypto-js: 4.2.0
|
|
||||||
fontkit: 2.0.4
|
|
||||||
jay-peg: 1.1.1
|
|
||||||
linebreak: 1.1.0
|
|
||||||
vite-compatible-readable-stream: 3.6.1
|
|
||||||
|
|
||||||
'@react-pdf/png-js@3.0.0':
|
|
||||||
dependencies:
|
|
||||||
browserify-zlib: 0.2.0
|
|
||||||
|
|
||||||
'@react-pdf/primitives@4.1.1': {}
|
|
||||||
|
|
||||||
'@react-pdf/reconciler@2.0.0(react@19.2.4)':
|
|
||||||
dependencies:
|
|
||||||
object-assign: 4.1.1
|
|
||||||
react: 19.2.4
|
|
||||||
scheduler: 0.25.0-rc-603e6108-20241029
|
|
||||||
|
|
||||||
'@react-pdf/render@4.3.2':
|
|
||||||
dependencies:
|
|
||||||
'@babel/runtime': 7.29.2
|
|
||||||
'@react-pdf/fns': 3.1.2
|
|
||||||
'@react-pdf/primitives': 4.1.1
|
|
||||||
'@react-pdf/textkit': 6.1.0
|
|
||||||
'@react-pdf/types': 2.9.2
|
|
||||||
abs-svg-path: 0.1.1
|
|
||||||
color-string: 1.9.1
|
|
||||||
normalize-svg-path: 1.1.0
|
|
||||||
parse-svg-path: 0.1.2
|
|
||||||
svg-arc-to-cubic-bezier: 3.2.0
|
|
||||||
|
|
||||||
'@react-pdf/renderer@4.3.2(react@19.2.4)':
|
|
||||||
dependencies:
|
|
||||||
'@babel/runtime': 7.29.2
|
|
||||||
'@react-pdf/fns': 3.1.2
|
|
||||||
'@react-pdf/font': 4.0.4
|
|
||||||
'@react-pdf/layout': 4.4.2
|
|
||||||
'@react-pdf/pdfkit': 4.1.0
|
|
||||||
'@react-pdf/primitives': 4.1.1
|
|
||||||
'@react-pdf/reconciler': 2.0.0(react@19.2.4)
|
|
||||||
'@react-pdf/render': 4.3.2
|
|
||||||
'@react-pdf/types': 2.9.2
|
|
||||||
events: 3.3.0
|
|
||||||
object-assign: 4.1.1
|
|
||||||
prop-types: 15.8.1
|
|
||||||
queue: 6.0.2
|
|
||||||
react: 19.2.4
|
|
||||||
|
|
||||||
'@react-pdf/stylesheet@6.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@react-pdf/fns': 3.1.2
|
|
||||||
'@react-pdf/types': 2.9.2
|
|
||||||
color-string: 1.9.1
|
|
||||||
hsl-to-hex: 1.0.0
|
|
||||||
media-engine: 1.0.3
|
|
||||||
postcss-value-parser: 4.2.0
|
|
||||||
|
|
||||||
'@react-pdf/textkit@6.1.0':
|
|
||||||
dependencies:
|
|
||||||
'@react-pdf/fns': 3.1.2
|
|
||||||
bidi-js: 1.0.3
|
|
||||||
hyphen: 1.14.1
|
|
||||||
unicode-properties: 1.4.1
|
|
||||||
|
|
||||||
'@react-pdf/types@2.9.2':
|
|
||||||
dependencies:
|
|
||||||
'@react-pdf/font': 4.0.4
|
|
||||||
'@react-pdf/primitives': 4.1.1
|
|
||||||
'@react-pdf/stylesheet': 6.1.2
|
|
||||||
|
|
||||||
'@react-three/drei@10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2))(@types/react@19.2.14)(@types/three@0.183.1)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2)':
|
'@react-three/drei@10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2))(@types/react@19.2.14)(@types/three@0.183.1)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.29.2
|
'@babel/runtime': 7.29.2
|
||||||
@@ -11426,8 +11195,6 @@ snapshots:
|
|||||||
|
|
||||||
abbrev@2.0.0: {}
|
abbrev@2.0.0: {}
|
||||||
|
|
||||||
abs-svg-path@0.1.1: {}
|
|
||||||
|
|
||||||
accepts@1.3.8:
|
accepts@1.3.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
@@ -11748,10 +11515,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
base64-js: 1.5.1
|
base64-js: 1.5.1
|
||||||
|
|
||||||
browserify-zlib@0.2.0:
|
|
||||||
dependencies:
|
|
||||||
pako: 1.0.11
|
|
||||||
|
|
||||||
browserslist@4.28.2:
|
browserslist@4.28.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
baseline-browser-mapping: 2.10.13
|
baseline-browser-mapping: 2.10.13
|
||||||
@@ -11966,11 +11729,6 @@ snapshots:
|
|||||||
|
|
||||||
color-name@1.1.4: {}
|
color-name@1.1.4: {}
|
||||||
|
|
||||||
color-string@1.9.1:
|
|
||||||
dependencies:
|
|
||||||
color-name: 1.1.4
|
|
||||||
simple-swizzle: 0.2.4
|
|
||||||
|
|
||||||
colorette@2.0.20: {}
|
colorette@2.0.20: {}
|
||||||
|
|
||||||
combined-stream@1.0.8:
|
combined-stream@1.0.8:
|
||||||
@@ -12117,8 +11875,6 @@ snapshots:
|
|||||||
shebang-command: 2.0.0
|
shebang-command: 2.0.0
|
||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
|
|
||||||
crypto-js@4.2.0: {}
|
|
||||||
|
|
||||||
crypto-random-string@2.0.0: {}
|
crypto-random-string@2.0.0: {}
|
||||||
|
|
||||||
csp_evaluator@1.1.5: {}
|
csp_evaluator@1.1.5: {}
|
||||||
@@ -12450,8 +12206,6 @@ snapshots:
|
|||||||
|
|
||||||
electron-to-chromium@1.5.330: {}
|
electron-to-chromium@1.5.330: {}
|
||||||
|
|
||||||
emoji-regex-xs@1.0.0: {}
|
|
||||||
|
|
||||||
emoji-regex@10.6.0: {}
|
emoji-regex@10.6.0: {}
|
||||||
|
|
||||||
emoji-regex@8.0.0: {}
|
emoji-regex@8.0.0: {}
|
||||||
@@ -13513,12 +13267,6 @@ snapshots:
|
|||||||
|
|
||||||
hoopy@0.1.4: {}
|
hoopy@0.1.4: {}
|
||||||
|
|
||||||
hsl-to-hex@1.0.0:
|
|
||||||
dependencies:
|
|
||||||
hsl-to-rgb-for-reals: 1.1.1
|
|
||||||
|
|
||||||
hsl-to-rgb-for-reals@1.1.1: {}
|
|
||||||
|
|
||||||
html-encoding-sniffer@6.0.0(@noble/hashes@1.8.0):
|
html-encoding-sniffer@6.0.0(@noble/hashes@1.8.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@exodus/bytes': 1.15.0(@noble/hashes@1.8.0)
|
'@exodus/bytes': 1.15.0(@noble/hashes@1.8.0)
|
||||||
@@ -13617,8 +13365,6 @@ snapshots:
|
|||||||
|
|
||||||
husky@9.1.7: {}
|
husky@9.1.7: {}
|
||||||
|
|
||||||
hyphen@1.14.1: {}
|
|
||||||
|
|
||||||
i18next-fs-backend@2.6.3: {}
|
i18next-fs-backend@2.6.3: {}
|
||||||
|
|
||||||
i18next@25.10.10(typescript@5.9.3):
|
i18next@25.10.10(typescript@5.9.3):
|
||||||
@@ -13770,8 +13516,6 @@ snapshots:
|
|||||||
|
|
||||||
is-arrayish@0.2.1: {}
|
is-arrayish@0.2.1: {}
|
||||||
|
|
||||||
is-arrayish@0.3.4: {}
|
|
||||||
|
|
||||||
is-async-function@2.1.1:
|
is-async-function@2.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
async-function: 1.0.0
|
async-function: 1.0.0
|
||||||
@@ -13909,8 +13653,6 @@ snapshots:
|
|||||||
|
|
||||||
is-unicode-supported@2.1.0: {}
|
is-unicode-supported@2.1.0: {}
|
||||||
|
|
||||||
is-url@1.2.4: {}
|
|
||||||
|
|
||||||
is-weakmap@2.0.2: {}
|
is-weakmap@2.0.2: {}
|
||||||
|
|
||||||
is-weakref@1.1.1:
|
is-weakref@1.1.1:
|
||||||
@@ -13961,10 +13703,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@pkgjs/parseargs': 0.11.0
|
'@pkgjs/parseargs': 0.11.0
|
||||||
|
|
||||||
jay-peg@1.1.1:
|
|
||||||
dependencies:
|
|
||||||
restructure: 3.0.2
|
|
||||||
|
|
||||||
jest-worker@27.5.1:
|
jest-worker@27.5.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.19.15
|
'@types/node': 22.19.15
|
||||||
@@ -14513,8 +14251,6 @@ snapshots:
|
|||||||
|
|
||||||
mdn-data@2.27.1: {}
|
mdn-data@2.27.1: {}
|
||||||
|
|
||||||
media-engine@1.0.3: {}
|
|
||||||
|
|
||||||
media-typer@0.3.0: {}
|
media-typer@0.3.0: {}
|
||||||
|
|
||||||
meow@13.2.0: {}
|
meow@13.2.0: {}
|
||||||
@@ -14981,10 +14717,6 @@ snapshots:
|
|||||||
|
|
||||||
normalize-path@3.0.0: {}
|
normalize-path@3.0.0: {}
|
||||||
|
|
||||||
normalize-svg-path@1.1.0:
|
|
||||||
dependencies:
|
|
||||||
svg-arc-to-cubic-bezier: 3.2.0
|
|
||||||
|
|
||||||
npm-run-path@4.0.1:
|
npm-run-path@4.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 3.1.1
|
path-key: 3.1.1
|
||||||
@@ -15195,8 +14927,6 @@ snapshots:
|
|||||||
|
|
||||||
pako@0.2.9: {}
|
pako@0.2.9: {}
|
||||||
|
|
||||||
pako@1.0.11: {}
|
|
||||||
|
|
||||||
parent-module@1.0.1:
|
parent-module@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
callsites: 3.1.0
|
callsites: 3.1.0
|
||||||
@@ -15224,8 +14954,6 @@ snapshots:
|
|||||||
json-parse-even-better-errors: 2.3.1
|
json-parse-even-better-errors: 2.3.1
|
||||||
lines-and-columns: 1.2.4
|
lines-and-columns: 1.2.4
|
||||||
|
|
||||||
parse-svg-path@0.1.2: {}
|
|
||||||
|
|
||||||
parse5-htmlparser2-tree-adapter@7.1.0:
|
parse5-htmlparser2-tree-adapter@7.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
domhandler: 5.0.3
|
domhandler: 5.0.3
|
||||||
@@ -15276,13 +15004,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
through: 2.3.8
|
through: 2.3.8
|
||||||
|
|
||||||
pdf-lib@1.17.1:
|
|
||||||
dependencies:
|
|
||||||
'@pdf-lib/standard-fonts': 1.0.0
|
|
||||||
'@pdf-lib/upng': 1.0.1
|
|
||||||
pako: 1.0.11
|
|
||||||
tslib: 1.14.1
|
|
||||||
|
|
||||||
pdf-parse@2.4.5:
|
pdf-parse@2.4.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@napi-rs/canvas': 0.1.80
|
'@napi-rs/canvas': 0.1.80
|
||||||
@@ -15532,10 +15253,6 @@ snapshots:
|
|||||||
|
|
||||||
queue-microtask@1.2.3: {}
|
queue-microtask@1.2.3: {}
|
||||||
|
|
||||||
queue@6.0.2:
|
|
||||||
dependencies:
|
|
||||||
inherits: 2.0.4
|
|
||||||
|
|
||||||
quick-format-unescaped@4.0.4: {}
|
quick-format-unescaped@4.0.4: {}
|
||||||
|
|
||||||
range-parser@1.2.1: {}
|
range-parser@1.2.1: {}
|
||||||
@@ -15954,8 +15671,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
xmlchars: 2.2.0
|
xmlchars: 2.2.0
|
||||||
|
|
||||||
scheduler@0.25.0-rc-603e6108-20241029: {}
|
|
||||||
|
|
||||||
scheduler@0.27.0: {}
|
scheduler@0.27.0: {}
|
||||||
|
|
||||||
schema-dts@1.1.5: {}
|
schema-dts@1.1.5: {}
|
||||||
@@ -16108,10 +15823,6 @@ snapshots:
|
|||||||
|
|
||||||
signal-exit@4.1.0: {}
|
signal-exit@4.1.0: {}
|
||||||
|
|
||||||
simple-swizzle@0.2.4:
|
|
||||||
dependencies:
|
|
||||||
is-arrayish: 0.3.4
|
|
||||||
|
|
||||||
sirv@2.0.4:
|
sirv@2.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@polka/url': 1.0.0-next.29
|
'@polka/url': 1.0.0-next.29
|
||||||
@@ -16348,10 +16059,6 @@ snapshots:
|
|||||||
define-properties: 1.2.1
|
define-properties: 1.2.1
|
||||||
es-object-atoms: 1.1.1
|
es-object-atoms: 1.1.1
|
||||||
|
|
||||||
string_decoder@1.3.0:
|
|
||||||
dependencies:
|
|
||||||
safe-buffer: 5.2.1
|
|
||||||
|
|
||||||
stringify-entities@4.0.4:
|
stringify-entities@4.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
character-entities-html4: 2.1.0
|
character-entities-html4: 2.1.0
|
||||||
@@ -16422,8 +16129,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
|
|
||||||
svg-arc-to-cubic-bezier@3.2.0: {}
|
|
||||||
|
|
||||||
svg-to-pdfkit@0.1.8:
|
svg-to-pdfkit@0.1.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
pdfkit: 0.18.0
|
pdfkit: 0.18.0
|
||||||
@@ -16892,12 +16597,6 @@ snapshots:
|
|||||||
d3-time: 3.1.0
|
d3-time: 3.1.0
|
||||||
d3-timer: 3.0.1
|
d3-timer: 3.0.1
|
||||||
|
|
||||||
vite-compatible-readable-stream@3.6.1:
|
|
||||||
dependencies:
|
|
||||||
inherits: 2.0.4
|
|
||||||
string_decoder: 1.3.0
|
|
||||||
util-deprecate: 1.0.2
|
|
||||||
|
|
||||||
vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
|
vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
lightningcss: 1.32.0
|
lightningcss: 1.32.0
|
||||||
@@ -17229,8 +16928,6 @@ snapshots:
|
|||||||
|
|
||||||
yoctocolors@2.1.2: {}
|
yoctocolors@2.1.2: {}
|
||||||
|
|
||||||
yoga-layout@3.2.1: {}
|
|
||||||
|
|
||||||
zod-validation-error@4.0.2(zod@3.25.76):
|
zod-validation-error@4.0.2(zod@3.25.76):
|
||||||
dependencies:
|
dependencies:
|
||||||
zod: 3.25.76
|
zod: 3.25.76
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
#!/usr/bin/env ts-node
|
|
||||||
/**
|
|
||||||
* Master Brochure Generator
|
|
||||||
*
|
|
||||||
* Generates a multi-page PDF brochure containing all products.
|
|
||||||
* Combines Payload CMS marketing text with Excel technical data.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import * as React from 'react';
|
|
||||||
import { renderToBuffer } from '@react-pdf/renderer';
|
|
||||||
import { getAllProducts } from '../lib/products';
|
|
||||||
import { getExcelTechnicalDataForProduct, preloadExcelData } from '../lib/excel-products';
|
|
||||||
import { PDFBrochure } from '../lib/pdf-brochure';
|
|
||||||
import { ProductData as PDFProductData } from '../lib/pdf-datasheet';
|
|
||||||
|
|
||||||
const CONFIG = {
|
|
||||||
outputDir: path.join(process.cwd(), 'public/downloads'),
|
|
||||||
locales: ['en', 'de'] as const,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
async function ensureOutputDir() {
|
|
||||||
if (!fs.existsSync(CONFIG.outputDir)) {
|
|
||||||
fs.mkdirSync(CONFIG.outputDir, { recursive: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateBrochure(locale: 'en' | 'de') {
|
|
||||||
console.log(`\nGenerating brochure for locale: ${locale.toUpperCase()}...`);
|
|
||||||
|
|
||||||
// 1. Load data
|
|
||||||
const products = await getAllProducts(locale);
|
|
||||||
console.log(`- Loaded ${products.length} products from Payload`);
|
|
||||||
|
|
||||||
// 2. Map and Enrich
|
|
||||||
const pdfProducts: PDFProductData[] = products.map((p, index) => {
|
|
||||||
const excelData = getExcelTechnicalDataForProduct({
|
|
||||||
slug: p.slug,
|
|
||||||
sku: p.frontmatter.sku,
|
|
||||||
name: p.frontmatter.title,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: index + 1,
|
|
||||||
name: p.frontmatter.title,
|
|
||||||
sku: p.frontmatter.sku,
|
|
||||||
shortDescriptionHtml: p.frontmatter.description,
|
|
||||||
descriptionHtml: p.frontmatter.description,
|
|
||||||
applicationHtml: '',
|
|
||||||
featuredImage: p.frontmatter.images[0] || null,
|
|
||||||
images: p.frontmatter.images,
|
|
||||||
categories: p.frontmatter.categories.map(name => ({ name })),
|
|
||||||
attributes: excelData?.attributes || [],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (pdfProducts.length === 0) {
|
|
||||||
console.warn(`! No products found for ${locale}. Skipping.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Render
|
|
||||||
console.log(`- Rendering PDF with ${pdfProducts.length} pages...`);
|
|
||||||
const buffer = await renderToBuffer(
|
|
||||||
React.createElement(PDFBrochure, {
|
|
||||||
products: pdfProducts,
|
|
||||||
locale,
|
|
||||||
title: locale === 'de' ? 'Produktkatalog' : 'Product Catalog',
|
|
||||||
subtitle: '2026',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// 4. Save
|
|
||||||
const fileName = `KLZ_Cables_Brochure_2026_${locale.toUpperCase()}.pdf`;
|
|
||||||
const filePath = path.join(CONFIG.outputDir, fileName);
|
|
||||||
fs.writeFileSync(filePath, buffer);
|
|
||||||
|
|
||||||
const stats = fs.statSync(filePath);
|
|
||||||
console.log(`✓ Generated: ${fileName} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const start = Date.now();
|
|
||||||
console.log('--- KLZ Brochure Generator ---');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await ensureOutputDir();
|
|
||||||
preloadExcelData();
|
|
||||||
|
|
||||||
for (const locale of CONFIG.locales) {
|
|
||||||
await generateBrochure(locale);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\n✅ Brochure generation completed in ${((Date.now() - start) / 1000).toFixed(2)}s`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('\n❌ Fatal error:', error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(console.error);
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,8 @@ module.exports = {
|
|||||||
'./app/**/*.{js,ts,jsx,tsx}',
|
'./app/**/*.{js,ts,jsx,tsx}',
|
||||||
'./components/**/*.{js,ts,jsx,tsx}',
|
'./components/**/*.{js,ts,jsx,tsx}',
|
||||||
'./content/**/*.{mdx,md}',
|
'./content/**/*.{mdx,md}',
|
||||||
|
'./node_modules/@mintel/annotator/dist/**/*.{js,ts,jsx,tsx,mjs}',
|
||||||
|
'./node_modules/.pnpm/@mintel+annotator*/node_modules/@mintel/annotator/dist/**/*.{js,ts,jsx,tsx,mjs}'
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
|||||||
@@ -7,21 +7,21 @@ describe('Task 6 Content Updates', () => {
|
|||||||
const enHomePath = path.join(process.cwd(), 'content', 'en', 'home.mdx');
|
const enHomePath = path.join(process.cwd(), 'content', 'en', 'home.mdx');
|
||||||
const deKompetenzenPath = path.join(process.cwd(), 'content', 'de', 'kompetenzen.mdx');
|
const deKompetenzenPath = path.join(process.cwd(), 'content', 'de', 'kompetenzen.mdx');
|
||||||
const enKompetenzenPath = path.join(process.cwd(), 'content', 'en', 'kompetenzen.mdx');
|
const enKompetenzenPath = path.join(process.cwd(), 'content', 'en', 'kompetenzen.mdx');
|
||||||
const deKabeltiefbauPath = path.join(process.cwd(), 'content', 'de', 'kabeltiefbau.mdx');
|
const deKabelnetzbauPath = path.join(process.cwd(), 'content', 'de', 'kabeltiefbau.mdx');
|
||||||
const enKabeltiefbauPath = path.join(process.cwd(), 'content', 'en', 'kabeltiefbau.mdx');
|
const enKabelnetzbauPath = path.join(process.cwd(), 'content', 'en', 'kabeltiefbau.mdx');
|
||||||
const deUeberUnsPath = path.join(process.cwd(), 'content', 'de', 'ueber-uns.mdx');
|
const deUeberUnsPath = path.join(process.cwd(), 'content', 'de', 'ueber-uns.mdx');
|
||||||
const enUeberUnsPath = path.join(process.cwd(), 'content', 'en', 'ueber-uns.mdx');
|
const enUeberUnsPath = path.join(process.cwd(), 'content', 'en', 'ueber-uns.mdx');
|
||||||
|
|
||||||
it('should verify Kabelleitungstiefbau changes in DE and EN kompetenzen.mdx', () => {
|
it('should verify Kabelleitungsnetzbau changes in DE and EN kompetenzen.mdx', () => {
|
||||||
const deContent = fs.readFileSync(deKompetenzenPath, 'utf8');
|
const deContent = fs.readFileSync(deKompetenzenPath, 'utf8');
|
||||||
const enContent = fs.readFileSync(enKompetenzenPath, 'utf8');
|
const enContent = fs.readFileSync(enKompetenzenPath, 'utf8');
|
||||||
|
|
||||||
// DE Kompetenzen
|
// DE Kompetenzen
|
||||||
expect(deContent).toContain('title: "Kabelleitungstiefbau"');
|
expect(deContent).toContain('title: "Kabelleitungsnetzbau"');
|
||||||
expect(deContent).toContain('description: "Klassischer Grabenbau, professionelle Verlegung und Kabelmontage (Hoch-, Mittel- und Niederspannung) bis 110 kV."');
|
expect(deContent).toContain('description: "Klassischer Grabenbau, professionelle Verlegung und Kabelmontage (Hoch-, Mittel- und Niederspannung) bis 110 kV."');
|
||||||
expect(deContent).toContain('title: "Kabelleitungstiefbau (Hoch-/Mittel- und Niederspannung)"');
|
expect(deContent).toContain('title: "Kabelleitungsnetzbau (Hoch-/Mittel- und Niederspannung)"');
|
||||||
expect(deContent).toContain('"Kabelmontagen bis 110 kV"');
|
expect(deContent).toContain('"Kabelmontagen bis 110 kV"');
|
||||||
expect(deContent).toContain('"name": "Kabelleitungstiefbau"');
|
expect(deContent).toContain('"name": "Kabelleitungsnetzbau"');
|
||||||
expect(deContent).toContain('"description": "Klassischer Grabenbau, professionelle Verlegung und Kabelmontage (Hoch-, Mittel- und Niederspannung) bis 110 kV."');
|
expect(deContent).toContain('"description": "Klassischer Grabenbau, professionelle Verlegung und Kabelmontage (Hoch-, Mittel- und Niederspannung) bis 110 kV."');
|
||||||
|
|
||||||
// EN Competencies
|
// EN Competencies
|
||||||
@@ -33,13 +33,13 @@ describe('Task 6 Content Updates', () => {
|
|||||||
expect(enContent).toContain('"description": "Classic trench construction, professional laying and cable assembly (high, medium, and low voltage) up to 110 kV."');
|
expect(enContent).toContain('"description": "Classic trench construction, professional laying and cable assembly (high, medium, and low voltage) up to 110 kV."');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should verify Kabelleitungstiefbau changes in DE and EN home.mdx', () => {
|
it('should verify Kabelleitungsnetzbau changes in DE and EN home.mdx', () => {
|
||||||
const deContent = fs.readFileSync(deHomePath, 'utf8');
|
const deContent = fs.readFileSync(deHomePath, 'utf8');
|
||||||
const enContent = fs.readFileSync(enHomePath, 'utf8');
|
const enContent = fs.readFileSync(enHomePath, 'utf8');
|
||||||
|
|
||||||
// DE Home
|
// DE Home
|
||||||
expect(deContent).toContain('title: "Kabelleitungstiefbau"');
|
expect(deContent).toContain('title: "Kabelleitungsnetzbau"');
|
||||||
expect(deContent).toContain('description: "Kabelleitungstiefbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen bis 110 kV"');
|
expect(deContent).toContain('description: "Kabelleitungsnetzbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen bis 110 kV"');
|
||||||
expect(deContent).toContain('100+');
|
expect(deContent).toContain('100+');
|
||||||
expect(deContent).toContain('Mitarbeitende');
|
expect(deContent).toContain('Mitarbeitende');
|
||||||
|
|
||||||
@@ -50,18 +50,18 @@ describe('Task 6 Content Updates', () => {
|
|||||||
expect(enContent).toContain('Employees');
|
expect(enContent).toContain('Employees');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should verify Kabelleitungstiefbau changes in DE and EN kabeltiefbau.mdx', () => {
|
it('should verify Kabelleitungsnetzbau changes in DE and EN kabeltiefbau.mdx', () => {
|
||||||
const deContent = fs.readFileSync(deKabeltiefbauPath, 'utf8');
|
const deContent = fs.readFileSync(deKabelnetzbauPath, 'utf8');
|
||||||
const enContent = fs.readFileSync(enKabeltiefbauPath, 'utf8');
|
const enContent = fs.readFileSync(enKabelnetzbauPath, 'utf8');
|
||||||
|
|
||||||
// DE Kabeltiefbau
|
// DE Kabelnetzbau
|
||||||
expect(deContent).toContain('title: "Kabelleitungstiefbau"');
|
expect(deContent).toContain('title: "Kabelleitungsnetzbau"');
|
||||||
expect(deContent).toContain('excerpt: "Professioneller Kabelleitungstiefbau');
|
expect(deContent).toContain('excerpt: "Professioneller Kabelleitungsnetzbau');
|
||||||
expect(deContent).toContain('Kabelmontagen bis 110 kV');
|
expect(deContent).toContain('Kabelmontagen bis 110 kV');
|
||||||
expect(deContent).toContain('subtitle="Komplette Infrastruktur-Lösungen für Hoch-, Mittel- und Niederspannungsnetze sowie Kabelmontagen bis 110 kV"');
|
expect(deContent).toContain('subtitle="Komplette Infrastruktur-Lösungen für Hoch-, Mittel- und Niederspannungsnetze sowie Kabelmontagen bis 110 kV"');
|
||||||
expect(deContent).toContain('"Kabelmontagen bis 110 kV"');
|
expect(deContent).toContain('"Kabelmontagen bis 110 kV"');
|
||||||
|
|
||||||
// EN Kabeltiefbau
|
// EN Kabelnetzbau
|
||||||
expect(enContent).toContain('title: "Cable Civil Engineering"');
|
expect(enContent).toContain('title: "Cable Civil Engineering"');
|
||||||
expect(enContent).toContain('excerpt: "Professional cable civil engineering');
|
expect(enContent).toContain('excerpt: "Professional cable civil engineering');
|
||||||
expect(enContent).toContain('cable assembly up to 110 kV');
|
expect(enContent).toContain('cable assembly up to 110 kV');
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ describe('E2E Navigation & Content (MDX Architecture)', () => {
|
|||||||
it('should verify blog listing and post loading', async () => {
|
it('should verify blog listing and post loading', async () => {
|
||||||
await page.goto(`${BASE_URL}/de/blog`, { waitUntil: 'networkidle2', timeout: 60000 });
|
await page.goto(`${BASE_URL}/de/blog`, { waitUntil: 'networkidle2', timeout: 60000 });
|
||||||
let bodyText = await page.evaluate(() => document.body.innerText);
|
let bodyText = await page.evaluate(() => document.body.innerText);
|
||||||
expect(bodyText).toContain('Moderne Verfahren im Kabeltiefbau');
|
expect(bodyText).toContain('Moderne Verfahren im Kabelnetzbau');
|
||||||
|
|
||||||
// Click on the first article (if found) or navigate directly
|
// Click on the first article (if found) or navigate directly
|
||||||
await page.goto(`${BASE_URL}/de/blog/moderne-verfahren-kabeltiefbau`, { waitUntil: 'networkidle2', timeout: 60000 });
|
await page.goto(`${BASE_URL}/de/blog/moderne-verfahren-kabeltiefbau`, { waitUntil: 'networkidle2', timeout: 60000 });
|
||||||
|
|||||||
247
venv/bin/Activate.ps1
Normal file
247
venv/bin/Activate.ps1
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
<#
|
||||||
|
.Synopsis
|
||||||
|
Activate a Python virtual environment for the current PowerShell session.
|
||||||
|
|
||||||
|
.Description
|
||||||
|
Pushes the python executable for a virtual environment to the front of the
|
||||||
|
$Env:PATH environment variable and sets the prompt to signify that you are
|
||||||
|
in a Python virtual environment. Makes use of the command line switches as
|
||||||
|
well as the `pyvenv.cfg` file values present in the virtual environment.
|
||||||
|
|
||||||
|
.Parameter VenvDir
|
||||||
|
Path to the directory that contains the virtual environment to activate. The
|
||||||
|
default value for this is the parent of the directory that the Activate.ps1
|
||||||
|
script is located within.
|
||||||
|
|
||||||
|
.Parameter Prompt
|
||||||
|
The prompt prefix to display when this virtual environment is activated. By
|
||||||
|
default, this prompt is the name of the virtual environment folder (VenvDir)
|
||||||
|
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1
|
||||||
|
Activates the Python virtual environment that contains the Activate.ps1 script.
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1 -Verbose
|
||||||
|
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||||
|
and shows extra information about the activation as it executes.
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
|
||||||
|
Activates the Python virtual environment located in the specified location.
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1 -Prompt "MyPython"
|
||||||
|
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||||
|
and prefixes the current prompt with the specified string (surrounded in
|
||||||
|
parentheses) while the virtual environment is active.
|
||||||
|
|
||||||
|
.Notes
|
||||||
|
On Windows, it may be required to enable this Activate.ps1 script by setting the
|
||||||
|
execution policy for the user. You can do this by issuing the following PowerShell
|
||||||
|
command:
|
||||||
|
|
||||||
|
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||||
|
|
||||||
|
For more information on Execution Policies:
|
||||||
|
https://go.microsoft.com/fwlink/?LinkID=135170
|
||||||
|
|
||||||
|
#>
|
||||||
|
Param(
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[String]
|
||||||
|
$VenvDir,
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[String]
|
||||||
|
$Prompt
|
||||||
|
)
|
||||||
|
|
||||||
|
<# Function declarations --------------------------------------------------- #>
|
||||||
|
|
||||||
|
<#
|
||||||
|
.Synopsis
|
||||||
|
Remove all shell session elements added by the Activate script, including the
|
||||||
|
addition of the virtual environment's Python executable from the beginning of
|
||||||
|
the PATH variable.
|
||||||
|
|
||||||
|
.Parameter NonDestructive
|
||||||
|
If present, do not remove this function from the global namespace for the
|
||||||
|
session.
|
||||||
|
|
||||||
|
#>
|
||||||
|
function global:deactivate ([switch]$NonDestructive) {
|
||||||
|
# Revert to original values
|
||||||
|
|
||||||
|
# The prior prompt:
|
||||||
|
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
|
||||||
|
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
|
||||||
|
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
|
||||||
|
}
|
||||||
|
|
||||||
|
# The prior PYTHONHOME:
|
||||||
|
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
|
||||||
|
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
|
||||||
|
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
|
||||||
|
}
|
||||||
|
|
||||||
|
# The prior PATH:
|
||||||
|
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
|
||||||
|
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
|
||||||
|
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just remove the VIRTUAL_ENV altogether:
|
||||||
|
if (Test-Path -Path Env:VIRTUAL_ENV) {
|
||||||
|
Remove-Item -Path env:VIRTUAL_ENV
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just remove VIRTUAL_ENV_PROMPT altogether.
|
||||||
|
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
|
||||||
|
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
|
||||||
|
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
|
||||||
|
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
# Leave deactivate function in the global namespace if requested:
|
||||||
|
if (-not $NonDestructive) {
|
||||||
|
Remove-Item -Path function:deactivate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<#
|
||||||
|
.Description
|
||||||
|
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
|
||||||
|
given folder, and returns them in a map.
|
||||||
|
|
||||||
|
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
|
||||||
|
two strings separated by `=` (with any amount of whitespace surrounding the =)
|
||||||
|
then it is considered a `key = value` line. The left hand string is the key,
|
||||||
|
the right hand is the value.
|
||||||
|
|
||||||
|
If the value starts with a `'` or a `"` then the first and last character is
|
||||||
|
stripped from the value before being captured.
|
||||||
|
|
||||||
|
.Parameter ConfigDir
|
||||||
|
Path to the directory that contains the `pyvenv.cfg` file.
|
||||||
|
#>
|
||||||
|
function Get-PyVenvConfig(
|
||||||
|
[String]
|
||||||
|
$ConfigDir
|
||||||
|
) {
|
||||||
|
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
|
||||||
|
|
||||||
|
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
|
||||||
|
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
|
||||||
|
|
||||||
|
# An empty map will be returned if no config file is found.
|
||||||
|
$pyvenvConfig = @{ }
|
||||||
|
|
||||||
|
if ($pyvenvConfigPath) {
|
||||||
|
|
||||||
|
Write-Verbose "File exists, parse `key = value` lines"
|
||||||
|
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
|
||||||
|
|
||||||
|
$pyvenvConfigContent | ForEach-Object {
|
||||||
|
$keyval = $PSItem -split "\s*=\s*", 2
|
||||||
|
if ($keyval[0] -and $keyval[1]) {
|
||||||
|
$val = $keyval[1]
|
||||||
|
|
||||||
|
# Remove extraneous quotations around a string value.
|
||||||
|
if ("'""".Contains($val.Substring(0, 1))) {
|
||||||
|
$val = $val.Substring(1, $val.Length - 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
$pyvenvConfig[$keyval[0]] = $val
|
||||||
|
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $pyvenvConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
<# Begin Activate script --------------------------------------------------- #>
|
||||||
|
|
||||||
|
# Determine the containing directory of this script
|
||||||
|
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$VenvExecDir = Get-Item -Path $VenvExecPath
|
||||||
|
|
||||||
|
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
|
||||||
|
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
|
||||||
|
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
|
||||||
|
|
||||||
|
# Set values required in priority: CmdLine, ConfigFile, Default
|
||||||
|
# First, get the location of the virtual environment, it might not be
|
||||||
|
# VenvExecDir if specified on the command line.
|
||||||
|
if ($VenvDir) {
|
||||||
|
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
|
||||||
|
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
|
||||||
|
Write-Verbose "VenvDir=$VenvDir"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Next, read the `pyvenv.cfg` file to determine any required value such
|
||||||
|
# as `prompt`.
|
||||||
|
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
|
||||||
|
|
||||||
|
# Next, set the prompt from the command line, or the config file, or
|
||||||
|
# just use the name of the virtual environment folder.
|
||||||
|
if ($Prompt) {
|
||||||
|
Write-Verbose "Prompt specified as argument, using '$Prompt'"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
|
||||||
|
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
|
||||||
|
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
|
||||||
|
$Prompt = $pyvenvCfg['prompt'];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
|
||||||
|
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
|
||||||
|
$Prompt = Split-Path -Path $venvDir -Leaf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Verbose "Prompt = '$Prompt'"
|
||||||
|
Write-Verbose "VenvDir='$VenvDir'"
|
||||||
|
|
||||||
|
# Deactivate any currently active virtual environment, but leave the
|
||||||
|
# deactivate function in place.
|
||||||
|
deactivate -nondestructive
|
||||||
|
|
||||||
|
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
|
||||||
|
# that there is an activated venv.
|
||||||
|
$env:VIRTUAL_ENV = $VenvDir
|
||||||
|
|
||||||
|
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
|
||||||
|
|
||||||
|
Write-Verbose "Setting prompt to '$Prompt'"
|
||||||
|
|
||||||
|
# Set the prompt to include the env name
|
||||||
|
# Make sure _OLD_VIRTUAL_PROMPT is global
|
||||||
|
function global:_OLD_VIRTUAL_PROMPT { "" }
|
||||||
|
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
|
||||||
|
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
|
||||||
|
|
||||||
|
function global:prompt {
|
||||||
|
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
|
||||||
|
_OLD_VIRTUAL_PROMPT
|
||||||
|
}
|
||||||
|
$env:VIRTUAL_ENV_PROMPT = $Prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clear PYTHONHOME
|
||||||
|
if (Test-Path -Path Env:PYTHONHOME) {
|
||||||
|
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
|
||||||
|
Remove-Item -Path Env:PYTHONHOME
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add the venv to the PATH
|
||||||
|
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
|
||||||
|
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
|
||||||
63
venv/bin/activate
Normal file
63
venv/bin/activate
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# This file must be used with "source bin/activate" *from bash*
|
||||||
|
# you cannot run it directly
|
||||||
|
|
||||||
|
deactivate () {
|
||||||
|
# reset old environment variables
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
|
||||||
|
PATH="${_OLD_VIRTUAL_PATH:-}"
|
||||||
|
export PATH
|
||||||
|
unset _OLD_VIRTUAL_PATH
|
||||||
|
fi
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
|
||||||
|
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
|
||||||
|
export PYTHONHOME
|
||||||
|
unset _OLD_VIRTUAL_PYTHONHOME
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Call hash to forget past commands. Without forgetting
|
||||||
|
# past commands the $PATH changes we made may not be respected
|
||||||
|
hash -r 2> /dev/null
|
||||||
|
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
||||||
|
PS1="${_OLD_VIRTUAL_PS1:-}"
|
||||||
|
export PS1
|
||||||
|
unset _OLD_VIRTUAL_PS1
|
||||||
|
fi
|
||||||
|
|
||||||
|
unset VIRTUAL_ENV
|
||||||
|
unset VIRTUAL_ENV_PROMPT
|
||||||
|
if [ ! "${1:-}" = "nondestructive" ] ; then
|
||||||
|
# Self destruct!
|
||||||
|
unset -f deactivate
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# unset irrelevant variables
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
VIRTUAL_ENV="/Volumes/Alpha SSD/Coding/e-tib.com/venv"
|
||||||
|
export VIRTUAL_ENV
|
||||||
|
|
||||||
|
_OLD_VIRTUAL_PATH="$PATH"
|
||||||
|
PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
export PATH
|
||||||
|
|
||||||
|
# unset PYTHONHOME if set
|
||||||
|
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
|
||||||
|
# could use `if (set -u; : $PYTHONHOME) ;` in bash
|
||||||
|
if [ -n "${PYTHONHOME:-}" ] ; then
|
||||||
|
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
|
||||||
|
unset PYTHONHOME
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
||||||
|
_OLD_VIRTUAL_PS1="${PS1:-}"
|
||||||
|
PS1="(venv) ${PS1:-}"
|
||||||
|
export PS1
|
||||||
|
VIRTUAL_ENV_PROMPT="(venv) "
|
||||||
|
export VIRTUAL_ENV_PROMPT
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Call hash to forget past commands. Without forgetting
|
||||||
|
# past commands the $PATH changes we made may not be respected
|
||||||
|
hash -r 2> /dev/null
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user