wip
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import { MDXRemote } from 'next-mdx-remote/rsc';
|
||||
import { getPostBySlug, getAdjacentPosts } from '@/lib/blog';
|
||||
import { getPostBySlug, getAdjacentPosts, getReadingTime, getHeadings } from '@/lib/blog';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
interface BlogPostProps {
|
||||
@@ -46,7 +46,7 @@ import ChatBubble from '@/components/blog/ChatBubble';
|
||||
import SplitHeading from '@/components/blog/SplitHeading';
|
||||
import PostNavigation from '@/components/blog/PostNavigation';
|
||||
import PowerCTA from '@/components/blog/PowerCTA';
|
||||
import ShareButton from '@/components/blog/ShareButton';
|
||||
import TableOfContents from '@/components/blog/TableOfContents';
|
||||
|
||||
const components = {
|
||||
VisualLinkPreview,
|
||||
@@ -57,6 +57,7 @@ const components = {
|
||||
ChatBubble,
|
||||
PowerCTA,
|
||||
SplitHeading,
|
||||
h1: () => null,
|
||||
a: ({ href, children, ...props }: any) => {
|
||||
if (href?.startsWith('/')) {
|
||||
return (
|
||||
@@ -182,77 +183,63 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
|
||||
notFound();
|
||||
}
|
||||
|
||||
const headings = getHeadings(post.content);
|
||||
|
||||
return (
|
||||
<article className="bg-white min-h-screen font-sans">
|
||||
<article className="bg-white min-h-screen font-sans selection:bg-primary/10 selection:text-primary">
|
||||
|
||||
{/* Featured Image Header */}
|
||||
{post.frontmatter.featuredImage && (
|
||||
<div className="relative w-full h-[60vh] min-h-[400px] overflow-hidden group">
|
||||
{post.frontmatter.featuredImage ? (
|
||||
<div className="relative w-full h-[70vh] min-h-[500px] overflow-hidden group">
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center transition-transform duration-[2s] ease-out scale-105 group-hover:scale-100"
|
||||
className="absolute inset-0 bg-cover bg-center transition-transform duration-[3s] ease-out scale-110 group-hover:scale-100"
|
||||
style={{ backgroundImage: `url(${post.frontmatter.featuredImage})` }}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-neutral-dark via-neutral-dark/40 to-transparent" />
|
||||
|
||||
{/* Title overlay on image */}
|
||||
<div className="absolute inset-0 flex flex-col justify-end p-8 md:p-16 lg:p-24">
|
||||
<div className="container mx-auto max-w-3xl">
|
||||
{post.frontmatter.category && (
|
||||
<span className="inline-block px-4 py-1.5 bg-primary/90 backdrop-blur-sm text-white text-sm font-bold uppercase tracking-wider rounded-full mb-6 shadow-lg transform transition-transform hover:scale-105">
|
||||
{post.frontmatter.category}
|
||||
</span>
|
||||
)}
|
||||
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-white mb-6 leading-tight drop-shadow-xl">
|
||||
{post.frontmatter.title}
|
||||
</h1>
|
||||
<div className="flex items-center gap-4 text-white/90 text-sm md:text-base font-medium">
|
||||
<time dateTime={post.frontmatter.date}>
|
||||
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</time>
|
||||
<span className="w-1.5 h-1.5 bg-primary rounded-full" />
|
||||
<span>KLZ Cables</span>
|
||||
<div className="ml-auto hidden md:block">
|
||||
<ShareButton
|
||||
title={post.frontmatter.title}
|
||||
text={post.frontmatter.excerpt || ''}
|
||||
url={`https://klz-cables.com/${locale}/blog/${slug}`}
|
||||
locale={locale}
|
||||
/>
|
||||
<div className="absolute inset-0 flex flex-col justify-end pb-16 md:pb-24">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="max-w-4xl">
|
||||
{post.frontmatter.category && (
|
||||
<div className="overflow-hidden mb-6">
|
||||
<span className="inline-block px-4 py-1.5 bg-accent text-neutral-dark text-xs font-bold uppercase tracking-[0.2em] rounded-sm animate-slight-fade-in-from-bottom">
|
||||
{post.frontmatter.category}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<h1 className="text-4xl md:text-6xl lg:text-7xl font-bold text-white mb-8 leading-[1.1] drop-shadow-2xl animate-slight-fade-in-from-bottom [animation-delay:200ms]">
|
||||
{post.frontmatter.title}
|
||||
</h1>
|
||||
<div className="flex flex-wrap items-center gap-6 text-white/80 text-sm md:text-base font-medium animate-slight-fade-in-from-bottom [animation-delay:400ms]">
|
||||
<time dateTime={post.frontmatter.date}>
|
||||
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</time>
|
||||
<span className="w-1 h-1 bg-white/30 rounded-full" />
|
||||
<span>{getReadingTime(post.content)} min read</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="container mx-auto px-4 py-16 md:py-24 max-w-3xl">
|
||||
{/* Mobile Share Button */}
|
||||
<div className="md:hidden mb-8 flex justify-end">
|
||||
<ShareButton
|
||||
title={post.frontmatter.title}
|
||||
text={post.frontmatter.excerpt || ''}
|
||||
url={`https://klz-cables.com/${locale}/blog/${slug}`}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
{/* If no featured image, show header here */}
|
||||
{!post.frontmatter.featuredImage && (
|
||||
<header className="mb-16 text-center">
|
||||
) : (
|
||||
<header className="pt-32 pb-16 bg-neutral-50 border-b border-neutral-100">
|
||||
<div className="container mx-auto px-4 max-w-4xl">
|
||||
{post.frontmatter.category && (
|
||||
<div className="mb-6">
|
||||
<span className="inline-block px-4 py-1.5 bg-primary/10 text-primary text-sm font-bold uppercase tracking-wider rounded-full">
|
||||
<span className="inline-block px-4 py-1.5 bg-primary/10 text-primary text-xs font-bold uppercase tracking-[0.2em] rounded-sm">
|
||||
{post.frontmatter.category}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<h1 className="text-4xl md:text-5xl lg:text-6xl font-extrabold text-text-primary mb-8 leading-tight">
|
||||
<h1 className="text-4xl md:text-6xl font-bold text-text-primary mb-8 leading-tight">
|
||||
{post.frontmatter.title}
|
||||
</h1>
|
||||
<div className="flex items-center justify-center gap-4 text-text-secondary font-medium">
|
||||
<div className="flex items-center gap-6 text-text-secondary font-medium">
|
||||
<time dateTime={post.frontmatter.date}>
|
||||
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
|
||||
year: 'numeric',
|
||||
@@ -260,75 +247,92 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
|
||||
day: 'numeric'
|
||||
})}
|
||||
</time>
|
||||
<span className="w-1.5 h-1.5 bg-primary rounded-full" />
|
||||
<span>KLZ Cables</span>
|
||||
<span className="w-1 h-1 bg-neutral-300 rounded-full" />
|
||||
<span>{getReadingTime(post.content)} min read</span>
|
||||
</div>
|
||||
</header>
|
||||
)}
|
||||
|
||||
{/* Excerpt/Lead paragraph if available */}
|
||||
{post.frontmatter.excerpt && (
|
||||
<div className="mb-16">
|
||||
<p className="text-xl md:text-2xl text-text-primary leading-relaxed font-medium border-l-4 border-primary pl-6 py-2">
|
||||
{post.frontmatter.excerpt}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
)}
|
||||
|
||||
{/* Main content with enhanced styling */}
|
||||
<div className="prose prose-lg md:prose-xl max-w-none prose-headings:font-bold prose-headings:text-text-primary prose-p:text-text-secondary prose-p:leading-relaxed prose-a:text-primary prose-a:no-underline hover:prose-a:underline prose-img:rounded-xl prose-img:shadow-lg">
|
||||
<MDXRemote source={post.content} components={components} />
|
||||
</div>
|
||||
{/* Main Content Area with Sticky Narrative Layout */}
|
||||
<div className="container mx-auto px-4 py-16 md:py-24">
|
||||
<div className="sticky-narrative-container">
|
||||
{/* Left Column: Content */}
|
||||
<div className="sticky-narrative-content">
|
||||
{/* Excerpt/Lead paragraph if available */}
|
||||
{post.frontmatter.excerpt && (
|
||||
<div className="mb-16 animate-slight-fade-in-from-bottom [animation-delay:600ms]">
|
||||
<p className="text-xl md:text-2xl text-text-primary leading-relaxed font-medium border-l-4 border-primary pl-8 py-2 italic">
|
||||
{post.frontmatter.excerpt}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Structured Data */}
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BlogPosting',
|
||||
headline: post.frontmatter.title,
|
||||
datePublished: post.frontmatter.date,
|
||||
image: post.frontmatter.featuredImage ? `https://klz-cables.com${post.frontmatter.featuredImage}` : undefined,
|
||||
author: {
|
||||
'@type': 'Organization',
|
||||
name: 'KLZ Cables',
|
||||
url: 'https://klz-cables.com',
|
||||
},
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
name: 'KLZ Cables',
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: 'https://klz-cables.com/logo.png', // Assuming logo exists
|
||||
},
|
||||
},
|
||||
description: post.frontmatter.excerpt,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
{/* Main content with enhanced styling */}
|
||||
<div className="prose prose-lg md:prose-xl max-w-none prose-headings:font-bold prose-headings:text-text-primary prose-p:text-text-secondary prose-p:leading-relaxed prose-a:text-primary prose-a:no-underline hover:prose-a:underline prose-img:rounded-2xl prose-img:shadow-2xl prose-blockquote:border-primary prose-blockquote:bg-primary/5 prose-blockquote:rounded-r-2xl prose-strong:text-primary animate-slight-fade-in-from-bottom [animation-delay:800ms]">
|
||||
<MDXRemote source={post.content} components={components} />
|
||||
</div>
|
||||
|
||||
{/* Power CTA */}
|
||||
<div className="mt-20">
|
||||
<PowerCTA locale={locale} />
|
||||
</div>
|
||||
{/* Power CTA */}
|
||||
<div className="mt-24 animate-slight-fade-in-from-bottom">
|
||||
<PowerCTA locale={locale} />
|
||||
</div>
|
||||
|
||||
{/* Post Navigation */}
|
||||
<PostNavigation prev={prev} next={next} locale={locale} />
|
||||
{/* Post Navigation */}
|
||||
<div className="mt-16">
|
||||
<PostNavigation prev={prev} next={next} locale={locale} />
|
||||
</div>
|
||||
|
||||
{/* Back to blog link */}
|
||||
<div className="mt-16 pt-10 border-t border-neutral-200 text-center">
|
||||
<Link
|
||||
href={`/${locale}/blog`}
|
||||
className="inline-flex items-center gap-2 text-text-secondary hover:text-primary font-medium text-lg transition-colors group"
|
||||
>
|
||||
<svg className="w-5 h-5 transition-transform group-hover:-translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
{locale === 'de' ? 'Zurück zur Übersicht' : 'Back to Overview'}
|
||||
</Link>
|
||||
{/* Back to blog link */}
|
||||
<div className="mt-16 pt-10 border-t border-neutral-100">
|
||||
<Link
|
||||
href={`/${locale}/blog`}
|
||||
className="inline-flex items-center gap-3 text-text-secondary hover:text-primary font-bold text-sm uppercase tracking-widest transition-all group"
|
||||
>
|
||||
<svg className="w-5 h-5 transition-transform group-hover:-translate-x-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
{locale === 'de' ? 'Zurück zur Übersicht' : 'Back to Overview'}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Sticky Sidebar */}
|
||||
<aside className="sticky-narrative-sidebar hidden lg:block">
|
||||
<div className="space-y-12">
|
||||
<TableOfContents headings={headings} locale={locale} />
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Structured Data */}
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BlogPosting',
|
||||
headline: post.frontmatter.title,
|
||||
datePublished: post.frontmatter.date,
|
||||
image: post.frontmatter.featuredImage ? `https://klz-cables.com${post.frontmatter.featuredImage}` : undefined,
|
||||
author: {
|
||||
'@type': 'Organization',
|
||||
name: 'KLZ Cables',
|
||||
url: 'https://klz-cables.com',
|
||||
},
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
name: 'KLZ Cables',
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: 'https://klz-cables.com/logo.png',
|
||||
},
|
||||
},
|
||||
description: post.frontmatter.excerpt,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function Footer() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<footer className="bg-primary-dark text-white py-24 relative overflow-hidden">
|
||||
<footer className="bg-primary text-white py-24 relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-white/20 to-transparent" />
|
||||
|
||||
<Container>
|
||||
|
||||
@@ -147,7 +147,7 @@ export default function Header() {
|
||||
|
||||
{/* Mobile Menu Overlay */}
|
||||
<div className={cn(
|
||||
"fixed inset-0 bg-primary-dark z-40 lg:hidden transition-all duration-500 ease-in-out flex flex-col",
|
||||
"fixed inset-0 bg-primary z-40 lg:hidden transition-all duration-500 ease-in-out flex flex-col",
|
||||
isMobileMenuOpen ? "opacity-100 translate-y-0" : "opacity-0 -translate-y-full pointer-events-none"
|
||||
)}>
|
||||
<div className="flex-grow flex flex-col justify-center items-center p-8 space-y-8">
|
||||
|
||||
@@ -47,24 +47,36 @@ export default function AnimatedImage({
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`relative overflow-hidden rounded-xl shadow-lg my-12 ${className}`}
|
||||
className={`relative overflow-hidden rounded-2xl shadow-2xl my-16 group ${className}`}
|
||||
>
|
||||
<div className={`
|
||||
absolute inset-0 bg-primary/10 z-10 pointer-events-none transition-opacity duration-1000
|
||||
${isLoaded && isInView ? 'opacity-0' : 'opacity-100'}
|
||||
`} />
|
||||
|
||||
<Image
|
||||
src={src}
|
||||
alt={alt}
|
||||
width={width}
|
||||
height={height}
|
||||
className={`
|
||||
duration-1000 ease-out w-full h-auto
|
||||
${isLoaded && isInView ? 'scale-100 blur-0 opacity-100' : 'scale-110 blur-xl opacity-0'}
|
||||
duration-[1.5s] ease-out w-full h-auto transition-all
|
||||
${isLoaded && isInView ? 'scale-100 blur-0 opacity-100' : 'scale-110 blur-2xl opacity-0'}
|
||||
group-hover:scale-105
|
||||
`}
|
||||
onLoad={() => setIsLoaded(true)}
|
||||
priority={priority}
|
||||
/>
|
||||
|
||||
{/* Subtle reflection overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-tr from-white/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none" />
|
||||
|
||||
{alt && (
|
||||
<p className="text-sm text-text-secondary text-center mt-3 italic px-4 pb-4">
|
||||
{alt}
|
||||
</p>
|
||||
<div className="absolute bottom-0 left-0 right-0 p-6 bg-gradient-to-t from-black/60 to-transparent translate-y-full group-hover:translate-y-0 transition-transform duration-500">
|
||||
<p className="text-sm text-white font-medium italic">
|
||||
{alt}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -46,15 +46,18 @@ export default function ChatBubble({
|
||||
</div>
|
||||
|
||||
<div className={`
|
||||
relative px-6 py-4 rounded-2xl shadow-sm border
|
||||
relative px-8 py-6 rounded-3xl shadow-sm border transition-all duration-300 hover:shadow-md
|
||||
${isRight
|
||||
? 'bg-primary text-white rounded-tr-none border-primary'
|
||||
? 'bg-neutral-dark text-white rounded-tr-none border-neutral-dark'
|
||||
: 'bg-white text-text-primary rounded-tl-none border-neutral-200'
|
||||
}
|
||||
`}>
|
||||
<div className={`prose prose-sm max-w-none ${isRight ? 'prose-invert' : ''}`}>
|
||||
<div className={`prose prose-lg max-w-none ${isRight ? 'prose-invert' : ''}`}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{/* Industrial accent dot */}
|
||||
<div className={`absolute top-4 ${isRight ? 'left-4' : 'right-4'} w-1.5 h-1.5 rounded-full ${isRight ? 'bg-primary' : 'bg-primary/30'}`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import Scribble from '@/components/Scribble';
|
||||
|
||||
interface HighlightBoxProps {
|
||||
title?: string;
|
||||
@@ -7,21 +8,31 @@ interface HighlightBoxProps {
|
||||
}
|
||||
|
||||
const colorStyles = {
|
||||
primary: 'bg-gradient-to-br from-primary/10 to-primary/5 border-primary/30',
|
||||
secondary: 'bg-gradient-to-br from-blue-50 to-blue-100/50 border-blue-200',
|
||||
accent: 'bg-gradient-to-br from-green-50 to-green-100/50 border-green-200',
|
||||
primary: 'bg-gradient-to-br from-primary/5 to-transparent border-primary/20',
|
||||
secondary: 'bg-gradient-to-br from-blue-50/50 to-transparent border-blue-200/50',
|
||||
accent: 'bg-gradient-to-br from-accent/5 to-transparent border-accent/20',
|
||||
};
|
||||
|
||||
export default function HighlightBox({ title, children, color = 'primary' }: HighlightBoxProps) {
|
||||
return (
|
||||
<div className={`my-10 p-8 rounded-2xl border-2 ${colorStyles[color]} shadow-sm`}>
|
||||
<div className={`my-12 p-8 md:p-10 rounded-3xl border ${colorStyles[color]} shadow-sm relative overflow-hidden group`}>
|
||||
{/* Industrial accent corner */}
|
||||
<div className="absolute top-0 right-0 w-16 h-16 bg-primary/5 -mr-8 -mt-8 rotate-45 transition-transform group-hover:scale-110" />
|
||||
|
||||
{title && (
|
||||
<h3 className="text-2xl font-bold text-text-primary mb-4 flex items-center gap-3">
|
||||
<span className="w-1.5 h-8 bg-primary rounded-full"></span>
|
||||
{title}
|
||||
<h3 className="text-2xl font-bold text-text-primary mb-6 flex items-center gap-4 relative">
|
||||
<span className="relative">
|
||||
{title}
|
||||
{color === 'accent' && (
|
||||
<Scribble
|
||||
variant="underline"
|
||||
className="absolute -bottom-2 left-0 w-full h-3 text-accent/40"
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</h3>
|
||||
)}
|
||||
<div className="prose prose-lg max-w-none">
|
||||
<div className="prose prose-lg max-w-none relative z-10">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,47 +9,63 @@ export default function PowerCTA({ locale }: PowerCTAProps) {
|
||||
const isDe = locale === 'de';
|
||||
|
||||
return (
|
||||
<div className="my-12 p-8 md:p-10 bg-white rounded-xl shadow-lg border border-neutral-100 relative overflow-hidden">
|
||||
{/* Decorative background element */}
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-primary/5 rounded-bl-full -mr-10 -mt-10" />
|
||||
<div className="my-16 p-10 md:p-16 bg-neutral-dark rounded-[2rem] shadow-2xl relative overflow-hidden group">
|
||||
{/* Industrial background pattern */}
|
||||
<div className="absolute inset-0 opacity-10 pointer-events-none">
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(#3b82f6_1px,transparent_1px)] [background-size:40px_40px]" />
|
||||
</div>
|
||||
|
||||
{/* Decorative accent */}
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-primary/20 rounded-full blur-3xl -mr-32 -mt-32 transition-transform group-hover:scale-110 duration-1000" />
|
||||
|
||||
<div className="relative z-10">
|
||||
<h3 className="text-2xl font-bold text-text-primary mb-4 flex items-center gap-3">
|
||||
<span className="text-3xl">💡</span>
|
||||
{isDe ? 'Benötigen Sie Strom?' : 'Need power?'}
|
||||
<span className="text-primary">{isDe ? 'Wir sind für Sie da!' : "We've got you covered!"}</span>
|
||||
<div className="inline-block px-4 py-1 bg-accent/20 text-accent text-xs font-bold uppercase tracking-[0.2em] rounded-full mb-8">
|
||||
{isDe ? 'Lösungen' : 'Solutions'}
|
||||
</div>
|
||||
|
||||
<h3 className="text-3xl md:text-5xl font-bold text-white mb-8 leading-tight">
|
||||
{isDe ? 'Bereit für die' : 'Ready for the'}
|
||||
<span className="text-accent block">{isDe ? 'Energiewende?' : 'Energy Transition?'}</span>
|
||||
</h3>
|
||||
|
||||
<p className="text-lg text-text-secondary mb-6 leading-relaxed">
|
||||
<p className="text-xl text-white/70 mb-10 leading-relaxed max-w-2xl">
|
||||
{isDe
|
||||
? 'Von der Planung von Wind- und Solarparks bis zur Lieferung hochwertiger Energiekabel wie NA2XS(F)2Y, NAYY und NA2XY erwecken wir Energienetze zum Leben.'
|
||||
: 'From wind and solar park planning to delivering high-quality energy cables like NA2XS(F)2Y, NAYY, and NA2XY, we bring energy networks to life.'
|
||||
? 'Von der Planung von Wind- und Solarparks bis zur Lieferung hochwertiger Energiekabel erwecken wir Ihre Projekte zum Leben.'
|
||||
: 'From wind and solar park planning to delivering high-quality energy cables, we bring your projects to life.'
|
||||
}
|
||||
</p>
|
||||
|
||||
<ul className="space-y-2 mb-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-12">
|
||||
{[
|
||||
isDe ? 'Schnelle Lieferung dank unseres strategischen Hubs.' : 'Fast delivery thanks to our strategic hub.',
|
||||
isDe ? 'Nachhaltige Lösungen für eine grünere Zukunft.' : 'Sustainable solutions for a greener tomorrow.',
|
||||
isDe ? 'Vertraut von Branchenführern in Deutschland, Österreich und den Niederlanden.' : 'Trusted by industry leaders in Germany, Austria and the Netherlands.'
|
||||
isDe ? 'Strategischer Hub für schnelle Lieferung' : 'Strategic hub for fast delivery',
|
||||
isDe ? 'Nachhaltige Kabelinfrastruktur' : 'Sustainable cable infrastructure',
|
||||
isDe ? 'Expertenberatung für Großprojekte' : 'Expert consulting for large-scale projects',
|
||||
isDe ? 'Zertifizierte Qualität nach EU-Standards' : 'Certified quality according to EU standards'
|
||||
].map((item, i) => (
|
||||
<li key={i} className="flex items-start gap-2 text-text-secondary">
|
||||
<span className="text-green-500 mt-1">✅</span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
<div key={i} className="flex items-center gap-4 text-white/80">
|
||||
<div className="w-6 h-6 rounded-full bg-accent/20 flex items-center justify-center flex-shrink-0">
|
||||
<svg className="w-3 h-3 text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-sm font-medium">{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center">
|
||||
<p className="font-medium text-text-primary">
|
||||
{isDe ? 'Lassen Sie uns gemeinsam eine grünere Zukunft gestalten.' : "Let's power a greener future together."}
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-6 items-start sm:items-center pt-8 border-t border-white/10">
|
||||
<Link
|
||||
href={`/${locale}/contact`}
|
||||
className="inline-flex items-center gap-2 px-6 py-3 bg-primary text-white font-bold rounded-lg hover:bg-primary/90 transition-all shadow-md hover:shadow-lg transform hover:-translate-y-0.5"
|
||||
className="inline-flex items-center gap-3 px-8 py-4 bg-primary text-white font-bold rounded-full hover:bg-primary/90 transition-all shadow-xl hover:shadow-primary/20 transform hover:-translate-y-1 group/btn"
|
||||
>
|
||||
{isDe ? '👉 Kontakt aufnehmen' : '👉 Get in touch'}
|
||||
{isDe ? 'Projekt anfragen' : 'Inquire Project'}
|
||||
<svg className="w-5 h-5 transition-transform group-hover/btn:translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
||||
</svg>
|
||||
</Link>
|
||||
<p className="text-white/50 text-sm font-medium">
|
||||
{isDe ? 'Kostenlose Erstberatung für Ihr Vorhaben.' : 'Free initial consultation for your project.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
34
components/blog/ReadingProgressBar.tsx
Normal file
34
components/blog/ReadingProgressBar.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
export default function ReadingProgressBar() {
|
||||
const [completion, setCompletion] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const updateScrollCompletion = () => {
|
||||
const currentProgress = window.scrollY;
|
||||
const scrollHeight = document.body.scrollHeight - window.innerHeight;
|
||||
if (scrollHeight) {
|
||||
setCompletion(
|
||||
Number((currentProgress / scrollHeight).toFixed(2)) * 100
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', updateScrollCompletion);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', updateScrollCompletion);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="fixed top-0 left-0 w-full h-1 z-50 bg-neutral-100">
|
||||
<div
|
||||
className="h-full bg-primary transition-all duration-150 ease-out"
|
||||
style={{ width: `${completion}%` }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,46 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
interface SplitHeadingProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export default function SplitHeading({ children, className = '' }: SplitHeadingProps) {
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
if (elementRef.current) {
|
||||
observer.observe(elementRef.current);
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
export default function SplitHeading({ children, className = '', id }: SplitHeadingProps) {
|
||||
return (
|
||||
<div
|
||||
ref={elementRef}
|
||||
className={`transform transition-all duration-1000 ease-out ${
|
||||
isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10'
|
||||
} ${className}`}
|
||||
id={id}
|
||||
className={className}
|
||||
>
|
||||
<h3 className="text-2xl md:text-3xl font-bold leading-tight text-text-primary">
|
||||
<h2 className="text-2xl md:text-3xl font-bold leading-tight text-text-primary">
|
||||
{children}
|
||||
</h3>
|
||||
</h2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,23 +12,28 @@ interface StatsProps {
|
||||
|
||||
export default function Stats({ stats }: StatsProps) {
|
||||
return (
|
||||
<div className="my-12 grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="my-16 grid grid-cols-1 md:grid-cols-3 border border-neutral-200 rounded-2xl overflow-hidden shadow-sm">
|
||||
{stats.map((stat, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-gradient-to-br from-primary/5 to-primary/10 p-6 rounded-xl border border-primary/20 text-center hover:shadow-md transition-shadow"
|
||||
className={`p-8 flex flex-col items-center text-center transition-all duration-500 hover:bg-neutral-50 group ${
|
||||
index !== stats.length - 1 ? 'border-b md:border-b-0 md:border-r border-neutral-200' : ''
|
||||
}`}
|
||||
>
|
||||
{stat.icon && (
|
||||
<div className="text-primary mb-3 flex justify-center">
|
||||
<div className="text-primary mb-4 transform transition-transform group-hover:scale-110 duration-500">
|
||||
{stat.icon}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-4xl font-bold text-primary mb-2">
|
||||
<div className="text-5xl font-bold text-text-primary mb-3 tracking-tight">
|
||||
{stat.value}
|
||||
</div>
|
||||
<div className="text-text-secondary font-medium">
|
||||
<div className="text-xs font-bold text-text-secondary uppercase tracking-[0.2em]">
|
||||
{stat.label}
|
||||
</div>
|
||||
|
||||
{/* Industrial accent line */}
|
||||
<div className="w-8 h-[2px] bg-primary/20 mt-6 transition-all group-hover:w-16 group-hover:bg-primary duration-500" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
86
components/blog/TableOfContents.tsx
Normal file
86
components/blog/TableOfContents.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { cn } from '@/components/ui/utils';
|
||||
|
||||
interface TocItem {
|
||||
id: string;
|
||||
text: string;
|
||||
level: number;
|
||||
}
|
||||
|
||||
interface TableOfContentsProps {
|
||||
headings: TocItem[];
|
||||
locale: string;
|
||||
}
|
||||
|
||||
export default function TableOfContents({ headings, locale }: TableOfContentsProps) {
|
||||
const [activeId, setActiveId] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const observerOptions = {
|
||||
rootMargin: '-100px 0% -80% 0%',
|
||||
threshold: 0
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
setActiveId(entry.target.id);
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
const elements = headings.map((h) => document.getElementById(h.id));
|
||||
elements.forEach((el) => {
|
||||
if (el) observer.observe(el);
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [headings]);
|
||||
|
||||
if (headings.length === 0) return null;
|
||||
|
||||
return (
|
||||
<nav className="hidden lg:block w-full ml-12">
|
||||
<div className="relative pl-6 border-l border-neutral-200">
|
||||
<h4 className="text-xs font-bold uppercase tracking-[0.2em] text-text-primary/50 mb-6">
|
||||
{locale === 'de' ? 'Inhalt' : 'Table of Contents'}
|
||||
</h4>
|
||||
<ul className="space-y-4">
|
||||
{headings.map((heading) => (
|
||||
<li
|
||||
key={heading.id}
|
||||
style={{ paddingLeft: `${(heading.level - 2) * 1}rem` }}
|
||||
className="relative"
|
||||
>
|
||||
{activeId === heading.id && (
|
||||
<div className="absolute -left-[25px] top-0 w-[2px] h-full bg-primary transition-all duration-300" />
|
||||
)}
|
||||
<a
|
||||
href={`#${heading.id}`}
|
||||
className={cn(
|
||||
"text-sm transition-all duration-300 hover:text-primary block leading-snug",
|
||||
activeId === heading.id
|
||||
? "text-primary font-bold translate-x-1"
|
||||
: "text-text-secondary font-medium hover:translate-x-1"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
const element = document.getElementById(heading.id);
|
||||
if (element) {
|
||||
const yOffset = -100;
|
||||
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
|
||||
window.scrollTo({ top: y, behavior: 'smooth' });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{heading.text}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -10,36 +10,64 @@ interface VisualLinkPreviewProps {
|
||||
}
|
||||
|
||||
export default function VisualLinkPreview({ url, title, summary, image }: VisualLinkPreviewProps) {
|
||||
const hostname = (() => {
|
||||
try {
|
||||
return new URL(url).hostname;
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
})();
|
||||
|
||||
return (
|
||||
<Link href={url} target="_blank" rel="noopener noreferrer" className="block my-8 no-underline group">
|
||||
<div className="flex flex-col md:flex-row border border-neutral-dark rounded-lg overflow-hidden bg-white hover:shadow-md transition-shadow">
|
||||
<div className="relative w-full md:w-48 h-48 md:h-auto flex-shrink-0 bg-neutral-light flex items-center justify-center">
|
||||
<Link href={url} target="_blank" rel="noopener noreferrer" className="block my-12 no-underline group">
|
||||
<div className="flex flex-col md:flex-row border border-neutral-200 rounded-2xl overflow-hidden bg-white transition-all duration-500 hover:shadow-2xl hover:border-primary/20 hover:-translate-y-1 group">
|
||||
<div className="relative w-full md:w-64 h-48 md:h-auto flex-shrink-0 bg-neutral-50 overflow-hidden">
|
||||
{image ? (
|
||||
<img
|
||||
<Image
|
||||
src={image}
|
||||
alt={title}
|
||||
className="w-full h-full object-cover"
|
||||
fill
|
||||
unoptimized
|
||||
className="object-cover transition-transform duration-700 group-hover:scale-110"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-neutral-dark">No Image</div>
|
||||
<div className="w-full h-full flex items-center justify-center bg-primary/5">
|
||||
<svg className="w-12 h-12 text-primary/20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
{/* Industrial overlay */}
|
||||
<div className="absolute inset-0 bg-primary/10 opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none" />
|
||||
</div>
|
||||
<div className="p-4 flex flex-col justify-center">
|
||||
<h3 className="text-lg font-bold text-primary mb-2 group-hover:underline line-clamp-2">
|
||||
|
||||
<div className="p-8 flex flex-col justify-center relative">
|
||||
{/* Industrial accent corner */}
|
||||
<div className="absolute top-0 right-0 w-12 h-12 bg-primary/5 -mr-6 -mt-6 rotate-45 transition-transform group-hover:scale-110" />
|
||||
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-[10px] font-bold uppercase tracking-[0.2em] text-primary/60 bg-primary/5 px-2 py-0.5 rounded">
|
||||
External Link
|
||||
</span>
|
||||
<span className="text-[10px] font-bold uppercase tracking-[0.2em] text-text-secondary/40">
|
||||
{hostname}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-bold text-text-primary mb-3 group-hover:text-primary transition-colors line-clamp-2 leading-tight">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="text-text-secondary text-sm line-clamp-3">
|
||||
|
||||
<p className="text-text-secondary text-base line-clamp-2 leading-relaxed mb-4">
|
||||
{summary}
|
||||
</p>
|
||||
<span className="text-xs text-text-secondary mt-2 opacity-70">
|
||||
{(() => {
|
||||
try {
|
||||
return new URL(url).hostname;
|
||||
} catch (e) {
|
||||
return url;
|
||||
}
|
||||
})()}
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-2 text-primary font-bold text-xs uppercase tracking-widest">
|
||||
<span>Read more</span>
|
||||
<svg className="w-4 h-4 transition-transform group-hover:translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -7,7 +7,7 @@ export default function CTA() {
|
||||
const locale = useLocale();
|
||||
|
||||
return (
|
||||
<Section className="bg-primary-dark text-white py-32 relative overflow-hidden">
|
||||
<Section className="bg-primary text-white py-32 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-1/3 h-full bg-accent/5 -skew-x-12 translate-x-1/2" />
|
||||
<div className="absolute bottom-0 left-0 w-1/4 h-1/2 bg-primary/10 rounded-full blur-3xl -translate-x-1/2 translate-y-1/2" />
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ export default function Experience() {
|
||||
className="object-cover object-center scale-105 animate-slow-zoom"
|
||||
unoptimized
|
||||
/>
|
||||
<div className="absolute inset-0 bg-primary-dark/80 mix-blend-multiply" />
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-primary-dark via-primary-dark/40 to-transparent" />
|
||||
<div className="absolute inset-0 bg-primary/80 mix-blend-multiply" />
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-primary via-primary/40 to-transparent" />
|
||||
</div>
|
||||
|
||||
<Container className="relative z-10">
|
||||
|
||||
@@ -8,12 +8,12 @@ export default function Hero() {
|
||||
const t = useTranslations('Home.hero');
|
||||
|
||||
return (
|
||||
<Section className="relative h-[70vh] md:h-[90vh] flex items-center justify-center overflow-hidden bg-primary-dark py-0 md:py-0 lg:py-0">
|
||||
<Section className="relative h-[70vh] md:h-[90vh] flex items-center justify-center overflow-hidden bg-primary py-0 md:py-0 lg:py-0">
|
||||
<HeroIllustration />
|
||||
|
||||
<Container className="relative z-10 text-left text-white w-full">
|
||||
<div className="max-w-5xl animate-slide-up">
|
||||
<Heading level={1} className="mb-4 md:mb-8 tracking-tight leading-[1.05] max-w-[15ch] md:max-w-none">
|
||||
<Heading level={1} className="mb-4 md:mb-8 tracking-tight leading-[1.05] max-w-[15ch] md:max-w-none text-white">
|
||||
{t.rich('title', {
|
||||
green: (chunks) => (
|
||||
<span className="relative inline-block">
|
||||
@@ -23,7 +23,7 @@ export default function Hero() {
|
||||
)
|
||||
})}
|
||||
</Heading>
|
||||
<p className="text-lg md:text-2xl text-white/70 leading-relaxed max-w-2xl mb-8 md:mb-12 line-clamp-2 md:line-clamp-none">
|
||||
<p className="text-lg md:text-2xl text-white leading-relaxed max-w-2xl mb-8 md:mb-12 line-clamp-2 md:line-clamp-none">
|
||||
{t('subtitle')}
|
||||
</p>
|
||||
<div className="flex flex-col md:flex-row gap-3 md:gap-6">
|
||||
|
||||
@@ -18,8 +18,8 @@ export default function MeetTheTeam() {
|
||||
className="object-cover scale-105 animate-slow-zoom"
|
||||
unoptimized
|
||||
/>
|
||||
<div className="absolute inset-0 bg-primary-dark/70 mix-blend-multiply" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-primary-dark via-primary-dark/20 to-transparent" />
|
||||
<div className="absolute inset-0 bg-primary/70 mix-blend-multiply" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-primary via-primary/20 to-transparent" />
|
||||
</div>
|
||||
|
||||
<Container className="relative z-10">
|
||||
@@ -43,10 +43,10 @@ export default function MeetTheTeam() {
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex -space-x-4">
|
||||
<div className="w-14 h-14 rounded-full border-4 border-primary-dark overflow-hidden relative">
|
||||
<div className="w-14 h-14 rounded-full border-4 border-primary overflow-hidden relative">
|
||||
<Image src="/uploads/2024/12/DSC07768-Large.webp" alt={teamT('michael.name')} fill className="object-cover" />
|
||||
</div>
|
||||
<div className="w-14 h-14 rounded-full border-4 border-primary-dark overflow-hidden relative">
|
||||
<div className="w-14 h-14 rounded-full border-4 border-primary overflow-hidden relative">
|
||||
<Image src="/uploads/2024/12/DSC07963-Large.webp" alt={teamT('klaus.name')} fill className="object-cover" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,7 @@ export default function ProductCategories() {
|
||||
];
|
||||
|
||||
return (
|
||||
<Section className="bg-neutral-light py-0 -mt-px">
|
||||
<Section className="bg-neutral-light py-0 md:py-0 lg:py-0 -mt-px">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4">
|
||||
{categories.map((category, idx) => (
|
||||
<Link key={idx} href={category.href} className="group block relative h-[400px] md:h-[500px] lg:h-[650px] overflow-hidden border-b md:border-b-0 md:border-r border-white/10 last:border-0">
|
||||
|
||||
@@ -5,7 +5,7 @@ export default function VideoSection() {
|
||||
const t = useTranslations('Home.video');
|
||||
|
||||
return (
|
||||
<section className="relative h-[70vh] overflow-hidden bg-primary-dark">
|
||||
<section className="relative h-[70vh] overflow-hidden bg-primary">
|
||||
<video
|
||||
className="w-full h-full object-cover opacity-60"
|
||||
autoPlay
|
||||
@@ -15,7 +15,7 @@ export default function VideoSection() {
|
||||
>
|
||||
<source src="/uploads/2024/12/making-of-metal-cable-on-factory-2023-11-27-04-55-16-utc-2.webm" type="video/mp4" />
|
||||
</video>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-primary-dark/60 via-transparent to-primary-dark/60 flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-primary/60 via-transparent to-primary/60 flex items-center justify-center">
|
||||
<div className="max-w-5xl px-6 text-center animate-slide-up">
|
||||
<h2 className="text-4xl md:text-6xl lg:text-7xl font-extrabold text-white leading-[1.1]">
|
||||
{t.rich('title', {
|
||||
|
||||
@@ -30,13 +30,13 @@ export function Heading({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('mb-8 md:mb-16', alignments[align], className)}>
|
||||
<div className={cn('mb-8 md:mb-16 text-primary', alignments[align], className)}>
|
||||
{subtitle && (
|
||||
<span className="inline-block text-accent font-bold tracking-widest uppercase text-xs md:text-sm mb-3 md:mb-4 animate-fade-in">
|
||||
{subtitle}
|
||||
</span>
|
||||
)}
|
||||
<Tag className={cn(sizes[level as keyof typeof sizes], 'text-primary')}>
|
||||
<Tag className={cn(sizes[level as keyof typeof sizes])}>
|
||||
{children}
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,7 @@ The design should feel **Industrial yet Modern**, **Reliable**, and **Sustainabl
|
||||
|
||||
### 2.2 Color Strategy
|
||||
- **Trust (Primary Blue)**: Deep, vibrant blue used for core branding and primary actions. Represents stability and the flow of electricity.
|
||||
- **Saturated Blue (#011dff)**: A high-intensity blue used for specific high-impact accents and digital-first elements.
|
||||
- **Growth (Accent Green)**: A bright, energetic green used exclusively for highlights related to renewable energy and the future.
|
||||
- **Foundation (Neutrals)**: A range of blacks, dark grays, and clean whites to provide a professional, industrial backdrop.
|
||||
|
||||
|
||||
22
lib/blog.ts
22
lib/blog.ts
@@ -73,3 +73,25 @@ export async function getAdjacentPosts(slug: string, locale: string): Promise<{
|
||||
|
||||
return { prev, next };
|
||||
}
|
||||
|
||||
export function getReadingTime(content: string): number {
|
||||
const wordsPerMinute = 200;
|
||||
const noOfWords = content.split(/\s/g).length;
|
||||
const minutes = noOfWords / wordsPerMinute;
|
||||
return Math.ceil(minutes);
|
||||
}
|
||||
|
||||
export function getHeadings(content: string): { id: string; text: string; level: number }[] {
|
||||
const headingLines = content.split('\n').filter((line) => line.match(/^#{2,3}\s/));
|
||||
|
||||
return headingLines.map((line) => {
|
||||
const level = line.match(/^#+/)?.[0].length || 0;
|
||||
const text = line.replace(/^#+\s/, '').trim();
|
||||
const id = text
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/\s+/g, '-');
|
||||
|
||||
return { id, text, level };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
--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;
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
@@ -51,6 +52,10 @@
|
||||
from { opacity: 0; transform: translateY(30px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes slight-fade-in-from-bottom {
|
||||
from { opacity: 0; transform: translateY(15px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
||||
@@ -19,7 +19,7 @@ module.exports = {
|
||||
colors: {
|
||||
// Brand Colors
|
||||
primary: {
|
||||
DEFAULT: '#0117bf',
|
||||
DEFAULT: '#011dff',
|
||||
dark: '#000e7a',
|
||||
light: '#3344cc',
|
||||
},
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user