cleanup
This commit is contained in:
353
lib/data.ts
353
lib/data.ts
@@ -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));
|
||||
}
|
||||
1268
lib/html-compat.ts
1268
lib/html-compat.ts
File diff suppressed because it is too large
Load Diff
@@ -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';
|
||||
339
lib/i18n.ts
339
lib/i18n.ts
@@ -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];
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
130
lib/seo.ts
130
lib/seo.ts
@@ -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,
|
||||
};
|
||||
}
|
||||
87
lib/utils.ts
87
lib/utils.ts
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user