Compare commits
14 Commits
v2.2.13-rc
...
v2.2.13-rc
| Author | SHA1 | Date | |
|---|---|---|---|
| 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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -82,7 +82,7 @@ const labels = (locale: 'en' | 'de') =>
|
||||
? {
|
||||
catalog: 'Unternehmensbroschüre',
|
||||
subtitle:
|
||||
'IHR PARTNER FÜR KABELLEITUNGSTIEFBAU, HORIZONTALSPÜLBOHRUNGEN, PLANUNG UND VERMESSUNG.',
|
||||
'IHR PARTNER FÜR KABELLEITUNGSNETZBAU, HORIZONTALSPÜLBOHRUNGEN, PLANUNG UND VERMESSUNG.',
|
||||
about: 'Über uns',
|
||||
toc: 'Inhalt',
|
||||
overview: 'Übersicht',
|
||||
@@ -101,7 +101,7 @@ const labels = (locale: 'en' | 'de') =>
|
||||
: {
|
||||
catalog: 'Company Brochure',
|
||||
subtitle:
|
||||
'YOUR PARTNER FOR CABLE TRENCHING, HORIZONTAL DIRECTIONAL DRILLING, PLANNING AND SURVEYING.',
|
||||
'YOUR PARTNER FOR CABLE NETWORK CONSTRUCTION, HORIZONTAL DIRECTIONAL DRILLING, PLANNING AND SURVEYING.',
|
||||
about: 'About Us',
|
||||
toc: 'Contents',
|
||||
overview: 'Overview',
|
||||
@@ -758,8 +758,8 @@ const AboutPage: React.FC<{
|
||||
<View style={{ marginTop: 12, marginBottom: 8 }}>
|
||||
<RichText style={{ fontSize: 9, color: C.gray600, lineHeight: 1.7 }} gap={6}>
|
||||
{locale === 'de'
|
||||
? 'E-TIB GmbH ist Ihr Spezialist für Kabelleitungstiefbau, Horizontalspülbohrungen, Planung und Vermessung. Wir realisieren komplexe Infrastrukturprojekte für Energieversorger und die Industrie – von der ersten Planung bis zur termingerechten Ausführung. Mit modernster Technik und erfahrener Mannschaft sorgen wir für Ihre Netze von morgen.'
|
||||
: 'E-TIB GmbH is your specialist for cable trenching, horizontal directional drilling, planning and surveying. We realize complex infrastructure projects for energy providers and industry – from initial planning to on-time execution. With state-of-the-art technology and an experienced team, we ensure your networks of tomorrow.'}
|
||||
? 'E-TIB GmbH ist Ihr Spezialist für Kabelleitungsnetzbau, Horizontalspülbohrungen, Planung und Vermessung. Wir realisieren komplexe Infrastrukturprojekte für Energieversorger und die Industrie – von der ersten Planung bis zur termingerechten Ausführung. Mit modernster Technik und erfahrener Mannschaft sorgen wir für Ihre Netze von morgen.'
|
||||
: 'E-TIB GmbH is your specialist for cable network construction, horizontal directional drilling, planning and surveying. We realize complex infrastructure projects for energy providers and industry – from initial planning to on-time execution. With state-of-the-art technology and an experienced team, we ensure your networks of tomorrow.'}
|
||||
</RichText>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -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.
@@ -4,7 +4,7 @@
|
||||
"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",
|
||||
@@ -26,7 +26,6 @@
|
||||
"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",
|
||||
@@ -145,7 +144,7 @@
|
||||
"prepare": "husky",
|
||||
"preinstall": "npx only-allow pnpm"
|
||||
},
|
||||
"version": "2.2.13-rc.42",
|
||||
"version": "2.2.13-rc.46",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
|
||||
77
pnpm-lock.yaml
generated
77
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)
|
||||
@@ -78,9 +78,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)
|
||||
@@ -1407,13 +1404,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}
|
||||
|
||||
@@ -4951,20 +4941,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'}
|
||||
@@ -5958,11 +5934,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
|
||||
@@ -6268,18 +6239,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'}
|
||||
@@ -7543,9 +7508,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==}
|
||||
|
||||
@@ -9358,18 +9320,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
|
||||
@@ -13258,15 +13208,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: {}
|
||||
@@ -14376,10 +14317,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):
|
||||
@@ -14915,16 +14852,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: {}
|
||||
@@ -16508,8 +16439,6 @@ snapshots:
|
||||
|
||||
tailwind-merge@3.5.0: {}
|
||||
|
||||
tailwind-merge@3.6.0: {}
|
||||
|
||||
tailwindcss@4.2.2: {}
|
||||
|
||||
tapable@2.3.2: {}
|
||||
|
||||
@@ -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