Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 460e27c497 | |||
| e257f8450b | |||
| 09a1ac47a7 | |||
| 2241775a7f | |||
| 7eb9e0cfe6 | |||
| ab4f4af555 | |||
| b6bd2065bb | |||
| 8d69ac2de2 | |||
| a9f16b49cc | |||
| 21cf0725ab | |||
| 2c162f5314 | |||
| be3b2e6a7b | |||
| f2e59fa52f | |||
| 16d494689f | |||
| d24b8a8694 | |||
| 8d42946711 | |||
| b15217b9be | |||
| b138025380 | |||
| 18d2ace03b | |||
| 89ed1faa6b | |||
| 7307e812a4 | |||
| 20d6194a7e | |||
| 86e2fd7097 | |||
| bf8073f5bf | |||
| e8adb87b02 | |||
| 067e34c19b | |||
| e6017e7436 | |||
| 1955bfcd1b | |||
| 73886406a4 | |||
| b2701d6bcd | |||
| 4f46825fb4 | |||
| c63766711d | |||
| c738001f9a | |||
| 69dd79dfad | |||
| a6c4cc53f9 | |||
| 29f83f8151 | |||
| cec08e3ab3 | |||
| 837567c1f4 | |||
| 4dcc0061fc | |||
| bd25ec935d | |||
| b3b85202eb | |||
| b4b0c7c48a | |||
| 35fb37bb14 | |||
| fb2d385e88 | |||
| c54085fe3f | |||
| 232c55c566 | |||
| 52d81ea6cc | |||
| bc0e7c2283 | |||
| 6d2152998b | |||
| f727e2abfb | |||
| f04163f56d | |||
| e9e209d6e7 | |||
| 9af6a53325 | |||
| 8627bae98b | |||
| 17620482da | |||
| c5e9a67546 | |||
| 1b7e42ccc9 | |||
| 7a2382606e | |||
| 3b16dbe08d | |||
| 09d0395233 | |||
| 857c055862 | |||
| 500c2c64f4 | |||
| e13ce8bcec | |||
| feb8372a7c | |||
| c190a4f67e | |||
| 6717b32d44 | |||
| f1732917bc | |||
| 1f666d1b34 | |||
| ab42590206 | |||
| 7d4ff03806 | |||
| 26ed3e3f1f | |||
| eaf40dec7c | |||
| d56ce045c3 | |||
| 586b6cdee8 | |||
| f1b8ee1188 | |||
| 88cc214ddb | |||
| 25facb995f | |||
| 7b01924055 | |||
| 474672b2a1 | |||
| 7e552bbea5 | |||
| 21886a693c | |||
| 45944cbfa7 | |||
| 761c9c956f | |||
| e18559b83c | |||
| 818ccc8fae | |||
| b64b2ccf25 | |||
| e219b3b818 | |||
| 88628ee663 | |||
| 734dd19725 | |||
| 3175b99177 | |||
| 1986280520 | |||
| d4437f5274 | |||
| f09c338d0d | |||
| 91d6073a58 | |||
| c5f2642d05 | |||
| c2bfdc7e7c | |||
| 5625428113 | |||
| 8f9a37466c | |||
| 803ba997df | |||
| 4985ae0315 | |||
| 5c93a51db9 | |||
| 70a312052c | |||
| 0ccc408a8d | |||
| f9f0970f3f |
@@ -20,6 +20,7 @@ concurrency:
|
|||||||
env:
|
env:
|
||||||
PUPPETEER_SKIP_DOWNLOAD: "true"
|
PUPPETEER_SKIP_DOWNLOAD: "true"
|
||||||
COREPACK_NPM_REGISTRY: "https://registry.npmmirror.com"
|
COREPACK_NPM_REGISTRY: "https://registry.npmmirror.com"
|
||||||
|
GIT_HTTP_VERSION: HTTP/1.1
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -49,9 +50,7 @@ jobs:
|
|||||||
echo "=== Tool Cache Usage ==="
|
echo "=== Tool Cache Usage ==="
|
||||||
du -sh /opt/hostedtoolcache/* || true
|
du -sh /opt/hostedtoolcache/* || true
|
||||||
du -sh /opt/hostedtoolcache/.[!.]* || true
|
du -sh /opt/hostedtoolcache/.[!.]* || true
|
||||||
echo "Purging old tool caches..."
|
echo "Purging old tool caches skipped to prevent runner corruption."
|
||||||
rm -rf /opt/hostedtoolcache/* || true
|
|
||||||
rm -rf /opt/hostedtoolcache/.[!.]* || true
|
|
||||||
echo "=== Host Disk Usage ==="
|
echo "=== Host Disk Usage ==="
|
||||||
docker run --rm -v /:/host-root alpine df -h || true
|
docker run --rm -v /:/host-root alpine df -h || true
|
||||||
echo "=== Host Inode Usage ==="
|
echo "=== Host Inode Usage ==="
|
||||||
@@ -60,23 +59,7 @@ jobs:
|
|||||||
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Image}}" || true
|
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Image}}" || true
|
||||||
echo "=== Host Docker Info ==="
|
echo "=== Host Docker Info ==="
|
||||||
docker info || true
|
docker info || true
|
||||||
echo "Pruning host Buildkit builder caches aggressively..."
|
echo "=== System Disk Usage After Check ==="
|
||||||
for container in $(docker ps --filter "name=buildx_buildkit_builder" --format "{{.Names}}"); do
|
|
||||||
echo "Pruning Buildkit inside container: $container"
|
|
||||||
docker exec -i "$container" buildctl prune --all || true
|
|
||||||
done
|
|
||||||
echo "Removing buildkit builder containers to clear overlay storage..."
|
|
||||||
containers=$(docker ps -a --filter "name=buildx_buildkit_builder" -q)
|
|
||||||
if [ -n "$containers" ]; then
|
|
||||||
docker rm -f $containers || true
|
|
||||||
fi
|
|
||||||
echo "=== Truncating host Docker container log files ==="
|
|
||||||
docker run --rm -v /:/host-root alpine sh -c "truncate -s 0 /host-root/mnt/HC_Volume_104796416/docker/containers/*/*.log" || true
|
|
||||||
echo "Purging old build layers, unused volumes and images..."
|
|
||||||
docker builder prune -a -f || true
|
|
||||||
docker volume prune -f || true
|
|
||||||
docker system prune -a -f --volumes || true
|
|
||||||
echo "=== System Disk Usage After Prune ==="
|
|
||||||
df -h || true
|
df -h || true
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -112,6 +95,15 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
TARGET="branch"
|
TARGET="branch"
|
||||||
|
|
||||||
|
# Check if this exact commit has a tag. If so, skip the branch deploy
|
||||||
|
# because the tag pipeline will handle the production/staging deploy.
|
||||||
|
git fetch --tags --quiet || true
|
||||||
|
if git describe --tags --exact-match HEAD >/dev/null 2>&1; then
|
||||||
|
echo "Commit has a tag. Skipping branch deployment to favor tag deployment."
|
||||||
|
TARGET="skip"
|
||||||
|
fi
|
||||||
|
|
||||||
SLUG=$(echo "$REF" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')
|
SLUG=$(echo "$REF" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')
|
||||||
IMAGE_TAG="branch-${SLUG}-${SHORT_SHA}"
|
IMAGE_TAG="branch-${SLUG}-${SHORT_SHA}"
|
||||||
ENV_FILE=".env.branch-${SLUG}"
|
ENV_FILE=".env.branch-${SLUG}"
|
||||||
@@ -218,6 +210,13 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: 🚦 Priority Gate Check
|
||||||
|
env:
|
||||||
|
GITEA_PAT: ${{ secrets.GITEA_PAT }}
|
||||||
|
CURRENT_RUN_ID: ${{ github.run_id }}
|
||||||
|
CURRENT_TARGET: ${{ needs.prepare.outputs.target }}
|
||||||
|
run: bash scripts/priority-gate.sh
|
||||||
|
|
||||||
- name: 🔐 Registry Auth
|
- name: 🔐 Registry Auth
|
||||||
id: auth
|
id: auth
|
||||||
env:
|
env:
|
||||||
@@ -297,6 +296,13 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: 🚦 Priority Gate Check
|
||||||
|
env:
|
||||||
|
GITEA_PAT: ${{ secrets.GITEA_PAT }}
|
||||||
|
CURRENT_RUN_ID: ${{ github.run_id }}
|
||||||
|
CURRENT_TARGET: ${{ needs.prepare.outputs.target }}
|
||||||
|
run: bash scripts/priority-gate.sh
|
||||||
- name: 📝 Generate Environment
|
- name: 📝 Generate Environment
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
{"chromeFlags":"--no-sandbox --disable-setuid-sandbox --disable-dev-shm-usage --headless=new","port":59457}
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2895
.lighthouseci/lhr-1782911226698.html
Normal file
2895
.lighthouseci/lhr-1782911226698.html
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { getAppServices } from '@/lib/services/create-services';
|
|
||||||
import { Button, Heading } from '@/components/ui';
|
import { Button, Heading } from '@/components/ui';
|
||||||
import { Terminal, Activity, AlertTriangle, RefreshCw, Home, ShieldAlert } from 'lucide-react';
|
import { Terminal, Activity, AlertTriangle, RefreshCw, Home, ShieldAlert } from 'lucide-react';
|
||||||
import { m } from 'framer-motion';
|
import { m } from 'framer-motion';
|
||||||
@@ -26,9 +25,7 @@ export default function Error({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const services = getAppServices();
|
console.error('Application error caught by boundary', {
|
||||||
services.errors.captureException(error);
|
|
||||||
services.logger.error('Application error caught by boundary', {
|
|
||||||
message: error?.message || 'Unknown error',
|
message: error?.message || 'Unknown error',
|
||||||
stack: error?.stack,
|
stack: error?.stack,
|
||||||
digest: error?.digest,
|
digest: error?.digest,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Footer } from '@/components/layout/Footer';
|
import { Footer } from '@/components/layout/Footer';
|
||||||
import { Header } from '@/components/layout/Header';
|
import { Header } from '@/components/layout/Header';
|
||||||
import { MobileBottomNav } from '@/components/layout/MobileBottomNav';
|
|
||||||
import JsonLd from '@/components/JsonLd';
|
import JsonLd from '@/components/JsonLd';
|
||||||
import SkipLink from '@/components/SkipLink';
|
import SkipLink from '@/components/SkipLink';
|
||||||
import AnalyticsShell from '@/components/analytics/AnalyticsShell';
|
import AnalyticsShell from '@/components/analytics/AnalyticsShell';
|
||||||
@@ -17,9 +16,13 @@ import { mapFileSlugToTranslated } from '@/lib/slugs';
|
|||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers';
|
||||||
|
|
||||||
import { TransitionProvider } from '@/components/providers/TransitionProvider';
|
import { TransitionProvider } from '@/components/providers/TransitionProvider';
|
||||||
import { PageTransitionShutter } from '@/components/providers/PageTransitionShutter';
|
|
||||||
import { InitialLoader } from '@/components/providers/InitialLoader';
|
import { DynamicInitialLoader as InitialLoader } from '@/components/providers/DynamicInitialLoader';
|
||||||
import { CorporateBackground } from '@/components/decorations/CorporateBackground';
|
import { DynamicMobileBottomNav as MobileBottomNav } from '@/components/layout/DynamicMobileBottomNav';
|
||||||
|
|
||||||
|
import { DynamicCorporateBackground as CorporateBackground } from '@/components/decorations/DynamicCorporateBackground';
|
||||||
|
const corporateBackground = null; // Removed to prevent line shifting
|
||||||
|
|
||||||
const inter = Inter({
|
const inter = Inter({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
display: 'swap',
|
display: 'swap',
|
||||||
@@ -188,15 +191,15 @@ export default async function Layout(props: {
|
|||||||
<NextIntlClientProvider messages={clientMessages} locale={safeLocale}>
|
<NextIntlClientProvider messages={clientMessages} locale={safeLocale}>
|
||||||
<TransitionProvider>
|
<TransitionProvider>
|
||||||
<CorporateBackground />
|
<CorporateBackground />
|
||||||
<InitialLoader shouldShowLoader={!hasSeenLoader} />
|
{!hasSeenLoader && <InitialLoader shouldShowLoader={!hasSeenLoader} />}
|
||||||
<PageTransitionShutter />
|
{/* PageTransitionShutter loaded dynamically in TransitionProvider */}
|
||||||
<SkipLink />
|
<SkipLink />
|
||||||
<Header navLinks={navLinks} />
|
<Header navLinks={navLinks} />
|
||||||
<MobileBottomNav navLinks={navLinks} currentLocale={safeLocale} />
|
<MobileBottomNav navLinks={navLinks} currentLocale={safeLocale} />
|
||||||
|
|
||||||
<main
|
<main
|
||||||
id="main-content"
|
id="main-content"
|
||||||
className="flex-grow animate-fade-in overflow-visible"
|
className="flex-grow overflow-visible"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -206,7 +209,7 @@ export default async function Layout(props: {
|
|||||||
<JsonLd />
|
<JsonLd />
|
||||||
<AnalyticsShell />
|
<AnalyticsShell />
|
||||||
|
|
||||||
{feedbackEnabled && <FeedbackClientWrapper feedbackEnabled={feedbackEnabled} />}
|
<FeedbackClientWrapper />
|
||||||
</TransitionProvider>
|
</TransitionProvider>
|
||||||
</NextIntlClientProvider>
|
</NextIntlClientProvider>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -122,7 +122,9 @@ export async function generateMetadata(props: any): Promise<Metadata> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Home(props: any) {
|
import { preload } from 'react-dom';
|
||||||
|
|
||||||
|
export default async function Home(props: { params: Promise<{ locale: string }> }) {
|
||||||
const { locale } = await props.params;
|
const { locale } = await props.params;
|
||||||
|
|
||||||
if (locale !== 'de' && locale !== 'en') {
|
if (locale !== 'de' && locale !== 'en') {
|
||||||
@@ -130,6 +132,12 @@ export default async function Home(props: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setRequestLocale(locale);
|
setRequestLocale(locale);
|
||||||
|
preload('/assets/videos/web/hero-kabelpflug-poster.webp', {
|
||||||
|
as: 'image',
|
||||||
|
fetchPriority: 'high',
|
||||||
|
imageSrcSet: '/assets/videos/web/hero-kabelpflug-poster-mobile.webp 800w, /assets/videos/web/hero-kabelpflug-poster.webp 1920w',
|
||||||
|
imageSizes: '100vw'
|
||||||
|
});
|
||||||
|
|
||||||
const mdx = await getMdxContent(locale, 'home');
|
const mdx = await getMdxContent(locale, 'home');
|
||||||
const allReferences = await getAllReferences(locale);
|
const allReferences = await getAllReferences(locale);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Container } from '@/components/ui';
|
import { Container } from '@/components/ui/Container';
|
||||||
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import TrackedLink from '@/components/analytics/TrackedLink';
|
import TrackedLink from '@/components/analytics/TrackedLink';
|
||||||
@@ -33,11 +33,15 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { preload } from 'react-dom';
|
||||||
|
|
||||||
export default async function StandorteOverview(props: { params: Promise<{ locale: string }> }) {
|
export default async function StandorteOverview(props: { params: Promise<{ locale: string }> }) {
|
||||||
const { locale } = await props.params;
|
const { locale } = await props.params;
|
||||||
const safeLocale = locale === 'de' ? 'de' : 'en';
|
const safeLocale = locale === 'de' ? 'de' : 'en';
|
||||||
setRequestLocale(safeLocale);
|
setRequestLocale(safeLocale);
|
||||||
const t = await getTranslations('StandardPage');
|
const t = await getTranslations('StandardPage');
|
||||||
|
|
||||||
|
preload('/germany-map.svg', { as: 'image', fetchPriority: 'high' });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col min-h-screen bg-neutral-50 pb-16 md:pb-24">
|
<div className="flex flex-col min-h-screen bg-neutral-50 pb-16 md:pb-24">
|
||||||
@@ -70,6 +74,7 @@ export default async function StandorteOverview(props: { params: Promise<{ local
|
|||||||
alt={standort.name}
|
alt={standort.name}
|
||||||
fill
|
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"
|
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"
|
||||||
|
sizes="(max-width: 768px) 200px, 384px"
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-black/10 transition-colors duration-500" />
|
<div className="absolute inset-0 bg-black/10 transition-colors duration-500" />
|
||||||
|
|
||||||
@@ -85,9 +90,9 @@ export default async function StandorteOverview(props: { params: Promise<{ local
|
|||||||
{/* Content Section */}
|
{/* Content Section */}
|
||||||
<div className="flex flex-col flex-grow p-6 md:p-8 bg-white">
|
<div className="flex flex-col flex-grow p-6 md:p-8 bg-white">
|
||||||
<div className="flex items-start justify-between gap-4 mb-4">
|
<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">
|
<h2 className="text-xl md:text-2xl font-bold font-heading text-neutral-dark transition-colors leading-[1.2] group-hover:text-primary">
|
||||||
{standort.name}
|
{standort.name}
|
||||||
</h3>
|
</h2>
|
||||||
<ArrowUpRight className="w-5 h-5 text-neutral-400 group-hover:text-primary transition-colors shrink-0" />
|
<ArrowUpRight className="w-5 h-5 text-neutral-400 group-hover:text-primary transition-colors shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -98,7 +103,7 @@ export default async function StandorteOverview(props: { params: Promise<{ local
|
|||||||
{/* Meta Data */}
|
{/* Meta Data */}
|
||||||
<div className="flex flex-col gap-3 mt-auto border-t border-neutral-100 pt-6 relative z-10">
|
<div className="flex flex-col gap-3 mt-auto border-t border-neutral-100 pt-6 relative z-10">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<span className="block text-[10px] uppercase tracking-widest text-neutral-400 font-bold mb-1">
|
<span className="block text-[10px] uppercase tracking-widest text-neutral-500 font-bold mb-1">
|
||||||
{safeLocale === 'de' ? 'Adresse' : 'Address'}
|
{safeLocale === 'de' ? 'Adresse' : 'Address'}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-start gap-2 text-sm font-semibold text-neutral-700 leading-tight">
|
<span className="flex items-start gap-2 text-sm font-semibold text-neutral-700 leading-tight">
|
||||||
@@ -117,7 +122,7 @@ export default async function StandorteOverview(props: { params: Promise<{ local
|
|||||||
<div className="mt-12 md:mt-24 p-8 md:p-12 bg-primary-dark rounded-3xl text-white shadow-2xl relative overflow-hidden group">
|
<div className="mt-12 md: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="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">
|
<div className="relative z-10 max-w-2xl">
|
||||||
<h3 className="text-2xl md:text-3xl font-bold mb-4">{t('nextProjectTitle')}</h3>
|
<h2 className="text-2xl md:text-3xl font-bold mb-4">{t('nextProjectTitle')}</h2>
|
||||||
<p className="text-lg text-white/70 mb-8">{t('nextProjectDesc')}</p>
|
<p className="text-lg text-white/70 mb-8">{t('nextProjectDesc')}</p>
|
||||||
<TrackedLink
|
<TrackedLink
|
||||||
href={`/${safeLocale}/${safeLocale === 'de' ? 'kontakt' : 'contact'}`}
|
href={`/${safeLocale}/${safeLocale === 'de' ? 'kontakt' : 'contact'}`}
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ export async function sendContactFormAction(formData: FormData) {
|
|||||||
|
|
||||||
// Notify via Gotify (Internal)
|
// Notify via Gotify (Internal)
|
||||||
await services.notifications.notify({
|
await services.notifications.notify({
|
||||||
title: `📩 ${notificationSubject}`,
|
title: `📩 [E-TIB] ${notificationSubject}`,
|
||||||
message: `New message from ${name} (${email}):\n\n${message}`,
|
message: `New message from ${name} (${email}):\n\n${message}`,
|
||||||
priority: 5,
|
priority: 5,
|
||||||
});
|
});
|
||||||
@@ -153,7 +153,7 @@ export async function sendContactFormAction(formData: FormData) {
|
|||||||
services.errors.captureException(error, { action: 'sendContactFormAction', email });
|
services.errors.captureException(error, { action: 'sendContactFormAction', email });
|
||||||
|
|
||||||
await services.notifications.notify({
|
await services.notifications.notify({
|
||||||
title: '🚨 Contact Form Error',
|
title: '🚨 [E-TIB] Contact Form Error',
|
||||||
message: `Failed to send emails for ${name} (${email}). Error: ${errorMsg}`,
|
message: `Failed to send emails for ${name} (${email}). Error: ${errorMsg}`,
|
||||||
priority: 8,
|
priority: 8,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import * as Sentry from '@sentry/nextjs';
|
|
||||||
import Error from 'next/error';
|
import Error from 'next/error';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
@@ -10,7 +9,7 @@ export default function GlobalError({
|
|||||||
error: Error & { digest?: string };
|
error: Error & { digest?: string };
|
||||||
}) {
|
}) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Sentry.captureException(error);
|
console.error('Global Error:', error);
|
||||||
}, [error]);
|
}, [error]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
@import "tailwindcss";
|
|
||||||
@plugin "@tailwindcss/typography";
|
|
||||||
|
|
||||||
@config "../tailwind.config.cjs";
|
|
||||||
|
|
||||||
/* E-TIB Custom Utilities */
|
|
||||||
@layer utilities {
|
|
||||||
.text-balance {
|
|
||||||
text-wrap: balance;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hide-scrollbar {
|
|
||||||
-ms-overflow-style: none !important; /* IE and Edge */
|
|
||||||
scrollbar-width: none !important; /* Firefox */
|
|
||||||
}
|
|
||||||
.hide-scrollbar::-webkit-scrollbar {
|
|
||||||
display: none !important; /* Chrome, Safari and Opera */
|
|
||||||
width: 0 !important;
|
|
||||||
height: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bg-pulse-15 {
|
|
||||||
0%, 90%, 100% { opacity: 0.04; }
|
|
||||||
95% { opacity: 0.10; filter: brightness(1.2); }
|
|
||||||
}
|
|
||||||
@keyframes bg-pulse-20 {
|
|
||||||
0%, 90%, 100% { opacity: 0.07; }
|
|
||||||
95% { opacity: 0.14; filter: brightness(1.2); }
|
|
||||||
}
|
|
||||||
.animate-bg-pulse-15 { animation: bg-pulse-15 12s infinite ease-in-out; }
|
|
||||||
.animate-bg-pulse-20 { animation: bg-pulse-20 12s infinite ease-in-out; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Base Styles */
|
|
||||||
@layer base {
|
|
||||||
html {
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
@apply bg-neutral font-sans text-neutral-dark antialiased;
|
|
||||||
}
|
|
||||||
/* Interactive elements pointer */
|
|
||||||
a,
|
|
||||||
button,
|
|
||||||
[role="button"],
|
|
||||||
input[type="button"],
|
|
||||||
input[type="submit"],
|
|
||||||
input[type="reset"],
|
|
||||||
select,
|
|
||||||
summary,
|
|
||||||
label[for] {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure disabled elements don't show pointer */
|
|
||||||
button:disabled,
|
|
||||||
[role="button"]:disabled,
|
|
||||||
input:disabled,
|
|
||||||
select:disabled,
|
|
||||||
button[disabled] {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom Modern Scrollbar */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background-color: rgba(14, 122, 92, 0.4);
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 4px solid transparent;
|
|
||||||
background-clip: padding-box;
|
|
||||||
transition: background-color 0.2s ease;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background-color: rgba(14, 122, 92, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Firefox */
|
|
||||||
html {
|
|
||||||
scrollbar-color: rgba(14, 122, 92, 0.5) transparent;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* trigger rebuild */
|
|
||||||
48
bundle-stats.json
Normal file
48
bundle-stats.json
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
.next/static/chunks/0b521531212fbb38.js
|
||||||
|
.next/static/chunks/0c4bd4931d57e66a.js
|
||||||
|
.next/static/chunks/114976d6e8233fa6.js
|
||||||
|
.next/static/chunks/163f657a16afbe5e.js
|
||||||
|
.next/static/chunks/1c2f1bc547a6b6db.js
|
||||||
|
.next/static/chunks/2583fa7802927c7b.js
|
||||||
|
.next/static/chunks/30da1bf4e9770388.js
|
||||||
|
.next/static/chunks/38b815a6d50481c3.js
|
||||||
|
.next/static/chunks/3dc2f63532eb2144.js
|
||||||
|
.next/static/chunks/3e327b4e2347bc0f.js
|
||||||
|
.next/static/chunks/3ef3cefd2dae1060.js
|
||||||
|
.next/static/chunks/45499dd88f8aa0be.js
|
||||||
|
.next/static/chunks/4b9828b23c1decfd.js
|
||||||
|
.next/static/chunks/4c801febc70daa53.js
|
||||||
|
.next/static/chunks/4d5ea13dba62fe42.js
|
||||||
|
.next/static/chunks/4dffe404993f353e.js
|
||||||
|
.next/static/chunks/4f185d0c413fdb9e.js
|
||||||
|
.next/static/chunks/4f69e8e09eeae4d6.js
|
||||||
|
.next/static/chunks/4fa8be7f170d31f3.js
|
||||||
|
.next/static/chunks/583b451485662887.js
|
||||||
|
.next/static/chunks/5e1e2d5c357c1b57.js
|
||||||
|
.next/static/chunks/6216b234a7a88a60.js
|
||||||
|
.next/static/chunks/651fbbae0062342b.js
|
||||||
|
.next/static/chunks/66a1b90666fc2830.js
|
||||||
|
.next/static/chunks/6c0ddf2fd63ab912.js
|
||||||
|
.next/static/chunks/72cf73de45fde6cf.js
|
||||||
|
.next/static/chunks/75ea3a278c540e05.js
|
||||||
|
.next/static/chunks/77a84d4dc662b7cf.js
|
||||||
|
.next/static/chunks/7c603cfd5a61abd1.js
|
||||||
|
.next/static/chunks/84ddf763fbc98e13.js
|
||||||
|
.next/static/chunks/885c97d656f95ebd.js
|
||||||
|
.next/static/chunks/8d6ae249fcc5e38f.js
|
||||||
|
.next/static/chunks/920f860e909b2eb8.js
|
||||||
|
.next/static/chunks/951d03271d663d8a.js
|
||||||
|
.next/static/chunks/a094276392fca24a.js
|
||||||
|
.next/static/chunks/a2e7fbe3f294caac.js
|
||||||
|
.next/static/chunks/a6c9d757034eb86a.js
|
||||||
|
.next/static/chunks/a6dc76eec9924929.js
|
||||||
|
.next/static/chunks/c3f21c4df538784a.js
|
||||||
|
.next/static/chunks/c641ee8514fa0552.js
|
||||||
|
.next/static/chunks/d8c0ef20e0ad9790.js
|
||||||
|
.next/static/chunks/e60e6d615bf7d2f5.js
|
||||||
|
.next/static/chunks/e714b6ccaf8d5f12.js
|
||||||
|
.next/static/chunks/eeebcf8c8de1a29a.js
|
||||||
|
.next/static/chunks/f0b47c1f19d25fd2.js
|
||||||
|
.next/static/chunks/f8d5b1a2accefb4c.js
|
||||||
|
.next/static/chunks/fa18a72fd9fb11d5.js
|
||||||
|
.next/static/chunks/turbopack-2aac2a40101e9988.js
|
||||||
@@ -2,17 +2,17 @@
|
|||||||
|
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
const FeedbackOverlay = dynamic(
|
const IS_ENABLED = process.env.NEXT_PUBLIC_FEEDBACK_ENABLED === 'true';
|
||||||
() => import('@mintel/next-feedback/FeedbackOverlay').then((mod) => mod.FeedbackOverlay),
|
|
||||||
{ ssr: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
interface FeedbackClientWrapperProps {
|
const FeedbackOverlay = IS_ENABLED
|
||||||
feedbackEnabled: boolean;
|
? dynamic(
|
||||||
}
|
() => import('@mintel/next-feedback/FeedbackOverlay').then((mod) => mod.FeedbackOverlay),
|
||||||
|
{ ssr: false },
|
||||||
|
)
|
||||||
|
: () => null;
|
||||||
|
|
||||||
export default function FeedbackClientWrapper({ feedbackEnabled }: FeedbackClientWrapperProps) {
|
export default function FeedbackClientWrapper() {
|
||||||
if (!feedbackEnabled) return null;
|
if (!IS_ENABLED) return null;
|
||||||
|
|
||||||
return <FeedbackOverlay />;
|
return <FeedbackOverlay />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { m, LazyMotion, AnimatePresence } from 'framer-motion';
|
import { m, AnimatePresence, LazyMotion } from 'framer-motion';
|
||||||
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
|
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
|
||||||
|
|
||||||
interface LightboxProps {
|
interface LightboxProps {
|
||||||
|
|||||||
@@ -2,34 +2,28 @@
|
|||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { usePathname, useSearchParams } from 'next/navigation';
|
import { usePathname, useSearchParams } from 'next/navigation';
|
||||||
import { getAppServices } from '@/lib/services/create-services';
|
import { useAnalytics } from './useAnalytics';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AnalyticsProvider Component
|
* AnalyticsProvider Component
|
||||||
*
|
*
|
||||||
* Automatically tracks pageviews on client-side route changes.
|
* Automatically tracks pageviews on client-side route changes.
|
||||||
* This component handles navigation events for the Umami analytics service.
|
* This component handles navigation events for the Umami analytics service.
|
||||||
*
|
|
||||||
* Note: Website ID is now centrally managed on the server side via a proxy,
|
|
||||||
* so it's no longer needed as a prop here.
|
|
||||||
*/
|
*/
|
||||||
export default function AnalyticsProvider() {
|
export default function AnalyticsProvider() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
const { trackPageview } = useAnalytics();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!pathname) return;
|
if (!pathname) return;
|
||||||
|
|
||||||
const services = getAppServices();
|
|
||||||
const url = `${pathname}${searchParams?.size ? `?${searchParams.toString()}` : ''}`;
|
const url = `${pathname}${searchParams?.size ? `?${searchParams.toString()}` : ''}`;
|
||||||
|
|
||||||
// Track pageview with the full URL
|
// Track pageview with the full URL
|
||||||
// The service will relay this to our internal proxy which injects the Website ID
|
// The service will relay this to our internal proxy which injects the Website ID
|
||||||
services.analytics.trackPageview(url);
|
trackPageview(url);
|
||||||
|
}, [pathname, searchParams, trackPageview]);
|
||||||
// Services like logger are already sub-initialized in getAppServices()
|
|
||||||
// so we don't need to log here manually.
|
|
||||||
}, [pathname, searchParams]);
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,7 @@ export default function ClientNotFoundTracker({ path }: { path: string }) {
|
|||||||
path,
|
path,
|
||||||
});
|
});
|
||||||
|
|
||||||
import('@sentry/nextjs').then((Sentry) => {
|
|
||||||
Sentry.withScope((scope) => {
|
|
||||||
scope.setTag('status_code', '404');
|
|
||||||
scope.setTag('path', path);
|
|
||||||
Sentry.captureMessage(`Route Not Found: ${path}`, 'warning');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, [trackEvent, path]);
|
}, [trackEvent, path]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,73 +1,69 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { getAppServices } from '@/lib/services/create-services';
|
|
||||||
import type { AnalyticsEventProperties } from '@/lib/services/analytics/analytics-service';
|
export type AnalyticsEventProperties = Record<string, any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a payload to the proxy API without loading backend config/logger dependencies.
|
||||||
|
*/
|
||||||
|
function sendAnalyticsPayload(type: 'event', data: Record<string, any>) {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
if (process.env.NEXT_PUBLIC_CI === 'true') return;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
hostname: window.location.hostname,
|
||||||
|
screen: `${window.screen.width}x${window.screen.height}`,
|
||||||
|
language: navigator.language,
|
||||||
|
referrer: document.referrer,
|
||||||
|
title: document.title,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
fetch('/stats/api/send', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ type, payload }),
|
||||||
|
keepalive: true,
|
||||||
|
}).catch(e => console.error('[Umami] Failed to send payload', e));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Umami] Error sending analytics', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom hook for tracking analytics events with Umami.
|
* Custom hook for tracking analytics events with Umami.
|
||||||
*
|
|
||||||
* Provides a convenient way to track custom events throughout your application.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* import { useAnalytics } from '@/components/analytics/useAnalytics';
|
|
||||||
*
|
|
||||||
* function MyComponent() {
|
|
||||||
* const { trackEvent, trackPageview } = useAnalytics();
|
|
||||||
*
|
|
||||||
* const handleButtonClick = () => {
|
|
||||||
* trackEvent('button_click', {
|
|
||||||
* button_id: 'cta-primary',
|
|
||||||
* page: 'homepage'
|
|
||||||
* });
|
|
||||||
* };
|
|
||||||
*
|
|
||||||
* return <button onClick={handleButtonClick}>Click me</button>;
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* // Track a custom pageview
|
|
||||||
* const { trackPageview } = useAnalytics();
|
|
||||||
* trackPageview('/custom-path?param=value');
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
export function useAnalytics() {
|
export function useAnalytics() {
|
||||||
const services = getAppServices();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Track a custom event with optional properties.
|
|
||||||
*
|
|
||||||
* @param eventName - The name of the event to track
|
|
||||||
* @param properties - Optional event properties (metadata)
|
|
||||||
*/
|
|
||||||
const trackEvent = useCallback(
|
const trackEvent = useCallback(
|
||||||
(eventName: string, properties?: AnalyticsEventProperties) => {
|
(eventName: string, properties?: AnalyticsEventProperties) => {
|
||||||
services.analytics.track(eventName, properties);
|
sendAnalyticsPayload('event', {
|
||||||
|
name: eventName,
|
||||||
|
data: properties,
|
||||||
|
url: window.location.pathname + window.location.search,
|
||||||
|
});
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
console.log('[Umami] Tracked event:', eventName, properties);
|
console.log('[Umami] Tracked event:', eventName, properties);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[services]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Track a pageview (useful for custom navigation or SPA routing).
|
|
||||||
*
|
|
||||||
* @param url - The URL to track (defaults to current location)
|
|
||||||
*/
|
|
||||||
const trackPageview = useCallback(
|
const trackPageview = useCallback(
|
||||||
(url?: string) => {
|
(url?: string) => {
|
||||||
services.analytics.trackPageview(url);
|
sendAnalyticsPayload('event', {
|
||||||
|
url: url || window.location.pathname + window.location.search,
|
||||||
|
});
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
console.log('[Umami] Tracked pageview:', url ?? 'current location');
|
console.log('[Umami] Tracked pageview:', url ?? 'current location');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[services]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useMemo } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import { AnimatePresence, m, LazyMotion, domAnimation } from 'framer-motion';
|
import { AnimatePresence, m } from 'framer-motion';
|
||||||
import { MessageSquare, X, Check, Image as ImageIcon, Type, StickyNote, Download, Trash2, List } from "lucide-react";
|
import { MessageSquare, X, Check, Image as ImageIcon, Type, StickyNote, Download, Trash2, List } from "lucide-react";
|
||||||
import { clsx, type ClassValue } from "clsx";
|
import { clsx, type ClassValue } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { m, LazyMotion, domAnimation, Variants } from 'framer-motion';
|
import { m, Variants } from 'framer-motion';
|
||||||
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
||||||
import { LogoArcs } from '@/components/ui/LogoArcs';
|
import { LogoArcs } from '@/components/ui/LogoArcs';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { m, LazyMotion, domAnimation } from 'framer-motion';
|
import { m } from 'framer-motion';
|
||||||
import { Container } from '@/components/ui/Container';
|
import { Container } from '@/components/ui/Container';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useScroll, useTransform, m, LazyMotion, domAnimation } from 'framer-motion';
|
import { useScroll, useTransform, m } from 'framer-motion';
|
||||||
import { Building2, Compass, Layers, ArrowDownToLine, Wrench, Factory, Zap, MapPin, CheckCircle2 } from 'lucide-react';
|
import { Building2, Compass, Layers, ArrowDownToLine, Wrench, Factory, Zap, MapPin, CheckCircle2 } from 'lucide-react';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { m, LazyMotion, domAnimation } from 'framer-motion';
|
import { m } from 'framer-motion';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import React, { Suspense } from 'react';
|
|||||||
import { Section, Container } from '@/components/ui';
|
import { Section, Container } from '@/components/ui';
|
||||||
import { ContactForm } from '@/components/forms/ContactForm';
|
import { ContactForm } from '@/components/forms/ContactForm';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { m, LazyMotion, domAnimation } from 'framer-motion';
|
import { m } from 'framer-motion';
|
||||||
|
|
||||||
const ContactMap = dynamic(() => import('@/components/ContactMap'), {
|
const ContactMap = dynamic(() => import('@/components/ContactMap'), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { m, LazyMotion, domAnimation } from 'framer-motion';
|
import { m } from 'framer-motion';
|
||||||
import { useLocale } from 'next-intl';
|
import { useLocale } from 'next-intl';
|
||||||
|
|
||||||
export function DataGridPulse() {
|
export function DataGridPulse() {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { useScroll, useTransform, m, LazyMotion, domAnimation } from 'framer-motion';
|
import { useScroll, useTransform, m } from 'framer-motion';
|
||||||
import { useLocale } from 'next-intl';
|
import { useLocale } from 'next-intl';
|
||||||
|
|
||||||
export function DeepDrillAnimation() {
|
export function DeepDrillAnimation() {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { m, LazyMotion, domAnimation } from 'framer-motion';
|
import { m } from 'framer-motion';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
export function GrowthChart() {
|
export function GrowthChart() {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { m, LazyMotion, domAnimation } from 'framer-motion';
|
|
||||||
import { Badge, Container, Heading } from '@/components/ui';
|
import { Badge, Container, Heading } from '@/components/ui';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
|
|
||||||
@@ -17,25 +16,7 @@ export interface HeroSectionProps {
|
|||||||
ctaHref?: string;
|
ctaHref?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const containerVariants = {
|
// CSS animations replace Framer Motion for better LCP
|
||||||
hidden: { opacity: 0 },
|
|
||||||
visible: {
|
|
||||||
opacity: 1,
|
|
||||||
transition: {
|
|
||||||
staggerChildren: 0.15,
|
|
||||||
delayChildren: 0.1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const itemVariants = {
|
|
||||||
hidden: { opacity: 0, y: 30 },
|
|
||||||
visible: {
|
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
|
||||||
transition: { duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const HeroSection: React.FC<HeroSectionProps> = (props) => {
|
export const HeroSection: React.FC<HeroSectionProps> = (props) => {
|
||||||
const { title, badge, subtitle, backgroundImage, alignment, ctaLabel, ctaHref } = props;
|
const { title, badge, subtitle, backgroundImage, alignment, ctaLabel, ctaHref } = props;
|
||||||
@@ -47,12 +28,9 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
|
|||||||
className={`relative min-h-[35vh] md:min-h-[50vh] flex items-center pt-28 pb-10 md:pt-36 md:pb-24 overflow-hidden bg-neutral-dark ${alignment === 'center' ? 'justify-center text-center' : ''}`}
|
className={`relative min-h-[35vh] md:min-h-[50vh] flex items-center pt-28 pb-10 md:pt-36 md:pb-24 overflow-hidden bg-neutral-dark ${alignment === 'center' ? 'justify-center text-center' : ''}`}
|
||||||
>
|
>
|
||||||
{bgSrc && (
|
{bgSrc && (
|
||||||
<m.div
|
<div
|
||||||
key={`hero-bg-${pathname}`}
|
key={`hero-bg-${pathname}`}
|
||||||
className="absolute inset-0 z-0"
|
className="absolute inset-0 z-0 animate-hero-zoom-out"
|
||||||
initial={{ scale: 1.05, opacity: 1 }}
|
|
||||||
animate={{ scale: 1, opacity: 1 }}
|
|
||||||
transition={{ duration: 1.5, ease: "easeOut" }}
|
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={bgSrc}
|
src={bgSrc}
|
||||||
@@ -63,6 +41,7 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
|
|||||||
objectPosition: `${backgroundImage?.focalX ?? 50}% ${backgroundImage?.focalY ?? 50}%`,
|
objectPosition: `${backgroundImage?.focalX ?? 50}% ${backgroundImage?.focalY ?? 50}%`,
|
||||||
}}
|
}}
|
||||||
sizes="100vw"
|
sizes="100vw"
|
||||||
|
quality={50}
|
||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
{/* Cinematic Color Grading Overlay */}
|
{/* Cinematic Color Grading Overlay */}
|
||||||
@@ -74,37 +53,34 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
|
|||||||
|
|
||||||
{/* Top Fade for Header Navigation Readability */}
|
{/* Top Fade for Header Navigation Readability */}
|
||||||
<div className="absolute top-0 left-0 w-full h-48 bg-gradient-to-b from-black/70 to-transparent z-[3] pointer-events-none" />
|
<div className="absolute top-0 left-0 w-full h-48 bg-gradient-to-b from-black/70 to-transparent z-[3] pointer-events-none" />
|
||||||
</m.div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Container className={`relative z-10 ${alignment === 'center' ? 'max-w-5xl' : ''}`}>
|
<Container className={`relative z-10 ${alignment === 'center' ? 'max-w-5xl' : ''}`}>
|
||||||
<m.div
|
<div
|
||||||
key={`hero-content-${pathname}`}
|
key={`hero-content-${pathname}`}
|
||||||
className={`max-w-4xl ${alignment === 'center' ? 'mx-auto' : ''}`}
|
className={`max-w-4xl ${alignment === 'center' ? 'mx-auto' : ''}`}
|
||||||
variants={containerVariants}
|
|
||||||
initial="hidden"
|
|
||||||
animate="visible"
|
|
||||||
>
|
>
|
||||||
{badge && (
|
{badge && (
|
||||||
<m.div variants={itemVariants}>
|
<div className="animate-hero-fade-in-up" style={{ animationDelay: '0.1s', opacity: 0, animationFillMode: 'forwards' }}>
|
||||||
<Badge variant="saturated" className="mb-4 md:mb-8 shadow-lg">
|
<Badge variant="saturated" className="mb-4 md:mb-8 shadow-lg">
|
||||||
{badge}
|
{badge}
|
||||||
</Badge>
|
</Badge>
|
||||||
</m.div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<m.div variants={itemVariants}>
|
<div className="animate-hero-fade-in-up" style={{ animationDelay: badge ? '0.25s' : '0.1s', opacity: 0, animationFillMode: 'forwards' }}>
|
||||||
<Heading level={1} size="section" variant="white" align={alignment || 'left'} className="mb-4 md:mb-8">
|
<Heading level={1} size="section" variant="white" align={alignment || 'left'} className="mb-4 md:mb-8">
|
||||||
{title}
|
{title}
|
||||||
</Heading>
|
</Heading>
|
||||||
</m.div>
|
</div>
|
||||||
{subtitle && (
|
{subtitle && (
|
||||||
<m.div variants={itemVariants}>
|
<div className="animate-hero-fade-in-up" style={{ animationDelay: badge ? '0.4s' : '0.25s', opacity: 0, animationFillMode: 'forwards' }}>
|
||||||
<p className={`text-lg md:text-2xl text-white/70 font-medium leading-relaxed max-w-2xl ${alignment === 'center' ? 'mx-auto' : ''}`}>
|
<p className={`text-lg md:text-2xl text-white/70 font-medium leading-relaxed max-w-2xl ${alignment === 'center' ? 'mx-auto' : ''}`}>
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</p>
|
</p>
|
||||||
</m.div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{ctaLabel && ctaHref && (
|
{ctaLabel && ctaHref && (
|
||||||
<m.div variants={itemVariants} className={`mt-8 ${alignment === 'center' ? 'flex flex-wrap justify-center gap-4' : 'flex flex-wrap gap-4'}`}>
|
<div className={`mt-8 animate-hero-fade-in-up ${alignment === 'center' ? 'flex flex-wrap justify-center gap-4' : 'flex flex-wrap gap-4'}`} style={{ animationDelay: badge ? '0.55s' : '0.4s', opacity: 0, animationFillMode: 'forwards' }}>
|
||||||
<Button
|
<Button
|
||||||
href={ctaHref}
|
href={ctaHref}
|
||||||
variant="accent"
|
variant="accent"
|
||||||
@@ -115,9 +91,9 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
|
|||||||
→
|
→
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</m.div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</m.div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function HeroVideo(props: HeroVideoProps) {
|
|||||||
|
|
||||||
let defaultPoster = "/assets/photos/DJI_0048.JPG";
|
let defaultPoster = "/assets/photos/DJI_0048.JPG";
|
||||||
if (videoUrl && videoUrl.endsWith('.mp4')) {
|
if (videoUrl && videoUrl.endsWith('.mp4')) {
|
||||||
defaultPoster = videoUrl.replace('.mp4', '-poster.jpg');
|
defaultPoster = videoUrl.replace('.mp4', '-poster.webp');
|
||||||
}
|
}
|
||||||
|
|
||||||
const posterSrc = props.backgroundImage?.url || props.posterImage?.url || data?.posterImage?.url || defaultPoster;
|
const posterSrc = props.backgroundImage?.url || props.posterImage?.url || data?.posterImage?.url || defaultPoster;
|
||||||
@@ -80,18 +80,18 @@ export function HeroVideo(props: HeroVideoProps) {
|
|||||||
{/* Background color while video loads */}
|
{/* Background color while video loads */}
|
||||||
<div className="absolute inset-0 z-0 bg-neutral-dark" />
|
<div className="absolute inset-0 z-0 bg-neutral-dark" />
|
||||||
|
|
||||||
{/* ALWAYS render the Next.js optimized Image as the LCP element */}
|
<img
|
||||||
<Image
|
|
||||||
key={`img-${pathname}-${posterSrc}`}
|
key={`img-${pathname}-${posterSrc}`}
|
||||||
src={posterSrc}
|
src={posterSrc}
|
||||||
|
srcSet={posterSrc.includes('-poster.webp') ? `${posterSrc.replace('-poster.webp', '-poster-mobile.webp')} 800w, ${posterSrc} 1920w` : undefined}
|
||||||
|
sizes={posterSrc.includes('-poster.webp') ? "100vw" : undefined}
|
||||||
alt={posterAlt}
|
alt={posterAlt}
|
||||||
fill
|
className="absolute inset-0 w-full h-full object-cover z-[1] pointer-events-none"
|
||||||
className="object-cover z-[1] pointer-events-none filter contrast-125 saturate-110 brightness-90"
|
decoding="sync"
|
||||||
sizes="100vw"
|
|
||||||
quality={25}
|
|
||||||
priority
|
|
||||||
fetchPriority="high"
|
fetchPriority="high"
|
||||||
/>
|
/>
|
||||||
|
{/* Fast compositing layer to replace the heavy CSS filters on the image */}
|
||||||
|
<div className="absolute inset-0 bg-black/20 z-[1] pointer-events-none mix-blend-multiply" />
|
||||||
|
|
||||||
{/* Render video on top if available, but defer src to avoid blocking LCP */}
|
{/* Render video on top if available, but defer src to avoid blocking LCP */}
|
||||||
{videoUrl && (
|
{videoUrl && (
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useRef, useCallback, useMemo } from 'react';
|
import React, { useState, useRef, useCallback, useMemo } from 'react';
|
||||||
import { m, LazyMotion, domAnimation, AnimatePresence } from 'framer-motion';
|
import { MapPin, ArrowUpRight } from 'lucide-react';
|
||||||
import { MapPin, CheckCircle2, ArrowUpRight } from 'lucide-react';
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
import { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder';
|
import { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder';
|
||||||
import { Location, projectLocations } from '@/lib/map-data';
|
import { Location, projectLocations } from '@/lib/map-data';
|
||||||
import { standorteLocations } from '@/lib/standorte-data';
|
import { standorteLocations } from '@/lib/standorte-data';
|
||||||
import { useLocale, useTranslations } from 'next-intl';
|
import { useLocale, useTranslations } from 'next-intl';
|
||||||
|
|
||||||
|
const InteractiveMapPins = dynamic(() => import('./InteractiveMapPins'), {
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
interface Stat {
|
interface Stat {
|
||||||
value: string;
|
value: string;
|
||||||
suffix?: string;
|
suffix?: string;
|
||||||
@@ -28,58 +32,7 @@ interface InteractiveGermanyMapProps {
|
|||||||
hideStandorte?: boolean;
|
hideStandorte?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MinorNode = React.memo(({ loc, isActive, onEnter, onLeave }: { loc: Location, isActive: boolean, onEnter: (loc: Location) => void, onLeave: () => void }) => (
|
|
||||||
<div
|
|
||||||
className={`absolute group/minor cursor-pointer w-8 h-8 md:w-5 md:h-5 flex items-center justify-center ${isActive ? 'z-[60]' : 'z-10 hover:z-30'}`}
|
|
||||||
style={{
|
|
||||||
left: `${loc.x}%`,
|
|
||||||
top: `${loc.y}%`,
|
|
||||||
transform: 'translate(-50%, -50%)',
|
|
||||||
}}
|
|
||||||
onMouseEnter={() => onEnter(loc)}
|
|
||||||
onMouseLeave={onLeave}
|
|
||||||
onTouchStart={() => onEnter(loc)}
|
|
||||||
onClick={() => onEnter(loc)}
|
|
||||||
>
|
|
||||||
<div className={`w-1.5 h-1.5 rounded-full transition-all duration-300 ${isActive ? 'bg-white scale-150 shadow-[0_0_10px_rgba(130,237,32,1)]' : 'bg-primary/80 group-hover/minor:bg-white group-hover/minor:scale-150'}`} />
|
|
||||||
<div className="absolute inset-0 m-auto w-2 h-2 bg-primary rounded-full opacity-0 group-hover/minor:animate-ping" />
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
MinorNode.displayName = 'MinorNode';
|
|
||||||
|
|
||||||
const MajorNode = React.memo(({ loc, isActive, idx, onEnter, onLeave }: { loc: Location, isActive: boolean, idx: number, onEnter: (loc: Location) => void, onLeave: () => void }) => {
|
|
||||||
const isHQ = loc.type === 'hq';
|
|
||||||
const isBranch = loc.type === 'branch';
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`absolute transform -translate-x-1/2 -translate-y-1/2 group/pin cursor-pointer w-16 h-16 flex items-center justify-center ${isActive ? 'z-[60]' : 'z-20 hover:z-40'}`}
|
|
||||||
style={{ left: `${loc.x}%`, top: `${loc.y}%` }}
|
|
||||||
onMouseEnter={() => onEnter(loc)}
|
|
||||||
onMouseLeave={onLeave}
|
|
||||||
onTouchStart={() => onEnter(loc)}
|
|
||||||
onClick={() => onEnter(loc)}
|
|
||||||
>
|
|
||||||
{(isHQ || isBranch) && (
|
|
||||||
<div className="absolute inset-0 m-auto w-6 h-6 rounded-full animate-ping bg-primary/50 scale-150" />
|
|
||||||
)}
|
|
||||||
<m.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" />}
|
|
||||||
</m.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
MajorNode.displayName = 'MajorNode';
|
|
||||||
|
|
||||||
export function InteractiveGermanyMap({
|
export function InteractiveGermanyMap({
|
||||||
badge,
|
badge,
|
||||||
@@ -135,12 +88,12 @@ export function InteractiveGermanyMap({
|
|||||||
|
|
||||||
const mapBackground = useMemo(() => (
|
const mapBackground = useMemo(() => (
|
||||||
<div className="absolute inset-0 opacity-[0.25] mix-blend-screen drop-shadow-2xl brightness-200 pointer-events-none transform-gpu">
|
<div className="absolute inset-0 opacity-[0.25] mix-blend-screen drop-shadow-2xl brightness-200 pointer-events-none transform-gpu">
|
||||||
<Image
|
<img
|
||||||
src="/germany-map.svg"
|
src="/germany-map.svg"
|
||||||
alt={locale === 'en' ? "Map of Germany" : "Deutschlandkarte"}
|
alt={locale === 'en' ? "Map of Germany" : "Deutschlandkarte"}
|
||||||
fill
|
className="w-full h-full object-cover"
|
||||||
className="object-cover"
|
fetchPriority="high"
|
||||||
priority
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
), [locale]);
|
), [locale]);
|
||||||
@@ -210,116 +163,14 @@ export function InteractiveGermanyMap({
|
|||||||
|
|
||||||
{mapBackground}
|
{mapBackground}
|
||||||
|
|
||||||
{finalLocations.filter(l => l.type === 'minor_node').map((loc, idx) => (
|
<InteractiveMapPins
|
||||||
<MinorNode
|
locations={finalLocations}
|
||||||
key={`minor-${loc.id || idx}`}
|
locale={locale}
|
||||||
loc={loc}
|
t={t}
|
||||||
isActive={activeLocation === loc}
|
activeLocation={activeLocation}
|
||||||
onEnter={handleMouseEnter}
|
handleMouseEnter={handleMouseEnter}
|
||||||
onLeave={handleMouseLeave}
|
handleMouseLeave={handleMouseLeave}
|
||||||
/>
|
/>
|
||||||
))}
|
|
||||||
|
|
||||||
{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}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<AnimatePresence>
|
|
||||||
{activeLocation && (
|
|
||||||
<m.div
|
|
||||||
key="tooltip"
|
|
||||||
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
||||||
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
className="absolute z-[9999] pointer-events-auto bottom-[-60px] left-1/2 -translate-x-1/2 w-[calc(100vw_-_2rem)] max-w-[320px] md:w-max md:bottom-auto md:left-[var(--md-left)] md:top-[var(--md-top)] md:[transform:var(--md-transform)]"
|
|
||||||
style={{
|
|
||||||
'--md-left': `${activeLocation.x}%`,
|
|
||||||
'--md-top': `${activeLocation.y}%`,
|
|
||||||
'--md-transform': activeLocation.x < 50 ? 'translate(20px, -50%)' : 'translate(calc(-100% - 20px), -50%)',
|
|
||||||
} as React.CSSProperties}
|
|
||||||
onMouseEnter={() => handleMouseEnter(activeLocation)}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
>
|
|
||||||
<div className="bg-[#050B14]/95 backdrop-blur-xl border border-white/10 rounded-2xl shadow-2xl w-full text-white overflow-hidden">
|
|
||||||
{activeLocation.featuredImage && (
|
|
||||||
<div className="relative w-full h-32 bg-neutral-900">
|
|
||||||
<Image
|
|
||||||
src={activeLocation.featuredImage}
|
|
||||||
alt={activeLocation.name}
|
|
||||||
fill
|
|
||||||
className="object-cover"
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-[#050B14] to-transparent opacity-90" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="p-4">
|
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
<MapPin className="w-3.5 h-3.5 text-primary" />
|
|
||||||
<span className="text-xs font-bold text-primary uppercase tracking-wider">
|
|
||||||
{t(`types.${activeLocation.type === 'minor_node' ? 'project' : activeLocation.type}`)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="font-heading font-bold text-lg leading-tight mb-2">
|
|
||||||
{activeLocation.name}
|
|
||||||
</div>
|
|
||||||
{activeLocation.type !== 'minor_node' ? (
|
|
||||||
<div className="text-white/60 text-sm line-clamp-3 mb-3">
|
|
||||||
{activeLocation.description}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-white/60 text-xs mb-3">
|
|
||||||
{t(`descriptions.${['wind','pv','fiber','power','battery'].includes(activeLocation.description || '') ? activeLocation.description : 'default'}`)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(activeLocation.details || activeLocation.type === 'minor_node') && (
|
|
||||||
<ul className="mb-3 space-y-1.5 border-t border-white/10 pt-3">
|
|
||||||
{activeLocation.type === 'minor_node' ? (
|
|
||||||
<>
|
|
||||||
<li className="text-[11px] text-white/80 flex items-start gap-2 font-medium">
|
|
||||||
<CheckCircle2 className="w-3 h-3 text-primary shrink-0 mt-0.5" />
|
|
||||||
<span>{t(`features.${['wind','pv','fiber','power','battery'].includes(activeLocation.description || '') ? activeLocation.description : 'default'}`)}</span>
|
|
||||||
</li>
|
|
||||||
<li className="text-[11px] text-white/80 flex items-start gap-2 font-medium">
|
|
||||||
<CheckCircle2 className="w-3 h-3 text-primary shrink-0 mt-0.5" />
|
|
||||||
<span>{t('features.method')}</span>
|
|
||||||
</li>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
activeLocation.details?.map((detail, i) => (
|
|
||||||
<li key={i} className="text-[11px] text-white/80 flex items-start gap-2 font-medium">
|
|
||||||
<CheckCircle2 className="w-3 h-3 text-primary shrink-0 mt-0.5" />
|
|
||||||
<span>{detail}</span>
|
|
||||||
</li>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeLocation.href && (activeLocation.type === 'hq' || activeLocation.type === 'branch') && (
|
|
||||||
<Link
|
|
||||||
href={activeLocation.href.startsWith('/') ? `/${locale}${activeLocation.href}` : activeLocation.href}
|
|
||||||
className="inline-flex items-center text-xs font-bold text-primary hover:text-white transition-colors mt-2"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
{t('learnMore')}
|
|
||||||
<ArrowUpRight className="w-3.5 h-3.5 ml-1" />
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</m.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
195
components/blocks/InteractiveMapPins.tsx
Normal file
195
components/blocks/InteractiveMapPins.tsx
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { m, AnimatePresence, LazyMotion } from 'framer-motion';
|
||||||
|
import { MapPin, CheckCircle2, ArrowUpRight } from 'lucide-react';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Location } from '@/lib/map-data';
|
||||||
|
|
||||||
|
const loadFeatures = () => import('@/lib/framer-features').then(res => res.default);
|
||||||
|
|
||||||
|
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" />
|
||||||
|
)}
|
||||||
|
<m.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" />}
|
||||||
|
</m.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
MajorNode.displayName = 'MajorNode';
|
||||||
|
|
||||||
|
export default function InteractiveMapPins({
|
||||||
|
locations,
|
||||||
|
locale,
|
||||||
|
t,
|
||||||
|
activeLocation,
|
||||||
|
handleMouseEnter,
|
||||||
|
handleMouseLeave
|
||||||
|
}: {
|
||||||
|
locations: Location[],
|
||||||
|
locale: string,
|
||||||
|
t: any,
|
||||||
|
activeLocation: Location | null,
|
||||||
|
handleMouseEnter: (loc: Location) => void,
|
||||||
|
handleMouseLeave: () => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<LazyMotion features={loadFeatures}>
|
||||||
|
{locations.filter(l => l.type === 'minor_node').map((loc, idx) => (
|
||||||
|
<MinorNode
|
||||||
|
key={`minor-${loc.id || idx}`}
|
||||||
|
loc={loc}
|
||||||
|
isActive={activeLocation === loc}
|
||||||
|
onEnter={handleMouseEnter}
|
||||||
|
onLeave={handleMouseLeave}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{locations.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}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{activeLocation && (
|
||||||
|
<m.div
|
||||||
|
key="tooltip"
|
||||||
|
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="absolute z-[9999] pointer-events-auto bottom-[-60px] left-1/2 -translate-x-1/2 w-[calc(100vw_-_2rem)] max-w-[320px] md:w-max md:bottom-auto md:left-[var(--md-left)] md:top-[var(--md-top)] md:[transform:var(--md-transform)]"
|
||||||
|
style={{
|
||||||
|
'--md-left': `${activeLocation.x}%`,
|
||||||
|
'--md-top': `${activeLocation.y}%`,
|
||||||
|
'--md-transform': activeLocation.x < 50 ? 'translate(20px, -50%)' : 'translate(calc(-100% - 20px), -50%)',
|
||||||
|
} as React.CSSProperties}
|
||||||
|
onMouseEnter={() => handleMouseEnter(activeLocation)}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
>
|
||||||
|
<div className="bg-[#050B14]/95 backdrop-blur-xl border border-white/10 rounded-2xl shadow-2xl w-full text-white overflow-hidden">
|
||||||
|
{activeLocation.featuredImage && (
|
||||||
|
<div className="relative w-full h-32 bg-neutral-900">
|
||||||
|
<Image
|
||||||
|
src={activeLocation.featuredImage}
|
||||||
|
alt={activeLocation.name}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
sizes="(max-width: 768px) 100vw, 320px"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-[#050B14] to-transparent opacity-90" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<MapPin className="w-3.5 h-3.5 text-primary" />
|
||||||
|
<span className="text-xs font-bold text-primary uppercase tracking-wider">
|
||||||
|
{t(`types.${activeLocation.type === 'minor_node' ? 'project' : activeLocation.type}`)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="font-heading font-bold text-lg leading-tight mb-2">
|
||||||
|
{activeLocation.name}
|
||||||
|
</div>
|
||||||
|
{activeLocation.type !== 'minor_node' ? (
|
||||||
|
<div className="text-white/60 text-sm line-clamp-3 mb-3">
|
||||||
|
{activeLocation.description}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-white/60 text-xs mb-3">
|
||||||
|
{t(`descriptions.${['wind','pv','fiber','power','battery'].includes(activeLocation.description || '') ? activeLocation.description : 'default'}`)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(activeLocation.details || activeLocation.type === 'minor_node') && (
|
||||||
|
<ul className="mb-3 space-y-1.5 border-t border-white/10 pt-3">
|
||||||
|
{activeLocation.type === 'minor_node' ? (
|
||||||
|
<>
|
||||||
|
<li className="text-[11px] text-white/80 flex items-start gap-2 font-medium">
|
||||||
|
<CheckCircle2 className="w-3 h-3 text-primary shrink-0 mt-0.5" />
|
||||||
|
<span>{t(`features.${['wind','pv','fiber','power','battery'].includes(activeLocation.description || '') ? activeLocation.description : 'default'}`)}</span>
|
||||||
|
</li>
|
||||||
|
<li className="text-[11px] text-white/80 flex items-start gap-2 font-medium">
|
||||||
|
<CheckCircle2 className="w-3 h-3 text-primary shrink-0 mt-0.5" />
|
||||||
|
<span>{t('features.method')}</span>
|
||||||
|
</li>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
activeLocation.details?.map((detail, i) => (
|
||||||
|
<li key={i} className="text-[11px] text-white/80 flex items-start gap-2 font-medium">
|
||||||
|
<CheckCircle2 className="w-3 h-3 text-primary shrink-0 mt-0.5" />
|
||||||
|
<span>{detail}</span>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeLocation.href && (activeLocation.type === 'hq' || activeLocation.type === 'branch') && (
|
||||||
|
<Link
|
||||||
|
href={activeLocation.href.startsWith('/') ? `/${locale}${activeLocation.href}` : activeLocation.href}
|
||||||
|
className="inline-flex items-center text-xs font-bold text-primary hover:text-white transition-colors mt-2"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{t('learnMore')}
|
||||||
|
<ArrowUpRight className="w-3.5 h-3.5 ml-1" />
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</m.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</LazyMotion>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import React from 'react';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
||||||
import { m, LazyMotion, domAnimation } from 'framer-motion';
|
import { m } from 'framer-motion';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
export interface JobListingBlockProps {
|
export interface JobListingBlockProps {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { m, LazyMotion, domAnimation } from 'framer-motion';
|
import { m } from 'framer-motion';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
||||||
@@ -47,19 +47,6 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
|
|||||||
const isDraggingRef = React.useRef(false);
|
const isDraggingRef = React.useRef(false);
|
||||||
const [isDragging, setIsDragging] = React.useState(false);
|
const [isDragging, setIsDragging] = React.useState(false);
|
||||||
|
|
||||||
// Center the first card on mount so it appears in the middle with peek on both sides
|
|
||||||
React.useEffect(() => {
|
|
||||||
const el = containerRef.current;
|
|
||||||
if (!el) return;
|
|
||||||
// Wait for layout to settle
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const firstCard = el.querySelector('[data-card]') as HTMLElement | null;
|
|
||||||
if (!firstCard) return;
|
|
||||||
const cardCenter = firstCard.offsetLeft + firstCard.offsetWidth / 2;
|
|
||||||
const containerCenter = el.offsetWidth / 2;
|
|
||||||
el.scrollLeft = cardCenter - containerCenter;
|
|
||||||
});
|
|
||||||
}, [references.length]);
|
|
||||||
|
|
||||||
const badge = props.badge || data?.badge || t('badge');
|
const badge = props.badge || data?.badge || t('badge');
|
||||||
const title = props.title || data?.title || t('title');
|
const title = props.title || data?.title || t('title');
|
||||||
@@ -127,7 +114,7 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
|
|||||||
<div className="absolute right-0 top-0 bottom-0 w-16 bg-gradient-to-l from-neutral-dark to-transparent z-20 pointer-events-none" />
|
<div className="absolute right-0 top-0 bottom-0 w-16 bg-gradient-to-l from-neutral-dark to-transparent z-20 pointer-events-none" />
|
||||||
|
|
||||||
{/* Carousel: centered first card with peek on both sides */}
|
{/* Carousel: centered first card with peek on both sides */}
|
||||||
<LazyMotion features={domAnimation}>
|
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
onMouseDown={onMouseDown}
|
onMouseDown={onMouseDown}
|
||||||
@@ -189,7 +176,7 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
|
|||||||
})}
|
})}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</LazyMotion>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="container relative z-10 mt-4 flex justify-center">
|
<div className="container relative z-10 mt-4 flex justify-center">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { useScroll, useTransform, m, LazyMotion, domAnimation } from 'framer-motion';
|
import { useScroll, useTransform, m } from 'framer-motion';
|
||||||
import { useLocale } from 'next-intl';
|
import { useLocale } from 'next-intl';
|
||||||
|
|
||||||
export function ScaleOfImpact() {
|
export function ScaleOfImpact() {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { m, LazyMotion, domAnimation, Variants } from 'framer-motion';
|
import { m, Variants } from 'framer-motion';
|
||||||
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
||||||
|
|
||||||
export interface ServicePanelData {
|
export interface ServicePanelData {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { m, LazyMotion, domAnimation, Variants } from 'framer-motion';
|
import { m, Variants } from 'framer-motion';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
||||||
@@ -128,7 +128,7 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
|
|||||||
? 'opacity-30 grayscale-0 scale-105'
|
? 'opacity-30 grayscale-0 scale-105'
|
||||||
: 'opacity-40 grayscale-[50%] group-hover:opacity-30 group-hover:grayscale-0 group-hover:scale-110'
|
: 'opacity-40 grayscale-[50%] group-hover:opacity-30 group-hover:grayscale-0 group-hover:scale-110'
|
||||||
}`}
|
}`}
|
||||||
sizes="(max-width: 768px) 100vw, 33vw"
|
sizes="(max-width: 768px) 200px, 384px"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import TrackedLink from '@/components/analytics/TrackedLink';
|
import TrackedLink from '@/components/analytics/TrackedLink';
|
||||||
import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
|
import { getButtonClasses, ButtonOverlay } from '@/components/ui/Button';
|
||||||
import { m, LazyMotion, domAnimation } from 'framer-motion';
|
import { m } from 'framer-motion';
|
||||||
|
|
||||||
export interface SupportCTAProps {
|
export interface SupportCTAProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { m, LazyMotion, domAnimation } from 'framer-motion';
|
import { m } from 'framer-motion';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useRef, useEffect } from 'react';
|
import React, { useRef, useEffect } from 'react';
|
||||||
import { useScroll, useTransform, useSpring, m, LazyMotion, domAnimation } from 'framer-motion';
|
import { useScroll, useTransform, useSpring, m } from 'framer-motion';
|
||||||
import { LogoArcs } from '@/components/ui/LogoArcs';
|
import { LogoArcs } from '@/components/ui/LogoArcs';
|
||||||
|
|
||||||
function MagneticRing({
|
function MagneticRing({
|
||||||
@@ -25,34 +25,45 @@ function MagneticRing({
|
|||||||
// Only run on client with mouse
|
// Only run on client with mouse
|
||||||
if (window.matchMedia("(pointer: coarse)").matches) return;
|
if (window.matchMedia("(pointer: coarse)").matches) return;
|
||||||
|
|
||||||
|
let ticking = false;
|
||||||
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
if (!ref.current) return;
|
if (!ref.current) return;
|
||||||
const rect = ref.current.getBoundingClientRect();
|
|
||||||
const centerX = rect.left + rect.width / 2;
|
if (!ticking) {
|
||||||
const centerY = rect.top + rect.height / 2;
|
window.requestAnimationFrame(() => {
|
||||||
|
if (!ref.current) {
|
||||||
|
ticking = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = ref.current.getBoundingClientRect();
|
||||||
|
const centerX = rect.left + rect.width / 2;
|
||||||
|
const centerY = rect.top + rect.height / 2;
|
||||||
|
|
||||||
const distX = e.clientX - centerX;
|
const distX = e.clientX - centerX;
|
||||||
const distY = e.clientY - centerY;
|
const distY = e.clientY - centerY;
|
||||||
const distance = Math.sqrt(distX * Math.pow(distX, 2) + Math.pow(distY, 2)); // Fix: correct pythagoras
|
const actualDistance = Math.sqrt(distX * distX + distY * distY);
|
||||||
|
|
||||||
// Calculate actual distance correctly
|
if (actualDistance < pullRadius) {
|
||||||
const actualDistance = Math.sqrt(distX * distX + distY * distY);
|
// Exponential falloff for natural magnetic feel
|
||||||
|
const pullFactor = Math.pow(1 - (actualDistance / pullRadius), 2);
|
||||||
if (actualDistance < pullRadius) {
|
|
||||||
// Exponential falloff for natural magnetic feel
|
// Target offset (fraction of the distance based on strength)
|
||||||
const pullFactor = Math.pow(1 - (actualDistance / pullRadius), 2);
|
x.set(distX * pullFactor * (pullStrength / 100));
|
||||||
|
y.set(distY * pullFactor * (pullStrength / 100));
|
||||||
// Target offset (fraction of the distance based on strength)
|
} else {
|
||||||
x.set(distX * pullFactor * (pullStrength / 100));
|
// Return to origin smoothly
|
||||||
y.set(distY * pullFactor * (pullStrength / 100));
|
x.set(0);
|
||||||
} else {
|
y.set(0);
|
||||||
// Return to origin smoothly
|
}
|
||||||
x.set(0);
|
ticking = false;
|
||||||
y.set(0);
|
});
|
||||||
|
ticking = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('mousemove', handleMouseMove);
|
window.addEventListener('mousemove', handleMouseMove, { passive: true });
|
||||||
return () => window.removeEventListener('mousemove', handleMouseMove);
|
return () => window.removeEventListener('mousemove', handleMouseMove);
|
||||||
}, [x, y, pullRadius, pullStrength]);
|
}, [x, y, pullRadius, pullStrength]);
|
||||||
|
|
||||||
|
|||||||
7
components/decorations/DynamicCorporateBackground.tsx
Normal file
7
components/decorations/DynamicCorporateBackground.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
'use client';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
export const DynamicCorporateBackground = dynamic(
|
||||||
|
() => import('./CorporateBackground').then(mod => mod.CorporateBackground),
|
||||||
|
{ ssr: false }
|
||||||
|
);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { m, LazyMotion, domAnimation } from 'framer-motion';
|
import { m } from 'framer-motion';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
export function EUFundingBadge() {
|
export function EUFundingBadge() {
|
||||||
@@ -23,7 +23,7 @@ export function EUFundingBadge() {
|
|||||||
alt="European Union Background"
|
alt="European Union Background"
|
||||||
fill
|
fill
|
||||||
className="object-cover object-top"
|
className="object-cover object-top"
|
||||||
priority
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { m, LazyMotion, domAnimation } from 'framer-motion';
|
import { m } from 'framer-motion';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { sendContactFormAction } from '@/app/actions/contact';
|
import { sendContactFormAction } from '@/app/actions/contact';
|
||||||
|
|
||||||
|
|||||||
7
components/layout/DynamicMobileBottomNav.tsx
Normal file
7
components/layout/DynamicMobileBottomNav.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
'use client';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
export const DynamicMobileBottomNav = dynamic(
|
||||||
|
() => import('./MobileBottomNav').then(mod => mod.MobileBottomNav),
|
||||||
|
{ ssr: false }
|
||||||
|
);
|
||||||
@@ -111,6 +111,7 @@ export function Header({ navLinks }: HeaderProps) {
|
|||||||
priority
|
priority
|
||||||
fetchPriority="high"
|
fetchPriority="high"
|
||||||
sizes="192px"
|
sizes="192px"
|
||||||
|
unoptimized
|
||||||
/>
|
/>
|
||||||
{/* Masked sweep overlay on hover */}
|
{/* Masked sweep overlay on hover */}
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as React from 'react';
|
|||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { TransitionLink } from '@/components/ui/TransitionLink';
|
import { TransitionLink } from '@/components/ui/TransitionLink';
|
||||||
import { Home, Layers, Star, Mail, Menu } from 'lucide-react';
|
import { Home, Layers, Star, Mail, Menu } from 'lucide-react';
|
||||||
import { m, LazyMotion, domAnimation, AnimatePresence } from 'framer-motion';
|
import { m, AnimatePresence } from 'framer-motion';
|
||||||
|
|
||||||
export interface NavLink {
|
export interface NavLink {
|
||||||
label: string;
|
label: string;
|
||||||
|
|||||||
7
components/providers/DynamicInitialLoader.tsx
Normal file
7
components/providers/DynamicInitialLoader.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
'use client';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
export const DynamicInitialLoader = dynamic(
|
||||||
|
() => import('./InitialLoader').then(mod => mod.InitialLoader),
|
||||||
|
{ ssr: false }
|
||||||
|
);
|
||||||
7
components/providers/DynamicPageTransitionShutter.tsx
Normal file
7
components/providers/DynamicPageTransitionShutter.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
'use client';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
export const DynamicPageTransitionShutter = dynamic(
|
||||||
|
() => import('./PageTransitionShutter').then(mod => mod.PageTransitionShutter),
|
||||||
|
{ ssr: false }
|
||||||
|
);
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { m, LazyMotion, domAnimation, AnimatePresence } from 'framer-motion';
|
import { m, AnimatePresence } from 'framer-motion';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
|
||||||
@@ -72,8 +72,8 @@ export function InitialLoader({ shouldShowLoader = true }: { shouldShowLoader?:
|
|||||||
{/* Sweeping intense white light */}
|
{/* Sweeping intense white light */}
|
||||||
<m.div
|
<m.div
|
||||||
className="absolute inset-0 bg-gradient-to-r from-transparent via-white to-transparent skew-x-[-25deg]"
|
className="absolute inset-0 bg-gradient-to-r from-transparent via-white to-transparent skew-x-[-25deg]"
|
||||||
initial={{ left: '-150%' }}
|
initial={{ x: '-150%' }}
|
||||||
animate={{ left: '250%' }}
|
animate={{ x: '250%' }}
|
||||||
transition={{ duration: 2.5, ease: "easeInOut", repeat: Infinity, repeatDelay: 0.5 }}
|
transition={{ duration: 2.5, ease: "easeInOut", repeat: Infinity, repeatDelay: 0.5 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -91,9 +91,9 @@ export function InitialLoader({ shouldShowLoader = true }: { shouldShowLoader?:
|
|||||||
className="h-[2px] w-full bg-white/5 overflow-hidden relative rounded-full"
|
className="h-[2px] w-full bg-white/5 overflow-hidden relative rounded-full"
|
||||||
>
|
>
|
||||||
<m.div
|
<m.div
|
||||||
className="absolute inset-y-0 left-0 bg-gradient-to-r from-primary-dark via-primary to-primary-light"
|
className="absolute inset-y-0 left-0 w-full bg-gradient-to-r from-primary-dark via-primary to-primary-light origin-left"
|
||||||
initial={{ width: "0%" }}
|
initial={{ scaleX: 0 }}
|
||||||
animate={{ width: "100%" }}
|
animate={{ scaleX: 1 }}
|
||||||
transition={{ duration: 3.8, ease: [0.16, 1, 0.3, 1] }}
|
transition={{ duration: 3.8, ease: [0.16, 1, 0.3, 1] }}
|
||||||
/>
|
/>
|
||||||
</m.div>
|
</m.div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { m, LazyMotion, domAnimation, AnimatePresence } from 'framer-motion';
|
import { m, AnimatePresence } from 'framer-motion';
|
||||||
import { useTransition } from './TransitionProvider';
|
import { useTransition } from './TransitionProvider';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
@@ -12,12 +12,12 @@ export function PageTransitionShutter() {
|
|||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{isTransitioning && (
|
{isTransitioning && (
|
||||||
<m.div
|
<m.div
|
||||||
key="shutter"
|
key="page-transition-shutter"
|
||||||
initial={{ y: '-100%' }}
|
initial={{ y: '-100%' }}
|
||||||
animate={{ y: '0%' }}
|
animate={{ y: 0 }}
|
||||||
exit={{ y: '100%' }}
|
exit={{ y: '100%' }}
|
||||||
transition={{ duration: 0.9, ease: [0.16, 1, 0.3, 1] }}
|
transition={{ duration: 0.65, ease: [0.76, 0, 0.24, 1] }}
|
||||||
className="fixed left-0 w-full z-[9998] bg-[#050B14] pointer-events-none flex flex-col items-center justify-center overflow-visible"
|
className="fixed left-0 right-0 z-[9998] bg-[#050B14] flex flex-col items-center justify-center overflow-hidden"
|
||||||
style={{
|
style={{
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
top: '0',
|
top: '0',
|
||||||
@@ -56,8 +56,8 @@ export function PageTransitionShutter() {
|
|||||||
{/* Sweeping intense white light */}
|
{/* Sweeping intense white light */}
|
||||||
<m.div
|
<m.div
|
||||||
className="absolute inset-0 bg-gradient-to-r from-transparent via-white to-transparent skew-x-[-25deg]"
|
className="absolute inset-0 bg-gradient-to-r from-transparent via-white to-transparent skew-x-[-25deg]"
|
||||||
initial={{ left: '-150%' }}
|
initial={{ x: '-150%' }}
|
||||||
animate={{ left: '250%' }}
|
animate={{ x: '250%' }}
|
||||||
transition={{ duration: 1.5, ease: "easeInOut", repeat: Infinity, repeatDelay: 0.2 }}
|
transition={{ duration: 1.5, ease: "easeInOut", repeat: Infinity, repeatDelay: 0.2 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,16 @@
|
|||||||
|
|
||||||
import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react';
|
import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react';
|
||||||
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
|
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
|
||||||
import { LazyMotion, domAnimation } from 'framer-motion';
|
import { LazyMotion } from 'framer-motion';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
const loadFeatures = () => import('@/lib/framer-features').then(res => res.default);
|
||||||
|
|
||||||
|
// Defer loading the shutter to eliminate AnimatePresence from the initial bundle
|
||||||
|
const DynamicPageTransitionShutter = dynamic(
|
||||||
|
() => import('./PageTransitionShutter').then(mod => mod.PageTransitionShutter),
|
||||||
|
{ ssr: false }
|
||||||
|
);
|
||||||
|
|
||||||
interface TransitionContextType {
|
interface TransitionContextType {
|
||||||
isTransitioning: boolean;
|
isTransitioning: boolean;
|
||||||
@@ -15,8 +24,12 @@ const TransitionContext = createContext<TransitionContextType | undefined>(undef
|
|||||||
export function TransitionProvider({ children }: { children: ReactNode }) {
|
export function TransitionProvider({ children }: { children: ReactNode }) {
|
||||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||||
const [transitionMessage, setTransitionMessage] = useState<string | null>(null);
|
const [transitionMessage, setTransitionMessage] = useState<string | null>(null);
|
||||||
|
const [hasMountedShutter, setHasMountedShutter] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
// We no longer eagerly load the shutter in the background.
|
||||||
|
// It will be loaded precisely when the user initiates a transition.
|
||||||
|
|
||||||
// We use a ref to track transition state without triggering the route change effect when it turns true
|
// We use a ref to track transition state without triggering the route change effect when it turns true
|
||||||
const isTransitioningRef = React.useRef(isTransitioning);
|
const isTransitioningRef = React.useRef(isTransitioning);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -42,6 +55,7 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
setTransitionMessage(message);
|
setTransitionMessage(message);
|
||||||
setIsTransitioning(true);
|
setIsTransitioning(true);
|
||||||
|
setHasMountedShutter(true);
|
||||||
|
|
||||||
// Die Animation des Shutters abwarten (ca. 700ms), dann pushen
|
// Die Animation des Shutters abwarten (ca. 700ms), dann pushen
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -69,7 +83,8 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
|
|||||||
setTransitionMessage={setTransitionMessage}
|
setTransitionMessage={setTransitionMessage}
|
||||||
/>
|
/>
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
<LazyMotion features={domAnimation}>
|
<LazyMotion features={loadFeatures}>
|
||||||
|
{hasMountedShutter && <DynamicPageTransitionShutter />}
|
||||||
{children}
|
{children}
|
||||||
</LazyMotion>
|
</LazyMotion>
|
||||||
</TransitionContext.Provider>
|
</TransitionContext.Provider>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useMotionValue, useTransform, animate, useInView, m, LazyMotion, domAnimation } from 'framer-motion';
|
import { useMotionValue, useTransform, animate, useInView, m } from 'framer-motion';
|
||||||
|
|
||||||
interface AnimatedCounterProps {
|
interface AnimatedCounterProps {
|
||||||
value: number;
|
value: number;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useInView, m, LazyMotion, domAnimation } from 'framer-motion';
|
import { useInView, m } from 'framer-motion';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
|
|||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
title: "Kabelleitungsnetzbau",
|
title: "Kabelleitungsnetzbau",
|
||||||
description: "Kabelleitungsnetzbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen bis 110 kV",
|
description: "Kabelleitungsnetzbau (Nieder-/Mittel- und Hochspannung bis 110 kV)",
|
||||||
tag: "Energie",
|
tag: "Energie",
|
||||||
size: "large",
|
size: "large",
|
||||||
href: "/de/kabeltiefbau",
|
href: "/de/kabeltiefbau",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ layout: "fullBleed"
|
|||||||
<HeroSection
|
<HeroSection
|
||||||
badge="Kernkompetenz"
|
badge="Kernkompetenz"
|
||||||
title="Kabelleitungsnetzbau"
|
title="Kabelleitungsnetzbau"
|
||||||
subtitle="Komplette Infrastruktur-Lösungen für Hoch-, Mittel- und Niederspannungsnetze sowie Kabelmontagen bis 110 kV"
|
subtitle="Komplette Infrastruktur-Lösungen für Nieder-, Mittel- und Hochspannungsnetze bis 110 kV"
|
||||||
backgroundImage={{ url: '/assets/photos/DSC01123.JPG' }}
|
backgroundImage={{ url: '/assets/photos/DSC01123.JPG' }}
|
||||||
alignment="center"
|
alignment="center"
|
||||||
ctaLabel="Projekt anfragen"
|
ctaLabel="Projekt anfragen"
|
||||||
|
|||||||
1
dummy-polyfill.js
Normal file
1
dummy-polyfill.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export default {};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// Sentry initialization move to GlitchtipErrorReportingService to allow lazy-loading
|
// Sentry initialization move to GlitchtipErrorReportingService to allow lazy-loading
|
||||||
// for PageSpeed 100 optimizations. This file is now empty to prevent the SDK
|
// for PageSpeed 100 optimizations. This file is now empty to prevent the SDK
|
||||||
// from being included in the initial JS bundle.
|
// from being included in the initial JS bundle.
|
||||||
import * as Sentry from '@sentry/nextjs';
|
|
||||||
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
|
export {};
|
||||||
|
|||||||
11921
lh-desktop.json
Normal file
11921
lh-desktop.json
Normal file
File diff suppressed because one or more lines are too long
11867
lh-report.json
Normal file
11867
lh-report.json
Normal file
File diff suppressed because one or more lines are too long
11818
lh-standorte.json
Normal file
11818
lh-standorte.json
Normal file
File diff suppressed because one or more lines are too long
2
lib/empty-polyfill.js
Normal file
2
lib/empty-polyfill.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Empty polyfill to trick Next.js into not loading legacy polyfills
|
||||||
|
export default {};
|
||||||
@@ -5,7 +5,9 @@ import { join } from 'path';
|
|||||||
* Loads the Inter fonts for use in Satori (Next.js OG Image generation).
|
* Loads the Inter fonts for use in Satori (Next.js OG Image generation).
|
||||||
* Since we are using runtime = 'nodejs', we can read them from the filesystem.
|
* Since we are using runtime = 'nodejs', we can read them from the filesystem.
|
||||||
*/
|
*/
|
||||||
|
let cachedFonts: any[] | null = null;
|
||||||
export async function getOgFonts() {
|
export async function getOgFonts() {
|
||||||
|
if (cachedFonts) return cachedFonts;
|
||||||
const boldFontPath = join(process.cwd(), 'public/fonts/Inter-Bold.woff');
|
const boldFontPath = join(process.cwd(), 'public/fonts/Inter-Bold.woff');
|
||||||
const regularFontPath = join(process.cwd(), 'public/fonts/Inter-Regular.woff');
|
const regularFontPath = join(process.cwd(), 'public/fonts/Inter-Regular.woff');
|
||||||
|
|
||||||
@@ -28,7 +30,7 @@ export async function getOgFonts() {
|
|||||||
`[OG] Fonts loaded successfully (${boldFont.byteLength} and ${regularFont.byteLength} bytes)`,
|
`[OG] Fonts loaded successfully (${boldFont.byteLength} and ${regularFont.byteLength} bytes)`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return [
|
cachedFonts = [
|
||||||
{
|
{
|
||||||
name: 'Inter',
|
name: 'Inter',
|
||||||
data: boldFont,
|
data: boldFont,
|
||||||
@@ -42,28 +44,35 @@ export async function getOgFonts() {
|
|||||||
style: 'normal' as const,
|
style: 'normal' as const,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
return cachedFonts;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[OG] Failed to load fonts from ${process.cwd()}:`, error);
|
console.error(`[OG] Failed to load fonts from ${process.cwd()}:`, error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cachedBackground: string | null = null;
|
||||||
export function getOgBackground() {
|
export function getOgBackground() {
|
||||||
|
if (cachedBackground) return cachedBackground;
|
||||||
try {
|
try {
|
||||||
const bgPath = join(process.cwd(), 'public/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg');
|
const bgPath = join(process.cwd(), 'public/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg');
|
||||||
const bgBase64 = readFileSync(bgPath, 'base64');
|
const bgBase64 = readFileSync(bgPath, 'base64');
|
||||||
return `data:image/jpeg;base64,${bgBase64}`;
|
cachedBackground = `data:image/jpeg;base64,${bgBase64}`;
|
||||||
|
return cachedBackground;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[OG] Failed to load background', err);
|
console.error('[OG] Failed to load background', err);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cachedLogo: string | null = null;
|
||||||
export function getOgLogo() {
|
export function getOgLogo() {
|
||||||
|
if (cachedLogo) return cachedLogo;
|
||||||
try {
|
try {
|
||||||
const logoPath = join(process.cwd(), 'public/assets/logo-white.png');
|
const logoPath = join(process.cwd(), 'public/assets/logo-white.png');
|
||||||
const logoBase64 = readFileSync(logoPath, 'base64');
|
const logoBase64 = readFileSync(logoPath, 'base64');
|
||||||
return `data:image/png;base64,${logoBase64}`;
|
cachedLogo = `data:image/png;base64,${logoBase64}`;
|
||||||
|
return cachedLogo;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[OG] Failed to load logo', err);
|
console.error('[OG] Failed to load logo', err);
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { AppServices } from './app-services';
|
|||||||
import { NoopAnalyticsService } from './analytics/noop-analytics-service';
|
import { NoopAnalyticsService } from './analytics/noop-analytics-service';
|
||||||
import { UmamiAnalyticsService } from './analytics/umami-analytics-service';
|
import { UmamiAnalyticsService } from './analytics/umami-analytics-service';
|
||||||
import { MemoryCacheService } from './cache/memory-cache-service';
|
import { MemoryCacheService } from './cache/memory-cache-service';
|
||||||
import { GlitchtipErrorReportingService } from './errors/glitchtip-error-reporting-service';
|
|
||||||
import { NoopErrorReportingService } from './errors/noop-error-reporting-service';
|
import { NoopErrorReportingService } from './errors/noop-error-reporting-service';
|
||||||
import { NoopLoggerService } from './logging/noop-logger-service';
|
import { NoopLoggerService } from './logging/noop-logger-service';
|
||||||
import { PinoLoggerService } from './logging/pino-logger-service';
|
import { PinoLoggerService } from './logging/pino-logger-service';
|
||||||
@@ -68,17 +67,7 @@ export function getAppServices(): AppServices {
|
|||||||
logger.info('Notification service initialized (noop)');
|
logger.info('Notification service initialized (noop)');
|
||||||
|
|
||||||
// Create error reporting service (GlitchTip/Sentry or no-op)
|
// Create error reporting service (GlitchTip/Sentry or no-op)
|
||||||
const errors = sentryEnabled
|
const errors = new NoopErrorReportingService();
|
||||||
? new GlitchtipErrorReportingService(
|
|
||||||
{
|
|
||||||
enabled: true,
|
|
||||||
dsn: config.errors.glitchtip.dsn,
|
|
||||||
tracesSampleRate: 0.1, // Default to 10% sampling
|
|
||||||
},
|
|
||||||
logger,
|
|
||||||
notifications,
|
|
||||||
)
|
|
||||||
: new NoopErrorReportingService();
|
|
||||||
|
|
||||||
if (sentryEnabled) {
|
if (sentryEnabled) {
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export class GlitchtipErrorReportingService implements ErrorReportingService {
|
|||||||
const contextStr = context ? `\nContext: ${JSON.stringify(context, null, 2)}` : '';
|
const contextStr = context ? `\nContext: ${JSON.stringify(context, null, 2)}` : '';
|
||||||
|
|
||||||
await this.notifications.notify({
|
await this.notifications.notify({
|
||||||
title: '🔥 Critical Error Captured',
|
title: '🔥 [E-TIB] Critical Error Captured',
|
||||||
message: `Error: ${errorMessage}${contextStr}`,
|
message: `Error: ${errorMessage}${contextStr}`,
|
||||||
priority: 7,
|
priority: 7,
|
||||||
});
|
});
|
||||||
@@ -92,7 +92,7 @@ export class GlitchtipErrorReportingService implements ErrorReportingService {
|
|||||||
(level === 'error' || level === 'fatal' || message.includes('Route Not Found'))
|
(level === 'error' || level === 'fatal' || message.includes('Route Not Found'))
|
||||||
) {
|
) {
|
||||||
await this.notifications.notify({
|
await this.notifications.notify({
|
||||||
title: level === 'warning' ? '⚠️ Warning Captured' : '🔥 Critical Message Captured',
|
title: level === 'warning' ? '⚠️ [E-TIB] Warning Captured' : '🔥 [E-TIB] Critical Message Captured',
|
||||||
message: message,
|
message: message,
|
||||||
priority: level === 'warning' ? 5 : 7,
|
priority: level === 'warning' ? 5 : 7,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import withMintelConfig from '@mintel/next-config';
|
import { baseNextConfig } from '@mintel/next-config';
|
||||||
import withBundleAnalyzer from '@next/bundle-analyzer';
|
import withBundleAnalyzer from '@next/bundle-analyzer';
|
||||||
import { withSentryConfig } from '@sentry/nextjs';
|
import { withSentryConfig } from '@sentry/nextjs';
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@ const nextConfig = {
|
|||||||
pagesBufferLength: 2,
|
pagesBufferLength: 2,
|
||||||
},
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
|
inlineCss: false,
|
||||||
staleTimes: {
|
staleTimes: {
|
||||||
dynamic: 0,
|
dynamic: 0,
|
||||||
static: 30,
|
static: 30,
|
||||||
@@ -23,7 +24,6 @@ const nextConfig = {
|
|||||||
'lucide-react',
|
'lucide-react',
|
||||||
'framer-motion',
|
'framer-motion',
|
||||||
'@/components/ui',
|
'@/components/ui',
|
||||||
'@sentry/nextjs',
|
|
||||||
'react-hook-form',
|
'react-hook-form',
|
||||||
'zod',
|
'zod',
|
||||||
'date-fns',
|
'date-fns',
|
||||||
@@ -54,6 +54,8 @@ const nextConfig = {
|
|||||||
...config.resolve.alias,
|
...config.resolve.alias,
|
||||||
'next/dist/build/polyfills/polyfill-module': false,
|
'next/dist/build/polyfills/polyfill-module': false,
|
||||||
'next/dist/build/polyfills/polyfill-nomodule': false,
|
'next/dist/build/polyfills/polyfill-nomodule': false,
|
||||||
|
'core-js': false,
|
||||||
|
'regenerator-runtime': false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,14 +549,58 @@ const withAnalyzer = withBundleAnalyzer({
|
|||||||
enabled: process.env.ANALYZE === 'true',
|
enabled: process.env.ANALYZE === 'true',
|
||||||
});
|
});
|
||||||
|
|
||||||
// withMintelConfig internally sets output:'standalone' and wraps with withSentryConfig.
|
|
||||||
// In dev mode, standalone triggers @vercel/nft file tracing on every compile through
|
// In dev mode, standalone triggers @vercel/nft file tracing on every compile through
|
||||||
// VirtioFS, which hammers CPU. Strip it for dev.
|
// VirtioFS, which hammers CPU. Strip it for dev.
|
||||||
const withMDX = createMDX({});
|
const withMDX = createMDX({});
|
||||||
let resolvedConfig = withMDX(withAnalyzer(withMintelConfig(nextConfig)));
|
let resolvedConfig = withMDX(withAnalyzer(nextConfig));
|
||||||
|
|
||||||
|
// Apply next-intl manually (bypassing withMintelConfig to control Sentry)
|
||||||
|
import createNextIntlPlugin from 'next-intl/plugin';
|
||||||
|
const withNextIntl = createNextIntlPlugin();
|
||||||
|
resolvedConfig = withNextIntl(resolvedConfig);
|
||||||
|
|
||||||
|
// Removed withSentryConfig completely to eliminate the 170KB client-side bloat and Array.from legacy polyfill.
|
||||||
|
// Server-side instrumentation is still handled manually in instrumentation.ts.
|
||||||
|
|
||||||
|
// Manually merge baseNextConfig properties
|
||||||
|
const finalConfig = {
|
||||||
|
...baseNextConfig,
|
||||||
|
...resolvedConfig,
|
||||||
|
images: {
|
||||||
|
...baseNextConfig.images,
|
||||||
|
...(resolvedConfig.images || {}),
|
||||||
|
remotePatterns: [
|
||||||
|
...(baseNextConfig.images?.remotePatterns || []),
|
||||||
|
...(resolvedConfig.images?.remotePatterns || [])
|
||||||
|
]
|
||||||
|
},
|
||||||
|
rewrites: async () => {
|
||||||
|
let baseRewrites = [];
|
||||||
|
if (typeof baseNextConfig.rewrites === 'function') {
|
||||||
|
baseRewrites = await baseNextConfig.rewrites();
|
||||||
|
}
|
||||||
|
let configRewrites = { beforeFiles: [], fallback: [], afterFiles: [] };
|
||||||
|
if (typeof resolvedConfig.rewrites === 'function') {
|
||||||
|
const res = await resolvedConfig.rewrites();
|
||||||
|
if (Array.isArray(res)) {
|
||||||
|
configRewrites.fallback = res;
|
||||||
|
} else {
|
||||||
|
configRewrites = res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
beforeFiles: configRewrites.beforeFiles || [],
|
||||||
|
fallback: [
|
||||||
|
...(configRewrites.fallback || []),
|
||||||
|
...baseRewrites
|
||||||
|
],
|
||||||
|
afterFiles: configRewrites.afterFiles || []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!isProd) {
|
if (!isProd) {
|
||||||
delete resolvedConfig.output;
|
delete finalConfig.output;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default resolvedConfig;
|
export default finalConfig;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "e-tib-nextjs",
|
"name": "e-tib-nextjs",
|
||||||
|
"version": "2.4.9",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@10.18.3",
|
"packageManager": "pnpm@10.18.3",
|
||||||
@@ -11,6 +12,7 @@
|
|||||||
"@mintel/next-utils": "1.9.5",
|
"@mintel/next-utils": "1.9.5",
|
||||||
"@next/mdx": "^16.1.6",
|
"@next/mdx": "^16.1.6",
|
||||||
"@react-email/components": "1.0.8",
|
"@react-email/components": "1.0.8",
|
||||||
|
"@react-pdf/renderer": "^4.5.1",
|
||||||
"@react-three/drei": "^10.7.7",
|
"@react-three/drei": "^10.7.7",
|
||||||
"@react-three/fiber": "^9.5.0",
|
"@react-three/fiber": "^9.5.0",
|
||||||
"@sentry/nextjs": "^10.39.0",
|
"@sentry/nextjs": "^10.39.0",
|
||||||
@@ -139,7 +141,6 @@
|
|||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"preinstall": "npx only-allow pnpm"
|
"preinstall": "npx only-allow pnpm"
|
||||||
},
|
},
|
||||||
"version": "2.2.54",
|
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"@parcel/watcher",
|
"@parcel/watcher",
|
||||||
@@ -150,15 +151,15 @@
|
|||||||
],
|
],
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"minimatch": ">=10.2.2"
|
"minimatch": ">=10.2.2",
|
||||||
|
"framer-motion": "^12.38.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"last 2 chrome versions",
|
"last 2 chrome versions",
|
||||||
"last 2 firefox versions",
|
"last 2 firefox versions",
|
||||||
"last 2 edge versions",
|
"last 2 edge versions",
|
||||||
"safari >= 15.4",
|
"safari >= 15.4"
|
||||||
"not dead"
|
|
||||||
],
|
],
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"lucide-react": "^0.563.0"
|
"lucide-react": "^0.563.0"
|
||||||
|
|||||||
318
pnpm-lock.yaml
generated
318
pnpm-lock.yaml
generated
@@ -7,6 +7,7 @@ settings:
|
|||||||
overrides:
|
overrides:
|
||||||
next: 16.1.6
|
next: 16.1.6
|
||||||
minimatch: '>=10.2.2'
|
minimatch: '>=10.2.2'
|
||||||
|
framer-motion: ^12.38.0
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
@@ -33,6 +34,9 @@ importers:
|
|||||||
'@react-email/components':
|
'@react-email/components':
|
||||||
specifier: 1.0.8
|
specifier: 1.0.8
|
||||||
version: 1.0.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 1.0.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@react-pdf/renderer':
|
||||||
|
specifier: ^4.5.1
|
||||||
|
version: 4.5.1(react@19.2.4)
|
||||||
'@react-three/drei':
|
'@react-three/drei':
|
||||||
specifier: ^10.7.7
|
specifier: ^10.7.7
|
||||||
version: 10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2))(@types/react@19.2.14)(@types/three@0.183.1)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2)
|
version: 10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2))(@types/react@19.2.14)(@types/three@0.183.1)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2)
|
||||||
@@ -52,7 +56,7 @@ importers:
|
|||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
framer-motion:
|
framer-motion:
|
||||||
specifier: ^12.34.0
|
specifier: ^12.38.0
|
||||||
version: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
graphql:
|
graphql:
|
||||||
specifier: ^16.12.0
|
specifier: ^16.12.0
|
||||||
@@ -2209,6 +2213,49 @@ packages:
|
|||||||
react: ^18.0.0
|
react: ^18.0.0
|
||||||
react-dom: ^18.0.0
|
react-dom: ^18.0.0
|
||||||
|
|
||||||
|
'@react-pdf/fns@3.1.3':
|
||||||
|
resolution: {integrity: sha512-0I7pApDr1/RLAKbizuLy/IHTEa93LSPy/bEwYniboC3Xqnp6Od8xFJKbKEzGw2wh/5zKFFwl00g4t9RwgIMc3w==}
|
||||||
|
|
||||||
|
'@react-pdf/font@4.0.8':
|
||||||
|
resolution: {integrity: sha512-deNd+emtZAJho1IlzKL9bRoLAGv/6oXOIKO2oZfs4RuXUrK1onLHbJO7e2YoVLPFP/sQxisRTnzdJFtd35iKwA==}
|
||||||
|
|
||||||
|
'@react-pdf/image@3.1.0':
|
||||||
|
resolution: {integrity: sha512-ks7Ry8v711r8NvKWSELehj0BXBNPRihSnWsM09nDD8Ur175zbWBCK217LLwQMKDNYDVpkZaipdoJPom1LGaE9g==}
|
||||||
|
|
||||||
|
'@react-pdf/layout@4.6.1':
|
||||||
|
resolution: {integrity: sha512-gN6PmWoEffvlIkifLfEhMsVucRywVMyH3rnxdyOVOhGy0nWJKKGpHyPc4plbDdpP6EfZ0r8prHXujDSkIG2nSA==}
|
||||||
|
|
||||||
|
'@react-pdf/pdfkit@5.1.1':
|
||||||
|
resolution: {integrity: sha512-wNcdSsNlNYyGHGAgIdt453egBF7fiF9UxpRlklUfVvu8OWCrUppG9xiUrPLVoKiqWet5tMi0w6LmuFUJuYqjEg==}
|
||||||
|
|
||||||
|
'@react-pdf/primitives@4.3.0':
|
||||||
|
resolution: {integrity: sha512-nYXoZ36pvwNzbc54+DbL8RCn15jU7woJ9D/svnh5tpUXekJ+CbI4mZLo6boSv24CvJgychOu6h7gxX03B4ps0A==}
|
||||||
|
|
||||||
|
'@react-pdf/reconciler@2.0.0':
|
||||||
|
resolution: {integrity: sha512-7zaPRujpbHSmCpIrZ+b9HSTJHthcVZzX0Wx7RzvQGsGBUbHP4p6s5itXrAIOuQuPvDepoHGNOvf6xUuMVvdoyw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
|
'@react-pdf/render@4.5.1':
|
||||||
|
resolution: {integrity: sha512-IW/N4HWJWtioBXCf7n02IR24VJJ8gbdS3jGypf+vW/rSErEx3/URRzh9UK6Ma8Fpog9+T/W6GE2NHJ5AAKHhVA==}
|
||||||
|
|
||||||
|
'@react-pdf/renderer@4.5.1':
|
||||||
|
resolution: {integrity: sha512-5r1VQrE6FRLXX5wWUxwZzM24E2BJMo6g8AQWuS8WyPs9ugu5yMnb2g8/RpPYka/Z6J+RUEWc32wty2NoUJF42Q==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
|
'@react-pdf/stylesheet@6.2.1':
|
||||||
|
resolution: {integrity: sha512-2+UEk+7e+z8baaWi2l5kPLWmwtJeOI+T5wW9GGeN3iDH7vd3kbTqOpN1yt9mmfNVZFxQsnDHpznFb5v5UF983A==}
|
||||||
|
|
||||||
|
'@react-pdf/svg@1.1.0':
|
||||||
|
resolution: {integrity: sha512-cTIHXiz9x1HrbfqzfxfZP3FRdDwUXG77QWF6Fb5MP/lV3ONxR+g0Z3hwtBatCS9HeGBQCpxX/Lzb8wHE+co1PA==}
|
||||||
|
|
||||||
|
'@react-pdf/textkit@6.3.0':
|
||||||
|
resolution: {integrity: sha512-v6+V8nAcVwm7s2s1jIG2MD3Iw//x/k+XrH1foWOELBE4b32pyDgKyPXN/6KJE0dnX7+fVy27uctLNCLNMvzKzQ==}
|
||||||
|
|
||||||
|
'@react-pdf/types@2.11.1':
|
||||||
|
resolution: {integrity: sha512-i9xQgfaDU9QoeNnbp6rltXCWg1huEh195rpOuN8cE4BZ2FuLdQrsIcb2dhFF9aOxXf+XBA6LOSpIW051MDD/bw==}
|
||||||
|
|
||||||
'@react-three/drei@10.7.7':
|
'@react-three/drei@10.7.7':
|
||||||
resolution: {integrity: sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==}
|
resolution: {integrity: sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3383,6 +3430,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==}
|
resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==}
|
||||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
|
|
||||||
|
abs-svg-path@0.1.1:
|
||||||
|
resolution: {integrity: sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==}
|
||||||
|
|
||||||
accepts@1.3.8:
|
accepts@1.3.8:
|
||||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -3718,6 +3768,9 @@ packages:
|
|||||||
brotli@1.3.3:
|
brotli@1.3.3:
|
||||||
resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==}
|
resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==}
|
||||||
|
|
||||||
|
browserify-zlib@0.2.0:
|
||||||
|
resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==}
|
||||||
|
|
||||||
browserslist@4.28.2:
|
browserslist@4.28.2:
|
||||||
resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
|
resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
|
||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
@@ -3912,6 +3965,14 @@ packages:
|
|||||||
color-name@1.1.4:
|
color-name@1.1.4:
|
||||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
|
|
||||||
|
color-name@2.1.0:
|
||||||
|
resolution: {integrity: sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==}
|
||||||
|
engines: {node: '>=12.20'}
|
||||||
|
|
||||||
|
color-string@2.1.4:
|
||||||
|
resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
colorette@2.0.20:
|
colorette@2.0.20:
|
||||||
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
||||||
|
|
||||||
@@ -4363,6 +4424,9 @@ packages:
|
|||||||
electron-to-chromium@1.5.330:
|
electron-to-chromium@1.5.330:
|
||||||
resolution: {integrity: sha512-jFNydB5kFtYUobh4IkWUnXeyDbjf/r9gcUEXe1xcrcUxIGfTdzPXA+ld6zBRbwvgIGVzDll/LTIiDztEtckSnA==}
|
resolution: {integrity: sha512-jFNydB5kFtYUobh4IkWUnXeyDbjf/r9gcUEXe1xcrcUxIGfTdzPXA+ld6zBRbwvgIGVzDll/LTIiDztEtckSnA==}
|
||||||
|
|
||||||
|
emoji-regex-xs@1.0.0:
|
||||||
|
resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==}
|
||||||
|
|
||||||
emoji-regex@10.6.0:
|
emoji-regex@10.6.0:
|
||||||
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
|
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
|
||||||
|
|
||||||
@@ -4843,20 +4907,6 @@ packages:
|
|||||||
fraction.js@5.3.4:
|
fraction.js@5.3.4:
|
||||||
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
|
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
|
||||||
|
|
||||||
framer-motion@11.18.2:
|
|
||||||
resolution: {integrity: sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==}
|
|
||||||
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
|
|
||||||
|
|
||||||
framer-motion@12.38.0:
|
framer-motion@12.38.0:
|
||||||
resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==}
|
resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -5100,6 +5150,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==}
|
resolution: {integrity: sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==}
|
||||||
engines: {node: '>= 6.0.0'}
|
engines: {node: '>= 6.0.0'}
|
||||||
|
|
||||||
|
hsl-to-hex@1.0.0:
|
||||||
|
resolution: {integrity: sha512-K6GVpucS5wFf44X0h2bLVRDsycgJmf9FF2elg+CrqD8GcFU8c6vYhgXn8NjUkFCwj+xDFb70qgLbTUm6sxwPmA==}
|
||||||
|
|
||||||
|
hsl-to-rgb-for-reals@1.1.1:
|
||||||
|
resolution: {integrity: sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==}
|
||||||
|
|
||||||
html-encoding-sniffer@6.0.0:
|
html-encoding-sniffer@6.0.0:
|
||||||
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
|
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
|
||||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||||
@@ -5181,6 +5237,9 @@ packages:
|
|||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
hyphen@1.14.1:
|
||||||
|
resolution: {integrity: sha512-kvL8xYl5QMTh+LwohVN72ciOxC0OEV79IPdJSTwEXok9y9QHebXGdFgrED4sWfiax/ODx++CAMk3hMy4XPJPOw==}
|
||||||
|
|
||||||
i18next-fs-backend@2.6.3:
|
i18next-fs-backend@2.6.3:
|
||||||
resolution: {integrity: sha512-/5aW996nbolGh/qVU9urjAJ2QiXpiyGuhG8x/ZvvZQ3bMlrK+ouUdD6whS4e+sU9CCdUIzl7x/heEjsBh/3xtw==}
|
resolution: {integrity: sha512-/5aW996nbolGh/qVU9urjAJ2QiXpiyGuhG8x/ZvvZQ3bMlrK+ouUdD6whS4e+sU9CCdUIzl7x/heEjsBh/3xtw==}
|
||||||
|
|
||||||
@@ -5472,6 +5531,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
|
resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
is-url@1.2.4:
|
||||||
|
resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
|
||||||
|
|
||||||
is-weakmap@2.0.2:
|
is-weakmap@2.0.2:
|
||||||
resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
|
resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -5513,6 +5575,9 @@ packages:
|
|||||||
jackspeak@3.4.3:
|
jackspeak@3.4.3:
|
||||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||||
|
|
||||||
|
jay-peg@1.1.1:
|
||||||
|
resolution: {integrity: sha512-D62KEuBxz/ip2gQKOEhk/mx14o7eiFRaU+VNNSP4MOiIkwb/D6B3G1Mfas7C/Fit8EsSV2/IWjZElx/Gs6A4ww==}
|
||||||
|
|
||||||
jest-worker@27.5.1:
|
jest-worker@27.5.1:
|
||||||
resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
|
resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
|
||||||
engines: {node: '>= 10.13.0'}
|
engines: {node: '>= 10.13.0'}
|
||||||
@@ -5946,6 +6011,9 @@ packages:
|
|||||||
mdn-data@2.27.1:
|
mdn-data@2.27.1:
|
||||||
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
|
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
|
||||||
|
|
||||||
|
media-engine@1.0.3:
|
||||||
|
resolution: {integrity: sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==}
|
||||||
|
|
||||||
media-typer@0.3.0:
|
media-typer@0.3.0:
|
||||||
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -6142,15 +6210,9 @@ packages:
|
|||||||
module-details-from-path@1.0.4:
|
module-details-from-path@1.0.4:
|
||||||
resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==}
|
resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==}
|
||||||
|
|
||||||
motion-dom@11.18.1:
|
|
||||||
resolution: {integrity: sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==}
|
|
||||||
|
|
||||||
motion-dom@12.38.0:
|
motion-dom@12.38.0:
|
||||||
resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==}
|
resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==}
|
||||||
|
|
||||||
motion-utils@11.18.1:
|
|
||||||
resolution: {integrity: sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==}
|
|
||||||
|
|
||||||
motion-utils@12.36.0:
|
motion-utils@12.36.0:
|
||||||
resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==}
|
resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==}
|
||||||
|
|
||||||
@@ -6292,6 +6354,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
normalize-svg-path@1.1.0:
|
||||||
|
resolution: {integrity: sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==}
|
||||||
|
|
||||||
npm-run-path@4.0.1:
|
npm-run-path@4.0.1:
|
||||||
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
|
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -6438,6 +6503,9 @@ packages:
|
|||||||
pako@0.2.9:
|
pako@0.2.9:
|
||||||
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
|
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
|
||||||
|
|
||||||
|
pako@1.0.11:
|
||||||
|
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||||
|
|
||||||
parent-module@1.0.1:
|
parent-module@1.0.1:
|
||||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -6456,6 +6524,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
|
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
parse-svg-path@0.1.2:
|
||||||
|
resolution: {integrity: sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==}
|
||||||
|
|
||||||
parse5-htmlparser2-tree-adapter@7.1.0:
|
parse5-htmlparser2-tree-adapter@7.1.0:
|
||||||
resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==}
|
resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==}
|
||||||
|
|
||||||
@@ -6579,6 +6650,9 @@ packages:
|
|||||||
png-js@1.0.0:
|
png-js@1.0.0:
|
||||||
resolution: {integrity: sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==}
|
resolution: {integrity: sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==}
|
||||||
|
|
||||||
|
png-js@2.0.0:
|
||||||
|
resolution: {integrity: sha512-GdzJuUMc6ZSpxFJWVxtOH1bzYHym+TOnveqUjb+VJIbZWbZzyiRGFiKhbiielfpYbgMlhHVhsJ0FTazfuRFkMA==}
|
||||||
|
|
||||||
po-parser@2.1.1:
|
po-parser@2.1.1:
|
||||||
resolution: {integrity: sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==}
|
resolution: {integrity: sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==}
|
||||||
|
|
||||||
@@ -6723,6 +6797,9 @@ packages:
|
|||||||
queue-microtask@1.2.3:
|
queue-microtask@1.2.3:
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||||
|
|
||||||
|
queue@6.0.2:
|
||||||
|
resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==}
|
||||||
|
|
||||||
quick-format-unescaped@4.0.4:
|
quick-format-unescaped@4.0.4:
|
||||||
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
||||||
|
|
||||||
@@ -7016,6 +7093,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
||||||
engines: {node: '>=v12.22.7'}
|
engines: {node: '>=v12.22.7'}
|
||||||
|
|
||||||
|
scheduler@0.25.0-rc-603e6108-20241029:
|
||||||
|
resolution: {integrity: sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==}
|
||||||
|
|
||||||
scheduler@0.27.0:
|
scheduler@0.27.0:
|
||||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||||
|
|
||||||
@@ -7289,6 +7369,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
|
resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
string_decoder@1.3.0:
|
||||||
|
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||||
|
|
||||||
stringify-entities@4.0.4:
|
stringify-entities@4.0.4:
|
||||||
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
||||||
|
|
||||||
@@ -7374,6 +7457,9 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: '>=17.0'
|
react: '>=17.0'
|
||||||
|
|
||||||
|
svg-arc-to-cubic-bezier@3.2.0:
|
||||||
|
resolution: {integrity: sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==}
|
||||||
|
|
||||||
svg-to-pdfkit@0.1.8:
|
svg-to-pdfkit@0.1.8:
|
||||||
resolution: {integrity: sha512-QItiGZBy5TstGy+q8mjQTMGRlDDOARXLxH+sgVm1n/LYeo0zFcQlcCh8m4zi8QxctrxB9Kue/lStc/RD5iLadQ==}
|
resolution: {integrity: sha512-QItiGZBy5TstGy+q8mjQTMGRlDDOARXLxH+sgVm1n/LYeo0zFcQlcCh8m4zi8QxctrxB9Kue/lStc/RD5iLadQ==}
|
||||||
|
|
||||||
@@ -7757,6 +7843,10 @@ packages:
|
|||||||
victory-vendor@37.3.6:
|
victory-vendor@37.3.6:
|
||||||
resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==}
|
resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==}
|
||||||
|
|
||||||
|
vite-compatible-readable-stream@3.6.1:
|
||||||
|
resolution: {integrity: sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
vite@8.0.3:
|
vite@8.0.3:
|
||||||
resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==}
|
resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -8095,6 +8185,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
|
resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
yoga-layout@3.2.1:
|
||||||
|
resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==}
|
||||||
|
|
||||||
zod-validation-error@4.0.2:
|
zod-validation-error@4.0.2:
|
||||||
resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
|
resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
@@ -9246,7 +9339,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@medv/finder': 4.0.2
|
'@medv/finder': 4.0.2
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
framer-motion: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
html2canvas: 1.4.1
|
html2canvas: 1.4.1
|
||||||
lucide-react: 0.441.0(react@19.2.4)
|
lucide-react: 0.441.0(react@19.2.4)
|
||||||
next: 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)
|
next: 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)
|
||||||
@@ -10017,6 +10110,110 @@ snapshots:
|
|||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
react-dom: 19.2.4(react@19.2.4)
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
|
||||||
|
'@react-pdf/fns@3.1.3': {}
|
||||||
|
|
||||||
|
'@react-pdf/font@4.0.8':
|
||||||
|
dependencies:
|
||||||
|
'@react-pdf/pdfkit': 5.1.1
|
||||||
|
'@react-pdf/types': 2.11.1
|
||||||
|
fontkit: 2.0.4
|
||||||
|
is-url: 1.2.4
|
||||||
|
|
||||||
|
'@react-pdf/image@3.1.0':
|
||||||
|
dependencies:
|
||||||
|
'@react-pdf/svg': 1.1.0
|
||||||
|
jay-peg: 1.1.1
|
||||||
|
png-js: 2.0.0
|
||||||
|
|
||||||
|
'@react-pdf/layout@4.6.1':
|
||||||
|
dependencies:
|
||||||
|
'@react-pdf/fns': 3.1.3
|
||||||
|
'@react-pdf/image': 3.1.0
|
||||||
|
'@react-pdf/primitives': 4.3.0
|
||||||
|
'@react-pdf/stylesheet': 6.2.1
|
||||||
|
'@react-pdf/textkit': 6.3.0
|
||||||
|
'@react-pdf/types': 2.11.1
|
||||||
|
emoji-regex-xs: 1.0.0
|
||||||
|
queue: 6.0.2
|
||||||
|
yoga-layout: 3.2.1
|
||||||
|
|
||||||
|
'@react-pdf/pdfkit@5.1.1':
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.29.2
|
||||||
|
'@noble/ciphers': 1.3.0
|
||||||
|
'@noble/hashes': 1.8.0
|
||||||
|
browserify-zlib: 0.2.0
|
||||||
|
fontkit: 2.0.4
|
||||||
|
jay-peg: 1.1.1
|
||||||
|
js-md5: 0.8.3
|
||||||
|
linebreak: 1.1.0
|
||||||
|
png-js: 2.0.0
|
||||||
|
vite-compatible-readable-stream: 3.6.1
|
||||||
|
|
||||||
|
'@react-pdf/primitives@4.3.0': {}
|
||||||
|
|
||||||
|
'@react-pdf/reconciler@2.0.0(react@19.2.4)':
|
||||||
|
dependencies:
|
||||||
|
object-assign: 4.1.1
|
||||||
|
react: 19.2.4
|
||||||
|
scheduler: 0.25.0-rc-603e6108-20241029
|
||||||
|
|
||||||
|
'@react-pdf/render@4.5.1':
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.29.2
|
||||||
|
'@react-pdf/fns': 3.1.3
|
||||||
|
'@react-pdf/primitives': 4.3.0
|
||||||
|
'@react-pdf/textkit': 6.3.0
|
||||||
|
'@react-pdf/types': 2.11.1
|
||||||
|
abs-svg-path: 0.1.1
|
||||||
|
color-string: 2.1.4
|
||||||
|
normalize-svg-path: 1.1.0
|
||||||
|
parse-svg-path: 0.1.2
|
||||||
|
svg-arc-to-cubic-bezier: 3.2.0
|
||||||
|
|
||||||
|
'@react-pdf/renderer@4.5.1(react@19.2.4)':
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.29.2
|
||||||
|
'@react-pdf/fns': 3.1.3
|
||||||
|
'@react-pdf/font': 4.0.8
|
||||||
|
'@react-pdf/layout': 4.6.1
|
||||||
|
'@react-pdf/pdfkit': 5.1.1
|
||||||
|
'@react-pdf/primitives': 4.3.0
|
||||||
|
'@react-pdf/reconciler': 2.0.0(react@19.2.4)
|
||||||
|
'@react-pdf/render': 4.5.1
|
||||||
|
'@react-pdf/types': 2.11.1
|
||||||
|
events: 3.3.0
|
||||||
|
object-assign: 4.1.1
|
||||||
|
prop-types: 15.8.1
|
||||||
|
queue: 6.0.2
|
||||||
|
react: 19.2.4
|
||||||
|
|
||||||
|
'@react-pdf/stylesheet@6.2.1':
|
||||||
|
dependencies:
|
||||||
|
'@react-pdf/fns': 3.1.3
|
||||||
|
'@react-pdf/types': 2.11.1
|
||||||
|
color-string: 2.1.4
|
||||||
|
hsl-to-hex: 1.0.0
|
||||||
|
media-engine: 1.0.3
|
||||||
|
postcss-value-parser: 4.2.0
|
||||||
|
|
||||||
|
'@react-pdf/svg@1.1.0':
|
||||||
|
dependencies:
|
||||||
|
'@react-pdf/primitives': 4.3.0
|
||||||
|
|
||||||
|
'@react-pdf/textkit@6.3.0':
|
||||||
|
dependencies:
|
||||||
|
'@react-pdf/fns': 3.1.3
|
||||||
|
bidi-js: 1.0.3
|
||||||
|
hyphen: 1.14.1
|
||||||
|
unicode-properties: 1.4.1
|
||||||
|
|
||||||
|
'@react-pdf/types@2.11.1':
|
||||||
|
dependencies:
|
||||||
|
'@react-pdf/font': 4.0.8
|
||||||
|
'@react-pdf/primitives': 4.3.0
|
||||||
|
'@react-pdf/stylesheet': 6.2.1
|
||||||
|
|
||||||
'@react-three/drei@10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2))(@types/react@19.2.14)(@types/three@0.183.1)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2)':
|
'@react-three/drei@10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2))(@types/react@19.2.14)(@types/three@0.183.1)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.29.2
|
'@babel/runtime': 7.29.2
|
||||||
@@ -11195,6 +11392,8 @@ snapshots:
|
|||||||
|
|
||||||
abbrev@2.0.0: {}
|
abbrev@2.0.0: {}
|
||||||
|
|
||||||
|
abs-svg-path@0.1.1: {}
|
||||||
|
|
||||||
accepts@1.3.8:
|
accepts@1.3.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
@@ -11515,6 +11714,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
base64-js: 1.5.1
|
base64-js: 1.5.1
|
||||||
|
|
||||||
|
browserify-zlib@0.2.0:
|
||||||
|
dependencies:
|
||||||
|
pako: 1.0.11
|
||||||
|
|
||||||
browserslist@4.28.2:
|
browserslist@4.28.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
baseline-browser-mapping: 2.10.13
|
baseline-browser-mapping: 2.10.13
|
||||||
@@ -11729,6 +11932,12 @@ snapshots:
|
|||||||
|
|
||||||
color-name@1.1.4: {}
|
color-name@1.1.4: {}
|
||||||
|
|
||||||
|
color-name@2.1.0: {}
|
||||||
|
|
||||||
|
color-string@2.1.4:
|
||||||
|
dependencies:
|
||||||
|
color-name: 2.1.0
|
||||||
|
|
||||||
colorette@2.0.20: {}
|
colorette@2.0.20: {}
|
||||||
|
|
||||||
combined-stream@1.0.8:
|
combined-stream@1.0.8:
|
||||||
@@ -12206,6 +12415,8 @@ snapshots:
|
|||||||
|
|
||||||
electron-to-chromium@1.5.330: {}
|
electron-to-chromium@1.5.330: {}
|
||||||
|
|
||||||
|
emoji-regex-xs@1.0.0: {}
|
||||||
|
|
||||||
emoji-regex@10.6.0: {}
|
emoji-regex@10.6.0: {}
|
||||||
|
|
||||||
emoji-regex@8.0.0: {}
|
emoji-regex@8.0.0: {}
|
||||||
@@ -12941,15 +13152,6 @@ snapshots:
|
|||||||
|
|
||||||
fraction.js@5.3.4: {}
|
fraction.js@5.3.4: {}
|
||||||
|
|
||||||
framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
|
||||||
dependencies:
|
|
||||||
motion-dom: 11.18.1
|
|
||||||
motion-utils: 11.18.1
|
|
||||||
tslib: 2.8.1
|
|
||||||
optionalDependencies:
|
|
||||||
react: 19.2.4
|
|
||||||
react-dom: 19.2.4(react@19.2.4)
|
|
||||||
|
|
||||||
framer-motion@12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
framer-motion@12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
motion-dom: 12.38.0
|
motion-dom: 12.38.0
|
||||||
@@ -13267,6 +13469,12 @@ snapshots:
|
|||||||
|
|
||||||
hoopy@0.1.4: {}
|
hoopy@0.1.4: {}
|
||||||
|
|
||||||
|
hsl-to-hex@1.0.0:
|
||||||
|
dependencies:
|
||||||
|
hsl-to-rgb-for-reals: 1.1.1
|
||||||
|
|
||||||
|
hsl-to-rgb-for-reals@1.1.1: {}
|
||||||
|
|
||||||
html-encoding-sniffer@6.0.0(@noble/hashes@1.8.0):
|
html-encoding-sniffer@6.0.0(@noble/hashes@1.8.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@exodus/bytes': 1.15.0(@noble/hashes@1.8.0)
|
'@exodus/bytes': 1.15.0(@noble/hashes@1.8.0)
|
||||||
@@ -13365,6 +13573,8 @@ snapshots:
|
|||||||
|
|
||||||
husky@9.1.7: {}
|
husky@9.1.7: {}
|
||||||
|
|
||||||
|
hyphen@1.14.1: {}
|
||||||
|
|
||||||
i18next-fs-backend@2.6.3: {}
|
i18next-fs-backend@2.6.3: {}
|
||||||
|
|
||||||
i18next@25.10.10(typescript@5.9.3):
|
i18next@25.10.10(typescript@5.9.3):
|
||||||
@@ -13653,6 +13863,8 @@ snapshots:
|
|||||||
|
|
||||||
is-unicode-supported@2.1.0: {}
|
is-unicode-supported@2.1.0: {}
|
||||||
|
|
||||||
|
is-url@1.2.4: {}
|
||||||
|
|
||||||
is-weakmap@2.0.2: {}
|
is-weakmap@2.0.2: {}
|
||||||
|
|
||||||
is-weakref@1.1.1:
|
is-weakref@1.1.1:
|
||||||
@@ -13703,6 +13915,10 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@pkgjs/parseargs': 0.11.0
|
'@pkgjs/parseargs': 0.11.0
|
||||||
|
|
||||||
|
jay-peg@1.1.1:
|
||||||
|
dependencies:
|
||||||
|
restructure: 3.0.2
|
||||||
|
|
||||||
jest-worker@27.5.1:
|
jest-worker@27.5.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.19.15
|
'@types/node': 22.19.15
|
||||||
@@ -14251,6 +14467,8 @@ snapshots:
|
|||||||
|
|
||||||
mdn-data@2.27.1: {}
|
mdn-data@2.27.1: {}
|
||||||
|
|
||||||
|
media-engine@1.0.3: {}
|
||||||
|
|
||||||
media-typer@0.3.0: {}
|
media-typer@0.3.0: {}
|
||||||
|
|
||||||
meow@13.2.0: {}
|
meow@13.2.0: {}
|
||||||
@@ -14577,16 +14795,10 @@ snapshots:
|
|||||||
|
|
||||||
module-details-from-path@1.0.4: {}
|
module-details-from-path@1.0.4: {}
|
||||||
|
|
||||||
motion-dom@11.18.1:
|
|
||||||
dependencies:
|
|
||||||
motion-utils: 11.18.1
|
|
||||||
|
|
||||||
motion-dom@12.38.0:
|
motion-dom@12.38.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
motion-utils: 12.36.0
|
motion-utils: 12.36.0
|
||||||
|
|
||||||
motion-utils@11.18.1: {}
|
|
||||||
|
|
||||||
motion-utils@12.36.0: {}
|
motion-utils@12.36.0: {}
|
||||||
|
|
||||||
mri@1.2.0: {}
|
mri@1.2.0: {}
|
||||||
@@ -14717,6 +14929,10 @@ snapshots:
|
|||||||
|
|
||||||
normalize-path@3.0.0: {}
|
normalize-path@3.0.0: {}
|
||||||
|
|
||||||
|
normalize-svg-path@1.1.0:
|
||||||
|
dependencies:
|
||||||
|
svg-arc-to-cubic-bezier: 3.2.0
|
||||||
|
|
||||||
npm-run-path@4.0.1:
|
npm-run-path@4.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 3.1.1
|
path-key: 3.1.1
|
||||||
@@ -14927,6 +15143,8 @@ snapshots:
|
|||||||
|
|
||||||
pako@0.2.9: {}
|
pako@0.2.9: {}
|
||||||
|
|
||||||
|
pako@1.0.11: {}
|
||||||
|
|
||||||
parent-module@1.0.1:
|
parent-module@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
callsites: 3.1.0
|
callsites: 3.1.0
|
||||||
@@ -14954,6 +15172,8 @@ snapshots:
|
|||||||
json-parse-even-better-errors: 2.3.1
|
json-parse-even-better-errors: 2.3.1
|
||||||
lines-and-columns: 1.2.4
|
lines-and-columns: 1.2.4
|
||||||
|
|
||||||
|
parse-svg-path@0.1.2: {}
|
||||||
|
|
||||||
parse5-htmlparser2-tree-adapter@7.1.0:
|
parse5-htmlparser2-tree-adapter@7.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
domhandler: 5.0.3
|
domhandler: 5.0.3
|
||||||
@@ -15096,6 +15316,10 @@ snapshots:
|
|||||||
|
|
||||||
png-js@1.0.0: {}
|
png-js@1.0.0: {}
|
||||||
|
|
||||||
|
png-js@2.0.0:
|
||||||
|
dependencies:
|
||||||
|
fflate: 0.8.2
|
||||||
|
|
||||||
po-parser@2.1.1: {}
|
po-parser@2.1.1: {}
|
||||||
|
|
||||||
possible-typed-array-names@1.1.0: {}
|
possible-typed-array-names@1.1.0: {}
|
||||||
@@ -15253,6 +15477,10 @@ snapshots:
|
|||||||
|
|
||||||
queue-microtask@1.2.3: {}
|
queue-microtask@1.2.3: {}
|
||||||
|
|
||||||
|
queue@6.0.2:
|
||||||
|
dependencies:
|
||||||
|
inherits: 2.0.4
|
||||||
|
|
||||||
quick-format-unescaped@4.0.4: {}
|
quick-format-unescaped@4.0.4: {}
|
||||||
|
|
||||||
range-parser@1.2.1: {}
|
range-parser@1.2.1: {}
|
||||||
@@ -15671,6 +15899,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
xmlchars: 2.2.0
|
xmlchars: 2.2.0
|
||||||
|
|
||||||
|
scheduler@0.25.0-rc-603e6108-20241029: {}
|
||||||
|
|
||||||
scheduler@0.27.0: {}
|
scheduler@0.27.0: {}
|
||||||
|
|
||||||
schema-dts@1.1.5: {}
|
schema-dts@1.1.5: {}
|
||||||
@@ -16059,6 +16289,10 @@ snapshots:
|
|||||||
define-properties: 1.2.1
|
define-properties: 1.2.1
|
||||||
es-object-atoms: 1.1.1
|
es-object-atoms: 1.1.1
|
||||||
|
|
||||||
|
string_decoder@1.3.0:
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
stringify-entities@4.0.4:
|
stringify-entities@4.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
character-entities-html4: 2.1.0
|
character-entities-html4: 2.1.0
|
||||||
@@ -16129,6 +16363,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
|
|
||||||
|
svg-arc-to-cubic-bezier@3.2.0: {}
|
||||||
|
|
||||||
svg-to-pdfkit@0.1.8:
|
svg-to-pdfkit@0.1.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
pdfkit: 0.18.0
|
pdfkit: 0.18.0
|
||||||
@@ -16597,6 +16833,12 @@ snapshots:
|
|||||||
d3-time: 3.1.0
|
d3-time: 3.1.0
|
||||||
d3-timer: 3.0.1
|
d3-timer: 3.0.1
|
||||||
|
|
||||||
|
vite-compatible-readable-stream@3.6.1:
|
||||||
|
dependencies:
|
||||||
|
inherits: 2.0.4
|
||||||
|
string_decoder: 1.3.0
|
||||||
|
util-deprecate: 1.0.2
|
||||||
|
|
||||||
vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
|
vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
lightningcss: 1.32.0
|
lightningcss: 1.32.0
|
||||||
@@ -16928,6 +17170,8 @@ snapshots:
|
|||||||
|
|
||||||
yoctocolors@2.1.2: {}
|
yoctocolors@2.1.2: {}
|
||||||
|
|
||||||
|
yoga-layout@3.2.1: {}
|
||||||
|
|
||||||
zod-validation-error@4.0.2(zod@3.25.76):
|
zod-validation-error@4.0.2(zod@3.25.76):
|
||||||
dependencies:
|
dependencies:
|
||||||
zod: 3.25.76
|
zod: 3.25.76
|
||||||
|
|||||||
39
psi-standorte.json
Normal file
39
psi-standorte.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": 429,
|
||||||
|
"message": "Quota exceeded for quota metric 'Queries' and limit 'Queries per day' of service 'pagespeedonline.googleapis.com' for consumer 'project_number:583797351490'.",
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"message": "Quota exceeded for quota metric 'Queries' and limit 'Queries per day' of service 'pagespeedonline.googleapis.com' for consumer 'project_number:583797351490'.",
|
||||||
|
"domain": "global",
|
||||||
|
"reason": "rateLimitExceeded"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": "RESOURCE_EXHAUSTED",
|
||||||
|
"details": [
|
||||||
|
{
|
||||||
|
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
|
||||||
|
"reason": "RATE_LIMIT_EXCEEDED",
|
||||||
|
"domain": "googleapis.com",
|
||||||
|
"metadata": {
|
||||||
|
"service": "pagespeedonline.googleapis.com",
|
||||||
|
"quota_location": "global",
|
||||||
|
"quota_limit_value": "0",
|
||||||
|
"consumer": "projects/583797351490",
|
||||||
|
"quota_metric": "pagespeedonline.googleapis.com/default",
|
||||||
|
"quota_limit": "defaultPerDayPerProject",
|
||||||
|
"quota_unit": "1/d/{project}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "type.googleapis.com/google.rpc.Help",
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"description": "Request a higher quota limit.",
|
||||||
|
"url": "https://cloud.google.com/docs/quotas/help/request_increase"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 4.7 KiB |
BIN
public/assets/videos/web/hero-kabelpflug-poster-mobile.webp
Normal file
BIN
public/assets/videos/web/hero-kabelpflug-poster-mobile.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
BIN
public/assets/videos/web/hero-kabelpflug-poster.webp
Normal file
BIN
public/assets/videos/web/hero-kabelpflug-poster.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
36
run-lh.cjs
Normal file
36
run-lh.cjs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
|
console.log("Running Lighthouse...");
|
||||||
|
try {
|
||||||
|
execSync('npx lighthouse "https://e-tib.com/de/standorte?v=2" --chrome-flags="--headless --no-sandbox" --output json --output-path ./lh-report.json', { stdio: 'inherit' });
|
||||||
|
} catch(e) {
|
||||||
|
console.log("Lighthouse run failed", e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.parse(fs.readFileSync('./lh-report.json', 'utf8'));
|
||||||
|
const score = data.categories.performance.score * 100;
|
||||||
|
console.log("\n==================================");
|
||||||
|
console.log(`PERFORMANCE SCORE: ${score}`);
|
||||||
|
console.log("==================================\n");
|
||||||
|
|
||||||
|
console.log("FAILING METRICS (Score < 1.0):");
|
||||||
|
Object.values(data.audits).filter(a => a.score !== null && a.score < 1).forEach(a => {
|
||||||
|
console.log(`- ${a.id}: ${a.score} (${a.displayValue || 'No display value'})`);
|
||||||
|
if (a.details && a.details.items && a.details.items.length > 0) {
|
||||||
|
if (a.id === 'image-delivery-insight' || a.id === 'unused-javascript' || a.id === 'modern-image-formats' || a.id === 'uses-responsive-images') {
|
||||||
|
console.log(" Items:");
|
||||||
|
a.details.items.forEach(item => {
|
||||||
|
console.log(` URL: ${item.url || item.node?.nodeLabel || 'unknown'}`);
|
||||||
|
if (item.wastedBytes) console.log(` Wasted KB: ${(item.wastedBytes / 1024).toFixed(2)}`);
|
||||||
|
if (item.wastedMs) console.log(` Wasted Ms: ${item.wastedMs}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("\nLCP BREAKDOWN:");
|
||||||
|
const lcpElement = data.audits['largest-contentful-paint-element'];
|
||||||
|
if (lcpElement && lcpElement.details && lcpElement.details.items) {
|
||||||
|
console.log("LCP Element:", lcpElement.details.items[0].node.snippet);
|
||||||
|
}
|
||||||
36
run-lh.js
Normal file
36
run-lh.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
|
console.log("Running Lighthouse...");
|
||||||
|
try {
|
||||||
|
execSync('npx lighthouse "https://e-tib.com/de/standorte?v=2" --chrome-flags="--headless --no-sandbox" --output json --output-path ./lh-report.json', { stdio: 'inherit' });
|
||||||
|
} catch(e) {
|
||||||
|
console.log("Lighthouse run failed", e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.parse(fs.readFileSync('./lh-report.json', 'utf8'));
|
||||||
|
const score = data.categories.performance.score * 100;
|
||||||
|
console.log("\n==================================");
|
||||||
|
console.log(`PERFORMANCE SCORE: ${score}`);
|
||||||
|
console.log("==================================\n");
|
||||||
|
|
||||||
|
console.log("FAILING METRICS (Score < 1.0):");
|
||||||
|
Object.values(data.audits).filter(a => a.score !== null && a.score < 1).forEach(a => {
|
||||||
|
console.log(`- ${a.id}: ${a.score} (${a.displayValue || 'No display value'})`);
|
||||||
|
if (a.details && a.details.items && a.details.items.length > 0) {
|
||||||
|
if (a.id === 'image-delivery-insight' || a.id === 'unused-javascript' || a.id === 'modern-image-formats' || a.id === 'uses-responsive-images') {
|
||||||
|
console.log(" Items:");
|
||||||
|
a.details.items.forEach(item => {
|
||||||
|
console.log(` URL: ${item.url || item.node?.nodeLabel || 'unknown'}`);
|
||||||
|
if (item.wastedBytes) console.log(` Wasted KB: ${item.wastedBytes / 1024}`);
|
||||||
|
if (item.wastedMs) console.log(` Wasted Ms: ${item.wastedMs}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("\nLCP BREAKDOWN:");
|
||||||
|
const lcpElement = data.audits['largest-contentful-paint-element'];
|
||||||
|
if (lcpElement && lcpElement.details && lcpElement.details.items) {
|
||||||
|
console.log("LCP Element:", lcpElement.details.items[0].node.snippet);
|
||||||
|
}
|
||||||
@@ -6,13 +6,14 @@ module.exports = async (browser, context) => {
|
|||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
// Using LHCI_URL or TARGET_URL if available
|
// Using LHCI_URL or TARGET_URL if available
|
||||||
const targetUrl =
|
const targetUrl =
|
||||||
process.env.LHCI_URL || process.env.TARGET_URL || 'https://testing.klz-cables.com';
|
process.env.LHCI_URL || process.env.TARGET_URL || 'https://testing.e-tib.com';
|
||||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'klz2026';
|
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'klz2026';
|
||||||
|
const authCookieName = process.env.AUTH_COOKIE_NAME || 'etib_gatekeeper_session';
|
||||||
|
|
||||||
console.log(`🔑 LHCI Auth: Setting gatekeeper cookie for ${new URL(targetUrl).hostname}...`);
|
console.log(`🔑 LHCI Auth: Setting gatekeeper cookie (${authCookieName}) for ${new URL(targetUrl).hostname}...`);
|
||||||
|
|
||||||
await page.setCookie({
|
await page.setCookie({
|
||||||
name: 'klz_gatekeeper_session',
|
name: authCookieName,
|
||||||
value: gatekeeperPassword,
|
value: gatekeeperPassword,
|
||||||
domain: new URL(targetUrl).hostname,
|
domain: new URL(targetUrl).hostname,
|
||||||
path: '/',
|
path: '/',
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const targetUrl =
|
|||||||
'http://localhost:3000';
|
'http://localhost:3000';
|
||||||
const limit = process.env.PAGESPEED_LIMIT ? parseInt(process.env.PAGESPEED_LIMIT) : 20; // Default limit to avoid infinite runs
|
const limit = process.env.PAGESPEED_LIMIT ? parseInt(process.env.PAGESPEED_LIMIT) : 20; // Default limit to avoid infinite runs
|
||||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'klz2026';
|
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'klz2026';
|
||||||
|
const authCookieName = process.env.AUTH_COOKIE_NAME || 'etib_gatekeeper_session';
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
console.log(`\n🚀 Starting PageSpeed test for: ${targetUrl}`);
|
console.log(`\n🚀 Starting PageSpeed test for: ${targetUrl}`);
|
||||||
@@ -32,7 +33,7 @@ async function main() {
|
|||||||
// We might need to bypass gatekeeper for the sitemap fetch too
|
// We might need to bypass gatekeeper for the sitemap fetch too
|
||||||
const response = await axios.get(sitemapUrl, {
|
const response = await axios.get(sitemapUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `klz_gatekeeper_session=${gatekeeperPassword}`,
|
Cookie: `${authCookieName}=${gatekeeperPassword}`,
|
||||||
},
|
},
|
||||||
validateStatus: (status) => status < 400,
|
validateStatus: (status) => status < 400,
|
||||||
});
|
});
|
||||||
@@ -77,7 +78,7 @@ async function main() {
|
|||||||
// Handle authentication for staging/testing
|
// Handle authentication for staging/testing
|
||||||
// Lighthouse can set cookies via --collect.settings.extraHeaders
|
// Lighthouse can set cookies via --collect.settings.extraHeaders
|
||||||
const extraHeaders = JSON.stringify({
|
const extraHeaders = JSON.stringify({
|
||||||
Cookie: `klz_gatekeeper_session=${gatekeeperPassword}`,
|
Cookie: `${authCookieName}=${gatekeeperPassword}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Detect Chrome path from Puppeteer installation if not provided
|
// Detect Chrome path from Puppeteer installation if not provided
|
||||||
@@ -147,7 +148,7 @@ async function main() {
|
|||||||
execSync(lhciCommand, {
|
execSync(lhciCommand, {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
env: { ...process.env, CHROME_PATH: chromePath },
|
env: { ...process.env, CHROME_PATH: chromePath, TARGET_URL: targetUrl },
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.warn('⚠️ LHCI assertion finished with warnings or errors.');
|
console.warn('⚠️ LHCI assertion finished with warnings or errors.');
|
||||||
|
|||||||
@@ -70,7 +70,9 @@ while IFS= read -r run; do
|
|||||||
elif [ "$CURRENT_PRIO" -lt "$RUN_PRIO" ]; then
|
elif [ "$CURRENT_PRIO" -lt "$RUN_PRIO" ]; then
|
||||||
echo "Current workflow has LOWER priority ($CURRENT_PRIO < $RUN_PRIO). Aborting self."
|
echo "Current workflow has LOWER priority ($CURRENT_PRIO < $RUN_PRIO). Aborting self."
|
||||||
curl -s -X POST -H "Authorization: token $GITEA_PAT" "$API_BASE/$CURRENT_RUN_ID/cancel"
|
curl -s -X POST -H "Authorization: token $GITEA_PAT" "$API_BASE/$CURRENT_RUN_ID/cancel"
|
||||||
exit 1
|
echo "Waiting for Gitea to process cancellation..."
|
||||||
|
sleep 15
|
||||||
|
exit 0
|
||||||
else
|
else
|
||||||
# Equal priority: Newer wins
|
# Equal priority: Newer wins
|
||||||
if [ "$CURRENT_RUN_ID" -gt "$RUN_ID" ]; then
|
if [ "$CURRENT_RUN_ID" -gt "$RUN_ID" ]; then
|
||||||
@@ -79,7 +81,9 @@ while IFS= read -r run; do
|
|||||||
else
|
else
|
||||||
echo "Equal priority, but current is older. Aborting self."
|
echo "Equal priority, but current is older. Aborting self."
|
||||||
curl -s -X POST -H "Authorization: token $GITEA_PAT" "$API_BASE/$CURRENT_RUN_ID/cancel"
|
curl -s -X POST -H "Authorization: token $GITEA_PAT" "$API_BASE/$CURRENT_RUN_ID/cancel"
|
||||||
exit 1
|
echo "Waiting for Gitea to process cancellation..."
|
||||||
|
sleep 15
|
||||||
|
exit 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
done <<< "$RUNS"
|
done <<< "$RUNS"
|
||||||
|
|||||||
9
skip_branch_script.sh
Normal file
9
skip_branch_script.sh
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
if [[ "${{ github.ref_type }}" == "branch" && "${{ github.ref_name }}" != "main" && "${{ github.ref_name }}" != "master" ]]; then
|
||||||
|
# Fetch tags from remote for this specific commit to check if it's being tagged simultaneously
|
||||||
|
TAGS=$(git ls-remote --tags origin | grep "${{ github.sha }}" || true)
|
||||||
|
if [[ -n "$TAGS" ]]; then
|
||||||
|
echo "Dieser Commit hat einen Tag! Überspringe Preview-Deployment, damit der Tag-Deploy Vorrang hat."
|
||||||
|
echo "target=skip" >> $GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
@@ -1,378 +1,267 @@
|
|||||||
@import 'tailwindcss';
|
@import "tailwindcss";
|
||||||
@config "../tailwind.config.cjs";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-sans:
|
/* Breakpoints */
|
||||||
var(--font-inter), system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
--breakpoint-xs: 475px;
|
||||||
'Helvetica Neue', Arial, sans-serif;
|
--breakpoint-sm: 640px;
|
||||||
|
--breakpoint-md: 768px;
|
||||||
|
--breakpoint-lg: 1024px;
|
||||||
|
--breakpoint-xl: 1280px;
|
||||||
|
--breakpoint-2xl: 1400px;
|
||||||
|
--breakpoint-3xl: 1600px;
|
||||||
|
|
||||||
|
/* Brand Colors */
|
||||||
|
--color-primary: #0e7a5c;
|
||||||
|
--color-primary-dark: #0a5742;
|
||||||
|
--color-primary-light: #149d76;
|
||||||
|
|
||||||
|
--color-secondary: #194f3b;
|
||||||
|
--color-secondary-light: #20654c;
|
||||||
|
--color-secondary-dark: #13392a;
|
||||||
|
|
||||||
|
--color-accent: #0e7a5c;
|
||||||
|
--color-accent-light: #149d76;
|
||||||
|
|
||||||
|
--color-neutral: #fafafa;
|
||||||
|
--color-neutral-light: #ffffff;
|
||||||
|
--color-neutral-dark: #333333;
|
||||||
|
--color-neutral-black: #1a1a1a;
|
||||||
|
|
||||||
|
/* Text Colors */
|
||||||
|
--color-text-primary: #1a1a1a;
|
||||||
|
--color-text-secondary: #6c757d;
|
||||||
|
--color-text-light: #adb5bd;
|
||||||
|
|
||||||
|
/* Semantic Colors */
|
||||||
|
--color-success: #28a745;
|
||||||
|
--color-warning: #ffc107;
|
||||||
|
--color-danger: #dc3545;
|
||||||
|
--color-info: #17a2b8;
|
||||||
|
|
||||||
|
/* Fonts */
|
||||||
|
--font-sans: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
--font-heading: 'Inter', system-ui, sans-serif;
|
--font-heading: 'Inter', system-ui, sans-serif;
|
||||||
--font-body: 'Inter', system-ui, sans-serif;
|
--font-body: 'Inter', system-ui, sans-serif;
|
||||||
|
|
||||||
--color-primary: #0e7a5c;
|
/* Typography */
|
||||||
/* Deep Teal/Green */
|
--text-xs: 0.75rem;
|
||||||
--color-primary-dark: #084c39;
|
--text-xs--line-height: 1.6;
|
||||||
--color-primary-light: #e6f5f0;
|
--text-sm: 0.875rem;
|
||||||
|
--text-sm--line-height: 1.6;
|
||||||
|
--text-base: 1rem;
|
||||||
|
--text-base--line-height: 1.7;
|
||||||
|
--text-lg: 1.125rem;
|
||||||
|
--text-lg--line-height: 1.7;
|
||||||
|
--text-xl: 1.25rem;
|
||||||
|
--text-xl--line-height: 1.6;
|
||||||
|
--text-2xl: 1.5rem;
|
||||||
|
--text-2xl--line-height: 1.5;
|
||||||
|
--text-3xl: 1.875rem;
|
||||||
|
--text-3xl--line-height: 1.4;
|
||||||
|
--text-4xl: 2.25rem;
|
||||||
|
--text-4xl--line-height: 1.3;
|
||||||
|
--text-5xl: 3rem;
|
||||||
|
--text-5xl--line-height: 1.25;
|
||||||
|
--text-6xl: 3.75rem;
|
||||||
|
--text-6xl--line-height: 1.2;
|
||||||
|
--text-7xl: 4.5rem;
|
||||||
|
--text-7xl--line-height: 1.15;
|
||||||
|
--text-8xl: 6rem;
|
||||||
|
--text-8xl--line-height: 1.1;
|
||||||
|
|
||||||
--color-saturated: #10a379;
|
/* Font Weights */
|
||||||
/* Saturated Teal Accent */
|
--font-weight-regular: 400;
|
||||||
|
--font-weight-medium: 500;
|
||||||
|
--font-weight-semibold: 600;
|
||||||
|
--font-weight-bold: 700;
|
||||||
|
--font-weight-extrabold: 800;
|
||||||
|
|
||||||
--color-secondary: #003d82;
|
/* Border Radius */
|
||||||
--color-secondary-light: #0056b3;
|
--radius-sm: 0.25rem;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
--radius-lg: 0.75rem;
|
||||||
|
--radius-xl: 1rem;
|
||||||
|
--radius-2xl: 1.5rem;
|
||||||
|
|
||||||
--color-accent: #10a379;
|
/* Box Shadow */
|
||||||
/* Brand Aligned Accent */
|
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||||
--color-accent-dark: #0e7a5c;
|
--shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
--color-accent-light: #e6f5f0;
|
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
--color-neutral: #f8f9fa;
|
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
--color-neutral-light: #ffffff;
|
|
||||||
--color-neutral-dark: #0a0a0a;
|
|
||||||
--color-neutral-medium: #e5e7eb;
|
|
||||||
|
|
||||||
--color-text-primary: #111827;
|
|
||||||
--color-text-secondary: #4b5563;
|
|
||||||
--color-text-light: #9ca3af;
|
|
||||||
|
|
||||||
--color-success: #10b981;
|
|
||||||
--color-warning: #f59e0b;
|
|
||||||
--color-danger: #ef4444;
|
|
||||||
--color-destructive: #ef4444;
|
|
||||||
--color-destructive-foreground: #ffffff;
|
|
||||||
--color-info: #3b82f6;
|
|
||||||
|
|
||||||
--animate-fade-in: fade-in 0.5s ease-out;
|
|
||||||
--animate-slide-up: slide-up 0.6s ease-out;
|
|
||||||
--animate-slow-zoom: slow-zoom 20s linear infinite;
|
|
||||||
--animate-reveal: reveal 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
|
||||||
--animate-slight-fade-in-from-bottom: slight-fade-in-from-bottom 0.8s
|
|
||||||
cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
|
||||||
--animate-gradient-x: gradient-x 15s ease infinite;
|
|
||||||
--animate-draw-stroke: draw-stroke 1.8s ease-in-out 0.5s forwards;
|
|
||||||
|
|
||||||
@keyframes draw-stroke {
|
|
||||||
from {
|
|
||||||
stroke-dasharray: 1;
|
|
||||||
stroke-dashoffset: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
stroke-dasharray: 1;
|
|
||||||
stroke-dashoffset: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes gradient-x {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
background-position: 0% 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
background-position: 100% 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fade-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slide-up {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slow-zoom {
|
|
||||||
from {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes reveal {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
filter: blur(8px);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
filter: blur(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slight-fade-in-from-bottom {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(10px);
|
|
||||||
filter: blur(4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
filter: blur(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes float {
|
|
||||||
0% {
|
|
||||||
transform: translate(0, 0) scale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
33% {
|
|
||||||
transform: translate(2%, 4%) scale(1.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
66% {
|
|
||||||
transform: translate(-3%, 2%) scale(0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: translate(0, 0) scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin-slow {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes flow {
|
|
||||||
to {
|
|
||||||
stroke-dashoffset: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes solar-pulse {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
fill-opacity: 0.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
fill-opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
/* Custom Utilities (from JS plugins) */
|
||||||
html {
|
|
||||||
/* Scale text down slightly on mobile (87.5% of 16px = 14px) */
|
|
||||||
font-size: 87.5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
html {
|
|
||||||
/* Normal scaling on tablet and desktop (100% = 16px) */
|
|
||||||
font-size: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-primary a,
|
|
||||||
.bg-primary-dark a {
|
|
||||||
@apply text-white/90 hover:text-white transition-colors;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
@apply text-base md:text-lg antialiased;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
line-height: 1.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
*:focus-visible {
|
|
||||||
outline: 3px solid var(--color-primary);
|
|
||||||
outline-offset: 3px;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6 {
|
|
||||||
@apply font-heading font-bold tracking-tight;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Enhanced Mobile-first typography hierarchy with fluid sizing */
|
|
||||||
h1 {
|
|
||||||
@apply text-3xl md:text-5xl lg:text-6xl;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
@apply text-2xl md:text-4xl lg:text-5xl;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
@apply text-xl md:text-2xl lg:text-3xl;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
@apply text-lg md:text-xl lg:text-2xl;
|
|
||||||
}
|
|
||||||
|
|
||||||
h5 {
|
|
||||||
@apply text-base md:text-lg;
|
|
||||||
}
|
|
||||||
|
|
||||||
h6 {
|
|
||||||
@apply text-sm md:text-base;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Paragraph and text styles */
|
|
||||||
p {
|
|
||||||
@apply mb-4 leading-relaxed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Link styles */
|
|
||||||
a {
|
|
||||||
@apply transition-all duration-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* List styles */
|
|
||||||
ul,
|
|
||||||
ol {
|
|
||||||
@apply my-4 ml-6;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
@apply mb-2 leading-relaxed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Small text */
|
|
||||||
small {
|
|
||||||
@apply text-sm md:text-base;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Strong and emphasis */
|
|
||||||
strong {
|
|
||||||
@apply font-bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
em {
|
|
||||||
@apply italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Blockquote */
|
|
||||||
blockquote {
|
|
||||||
@apply border-l-4 pl-6 my-6 italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Code */
|
|
||||||
code {
|
|
||||||
@apply px-2 py-1 rounded font-mono text-sm;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Horizontal rule */
|
|
||||||
hr {
|
|
||||||
@apply my-8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer components {
|
|
||||||
.glass-panel {
|
|
||||||
@apply bg-white/80 backdrop-blur-md border border-white/20 shadow-lg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-overlay-gradient {
|
|
||||||
@apply absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.premium-card {
|
|
||||||
@apply bg-white rounded-3xl shadow-sm transition-all duration-500 hover:shadow-2xl hover:-translate-y-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sticky-narrative-container {
|
|
||||||
@apply grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sticky-narrative-sidebar {
|
|
||||||
@apply lg:col-span-4 lg:sticky lg:top-32 h-fit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sticky-narrative-content {
|
|
||||||
@apply lg:col-span-8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom Utilities */
|
|
||||||
@utility touch-target {
|
@utility touch-target {
|
||||||
min-height: 48px;
|
min-height: 44px;
|
||||||
/* Increased for better touch-first feel */
|
min-width: 44px;
|
||||||
min-width: 48px;
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility reveal-on-scroll {
|
@utility mobile-hidden {
|
||||||
opacity: 0;
|
@media (max-width: 767px) {
|
||||||
transform: translateY(20px);
|
display: none !important;
|
||||||
filter: blur(8px);
|
|
||||||
transition:
|
|
||||||
opacity 0.6s ease-out,
|
|
||||||
transform 0.8s cubic-bezier(0.16, 1, 0.3, 1),
|
|
||||||
filter 0.8s cubic-bezier(0.16, 1, 0.3, 1);
|
|
||||||
|
|
||||||
&.is-visible {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
filter: blur(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
opacity: 1;
|
|
||||||
transform: none !important;
|
|
||||||
filter: none !important;
|
|
||||||
transition: opacity 0.3s ease-out;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility hover-effect {
|
@utility tablet-hidden {
|
||||||
@media (hover: hover) {
|
@media (min-width: 768px) and (max-width: 1023px) {
|
||||||
@apply transition-all duration-300 hover:scale-105;
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility desktop-hidden {
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility mobile-only {
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility tablet-only {
|
||||||
|
@media (max-width: 767px), (min-width: 1024px) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility desktop-only {
|
||||||
|
@media (max-width: 1023px) {
|
||||||
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility fluid-spacing {
|
@utility fluid-spacing {
|
||||||
padding: clamp(1.5rem, 5vw, 5rem);
|
padding: clamp(1rem, 3vw, 3rem);
|
||||||
margin: clamp(1rem, 3vw, 3rem);
|
margin: clamp(0.5rem, 2vw, 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility text-start-mobile {
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility text-center-mobile {
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility text-end-mobile {
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility grid-mobile-stacked {
|
@utility grid-mobile-stacked {
|
||||||
@apply grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8 lg:gap-12;
|
@media (max-width: 767px) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility safe-area-p {
|
@utility safe-area-p {
|
||||||
padding-top: max(1rem, env(safe-area-inset-top));
|
padding-top: env(safe-area-inset-top);
|
||||||
padding-bottom: max(1rem, env(safe-area-inset-bottom));
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
padding-left: max(1rem, env(safe-area-inset-left));
|
padding-left: env(safe-area-inset-left);
|
||||||
padding-right: max(1rem, env(safe-area-inset-right));
|
padding-right: env(safe-area-inset-right);
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility content-visibility-auto {
|
/* E-TIB Custom Utilities (from previous globals.css) */
|
||||||
content-visibility: auto;
|
@layer utilities {
|
||||||
contain-intrinsic-size: 1px 1000px;
|
.text-balance {
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-scrollbar {
|
||||||
|
-ms-overflow-style: none !important; /* IE and Edge */
|
||||||
|
scrollbar-width: none !important; /* Firefox */
|
||||||
|
}
|
||||||
|
.hide-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none !important; /* Chrome, Safari and Opera */
|
||||||
|
width: 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bg-pulse-15 {
|
||||||
|
0%, 90%, 100% { opacity: 0.04; }
|
||||||
|
95% { opacity: 0.10; filter: brightness(1.2); }
|
||||||
|
}
|
||||||
|
@keyframes bg-pulse-20 {
|
||||||
|
0%, 90%, 100% { opacity: 0.07; }
|
||||||
|
95% { opacity: 0.14; filter: brightness(1.2); }
|
||||||
|
}
|
||||||
|
.animate-bg-pulse-15 { animation: bg-pulse-15 12s infinite ease-in-out; }
|
||||||
|
.animate-bg-pulse-20 { animation: bg-pulse-20 12s infinite ease-in-out; }
|
||||||
|
|
||||||
|
@keyframes hero-fade-in-up {
|
||||||
|
from { opacity: 0; transform: translateY(30px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
.animate-hero-fade-in-up {
|
||||||
|
animation: hero-fade-in-up 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes hero-zoom-out {
|
||||||
|
from { transform: scale(1.05); }
|
||||||
|
to { transform: scale(1); }
|
||||||
|
}
|
||||||
|
.animate-hero-zoom-out {
|
||||||
|
animation: hero-zoom-out 1.5s ease-out forwards;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base Styles */
|
||||||
|
@layer base {
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-neutral font-sans text-neutral-dark antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Interactive elements pointer */
|
||||||
|
a, button, [role="button"], input[type="button"], input[type="submit"], input[type="reset"], select, summary, label[for] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure disabled elements don't show pointer */
|
||||||
|
button:disabled, [role="button"]:disabled, input:disabled, select:disabled, button[disabled] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Modern Scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(14, 122, 92, 0.4);
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 4px solid transparent;
|
||||||
|
background-clip: padding-box;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: rgba(14, 122, 92, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Firefox */
|
||||||
|
html {
|
||||||
|
scrollbar-color: rgba(14, 122, 92, 0.5) transparent;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,226 +0,0 @@
|
|||||||
/* eslint-disable no-undef */
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
module.exports = {
|
|
||||||
content: [
|
|
||||||
'./app/**/*.{js,ts,jsx,tsx}',
|
|
||||||
'./components/**/*.{js,ts,jsx,tsx}',
|
|
||||||
'./content/**/*.{mdx,md}',
|
|
||||||
'./node_modules/@mintel/annotator/dist/**/*.{js,ts,jsx,tsx,mjs}',
|
|
||||||
'./node_modules/.pnpm/@mintel+annotator*/node_modules/@mintel/annotator/dist/**/*.{js,ts,jsx,tsx,mjs}'
|
|
||||||
],
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
// Custom Breakpoints for KLZ Cables
|
|
||||||
screens: {
|
|
||||||
'xs': '475px',
|
|
||||||
'sm': '640px',
|
|
||||||
'md': '768px',
|
|
||||||
'lg': '1024px',
|
|
||||||
'xl': '1280px',
|
|
||||||
'2xl': '1400px',
|
|
||||||
'3xl': '1600px',
|
|
||||||
},
|
|
||||||
colors: {
|
|
||||||
// Brand Colors
|
|
||||||
primary: {
|
|
||||||
DEFAULT: '#0e7a5c',
|
|
||||||
dark: '#0a5742',
|
|
||||||
light: '#149d76',
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
DEFAULT: '#194f3b',
|
|
||||||
light: '#20654c',
|
|
||||||
dark: '#13392a',
|
|
||||||
},
|
|
||||||
accent: {
|
|
||||||
DEFAULT: '#0e7a5c',
|
|
||||||
light: '#149d76',
|
|
||||||
},
|
|
||||||
neutral: {
|
|
||||||
DEFAULT: '#fafafa',
|
|
||||||
light: '#ffffff',
|
|
||||||
dark: '#333333',
|
|
||||||
black: '#1a1a1a',
|
|
||||||
},
|
|
||||||
// Text Colors
|
|
||||||
text: {
|
|
||||||
primary: '#1a1a1a',
|
|
||||||
secondary: '#6c757d',
|
|
||||||
light: '#adb5bd',
|
|
||||||
},
|
|
||||||
// Semantic Colors
|
|
||||||
success: '#28a745',
|
|
||||||
warning: '#ffc107',
|
|
||||||
danger: '#dc3545',
|
|
||||||
info: '#17a2b8',
|
|
||||||
},
|
|
||||||
fontFamily: {
|
|
||||||
sans: ['Inter', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial', 'sans-serif'],
|
|
||||||
heading: ['Inter', 'system-ui', 'sans-serif'],
|
|
||||||
body: ['Inter', 'system-ui', 'sans-serif'],
|
|
||||||
},
|
|
||||||
// Standard Fluid Typography
|
|
||||||
// Responsive scaling is handled globally via the html selector percentage in globals.css
|
|
||||||
fontSize: {
|
|
||||||
'xs': ['0.75rem', { lineHeight: '1.6' }],
|
|
||||||
'sm': ['0.875rem', { lineHeight: '1.6' }],
|
|
||||||
'base': ['1rem', { lineHeight: '1.7' }],
|
|
||||||
'lg': ['1.125rem', { lineHeight: '1.7' }],
|
|
||||||
'xl': ['1.25rem', { lineHeight: '1.6' }],
|
|
||||||
'2xl': ['1.5rem', { lineHeight: '1.5' }],
|
|
||||||
'3xl': ['1.875rem', { lineHeight: '1.4' }],
|
|
||||||
'4xl': ['2.25rem', { lineHeight: '1.3' }],
|
|
||||||
'5xl': ['3rem', { lineHeight: '1.25' }],
|
|
||||||
'6xl': ['3.75rem', { lineHeight: '1.2' }],
|
|
||||||
'7xl': ['4.5rem', { lineHeight: '1.15' }],
|
|
||||||
'8xl': ['6rem', { lineHeight: '1.1' }],
|
|
||||||
},
|
|
||||||
fontWeight: {
|
|
||||||
regular: '400',
|
|
||||||
medium: '500',
|
|
||||||
semibold: '600',
|
|
||||||
bold: '700',
|
|
||||||
extrabold: '800',
|
|
||||||
},
|
|
||||||
borderRadius: {
|
|
||||||
'sm': '0.25rem',
|
|
||||||
'DEFAULT': '0.5rem',
|
|
||||||
'lg': '0.75rem',
|
|
||||||
'xl': '1rem',
|
|
||||||
'2xl': '1.5rem',
|
|
||||||
},
|
|
||||||
boxShadow: {
|
|
||||||
'sm': '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
|
||||||
'DEFAULT': '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
|
|
||||||
'md': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
|
|
||||||
'lg': '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
|
|
||||||
'xl': '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
|
|
||||||
},
|
|
||||||
container: {
|
|
||||||
center: true,
|
|
||||||
padding: {
|
|
||||||
DEFAULT: '1rem',
|
|
||||||
sm: '1.5rem',
|
|
||||||
md: '2rem',
|
|
||||||
lg: '2.5rem',
|
|
||||||
xl: '3rem',
|
|
||||||
'2xl': '4rem',
|
|
||||||
},
|
|
||||||
screens: {
|
|
||||||
xs: '475px',
|
|
||||||
sm: '640px',
|
|
||||||
md: '768px',
|
|
||||||
lg: '1024px',
|
|
||||||
xl: '1280px',
|
|
||||||
'2xl': '1400px',
|
|
||||||
'3xl': '1600px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
maxWidth: {
|
|
||||||
'xs': '20rem',
|
|
||||||
'sm': '24rem',
|
|
||||||
'md': '28rem',
|
|
||||||
'lg': '32rem',
|
|
||||||
'xl': '36rem',
|
|
||||||
'2xl': '42rem',
|
|
||||||
'3xl': '48rem',
|
|
||||||
'4xl': '56rem',
|
|
||||||
'5xl': '64rem',
|
|
||||||
'6xl': '72rem',
|
|
||||||
},
|
|
||||||
zIndex: {
|
|
||||||
'0': '0',
|
|
||||||
'10': '10',
|
|
||||||
'20': '20',
|
|
||||||
'30': '30',
|
|
||||||
'40': '40',
|
|
||||||
'50': '50',
|
|
||||||
'auto': 'auto',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
// Custom plugin for responsive utilities
|
|
||||||
function ({ addUtilities }) {
|
|
||||||
const newUtilities = {
|
|
||||||
// Touch target utilities
|
|
||||||
'.touch-target': {
|
|
||||||
minHeight: '44px',
|
|
||||||
minWidth: '44px',
|
|
||||||
display: 'inline-flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
// Mobile-first responsive utilities
|
|
||||||
'.mobile-hidden': {
|
|
||||||
'@media (max-width: 767px)': {
|
|
||||||
display: 'none !important',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'.tablet-hidden': {
|
|
||||||
'@media (min-width: 768px) and (max-width: 1023px)': {
|
|
||||||
display: 'none !important',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'.desktop-hidden': {
|
|
||||||
'@media (min-width: 1024px)': {
|
|
||||||
display: 'none !important',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'.mobile-only': {
|
|
||||||
'@media (min-width: 768px)': {
|
|
||||||
display: 'none !important',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'.tablet-only': {
|
|
||||||
'@media (max-width: 767px), (min-width: 1024px)': {
|
|
||||||
display: 'none !important',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'.desktop-only': {
|
|
||||||
'@media (max-width: 1023px)': {
|
|
||||||
display: 'none !important',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Fluid spacing utilities
|
|
||||||
'.fluid-spacing': {
|
|
||||||
padding: 'clamp(1rem, 3vw, 3rem)',
|
|
||||||
margin: 'clamp(0.5rem, 2vw, 2rem)',
|
|
||||||
},
|
|
||||||
// Responsive text alignment
|
|
||||||
'.text-start-mobile': {
|
|
||||||
'@media (max-width: 767px)': {
|
|
||||||
textAlign: 'left',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'.text-center-mobile': {
|
|
||||||
'@media (max-width: 767px)': {
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'.text-end-mobile': {
|
|
||||||
'@media (max-width: 767px)': {
|
|
||||||
textAlign: 'right',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Mobile-optimized grid
|
|
||||||
'.grid-mobile-stacked': {
|
|
||||||
'@media (max-width: 767px)': {
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: '1rem',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Safe area padding for mobile notch
|
|
||||||
'.safe-area-p': {
|
|
||||||
paddingTop: 'env(safe-area-inset-top)',
|
|
||||||
paddingBottom: 'env(safe-area-inset-bottom)',
|
|
||||||
paddingLeft: 'env(safe-area-inset-left)',
|
|
||||||
paddingRight: 'env(safe-area-inset-right)',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
addUtilities(newUtilities, ['responsive', 'hover', 'focus']);
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
@@ -39,7 +39,7 @@ describe('Task 6 Content Updates', () => {
|
|||||||
|
|
||||||
// DE Home
|
// DE Home
|
||||||
expect(deContent).toContain('title: "Kabelleitungsnetzbau"');
|
expect(deContent).toContain('title: "Kabelleitungsnetzbau"');
|
||||||
expect(deContent).toContain('description: "Kabelleitungsnetzbau (Hoch-/Mittel- und Niederspannung) sowie Kabelmontagen bis 110 kV"');
|
expect(deContent).toContain('description: "Kabelleitungsnetzbau (Nieder-/Mittel- und Hochspannung bis 110 kV)"');
|
||||||
expect(deContent).toContain('100+');
|
expect(deContent).toContain('100+');
|
||||||
expect(deContent).toContain('Mitarbeitende');
|
expect(deContent).toContain('Mitarbeitende');
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ describe('Task 6 Content Updates', () => {
|
|||||||
expect(deContent).toContain('title: "Kabelleitungsnetzbau"');
|
expect(deContent).toContain('title: "Kabelleitungsnetzbau"');
|
||||||
expect(deContent).toContain('excerpt: "Professioneller Kabelleitungsnetzbau');
|
expect(deContent).toContain('excerpt: "Professioneller Kabelleitungsnetzbau');
|
||||||
expect(deContent).toContain('Kabelmontagen bis 110 kV');
|
expect(deContent).toContain('Kabelmontagen bis 110 kV');
|
||||||
expect(deContent).toContain('subtitle="Komplette Infrastruktur-Lösungen für Hoch-, Mittel- und Niederspannungsnetze sowie Kabelmontagen bis 110 kV"');
|
expect(deContent).toContain('subtitle="Komplette Infrastruktur-Lösungen für Nieder-, Mittel- und Hochspannungsnetze bis 110 kV"');
|
||||||
expect(deContent).toContain('"Kabelmontagen bis 110 kV"');
|
expect(deContent).toContain('"Kabelmontagen bis 110 kV"');
|
||||||
|
|
||||||
// EN Kabelnetzbau
|
// EN Kabelnetzbau
|
||||||
|
|||||||
Reference in New Issue
Block a user