This commit is contained in:
2025-08-17 13:21:01 +02:00
commit c988d51438
51 changed files with 5742 additions and 0 deletions

110
lib/animateSplit.ts Normal file
View File

@@ -0,0 +1,110 @@
// Dynamic import to avoid SSR issues
let gsap: any
if (typeof window !== 'undefined') {
gsap = require('gsap').gsap
}
export function splitTextIntoChars(element: HTMLElement): HTMLElement[] {
const text = element.textContent || ''
const chars: HTMLElement[] = []
element.innerHTML = ''
for (let i = 0; i < text.length; i++) {
const char = text[i]
const span = document.createElement('span')
span.textContent = char === ' ' ? '\u00A0' : char
span.setAttribute('data-char', i.toString())
span.style.display = 'inline-block'
span.style.willChange = 'transform'
element.appendChild(span)
chars.push(span)
}
return chars
}
export function animateSplit(element: HTMLElement, options: {
delay?: number
stagger?: number
duration?: number
ease?: string
} = {}) {
if (!gsap) return
const {
delay = 0,
stagger = 0.015,
duration = 0.8,
ease = 'power2.out'
} = options
const chars = splitTextIntoChars(element)
// Set initial state
gsap.set(chars, {
yPercent: 110,
rotateZ: 5,
opacity: 0,
transformOrigin: 'center bottom'
})
// Animate in
return gsap.to(chars, {
yPercent: 0,
rotateZ: 0,
opacity: 1,
stagger,
duration,
ease,
delay
})
}
export function animateWords(element: HTMLElement, options: {
delay?: number
stagger?: number
duration?: number
ease?: string
} = {}) {
if (!gsap) return
const {
delay = 0,
stagger = 0.08,
duration = 0.6,
ease = 'power2.out'
} = options
const text = element.textContent || ''
const words = text.split(' ')
element.innerHTML = ''
const wordElements = words.map(word => {
const span = document.createElement('span')
span.textContent = word + ' '
span.style.display = 'inline-block'
span.style.willChange = 'transform'
element.appendChild(span)
return span
})
// Set initial state
gsap.set(wordElements, {
yPercent: 100,
opacity: 0,
transformOrigin: 'center bottom'
})
// Animate in
return gsap.to(wordElements, {
yPercent: 0,
opacity: 1,
stagger,
duration,
ease,
delay
})
}

15
lib/i18n.ts Normal file
View File

@@ -0,0 +1,15 @@
export const supportedLocales = ['en', 'de'] as const;
export type Locale = typeof supportedLocales[number];
export function getInitialLocale(cookieStore: any, _hdrs: any): Locale {
const cookie = cookieStore?.get?.('lang')?.value as Locale | undefined;
if (cookie && (supportedLocales as readonly string[]).includes(cookie)) return cookie;
return 'en';
}
export async function getDictionary(locale: Locale) {
// Use relative imports to ensure resolution in both server and edge runtimes
const common = await import(`../locales/${locale}/common.json`).then((m) => m.default);
const home = await import(`../locales/${locale}/home.json`).then((m) => m.default);
return { common, home, ...home } as any;
}

10
lib/scroll.ts Normal file
View File

@@ -0,0 +1,10 @@
export function supportsScrollTimeline(): boolean {
// @ts-ignore
return typeof (CSS as any)?.scrollTimeline !== 'undefined'
}
export function initScrollFallback() {
if (typeof window === 'undefined') return
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches
if (prefersReduced) return
}
export function disableAnimations() { }