Compare commits
12 Commits
v2.2.13-rc
...
v2.2.13-rc
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c9028f58b | |||
| 9de5f2938b | |||
| de41a0e29d | |||
| 6689dd3013 | |||
| 31fbcb32dd | |||
| 68eca73df2 | |||
| 3bb6eb3447 | |||
| a3c732dd13 | |||
| aaa1a8793c | |||
| 233509db67 | |||
| 17a807c48b | |||
| d8634efa9a |
1
.turbo/41698960411f6bf6-manifest.json
Normal file
1
.turbo/41698960411f6bf6-manifest.json
Normal file
@@ -0,0 +1 @@
|
||||
{"files":{".turbo/turbo-test.log":{"size":2776,"mtime_nanos":1781469605328811961,"mode":420,"is_dir":false}},"order":[".turbo/turbo-test.log"]}
|
||||
1
.turbo/41698960411f6bf6-meta.json
Normal file
1
.turbo/41698960411f6bf6-meta.json
Normal file
@@ -0,0 +1 @@
|
||||
{"hash":"41698960411f6bf6","duration":4168,"sha":"a3c732dd133b1b8261b19cf64b3bf684e269e839","dirty_hash":"1ef3b75af417f41468b9558671572e75e0fc055f9dbe6e805cfa6f59b1cddf70"}
|
||||
BIN
.turbo/41698960411f6bf6.tar.zst
Normal file
BIN
.turbo/41698960411f6bf6.tar.zst
Normal file
Binary file not shown.
1
.turbo/bb46018d4a7bc516-manifest.json
Normal file
1
.turbo/bb46018d4a7bc516-manifest.json
Normal file
@@ -0,0 +1 @@
|
||||
{"files":{".turbo/turbo-lint.log":{"size":12987,"mtime_nanos":1781469609469080321,"mode":420,"is_dir":false}},"order":[".turbo/turbo-lint.log"]}
|
||||
1
.turbo/bb46018d4a7bc516-meta.json
Normal file
1
.turbo/bb46018d4a7bc516-meta.json
Normal file
@@ -0,0 +1 @@
|
||||
{"hash":"bb46018d4a7bc516","duration":8307,"sha":"a3c732dd133b1b8261b19cf64b3bf684e269e839","dirty_hash":"1ef3b75af417f41468b9558671572e75e0fc055f9dbe6e805cfa6f59b1cddf70"}
|
||||
BIN
.turbo/bb46018d4a7bc516.tar.zst
Normal file
BIN
.turbo/bb46018d4a7bc516.tar.zst
Normal file
Binary file not shown.
1
.turbo/eb4eeeb717c1af0c-manifest.json
Normal file
1
.turbo/eb4eeeb717c1af0c-manifest.json
Normal file
@@ -0,0 +1 @@
|
||||
{"files":{".turbo/turbo-typecheck.log":{"size":85,"mtime_nanos":1781469608726900998,"mode":420,"is_dir":false}},"order":[".turbo/turbo-typecheck.log"]}
|
||||
1
.turbo/eb4eeeb717c1af0c-meta.json
Normal file
1
.turbo/eb4eeeb717c1af0c-meta.json
Normal file
@@ -0,0 +1 @@
|
||||
{"hash":"eb4eeeb717c1af0c","duration":7565,"sha":"a3c732dd133b1b8261b19cf64b3bf684e269e839","dirty_hash":"1ef3b75af417f41468b9558671572e75e0fc055f9dbe6e805cfa6f59b1cddf70"}
|
||||
BIN
.turbo/eb4eeeb717c1af0c.tar.zst
Normal file
BIN
.turbo/eb4eeeb717c1af0c.tar.zst
Normal file
Binary file not shown.
@@ -18,12 +18,17 @@
|
||||
6:10 warning 'Terminal' is defined but never used @typescript-eslint/no-unused-vars
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/app/[locale]/referenzen/[slug]/page.tsx
|
||||
92:26 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
|
||||
118:26 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/app/[locale]/referenzen/page.tsx
|
||||
1:21 warning 'Heading' is defined but never used @typescript-eslint/no-unused-vars
|
||||
1:30 warning 'Badge' is defined but never used @typescript-eslint/no-unused-vars
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/app/[locale]/standorte/[slug]/page.tsx
|
||||
9:10 warning 'getButtonClasses' is defined but never used @typescript-eslint/no-unused-vars
|
||||
9:28 warning 'ButtonOverlay' is defined but never used @typescript-eslint/no-unused-vars
|
||||
58:9 warning 't' is assigned a value but never used @typescript-eslint/no-unused-vars
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/components/JsonLd.tsx
|
||||
1:30 warning 'Graph' is defined but never used @typescript-eslint/no-unused-vars
|
||||
43:31 warning '_' is assigned a value but never used @typescript-eslint/no-unused-vars
|
||||
@@ -76,14 +81,17 @@ Calling setState synchronously within an effect body causes cascading renders th
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/components/blocks/InteractiveGermanyMap.tsx
|
||||
5:18 warning 'Factory' is defined but never used @typescript-eslint/no-unused-vars
|
||||
5:27 warning 'Zap' is defined but never used @typescript-eslint/no-unused-vars
|
||||
53:9 warning 'hq' is assigned a value but never used @typescript-eslint/no-unused-vars
|
||||
54:9 warning 'branch' is assigned a value but never used @typescript-eslint/no-unused-vars
|
||||
55:9 warning 'projects' is assigned a value but never used @typescript-eslint/no-unused-vars
|
||||
56:9 warning 'minorNodes' is assigned a value but never used @typescript-eslint/no-unused-vars
|
||||
56:9 warning 'hq' is assigned a value but never used @typescript-eslint/no-unused-vars
|
||||
57:9 warning 'branch' is assigned a value but never used @typescript-eslint/no-unused-vars
|
||||
58:9 warning 'projects' is assigned a value but never used @typescript-eslint/no-unused-vars
|
||||
59:9 warning 'minorNodes' is assigned a value but never used @typescript-eslint/no-unused-vars
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/components/blocks/ReferencesSlider.test.tsx
|
||||
5:10 warning 'NextIntlClientProvider' is defined but never used @typescript-eslint/no-unused-vars
|
||||
21:12 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
|
||||
20:12 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/components/blocks/SubCompanyTiles.tsx
|
||||
92:19 warning 'CardWrapper' is assigned a value but never used @typescript-eslint/no-unused-vars
|
||||
93:19 warning 'wrapperProps' is assigned a value but never used @typescript-eslint/no-unused-vars
|
||||
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/components/blocks/TeamGridBlock.tsx
|
||||
20:39 warning 'title' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars
|
||||
@@ -124,6 +132,15 @@ Calling setState synchronously within an effect body causes cascading renders th
|
||||
94:7 warning 'next' is never reassigned. Use 'const' instead prefer-const
|
||||
95:7 warning 'prev' is never reassigned. Use 'const' instead prefer-const
|
||||
|
||||
✖ 45 problems (0 errors, 45 warnings)
|
||||
/Volumes/Alpha SSD/Coding/e-tib.com/lib/standorte-data.ts
|
||||
1:10 warning 'LucideIcon' is defined but never used @typescript-eslint/no-unused-vars
|
||||
1:22 warning 'MapPin' is defined but never used @typescript-eslint/no-unused-vars
|
||||
1:30 warning 'Building2' is defined but never used @typescript-eslint/no-unused-vars
|
||||
1:41 warning 'HardHat' is defined but never used @typescript-eslint/no-unused-vars
|
||||
1:50 warning 'Phone' is defined but never used @typescript-eslint/no-unused-vars
|
||||
1:57 warning 'Mail' is defined but never used @typescript-eslint/no-unused-vars
|
||||
1:63 warning 'Navigation' is defined but never used @typescript-eslint/no-unused-vars
|
||||
|
||||
✖ 56 problems (0 errors, 56 warnings)
|
||||
0 errors and 2 warnings potentially fixable with the `--fix` option.
|
||||
|
||||
|
||||
@@ -5,27 +5,33 @@
|
||||
|
||||
[1m[46m RUN [49m[22m [36mv4.1.2 [39m[90m/Volumes/Alpha SSD/Coding/e-tib.com[39m
|
||||
|
||||
[32m✓[39m tests/mdx-links.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 4[2mms[22m[39m
|
||||
[32m✓[39m lib/mail/mailer.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 2[2mms[22m[39m
|
||||
[32m✓[39m lib/env.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 6[2mms[22m[39m
|
||||
(node:74132) [DEP0205] DeprecationWarning: `module.register()` is deprecated. Use `module.registerHooks()` instead.
|
||||
(Use `node --trace-deprecation ...` to show where the warning was created)
|
||||
[32m✓[39m tests/homepage-and-competence-content.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 10[2mms[22m[39m
|
||||
[32m✓[39m tests/task-12-links.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
||||
[32m✓[39m tests/mdx-links.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 11[2mms[22m[39m
|
||||
[32m✓[39m tests/legal-pages.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 15[2mms[22m[39m
|
||||
[32m✓[39m tests/messen-content.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 30[2mms[22m[39m
|
||||
[32m✓[39m lib/mail/mailer.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 4[2mms[22m[39m
|
||||
[32m✓[39m components/search/AISearchResults.test.tsx [2m([22m[2m1 test[22m[2m)[22m[32m 25[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 21[2mms[22m[39m
|
||||
[32m✓[39m components/search/AISearchResults.test.tsx [2m([22m[2m1 test[22m[2m)[22m[32m 26[2mms[22m[39m
|
||||
[32m✓[39m components/layout/LanguageSwitcher.test.tsx [2m([22m[2m5 tests[22m[2m)[22m[32m 26[2mms[22m[39m
|
||||
[32m✓[39m lib/env.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 3[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[2m4 tests[22m[2m)[22m[32m 40[2mms[22m[39m
|
||||
[32m✓[39m components/blocks/ReferencesSlider.test.tsx [2m([22m[2m2 tests[22m[2m)[22m[32m 43[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 6[2mms[22m[39m
|
||||
[32m✓[39m app/[locale]/referenzen/[slug]/page.test.tsx [2m([22m[2m1 test[22m[2m)[22m[32m 7[2mms[22m[39m
|
||||
|
||||
[2m Test Files [22m [1m[32m7 passed[39m[22m[90m (7)[39m
|
||||
[2m Tests [22m [1m[32m19 passed[39m[22m[90m (19)[39m
|
||||
[2m Start at [22m 14:12:48
|
||||
[2m Duration [22m 1.80s[2m (transform 863ms, setup 0ms, import 2.94s, tests 105ms, environment 3.18s)[22m
|
||||
[2m Test Files [22m [1m[32m11 passed[39m[22m[90m (11)[39m
|
||||
[2m Tests [22m [1m[32m27 passed[39m[22m[90m (27)[39m
|
||||
[2m Start at [22m 22:40:02
|
||||
[2m Duration [22m 2.69s[2m (transform 2.09s, setup 0ms, import 4.45s, tests 179ms, environment 8.10s)[22m
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import TrackedLink from '@/components/analytics/TrackedLink';
|
||||
import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
|
||||
import { getAllReferences } from '@/lib/references';
|
||||
import { defaultLocations, minorLocations } from '@/lib/map-data';
|
||||
|
||||
// Import components used in MDX
|
||||
import { HeroVideo as HomeHero } from '@/components/blocks/HeroVideo';
|
||||
import { SubCompanyTiles as HomeSubCompanyTiles } from '@/components/blocks/SubCompanyTiles';
|
||||
|
||||
@@ -93,7 +93,8 @@ export default async function Layout(props: {
|
||||
children: [
|
||||
{ label: safeLocale === 'de' ? 'Firma' : 'Company', url: `/${safeLocale}/${await mapFileSlugToTranslated('ueber-uns', safeLocale)}` },
|
||||
{ label: safeLocale === 'de' ? 'Unser Team' : 'Our Team', url: `/${safeLocale}/team` },
|
||||
{ label: safeLocale === 'de' ? 'Zertifikate' : 'Certificates', url: `/${safeLocale}/zertifikate` }
|
||||
{ label: safeLocale === 'de' ? 'Zertifikate' : 'Certificates', url: `/${safeLocale}/zertifikate` },
|
||||
{ label: safeLocale === 'de' ? 'Standorte' : 'Locations', url: `/${safeLocale}/standorte` }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
216
app/[locale]/standorte/[slug]/page.tsx
Normal file
216
app/[locale]/standorte/[slug]/page.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
import { Container, Badge, Heading } from '@/components/ui';
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
import { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { standorteData, getStandortById } from '@/lib/standorte-data';
|
||||
import { SITE_URL } from '@/lib/schema';
|
||||
import Image from 'next/image';
|
||||
import { MapPin, Phone, Mail, Navigation, CheckCircle2, ArrowUpRight } from 'lucide-react';
|
||||
import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
|
||||
import TrackedLink from '@/components/analytics/TrackedLink';
|
||||
import { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{
|
||||
locale: string;
|
||||
slug: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const locales = ['de', 'en'];
|
||||
const params: { locale: string; slug: string }[] = [];
|
||||
|
||||
for (const locale of locales) {
|
||||
for (const standort of standorteData) {
|
||||
params.push({ locale, slug: standort.id });
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||
const { locale, slug } = await params;
|
||||
if (locale !== 'de' && locale !== 'en') return {};
|
||||
|
||||
const standort = getStandortById(slug);
|
||||
if (!standort) return {};
|
||||
|
||||
return {
|
||||
title: `${standort.name} | E-TIB Gruppe`,
|
||||
description: standort.description[locale],
|
||||
alternates: {
|
||||
canonical: `${SITE_URL}/${locale}/standorte/${slug}`,
|
||||
languages: {
|
||||
de: `${SITE_URL}/de/standorte/${slug}`,
|
||||
en: `${SITE_URL}/en/standorte/${slug}`,
|
||||
'x-default': `${SITE_URL}/en/standorte/${slug}`,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function StandortDetail(props: { params: Promise<{ locale: string; slug: string }> }) {
|
||||
const { locale, slug } = await props.params;
|
||||
const safeLocale = locale === 'de' ? 'de' : 'en';
|
||||
setRequestLocale(safeLocale);
|
||||
const t = await getTranslations('StandardPage');
|
||||
|
||||
const standort = getStandortById(slug);
|
||||
if (!standort) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen bg-neutral-50 pb-16 md:pb-24">
|
||||
{/* Industrial Hero Section */}
|
||||
<section className="bg-primary-dark text-white pt-40 pb-12 md:pt-48 md:pb-16 min-h-[40vh] md:min-h-[45vh] flex flex-col justify-end relative overflow-hidden">
|
||||
<div className="absolute inset-0 opacity-20 pointer-events-none">
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-accent/50 via-transparent to-transparent" />
|
||||
</div>
|
||||
<Container className="relative z-10">
|
||||
<div className="max-w-4xl">
|
||||
<Badge variant="accent" className="mb-4 md:mb-6 shadow-sm flex inline-flex items-center gap-2">
|
||||
<MapPin className="w-4 h-4" />
|
||||
{standort.type === 'hq' ? (safeLocale === 'de' ? 'Hauptsitz' : 'Headquarters') : (safeLocale === 'de' ? 'Niederlassung' : 'Branch')}
|
||||
</Badge>
|
||||
<Heading level={1} variant="white" className="mb-6 drop-shadow-sm">
|
||||
{standort.name}
|
||||
</Heading>
|
||||
<p className="text-xl md:text-2xl text-white/80 max-w-3xl leading-relaxed font-medium">
|
||||
{standort.description[safeLocale]}
|
||||
</p>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
|
||||
<Container className="relative z-20 mt-12 md:mt-16">
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-12">
|
||||
{/* Main Content (Images & Description) */}
|
||||
<div className="lg:col-span-8 space-y-8">
|
||||
{/* Featured Image */}
|
||||
<div className="relative aspect-video w-full rounded-3xl overflow-hidden bg-neutral-900 shadow-xl border border-neutral-200">
|
||||
<Image
|
||||
src={standort.image}
|
||||
alt={standort.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/40 to-transparent" />
|
||||
</div>
|
||||
|
||||
{/* Key Features */}
|
||||
<div className="bg-white rounded-3xl p-8 md:p-10 shadow-sm border border-neutral-100">
|
||||
<h2 className="text-2xl font-bold font-heading mb-6 text-neutral-dark">
|
||||
{safeLocale === 'de' ? 'Unsere Leistungen vor Ort' : 'Our Local Services'}
|
||||
</h2>
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{standort.keyFeatures[safeLocale].map((feature, idx) => (
|
||||
<li key={idx} className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-5 h-5 text-primary shrink-0 mt-0.5" />
|
||||
<span className="text-neutral-700 font-medium">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Gallery (if any) */}
|
||||
{standort.gallery && standort.gallery.length > 0 && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{standort.gallery.map((img, idx) => (
|
||||
<div key={idx} className="relative aspect-[4/3] rounded-2xl overflow-hidden bg-neutral-100">
|
||||
<Image src={img} alt={`Gallery ${idx + 1}`} fill className="object-cover" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sidebar (Contact & Address) */}
|
||||
<div className="lg:col-span-4">
|
||||
<div className="sticky top-32 space-y-6">
|
||||
|
||||
{/* Contact Card */}
|
||||
<div className="bg-[#050B14] text-white rounded-3xl p-8 shadow-2xl relative overflow-hidden group">
|
||||
<AnimatedGlossyBorder opacity={0.5} className="z-10" />
|
||||
<div className="absolute top-0 right-0 w-64 h-full bg-primary/5 blur-3xl -translate-y-1/2 translate-x-1/3" />
|
||||
|
||||
<div className="relative z-20">
|
||||
<h3 className="text-xl font-bold font-heading mb-6 flex items-center gap-2">
|
||||
<Navigation className="w-5 h-5 text-primary" />
|
||||
{safeLocale === 'de' ? 'Kontakt & Anfahrt' : 'Contact & Directions'}
|
||||
</h3>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 rounded-full bg-white/5 flex items-center justify-center shrink-0">
|
||||
<MapPin className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-white/50 font-bold uppercase tracking-wider mb-1">
|
||||
{safeLocale === 'de' ? 'Adresse' : 'Address'}
|
||||
</div>
|
||||
<div className="text-white font-medium">
|
||||
{standort.address.street}<br />
|
||||
{standort.address.city}
|
||||
</div>
|
||||
{(() => {
|
||||
const baseUrl = 'https://maps.google.com/';
|
||||
const mapsUrl = `${baseUrl}?q=${encodeURIComponent(`${standort.address.street}, ${standort.address.city}`)}`;
|
||||
return (
|
||||
<TrackedLink
|
||||
href={mapsUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 mt-2 text-primary hover:text-white transition-colors text-sm font-bold"
|
||||
eventProperties={{ location: 'standort_maps_link', slug: standort.id }}
|
||||
>
|
||||
{safeLocale === 'de' ? 'In Google Maps öffnen' : 'Open in Google Maps'}
|
||||
<ArrowUpRight className="w-3.5 h-3.5" />
|
||||
</TrackedLink>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 rounded-full bg-white/5 flex items-center justify-center shrink-0">
|
||||
<Phone className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-white/50 font-bold uppercase tracking-wider mb-1">
|
||||
{safeLocale === 'de' ? 'Telefon' : 'Phone'}
|
||||
</div>
|
||||
<a href={`tel:${standort.contact.phone.replace(/[^0-9+]/g, '')}`} className="text-white font-medium hover:text-primary transition-colors">
|
||||
{standort.contact.phone}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 rounded-full bg-white/5 flex items-center justify-center shrink-0">
|
||||
<Mail className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-white/50 font-bold uppercase tracking-wider mb-1">
|
||||
Email
|
||||
</div>
|
||||
<a href={`mailto:${standort.contact.email}`} className="text-white font-medium hover:text-primary transition-colors">
|
||||
{standort.contact.email}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
142
app/[locale]/standorte/page.tsx
Normal file
142
app/[locale]/standorte/page.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { Container } from '@/components/ui';
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
import { Metadata } from 'next';
|
||||
import TrackedLink from '@/components/analytics/TrackedLink';
|
||||
import { MapPin, Navigation, ArrowUpRight } from 'lucide-react';
|
||||
import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
|
||||
import { SITE_URL } from '@/lib/schema';
|
||||
import Image from 'next/image';
|
||||
import { standorteData, standorteLocations } from '@/lib/standorte-data';
|
||||
import { InteractiveGermanyMap } from '@/components/blocks/InteractiveGermanyMap';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{
|
||||
locale: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
const safeLocale = locale === 'de' ? 'de' : 'en';
|
||||
|
||||
return {
|
||||
title: safeLocale === 'en' ? 'Our Locations' : 'Unsere Standorte',
|
||||
description: safeLocale === 'en' ? 'Our operational locations across Germany.' : 'Unsere operativen Standorte deutschlandweit.',
|
||||
alternates: {
|
||||
canonical: `${SITE_URL}/${safeLocale}/standorte`,
|
||||
languages: {
|
||||
de: `${SITE_URL}/de/standorte`,
|
||||
en: `${SITE_URL}/en/standorte`,
|
||||
'x-default': `${SITE_URL}/en/standorte`,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function StandorteOverview(props: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await props.params;
|
||||
const safeLocale = locale === 'de' ? 'de' : 'en';
|
||||
setRequestLocale(safeLocale);
|
||||
const t = await getTranslations('StandardPage');
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen bg-neutral-50 pb-16 md:pb-24">
|
||||
{/* Map Hero Section */}
|
||||
<InteractiveGermanyMap
|
||||
isHero={true}
|
||||
locations={standorteLocations}
|
||||
badge={safeLocale === 'en' ? 'Our Locations' : 'Unsere Standorte'}
|
||||
title={safeLocale === 'en' ? 'Nationwide operational for you.' : 'Deutschlandweit für Sie im Einsatz.'}
|
||||
description={safeLocale === '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.'}
|
||||
/>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<Container className="relative z-20 mt-16 md:mt-24">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
|
||||
{standorteData.map((standort) => (
|
||||
<TrackedLink
|
||||
key={standort.id}
|
||||
href={`/${safeLocale}/standorte/${standort.id}`}
|
||||
eventProperties={{ location: 'standorte_list_card', slug: standort.id }}
|
||||
className="group flex flex-col bg-white border border-neutral-100 rounded-3xl overflow-hidden shadow-sm hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-1 cursor-pointer block"
|
||||
>
|
||||
<div className="flex flex-col h-full relative">
|
||||
{/* Image Section */}
|
||||
<div className="relative h-64 w-full bg-[#050B14] overflow-hidden shrink-0 border-b border-neutral-100">
|
||||
<Image
|
||||
src={standort.image}
|
||||
alt={standort.name}
|
||||
fill
|
||||
className="object-cover transition-transform duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] filter grayscale-[20%] group-hover:scale-105 group-hover:grayscale-0"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/10 transition-colors duration-500" />
|
||||
|
||||
{/* Location Badge */}
|
||||
<div className="absolute top-4 left-4 z-10">
|
||||
<span className="bg-white/95 backdrop-blur-md text-neutral-dark px-3 py-1.5 rounded-sm text-[10px] font-bold uppercase tracking-widest flex items-center gap-1.5 shadow-sm border border-neutral-200">
|
||||
<MapPin className="w-3 h-3 text-primary" />
|
||||
{standort.address.city}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Section */}
|
||||
<div className="flex flex-col flex-grow p-6 md:p-8 bg-white">
|
||||
<div className="flex items-start justify-between gap-4 mb-4">
|
||||
<h3 className="text-xl md:text-2xl font-bold font-heading text-neutral-dark transition-colors leading-[1.2] group-hover:text-primary">
|
||||
{standort.name}
|
||||
</h3>
|
||||
<ArrowUpRight className="w-5 h-5 text-neutral-400 group-hover:text-primary transition-colors shrink-0" />
|
||||
</div>
|
||||
|
||||
<p className="text-neutral-500 text-sm leading-relaxed mb-8 line-clamp-3">
|
||||
{standort.description[safeLocale]}
|
||||
</p>
|
||||
|
||||
{/* Meta Data */}
|
||||
<div className="flex flex-col gap-3 mt-auto border-t border-neutral-100 pt-6 relative z-10">
|
||||
<div className="min-w-0">
|
||||
<span className="block text-[10px] uppercase tracking-widest text-neutral-400 font-bold mb-1">
|
||||
{safeLocale === 'de' ? 'Adresse' : 'Address'}
|
||||
</span>
|
||||
<span className="flex items-start gap-2 text-sm font-semibold text-neutral-700 leading-tight">
|
||||
<Navigation className="w-3.5 h-3.5 text-primary shrink-0 mt-[2px]" />
|
||||
<span className="line-clamp-2">{standort.address.street}, {standort.address.city}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TrackedLink>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Support Section */}
|
||||
<div className="mt-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group">
|
||||
<div className="absolute top-0 right-0 w-64 h-full bg-accent/5 -skew-x-12 translate-x-1/2 transition-transform group-hover:translate-x-1/3" />
|
||||
<div className="relative z-10 max-w-2xl">
|
||||
<h3 className="text-2xl md:text-3xl font-bold mb-4">{t('nextProjectTitle')}</h3>
|
||||
<p className="text-lg text-white/70 mb-8">{t('nextProjectDesc')}</p>
|
||||
<TrackedLink
|
||||
href={`/${safeLocale}/${safeLocale === 'de' ? 'kontakt' : 'contact'}`}
|
||||
className={getButtonClasses('accent', 'lg')}
|
||||
eventProperties={{
|
||||
location: 'standorte_support_cta',
|
||||
}}
|
||||
>
|
||||
<span className="relative z-10 flex items-center justify-center gap-2 transition-colors duration-500 group-hover/btn:text-primary-dark">
|
||||
{t('contactUs')}
|
||||
<span className="ml-2 transition-transform group-hover/btn:translate-x-1">
|
||||
→
|
||||
</span>
|
||||
</span>
|
||||
<ButtonOverlay variant="accent" />
|
||||
</TrackedLink>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,8 @@ interface TrackedLinkProps {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
target?: string;
|
||||
rel?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,6 +27,8 @@ export default function TrackedLink({
|
||||
className,
|
||||
children,
|
||||
onClick,
|
||||
target,
|
||||
rel,
|
||||
}: TrackedLinkProps) {
|
||||
const { trackEvent } = useAnalytics();
|
||||
|
||||
@@ -41,7 +45,7 @@ export default function TrackedLink({
|
||||
};
|
||||
|
||||
return (
|
||||
<Link href={href} className={className} onClick={handleClick}>
|
||||
<Link href={href} className={className} onClick={handleClick} target={target} rel={rel}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -5,16 +5,19 @@ import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { MapPin, Factory, Zap, CheckCircle2, ArrowUpRight } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder';
|
||||
import { Location, defaultLocations, minorLocations } from '@/lib/map-data';
|
||||
import { standorteLocations } from '@/lib/standorte-data';
|
||||
import { useLocale, useTranslations } from 'next-intl';
|
||||
|
||||
const allLocations = [...defaultLocations, ...minorLocations];
|
||||
const allLocations = [...standorteLocations, ...defaultLocations, ...minorLocations];
|
||||
|
||||
interface Stat {
|
||||
value: string;
|
||||
suffix?: string;
|
||||
label: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
interface InteractiveGermanyMapProps {
|
||||
@@ -42,13 +45,13 @@ export function InteractiveGermanyMap({
|
||||
|
||||
const finalStats = stats || [
|
||||
{ value: '100', suffix: '%', label: locale === 'en' ? 'Nationwide Reach' : 'Überregionale Reichweite' },
|
||||
{ value: '3', suffix: '', label: locale === 'en' ? 'Operational Locations' : 'Operative Standorte' },
|
||||
{ value: '3', suffix: '', label: locale === 'en' ? 'Operational Locations' : 'Operative Standorte', href: `/${locale}/standorte` },
|
||||
];
|
||||
|
||||
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 and Bülstedt, we control and implement complex infrastructure projects nationwide.' : 'Von unseren strategischen Standorten in Guben und Bülstedt steuern und realisieren wir komplexe Infrastrukturprojekte im gesamten Bundesgebiet.');
|
||||
const finalDescription = description || (locale === 'en' ? 'From our strategic locations in Guben, Kirchheilingen and Bülstedt, we control and implement complex infrastructure projects nationwide.' : 'Von unseren strategischen Standorten in Guben, Kirchheilingen und Bülstedt steuern und realisieren wir komplexe Infrastrukturprojekte im gesamten Bundesgebiet.');
|
||||
|
||||
const hq = locations.find((l) => l.type === 'hq');
|
||||
const branch = locations.find((l) => l.type === 'branch');
|
||||
@@ -90,17 +93,32 @@ export function InteractiveGermanyMap({
|
||||
|
||||
{/* Industrial Stats Grid */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{finalStats.map((stat, i) => (
|
||||
<div key={i} className="bg-white/5 border border-white/10 rounded-2xl p-6 backdrop-blur-md relative overflow-hidden group">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
<div className="relative z-10">
|
||||
<div className="text-4xl font-extrabold text-white mb-1 flex items-baseline gap-1">
|
||||
{stat.value}<span className="text-xl text-primary">{stat.suffix}</span>
|
||||
{finalStats.map((stat, i) => {
|
||||
const StatCard = () => (
|
||||
<div className={`bg-white/5 border border-white/10 rounded-2xl p-6 backdrop-blur-md relative overflow-hidden group h-full ${stat.href ? 'cursor-pointer' : ''}`}>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
<div className="relative z-10 flex flex-col h-full justify-between">
|
||||
<div className="text-4xl font-extrabold text-white mb-1 flex items-baseline gap-1">
|
||||
{stat.value}<span className="text-xl text-primary">{stat.suffix}</span>
|
||||
</div>
|
||||
<div className={`text-sm text-white/50 font-medium flex items-center ${stat.href ? 'group-hover:text-primary transition-colors' : ''}`}>
|
||||
{stat.label}
|
||||
{stat.href && <ArrowUpRight className="w-4 h-4 ml-1 opacity-0 -translate-y-1 translate-x-1 group-hover:opacity-100 group-hover:translate-y-0 group-hover:translate-x-0 transition-all duration-300" />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-white/50 font-medium">{stat.label}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
|
||||
return stat.href ? (
|
||||
<Link key={i} href={stat.href} className="block group/link hover:-translate-y-1 transition-transform duration-300">
|
||||
<StatCard />
|
||||
</Link>
|
||||
) : (
|
||||
<div key={i}>
|
||||
<StatCard />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -130,8 +130,14 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
|
||||
transition={{ delay: i * 0.1, duration: 0.6, ease: "easeOut" }}
|
||||
className="flex-shrink-0 w-[320px] md:w-[480px] snap-center group pointer-events-auto"
|
||||
>
|
||||
<div
|
||||
<Link
|
||||
href={`/${locale}/referenzen/${ref.slug}`}
|
||||
data-testid="reference-tile"
|
||||
onClick={(e) => {
|
||||
if (dragDistanceRef.current > 5) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
className="block relative aspect-[16/10] bg-neutral-800 rounded-2xl overflow-hidden mb-5 border border-white/5 shadow-2xl"
|
||||
>
|
||||
<Image
|
||||
@@ -151,7 +157,7 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
|
||||
</span>
|
||||
<h4 className="font-heading text-xl md:text-2xl font-bold leading-tight break-words">{ref.title}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import * as React from 'react';
|
||||
import { motion, Variants } from 'framer-motion';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
||||
import { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder';
|
||||
|
||||
@@ -87,8 +88,9 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
|
||||
{companiesData.map((company: any, index: number) => {
|
||||
const isCurrent = company.title.toUpperCase() === 'E-TIB GMBH';
|
||||
const hasUrl = !!company.url;
|
||||
const CardWrapper = hasUrl ? 'a' : 'div';
|
||||
const wrapperProps = hasUrl ? { href: company.url, target: "_blank", rel: "noopener noreferrer" } : {};
|
||||
const isExternal = hasUrl && company.url.startsWith('http');
|
||||
const CardWrapper = hasUrl ? (isExternal ? 'a' : Link) : 'div';
|
||||
const wrapperProps = hasUrl ? (isExternal ? { href: company.url, target: "_blank", rel: "noopener noreferrer" } : { href: company.url }) : {};
|
||||
|
||||
// Generate logo representation based on title
|
||||
const isEtib = company.title.includes('E-TIB');
|
||||
@@ -112,33 +114,21 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
|
||||
const positionClass = bgPositionClasses[index % bgPositionClasses.length];
|
||||
const animationClass = animationClasses[index % animationClasses.length];
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={index}
|
||||
variants={itemVariants}
|
||||
className="h-full"
|
||||
>
|
||||
<CardWrapper
|
||||
{...wrapperProps}
|
||||
className={`group relative overflow-hidden aspect-[4/3] flex flex-col justify-center items-center shadow-xl transition-all duration-500 bg-neutral-dark block w-full h-full sm:rounded-xl select-none ${
|
||||
isCurrent
|
||||
? 'border-2 border-primary shadow-[0_0_30px_rgba(17,124,97,0.15)] cursor-default'
|
||||
: 'border border-white/5 hover:shadow-2xl hover:-translate-y-1 hover:border-white/10 cursor-pointer'
|
||||
}`}
|
||||
>
|
||||
<div className="absolute inset-0 bg-[#0a192f] z-0">
|
||||
<Image
|
||||
src={company.backgroundImage}
|
||||
alt="Background"
|
||||
fill
|
||||
className={`object-cover transition-all duration-1000 ease-out ${
|
||||
isCurrent
|
||||
? 'opacity-30 grayscale-0 scale-105'
|
||||
: 'opacity-40 grayscale-[50%] group-hover:opacity-30 group-hover:grayscale-0 group-hover:scale-110'
|
||||
}`}
|
||||
sizes="(max-width: 768px) 100vw, 33vw"
|
||||
/>
|
||||
</div>
|
||||
const CardContent = (
|
||||
<>
|
||||
<div className="absolute inset-0 bg-[#0a192f] z-0">
|
||||
<Image
|
||||
src={company.backgroundImage}
|
||||
alt="Background"
|
||||
fill
|
||||
className={`object-cover transition-all duration-1000 ease-out ${
|
||||
isCurrent
|
||||
? 'opacity-30 grayscale-0 scale-105'
|
||||
: 'opacity-40 grayscale-[50%] group-hover:opacity-30 group-hover:grayscale-0 group-hover:scale-110'
|
||||
}`}
|
||||
sizes="(max-width: 768px) 100vw, 33vw"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Heavy dark gradient overlay to make logos pop */}
|
||||
<div className={`absolute inset-0 z-0 transition-colors duration-700 mix-blend-multiply ${
|
||||
@@ -227,13 +217,42 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
|
||||
{company.url && (
|
||||
<div className="absolute bottom-6 left-0 right-0 flex justify-center opacity-0 group-hover:opacity-100 transform translate-y-4 group-hover:translate-y-0 transition-all duration-500 z-20">
|
||||
<div className="flex items-center text-white font-bold uppercase text-xs tracking-widest bg-white/10 backdrop-blur-md px-4 py-2 rounded-full border border-white/20">
|
||||
Website Besuchen
|
||||
{isExternal ? (isDe ? 'Website Besuchen' : 'Visit Website') : (isDe ? 'Zum Standort' : 'View Location')}
|
||||
<svg className="w-4 h-4 ml-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</CardWrapper>
|
||||
|
||||
</>
|
||||
);
|
||||
|
||||
const sharedClass = `group relative overflow-hidden aspect-[4/3] flex flex-col justify-center items-center shadow-xl transition-all duration-500 bg-neutral-dark block w-full h-full sm:rounded-xl select-none ${
|
||||
isCurrent
|
||||
? 'border border-white/5 hover:shadow-2xl hover:-translate-y-1 hover:border-white/10 cursor-pointer'
|
||||
: 'border border-white/5 hover:shadow-2xl hover:-translate-y-1 hover:border-white/10 cursor-pointer'
|
||||
}`;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={index}
|
||||
variants={itemVariants}
|
||||
className="h-full"
|
||||
>
|
||||
{hasUrl ? (
|
||||
isExternal ? (
|
||||
<a href={company.url} target="_blank" rel="noopener noreferrer" className={sharedClass}>
|
||||
{CardContent}
|
||||
</a>
|
||||
) : (
|
||||
<Link href={company.url as string} className={sharedClass}>
|
||||
{CardContent}
|
||||
</Link>
|
||||
)
|
||||
) : (
|
||||
<div className={sharedClass}>
|
||||
{CardContent}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface NavLink {
|
||||
label: string;
|
||||
url: string;
|
||||
children?: NavLink[];
|
||||
isGroupLabel?: boolean;
|
||||
}
|
||||
|
||||
interface HeaderProps {
|
||||
@@ -218,11 +219,20 @@ function NavItem({ link, currentLocale, pathname, isSolidMode }: { link: NavLink
|
||||
{/* Top glowing accent line */}
|
||||
<div className="absolute top-0 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-primary to-transparent opacity-70" />
|
||||
|
||||
{link.children.map((child) => {
|
||||
{link.children.map((child, idx) => {
|
||||
const childUrl = child.url.startsWith('/') && !child.url.match(/^\/(en|de)/)
|
||||
? `/${currentLocale}${child.url}`
|
||||
: child.url;
|
||||
const isChildActive = pathname === childUrl;
|
||||
|
||||
if (child.isGroupLabel) {
|
||||
return (
|
||||
<div key={`group-${idx}`} className={`mt-2 mb-1 px-4 text-[11px] font-bold uppercase tracking-wider ${isSolidMode ? 'text-neutral-400' : 'text-neutral-500'}`}>
|
||||
{child.label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TransitionLink
|
||||
key={child.url}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: "Grabenlose Leitungsverlegung"
|
||||
title: "Bohrtechnik"
|
||||
date: "2024-03-20"
|
||||
excerpt: "Präzise Horizontalspülbohrungen und Erdraketen in allen Bodenklassen für eine absolut oberflächenschonende und effiziente Leitungsverlegung."
|
||||
layout: "fullBleed"
|
||||
@@ -7,8 +7,8 @@ layout: "fullBleed"
|
||||
|
||||
<HeroSection
|
||||
badge="Kernkompetenz"
|
||||
title="Grabenlose Verlegung"
|
||||
subtitle="Oberflächenschonende Horizontalspülbohrungen und Erdraketen"
|
||||
title="Bohrtechnik"
|
||||
subtitle="Grabenlose Leitungsverlegung, Horizontalspülbohrungen und Erdraketen"
|
||||
backgroundImage={{ url: '/assets/photos/DSC08653.JPG' }}
|
||||
alignment="center"
|
||||
ctaLabel="Projekt anfragen"
|
||||
|
||||
@@ -20,6 +20,7 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
|
||||
title: "E-TIB GMBH",
|
||||
description: "Das komplette Leistungsspektrum zur Errichtung von Kabeltrassen aus einer Hand.",
|
||||
icon: "M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z",
|
||||
url: "/de/standorte/guben",
|
||||
backgroundImage: "/assets/photos/DJI_0243.JPG"
|
||||
},
|
||||
{
|
||||
@@ -33,6 +34,7 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
|
||||
title: "E-TIB Verwaltung GmbH",
|
||||
description: "Zentrale Dienste, Einkauf und Finanzen sowie Verwaltung von Immobilien, Grundstücken, Maschinen und Geräten.",
|
||||
icon: "M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4",
|
||||
url: "/de/standorte/buelstedt",
|
||||
backgroundImage: "/assets/photos/DJI_0048.JPG"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -65,7 +65,7 @@ layout: "fullBleed"
|
||||
title: "E-TIB GMBH",
|
||||
description: "Das komplette Leistungsspektrum zur Errichtung von Kabeltrassen aus einer Hand.",
|
||||
icon: "M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z",
|
||||
url: "/de/kabeltiefbau",
|
||||
url: "/de/standorte/guben",
|
||||
backgroundImage: "/assets/photos/DJI_0243.JPG"
|
||||
},
|
||||
{
|
||||
@@ -79,6 +79,7 @@ layout: "fullBleed"
|
||||
title: "E-TIB Verwaltung GmbH",
|
||||
description: "Zentrale Dienste, Einkauf und Finanzen sowie Verwaltung von Immobilien, Grundstücken, Maschinen und Geräten.",
|
||||
icon: "M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4",
|
||||
url: "/de/standorte/buelstedt",
|
||||
backgroundImage: "/assets/photos/DJI_0048.JPG"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: "Trenchless Line Laying"
|
||||
title: "Drilling Technology"
|
||||
date: "2024-03-20"
|
||||
excerpt: "Precise horizontal directional drilling and earth rockets in all soil classes for surface-friendly and efficient line laying."
|
||||
layout: "fullBleed"
|
||||
@@ -7,8 +7,8 @@ layout: "fullBleed"
|
||||
|
||||
<HeroSection
|
||||
badge="Core Competency"
|
||||
title="Trenchless Laying"
|
||||
subtitle="Surface-friendly horizontal directional drilling and earth rockets"
|
||||
title="Drilling Technology"
|
||||
subtitle="Trenchless Laying, horizontal directional drilling and earth rockets"
|
||||
backgroundImage={{ url: '/assets/photos/DSC08653.JPG' }}
|
||||
alignment="center"
|
||||
ctaLabel="Request a project"
|
||||
|
||||
@@ -20,6 +20,7 @@ description: "E-TIB GmbH is your reliable partner for complex cable routes, hori
|
||||
title: "E-TIB GMBH",
|
||||
description: "The complete range of services for the construction of cable routes from a single source.",
|
||||
icon: "M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z",
|
||||
url: "/en/standorte/guben",
|
||||
backgroundImage: "/assets/photos/DJI_0243.JPG"
|
||||
},
|
||||
{
|
||||
@@ -33,6 +34,7 @@ description: "E-TIB GmbH is your reliable partner for complex cable routes, hori
|
||||
title: "E-TIB Verwaltung GmbH",
|
||||
description: "Central services, purchasing, finance, and management of real estate, properties, machinery, and equipment.",
|
||||
icon: "M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4",
|
||||
url: "/en/standorte/buelstedt",
|
||||
backgroundImage: "/assets/photos/DJI_0048.JPG"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -65,7 +65,7 @@ layout: "fullBleed"
|
||||
title: "E-TIB GMBH",
|
||||
description: "The complete range of services for the construction of cable routes from a single source.",
|
||||
icon: "M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z",
|
||||
url: "/en/cable-civil-engineering",
|
||||
url: "/en/standorte/guben",
|
||||
backgroundImage: "/assets/photos/DJI_0243.JPG"
|
||||
},
|
||||
{
|
||||
@@ -79,6 +79,7 @@ layout: "fullBleed"
|
||||
title: "E-TIB Verwaltung GmbH",
|
||||
description: "Central services, purchasing, finance, and management of real estate, properties, machinery, and equipment.",
|
||||
icon: "M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4",
|
||||
url: "/en/standorte/buelstedt",
|
||||
backgroundImage: "/assets/photos/DJI_0048.JPG"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -11,34 +11,6 @@ export interface Location {
|
||||
}
|
||||
|
||||
export const defaultLocations: Location[] = [
|
||||
{
|
||||
id: 'guben',
|
||||
name: 'Guben (Hauptsitz)',
|
||||
type: 'hq',
|
||||
x: 85,
|
||||
y: 41.1,
|
||||
description: 'E-TIB GmbH Holding & Bohrtechnik GmbH',
|
||||
details: ['Zentrale Steuerung', 'Kabelleitungstiefbau', 'Maschinenpark'],
|
||||
},
|
||||
{
|
||||
id: 'kirchheilingen',
|
||||
name: 'E-TIB Bohrtechnik GmbH',
|
||||
type: 'branch',
|
||||
x: 52,
|
||||
y: 55,
|
||||
description: 'Standort in Thüringen',
|
||||
href: '/bohrtechnik',
|
||||
details: ['Bahnhofstraße 180a', '99947 Kirchheilingen'],
|
||||
},
|
||||
{
|
||||
id: 'buelstedt',
|
||||
name: 'Bülstedt (Ingenieurgesellschaft)',
|
||||
type: 'branch',
|
||||
x: 37,
|
||||
y: 25.33,
|
||||
description: 'E-TIB Ingenieurgesellschaft mbH',
|
||||
details: ['Planung & Projektierung', 'Vermessung', 'Dokumentation'],
|
||||
},
|
||||
{
|
||||
id: 'dettmannsdorf',
|
||||
name: 'Dettmannsdorf (PV)',
|
||||
|
||||
174
lib/standorte-data.ts
Normal file
174
lib/standorte-data.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { LucideIcon, MapPin, Building2, HardHat, Phone, Mail, Navigation } from 'lucide-react';
|
||||
|
||||
export interface ContactPerson {
|
||||
name: string;
|
||||
role: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
export interface StandortData {
|
||||
id: string;
|
||||
name: string;
|
||||
shortName: string;
|
||||
description: Record<'de' | 'en', string>;
|
||||
address: {
|
||||
street: string;
|
||||
city: string;
|
||||
};
|
||||
contact: {
|
||||
phone: string;
|
||||
email: string;
|
||||
};
|
||||
coordinates: {
|
||||
lat: number;
|
||||
lng: number;
|
||||
};
|
||||
keyFeatures: Record<'de' | 'en', string[]>;
|
||||
image: string;
|
||||
gallery: string[];
|
||||
type: 'hq' | 'branch';
|
||||
}
|
||||
|
||||
export const standorteData: StandortData[] = [
|
||||
{
|
||||
id: 'guben',
|
||||
name: 'E-TIB GmbH Holding & Bohrtechnik GmbH',
|
||||
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.',
|
||||
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: {
|
||||
street: 'Gewerbestraße 22',
|
||||
city: '03172 Guben'
|
||||
},
|
||||
contact: {
|
||||
phone: '+49 (0) 3561 / 68577 33',
|
||||
email: 'info@e-tib.com'
|
||||
},
|
||||
coordinates: {
|
||||
lat: 51.942,
|
||||
lng: 14.717
|
||||
},
|
||||
keyFeatures: {
|
||||
de: [
|
||||
'Zentrale strategische Steuerung',
|
||||
'Kabelleitungstiefbau & Infrastruktur',
|
||||
'Koordination Großmaschinenpark',
|
||||
'Zentrales Projektmanagement'
|
||||
],
|
||||
en: [
|
||||
'Central strategic management',
|
||||
'Underground cable engineering & infrastructure',
|
||||
'Coordination of heavy machinery',
|
||||
'Central project management'
|
||||
]
|
||||
},
|
||||
image: '/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg',
|
||||
gallery: [
|
||||
'/assets/photos/DSC02114.jpg',
|
||||
'/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-45.jpg'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'kirchheilingen',
|
||||
name: 'E-TIB Bohrtechnik GmbH',
|
||||
shortName: 'Standort Thüringen',
|
||||
type: 'branch',
|
||||
description: {
|
||||
de: 'Unser spezialisierter Standort für Horizontalspülbohrtechnik in Thüringen. Mit modernsten Bohranlagen realisieren wir grabenlose Leitungsverlegungen für komplexe Infrastrukturprojekte, umweltschonend und hocheffizient.',
|
||||
en: 'Our specialized location for horizontal directional drilling in Thuringia. Utilizing state-of-the-art drilling rigs, we implement trenchless cable laying for complex infrastructure projects, environmentally friendly and highly efficient.'
|
||||
},
|
||||
address: {
|
||||
street: 'Bahnhofstraße 180a',
|
||||
city: '99947 Kirchheilingen'
|
||||
},
|
||||
contact: {
|
||||
phone: '+49 (0) 3561 / 68577 33', // Placeholder, using central
|
||||
email: 'info@e-tib.com'
|
||||
},
|
||||
coordinates: {
|
||||
lat: 51.1852,
|
||||
lng: 10.6865
|
||||
},
|
||||
keyFeatures: {
|
||||
de: [
|
||||
'Horizontalspülbohrtechnik (HDD)',
|
||||
'Grabenlose Rohrverlegung',
|
||||
'Felsbohrungen',
|
||||
'Unterquerung von Gewässern & Straßen'
|
||||
],
|
||||
en: [
|
||||
'Horizontal Directional Drilling (HDD)',
|
||||
'Trenchless pipe laying',
|
||||
'Rock drilling',
|
||||
'Crossing underneath water bodies & roads'
|
||||
]
|
||||
},
|
||||
image: '/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie_Vermessung_(247_von_80).jpg',
|
||||
gallery: [
|
||||
'/assets/photos/DSC02089.jpg',
|
||||
'/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-6.jpg'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'buelstedt',
|
||||
name: 'E-TIB Ingenieurgesellschaft mbH',
|
||||
shortName: 'Bülstedt (Ingenieurgesellschaft)',
|
||||
type: 'branch',
|
||||
description: {
|
||||
de: 'Unsere Ingenieurgesellschaft in Bülstedt ist spezialisiert auf die hochpräzise Planung, Vermessung und Projektierung von Energie- und Telekommunikationsnetzen. Hier verschmelzen technische Expertise und modernes Datenmanagement.',
|
||||
en: 'Our engineering firm in Bülstedt specializes in the high-precision planning, surveying, and project management of energy and telecommunication networks. This is where technical expertise and modern data management converge.'
|
||||
},
|
||||
address: {
|
||||
street: 'Dorfstraße 1', // Placeholder
|
||||
city: '27412 Bülstedt'
|
||||
},
|
||||
contact: {
|
||||
phone: '+49 (0) 3561 / 68577 33', // Placeholder, using central
|
||||
email: 'info@e-tib.com'
|
||||
},
|
||||
coordinates: {
|
||||
lat: 53.2167,
|
||||
lng: 9.15
|
||||
},
|
||||
keyFeatures: {
|
||||
de: [
|
||||
'Trassenplanung & Projektierung',
|
||||
'Geodätische Vermessung (GPS/Tachymetrie)',
|
||||
'As-Built Dokumentation (GIS/CAD)',
|
||||
'Genehmigungsmanagement'
|
||||
],
|
||||
en: [
|
||||
'Route planning & project management',
|
||||
'Geodetic surveying (GPS/Tacheometry)',
|
||||
'As-built documentation (GIS/CAD)',
|
||||
'Permit management'
|
||||
]
|
||||
},
|
||||
image: '/assets/photos/etib_ingenieure_tiefbau_solar_pv_planung_vermessung_gis-256.jpg',
|
||||
gallery: [
|
||||
'/assets/photos/etib_ingenieure_tiefbau_solar_pv_planung_vermessung_gis-294.jpg',
|
||||
'/assets/photos/etib_ingenieure_tiefbau_solar_pv_planung_vermessung_gis-298.jpg'
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export function getStandortById(id: string): StandortData | undefined {
|
||||
return standorteData.find(s => s.id === id);
|
||||
}
|
||||
|
||||
// Convert StandortData to the Location format required by the map
|
||||
export const standorteLocations = standorteData.map(standort => ({
|
||||
id: standort.id,
|
||||
name: standort.shortName,
|
||||
type: standort.type,
|
||||
x: standort.id === 'kirchheilingen' ? 52 : standort.id === 'buelstedt' ? 37 : 85,
|
||||
y: standort.id === 'kirchheilingen' ? 55 : standort.id === 'buelstedt' ? 25.33 : 41.1,
|
||||
description: standort.name,
|
||||
href: `/standorte/${standort.id}`,
|
||||
details: standort.keyFeatures.de.slice(0, 3)
|
||||
}));
|
||||
Reference in New Issue
Block a user