This commit is contained in:
2026-01-16 18:24:45 +01:00
parent 815c410092
commit 36e2a84a54
223 changed files with 2 additions and 272264 deletions

View File

@@ -1,353 +0,0 @@
/**
* Data utilities for Next.js WordPress migration
*/
import wordpressData from '../data/processed/wordpress-data.json';
import { getExcelTechnicalDataForProduct } from './excel-products';
export interface SiteInfo {
title: string;
description: string;
baseUrl: string;
defaultLocale: string;
locales: string[];
}
export interface TranslationReference {
locale: string;
id: number;
}
export interface Page {
id: number;
translationKey: string;
locale: string;
slug: string;
path: string;
title: string;
titleHtml: string;
contentHtml: string;
excerptHtml: string;
featuredImage: number | null;
updatedAt: string;
translation: TranslationReference | null;
}
export interface Post {
id: number;
translationKey: string;
locale: string;
slug: string;
path: string;
title: string;
titleHtml: string;
contentHtml: string;
excerptHtml: string;
featuredImage: number | null;
datePublished: string;
updatedAt: string;
translation: TranslationReference | null;
}
export interface Product {
id: number;
translationKey: string;
locale: string;
slug: string;
path: string;
name: string;
shortDescriptionHtml: string;
descriptionHtml: string;
images: string[];
featuredImage: string | null;
sku: string;
regularPrice: string;
salePrice: string;
currency: string;
stockStatus: string;
categories: Array<{ id: number; name: string; slug: string }>;
attributes: any[];
variations: any[];
updatedAt: string;
translation: TranslationReference | null;
// Excel-derived technical data
excelConfigurations?: string[];
excelAttributes?: Array<{
name: string;
options: string[];
}>;
}
export interface ProductCategory {
id: number;
translationKey: string;
locale: string;
slug: string;
name: string;
path: string;
description: string;
count: number;
translation: TranslationReference | null;
}
export interface Media {
id: number;
filename: string;
url: string;
localPath: string;
alt: string;
width: number | null;
height: number | null;
mimeType: string | null;
}
export interface Redirect {
source: string;
destination: string;
permanent: boolean;
locale: string;
}
export interface WordPressData {
site: SiteInfo;
content: {
pages: Page[];
posts: Post[];
products: Product[];
categories: ProductCategory[];
};
assets: {
media: Media[];
map: Record<string, string>;
};
redirects: Redirect[];
exportDate: string;
}
// Load data
// Use type assertion to handle the JSON import properly
const data = wordpressData as unknown as WordPressData;
// Data access functions
export const getSiteInfo = (): SiteInfo => data.site;
export const getAllPages = (): Page[] => data.content.pages;
export const getAllPosts = (): Post[] => data.content.posts;
export const getAllProducts = (): Product[] => data.content.products;
export const getAllCategories = (): ProductCategory[] => data.content.categories;
export const getMediaById = (id: number): Media | undefined => {
return data.assets.media.find(m => m.id === id);
};
export const getMediaByUrl = (url: string): Media | undefined => {
const localPath = data.assets.map[url];
if (!localPath) return undefined;
return data.assets.media.find(m => m.localPath === localPath);
};
export const getAssetMap = (): Record<string, string> => {
return data.assets?.map || {};
};
export const getRedirects = (): Redirect[] => data.redirects;
// Locale-specific queries
export const getPagesByLocale = (locale: string): Page[] => {
return data.content.pages.filter(p => p.locale === locale);
};
export const getPostsByLocale = (locale: string): Post[] => {
return data.content.posts.filter(p => p.locale === locale);
};
export const getProductsByLocale = (locale: string): Product[] => {
return data.content.products.filter(p => p.locale === locale);
};
export const getCategoriesByLocale = (locale: string): ProductCategory[] => {
return data.content.categories.filter(c => c.locale === locale);
};
// Single item queries
export const getPageBySlug = (slug: string, locale: string): Page | undefined => {
return data.content.pages.find(p => p.slug === slug && p.locale === locale);
};
export const getPostBySlug = (slug: string, locale: string): Post | undefined => {
return data.content.posts.find(p => p.slug === slug && p.locale === locale);
};
export const getProductBySlug = (slug: string, locale: string): Product | undefined => {
return data.content.products.find(p => p.slug === slug && p.locale === locale);
};
export const getCategoryBySlug = (slug: string, locale: string): ProductCategory | undefined => {
return data.content.categories.find(c => c.slug === slug && c.locale === locale);
};
// Translation helpers
export const getTranslation = <T extends { translationKey: string; locale: string }>(
item: T,
targetLocale: string
): T | undefined => {
const collection = [
...getAllPages(),
...getAllPosts(),
...getAllProducts(),
...getAllCategories()
];
const result = collection.find(
(i: any) => i.translationKey === item.translationKey && i.locale === targetLocale
);
return result as unknown as T | undefined;
};
// Asset URL replacement
export const replaceAssetUrls = (html: string): string => {
let result = html;
Object.entries(data.assets.map).forEach(([wpUrl, localPath]) => {
result = result.replace(new RegExp(wpUrl, 'g'), localPath);
});
return result;
};
// Additional functions for product categories
export const getProductCategory = (slug: string, locale: string): ProductCategory | undefined => {
return data.content.categories.find(c => c.slug === slug && c.locale === locale);
};
export const getProductsByCategory = (categoryId: number, locale: string): Product[] => {
return data.content.products.filter(p =>
p.locale === locale && p.categories.some(c => c.id === categoryId)
);
};
// Get products by category slug
export const getProductsByCategorySlug = (categorySlug: string, locale: string): Product[] => {
const category = getCategoryBySlug(categorySlug, locale);
if (!category) return [];
return getProductsByCategory(category.id, locale);
};
// Get related products (same category, excluding current product)
export const getRelatedProducts = (product: Product, locale: string, limit: number = 4): Product[] => {
if (product.categories.length === 0) return [];
// Get first category
const firstCategory = product.categories[0];
const categoryProducts = getProductsByCategory(firstCategory.id, locale);
// Filter out current product and limit results
return categoryProducts
.filter(p => p.id !== product.id)
.slice(0, limit);
};
// Get categories by slugs
export const getCategoriesBySlugs = (slugs: string[], locale: string): ProductCategory[] => {
return data.content.categories.filter(c =>
slugs.includes(c.slug) && c.locale === locale
);
};
// Locale-specific queries for static generation
export const getAllCategorySlugsForLocale = (locale: string): string[] => {
return [...new Set(data.content.categories.filter(c => c.locale === locale).map(c => c.slug))];
};
export const getAllPageSlugsForLocale = (locale: string): string[] => {
return [...new Set(data.content.pages.filter(p => p.locale === locale).map(p => p.slug))];
};
export const getAllPostSlugsForLocale = (locale: string): string[] => {
return [...new Set(data.content.posts.filter(p => p.locale === locale).map(p => p.slug))];
};
export const getAllProductSlugsForLocale = (locale: string): string[] => {
return [...new Set(data.content.products.filter(p => p.locale === locale).map(p => p.slug))];
};
// Get items for locale
export const getCategoriesForLocale = (locale: string): ProductCategory[] => {
return data.content.categories.filter(c => c.locale === locale);
};
export const getPagesForLocale = (locale: string): Page[] => {
return data.content.pages.filter(p => p.locale === locale);
};
export const getPostsForLocale = (locale: string): Post[] => {
return data.content.posts.filter(p => p.locale === locale);
};
export const getProductsForLocale = (locale: string): Product[] => {
return data.content.products.filter(p => p.locale === locale);
};
/**
* Enrich a product with Excel-derived technical data
* This function merges Excel data into the product's attributes
*/
export function enrichProductWithExcelData(product: Product): Product {
// Skip if already enriched
if (product.excelConfigurations || product.excelAttributes) {
return product;
}
const excelData = getExcelTechnicalDataForProduct({
name: product.name,
slug: product.slug,
sku: product.sku,
translationKey: product.translationKey,
});
if (!excelData) {
return product;
}
// Create a copy of the product with Excel data
const enrichedProduct: Product = {
...product,
excelConfigurations: excelData.configurations,
excelAttributes: excelData.attributes,
};
return enrichedProduct;
}
/**
* Get a single product by slug with Excel enrichment
*/
export function getProductBySlugWithExcel(slug: string, locale: string): Product | undefined {
const product = getProductBySlug(slug, locale);
if (!product) return undefined;
return enrichProductWithExcelData(product);
}
/**
* Get all products for a locale with Excel enrichment
*/
export function getProductsForLocaleWithExcel(locale: string): Product[] {
const products = getProductsForLocale(locale);
return products.map(p => enrichProductWithExcelData(p));
}
/**
* Get products by category with Excel enrichment
*/
export function getProductsByCategoryWithExcel(categoryId: number, locale: string): Product[] {
const products = getProductsByCategory(categoryId, locale);
return products.map(p => enrichProductWithExcelData(p));
}
/**
* Get products by category slug with Excel enrichment
*/
export function getProductsByCategorySlugWithExcel(categorySlug: string, locale: string): Product[] {
const products = getProductsByCategorySlug(categorySlug, locale);
return products.map(p => enrichProductWithExcelData(p));
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
import type { Locale } from './i18n';
export const i18n = {
defaultLocale: 'en' as Locale,
locales: ['en', 'de'] as Locale[],
} as const;
export type LocaleParam = 'en' | 'de';

View File

@@ -1,339 +0,0 @@
export type Locale = 'en' | 'de';
export const defaultLocale: Locale = 'en';
export const locales: Locale[] = ['en', 'de'];
// Simple translation dictionary
const translations = {
en: {
site: {
title: 'Kabel-Konfigurator',
description: 'Professional cable solutions - configure and order custom cables',
},
nav: {
home: 'Home',
blog: 'Blog',
products: 'Products',
contact: 'Contact',
privacy: 'Privacy Policy',
legal: 'Legal Notice',
terms: 'Terms & Conditions',
},
home: {
hero: 'Professional Cable Solutions',
heroSubtitle: 'Configure your custom cables online',
cta: 'Configure Now',
featuredPosts: 'Latest News',
featuredProducts: 'Featured Products',
},
blog: {
title: 'Blog',
description: 'Latest news and insights about cables and energy',
readMore: 'Read more',
noPosts: 'No posts available.',
backToBlog: '← Back to Blog',
categories: 'Categories',
featured: 'Featured Posts',
allPosts: 'All Posts',
noPostsDescription: 'Check back soon for new content.',
},
products: {
title: 'Products',
categories: 'Categories',
noProducts: 'No products available.',
noCategories: 'No categories available.',
inStock: 'In Stock',
outOfStock: 'Out of Stock',
price: 'Price',
sku: 'SKU',
viewAll: 'View All Products',
},
product: {
backToProducts: '← Back to Products',
description: 'Description',
specifications: 'Specifications',
price: 'Price',
sku: 'SKU',
stock: 'Stock Status',
inStock: 'In Stock',
outOfStock: 'Out of Stock',
},
productCategory: {
backToCategories: '← Back to Categories',
productsInCategory: 'Products in this category',
},
contact: {
title: 'Contact Us',
subtitle: 'Get in touch with our team',
name: 'Your Name',
email: 'Your Email',
message: 'Your Message',
submit: 'Send Message',
success: 'Message sent successfully!',
error: 'Failed to send message. Please try again.',
processing: 'Sending...',
phone: 'Phone (optional)',
subject: 'Subject',
company: 'Company (optional)',
requiredFields: 'Required fields are marked with *',
sending: 'Sending...',
errors: {
nameRequired: 'Please enter your name',
emailRequired: 'Please enter your email address',
emailInvalid: 'Please enter a valid email address',
messageRequired: 'Please enter your message',
},
},
consent: {
title: 'Cookie & Analytics Consent',
description: 'We use analytics cookies to improve our website. Please accept to continue.',
accept: 'Accept',
decline: 'Decline',
analytics: 'Analytics',
analyticsDesc: 'Help us understand how visitors use our site',
},
cookieConsent: {
message: 'We use cookies to enhance your browsing experience and analyze our traffic.',
privacyPolicy: 'Privacy Policy',
decline: 'Decline',
accept: 'Accept',
},
footer: {
rights: 'All rights reserved.',
madeWith: 'Made with Next.js',
},
common: {
readMore: 'Read more',
back: 'Back',
loading: 'Loading...',
noContent: 'No content available.',
date: 'Date',
updated: 'Updated',
},
form: {
success: 'Message sent successfully!',
error: {
submit: 'Failed to send message. Please try again.',
network: 'Network error. Please try again.',
},
sending: 'Sending...',
name: 'Your Name',
email: 'Your Email',
message: 'Your Message',
submit: 'Send Message',
},
},
de: {
site: {
title: 'Kabel-Konfigurator',
description: 'Professionelle Kabel-Lösungen - konfigurieren und bestellen Sie maßgeschneiderte Kabel',
},
nav: {
home: 'Startseite',
blog: 'Blog',
products: 'Produkte',
contact: 'Kontakt',
privacy: 'Datenschutz',
legal: 'Impressum',
terms: 'AGB',
},
home: {
hero: 'Professionelle Kabel-Lösungen',
heroSubtitle: 'Konfigurieren Sie Ihre maßgeschneiderten Kabel online',
cta: 'Jetzt konfigurieren',
featuredPosts: 'Aktuelle Neuigkeiten',
featuredProducts: 'Empfohlene Produkte',
},
blog: {
title: 'Blog',
description: 'Aktuelle Neuigkeiten und Einblicke über Kabel und Energie',
readMore: 'Weiterlesen',
noPosts: 'Keine Beiträge verfügbar.',
backToBlog: '← Zurück zum Blog',
categories: 'Kategorien',
featured: 'Beiträge',
allPosts: 'Alle Beiträge',
noPostsDescription: 'Schauen Sie bald wieder vorbei für neue Inhalte.',
},
products: {
title: 'Produkte',
categories: 'Kategorien',
noProducts: 'Keine Produkte verfügbar.',
noCategories: 'Keine Kategorien verfügbar.',
inStock: 'Auf Lager',
outOfStock: 'Nicht auf Lager',
price: 'Preis',
sku: 'Artikelnummer',
viewAll: 'Alle Produkte anzeigen',
},
product: {
backToProducts: '← Zurück zu Produkten',
description: 'Beschreibung',
specifications: 'Spezifikationen',
price: 'Preis',
sku: 'Artikelnummer',
stock: 'Lagerbestand',
inStock: 'Auf Lager',
outOfStock: 'Nicht auf Lager',
},
productCategory: {
backToCategories: '← Zurück zu Kategorien',
productsInCategory: 'Produkte in dieser Kategorie',
},
contact: {
title: 'Kontakt',
subtitle: 'Nehmen Sie Kontakt mit unserem Team auf',
name: 'Ihr Name',
email: 'Ihre E-Mail',
message: 'Ihre Nachricht',
submit: 'Nachricht senden',
success: 'Nachricht erfolgreich gesendet!',
error: 'Nachricht konnte nicht gesendet werden. Bitte versuchen Sie es erneut.',
processing: 'Wird gesendet...',
phone: 'Telefon (optional)',
subject: 'Betreff',
company: 'Firma (optional)',
requiredFields: 'Pflichtfelder sind mit * markiert',
sending: 'Wird gesendet...',
errors: {
nameRequired: 'Bitte geben Sie Ihren Namen ein',
emailRequired: 'Bitte geben Sie Ihre E-Mail-Adresse ein',
emailInvalid: 'Bitte geben Sie eine gültige E-Mail-Adresse ein',
messageRequired: 'Bitte geben Sie Ihre Nachricht ein',
},
},
consent: {
title: 'Cookie- & Analyse-Einwilligung',
description: 'Wir verwenden Analyse-Cookies, um unsere Website zu verbessern. Bitte akzeptieren Sie zur Fortsetzung.',
accept: 'Akzeptieren',
decline: 'Ablehnen',
analytics: 'Analyse',
analyticsDesc: 'Helfen Sie uns zu verstehen, wie Besucher unsere Seite nutzen',
},
cookieConsent: {
message: 'Wir verwenden Cookies, um Ihr Surferlebnis zu verbessern und unseren Traffic zu analysieren.',
privacyPolicy: 'Datenschutzrichtlinie',
decline: 'Ablehnen',
accept: 'Akzeptieren',
},
footer: {
rights: 'Alle Rechte vorbehalten.',
madeWith: 'Erstellt mit Next.js',
},
common: {
readMore: 'Weiterlesen',
back: 'Zurück',
loading: 'Wird geladen...',
noContent: 'Kein Inhalt verfügbar.',
date: 'Datum',
updated: 'Aktualisiert',
},
form: {
success: 'Nachricht erfolgreich gesendet!',
error: {
submit: 'Nachricht konnte nicht gesendet werden. Bitte versuchen Sie es erneut.',
network: 'Netzwerkfehler. Bitte versuchen Sie es erneut.',
},
sending: 'Wird gesendet...',
name: 'Ihr Name',
email: 'Ihre E-Mail',
message: 'Ihre Nachricht',
submit: 'Nachricht senden',
},
},
};
export function t(key: string, locale: Locale = 'en'): string {
const keys = key.split('.');
let value: any = translations[locale];
for (const k of keys) {
if (value && typeof value === 'object' && k in value) {
value = value[k];
} else {
// Fallback to English
value = translations.en;
for (const k2 of keys) {
if (value && typeof value === 'object' && k2 in value) {
value = value[k2];
} else {
return key; // Return the key itself if translation not found
}
}
return value;
}
}
// Ensure we always return a string
return typeof value === 'string' ? value : key;
}
export function getLocaleFromPath(path: string): Locale {
if (path.startsWith('/de/')) {
return 'de';
}
return 'en';
}
export function getLocalizedPath(path: string, locale: Locale): string {
if (locale === 'en') {
return path.replace('/de/', '/');
}
if (locale === 'de') {
if (path === '/') return '/de';
return path.startsWith('/de/') ? path : `/de${path}`;
}
return path;
}
export function getPathWithoutLocale(path: string): string {
if (path.startsWith('/de/')) {
return path.substring(3) || '/';
}
return path;
}
export const languageNames: Record<Locale, string> = {
en: 'English',
de: 'Deutsch',
};
export function getSiteInfo(locale?: Locale) {
const loc = locale || defaultLocale;
return {
title: t('site.title', loc),
description: t('site.description', loc),
locale: loc,
baseUrl: process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com',
locales: ['en', 'de'],
};
}
// Hook for client components (simplified version)
export function useTranslation(namespace?: string) {
// This would be used in client components
// For now, return a simple t function
return {
t: (key: string) => t(namespace ? `${namespace}.${key}` : key, defaultLocale)
};
}
// Get alternate URLs for SEO
export function getAlternateUrls(path: string) {
return [
{ locale: 'en', url: path.replace('/de/', '/') },
{ locale: 'de', url: path.startsWith('/de') ? path : `/de${path}` },
];
}
// Hook for client components - returns current locale
export function useLocale(): Locale {
// This is a simplified version for build purposes
// In a real app, this would use next/navigation to get the current path
return defaultLocale;
}
// Get dictionary for client components
export function getDictionary(locale: Locale) {
return translations[locale];
}

View File

@@ -1,326 +0,0 @@
/**
* Responsive Testing Utilities for KLZ Cables
* Tools for testing and validating responsive design
*/
import { BREAKPOINTS, getViewport, Viewport } from './responsive';
// Test viewport configurations
export const TEST_VIEWPORTS = {
mobile: {
width: 375,
height: 667,
name: 'Mobile (iPhone SE)',
breakpoint: 'xs',
},
mobileLarge: {
width: 414,
height: 896,
name: 'Mobile Large (iPhone 11)',
breakpoint: 'sm',
},
tablet: {
width: 768,
height: 1024,
name: 'Tablet (iPad)',
breakpoint: 'md',
},
tabletLandscape: {
width: 1024,
height: 768,
name: 'Tablet Landscape',
breakpoint: 'lg',
},
desktop: {
width: 1280,
height: 800,
name: 'Desktop (Laptop)',
breakpoint: 'xl',
},
desktopLarge: {
width: 1440,
height: 900,
name: 'Desktop Large',
breakpoint: '2xl',
},
desktopWide: {
width: 1920,
height: 1080,
name: 'Desktop Wide (Full HD)',
breakpoint: '3xl',
},
};
/**
* Responsive Design Checklist
* Comprehensive checklist for validating responsive design
*/
export const RESPONSIVE_CHECKLIST = {
layout: [
'Content stacks properly on mobile (1 column)',
'Grid layouts adapt to screen size (2-4 columns)',
'No horizontal scrolling at any breakpoint',
'Content remains within safe areas',
'Padding and margins scale appropriately',
],
typography: [
'Text remains readable at all sizes',
'Line height is optimized for mobile',
'Headings scale appropriately',
'No text overflow or clipping',
'Font size meets WCAG guidelines (16px minimum)',
],
navigation: [
'Mobile menu is accessible (44px touch targets)',
'Desktop navigation hides on mobile',
'Menu items are properly spaced',
'Active states are visible',
'Back/forward navigation works',
],
images: [
'Images load with appropriate sizes',
'Aspect ratios are maintained',
'No layout shift during loading',
'Lazy loading works correctly',
'Placeholder blur is applied',
],
forms: [
'Input fields are 44px minimum touch target',
'Labels remain visible',
'Error messages are readable',
'Form submits on mobile',
'Keyboard navigation works',
],
performance: [
'Images are properly sized for viewport',
'No unnecessary large assets on mobile',
'Critical CSS is loaded',
'Touch interactions are smooth',
'No layout thrashing',
],
accessibility: [
'Touch targets are 44px minimum',
'Focus indicators are visible',
'Screen readers work correctly',
'Color contrast meets WCAG AA',
'Zoom is not restricted',
],
};
/**
* Generate responsive design report
*/
export function generateResponsiveReport(): string {
const viewport = getViewport();
const report = `
Responsive Design Report - KLZ Cables
=====================================
Current Viewport:
- Width: ${viewport.width}px
- Height: ${viewport.height}px
- Breakpoint: ${viewport.breakpoint}
- Device Type: ${viewport.isMobile ? 'Mobile' : viewport.isTablet ? 'Tablet' : 'Desktop'}
Breakpoint Configuration:
- xs: ${BREAKPOINTS.xs}px
- sm: ${BREAKPOINTS.sm}px
- md: ${BREAKPOINTS.md}px
- lg: ${BREAKPOINTS.lg}px
- xl: ${BREAKPOINTS.xl}px
- 2xl: ${BREAKPOINTS['2xl']}px
- 3xl: ${BREAKPOINTS['3xl']}px
Touch Target Verification:
- Minimum: 44px × 44px
- Recommended: 48px × 48px
- Large: 56px × 56px
Image Optimization:
- Mobile Quality: 75%
- Tablet Quality: 85%
- Desktop Quality: 90%
Typography Scale:
- Fluid typography using CSS clamp()
- Mobile: 16px base
- Desktop: 18px base
- Line height: 1.4-1.6
Generated: ${new Date().toISOString()}
`.trim();
return report;
}
/**
* Validate responsive design rules
*/
export function validateResponsiveDesign(): {
passed: boolean;
warnings: string[];
errors: string[];
} {
const warnings: string[] = [];
const errors: string[] = [];
// Check viewport
if (typeof window === 'undefined') {
warnings.push('Server-side rendering detected - some checks skipped');
}
// Check minimum touch target size
const buttons = document.querySelectorAll('button, a');
buttons.forEach((el) => {
const rect = el.getBoundingClientRect();
if (rect.width < 44 || rect.height < 44) {
warnings.push(`Element ${el.tagName} has touch target < 44px`);
}
});
// Check for horizontal scroll
if (document.body.scrollWidth > window.innerWidth) {
errors.push('Horizontal scrolling detected');
}
// Check text size
const textElements = document.querySelectorAll('p, span, div');
textElements.forEach((el) => {
const computed = window.getComputedStyle(el);
const fontSize = parseFloat(computed.fontSize);
if (fontSize < 16 && el.textContent && el.textContent.length > 50) {
warnings.push(`Text element ${el.tagName} has font-size < 16px`);
}
});
return {
passed: errors.length === 0,
warnings,
errors,
};
}
/**
* Responsive design utilities for testing
*/
export const ResponsiveTestUtils = {
// Set viewport for testing
setViewport: (width: number, height: number) => {
if (typeof window !== 'undefined') {
window.innerWidth = width;
window.innerHeight = height;
window.dispatchEvent(new Event('resize'));
}
},
// Simulate mobile viewport
simulateMobile: () => {
ResponsiveTestUtils.setViewport(375, 667);
},
// Simulate tablet viewport
simulateTablet: () => {
ResponsiveTestUtils.setViewport(768, 1024);
},
// Simulate desktop viewport
simulateDesktop: () => {
ResponsiveTestUtils.setViewport(1280, 800);
},
// Check if element is in viewport
isElementInViewport: (element: HTMLElement, offset = 0): boolean => {
const rect = element.getBoundingClientRect();
return (
rect.top >= -offset &&
rect.left >= -offset &&
rect.bottom <= (window.innerHeight + offset) &&
rect.right <= (window.innerWidth + offset)
);
},
// Measure touch target size
measureTouchTarget: (element: HTMLElement): { width: number; height: number; valid: boolean } => {
const rect = element.getBoundingClientRect();
return {
width: rect.width,
height: rect.height,
valid: rect.width >= 44 && rect.height >= 44,
};
},
// Check text readability
checkTextReadability: (element: HTMLElement): { fontSize: number; lineHeight: number; valid: boolean } => {
const computed = window.getComputedStyle(element);
const fontSize = parseFloat(computed.fontSize);
const lineHeight = parseFloat(computed.lineHeight);
return {
fontSize,
lineHeight,
valid: fontSize >= 16 && lineHeight >= 1.4,
};
},
// Generate responsive test report
generateTestReport: () => {
const viewport = getViewport();
const validation = validateResponsiveDesign();
return {
viewport,
validation,
timestamp: new Date().toISOString(),
};
},
};
/**
* Responsive design patterns for common scenarios
*/
export const RESPONSIVE_PATTERNS = {
// Mobile-first card grid
cardGrid: {
mobile: { columns: 1, gap: '1rem' },
tablet: { columns: 2, gap: '1.5rem' },
desktop: { columns: 3, gap: '2rem' },
},
// Hero section
hero: {
mobile: { layout: 'stacked', padding: '2rem 1rem' },
tablet: { layout: 'split', padding: '3rem 2rem' },
desktop: { layout: 'split', padding: '4rem 3rem' },
},
// Form layout
form: {
mobile: { columns: 1, fieldWidth: '100%' },
tablet: { columns: 2, fieldWidth: '48%' },
desktop: { columns: 2, fieldWidth: '48%' },
},
// Navigation
navigation: {
mobile: { type: 'hamburger', itemsPerScreen: 6 },
tablet: { type: 'hybrid', itemsPerScreen: 8 },
desktop: { type: 'full', itemsPerScreen: 12 },
},
// Image gallery
gallery: {
mobile: { columns: 1, aspectRatio: '4:3' },
tablet: { columns: 2, aspectRatio: '1:1' },
desktop: { columns: 3, aspectRatio: '16:9' },
},
};
export default {
TEST_VIEWPORTS,
RESPONSIVE_CHECKLIST,
generateResponsiveReport,
validateResponsiveDesign,
ResponsiveTestUtils,
RESPONSIVE_PATTERNS,
};

View File

@@ -1,496 +0,0 @@
/**
* Responsive Design Utilities for KLZ Cables
* Mobile-first approach with comprehensive breakpoint detection and responsive helpers
*/
// Breakpoint definitions matching Tailwind config
export const BREAKPOINTS = {
xs: 475,
sm: 640,
md: 768,
lg: 1024,
xl: 1280,
'2xl': 1400,
'3xl': 1600,
} as const;
export type BreakpointKey = keyof typeof BREAKPOINTS;
// Viewport interface
export interface Viewport {
width: number;
height: number;
isMobile: boolean;
isTablet: boolean;
isDesktop: boolean;
isLargeDesktop: boolean;
breakpoint: BreakpointKey;
}
// Responsive prop interface
export interface ResponsiveProp<T> {
mobile?: T;
tablet?: T;
desktop?: T;
default?: T;
}
// Visibility options interface
export interface VisibilityOptions {
mobile?: boolean;
tablet?: boolean;
desktop?: boolean;
}
/**
* Get current viewport information (client-side only)
*/
export function getViewport(): Viewport {
if (typeof window === 'undefined') {
return {
width: 0,
height: 0,
isMobile: false,
isTablet: false,
isDesktop: false,
isLargeDesktop: false,
breakpoint: 'xs',
};
}
const width = window.innerWidth;
const height = window.innerHeight;
// Determine breakpoint
let breakpoint: BreakpointKey = 'xs';
if (width >= BREAKPOINTS['3xl']) breakpoint = '3xl';
else if (width >= BREAKPOINTS['2xl']) breakpoint = '2xl';
else if (width >= BREAKPOINTS.xl) breakpoint = 'xl';
else if (width >= BREAKPOINTS.lg) breakpoint = 'lg';
else if (width >= BREAKPOINTS.md) breakpoint = 'md';
else if (width >= BREAKPOINTS.sm) breakpoint = 'sm';
return {
width,
height,
isMobile: width < BREAKPOINTS.md,
isTablet: width >= BREAKPOINTS.md && width < BREAKPOINTS.lg,
isDesktop: width >= BREAKPOINTS.lg,
isLargeDesktop: width >= BREAKPOINTS.xl,
breakpoint,
};
}
/**
* Check if viewport matches specific breakpoint conditions
*/
export function checkBreakpoint(
condition: 'mobile' | 'tablet' | 'desktop' | 'largeDesktop' | BreakpointKey,
viewport: Viewport
): boolean {
const conditions = {
mobile: viewport.isMobile,
tablet: viewport.isTablet,
desktop: viewport.isDesktop,
largeDesktop: viewport.isLargeDesktop,
};
if (condition in conditions) {
return conditions[condition as keyof typeof conditions];
}
// Check specific breakpoint
const targetBreakpoint = BREAKPOINTS[condition as BreakpointKey];
return viewport.width >= targetBreakpoint;
}
/**
* Responsive prop resolver - returns appropriate value based on viewport
*/
export function resolveResponsiveProp<T>(
value: T | ResponsiveProp<T>,
viewport: Viewport
): T {
if (typeof value !== 'object' || value === null) {
return value as T;
}
const prop = value as ResponsiveProp<T>;
if (viewport.isMobile && prop.mobile !== undefined) {
return prop.mobile;
}
if (viewport.isTablet && prop.tablet !== undefined) {
return prop.tablet;
}
if (viewport.isDesktop && prop.desktop !== undefined) {
return prop.desktop;
}
return (prop.default ?? Object.values(prop)[0]) as T;
}
/**
* Generate responsive image sizes attribute
*/
export function generateImageSizes(): string {
return '(max-width: 640px) 100vw, (max-width: 768px) 50vw, (max-width: 1024px) 33vw, 25vw';
}
/**
* Get optimal image dimensions for different breakpoints
*/
export function getImageDimensionsForBreakpoint(
breakpoint: BreakpointKey,
aspectRatio: number = 16 / 9
) {
const baseWidths = {
xs: 400,
sm: 640,
md: 768,
lg: 1024,
xl: 1280,
'2xl': 1400,
'3xl': 1600,
};
const width = baseWidths[breakpoint];
const height = Math.round(width / aspectRatio);
return { width, height };
}
/**
* Generate responsive srcset for images
*/
export function generateSrcset(
baseUrl: string,
formats: string[] = ['webp', 'jpg']
): string {
const sizes = [480, 640, 768, 1024, 1280, 1600];
return formats
.map(format =>
sizes
.map(size => `${baseUrl}-${size}w.${format} ${size}w`)
.join(', ')
)
.join(', ');
}
/**
* Check if element is in viewport (for lazy loading)
*/
export function isInViewport(element: HTMLElement, offset = 0): boolean {
if (!element || typeof window === 'undefined') return false;
const rect = element.getBoundingClientRect();
return (
rect.top >= -offset &&
rect.left >= -offset &&
rect.bottom <= (window.innerHeight + offset) &&
rect.right <= (window.innerWidth + offset)
);
}
/**
* Generate responsive CSS clamp values for typography
*/
export function clamp(
min: number,
preferred: number,
max: number,
unit: 'rem' | 'px' = 'rem'
): string {
const minVal = unit === 'rem' ? `${min}rem` : `${min}px`;
const maxVal = unit === 'rem' ? `${max}rem` : `${max}px`;
const preferredVal = `${preferred}vw`;
return `clamp(${minVal}, ${preferredVal}, ${maxVal})`;
}
/**
* Get touch target size based on device type
*/
export function getTouchTargetSize(isMobile: boolean, isLargeDesktop: boolean): string {
if (isLargeDesktop) return '72px'; // lg
if (isMobile) return '44px'; // sm (minimum)
return '56px'; // md
}
/**
* Responsive spacing utility
*/
export function getResponsiveSpacing(
base: number,
viewport: Viewport,
multiplier: { mobile?: number; tablet?: number; desktop?: number } = {}
): string {
const { isMobile, isTablet, isDesktop } = viewport;
let factor = 1;
if (isMobile) factor = multiplier.mobile ?? 1;
else if (isTablet) factor = multiplier.tablet ?? 1.25;
else if (isDesktop) factor = multiplier.desktop ?? 1.5;
return `${base * factor}rem`;
}
/**
* Generate responsive grid template
*/
export function getResponsiveGrid(
viewport: Viewport,
options: {
mobile?: number;
tablet?: number;
desktop?: number;
gap?: string;
} = {}
): { columns: number; gap: string } {
const { isMobile, isTablet, isDesktop } = viewport;
const columns = isMobile
? (options.mobile ?? 1)
: isTablet
? (options.tablet ?? 2)
: (options.desktop ?? 3);
const gap = options.gap ?? (isMobile ? '1rem' : isTablet ? '1.5rem' : '2rem');
return { columns, gap };
}
/**
* Check if touch device
*/
export function isTouchDevice(): boolean {
if (typeof window === 'undefined') return false;
return (
'ontouchstart' in window ||
navigator.maxTouchPoints > 0 ||
(navigator as any).msMaxTouchPoints > 0
);
}
/**
* Generate responsive meta tag content
*/
export function generateViewportMeta(): string {
return 'width=device-width, initial-scale=1, viewport-fit=cover, maximum-scale=5, minimum-scale=1';
}
/**
* Responsive text truncation with ellipsis
*/
export function truncateText(
text: string,
viewport: Viewport,
maxLength: { mobile?: number; tablet?: number; desktop?: number } = {}
): string {
const limit = viewport.isMobile
? (maxLength.mobile ?? 100)
: viewport.isTablet
? (maxLength.tablet ?? 150)
: (maxLength.desktop ?? 200);
return text.length > limit ? `${text.substring(0, limit)}...` : text;
}
/**
* Calculate optimal line height based on viewport and text size
*/
export function getOptimalLineHeight(
fontSize: number,
viewport: Viewport
): string {
const baseLineHeight = 1.6;
// Tighter line height for mobile to improve readability
if (viewport.isMobile) {
return fontSize < 16 ? '1.5' : '1.4';
}
// More breathing room for larger screens
if (viewport.isDesktop) {
return fontSize > 24 ? '1.3' : '1.5';
}
return baseLineHeight.toString();
}
/**
* Generate responsive CSS custom properties
*/
export function generateResponsiveCSSVars(
prefix: string,
values: {
mobile: Record<string, string>;
tablet?: Record<string, string>;
desktop?: Record<string, string>;
}
): string {
const { mobile, tablet, desktop } = values;
let css = `:root {`;
Object.entries(mobile).forEach(([key, value]) => {
css += `--${prefix}-${key}: ${value};`;
});
css += `}`;
if (tablet) {
css += `@media (min-width: ${BREAKPOINTS.md}px) { :root {`;
Object.entries(tablet).forEach(([key, value]) => {
css += `--${prefix}-${key}: ${value};`;
});
css += `} }`;
}
if (desktop) {
css += `@media (min-width: ${BREAKPOINTS.lg}px) { :root {`;
Object.entries(desktop).forEach(([key, value]) => {
css += `--${prefix}-${key}: ${value};`;
});
css += `} }`;
}
return css;
}
/**
* Calculate responsive offset for sticky elements
*/
export function getStickyOffset(
viewport: Viewport,
elementHeight: number
): number {
if (viewport.isMobile) {
return elementHeight * 0.5;
}
if (viewport.isTablet) {
return elementHeight * 0.75;
}
return elementHeight;
}
/**
* Generate responsive animation duration
*/
export function getResponsiveDuration(
baseDuration: number,
viewport: Viewport
): number {
if (viewport.isMobile) {
return baseDuration * 0.75; // Faster on mobile
}
return baseDuration;
}
/**
* Check if viewport is in safe area for content
*/
export function isContentSafeArea(viewport: Viewport): boolean {
// Ensure minimum content width for readability
const minWidth = 320;
return viewport.width >= minWidth;
}
/**
* Responsive form field width
*/
export function getFormFieldWidth(
viewport: Viewport,
options: { full?: boolean; half?: boolean; third?: boolean } = {}
): string {
if (options.full || viewport.isMobile) return '100%';
if (options.half) return '48%';
if (options.third) return '31%';
return viewport.isTablet ? '48%' : '31%';
}
/**
* Generate responsive accessibility attributes
*/
export function getResponsiveA11yProps(viewport: Viewport) {
return {
// Larger touch targets on mobile
'aria-touch-target': viewport.isMobile ? 'large' : 'standard',
// Mobile-optimized announcements
'aria-mobile-optimized': viewport.isMobile ? 'true' : 'false',
};
}
/**
* Check if viewport width meets minimum requirement
*/
export function meetsMinimumWidth(viewport: Viewport, minWidth: number): boolean {
return viewport.width >= minWidth;
}
/**
* Get responsive column count for grid layouts
*/
export function getResponsiveColumns(viewport: Viewport): number {
if (viewport.isMobile) return 1;
if (viewport.isTablet) return 2;
return 3;
}
/**
* Generate responsive padding based on viewport
*/
export function getResponsivePadding(viewport: Viewport): string {
if (viewport.isMobile) return '1rem';
if (viewport.isTablet) return '1.5rem';
if (viewport.isDesktop) return '2rem';
return '3rem';
}
/**
* Check if viewport is landscape orientation
*/
export function isLandscape(viewport: Viewport): boolean {
return viewport.width > viewport.height;
}
/**
* Get optimal image quality based on viewport
*/
export function getOptimalImageQuality(viewport: Viewport): number {
if (viewport.isMobile) return 75;
if (viewport.isTablet) return 85;
return 90;
}
export default {
BREAKPOINTS,
getViewport,
checkBreakpoint,
resolveResponsiveProp,
generateImageSizes,
getImageDimensionsForBreakpoint,
generateSrcset,
isInViewport,
clamp,
getTouchTargetSize,
getResponsiveSpacing,
getResponsiveGrid,
isTouchDevice,
generateViewportMeta,
truncateText,
getOptimalLineHeight,
generateResponsiveCSSVars,
getStickyOffset,
getResponsiveDuration,
isContentSafeArea,
getFormFieldWidth,
getResponsiveA11yProps,
meetsMinimumWidth,
getResponsiveColumns,
getResponsivePadding,
isLandscape,
getOptimalImageQuality,
};

View File

@@ -1,130 +0,0 @@
import { Metadata } from 'next';
import { getSiteInfo } from './i18n';
import type { Locale } from './i18n';
export interface SEOParams {
title?: string;
description?: string;
locale?: Locale;
canonical?: string;
ogType?: 'website' | 'article' | 'product';
ogImages?: string[];
publishedTime?: string;
updatedTime?: string;
author?: string;
}
export function generateSEOMetadata({
title,
description,
locale = 'en',
canonical,
ogType = 'website',
ogImages = [],
publishedTime,
updatedTime,
author,
}: SEOParams): Metadata {
const site = getSiteInfo(locale);
const pageTitle = title ? `${title} | ${site.title}` : site.title;
const pageDescription = description || site.description;
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || site.baseUrl;
const path = canonical || '/';
const fullUrl = `${baseUrl}${path}`;
// Generate alternate URLs for both locales
const alternates = {
canonical: fullUrl,
languages: {
'en': `${baseUrl}${path.replace('/de/', '/')}`,
'de': `${baseUrl}${path.startsWith('/de') ? path : `/de${path}`}`,
},
};
const openGraph = {
title: pageTitle,
description: pageDescription,
url: fullUrl,
siteName: site.title,
locale: locale === 'de' ? 'de_DE' : 'en_US',
type: ogType,
...(ogImages.length > 0 && { images: ogImages }),
...(publishedTime && { publishedTime }),
...(updatedTime && { updatedTime }),
...(author && { authors: [author] }),
};
const twitter = {
card: 'summary_large_image',
title: pageTitle,
description: pageDescription,
...(ogImages.length > 0 && { images: ogImages }),
};
return {
title: pageTitle,
description: pageDescription,
alternates,
openGraph,
twitter,
authors: author ? [{ name: author }] : undefined,
metadataBase: new URL(baseUrl),
};
}
// Helper for blog posts
export function getPostSEO(post: any, locale: Locale): Metadata {
return generateSEOMetadata({
title: post.title,
description: post.excerptHtml?.replace(/<[^>]*>/g, '') || '',
canonical: post.path,
locale: locale,
ogType: 'article',
ogImages: post.featuredImage ? [post.featuredImage] : [],
publishedTime: post.datePublished,
updatedTime: post.updatedAt,
author: 'KLZ Cables Team',
});
}
// Helper for products
export function getProductSEO(product: any, locale: Locale): Metadata {
return generateSEOMetadata({
title: product.name,
description: product.shortDescriptionHtml?.replace(/<[^>]*>/g, '') || '',
canonical: product.path,
locale: locale,
ogType: 'product',
ogImages: product.images || [],
});
}
// Helper for categories
export function getCategorySEO(category: any, locale: Locale): Metadata {
return generateSEOMetadata({
title: category.name,
description: category.description || `Products in ${category.name}`,
canonical: category.path,
locale: locale,
ogType: 'website',
});
}
export function generateSitemapItem({
path,
lastmod,
priority = 0.7,
}: {
path: string;
lastmod?: string;
priority?: number;
}) {
return {
url: path,
lastmod: lastmod || new Date().toISOString().split('T')[0],
changefreq: 'weekly',
priority,
};
}

View File

@@ -1,87 +0,0 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
/**
* Utility function to merge Tailwind CSS classes with clsx support
* Handles class merging, conflict resolution, and conditional classes
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
/**
* Utility function to check if a value is not null or undefined
*/
export function isNonNullable<T>(value: T | null | undefined): value is T {
return value != null;
}
/**
* Utility function to format currency
*/
export function formatCurrency(amount: number, currency: string = 'EUR', locale: string = 'de-DE'): string {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(amount);
}
/**
* Utility function to format date
*/
export function formatDate(date: Date | string, locale: string = 'de-DE'): string {
const d = typeof date === 'string' ? new Date(date) : date;
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(d);
}
/**
* Utility function to generate slug from text
*/
export function generateSlug(text: string): string {
return text
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim();
}
/**
* Utility function to debounce function calls
*/
export function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout;
return (...args: Parameters<T>) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
/**
* Utility function to get initials from a name
*/
export function getInitials(name: string): string {
return name
.split(' ')
.map(part => part[0])
.join('')
.toUpperCase()
.slice(0, 2);
}
/**
* Utility function to truncate text
*/
export function truncate(text: string, maxLength: number, suffix = '...'): string {
if (text.length <= maxLength) return text;
return text.slice(0, maxLength - suffix.length) + suffix;
}