Compare commits
15 Commits
v2.2.13-rc
...
v2.2.13-rc
| Author | SHA1 | Date | |
|---|---|---|---|
| d8d35da574 | |||
| 792d91f9d5 | |||
| d10d038617 | |||
| 490ce4abb6 | |||
| bed9be1f81 | |||
| f52a3e3c46 | |||
| 71825724db | |||
| 1bb26e8db4 | |||
| 604b96afe0 | |||
| 59df260164 | |||
| 3449d2eaa4 | |||
| 1723be4008 | |||
| 830d425859 | |||
| d6b9aca50f | |||
| caefd5d525 |
@@ -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
|
||||
|
||||
# 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/
|
||||
|
||||
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 });
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export async function generateMetadata(props: {
|
||||
template: '%s | E-TIB',
|
||||
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),
|
||||
manifest: '/manifest.webmanifest',
|
||||
alternates: {
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -9,27 +9,28 @@ export async function getAnnotatorAssets() {
|
||||
publicDir = path.join(process.cwd(), 'public');
|
||||
}
|
||||
|
||||
const walk = (dir: string): string[] => {
|
||||
const walk = async (dir: string): Promise<string[]> => {
|
||||
let results: string[] = [];
|
||||
if (!fs.existsSync(dir)) return results;
|
||||
const list = fs.readdirSync(dir);
|
||||
list.forEach((file) => {
|
||||
|
||||
const list = await fs.promises.readdir(dir);
|
||||
for (const file of list) {
|
||||
const filePath = path.join(dir, file);
|
||||
const stat = fs.statSync(filePath);
|
||||
const stat = await fs.promises.stat(filePath);
|
||||
if (stat && stat.isDirectory()) {
|
||||
results = results.concat(walk(filePath));
|
||||
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 = walk(publicDir);
|
||||
console.log('[Annotator] publicDir:', publicDir);
|
||||
console.log('[Annotator] Assets found:', results.length);
|
||||
console.log('[Annotator] First 5 paths:', results.slice(0, 5));
|
||||
|
||||
|
||||
const results = await walk(publicDir);
|
||||
|
||||
// Console logs removed for absolute silence & speed
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export default function TestPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1>TEST PAGE WORKS</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { Annotator } from '@mintel/annotator';
|
||||
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 => {
|
||||
console.log('[AnnotatorClientWrapper] fetchedAssets:', fetchedAssets);
|
||||
if (Array.isArray(fetchedAssets)) {
|
||||
setAssets(fetchedAssets);
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('[AnnotatorClientWrapper] Action failed:', err);
|
||||
});
|
||||
getAnnotatorAssets()
|
||||
.then(fetchedAssets => {
|
||||
if (Array.isArray(fetchedAssets)) {
|
||||
setAssets(fetchedAssets);
|
||||
}
|
||||
})
|
||||
.catch(err => console.error('[AnnotatorClientWrapper] Fetch failed:', err));
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (annotations: any[]) => {
|
||||
|
||||
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";
|
||||
@@ -1,13 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useRef } from 'react';
|
||||
import React, { useState, useRef, useCallback, useMemo } from 'react';
|
||||
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 { useRouter, usePathname } from 'next/navigation';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
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 { useLocale, useTranslations } from 'next-intl';
|
||||
|
||||
@@ -28,6 +28,59 @@ interface InteractiveGermanyMapProps {
|
||||
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({
|
||||
badge,
|
||||
title,
|
||||
@@ -40,22 +93,21 @@ export function InteractiveGermanyMap({
|
||||
const [activeLocation, setActiveLocation] = useState<Location | null>(null);
|
||||
const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const handleMouseEnter = (loc: Location) => {
|
||||
const handleMouseEnter = useCallback((loc: Location) => {
|
||||
if (hoverTimeoutRef.current) {
|
||||
clearTimeout(hoverTimeoutRef.current);
|
||||
hoverTimeoutRef.current = null;
|
||||
}
|
||||
setActiveLocation(loc);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
setActiveLocation(null);
|
||||
}, 200);
|
||||
hoverTimeoutRef.current = timeout;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const locale = useLocale();
|
||||
const t = useTranslations('InteractiveGermanyMap');
|
||||
@@ -67,14 +119,11 @@ export function InteractiveGermanyMap({
|
||||
];
|
||||
|
||||
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 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;
|
||||
|
||||
// Ensure the 3 standorte are always included, without duplicating if already present by ID
|
||||
const base = [...locations];
|
||||
standorteLocations.forEach(sl => {
|
||||
if (!base.some(l => l.id === sl.id)) {
|
||||
@@ -84,19 +133,28 @@ export function InteractiveGermanyMap({
|
||||
return base;
|
||||
}, [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 (
|
||||
<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`}>
|
||||
{/* Animated Border Glow */}
|
||||
{!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 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'}`}>
|
||||
|
||||
{/* Content Left */}
|
||||
<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">
|
||||
<MapPin className="w-4 h-4" />
|
||||
@@ -117,7 +175,6 @@ export function InteractiveGermanyMap({
|
||||
{finalDescription}
|
||||
</p>
|
||||
|
||||
{/* Industrial Stats Grid */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{finalStats.map((stat, i) => {
|
||||
const StatCard = () => (
|
||||
@@ -148,85 +205,32 @@ export function InteractiveGermanyMap({
|
||||
</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">
|
||||
{/* Map Container - Enforce strict aspect ratio matching the SVG (1024x1024) */}
|
||||
<div className="relative w-full max-w-[600px] aspect-square group pointer-events-auto">
|
||||
|
||||
{/* Map SVG */}
|
||||
<div className="absolute inset-0 opacity-[0.25] mix-blend-screen drop-shadow-2xl brightness-200 pointer-events-none">
|
||||
<Image
|
||||
src="/germany-map.svg"
|
||||
alt={locale === 'en' ? "Map of Germany" : "Deutschlandkarte"}
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
{mapBackground}
|
||||
|
||||
{finalLocations.filter(l => l.type === 'minor_node').map((loc, idx) => (
|
||||
<MinorNode
|
||||
key={`minor-${loc.id || idx}`}
|
||||
loc={loc}
|
||||
isActive={activeLocation === loc}
|
||||
onEnter={handleMouseEnter}
|
||||
onLeave={handleMouseLeave}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Minor Location Markers (The 100+ generic projects) */}
|
||||
{finalLocations.filter(l => l.type === 'minor_node').map((loc, idx) => {
|
||||
const isActive = activeLocation === loc;
|
||||
return (
|
||||
<div
|
||||
key={`minor-${idx}`}
|
||||
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={() => 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>
|
||||
)})}
|
||||
{finalLocations.filter(l => l.type !== 'minor_node').map((loc, idx) => (
|
||||
<MajorNode
|
||||
key={loc.id}
|
||||
loc={loc}
|
||||
idx={idx}
|
||||
isActive={activeLocation?.id === loc.id}
|
||||
onEnter={handleMouseEnter}
|
||||
onLeave={handleMouseLeave}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* 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>
|
||||
{activeLocation && (
|
||||
<motion.div
|
||||
@@ -245,7 +249,6 @@ export function InteractiveGermanyMap({
|
||||
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">
|
||||
|
||||
{activeLocation.featuredImage && (
|
||||
<div className="relative w-full h-32 bg-neutral-900">
|
||||
<Image
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
@@ -46,7 +46,7 @@ export function Footer({ companyInfo }: FooterProps) {
|
||||
<p className="text-neutral-400 text-base leading-relaxed max-w-sm">
|
||||
{locale === 'de'
|
||||
? '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>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
|
||||
setTimeout(() => {
|
||||
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(() => {
|
||||
setIsTransitioning((current) => {
|
||||
if (current) {
|
||||
@@ -69,7 +69,7 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
return current;
|
||||
});
|
||||
}, 5000);
|
||||
}, 60000);
|
||||
}, 700);
|
||||
}, [isTransitioning, router]);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Kabelleitungstiefbau im Bereich Telekommunikation"
|
||||
title: "Kabelleitungsnetzbau im Bereich Telekommunikation"
|
||||
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"
|
||||
---
|
||||
|
||||
|
||||
@@ -117,12 +117,12 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
|
||||
ctaHref="/de/kompetenzen"
|
||||
items={[
|
||||
{
|
||||
title: "Kabelleitungstiefbau",
|
||||
description: "Kabelleitungstiefbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen bis 110 kV",
|
||||
title: "Kabelleitungsnetzbau",
|
||||
description: "Kabelleitungsnetzbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen bis 110 kV",
|
||||
tag: "Energie",
|
||||
size: "large",
|
||||
href: "/de/kabelnetzbau",
|
||||
image: { url: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-54.jpg", alt: "Kabelleitungstiefbau" }
|
||||
href: "/de/kabeltiefbau",
|
||||
image: { url: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-54.jpg", alt: "Kabelleitungsnetzbau" }
|
||||
},
|
||||
{
|
||||
title: "Bohrtechnik",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
title: "Kabelleitungstiefbau"
|
||||
title: "Kabelleitungsnetzbau"
|
||||
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"
|
||||
---
|
||||
|
||||
<HeroSection
|
||||
badge="Kernkompetenz"
|
||||
title="Kabelleitungstiefbau"
|
||||
title="Kabelleitungsnetzbau"
|
||||
subtitle="Komplette Infrastruktur-Lösungen für Hoch-, Mittel- und Niederspannungsnetze sowie Kabelmontagen bis 110 kV"
|
||||
backgroundImage={{ url: '/assets/photos/DSC01123.JPG' }}
|
||||
alignment="center"
|
||||
|
||||
@@ -8,7 +8,7 @@ layout: "fullBleed"
|
||||
<HomeHero
|
||||
badge="Unsere Leistungen"
|
||||
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"
|
||||
linkText="Kontakt aufnehmen"
|
||||
linkHref="/de/kontakt"
|
||||
@@ -23,12 +23,12 @@ layout: "fullBleed"
|
||||
ctaHref="/de/kontakt"
|
||||
items={[
|
||||
{
|
||||
title: "Kabelleitungstiefbau",
|
||||
title: "Kabelleitungsnetzbau",
|
||||
description: "Klassischer Grabenbau, professionelle Verlegung und Kabelmontage (Hoch-, Mittel- und Niederspannung) bis 110 kV.",
|
||||
tag: "Energie",
|
||||
size: "large",
|
||||
href: "/de/kabelnetzbau",
|
||||
image: { url: "/assets/photos/DSC01123.JPG", alt: "Kabelleitungstiefbau" }
|
||||
href: "/de/kabeltiefbau",
|
||||
image: { url: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-54.jpg", alt: "Kabelleitungsnetzbau" }
|
||||
},
|
||||
{
|
||||
title: "Bohrtechnik",
|
||||
@@ -36,7 +36,7 @@ layout: "fullBleed"
|
||||
tag: "Innovation",
|
||||
size: "medium",
|
||||
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",
|
||||
@@ -70,7 +70,7 @@ layout: "fullBleed"
|
||||
panels={[
|
||||
{
|
||||
id: "energy",
|
||||
title: "Kabelleitungstiefbau (Hoch-/Mittel- und Niederspannung)",
|
||||
title: "Kabelleitungsnetzbau (Hoch-/Mittel- und Niederspannung)",
|
||||
icon: "energy",
|
||||
lists: [
|
||||
[
|
||||
@@ -86,7 +86,7 @@ layout: "fullBleed"
|
||||
},
|
||||
{
|
||||
id: "fiber",
|
||||
title: "Kabelleitungstiefbau (Telekommunikation)",
|
||||
title: "Kabelleitungsnetzbau (Telekommunikation)",
|
||||
icon: "fiber",
|
||||
lists: [
|
||||
[
|
||||
@@ -143,7 +143,7 @@ layout: "fullBleed"
|
||||
<JsonLd data={[
|
||||
{
|
||||
"@type": "Service",
|
||||
"name": "Kabelleitungstiefbau",
|
||||
"name": "Kabelleitungsnetzbau",
|
||||
"description": "Klassischer Grabenbau, professionelle Verlegung und Kabelmontage (Hoch-, Mittel- und Niederspannung) bis 110 kV.",
|
||||
"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">
|
||||
<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>
|
||||
|
||||
## Fundament für den Projekterfolg
|
||||
|
||||
@@ -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."
|
||||
---
|
||||
|
||||
<HomeHero
|
||||
|
||||
title={"The Experts for\nCable Construction"}
|
||||
description="We realize complex supply lines for a modern society. Reliable, innovative and with the highest precision in cable construction and drilling technology."
|
||||
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 network construction and drilling technology."
|
||||
videoUrl="/assets/videos/web/hero-kabelpflug.mp4"
|
||||
linkText="Our Services"
|
||||
linkHref="/en/competencies"
|
||||
@@ -121,7 +121,7 @@ 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",
|
||||
tag: "Energy",
|
||||
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" }
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Competencies - Cable Civil Engineering & Horizontal Directional Drilling"
|
||||
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"
|
||||
---
|
||||
|
||||
@@ -27,8 +27,8 @@ layout: "fullBleed"
|
||||
description: "Classic trench construction, professional laying and cable assembly (high, medium, and low voltage) up to 110 kV.",
|
||||
tag: "Energy",
|
||||
size: "large",
|
||||
href: "/en/cable-civil-engineering",
|
||||
image: { url: "/assets/photos/DSC01123.JPG", alt: "Cable Civil Engineering" }
|
||||
href: "/en/kabeltiefbau",
|
||||
image: { url: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-54.jpg", alt: "Cable Civil Engineering" }
|
||||
},
|
||||
{
|
||||
title: "Drilling Technology",
|
||||
@@ -36,7 +36,7 @@ layout: "fullBleed"
|
||||
tag: "Innovation",
|
||||
size: "medium",
|
||||
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",
|
||||
|
||||
@@ -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,
|
||||
logo: 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',
|
||||
email: 'info@e-tib.com',
|
||||
address: {
|
||||
|
||||
@@ -38,7 +38,7 @@ export const standorteData: StandortData[] = [
|
||||
shortName: 'Guben (Hauptsitz)',
|
||||
type: 'hq',
|
||||
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.'
|
||||
},
|
||||
address: {
|
||||
@@ -56,13 +56,13 @@ export const standorteData: StandortData[] = [
|
||||
keyFeatures: {
|
||||
de: [
|
||||
'Zentrale strategische Steuerung',
|
||||
'Kabelleitungstiefbau & Infrastruktur',
|
||||
'Kabelleitungsnetzbau & Infrastruktur',
|
||||
'Koordination Großmaschinenpark',
|
||||
'Zentrales Projektmanagement'
|
||||
],
|
||||
en: [
|
||||
'Central strategic management',
|
||||
'Underground cable engineering & infrastructure',
|
||||
'Cable network engineering & infrastructure',
|
||||
'Coordination of heavy machinery',
|
||||
'Central project management'
|
||||
]
|
||||
|
||||
@@ -20,11 +20,11 @@
|
||||
"categories": {}
|
||||
},
|
||||
"Index": {
|
||||
"title": "E-TIB GmbH – Experts in Underground Cable Engineering & Infrastructure",
|
||||
"description": "Your partner for underground cable engineering, horizontal drilling, and fiber optic installation in Guben and nationwide.",
|
||||
"title": "E-TIB GmbH – Experts in Cable network engineering & Infrastructure",
|
||||
"description": "Your partner for Cable network engineering, horizontal drilling, and fiber optic installation in Guben and nationwide.",
|
||||
"meta": {
|
||||
"title": "E-TIB GmbH | Specialist for Underground Cable 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."
|
||||
"title": "E-TIB GmbH | Specialist for Cable network engineering & Grid Expansion",
|
||||
"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": {
|
||||
@@ -49,12 +49,12 @@
|
||||
"career": "Career",
|
||||
"contact": "Contact",
|
||||
"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": {
|
||||
"meta": {
|
||||
"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": {
|
||||
"title": "Infrastructure for the Future",
|
||||
@@ -71,7 +71,7 @@
|
||||
"subtitle": "Specialized units for complex projects",
|
||||
"etib": {
|
||||
"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": {
|
||||
"title": "E‑TIB Bohrtechnik GmbH",
|
||||
@@ -90,7 +90,7 @@
|
||||
"Contact": {
|
||||
"meta": {
|
||||
"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",
|
||||
"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.",
|
||||
"badge": "Service Spectrum",
|
||||
"items": {
|
||||
"kabelbau": "Underground Cable Engineering",
|
||||
"kabelbau": "Cable network engineering",
|
||||
"pflugarbeiten": "Cable Plowing",
|
||||
"bohrungen": "Horizontal Directional Drilling",
|
||||
"elektromontage": "Electrical Installations up to 110 kV",
|
||||
@@ -149,7 +149,7 @@
|
||||
},
|
||||
"Home": {
|
||||
"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.",
|
||||
"cta": "Request now",
|
||||
"searchPlaceholder": "Describe project or search for cables...",
|
||||
@@ -207,7 +207,7 @@
|
||||
"Blog": {
|
||||
"meta": {
|
||||
"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",
|
||||
"readFullArticle": "Read Full Article",
|
||||
@@ -226,7 +226,7 @@
|
||||
"ReferencesSlider": {
|
||||
"badge": "Selected Projects",
|
||||
"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"
|
||||
},
|
||||
"CompanyTimeline": {
|
||||
|
||||
Binary file not shown.
11
package.json
11
package.json
@@ -4,14 +4,13 @@
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.18.3",
|
||||
"dependencies": {
|
||||
"@mintel/annotator": "file:./mintel-annotator-1.0.7.tgz",
|
||||
"@medv/finder": "^4.0.2",
|
||||
"@mintel/mail": "1.9.5",
|
||||
"@mintel/next-config": "1.9.5",
|
||||
"@mintel/next-feedback": "1.9.5",
|
||||
"@mintel/next-utils": "1.9.5",
|
||||
"@next/mdx": "^16.1.6",
|
||||
"@react-email/components": "1.0.8",
|
||||
"@react-pdf/renderer": "^4.3.2",
|
||||
"@react-three/drei": "^10.7.7",
|
||||
"@react-three/fiber": "^9.5.0",
|
||||
"@sentry/nextjs": "^10.39.0",
|
||||
@@ -26,13 +25,11 @@
|
||||
"ioredis": "^5.9.3",
|
||||
"jsdom": "^27.4.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"mintel-annotator": "link:mintel-annotator",
|
||||
"next": "16.1.6",
|
||||
"next-i18next": "^15.4.3",
|
||||
"next-intl": "^4.8.2",
|
||||
"next-mdx-remote": "^5.0.0",
|
||||
"nodemailer": "^8.0.7",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pino": "^10.3.0",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"react": "^19.2.4",
|
||||
@@ -125,10 +122,6 @@
|
||||
"check:assets": "tsx ./scripts/check-broken-assets.ts",
|
||||
"check:forms": "tsx ./scripts/check-forms.ts",
|
||||
"check:apis": "tsx ./scripts/check-apis.ts",
|
||||
"pdf:datasheets": "tsx ./scripts/generate-pdf-datasheets.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:prod": "bash ./scripts/assets-sync.sh local prod",
|
||||
"assets:pull:testing": "bash ./scripts/assets-sync.sh testing local",
|
||||
@@ -145,7 +138,7 @@
|
||||
"prepare": "husky",
|
||||
"preinstall": "npx only-allow pnpm"
|
||||
},
|
||||
"version": "2.2.13-rc.42",
|
||||
"version": "2.2.13-rc.46",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
|
||||
383
pnpm-lock.yaml
generated
383
pnpm-lock.yaml
generated
@@ -12,9 +12,9 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@mintel/annotator':
|
||||
specifier: file:./mintel-annotator-1.0.7.tgz
|
||||
version: file:mintel-annotator-1.0.7.tgz(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@medv/finder':
|
||||
specifier: ^4.0.2
|
||||
version: 4.0.2
|
||||
'@mintel/mail':
|
||||
specifier: 1.9.5
|
||||
version: 1.9.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
@@ -33,9 +33,6 @@ importers:
|
||||
'@react-email/components':
|
||||
specifier: 1.0.8
|
||||
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':
|
||||
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)
|
||||
@@ -78,9 +75,6 @@ importers:
|
||||
leaflet:
|
||||
specifier: ^1.9.4
|
||||
version: 1.9.4
|
||||
mintel-annotator:
|
||||
specifier: link:mintel-annotator
|
||||
version: link:mintel-annotator
|
||||
next:
|
||||
specifier: 16.1.6
|
||||
version: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0)
|
||||
@@ -96,9 +90,6 @@ importers:
|
||||
nodemailer:
|
||||
specifier: ^8.0.7
|
||||
version: 8.0.7
|
||||
pdf-lib:
|
||||
specifier: ^1.17.1
|
||||
version: 1.17.1
|
||||
pino:
|
||||
specifier: ^10.3.0
|
||||
version: 10.3.1
|
||||
@@ -1407,13 +1398,6 @@ packages:
|
||||
'@medv/finder@4.0.2':
|
||||
resolution: {integrity: sha512-RraNY9SCcx4KZV0Dh6BEW6XEW2swkqYca74pkFFRw6hHItSHiy+O/xMnpbofjYbzXj0tSpBGthUF1hHTsr3vIQ==}
|
||||
|
||||
'@mintel/annotator@file:mintel-annotator-1.0.7.tgz':
|
||||
resolution: {integrity: sha512-At2+o4CxJBBK82Cl0xC/acAbs5RrNxnwDbuAGhmxMHGjL5Qero3TKxIvqI56FOgdZpdALfA2CT8aUpTsB0Vt+w==, tarball: file:mintel-annotator-1.0.7.tgz}
|
||||
version: 1.0.7
|
||||
peerDependencies:
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
|
||||
'@mintel/eslint-config@1.9.11':
|
||||
resolution: {integrity: sha512-g0FfWbSjfNuKAWa9yd4eC02mr9db66Y71TZ+FN3aUrLLMjwn3qqtEHez+8Dbray0loLiGDkm75EI7N0+mgA9yA==, tarball: https://git.infra.mintel.me/api/packages/mmintel/npm/%40mintel%2Feslint-config/-/1.9.11/eslint-config-1.9.11.tgz}
|
||||
|
||||
@@ -1907,12 +1891,6 @@ packages:
|
||||
'@paulirish/trace_engine@0.0.53':
|
||||
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':
|
||||
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
|
||||
|
||||
@@ -2231,49 +2209,6 @@ packages:
|
||||
react: ^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':
|
||||
resolution: {integrity: sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==}
|
||||
peerDependencies:
|
||||
@@ -3448,9 +3383,6 @@ packages:
|
||||
resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==}
|
||||
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:
|
||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -3786,9 +3718,6 @@ packages:
|
||||
brotli@1.3.3:
|
||||
resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==}
|
||||
|
||||
browserify-zlib@0.2.0:
|
||||
resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==}
|
||||
|
||||
browserslist@4.28.2:
|
||||
resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
@@ -3983,9 +3912,6 @@ packages:
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
color-string@1.9.1:
|
||||
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
|
||||
|
||||
colorette@2.0.20:
|
||||
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
||||
|
||||
@@ -4122,9 +4048,6 @@ packages:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
crypto-js@4.2.0:
|
||||
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
|
||||
|
||||
crypto-random-string@2.0.0:
|
||||
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -4440,9 +4363,6 @@ packages:
|
||||
electron-to-chromium@1.5.330:
|
||||
resolution: {integrity: sha512-jFNydB5kFtYUobh4IkWUnXeyDbjf/r9gcUEXe1xcrcUxIGfTdzPXA+ld6zBRbwvgIGVzDll/LTIiDztEtckSnA==}
|
||||
|
||||
emoji-regex-xs@1.0.0:
|
||||
resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==}
|
||||
|
||||
emoji-regex@10.6.0:
|
||||
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
|
||||
|
||||
@@ -4951,20 +4871,6 @@ packages:
|
||||
react-dom:
|
||||
optional: true
|
||||
|
||||
framer-motion@12.40.0:
|
||||
resolution: {integrity: sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg==}
|
||||
peerDependencies:
|
||||
'@emotion/is-prop-valid': '*'
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/is-prop-valid':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
|
||||
fresh@0.5.2:
|
||||
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -5194,12 +5100,6 @@ packages:
|
||||
resolution: {integrity: sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==}
|
||||
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:
|
||||
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
@@ -5281,9 +5181,6 @@ packages:
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
hyphen@1.14.1:
|
||||
resolution: {integrity: sha512-kvL8xYl5QMTh+LwohVN72ciOxC0OEV79IPdJSTwEXok9y9QHebXGdFgrED4sWfiax/ODx++CAMk3hMy4XPJPOw==}
|
||||
|
||||
i18next-fs-backend@2.6.3:
|
||||
resolution: {integrity: sha512-/5aW996nbolGh/qVU9urjAJ2QiXpiyGuhG8x/ZvvZQ3bMlrK+ouUdD6whS4e+sU9CCdUIzl7x/heEjsBh/3xtw==}
|
||||
|
||||
@@ -5417,9 +5314,6 @@ packages:
|
||||
is-arrayish@0.2.1:
|
||||
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
|
||||
|
||||
is-arrayish@0.3.4:
|
||||
resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==}
|
||||
|
||||
is-async-function@2.1.1:
|
||||
resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -5578,9 +5472,6 @@ packages:
|
||||
resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
is-url@1.2.4:
|
||||
resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
|
||||
|
||||
is-weakmap@2.0.2:
|
||||
resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -5622,9 +5513,6 @@ packages:
|
||||
jackspeak@3.4.3:
|
||||
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:
|
||||
resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
|
||||
engines: {node: '>= 10.13.0'}
|
||||
@@ -5958,11 +5846,6 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
lucide-react@1.21.0:
|
||||
resolution: {integrity: sha512-reEZMXq8Qdd5jg5XYkQ5TR1fB/GiQ7ih4vcrthYDtgjSDwh0i6/YLiGjsWsIwgN49gpAnd4J2elSNzncMEEUUQ==}
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
lz-string@1.5.0:
|
||||
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
|
||||
hasBin: true
|
||||
@@ -6063,9 +5946,6 @@ packages:
|
||||
mdn-data@2.27.1:
|
||||
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
|
||||
|
||||
media-engine@1.0.3:
|
||||
resolution: {integrity: sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==}
|
||||
|
||||
media-typer@0.3.0:
|
||||
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -6268,18 +6148,12 @@ packages:
|
||||
motion-dom@12.38.0:
|
||||
resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==}
|
||||
|
||||
motion-dom@12.40.0:
|
||||
resolution: {integrity: sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg==}
|
||||
|
||||
motion-utils@11.18.1:
|
||||
resolution: {integrity: sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==}
|
||||
|
||||
motion-utils@12.36.0:
|
||||
resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==}
|
||||
|
||||
motion-utils@12.39.0:
|
||||
resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==}
|
||||
|
||||
mri@1.2.0:
|
||||
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -6418,9 +6292,6 @@ packages:
|
||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
normalize-svg-path@1.1.0:
|
||||
resolution: {integrity: sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==}
|
||||
|
||||
npm-run-path@4.0.1:
|
||||
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -6567,9 +6438,6 @@ packages:
|
||||
pako@0.2.9:
|
||||
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:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -6588,9 +6456,6 @@ packages:
|
||||
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
parse-svg-path@0.1.2:
|
||||
resolution: {integrity: sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==}
|
||||
|
||||
parse5-htmlparser2-tree-adapter@7.1.0:
|
||||
resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==}
|
||||
|
||||
@@ -6642,9 +6507,6 @@ packages:
|
||||
pause-stream@0.0.11:
|
||||
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:
|
||||
resolution: {integrity: sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg==}
|
||||
engines: {node: '>=20.16.0 <21 || >=22.3.0'}
|
||||
@@ -6861,9 +6723,6 @@ packages:
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
queue@6.0.2:
|
||||
resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==}
|
||||
|
||||
quick-format-unescaped@4.0.4:
|
||||
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
||||
|
||||
@@ -7157,9 +7016,6 @@ packages:
|
||||
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
||||
engines: {node: '>=v12.22.7'}
|
||||
|
||||
scheduler@0.25.0-rc-603e6108-20241029:
|
||||
resolution: {integrity: sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==}
|
||||
|
||||
scheduler@0.27.0:
|
||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||
|
||||
@@ -7257,9 +7113,6 @@ packages:
|
||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
simple-swizzle@0.2.4:
|
||||
resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==}
|
||||
|
||||
sirv@2.0.4:
|
||||
resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
|
||||
engines: {node: '>= 10'}
|
||||
@@ -7436,9 +7289,6 @@ packages:
|
||||
resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
string_decoder@1.3.0:
|
||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||
|
||||
stringify-entities@4.0.4:
|
||||
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
||||
|
||||
@@ -7524,9 +7374,6 @@ packages:
|
||||
peerDependencies:
|
||||
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:
|
||||
resolution: {integrity: sha512-QItiGZBy5TstGy+q8mjQTMGRlDDOARXLxH+sgVm1n/LYeo0zFcQlcCh8m4zi8QxctrxB9Kue/lStc/RD5iLadQ==}
|
||||
|
||||
@@ -7543,9 +7390,6 @@ packages:
|
||||
tailwind-merge@3.5.0:
|
||||
resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==}
|
||||
|
||||
tailwind-merge@3.6.0:
|
||||
resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==}
|
||||
|
||||
tailwindcss@4.2.2:
|
||||
resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==}
|
||||
|
||||
@@ -7913,10 +7757,6 @@ packages:
|
||||
victory-vendor@37.3.6:
|
||||
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:
|
||||
resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -8255,9 +8095,6 @@ packages:
|
||||
resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
yoga-layout@3.2.1:
|
||||
resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==}
|
||||
|
||||
zod-validation-error@4.0.2:
|
||||
resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
@@ -9358,18 +9195,6 @@ snapshots:
|
||||
|
||||
'@medv/finder@4.0.2': {}
|
||||
|
||||
'@mintel/annotator@file:mintel-annotator-1.0.7.tgz(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||
dependencies:
|
||||
'@medv/finder': 4.0.2
|
||||
clsx: 2.1.1
|
||||
framer-motion: 12.40.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
lucide-react: 1.21.0(react@19.2.4)
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
tailwind-merge: 3.6.0
|
||||
transitivePeerDependencies:
|
||||
- '@emotion/is-prop-valid'
|
||||
|
||||
'@mintel/eslint-config@1.9.11(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint/eslintrc': 3.3.5
|
||||
@@ -9913,14 +9738,6 @@ snapshots:
|
||||
legacy-javascript: 0.0.1
|
||||
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': {}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
@@ -10200,107 +10017,6 @@ snapshots:
|
||||
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)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
@@ -11479,8 +11195,6 @@ snapshots:
|
||||
|
||||
abbrev@2.0.0: {}
|
||||
|
||||
abs-svg-path@0.1.1: {}
|
||||
|
||||
accepts@1.3.8:
|
||||
dependencies:
|
||||
mime-types: 2.1.35
|
||||
@@ -11801,10 +11515,6 @@ snapshots:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
|
||||
browserify-zlib@0.2.0:
|
||||
dependencies:
|
||||
pako: 1.0.11
|
||||
|
||||
browserslist@4.28.2:
|
||||
dependencies:
|
||||
baseline-browser-mapping: 2.10.13
|
||||
@@ -12019,11 +11729,6 @@ snapshots:
|
||||
|
||||
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: {}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
@@ -12170,8 +11875,6 @@ snapshots:
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
crypto-js@4.2.0: {}
|
||||
|
||||
crypto-random-string@2.0.0: {}
|
||||
|
||||
csp_evaluator@1.1.5: {}
|
||||
@@ -12503,8 +12206,6 @@ snapshots:
|
||||
|
||||
electron-to-chromium@1.5.330: {}
|
||||
|
||||
emoji-regex-xs@1.0.0: {}
|
||||
|
||||
emoji-regex@10.6.0: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
@@ -13258,15 +12959,6 @@ snapshots:
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
|
||||
framer-motion@12.40.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
dependencies:
|
||||
motion-dom: 12.40.0
|
||||
motion-utils: 12.39.0
|
||||
tslib: 2.8.1
|
||||
optionalDependencies:
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
|
||||
fresh@0.5.2: {}
|
||||
|
||||
from@0.1.7: {}
|
||||
@@ -13575,12 +13267,6 @@ snapshots:
|
||||
|
||||
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):
|
||||
dependencies:
|
||||
'@exodus/bytes': 1.15.0(@noble/hashes@1.8.0)
|
||||
@@ -13679,8 +13365,6 @@ snapshots:
|
||||
|
||||
husky@9.1.7: {}
|
||||
|
||||
hyphen@1.14.1: {}
|
||||
|
||||
i18next-fs-backend@2.6.3: {}
|
||||
|
||||
i18next@25.10.10(typescript@5.9.3):
|
||||
@@ -13832,8 +13516,6 @@ snapshots:
|
||||
|
||||
is-arrayish@0.2.1: {}
|
||||
|
||||
is-arrayish@0.3.4: {}
|
||||
|
||||
is-async-function@2.1.1:
|
||||
dependencies:
|
||||
async-function: 1.0.0
|
||||
@@ -13971,8 +13653,6 @@ snapshots:
|
||||
|
||||
is-unicode-supported@2.1.0: {}
|
||||
|
||||
is-url@1.2.4: {}
|
||||
|
||||
is-weakmap@2.0.2: {}
|
||||
|
||||
is-weakref@1.1.1:
|
||||
@@ -14023,10 +13703,6 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@pkgjs/parseargs': 0.11.0
|
||||
|
||||
jay-peg@1.1.1:
|
||||
dependencies:
|
||||
restructure: 3.0.2
|
||||
|
||||
jest-worker@27.5.1:
|
||||
dependencies:
|
||||
'@types/node': 22.19.15
|
||||
@@ -14376,10 +14052,6 @@ snapshots:
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
|
||||
lucide-react@1.21.0(react@19.2.4):
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
|
||||
lz-string@1.5.0: {}
|
||||
|
||||
maath@0.10.8(@types/three@0.183.1)(three@0.183.2):
|
||||
@@ -14579,8 +14251,6 @@ snapshots:
|
||||
|
||||
mdn-data@2.27.1: {}
|
||||
|
||||
media-engine@1.0.3: {}
|
||||
|
||||
media-typer@0.3.0: {}
|
||||
|
||||
meow@13.2.0: {}
|
||||
@@ -14915,16 +14585,10 @@ snapshots:
|
||||
dependencies:
|
||||
motion-utils: 12.36.0
|
||||
|
||||
motion-dom@12.40.0:
|
||||
dependencies:
|
||||
motion-utils: 12.39.0
|
||||
|
||||
motion-utils@11.18.1: {}
|
||||
|
||||
motion-utils@12.36.0: {}
|
||||
|
||||
motion-utils@12.39.0: {}
|
||||
|
||||
mri@1.2.0: {}
|
||||
|
||||
mrmime@2.0.1: {}
|
||||
@@ -15053,10 +14717,6 @@ snapshots:
|
||||
|
||||
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:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
@@ -15267,8 +14927,6 @@ snapshots:
|
||||
|
||||
pako@0.2.9: {}
|
||||
|
||||
pako@1.0.11: {}
|
||||
|
||||
parent-module@1.0.1:
|
||||
dependencies:
|
||||
callsites: 3.1.0
|
||||
@@ -15296,8 +14954,6 @@ snapshots:
|
||||
json-parse-even-better-errors: 2.3.1
|
||||
lines-and-columns: 1.2.4
|
||||
|
||||
parse-svg-path@0.1.2: {}
|
||||
|
||||
parse5-htmlparser2-tree-adapter@7.1.0:
|
||||
dependencies:
|
||||
domhandler: 5.0.3
|
||||
@@ -15348,13 +15004,6 @@ snapshots:
|
||||
dependencies:
|
||||
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:
|
||||
dependencies:
|
||||
'@napi-rs/canvas': 0.1.80
|
||||
@@ -15604,10 +15253,6 @@ snapshots:
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
queue@6.0.2:
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
|
||||
quick-format-unescaped@4.0.4: {}
|
||||
|
||||
range-parser@1.2.1: {}
|
||||
@@ -16026,8 +15671,6 @@ snapshots:
|
||||
dependencies:
|
||||
xmlchars: 2.2.0
|
||||
|
||||
scheduler@0.25.0-rc-603e6108-20241029: {}
|
||||
|
||||
scheduler@0.27.0: {}
|
||||
|
||||
schema-dts@1.1.5: {}
|
||||
@@ -16180,10 +15823,6 @@ snapshots:
|
||||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
simple-swizzle@0.2.4:
|
||||
dependencies:
|
||||
is-arrayish: 0.3.4
|
||||
|
||||
sirv@2.0.4:
|
||||
dependencies:
|
||||
'@polka/url': 1.0.0-next.29
|
||||
@@ -16420,10 +16059,6 @@ snapshots:
|
||||
define-properties: 1.2.1
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
string_decoder@1.3.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
stringify-entities@4.0.4:
|
||||
dependencies:
|
||||
character-entities-html4: 2.1.0
|
||||
@@ -16494,8 +16129,6 @@ snapshots:
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
|
||||
svg-arc-to-cubic-bezier@3.2.0: {}
|
||||
|
||||
svg-to-pdfkit@0.1.8:
|
||||
dependencies:
|
||||
pdfkit: 0.18.0
|
||||
@@ -16508,8 +16141,6 @@ snapshots:
|
||||
|
||||
tailwind-merge@3.5.0: {}
|
||||
|
||||
tailwind-merge@3.6.0: {}
|
||||
|
||||
tailwindcss@4.2.2: {}
|
||||
|
||||
tapable@2.3.2: {}
|
||||
@@ -16966,12 +16597,6 @@ snapshots:
|
||||
d3-time: 3.1.0
|
||||
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):
|
||||
dependencies:
|
||||
lightningcss: 1.32.0
|
||||
@@ -17303,8 +16928,6 @@ snapshots:
|
||||
|
||||
yoctocolors@2.1.2: {}
|
||||
|
||||
yoga-layout@3.2.1: {}
|
||||
|
||||
zod-validation-error@4.0.2(zod@3.25.76):
|
||||
dependencies:
|
||||
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
@@ -7,21 +7,21 @@ describe('Task 6 Content Updates', () => {
|
||||
const enHomePath = path.join(process.cwd(), 'content', 'en', 'home.mdx');
|
||||
const deKompetenzenPath = path.join(process.cwd(), 'content', 'de', 'kompetenzen.mdx');
|
||||
const enKompetenzenPath = path.join(process.cwd(), 'content', 'en', 'kompetenzen.mdx');
|
||||
const deKabeltiefbauPath = path.join(process.cwd(), 'content', 'de', 'kabeltiefbau.mdx');
|
||||
const enKabeltiefbauPath = path.join(process.cwd(), 'content', 'en', 'kabeltiefbau.mdx');
|
||||
const deKabelnetzbauPath = path.join(process.cwd(), 'content', 'de', 'kabeltiefbau.mdx');
|
||||
const enKabelnetzbauPath = path.join(process.cwd(), 'content', 'en', 'kabeltiefbau.mdx');
|
||||
const deUeberUnsPath = path.join(process.cwd(), 'content', 'de', '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 enContent = fs.readFileSync(enKompetenzenPath, 'utf8');
|
||||
|
||||
// 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('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('"name": "Kabelleitungstiefbau"');
|
||||
expect(deContent).toContain('"name": "Kabelleitungsnetzbau"');
|
||||
expect(deContent).toContain('"description": "Klassischer Grabenbau, professionelle Verlegung und Kabelmontage (Hoch-, Mittel- und Niederspannung) bis 110 kV."');
|
||||
|
||||
// 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."');
|
||||
});
|
||||
|
||||
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 enContent = fs.readFileSync(enHomePath, 'utf8');
|
||||
|
||||
// DE Home
|
||||
expect(deContent).toContain('title: "Kabelleitungstiefbau"');
|
||||
expect(deContent).toContain('description: "Kabelleitungstiefbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen bis 110 kV"');
|
||||
expect(deContent).toContain('title: "Kabelleitungsnetzbau"');
|
||||
expect(deContent).toContain('description: "Kabelleitungsnetzbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen bis 110 kV"');
|
||||
expect(deContent).toContain('100+');
|
||||
expect(deContent).toContain('Mitarbeitende');
|
||||
|
||||
@@ -50,18 +50,18 @@ describe('Task 6 Content Updates', () => {
|
||||
expect(enContent).toContain('Employees');
|
||||
});
|
||||
|
||||
it('should verify Kabelleitungstiefbau changes in DE and EN kabeltiefbau.mdx', () => {
|
||||
const deContent = fs.readFileSync(deKabeltiefbauPath, 'utf8');
|
||||
const enContent = fs.readFileSync(enKabeltiefbauPath, 'utf8');
|
||||
it('should verify Kabelleitungsnetzbau changes in DE and EN kabeltiefbau.mdx', () => {
|
||||
const deContent = fs.readFileSync(deKabelnetzbauPath, 'utf8');
|
||||
const enContent = fs.readFileSync(enKabelnetzbauPath, 'utf8');
|
||||
|
||||
// DE Kabeltiefbau
|
||||
expect(deContent).toContain('title: "Kabelleitungstiefbau"');
|
||||
expect(deContent).toContain('excerpt: "Professioneller Kabelleitungstiefbau');
|
||||
// DE Kabelnetzbau
|
||||
expect(deContent).toContain('title: "Kabelleitungsnetzbau"');
|
||||
expect(deContent).toContain('excerpt: "Professioneller Kabelleitungsnetzbau');
|
||||
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('"Kabelmontagen bis 110 kV"');
|
||||
|
||||
// EN Kabeltiefbau
|
||||
// EN Kabelnetzbau
|
||||
expect(enContent).toContain('title: "Cable Civil Engineering"');
|
||||
expect(enContent).toContain('excerpt: "Professional cable civil engineering');
|
||||
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 () => {
|
||||
await page.goto(`${BASE_URL}/de/blog`, { waitUntil: 'networkidle2', timeout: 60000 });
|
||||
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
|
||||
await page.goto(`${BASE_URL}/de/blog/moderne-verfahren-kabeltiefbau`, { waitUntil: 'networkidle2', timeout: 60000 });
|
||||
|
||||
Reference in New Issue
Block a user