RYP / src /lib /preferences.ts
Soumya79's picture
Upload 1361 files
f91a684 verified
export type ThemeMode = 'dark' | 'light';
export type InterfaceLanguage = 'english' | 'spanish' | 'french' | 'german';
export type FontSizePreference = 'small' | 'medium' | 'large';
export type DeliveryAddress = {
fullName: string;
phone: string;
street: string;
city: string;
state: string;
pincode: string;
country: string;
};
export type UserPreferences = {
themeMode: ThemeMode;
language: InterfaceLanguage;
fontSize: FontSizePreference;
emailNotifications: boolean;
pushNotifications: boolean;
soundEnabled: boolean;
autoSave: boolean;
twoFactorEnabled: boolean;
twoFactorCode: string;
dataAnalytics: boolean;
address: DeliveryAddress;
updatedAt: string;
};
type ImportedPreferencesPayload = Partial<UserPreferences> & {
version?: number;
exportedAt?: string;
};
export type AnalyticsEvent = {
type: string;
createdAt: string;
payload?: Record<string, string | number | boolean | null>;
};
const PREFERENCES_STORAGE_PREFIX = 'ryp_preferences';
const ANALYTICS_STORAGE_PREFIX = 'ryp_analytics';
const LEGACY_ADDRESS_KEY = 'cq_delivery_address';
const LEGACY_KEYS = {
emailNotifications: 'emailNotifications',
pushNotifications: 'pushNotifications',
soundEnabled: 'soundEnabled',
autoSave: 'autoSave',
twoFactorEnabled: 'twoFactorEnabled',
dataAnalytics: 'dataAnalytics',
language: 'language',
fontSize: 'fontSize',
themeMode: 'theme_mode',
} as const;
export const SETTINGS_EXPORT_VERSION = 1;
export const EMPTY_ADDRESS: DeliveryAddress = {
fullName: '',
phone: '',
street: '',
city: '',
state: '',
pincode: '',
country: 'India',
};
export function createDefaultPreferences(themeMode: ThemeMode = 'dark'): UserPreferences {
return {
themeMode,
language: 'english',
fontSize: 'medium',
emailNotifications: true,
pushNotifications: true,
soundEnabled: true,
autoSave: true,
twoFactorEnabled: false,
twoFactorCode: '',
dataAnalytics: true,
address: { ...EMPTY_ADDRESS },
updatedAt: new Date().toISOString(),
};
}
function preferencesStorageKey(userId: string) {
return `${PREFERENCES_STORAGE_PREFIX}_${userId}`;
}
function analyticsStorageKey(userId: string) {
return `${ANALYTICS_STORAGE_PREFIX}_${userId}`;
}
export function userScopedStorageKey(scope: string, userId: string) {
return `ryp_${scope}_${userId}`;
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
function normalizeBoolean(value: unknown, fallback: boolean) {
return typeof value === 'boolean' ? value : fallback;
}
function normalizeThemeMode(value: unknown, fallback: ThemeMode): ThemeMode {
return value === 'light' || value === 'dark' ? value : fallback;
}
function normalizeLanguage(value: unknown, fallback: InterfaceLanguage): InterfaceLanguage {
return value === 'english' || value === 'spanish' || value === 'french' || value === 'german'
? value
: fallback;
}
function normalizeFontSize(value: unknown, fallback: FontSizePreference): FontSizePreference {
return value === 'small' || value === 'medium' || value === 'large' ? value : fallback;
}
export function normalizeAddress(value: unknown): DeliveryAddress {
if (!isPlainObject(value)) {
return { ...EMPTY_ADDRESS };
}
return {
fullName: typeof value.fullName === 'string' ? value.fullName : '',
phone: typeof value.phone === 'string' ? value.phone : '',
street: typeof value.street === 'string' ? value.street : '',
city: typeof value.city === 'string' ? value.city : '',
state: typeof value.state === 'string' ? value.state : '',
pincode: typeof value.pincode === 'string' ? value.pincode : '',
country: typeof value.country === 'string' && value.country.trim() ? value.country : EMPTY_ADDRESS.country,
};
}
export function normalizePreferences(
value: unknown,
fallbackThemeMode: ThemeMode = 'dark',
): UserPreferences {
const defaults = createDefaultPreferences(fallbackThemeMode);
if (!isPlainObject(value)) {
return defaults;
}
const twoFactorEnabled = normalizeBoolean(value.twoFactorEnabled, defaults.twoFactorEnabled);
const twoFactorCode = typeof value.twoFactorCode === 'string' ? value.twoFactorCode.trim() : '';
return {
themeMode: normalizeThemeMode(value.themeMode, defaults.themeMode),
language: normalizeLanguage(value.language, defaults.language),
fontSize: normalizeFontSize(value.fontSize, defaults.fontSize),
emailNotifications: normalizeBoolean(value.emailNotifications, defaults.emailNotifications),
pushNotifications: normalizeBoolean(value.pushNotifications, defaults.pushNotifications),
soundEnabled: normalizeBoolean(value.soundEnabled, defaults.soundEnabled),
autoSave: normalizeBoolean(value.autoSave, defaults.autoSave),
twoFactorEnabled: twoFactorEnabled && twoFactorCode.length >= 6,
twoFactorCode,
dataAnalytics: normalizeBoolean(value.dataAnalytics, defaults.dataAnalytics),
address: normalizeAddress(value.address),
updatedAt: typeof value.updatedAt === 'string' && value.updatedAt.trim()
? value.updatedAt
: defaults.updatedAt,
};
}
function readJson<T>(key: string): T | null {
if (typeof window === 'undefined') {
return null;
}
const raw = window.localStorage.getItem(key);
if (!raw) {
return null;
}
try {
return JSON.parse(raw) as T;
} catch {
return null;
}
}
function writeJson(key: string, value: unknown) {
if (typeof window === 'undefined') {
return;
}
window.localStorage.setItem(key, JSON.stringify(value));
}
function removeValue(key: string) {
if (typeof window === 'undefined') {
return;
}
window.localStorage.removeItem(key);
}
function readLegacyPreferences(themeMode: ThemeMode): UserPreferences {
if (typeof window === 'undefined') {
return createDefaultPreferences(themeMode);
}
const defaults = createDefaultPreferences(themeMode);
const legacyAddress = normalizeAddress(readJson<DeliveryAddress>(LEGACY_ADDRESS_KEY));
return {
...defaults,
themeMode: normalizeThemeMode(window.localStorage.getItem(LEGACY_KEYS.themeMode), defaults.themeMode),
language: normalizeLanguage(window.localStorage.getItem(LEGACY_KEYS.language), defaults.language),
fontSize: normalizeFontSize(window.localStorage.getItem(LEGACY_KEYS.fontSize), defaults.fontSize),
emailNotifications: window.localStorage.getItem(LEGACY_KEYS.emailNotifications) !== 'false',
pushNotifications: window.localStorage.getItem(LEGACY_KEYS.pushNotifications) !== 'false',
soundEnabled: window.localStorage.getItem(LEGACY_KEYS.soundEnabled) !== 'false',
autoSave: window.localStorage.getItem(LEGACY_KEYS.autoSave) !== 'false',
twoFactorEnabled: window.localStorage.getItem(LEGACY_KEYS.twoFactorEnabled) === 'true',
dataAnalytics: window.localStorage.getItem(LEGACY_KEYS.dataAnalytics) !== 'false',
address: legacyAddress,
};
}
function clearLegacySettings() {
if (typeof window === 'undefined') {
return;
}
Object.values(LEGACY_KEYS).forEach((key) => {
window.localStorage.removeItem(key);
});
window.localStorage.removeItem(LEGACY_ADDRESS_KEY);
}
export function loadUserPreferences(userId: string, fallbackThemeMode: ThemeMode = 'dark') {
const stored = readJson<ImportedPreferencesPayload>(preferencesStorageKey(userId));
if (stored) {
return normalizePreferences(stored, fallbackThemeMode);
}
const migrated = readLegacyPreferences(fallbackThemeMode);
saveUserPreferences(userId, migrated);
clearLegacySettings();
return migrated;
}
export function saveUserPreferences(userId: string, preferences: UserPreferences) {
const normalized = normalizePreferences(preferences, preferences.themeMode);
normalized.updatedAt = new Date().toISOString();
writeJson(preferencesStorageKey(userId), normalized);
return normalized;
}
export function resetUserPreferences(userId: string, themeMode: ThemeMode = 'dark') {
const defaults = createDefaultPreferences(themeMode);
writeJson(preferencesStorageKey(userId), defaults);
return defaults;
}
export function exportUserPreferences(preferences: UserPreferences) {
return {
version: SETTINGS_EXPORT_VERSION,
exportedAt: new Date().toISOString(),
...preferences,
};
}
export function parseImportedPreferences(
rawText: string,
fallbackThemeMode: ThemeMode = 'dark',
) {
const parsed = JSON.parse(rawText) as ImportedPreferencesPayload;
return normalizePreferences(parsed, fallbackThemeMode);
}
export function createTwoFactorCode() {
return String(Math.floor(100000 + Math.random() * 900000));
}
export function fontSizeToRootRem(fontSize: FontSizePreference) {
if (fontSize === 'small') {
return '14px';
}
if (fontSize === 'large') {
return '18px';
}
return '16px';
}
export function getCodeEditorFontSize(fontSize: FontSizePreference) {
if (fontSize === 'small') {
return 13;
}
if (fontSize === 'large') {
return 17;
}
return 15;
}
export function trackAnalyticsEvent(
userId: string,
enabled: boolean,
type: string,
payload?: Record<string, string | number | boolean | null>,
) {
if (!enabled || typeof window === 'undefined') {
return;
}
const current = readJson<AnalyticsEvent[]>(analyticsStorageKey(userId)) ?? [];
const next = [
{
type,
payload,
createdAt: new Date().toISOString(),
},
...current,
].slice(0, 50);
writeJson(analyticsStorageKey(userId), next);
}
export function loadAnalyticsEvents(userId: string) {
return readJson<AnalyticsEvent[]>(analyticsStorageKey(userId)) ?? [];
}
export function clearAnalyticsEvents(userId: string) {
removeValue(analyticsStorageKey(userId));
}
export function saveUserScopedJson(scope: string, userId: string, value: unknown) {
writeJson(userScopedStorageKey(scope, userId), value);
}
export function loadUserScopedJson<T>(scope: string, userId: string) {
return readJson<T>(userScopedStorageKey(scope, userId));
}
export function removeUserScopedJson(scope: string, userId: string) {
removeValue(userScopedStorageKey(scope, userId));
}
export async function requestBrowserNotificationPermission() {
if (typeof window === 'undefined' || !('Notification' in window)) {
return 'unsupported' as const;
}
if (Notification.permission === 'granted') {
return 'granted' as const;
}
if (Notification.permission === 'denied') {
return 'denied' as const;
}
return Notification.requestPermission();
}
export function sendBrowserNotification(
enabled: boolean,
title: string,
options?: NotificationOptions,
) {
if (
!enabled ||
typeof window === 'undefined' ||
!('Notification' in window) ||
Notification.permission !== 'granted'
) {
return;
}
try {
const notification = new Notification(title, options);
window.setTimeout(() => notification.close(), 5000);
} catch {
// Ignore browser notification failures.
}
}
export function playUiSound(enabled: boolean, variant: 'success' | 'error' | 'soft' = 'soft') {
if (!enabled || typeof window === 'undefined') {
return;
}
const AudioCtor = window.AudioContext || (window as typeof window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
if (!AudioCtor) {
return;
}
const context = new AudioCtor();
const oscillator = context.createOscillator();
const gain = context.createGain();
const now = context.currentTime;
oscillator.type = variant === 'error' ? 'sawtooth' : 'sine';
oscillator.frequency.setValueAtTime(variant === 'error' ? 220 : variant === 'success' ? 660 : 440, now);
gain.gain.setValueAtTime(0.0001, now);
gain.gain.exponentialRampToValueAtTime(0.05, now + 0.02);
gain.gain.exponentialRampToValueAtTime(0.0001, now + (variant === 'soft' ? 0.12 : 0.18));
oscillator.connect(gain);
gain.connect(context.destination);
oscillator.start(now);
oscillator.stop(now + (variant === 'soft' ? 0.12 : 0.18));
window.setTimeout(() => {
void context.close().catch(() => undefined);
}, variant === 'soft' ? 180 : 260);
}