mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 02:23:47 +00:00
feat: Linear-style UI redesign with improved mobile experience
Major changes: - Redesign cabinet with Linear-style components and top navigation - Replace detail modals with dedicated pages (users, broadcasts, email templates) - Add email channel support for broadcasts - Remove pull-to-refresh, improve drag-and-drop on touch devices - Fix Telegram Mini App: fullscreen, back button, scroll restoration - Unify admin pages color scheme to design system - Mobile-first improvements: horizontal tabs for settings, better touch targets
This commit is contained in:
187
src/hooks/useUserThemePreferences.ts
Normal file
187
src/hooks/useUserThemePreferences.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { usePlatform } from '@/platform';
|
||||
import {
|
||||
type UserThemePreferences,
|
||||
type BorderRadiusPreset,
|
||||
type ThemeMode,
|
||||
DEFAULT_USER_PREFERENCES,
|
||||
BORDER_RADIUS_VALUES,
|
||||
} from '../types/theme';
|
||||
|
||||
const STORAGE_KEY = 'user_theme_preferences';
|
||||
|
||||
/**
|
||||
* Parse preferences from storage string
|
||||
*/
|
||||
function parsePreferences(value: string | null): UserThemePreferences {
|
||||
if (!value) return DEFAULT_USER_PREFERENCES;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return {
|
||||
theme: parsed.theme ?? DEFAULT_USER_PREFERENCES.theme,
|
||||
borderRadius: parsed.borderRadius ?? DEFAULT_USER_PREFERENCES.borderRadius,
|
||||
animationsEnabled: parsed.animationsEnabled ?? DEFAULT_USER_PREFERENCES.animationsEnabled,
|
||||
};
|
||||
} catch {
|
||||
return DEFAULT_USER_PREFERENCES;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply CSS variables to document
|
||||
*/
|
||||
function applyPreferencesToDOM(preferences: UserThemePreferences): void {
|
||||
const root = document.documentElement;
|
||||
|
||||
// Apply border radius
|
||||
root.style.setProperty('--bento-radius', BORDER_RADIUS_VALUES[preferences.borderRadius]);
|
||||
|
||||
// Apply animations toggle
|
||||
if (preferences.animationsEnabled) {
|
||||
root.classList.remove('reduce-motion');
|
||||
} else {
|
||||
root.classList.add('reduce-motion');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to manage user theme preferences
|
||||
* Stores in localStorage and optionally syncs with Telegram CloudStorage
|
||||
*/
|
||||
export function useUserThemePreferences() {
|
||||
const { cloudStorage, capabilities } = usePlatform();
|
||||
const [preferences, setPreferencesState] =
|
||||
useState<UserThemePreferences>(DEFAULT_USER_PREFERENCES);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Load preferences on mount
|
||||
useEffect(() => {
|
||||
async function loadPreferences() {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// Try Telegram CloudStorage first if available
|
||||
if (capabilities.hasCloudStorage && cloudStorage) {
|
||||
const cloudValue = await cloudStorage.getItem(STORAGE_KEY);
|
||||
if (cloudValue) {
|
||||
const prefs = parsePreferences(cloudValue);
|
||||
setPreferencesState(prefs);
|
||||
applyPreferencesToDOM(prefs);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to localStorage
|
||||
const localValue = localStorage.getItem(STORAGE_KEY);
|
||||
const prefs = parsePreferences(localValue);
|
||||
setPreferencesState(prefs);
|
||||
applyPreferencesToDOM(prefs);
|
||||
} catch (error) {
|
||||
console.warn('Failed to load theme preferences:', error);
|
||||
applyPreferencesToDOM(DEFAULT_USER_PREFERENCES);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
loadPreferences();
|
||||
}, [cloudStorage, capabilities.hasCloudStorage]);
|
||||
|
||||
// Save preferences
|
||||
const savePreferences = useCallback(
|
||||
async (newPreferences: UserThemePreferences) => {
|
||||
const value = JSON.stringify(newPreferences);
|
||||
|
||||
// Save to localStorage
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, value);
|
||||
} catch (error) {
|
||||
console.warn('Failed to save to localStorage:', error);
|
||||
}
|
||||
|
||||
// Sync to Telegram CloudStorage if available
|
||||
if (capabilities.hasCloudStorage && cloudStorage) {
|
||||
try {
|
||||
await cloudStorage.setItem(STORAGE_KEY, value);
|
||||
} catch (error) {
|
||||
console.warn('Failed to sync to CloudStorage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply to DOM
|
||||
applyPreferencesToDOM(newPreferences);
|
||||
setPreferencesState(newPreferences);
|
||||
},
|
||||
[cloudStorage, capabilities.hasCloudStorage],
|
||||
);
|
||||
|
||||
// Update individual preference
|
||||
const updatePreference = useCallback(
|
||||
<K extends keyof UserThemePreferences>(key: K, value: UserThemePreferences[K]) => {
|
||||
const newPreferences = { ...preferences, [key]: value };
|
||||
savePreferences(newPreferences);
|
||||
},
|
||||
[preferences, savePreferences],
|
||||
);
|
||||
|
||||
// Convenience setters
|
||||
const setTheme = useCallback(
|
||||
(theme: ThemeMode) => updatePreference('theme', theme),
|
||||
[updatePreference],
|
||||
);
|
||||
|
||||
const setBorderRadius = useCallback(
|
||||
(borderRadius: BorderRadiusPreset) => updatePreference('borderRadius', borderRadius),
|
||||
[updatePreference],
|
||||
);
|
||||
|
||||
const setAnimationsEnabled = useCallback(
|
||||
(enabled: boolean) => updatePreference('animationsEnabled', enabled),
|
||||
[updatePreference],
|
||||
);
|
||||
|
||||
// Reset to defaults
|
||||
const resetPreferences = useCallback(() => {
|
||||
savePreferences(DEFAULT_USER_PREFERENCES);
|
||||
}, [savePreferences]);
|
||||
|
||||
return {
|
||||
preferences,
|
||||
isLoading,
|
||||
setTheme,
|
||||
setBorderRadius,
|
||||
setAnimationsEnabled,
|
||||
updatePreference,
|
||||
resetPreferences,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get resolved theme (respects system preference when set to 'system')
|
||||
*/
|
||||
export function useResolvedTheme() {
|
||||
const { preferences } = useUserThemePreferences();
|
||||
const [systemTheme, setSystemTheme] = useState<'dark' | 'light'>('dark');
|
||||
|
||||
useEffect(() => {
|
||||
// Get initial system preference
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
setSystemTheme(mediaQuery.matches ? 'dark' : 'light');
|
||||
|
||||
// Listen for changes
|
||||
const handler = (e: MediaQueryListEvent) => {
|
||||
setSystemTheme(e.matches ? 'dark' : 'light');
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener('change', handler);
|
||||
return () => mediaQuery.removeEventListener('change', handler);
|
||||
}, []);
|
||||
|
||||
if (preferences.theme === 'system') {
|
||||
return systemTheme;
|
||||
}
|
||||
|
||||
return preferences.theme;
|
||||
}
|
||||
Reference in New Issue
Block a user