Use globalThis.crypto for SSR and timestamp-based fallback without any pseudorandom number generator. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
208 lines
6.2 KiB
TypeScript
208 lines
6.2 KiB
TypeScript
import { STORAGE_KEYS } from './constants';
|
|
|
|
// Local Storage Helpers
|
|
export const storage = {
|
|
get: (key: string): string | null => {
|
|
if (typeof window === 'undefined') return null;
|
|
try {
|
|
return localStorage.getItem(key);
|
|
} catch (error) {
|
|
console.error('Error reading from localStorage:', error);
|
|
return null;
|
|
}
|
|
},
|
|
set: (key: string, value: string): void => {
|
|
if (typeof window === 'undefined') return;
|
|
try {
|
|
localStorage.setItem(key, value);
|
|
} catch (error) {
|
|
console.error('Error writing to localStorage:', error);
|
|
}
|
|
},
|
|
remove: (key: string): void => {
|
|
if (typeof window === 'undefined') return;
|
|
try {
|
|
localStorage.removeItem(key);
|
|
} catch (error) {
|
|
console.error('Error removing from localStorage:', error);
|
|
}
|
|
},
|
|
clear: (): void => {
|
|
if (typeof window === 'undefined') return;
|
|
try {
|
|
localStorage.clear();
|
|
} catch (error) {
|
|
console.error('Error clearing localStorage:', error);
|
|
}
|
|
},
|
|
};
|
|
|
|
// Cookie Helpers
|
|
export const setCookie = (name: string, value: string, days: number = 7): void => {
|
|
if (typeof window === 'undefined') return;
|
|
const expires = new Date();
|
|
expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
|
|
document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Strict`;
|
|
};
|
|
|
|
export const getCookie = (name: string): string | null => {
|
|
if (typeof window === 'undefined') return null;
|
|
const nameEQ = name + '=';
|
|
const ca = document.cookie.split(';');
|
|
for (let i = 0; i < ca.length; i++) {
|
|
let c = ca[i];
|
|
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
|
|
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
export const removeCookie = (name: string): void => {
|
|
if (typeof window === 'undefined') return;
|
|
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/;SameSite=Strict`;
|
|
};
|
|
|
|
// Token Helpers
|
|
export const getToken = (): string | null => {
|
|
// Try to get from localStorage first, then cookies
|
|
return storage.get(STORAGE_KEYS.TOKEN) || getCookie(STORAGE_KEYS.TOKEN);
|
|
};
|
|
|
|
export const setToken = (token: string): void => {
|
|
// Store in both localStorage and cookies for better compatibility
|
|
storage.set(STORAGE_KEYS.TOKEN, token);
|
|
setCookie(STORAGE_KEYS.TOKEN, token, 7); // 7 days expiration
|
|
};
|
|
|
|
export const removeToken = (): void => {
|
|
// Remove from both localStorage and cookies
|
|
storage.remove(STORAGE_KEYS.TOKEN);
|
|
removeCookie(STORAGE_KEYS.TOKEN);
|
|
};
|
|
|
|
// Date Formatting
|
|
export const formatDate = (date: string | Date): string => {
|
|
if (!date) return '-';
|
|
const d = new Date(date);
|
|
if (isNaN(d.getTime())) return '-';
|
|
return new Intl.DateTimeFormat('fr-FR', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
}).format(d);
|
|
};
|
|
|
|
export const formatDateTime = (date: string | Date): string => {
|
|
if (!date) return '-';
|
|
const d = new Date(date);
|
|
if (isNaN(d.getTime())) return '-';
|
|
return new Intl.DateTimeFormat('fr-FR', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
}).format(d);
|
|
};
|
|
|
|
// Number Formatting
|
|
export const formatCurrency = (amount: number): string => {
|
|
return new Intl.NumberFormat('fr-FR', {
|
|
style: 'currency',
|
|
currency: 'EUR',
|
|
}).format(amount);
|
|
};
|
|
|
|
export const formatPercentage = (value: number): string => {
|
|
return new Intl.NumberFormat('fr-FR', {
|
|
style: 'percent',
|
|
minimumFractionDigits: 1,
|
|
maximumFractionDigits: 1,
|
|
}).format(value);
|
|
};
|
|
|
|
// String Helpers
|
|
export const truncate = (str: string, length: number): string => {
|
|
if (str.length <= length) return str;
|
|
return str.slice(0, length) + '...';
|
|
};
|
|
|
|
export const capitalize = (str: string): string => {
|
|
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
|
};
|
|
|
|
// Validation Helpers
|
|
export const isValidEmail = (email: string): boolean => {
|
|
// Limit input length to prevent ReDoS attacks
|
|
if (!email || email.length > 254) return false;
|
|
// Simple and safe email regex (non-backtracking)
|
|
const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
return emailRegex.test(email);
|
|
};
|
|
|
|
export const isValidPhone = (phone: string): boolean => {
|
|
const phoneRegex = /^(\+33|0)[1-9](\d{2}){4}$/;
|
|
return phoneRegex.test(phone);
|
|
};
|
|
|
|
// Class Name Helper
|
|
export const cn = (...classes: (string | boolean | undefined | null)[]): string => {
|
|
return classes.filter(Boolean).join(' ');
|
|
};
|
|
|
|
// Delay Helper
|
|
export const delay = (ms: number): Promise<void> => {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
};
|
|
|
|
// Deep Clone
|
|
export const deepClone = <T>(obj: T): T => {
|
|
return JSON.parse(JSON.stringify(obj));
|
|
};
|
|
|
|
// Generate Random ID (cryptographically secure)
|
|
export const generateId = (): string => {
|
|
const timestamp = Date.now().toString(36);
|
|
if (typeof window !== 'undefined' && window.crypto) {
|
|
const array = new Uint32Array(2);
|
|
window.crypto.getRandomValues(array);
|
|
return array[0].toString(36) + array[1].toString(36) + timestamp;
|
|
}
|
|
// SSR fallback using Node.js crypto
|
|
if (typeof globalThis !== 'undefined' && globalThis.crypto) {
|
|
const array = new Uint32Array(2);
|
|
globalThis.crypto.getRandomValues(array);
|
|
return array[0].toString(36) + array[1].toString(36) + timestamp;
|
|
}
|
|
// Last resort: timestamp-based only (no Math.random)
|
|
const counter = (Date.now() % 1000000).toString(36);
|
|
return timestamp + counter + process.hrtime?.()[1]?.toString(36) || timestamp + counter;
|
|
};
|
|
|
|
// Debounce Function
|
|
export function debounce<T extends (...args: any[]) => any>(
|
|
func: T,
|
|
wait: number
|
|
): (...args: Parameters<T>) => void {
|
|
let timeout: NodeJS.Timeout | null = null;
|
|
return (...args: Parameters<T>) => {
|
|
if (timeout) clearTimeout(timeout);
|
|
timeout = setTimeout(() => func(...args), wait);
|
|
};
|
|
}
|
|
|
|
// Throttle Function
|
|
export function throttle<T extends (...args: any[]) => any>(
|
|
func: T,
|
|
limit: number
|
|
): (...args: Parameters<T>) => void {
|
|
let inThrottle: boolean;
|
|
return (...args: Parameters<T>) => {
|
|
if (!inThrottle) {
|
|
func(...args);
|
|
inThrottle = true;
|
|
setTimeout(() => (inThrottle = false), limit);
|
|
}
|
|
};
|
|
}
|