the-tip-top-frontend/utils/helpers.ts
soufiane de643c17d0 fix: remove Math.random() completely from generateId
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>
2025-11-27 13:01:51 +01:00

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);
}
};
}