Compare commits
2 Commits
feature/mo
...
2cb0b50193
| Author | SHA1 | Date | |
|---|---|---|---|
| 2cb0b50193 | |||
| 7d6a52e1c6 |
10
.npmrc
10
.npmrc
@@ -1,7 +1,7 @@
|
|||||||
@mintel:registry=https://npm.infra.mintel.me
|
@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm
|
||||||
//npm.infra.mintel.me/:_authToken=YTJlYzk0ZDk1MzRiMTRmNmQxZWNkMTBkNmFhNTg4Y2I6MWVkMjdjNzE3NDVkMzhmMmQ4MDEyODEwNmJjZmQyYTkwMjY3M2VjZA==
|
//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=3b9e7c4da038461efd0c6fd006d432d2ace5cb14
|
||||||
//npm.infra.mintel.me/:always-auth=true
|
|
||||||
//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=YTJlYzk0ZDk1MzRiMTRmNmQxZWNkMTBkNmFhNTg4Y2I6MWVkMjdjNzE3NDVkMzhmMmQ4MDEyODEwNmJjZmQyYTkwMjY3M2VjZA==
|
|
||||||
//git.infra.mintel.me/api/packages/mmintel/npm/:always-auth=true
|
//git.infra.mintel.me/api/packages/mmintel/npm/:always-auth=true
|
||||||
//registry.infra.mintel.me/api/packages/mmintel/npm/:_authToken=YTJlYzk0ZDk1MzRiMTRmNmQxZWNkMTBkNmFhNTg4Y2I6MWVkMjdjNzE3NDVkMzhmMmQ4MDEyODEwNmJjZmQyYTkwMjY3M2VjZA==
|
//registry.infra.mintel.me/api/packages/mmintel/npm/:_authToken=3b9e7c4da038461efd0c6fd006d432d2ace5cb14
|
||||||
//registry.infra.mintel.me/api/packages/mmintel/npm/:always-auth=true
|
//registry.infra.mintel.me/api/packages/mmintel/npm/:always-auth=true
|
||||||
|
//npm.infra.mintel.me/:_authToken=3b9e7c4da038461efd0c6fd006d432d2ace5cb14
|
||||||
|
//npm.infra.mintel.me/:always-auth=true
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export default async function Page(props: { params: Promise<{ locale: string; sl
|
|||||||
<Badge variant="accent" className="mb-4 md:mb-6">
|
<Badge variant="accent" className="mb-4 md:mb-6">
|
||||||
{t('badge')}
|
{t('badge')}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Heading level={1} className="text-white mb-0">
|
<Heading level={1} variant="white" className="mb-0">
|
||||||
{pageData.frontmatter.title}
|
{pageData.frontmatter.title}
|
||||||
</Heading>
|
</Heading>
|
||||||
</div>
|
</div>
|
||||||
@@ -139,7 +139,7 @@ export default async function Page(props: { params: Promise<{ locale: string; sl
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<div className="container mx-auto px-4 py-16 md:py-24">
|
<Container className="py-16 md:py-24">
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="max-w-4xl mx-auto">
|
||||||
{/* Excerpt/Lead paragraph if available */}
|
{/* Excerpt/Lead paragraph if available */}
|
||||||
{pageData.frontmatter.excerpt && (
|
{pageData.frontmatter.excerpt && (
|
||||||
@@ -180,7 +180,7 @@ export default async function Page(props: { params: Promise<{ locale: string; sl
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { cookies } from 'next/headers';
|
|||||||
import { TransitionProvider } from '@/components/providers/TransitionProvider';
|
import { TransitionProvider } from '@/components/providers/TransitionProvider';
|
||||||
import { PageTransitionShutter } from '@/components/providers/PageTransitionShutter';
|
import { PageTransitionShutter } from '@/components/providers/PageTransitionShutter';
|
||||||
import { InitialLoader } from '@/components/providers/InitialLoader';
|
import { InitialLoader } from '@/components/providers/InitialLoader';
|
||||||
|
import { CorporateBackground } from '@/components/decorations/CorporateBackground';
|
||||||
|
|
||||||
const inter = Inter({
|
const inter = Inter({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
@@ -175,9 +176,10 @@ export default async function Layout(props: {
|
|||||||
{/* Preload critical hero video */}
|
{/* Preload critical hero video */}
|
||||||
<link rel="preload" as="video" href="/assets/videos/web/hero-bohrung.mp4" type="video/mp4" />
|
<link rel="preload" as="video" href="/assets/videos/web/hero-bohrung.mp4" type="video/mp4" />
|
||||||
</head>
|
</head>
|
||||||
<body className="flex flex-col min-h-screen font-sans antialiased overflow-x-hidden selection:bg-primary/90 selection:text-white">
|
<body className="relative flex flex-col min-h-screen font-sans antialiased overflow-x-hidden selection:bg-primary/90 selection:text-white">
|
||||||
<NextIntlClientProvider messages={clientMessages} locale={safeLocale}>
|
<NextIntlClientProvider messages={clientMessages} locale={safeLocale}>
|
||||||
<TransitionProvider>
|
<TransitionProvider>
|
||||||
|
<CorporateBackground />
|
||||||
<InitialLoader shouldShowLoader={!hasSeenLoader} />
|
<InitialLoader shouldShowLoader={!hasSeenLoader} />
|
||||||
<PageTransitionShutter />
|
<PageTransitionShutter />
|
||||||
<SkipLink />
|
<SkipLink />
|
||||||
|
|||||||
19
app/[locale]/template.tsx
Normal file
19
app/[locale]/template.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
export default function Template({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20, filter: 'blur(10px)' }}
|
||||||
|
animate={{ opacity: 1, y: 0, filter: 'blur(0px)' }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.7,
|
||||||
|
ease: [0.16, 1, 0.3, 1],
|
||||||
|
}}
|
||||||
|
className="w-full h-full"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -15,6 +15,17 @@
|
|||||||
.hide-scrollbar::-webkit-scrollbar {
|
.hide-scrollbar::-webkit-scrollbar {
|
||||||
display: none; /* Chrome, Safari and Opera */
|
display: none; /* Chrome, Safari and Opera */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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 */
|
/* Base Styles */
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { motion, Variants } from 'framer-motion';
|
import { motion, Variants } from 'framer-motion';
|
||||||
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
||||||
|
import { LogoArcs } from '@/components/ui/LogoArcs';
|
||||||
|
|
||||||
export interface BenefitItem {
|
export interface BenefitItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -100,13 +101,32 @@ export function BenefitGrid({ badge, title, description, benefits }: BenefitGrid
|
|||||||
whileInView="visible"
|
whileInView="visible"
|
||||||
viewport={{ once: true, margin: "-50px" }}
|
viewport={{ once: true, margin: "-50px" }}
|
||||||
>
|
>
|
||||||
{benefits.map((benefit) => (
|
{benefits.map((benefit, index) => {
|
||||||
<motion.div
|
const bgPositionClasses = [
|
||||||
key={benefit.id}
|
"-bottom-[30%] -right-[30%]",
|
||||||
variants={itemVariants}
|
"-top-[30%] -left-[30%]",
|
||||||
className="group relative overflow-hidden bg-neutral-50/50 border border-neutral-200/60 rounded-3xl p-8 transition-all duration-500 hover:bg-white hover:border-primary/30 hover:shadow-[0_20px_40px_-15px_rgba(0,0,0,0.05)] hover:-translate-y-1"
|
"-bottom-[30%] -left-[30%]"
|
||||||
>
|
];
|
||||||
<HoverShineOverlay shineColor="via-primary/5" />
|
const animationClasses = [
|
||||||
|
"animate-[spin_60s_linear_infinite]",
|
||||||
|
"animate-[spin_80s_linear_infinite]",
|
||||||
|
"animate-[spin_100s_linear_infinite]"
|
||||||
|
];
|
||||||
|
const positionClass = bgPositionClasses[index % bgPositionClasses.length];
|
||||||
|
const animationClass = animationClasses[index % animationClasses.length];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={benefit.id}
|
||||||
|
variants={itemVariants}
|
||||||
|
className="group relative overflow-hidden bg-neutral-50/50 border border-neutral-200/60 rounded-3xl p-8 transition-all duration-500 hover:bg-white hover:border-primary/30 hover:shadow-[0_20px_40px_-15px_rgba(0,0,0,0.05)] hover:-translate-y-1"
|
||||||
|
>
|
||||||
|
<HoverShineOverlay shineColor="via-primary/5" />
|
||||||
|
|
||||||
|
{/* Subtle Background Logo Arcs */}
|
||||||
|
<div className={`absolute ${positionClass} w-[120%] h-[120%] z-0 opacity-0 group-hover:opacity-[0.03] transition-opacity duration-1000 pointer-events-none mix-blend-multiply`}>
|
||||||
|
<LogoArcs className={`w-full h-full text-primary ${animationClass}`} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="relative z-10 flex items-start gap-4">
|
<div className="relative z-10 flex items-start gap-4">
|
||||||
<div className="relative shrink-0 w-12 h-12 flex items-center justify-center bg-primary/10 text-primary rounded-xl transition-all duration-500 group-hover:bg-primary group-hover:text-white group-hover:scale-110 group-hover:-rotate-3">
|
<div className="relative shrink-0 w-12 h-12 flex items-center justify-center bg-primary/10 text-primary rounded-xl transition-all duration-500 group-hover:bg-primary group-hover:text-white group-hover:scale-110 group-hover:-rotate-3">
|
||||||
@@ -125,7 +145,8 @@ export function BenefitGrid({ badge, title, description, benefits }: BenefitGrid
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import { motion } 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';
|
||||||
|
import { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder';
|
||||||
|
import { Container } from '@/components/ui/Container';
|
||||||
|
import { LogoArcs } from '@/components/ui/LogoArcs';
|
||||||
|
|
||||||
interface CompetenceBentoGridProps {
|
interface CompetenceBentoGridProps {
|
||||||
badge?: string;
|
badge?: string;
|
||||||
@@ -58,7 +61,7 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
|
|||||||
if (!isMounted) {
|
if (!isMounted) {
|
||||||
return (
|
return (
|
||||||
<section className="py-24 bg-neutral text-neutral-dark overflow-hidden relative">
|
<section className="py-24 bg-neutral text-neutral-dark overflow-hidden relative">
|
||||||
<div className="container">
|
<Container>
|
||||||
<div className="flex flex-col md:flex-row justify-between items-end mb-12">
|
<div className="flex flex-col md:flex-row justify-between items-end mb-12">
|
||||||
<div className="max-w-2xl">
|
<div className="max-w-2xl">
|
||||||
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
|
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
|
||||||
@@ -68,20 +71,20 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 auto-rows-[220px]">
|
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 auto-rows-[220px]">
|
||||||
{/* Simplified static grid */}
|
{/* Simplified static grid */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Container>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-24 bg-neutral text-neutral-dark relative overflow-hidden">
|
<section className="py-24 bg-neutral text-neutral-dark relative overflow-hidden">
|
||||||
<div className="container relative z-10 px-4">
|
<Container className="relative z-10 px-0 md:px-12 lg:px-16">
|
||||||
<div className="flex flex-col md:flex-row justify-between items-end mb-12">
|
<div className="flex flex-col md:flex-row justify-between items-end mb-12 px-4 md:px-0">
|
||||||
<div className="max-w-2xl">
|
<div className="max-w-2xl">
|
||||||
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
|
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
|
||||||
<h3 className="font-heading text-4xl md:text-5xl font-extrabold text-neutral-dark mb-4">{title}</h3>
|
<h3 className="font-heading text-4xl md:text-5xl font-extrabold text-neutral-dark mb-4">{title}</h3>
|
||||||
</div>
|
</div>
|
||||||
<Button href={ctaHref} variant="ghost" className="mt-6 md:mt-0 bg-primary/5 hover:bg-primary/10">
|
<Button href={ctaHref} variant="ghost" className="mt-6 md:mt-0 bg-primary/5 hover:bg-primary/10 mx-4 md:mx-0">
|
||||||
{ctaLabel}
|
{ctaLabel}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 ml-2 transform group-hover:translate-x-1 transition-transform" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 ml-2 transform group-hover:translate-x-1 transition-transform" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -111,7 +114,14 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
|
|||||||
className={gridClasses}
|
className={gridClasses}
|
||||||
>
|
>
|
||||||
<div className="absolute top-0 right-0 w-32 h-32 bg-primary/30 blur-3xl rounded-full -mr-10 -mt-10 group-hover:bg-primary/50 transition-colors duration-500" />
|
<div className="absolute top-0 right-0 w-32 h-32 bg-primary/30 blur-3xl rounded-full -mr-10 -mt-10 group-hover:bg-primary/50 transition-colors duration-500" />
|
||||||
|
|
||||||
|
{/* Subtle Background Logo Arcs */}
|
||||||
|
<div className={`absolute -bottom-[30%] -right-[30%] w-[120%] h-[120%] z-0 opacity-10 group-hover:opacity-20 transition-opacity duration-1000 pointer-events-none mix-blend-overlay`}>
|
||||||
|
<LogoArcs className={`w-full h-full text-white animate-[spin_60s_linear_infinite]`} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<HoverShineOverlay shineColor="via-white/30" />
|
<HoverShineOverlay shineColor="via-white/30" />
|
||||||
|
<AnimatedGlossyBorder color="white" className="opacity-0 group-hover:opacity-100 transition-opacity duration-700" borderWidth={1} />
|
||||||
<h4 className="font-bold text-xl md:text-2xl relative z-10 leading-snug">{item.title}</h4>
|
<h4 className="font-bold text-xl md:text-2xl relative z-10 leading-snug">{item.title}</h4>
|
||||||
{item.description && <p className="text-white/60 text-sm mt-3 relative z-10">{item.description}</p>}
|
{item.description && <p className="text-white/60 text-sm mt-3 relative z-10">{item.description}</p>}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -146,6 +156,12 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<HoverShineOverlay />
|
<HoverShineOverlay />
|
||||||
|
<AnimatedGlossyBorder color="white" className="opacity-0 group-hover:opacity-100 transition-opacity duration-700" borderWidth={1} />
|
||||||
|
|
||||||
|
{/* Subtle Background Logo Arcs */}
|
||||||
|
<div className="absolute -top-[30%] -left-[30%] w-[120%] h-[120%] z-0 opacity-0 group-hover:opacity-[0.05] transition-opacity duration-1000 pointer-events-none mix-blend-overlay">
|
||||||
|
<LogoArcs className={`w-full h-full text-white animate-[spin_80s_linear_infinite]`} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="relative z-10 p-8 flex flex-col justify-end h-full text-white">
|
<div className="relative z-10 p-8 flex flex-col justify-end h-full text-white">
|
||||||
{item.tag && (
|
{item.tag && (
|
||||||
@@ -166,7 +182,7 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Container>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export function HeroVideo(props: HeroVideoProps) {
|
|||||||
|
|
||||||
{videoUrl && (
|
{videoUrl && (
|
||||||
<video
|
<video
|
||||||
|
key={videoUrl}
|
||||||
className="absolute inset-0 w-full h-full object-cover z-1 pointer-events-none filter contrast-125 saturate-110 brightness-90"
|
className="absolute inset-0 w-full h-full object-cover z-1 pointer-events-none filter contrast-125 saturate-110 brightness-90"
|
||||||
src={videoUrl}
|
src={videoUrl}
|
||||||
autoPlay
|
autoPlay
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import * as React from 'react';
|
|||||||
import { motion, Variants } from 'framer-motion';
|
import { motion, Variants } from 'framer-motion';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
||||||
|
import { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder';
|
||||||
|
|
||||||
interface SubCompanyTilesProps {
|
interface SubCompanyTilesProps {
|
||||||
badge?: string;
|
badge?: string;
|
||||||
@@ -49,6 +50,7 @@ const itemVariants: Variants = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { LogoArcs } from '@/components/ui/LogoArcs';
|
||||||
|
|
||||||
export function SubCompanyTiles(props: SubCompanyTilesProps) {
|
export function SubCompanyTiles(props: SubCompanyTilesProps) {
|
||||||
const { data } = props;
|
const { data } = props;
|
||||||
@@ -91,6 +93,19 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
|
|||||||
const isEtib = company.title.includes('E-TIB');
|
const isEtib = company.title.includes('E-TIB');
|
||||||
const isIng = company.title.includes('Ingenieur');
|
const isIng = company.title.includes('Ingenieur');
|
||||||
const isNemo = company.title.includes('NEMO');
|
const isNemo = company.title.includes('NEMO');
|
||||||
|
|
||||||
|
const bgPositionClasses = [
|
||||||
|
"-bottom-[30%] -right-[30%]",
|
||||||
|
"-top-[30%] -left-[30%]",
|
||||||
|
"-bottom-[30%] -left-[30%]"
|
||||||
|
];
|
||||||
|
const animationClasses = [
|
||||||
|
"animate-[spin_60s_linear_infinite]",
|
||||||
|
"animate-[spin_80s_linear_infinite]",
|
||||||
|
"animate-[spin_100s_linear_infinite]"
|
||||||
|
];
|
||||||
|
const positionClass = bgPositionClasses[index % bgPositionClasses.length];
|
||||||
|
const animationClass = animationClasses[index % animationClasses.length];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -126,10 +141,22 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
|
|||||||
? 'bg-gradient-to-t from-[#0a192f]/90 via-[#0a192f]/60 to-[#0a192f]/40'
|
? 'bg-gradient-to-t from-[#0a192f]/90 via-[#0a192f]/60 to-[#0a192f]/40'
|
||||||
: 'bg-gradient-to-t from-[#0a192f]/90 via-[#0a192f]/60 to-[#0a192f]/40 group-hover:from-primary/90 group-hover:to-primary/20'
|
: 'bg-gradient-to-t from-[#0a192f]/90 via-[#0a192f]/60 to-[#0a192f]/40 group-hover:from-primary/90 group-hover:to-primary/20'
|
||||||
}`} />
|
}`} />
|
||||||
|
|
||||||
|
{/* Subtle Background Logo Arcs */}
|
||||||
|
<div className={`absolute ${positionClass} w-[120%] h-[120%] z-10 opacity-[0.02] group-hover:opacity-[0.08] transition-opacity duration-1000 pointer-events-none mix-blend-overlay`}>
|
||||||
|
<LogoArcs className={`w-full h-full text-white ${animationClass}`} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Premium Shine Sweep Effect (Industrial Reflection) */}
|
{/* Premium Shine Sweep Effect (Industrial Reflection) */}
|
||||||
<HoverShineOverlay shineColor="via-white/30" />
|
<HoverShineOverlay shineColor="via-white/30" />
|
||||||
|
|
||||||
|
{/* Glossy Animated Border */}
|
||||||
|
<AnimatedGlossyBorder
|
||||||
|
color={isCurrent ? "primary" : "white"}
|
||||||
|
className={`transition-opacity duration-700 ${isCurrent ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'}`}
|
||||||
|
borderWidth={1}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Current Site Indicator Badge */}
|
{/* Current Site Indicator Badge */}
|
||||||
{isCurrent && (
|
{isCurrent && (
|
||||||
<div className="absolute top-6 right-6 z-20">
|
<div className="absolute top-6 right-6 z-20">
|
||||||
|
|||||||
188
components/decorations/CorporateBackground.tsx
Normal file
188
components/decorations/CorporateBackground.tsx
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useRef, useEffect } from 'react';
|
||||||
|
import { motion, useScroll, useTransform, useSpring } from 'framer-motion';
|
||||||
|
import { LogoArcs } from '@/components/ui/LogoArcs';
|
||||||
|
|
||||||
|
function MagneticRing({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
pullStrength = 30, // How strong the pull is (higher = moves closer to mouse)
|
||||||
|
pullRadius = 600 // Distance from center at which it starts pulling
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode,
|
||||||
|
className?: string,
|
||||||
|
pullStrength?: number,
|
||||||
|
pullRadius?: number
|
||||||
|
}) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Spring physics for extremely smooth, organic movement
|
||||||
|
const x = useSpring(0, { stiffness: 40, damping: 25, mass: 1 });
|
||||||
|
const y = useSpring(0, { stiffness: 40, damping: 25, mass: 1 });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Only run on client with mouse
|
||||||
|
if (window.matchMedia("(pointer: coarse)").matches) return;
|
||||||
|
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
if (!ref.current) 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 distY = e.clientY - centerY;
|
||||||
|
const distance = Math.sqrt(distX * Math.pow(distX, 2) + Math.pow(distY, 2)); // Fix: correct pythagoras
|
||||||
|
|
||||||
|
// Calculate actual distance correctly
|
||||||
|
const actualDistance = Math.sqrt(distX * distX + distY * distY);
|
||||||
|
|
||||||
|
if (actualDistance < pullRadius) {
|
||||||
|
// Exponential falloff for natural magnetic feel
|
||||||
|
const pullFactor = Math.pow(1 - (actualDistance / pullRadius), 2);
|
||||||
|
|
||||||
|
// Target offset (fraction of the distance based on strength)
|
||||||
|
x.set(distX * pullFactor * (pullStrength / 100));
|
||||||
|
y.set(distY * pullFactor * (pullStrength / 100));
|
||||||
|
} else {
|
||||||
|
// Return to origin smoothly
|
||||||
|
x.set(0);
|
||||||
|
y.set(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('mousemove', handleMouseMove);
|
||||||
|
return () => window.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
}, [x, y, pullRadius, pullStrength]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
ref={ref}
|
||||||
|
style={{ x, y }}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CorporateBackground() {
|
||||||
|
const { scrollYProgress } = useScroll();
|
||||||
|
|
||||||
|
// Stronger parallax offsets for visible movement
|
||||||
|
const y1 = useTransform(scrollYProgress, [0, 1], [0, -400]);
|
||||||
|
const y2 = useTransform(scrollYProgress, [0, 1], [0, 500]);
|
||||||
|
const y3 = useTransform(scrollYProgress, [0, 1], [0, -600]);
|
||||||
|
const y4 = useTransform(scrollYProgress, [0, 1], [0, 350]);
|
||||||
|
const y5 = useTransform(scrollYProgress, [0, 1], [0, -500]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0 z-[50] overflow-hidden pointer-events-none" aria-hidden="true">
|
||||||
|
{/* 1. Top Right - Medium, rotating moderately */}
|
||||||
|
<motion.div
|
||||||
|
style={{
|
||||||
|
y: y1,
|
||||||
|
animationDelay: '0s',
|
||||||
|
maskImage: 'radial-gradient(circle at center, black 10%, transparent 60%)',
|
||||||
|
WebkitMaskImage: 'radial-gradient(circle at center, black 10%, transparent 60%)'
|
||||||
|
}}
|
||||||
|
className="absolute top-[0%] right-0 translate-x-[40%] w-[400px] h-[400px] md:w-[700px] md:h-[700px] animate-bg-pulse-20"
|
||||||
|
>
|
||||||
|
<MagneticRing pullRadius={700} pullStrength={40} className="w-full h-full">
|
||||||
|
<motion.div
|
||||||
|
animate={{ rotate: 360 }}
|
||||||
|
transition={{ duration: 40, repeat: Infinity, ease: "linear" }}
|
||||||
|
className="w-full h-full text-primary"
|
||||||
|
>
|
||||||
|
<LogoArcs className="w-full h-full" />
|
||||||
|
</motion.div>
|
||||||
|
</MagneticRing>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* 2. Quarter way down left - Smaller, rotating fast reverse */}
|
||||||
|
<motion.div
|
||||||
|
style={{
|
||||||
|
y: y2,
|
||||||
|
animationDelay: '3s',
|
||||||
|
maskImage: 'radial-gradient(circle at center, black 15%, transparent 65%)',
|
||||||
|
WebkitMaskImage: 'radial-gradient(circle at center, black 15%, transparent 65%)'
|
||||||
|
}}
|
||||||
|
className="absolute top-[20%] left-0 -translate-x-[40%] w-[300px] h-[300px] md:w-[500px] md:h-[500px] animate-bg-pulse-15"
|
||||||
|
>
|
||||||
|
<MagneticRing pullRadius={500} pullStrength={50} className="w-full h-full">
|
||||||
|
<motion.div
|
||||||
|
animate={{ rotate: -360 }}
|
||||||
|
transition={{ duration: 30, repeat: Infinity, ease: "linear" }}
|
||||||
|
className="w-full h-full text-secondary"
|
||||||
|
>
|
||||||
|
<LogoArcs className="w-full h-full -rotate-45" />
|
||||||
|
</motion.div>
|
||||||
|
</MagneticRing>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* 3. Halfway down right - Medium, rotating slow */}
|
||||||
|
<motion.div
|
||||||
|
style={{
|
||||||
|
y: y3,
|
||||||
|
animationDelay: '6s',
|
||||||
|
maskImage: 'radial-gradient(circle at center, black 10%, transparent 60%)',
|
||||||
|
WebkitMaskImage: 'radial-gradient(circle at center, black 10%, transparent 60%)'
|
||||||
|
}}
|
||||||
|
className="absolute top-[45%] right-0 translate-x-[30%] w-[350px] h-[350px] md:w-[600px] md:h-[600px] animate-bg-pulse-15"
|
||||||
|
>
|
||||||
|
<MagneticRing pullRadius={600} pullStrength={35} className="w-full h-full">
|
||||||
|
<motion.div
|
||||||
|
animate={{ rotate: 360 }}
|
||||||
|
transition={{ duration: 60, repeat: Infinity, ease: "linear" }}
|
||||||
|
className="w-full h-full text-primary"
|
||||||
|
>
|
||||||
|
<LogoArcs className="w-full h-full rotate-90" />
|
||||||
|
</motion.div>
|
||||||
|
</MagneticRing>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* 4. Bottom left - Larger, rotating moderately */}
|
||||||
|
<motion.div
|
||||||
|
style={{
|
||||||
|
y: y4,
|
||||||
|
animationDelay: '9s',
|
||||||
|
maskImage: 'radial-gradient(circle at center, black 5%, transparent 60%)',
|
||||||
|
WebkitMaskImage: 'radial-gradient(circle at center, black 5%, transparent 60%)'
|
||||||
|
}}
|
||||||
|
className="absolute top-[70%] left-0 -translate-x-[40%] w-[500px] h-[500px] md:w-[800px] md:h-[800px] animate-bg-pulse-20"
|
||||||
|
>
|
||||||
|
<MagneticRing pullRadius={800} pullStrength={30} className="w-full h-full">
|
||||||
|
<motion.div
|
||||||
|
animate={{ rotate: -360 }}
|
||||||
|
transition={{ duration: 50, repeat: Infinity, ease: "linear" }}
|
||||||
|
className="w-full h-full text-secondary"
|
||||||
|
>
|
||||||
|
<LogoArcs className="w-full h-full rotate-180" />
|
||||||
|
</motion.div>
|
||||||
|
</MagneticRing>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* 5. Deep bottom right - Smallest, very fast */}
|
||||||
|
<motion.div
|
||||||
|
style={{
|
||||||
|
y: y5,
|
||||||
|
animationDelay: '12s',
|
||||||
|
maskImage: 'radial-gradient(circle at center, black 20%, transparent 70%)',
|
||||||
|
WebkitMaskImage: 'radial-gradient(circle at center, black 20%, transparent 70%)'
|
||||||
|
}}
|
||||||
|
className="absolute top-[88%] right-0 translate-x-[50%] w-[250px] h-[250px] md:w-[400px] md:h-[400px] animate-bg-pulse-15"
|
||||||
|
>
|
||||||
|
<MagneticRing pullRadius={400} pullStrength={60} className="w-full h-full">
|
||||||
|
<motion.div
|
||||||
|
animate={{ rotate: 360 }}
|
||||||
|
transition={{ duration: 25, repeat: Infinity, ease: "linear" }}
|
||||||
|
className="w-full h-full text-primary"
|
||||||
|
>
|
||||||
|
<LogoArcs className="w-full h-full -rotate-12" />
|
||||||
|
</motion.div>
|
||||||
|
</MagneticRing>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -71,7 +71,7 @@ export function Header({ navLinks }: HeaderProps) {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`relative overflow-hidden w-full max-w-[1400px] mx-auto transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)] flex items-center justify-between ${
|
className={`relative overflow-hidden w-full max-w-7xl mx-auto transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)] flex items-center justify-between ${
|
||||||
isSolidMode
|
isSolidMode
|
||||||
? 'rounded-full bg-white/50 backdrop-blur-xl shadow-[0_8px_32px_rgba(0,0,0,0.08)] border border-white/40 py-2.5 px-6 md:px-8'
|
? 'rounded-full bg-white/50 backdrop-blur-xl shadow-[0_8px_32px_rgba(0,0,0,0.08)] border border-white/40 py-2.5 px-6 md:px-8'
|
||||||
: 'rounded-none bg-transparent py-6 px-4 md:px-8 border border-transparent'
|
: 'rounded-none bg-transparent py-6 px-4 md:px-8 border border-transparent'
|
||||||
@@ -125,7 +125,7 @@ export function Header({ navLinks }: HeaderProps) {
|
|||||||
<TransitionLink
|
<TransitionLink
|
||||||
key={link.url}
|
key={link.url}
|
||||||
href={mappedUrl}
|
href={mappedUrl}
|
||||||
className={`relative font-bold text-xs lg:text-sm uppercase tracking-widest transition-colors duration-300 group py-2 ${
|
className={`relative font-bold text-xs lg:text-sm uppercase tracking-widest transition-colors duration-300 group py-2 select-none ${
|
||||||
isSolidMode
|
isSolidMode
|
||||||
? (isActive ? 'text-primary' : 'text-text-primary hover:text-primary')
|
? (isActive ? 'text-primary' : 'text-text-primary hover:text-primary')
|
||||||
: (isActive ? 'text-white' : 'text-white/80 hover:text-white')
|
: (isActive ? 'text-white' : 'text-white/80 hover:text-white')
|
||||||
@@ -162,7 +162,7 @@ export function Header({ navLinks }: HeaderProps) {
|
|||||||
{/* Contact Button Desktop Only */}
|
{/* Contact Button Desktop Only */}
|
||||||
<TransitionLink
|
<TransitionLink
|
||||||
href={`/${currentLocale}/${currentLocale === 'de' ? 'kontakt' : 'contact'}`}
|
href={`/${currentLocale}/${currentLocale === 'de' ? 'kontakt' : 'contact'}`}
|
||||||
className={`relative hidden md:inline-flex ml-2 px-7 py-2.5 rounded-full font-bold text-xs lg:text-sm uppercase tracking-widest transition-all duration-500 overflow-hidden group/cta isolate ${
|
className={`relative hidden md:inline-flex ml-2 px-7 py-2.5 rounded-full font-bold text-xs lg:text-sm uppercase tracking-widest transition-all duration-500 overflow-hidden group/cta isolate select-none ${
|
||||||
isSolidMode
|
isSolidMode
|
||||||
? 'bg-primary text-white shadow-lg shadow-primary/20 hover:shadow-primary/40 hover:-translate-y-0.5 border border-transparent'
|
? 'bg-primary text-white shadow-lg shadow-primary/20 hover:shadow-primary/40 hover:-translate-y-0.5 border border-transparent'
|
||||||
: 'bg-white/10 text-white backdrop-blur-md border border-white/30 hover:bg-white/20 hover:-translate-y-0.5 hover:shadow-[0_0_20px_rgba(255,255,255,0.2)]'
|
: 'bg-white/10 text-white backdrop-blur-md border border-white/30 hover:bg-white/20 hover:-translate-y-0.5 hover:shadow-[0_0_20px_rgba(255,255,255,0.2)]'
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export function LanguageSwitcher({ mobile = false, isSolidMode = false }: Langua
|
|||||||
key={loc.code}
|
key={loc.code}
|
||||||
href={getSwitchedUrl(loc.code)}
|
href={getSwitchedUrl(loc.code)}
|
||||||
transitionMessage={currentLocale === loc.code ? undefined : `LANG_SWITCH:${currentLocObj.flag}:${loc.flag}`}
|
transitionMessage={currentLocale === loc.code ? undefined : `LANG_SWITCH:${currentLocObj.flag}:${loc.flag}`}
|
||||||
className={`group relative overflow-hidden flex items-center gap-3 px-5 py-3 rounded-xl text-lg font-semibold transition-all duration-300 ${
|
className={`group relative overflow-hidden flex items-center gap-3 px-5 py-3 rounded-xl text-lg font-semibold transition-all duration-300 select-none ${
|
||||||
currentLocale === loc.code
|
currentLocale === loc.code
|
||||||
? 'bg-primary/10 text-primary border border-primary/20'
|
? 'bg-primary/10 text-primary border border-primary/20'
|
||||||
: 'text-text-secondary hover:bg-neutral-50 border border-transparent'
|
: 'text-text-secondary hover:bg-neutral-50 border border-transparent'
|
||||||
@@ -83,7 +83,7 @@ export function LanguageSwitcher({ mobile = false, isSolidMode = false }: Langua
|
|||||||
key={loc.code}
|
key={loc.code}
|
||||||
href={getSwitchedUrl(loc.code)}
|
href={getSwitchedUrl(loc.code)}
|
||||||
transitionMessage={isActive ? undefined : `LANG_SWITCH:${currentLocObj.flag}:${loc.flag}`}
|
transitionMessage={isActive ? undefined : `LANG_SWITCH:${currentLocObj.flag}:${loc.flag}`}
|
||||||
className={`relative z-10 flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-bold uppercase tracking-wider transition-all duration-300 ${linkClass}`}
|
className={`relative z-10 flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-bold uppercase tracking-wider transition-all duration-300 select-none ${linkClass}`}
|
||||||
aria-label={`Switch to ${loc.fullLabel}`}
|
aria-label={`Switch to ${loc.fullLabel}`}
|
||||||
>
|
>
|
||||||
<span className="text-sm" aria-hidden="true">{loc.flag}</span>
|
<span className="text-sm" aria-hidden="true">{loc.flag}</span>
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export function MobileBottomNav({ navLinks, currentLocale }: MobileBottomNavProp
|
|||||||
<TransitionLink
|
<TransitionLink
|
||||||
key={item.label}
|
key={item.label}
|
||||||
href={mappedUrl}
|
href={mappedUrl}
|
||||||
className={`relative flex flex-col items-center justify-center w-full min-h-[44px] py-1 transition-colors duration-300 ${
|
className={`relative flex flex-col items-center justify-center w-full min-h-[44px] py-1 transition-colors duration-300 select-none ${
|
||||||
isActive ? 'text-primary' : 'text-neutral-500 hover:text-neutral-900'
|
isActive ? 'text-primary' : 'text-neutral-500 hover:text-neutral-900'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react';
|
import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
interface TransitionContextType {
|
interface TransitionContextType {
|
||||||
isTransitioning: boolean;
|
isTransitioning: boolean;
|
||||||
@@ -15,13 +15,39 @@ 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 router = useRouter();
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
// We use a ref to track transition state without triggering the route change effect when it turns true
|
||||||
|
const isTransitioningRef = React.useRef(isTransitioning);
|
||||||
|
useEffect(() => {
|
||||||
|
isTransitioningRef.current = isTransitioning;
|
||||||
|
}, [isTransitioning]);
|
||||||
|
|
||||||
|
// Watch for route changes to complete the transition
|
||||||
|
useEffect(() => {
|
||||||
|
if (isTransitioningRef.current) {
|
||||||
|
// The route has actually changed in the browser, or at least the RSC payload arrived
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setIsTransitioning(false);
|
||||||
|
setTimeout(() => setTransitionMessage(null), 300);
|
||||||
|
}, 100);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [pathname, searchParams]); // Run when pathname or search parameters change
|
||||||
|
|
||||||
const startTransition = useCallback((href: string, message: string | null = null) => {
|
const startTransition = useCallback((href: string, message: string | null = null) => {
|
||||||
// Wenn wir bereits navigieren, nichts tun
|
// Wenn wir bereits navigieren, nichts tun
|
||||||
if (isTransitioning) return;
|
if (isTransitioning) return;
|
||||||
|
|
||||||
|
// Normalize paths to avoid false mismatches (e.g. /de vs /de/)
|
||||||
|
const currentPath = typeof window !== 'undefined' ? window.location.pathname.replace(/\/$/, '') || '/' : '';
|
||||||
|
const targetPath = href.split('?')[0].replace(/\/$/, '') || '/';
|
||||||
|
const currentSearch = typeof window !== 'undefined' ? window.location.search : '';
|
||||||
|
const targetSearch = href.includes('?') ? '?' + href.split('?')[1] : '';
|
||||||
|
|
||||||
// Aktuelle URL prüfen, falls es dieselbe ist, nicht navigieren
|
// Aktuelle URL prüfen, falls es dieselbe ist, nicht navigieren
|
||||||
if (typeof window !== 'undefined' && window.location.pathname === href) {
|
if (currentPath === targetPath && currentSearch === targetSearch) {
|
||||||
// Optional: Wir könnten weich nach oben scrollen
|
// Optional: Wir könnten weich nach oben scrollen
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
return;
|
return;
|
||||||
@@ -34,13 +60,16 @@ export function TransitionProvider({ children }: { children: ReactNode }) {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
router.push(href);
|
router.push(href);
|
||||||
|
|
||||||
// Den Shutter wieder einfahren lassen, nachdem die Seite gewechselt hat
|
// Fallback: If for some reason the route doesn't change after 5 seconds, open the shutter anyway
|
||||||
// Next.js Route-Changes sind fast sofort da, aber wir geben dem Cache kurz Zeit
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsTransitioning(false);
|
setIsTransitioning((current) => {
|
||||||
// Message etwas verzögert zurücksetzen, damit es während dem Ausblenden nicht verschwindet
|
if (current) {
|
||||||
setTimeout(() => setTransitionMessage(null), 300);
|
setTimeout(() => setTransitionMessage(null), 300);
|
||||||
}, 300);
|
return false;
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
}, 700);
|
}, 700);
|
||||||
}, [isTransitioning, router]);
|
}, [isTransitioning, router]);
|
||||||
|
|
||||||
|
|||||||
46
components/ui/AnimatedGlossyBorder.tsx
Normal file
46
components/ui/AnimatedGlossyBorder.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cn } from './utils';
|
||||||
|
|
||||||
|
interface AnimatedGlossyBorderProps {
|
||||||
|
className?: string;
|
||||||
|
borderWidth?: 1 | 2;
|
||||||
|
opacity?: number;
|
||||||
|
color?: 'white' | 'primary';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AnimatedGlossyBorder({
|
||||||
|
className,
|
||||||
|
borderWidth = 1,
|
||||||
|
color = 'white'
|
||||||
|
}: AnimatedGlossyBorderProps) {
|
||||||
|
const isWhite = color === 'white';
|
||||||
|
const glowColor = isWhite ? 'rgba(255,255,255,0.6)' : 'rgba(17,124,97,0.6)';
|
||||||
|
|
||||||
|
const gradientColors = isWhite
|
||||||
|
? 'transparent 0%, rgba(255,255,255,0.1) 15%, rgba(255,255,255,0.85) 25%, rgba(255,255,255,0.1) 35%, transparent 50%, transparent 50%, rgba(255,255,255,0.1) 65%, rgba(255,255,255,0.85) 75%, rgba(255,255,255,0.1) 85%, transparent 100%'
|
||||||
|
: 'transparent 0%, rgba(17,124,97,0.1) 15%, rgba(17,124,97,0.85) 25%, rgba(17,124,97,0.1) 35%, transparent 50%, transparent 50%, rgba(17,124,97,0.1) 65%, rgba(17,124,97,0.85) 75%, rgba(17,124,97,0.1) 85%, transparent 100%';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn('absolute inset-0 pointer-events-none rounded-[inherit] z-20 mix-blend-overlay', className)}
|
||||||
|
style={{ filter: `drop-shadow(0 0 6px ${glowColor}) drop-shadow(0 0 2px ${glowColor})` }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 rounded-[inherit]"
|
||||||
|
style={{
|
||||||
|
border: `${borderWidth}px solid transparent`,
|
||||||
|
WebkitMask: 'linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0)',
|
||||||
|
WebkitMaskComposite: 'xor',
|
||||||
|
maskComposite: 'exclude'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[250%] aspect-square animate-[spin_8s_linear_infinite]"
|
||||||
|
style={{
|
||||||
|
background: `conic-gradient(from 0deg, ${gradientColors})`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ export function Badge({ children, className, variant = 'primary' }: { children:
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={cn('inline-flex items-center px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider', variants[variant], className)}>
|
<span className={cn('inline-flex items-center px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider select-none', variants[variant], className)}>
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TransitionLink } from '@/components/ui/TransitionLink';
|
import { TransitionLink } from '@/components/ui/TransitionLink';
|
||||||
|
import { AnimatedGlossyBorder } from '@/components/ui/AnimatedGlossyBorder';
|
||||||
import { cn } from './utils';
|
import { cn } from './utils';
|
||||||
|
|
||||||
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
@@ -19,7 +20,7 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const buttonBaseStyles =
|
export const buttonBaseStyles =
|
||||||
'inline-flex items-center justify-center whitespace-nowrap rounded-full font-semibold transition-all duration-500 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none active:scale-95 hover:-translate-y-1 hover:scale-[1.02] relative overflow-hidden group/btn isolate';
|
'inline-flex items-center justify-center whitespace-nowrap rounded-full font-semibold transition-all duration-500 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none active:scale-95 hover:-translate-y-1 hover:scale-[1.02] relative overflow-hidden group/btn isolate select-none';
|
||||||
|
|
||||||
export const buttonVariantsMap = {
|
export const buttonVariantsMap = {
|
||||||
primary: 'bg-primary text-white shadow-md hover:shadow-primary/30 hover:shadow-2xl',
|
primary: 'bg-primary text-white shadow-md hover:shadow-primary/30 hover:shadow-2xl',
|
||||||
@@ -58,13 +59,23 @@ export function getButtonClasses(variant: keyof typeof buttonVariantsMap = 'prim
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ButtonOverlay({ variant = 'primary' }: { variant?: keyof typeof buttonVariantsMap }) {
|
export function ButtonOverlay({ variant = 'primary' }: { variant?: keyof typeof buttonVariantsMap }) {
|
||||||
|
const isWhiteBg = variant === 'white' || variant === 'outline' || variant === 'ghost';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<>
|
||||||
className={cn(
|
<span
|
||||||
'absolute top-0 -left-[150%] h-[200%] w-[150%] bg-gradient-to-r from-transparent to-transparent skew-x-[-20deg] group-hover/btn:left-[100%] transition-all duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] z-30 pointer-events-none',
|
className={cn(
|
||||||
buttonShineColorsMap[variant]
|
'absolute top-0 -left-[150%] h-[200%] w-[150%] bg-gradient-to-r from-transparent to-transparent skew-x-[-20deg] group-hover/btn:left-[100%] transition-all duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] z-30 pointer-events-none',
|
||||||
|
buttonShineColorsMap[variant]
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{!isWhiteBg && (
|
||||||
|
<AnimatedGlossyBorder
|
||||||
|
color="white"
|
||||||
|
className="opacity-40 group-hover/btn:opacity-100 transition-opacity duration-700"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export function Label({ className, ...props }: LabelProps) {
|
|||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-[10px] md:text-xs font-extrabold text-primary uppercase tracking-widest',
|
'text-[10px] md:text-xs font-extrabold text-primary uppercase tracking-widest select-none',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
20
components/ui/LogoArcs.tsx
Normal file
20
components/ui/LogoArcs.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
// Reusable SVG component that precisely mimics the overlapping, sweeping arcs of the E-TIB logo.
|
||||||
|
// The circles are offset along a diagonal to bunch together at the bottom-left and spread apart at the top-right.
|
||||||
|
export const LogoArcs = ({ className }: { className?: string }) => (
|
||||||
|
<svg
|
||||||
|
className={className}
|
||||||
|
viewBox="0 0 200 200"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
{/* Outer ring */}
|
||||||
|
<circle cx="100" cy="100" r="90" strokeWidth="1.5" />
|
||||||
|
{/* Middle thicker ring */}
|
||||||
|
<circle cx="92" cy="108" r="78" strokeWidth="3.5" />
|
||||||
|
{/* Inner ring */}
|
||||||
|
<circle cx="84" cy="116" r="66" strokeWidth="1.5" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user