fix colors
This commit is contained in:
parent
0ca36b69eb
commit
1cbc9d7b8e
6 changed files with 651 additions and 312 deletions
|
@ -6,6 +6,7 @@ import NotificationSettings from "./SettingsSection/NotificationSettings";
|
|||
import DisplaySettings from "./SettingsSection/DisplaySettings";
|
||||
import ResumeSettings from "./SettingsSection/ResumeSettings";
|
||||
import EmailRequestSettings from "./SettingsSection/EmailRequestSettings";
|
||||
import ThemeToggle from "./universal/ThemeToggle";
|
||||
|
||||
// Import environment variables
|
||||
const logtoAppId = import.meta.env.LOGTO_APP_ID;
|
||||
|
@ -19,199 +20,238 @@ const safeLogtoAppId = logtoAppId || "missing_app_id";
|
|||
const safeLogtoAppSecret = logtoAppSecret || "missing_app_secret";
|
||||
const safeLogtoEndpoint = logtoEndpoint || "https://auth.ieeeucsd.org";
|
||||
const safeLogtoTokenEndpoint =
|
||||
logtoTokenEndpoint || "https://auth.ieeeucsd.org/oidc/token";
|
||||
logtoTokenEndpoint || "https://auth.ieeeucsd.org/oidc/token";
|
||||
const safeLogtoApiEndpoint = logtoApiEndpoint || "";
|
||||
---
|
||||
|
||||
<div id="settings-section" class="">
|
||||
<div class="mb-6">
|
||||
<h2 class="text-2xl font-bold">Settings</h2>
|
||||
<p class="opacity-70">Manage your account settings and preferences</p>
|
||||
</div>
|
||||
|
||||
<!-- Debug Environment Variables (Only visible in development) -->
|
||||
{
|
||||
import.meta.env.DEV && (
|
||||
<div class="card bg-base-100 shadow-xl border border-warning mb-6 p-4">
|
||||
<h3 class="text-lg font-bold text-warning">
|
||||
Debug Environment Variables
|
||||
</h3>
|
||||
<p class="text-sm mb-2">
|
||||
This section is only visible in development mode
|
||||
</p>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Value</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>LOGTO_APP_ID</td>
|
||||
<td>{logtoAppId ? "********" : "Not set"}</td>
|
||||
<td>{logtoAppId ? "✅" : "❌"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LOGTO_APP_SECRET</td>
|
||||
<td>{logtoAppSecret ? "********" : "Not set"}</td>
|
||||
<td>{logtoAppSecret ? "✅" : "❌"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LOGTO_ENDPOINT</td>
|
||||
<td>{logtoEndpoint || "Not set"}</td>
|
||||
<td>{logtoEndpoint ? "✅" : "❌"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LOGTO_TOKEN_ENDPOINT</td>
|
||||
<td>{logtoTokenEndpoint || "Not set"}</td>
|
||||
<td>{logtoTokenEndpoint ? "✅" : "❌"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LOGTO_API_ENDPOINT</td>
|
||||
<td>{logtoApiEndpoint || "Not set"}</td>
|
||||
<td>{logtoApiEndpoint ? "✅" : "❌"}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Profile Settings Card -->
|
||||
<div
|
||||
class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-6"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h3 class="card-title flex items-center gap-3">
|
||||
<div class="badge badge-primary p-3">
|
||||
<Icon name="heroicons:user" class="h-5 w-5" />
|
||||
</div>
|
||||
Profile Information
|
||||
</h3>
|
||||
<p class="text-sm opacity-70 mb-4">
|
||||
Update your personal information and profile details
|
||||
</p>
|
||||
<div class="divider"></div>
|
||||
<UserProfileSettings client:load logtoApiEndpoint={logtoApiEndpoint} />
|
||||
<div class="mb-6">
|
||||
<h2 class="text-2xl font-bold">Settings</h2>
|
||||
<p class="opacity-70">Manage your account settings and preferences</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resume Settings Card -->
|
||||
<div
|
||||
id="resume-management-section"
|
||||
class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-6"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h3 class="card-title flex items-center gap-3">
|
||||
<div class="badge badge-primary p-3">
|
||||
<Icon name="heroicons:document-text" class="h-5 w-5" />
|
||||
</div>
|
||||
Resume Management
|
||||
</h3>
|
||||
<p class="text-sm opacity-70 mb-4">
|
||||
Upload and manage your resume for recruiters and career opportunities
|
||||
</p>
|
||||
<div class="divider"></div>
|
||||
<ResumeSettings client:load />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Debug Environment Variables (Only visible in development) -->
|
||||
{
|
||||
import.meta.env.DEV && (
|
||||
<div class="card bg-card shadow-xl border border-warning mb-6 p-4">
|
||||
<h3 class="text-lg font-bold text-warning">
|
||||
Debug Environment Variables
|
||||
</h3>
|
||||
<p class="text-sm mb-2">
|
||||
This section is only visible in development mode
|
||||
</p>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table-auto w-full text-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Value</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>LOGTO_APP_ID</td>
|
||||
<td>{logtoAppId ? "********" : "Not set"}</td>
|
||||
<td>{logtoAppId ? "✅" : "❌"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LOGTO_APP_SECRET</td>
|
||||
<td>
|
||||
{logtoAppSecret ? "********" : "Not set"}
|
||||
</td>
|
||||
<td>{logtoAppSecret ? "✅" : "❌"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LOGTO_ENDPOINT</td>
|
||||
<td>{logtoEndpoint || "Not set"}</td>
|
||||
<td>{logtoEndpoint ? "✅" : "❌"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LOGTO_TOKEN_ENDPOINT</td>
|
||||
<td>{logtoTokenEndpoint || "Not set"}</td>
|
||||
<td>{logtoTokenEndpoint ? "✅" : "❌"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LOGTO_API_ENDPOINT</td>
|
||||
<td>{logtoApiEndpoint || "Not set"}</td>
|
||||
<td>{logtoApiEndpoint ? "✅" : "❌"}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- IEEE Email Request Card -->
|
||||
<div
|
||||
class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-6"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h3 class="card-title flex items-center gap-3">
|
||||
<div class="badge badge-primary p-3">
|
||||
<Icon name="heroicons:envelope" class="h-5 w-5" />
|
||||
</div>
|
||||
IEEE Email Address
|
||||
</h3>
|
||||
<p class="text-sm opacity-70 mb-4">
|
||||
Request an official IEEE UCSD email address (officers only)
|
||||
</p>
|
||||
<div class="divider"></div>
|
||||
<EmailRequestSettings client:load />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Account Security Settings Card -->
|
||||
<div
|
||||
class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-6"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h3 class="card-title flex items-center gap-3">
|
||||
<div class="badge badge-primary p-3">
|
||||
<Icon name="heroicons:lock-closed" class="h-5 w-5" />
|
||||
</div>
|
||||
Account Security
|
||||
</h3>
|
||||
<p class="text-sm opacity-70 mb-4">
|
||||
Manage your account security settings and authentication options
|
||||
</p>
|
||||
<div class="divider"></div>
|
||||
<AccountSecuritySettings
|
||||
client:load
|
||||
logtoAppId={safeLogtoAppId}
|
||||
logtoAppSecret={safeLogtoAppSecret}
|
||||
logtoEndpoint={safeLogtoEndpoint}
|
||||
logtoTokenEndpoint={safeLogtoTokenEndpoint}
|
||||
logtoApiEndpoint={safeLogtoApiEndpoint}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notification Settings Card -->
|
||||
<div
|
||||
class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-6 relative group"
|
||||
>
|
||||
<!-- Coming Soon Overlay -->
|
||||
<!-- Profile Settings Card -->
|
||||
<div
|
||||
class="absolute inset-0 bg-base-300 bg-opacity-90 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300 z-10 rounded-xl"
|
||||
class="card bg-card shadow-xl border border-border hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-6"
|
||||
>
|
||||
<div class="text-center">
|
||||
<h4 class="text-xl font-bold">Coming Soon</h4>
|
||||
<p class="text-sm opacity-70">
|
||||
Notification settings will be available in a future update
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3 class="card-title flex items-center gap-3">
|
||||
<div
|
||||
class="inline-flex items-center justify-center p-3 rounded-full bg-primary text-primary-foreground"
|
||||
>
|
||||
<Icon name="heroicons:user" class="h-5 w-5" />
|
||||
</div>
|
||||
Profile Information
|
||||
</h3>
|
||||
<p class="text-sm opacity-70 mb-4">
|
||||
Update your personal information and profile details
|
||||
</p>
|
||||
<div class="h-px w-full bg-border my-4"></div>
|
||||
<UserProfileSettings
|
||||
client:load
|
||||
logtoApiEndpoint={logtoApiEndpoint}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<h3 class="card-title flex items-center gap-3">
|
||||
<div class="badge badge-primary p-3">
|
||||
<Icon name="heroicons:bell" class="h-5 w-5" />
|
||||
<!-- Resume Settings Card -->
|
||||
<div
|
||||
id="resume-management-section"
|
||||
class="card bg-card shadow-xl border border-border hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-6"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h3 class="card-title flex items-center gap-3">
|
||||
<div
|
||||
class="inline-flex items-center justify-center p-3 rounded-full bg-primary text-primary-foreground"
|
||||
>
|
||||
<Icon name="heroicons:document-text" class="h-5 w-5" />
|
||||
</div>
|
||||
Resume Management
|
||||
</h3>
|
||||
<p class="text-sm opacity-70 mb-4">
|
||||
Upload and manage your resume for recruiters and career
|
||||
opportunities
|
||||
</p>
|
||||
<div class="h-px w-full bg-border my-4"></div>
|
||||
<ResumeSettings client:load />
|
||||
</div>
|
||||
Notification Preferences
|
||||
</h3>
|
||||
<p class="text-sm opacity-70 mb-4">
|
||||
Customize how and when you receive notifications
|
||||
</p>
|
||||
<div class="divider"></div>
|
||||
<NotificationSettings client:load />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Display Settings Card -->
|
||||
<div
|
||||
class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h3 class="card-title flex items-center gap-3">
|
||||
<div class="badge badge-primary p-3">
|
||||
<Icon name="heroicons:computer-desktop" class="h-5 w-5" />
|
||||
<!-- IEEE Email Request Card -->
|
||||
<div
|
||||
class="card bg-card shadow-xl border border-border hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-6"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h3 class="card-title flex items-center gap-3">
|
||||
<div
|
||||
class="inline-flex items-center justify-center p-3 rounded-full bg-primary text-primary-foreground"
|
||||
>
|
||||
<Icon name="heroicons:envelope" class="h-5 w-5" />
|
||||
</div>
|
||||
IEEE Email Address
|
||||
</h3>
|
||||
<p class="text-sm opacity-70 mb-4">
|
||||
Request an official IEEE UCSD email address (officers only)
|
||||
</p>
|
||||
<div class="h-px w-full bg-border my-4"></div>
|
||||
<EmailRequestSettings client:load />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Account Security Settings Card -->
|
||||
<div
|
||||
class="card bg-card shadow-xl border border-border hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-6"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h3 class="card-title flex items-center gap-3">
|
||||
<div
|
||||
class="inline-flex items-center justify-center p-3 rounded-full bg-primary text-primary-foreground"
|
||||
>
|
||||
<Icon name="heroicons:lock-closed" class="h-5 w-5" />
|
||||
</div>
|
||||
Account Security
|
||||
</h3>
|
||||
<p class="text-sm opacity-70 mb-4">
|
||||
Manage your account security settings and authentication options
|
||||
</p>
|
||||
<div class="h-px w-full bg-border my-4"></div>
|
||||
<AccountSecuritySettings
|
||||
client:load
|
||||
logtoAppId={safeLogtoAppId}
|
||||
logtoAppSecret={safeLogtoAppSecret}
|
||||
logtoEndpoint={safeLogtoEndpoint}
|
||||
logtoTokenEndpoint={safeLogtoTokenEndpoint}
|
||||
logtoApiEndpoint={safeLogtoApiEndpoint}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notification Settings Card -->
|
||||
<div
|
||||
class="card bg-card shadow-xl border border-border hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-6 relative group"
|
||||
>
|
||||
<!-- Coming Soon Overlay -->
|
||||
<div
|
||||
class="absolute inset-0 bg-muted bg-opacity-90 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300 z-10 rounded-xl"
|
||||
>
|
||||
<div class="text-center">
|
||||
<h4 class="text-xl font-bold">Coming Soon</h4>
|
||||
<p class="text-sm opacity-70">
|
||||
Notification settings will be available in a future update
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<h3 class="card-title flex items-center gap-3">
|
||||
<div
|
||||
class="inline-flex items-center justify-center p-3 rounded-full bg-primary text-primary-foreground"
|
||||
>
|
||||
<Icon name="heroicons:bell" class="h-5 w-5" />
|
||||
</div>
|
||||
Notification Preferences
|
||||
</h3>
|
||||
<p class="text-sm opacity-70 mb-4">
|
||||
Customize how and when you receive notifications
|
||||
</p>
|
||||
<div class="h-px w-full bg-border my-4"></div>
|
||||
<NotificationSettings client:load />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Display Settings Card -->
|
||||
<div
|
||||
class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h3 class="card-title flex items-center gap-3">
|
||||
<div class="badge badge-primary p-3">
|
||||
<Icon name="heroicons:computer-desktop" class="h-5 w-5" />
|
||||
</div>
|
||||
Display Settings
|
||||
</h3>
|
||||
<p class="text-sm opacity-70 mb-4">
|
||||
Customize your dashboard appearance and display preferences
|
||||
</p>
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="alert alert-warning mb-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
></path></svg
|
||||
>
|
||||
<div>
|
||||
<h3 class="font-bold">Light Mode Experimental</h3>
|
||||
<p class="text-sm">
|
||||
Light mode is still experimental and some UI elements
|
||||
may not display correctly.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DisplaySettings client:load />
|
||||
</div>
|
||||
Display Settings
|
||||
</h3>
|
||||
<p class="text-sm opacity-70 mb-4">
|
||||
Customize your dashboard appearance and display preferences
|
||||
</p>
|
||||
<div class="divider"></div>
|
||||
<DisplaySettings client:load />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,59 +3,44 @@ import { Authentication } from '../../../scripts/pocketbase/Authentication';
|
|||
import { Update } from '../../../scripts/pocketbase/Update';
|
||||
import { Collections } from '../../../schemas/pocketbase/schema';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
// Default display preferences
|
||||
const DEFAULT_DISPLAY_PREFERENCES = {
|
||||
theme: 'dark',
|
||||
fontSize: 'medium'
|
||||
};
|
||||
|
||||
// Default accessibility settings
|
||||
const DEFAULT_ACCESSIBILITY_SETTINGS = {
|
||||
colorBlindMode: false,
|
||||
reducedMotion: false
|
||||
};
|
||||
import { ThemeService, DEFAULT_THEME_SETTINGS, type ThemeSettings } from '../../../scripts/database/ThemeService';
|
||||
|
||||
export default function DisplaySettings() {
|
||||
const auth = Authentication.getInstance();
|
||||
const update = Update.getInstance();
|
||||
const [theme, setTheme] = useState(DEFAULT_DISPLAY_PREFERENCES.theme);
|
||||
const [fontSize, setFontSize] = useState(DEFAULT_DISPLAY_PREFERENCES.fontSize);
|
||||
const [colorBlindMode, setColorBlindMode] = useState(DEFAULT_ACCESSIBILITY_SETTINGS.colorBlindMode);
|
||||
const [reducedMotion, setReducedMotion] = useState(DEFAULT_ACCESSIBILITY_SETTINGS.reducedMotion);
|
||||
const themeService = ThemeService.getInstance();
|
||||
|
||||
// Current applied settings
|
||||
const [currentSettings, setCurrentSettings] = useState<ThemeSettings | null>(null);
|
||||
|
||||
// Form state (unsaved changes)
|
||||
const [theme, setTheme] = useState<'light' | 'dark'>(DEFAULT_THEME_SETTINGS.theme);
|
||||
const [fontSize, setFontSize] = useState(DEFAULT_THEME_SETTINGS.fontSize);
|
||||
const [colorBlindMode, setColorBlindMode] = useState(DEFAULT_THEME_SETTINGS.colorBlindMode);
|
||||
const [reducedMotion, setReducedMotion] = useState(DEFAULT_THEME_SETTINGS.reducedMotion);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// Track if form has unsaved changes
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
|
||||
// Load saved preferences on component mount
|
||||
useEffect(() => {
|
||||
const loadPreferences = async () => {
|
||||
try {
|
||||
// First check localStorage for immediate UI updates
|
||||
const savedTheme = localStorage.getItem('theme') || DEFAULT_DISPLAY_PREFERENCES.theme;
|
||||
// Ensure theme is either light or dark
|
||||
const validTheme = ['light', 'dark'].includes(savedTheme) ? savedTheme : DEFAULT_DISPLAY_PREFERENCES.theme;
|
||||
const savedFontSize = localStorage.getItem('fontSize') || DEFAULT_DISPLAY_PREFERENCES.fontSize;
|
||||
const savedColorBlindMode = localStorage.getItem('colorBlindMode') === 'true';
|
||||
const savedReducedMotion = localStorage.getItem('reducedMotion') === 'true';
|
||||
// First load theme settings from IndexedDB
|
||||
const themeSettings = await themeService.getThemeSettings();
|
||||
|
||||
setTheme(validTheme);
|
||||
setFontSize(savedFontSize);
|
||||
setColorBlindMode(savedColorBlindMode);
|
||||
setReducedMotion(savedReducedMotion);
|
||||
// Store current settings
|
||||
setCurrentSettings(themeSettings);
|
||||
|
||||
// Apply theme to document
|
||||
document.documentElement.setAttribute('data-theme', validTheme);
|
||||
// Set form state from theme settings
|
||||
setTheme(themeSettings.theme);
|
||||
setFontSize(themeSettings.fontSize);
|
||||
setColorBlindMode(themeSettings.colorBlindMode);
|
||||
setReducedMotion(themeSettings.reducedMotion);
|
||||
|
||||
// Apply font size
|
||||
applyFontSize(savedFontSize);
|
||||
|
||||
// Apply accessibility settings
|
||||
if (savedColorBlindMode) {
|
||||
document.documentElement.classList.add('color-blind-mode');
|
||||
}
|
||||
|
||||
if (savedReducedMotion) {
|
||||
document.documentElement.classList.add('reduced-motion');
|
||||
}
|
||||
// Reset changes flag
|
||||
setHasChanges(false);
|
||||
|
||||
// Then check if user has saved preferences in their profile
|
||||
const user = auth.getCurrentUser();
|
||||
|
@ -68,20 +53,20 @@ export default function DisplaySettings() {
|
|||
try {
|
||||
const userPrefs = JSON.parse(user.display_preferences);
|
||||
|
||||
// Only update if values exist and are different from localStorage
|
||||
if (userPrefs.theme && ['light', 'dark'].includes(userPrefs.theme) && userPrefs.theme !== validTheme) {
|
||||
setTheme(userPrefs.theme);
|
||||
localStorage.setItem('theme', userPrefs.theme);
|
||||
document.documentElement.setAttribute('data-theme', userPrefs.theme);
|
||||
// Only update if values exist and are different from IndexedDB
|
||||
if (userPrefs.theme && ['light', 'dark'].includes(userPrefs.theme) && userPrefs.theme !== themeSettings.theme) {
|
||||
setTheme(userPrefs.theme as 'light' | 'dark');
|
||||
// Don't update theme service yet, wait for save
|
||||
setHasChanges(true);
|
||||
} else if (!['light', 'dark'].includes(userPrefs.theme)) {
|
||||
// If theme is not valid, mark for update
|
||||
needsDisplayPrefsUpdate = true;
|
||||
}
|
||||
|
||||
if (userPrefs.fontSize && userPrefs.fontSize !== savedFontSize) {
|
||||
if (userPrefs.fontSize && userPrefs.fontSize !== themeSettings.fontSize) {
|
||||
setFontSize(userPrefs.fontSize);
|
||||
localStorage.setItem('fontSize', userPrefs.fontSize);
|
||||
applyFontSize(userPrefs.fontSize);
|
||||
// Don't update theme service yet, wait for save
|
||||
setHasChanges(true);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing display preferences:', e);
|
||||
|
@ -97,27 +82,17 @@ export default function DisplaySettings() {
|
|||
const accessibilityPrefs = JSON.parse(user.accessibility_settings);
|
||||
|
||||
if (typeof accessibilityPrefs.colorBlindMode === 'boolean' &&
|
||||
accessibilityPrefs.colorBlindMode !== savedColorBlindMode) {
|
||||
accessibilityPrefs.colorBlindMode !== themeSettings.colorBlindMode) {
|
||||
setColorBlindMode(accessibilityPrefs.colorBlindMode);
|
||||
localStorage.setItem('colorBlindMode', accessibilityPrefs.colorBlindMode.toString());
|
||||
|
||||
if (accessibilityPrefs.colorBlindMode) {
|
||||
document.documentElement.classList.add('color-blind-mode');
|
||||
} else {
|
||||
document.documentElement.classList.remove('color-blind-mode');
|
||||
}
|
||||
// Don't update theme service yet, wait for save
|
||||
setHasChanges(true);
|
||||
}
|
||||
|
||||
if (typeof accessibilityPrefs.reducedMotion === 'boolean' &&
|
||||
accessibilityPrefs.reducedMotion !== savedReducedMotion) {
|
||||
accessibilityPrefs.reducedMotion !== themeSettings.reducedMotion) {
|
||||
setReducedMotion(accessibilityPrefs.reducedMotion);
|
||||
localStorage.setItem('reducedMotion', accessibilityPrefs.reducedMotion.toString());
|
||||
|
||||
if (accessibilityPrefs.reducedMotion) {
|
||||
document.documentElement.classList.add('reduced-motion');
|
||||
} else {
|
||||
document.documentElement.classList.remove('reduced-motion');
|
||||
}
|
||||
// Don't update theme service yet, wait for save
|
||||
setHasChanges(true);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing accessibility settings:', e);
|
||||
|
@ -141,6 +116,23 @@ export default function DisplaySettings() {
|
|||
loadPreferences();
|
||||
}, []);
|
||||
|
||||
// Check for changes when form values change
|
||||
useEffect(() => {
|
||||
if (!currentSettings) return;
|
||||
|
||||
const hasThemeChanged = theme !== currentSettings.theme;
|
||||
const hasFontSizeChanged = fontSize !== currentSettings.fontSize;
|
||||
const hasColorBlindModeChanged = colorBlindMode !== currentSettings.colorBlindMode;
|
||||
const hasReducedMotionChanged = reducedMotion !== currentSettings.reducedMotion;
|
||||
|
||||
setHasChanges(
|
||||
hasThemeChanged ||
|
||||
hasFontSizeChanged ||
|
||||
hasColorBlindModeChanged ||
|
||||
hasReducedMotionChanged
|
||||
);
|
||||
}, [theme, fontSize, colorBlindMode, reducedMotion, currentSettings]);
|
||||
|
||||
// Initialize default settings if not set
|
||||
const initializeDefaultSettings = async (userId: string, updateDisplayPrefs: boolean, updateAccessibility: boolean) => {
|
||||
try {
|
||||
|
@ -162,91 +154,38 @@ export default function DisplaySettings() {
|
|||
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
await update.updateFields(Collections.USERS, userId, updateData);
|
||||
// console.log('Initialized default display and accessibility settings');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error initializing default settings:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Apply font size to document
|
||||
const applyFontSize = (size: string) => {
|
||||
const htmlElement = document.documentElement;
|
||||
|
||||
// Remove existing font size classes
|
||||
htmlElement.classList.remove('text-sm', 'text-base', 'text-lg', 'text-xl');
|
||||
|
||||
// Add new font size class
|
||||
switch (size) {
|
||||
case 'small':
|
||||
htmlElement.classList.add('text-sm');
|
||||
break;
|
||||
case 'medium':
|
||||
htmlElement.classList.add('text-base');
|
||||
break;
|
||||
case 'large':
|
||||
htmlElement.classList.add('text-lg');
|
||||
break;
|
||||
case 'extra-large':
|
||||
htmlElement.classList.add('text-xl');
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle theme change
|
||||
const handleThemeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const newTheme = e.target.value;
|
||||
const newTheme = e.target.value as 'light' | 'dark';
|
||||
setTheme(newTheme);
|
||||
|
||||
// Apply theme to document
|
||||
document.documentElement.setAttribute('data-theme', newTheme);
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem('theme', newTheme);
|
||||
// Changes will be applied on save
|
||||
};
|
||||
|
||||
// Handle font size change
|
||||
const handleFontSizeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const newSize = e.target.value;
|
||||
const newSize = e.target.value as 'small' | 'medium' | 'large' | 'extra-large';
|
||||
setFontSize(newSize);
|
||||
|
||||
// Apply font size
|
||||
applyFontSize(newSize);
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem('fontSize', newSize);
|
||||
// Changes will be applied on save
|
||||
};
|
||||
|
||||
// Handle color blind mode toggle
|
||||
const handleColorBlindModeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const enabled = e.target.checked;
|
||||
setColorBlindMode(enabled);
|
||||
|
||||
// Apply to document
|
||||
if (enabled) {
|
||||
document.documentElement.classList.add('color-blind-mode');
|
||||
} else {
|
||||
document.documentElement.classList.remove('color-blind-mode');
|
||||
}
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem('colorBlindMode', enabled.toString());
|
||||
// Changes will be applied on save
|
||||
};
|
||||
|
||||
// Handle reduced motion toggle
|
||||
const handleReducedMotionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const enabled = e.target.checked;
|
||||
setReducedMotion(enabled);
|
||||
|
||||
// Apply to document
|
||||
if (enabled) {
|
||||
document.documentElement.classList.add('reduced-motion');
|
||||
} else {
|
||||
document.documentElement.classList.remove('reduced-motion');
|
||||
}
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem('reducedMotion', enabled.toString());
|
||||
// Changes will be applied on save
|
||||
};
|
||||
|
||||
// Handle form submission
|
||||
|
@ -270,7 +209,17 @@ export default function DisplaySettings() {
|
|||
reducedMotion
|
||||
};
|
||||
|
||||
// Update user record
|
||||
// First update IndexedDB with the new settings
|
||||
await themeService.saveThemeSettings({
|
||||
id: "current",
|
||||
theme,
|
||||
fontSize,
|
||||
colorBlindMode,
|
||||
reducedMotion,
|
||||
updatedAt: Date.now()
|
||||
});
|
||||
|
||||
// Then update user record in PocketBase
|
||||
await update.updateFields(
|
||||
Collections.USERS,
|
||||
user.id,
|
||||
|
@ -280,6 +229,19 @@ export default function DisplaySettings() {
|
|||
}
|
||||
);
|
||||
|
||||
// Update current settings state to match the new settings
|
||||
setCurrentSettings({
|
||||
id: "current",
|
||||
theme,
|
||||
fontSize,
|
||||
colorBlindMode,
|
||||
reducedMotion,
|
||||
updatedAt: Date.now()
|
||||
});
|
||||
|
||||
// Reset changes flag
|
||||
setHasChanges(false);
|
||||
|
||||
// Show success message
|
||||
toast.success('Display settings saved successfully!');
|
||||
} catch (error) {
|
||||
|
@ -367,19 +329,26 @@ export default function DisplaySettings() {
|
|||
</div>
|
||||
|
||||
<p className="text-sm text-info">
|
||||
These settings are saved to your browser and your IEEE UCSD account. They will be applied whenever you log in.
|
||||
These settings are saved to your browser using IndexedDB and your IEEE UCSD account. They will be applied whenever you log in.
|
||||
</p>
|
||||
|
||||
<div className="form-control">
|
||||
<button
|
||||
type="submit"
|
||||
className={`btn btn-primary ${saving ? 'loading' : ''}`}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save Settings'}
|
||||
</button>
|
||||
<div className="flex flex-col gap-2">
|
||||
{hasChanges && (
|
||||
<p className="text-sm text-warning">
|
||||
You have unsaved changes. Click "Save Settings" to apply them.
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
className={`btn btn-primary ${saving ? 'loading' : ''}`}
|
||||
disabled={saving || !hasChanges}
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save Settings'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -27,6 +27,9 @@ interface OfflineChange {
|
|||
syncAttempts: number;
|
||||
}
|
||||
|
||||
// Import ThemeSettings interface
|
||||
import type { ThemeSettings } from "./ThemeService";
|
||||
|
||||
export class DashboardDatabase extends Dexie {
|
||||
users!: Dexie.Table<User, string>;
|
||||
events!: Dexie.Table<Event, string>;
|
||||
|
@ -38,6 +41,7 @@ export class DashboardDatabase extends Dexie {
|
|||
receipts!: Dexie.Table<Receipt, string>;
|
||||
sponsors!: Dexie.Table<Sponsor, string>;
|
||||
offlineChanges!: Dexie.Table<OfflineChange, string>;
|
||||
themeSettings!: Dexie.Table<ThemeSettings, string>;
|
||||
|
||||
// Store last sync timestamps
|
||||
syncInfo!: Dexie.Table<
|
||||
|
@ -77,6 +81,11 @@ export class DashboardDatabase extends Dexie {
|
|||
events:
|
||||
"id, event_name, event_code, start_date, end_date, published, files",
|
||||
});
|
||||
|
||||
// Add version 5 with themeSettings table
|
||||
this.version(5).stores({
|
||||
themeSettings: "id, theme, fontSize, updatedAt",
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize the database with default values
|
||||
|
@ -178,6 +187,7 @@ export class DexieService {
|
|||
await db.receipts.clear();
|
||||
await db.sponsors.clear();
|
||||
await db.offlineChanges.clear();
|
||||
// Note: We don't clear themeSettings as they should persist across logins
|
||||
|
||||
// Reset sync timestamps
|
||||
const collections = [
|
||||
|
|
238
src/scripts/database/ThemeService.ts
Normal file
238
src/scripts/database/ThemeService.ts
Normal file
|
@ -0,0 +1,238 @@
|
|||
import Dexie from "dexie";
|
||||
import { DexieService } from "./DexieService";
|
||||
|
||||
// Check if we're in a browser environment
|
||||
const isBrowser =
|
||||
typeof window !== "undefined" && typeof window.indexedDB !== "undefined";
|
||||
|
||||
// Interface for theme settings
|
||||
export interface ThemeSettings {
|
||||
id: string;
|
||||
theme: "light" | "dark";
|
||||
fontSize: "small" | "medium" | "large" | "extra-large";
|
||||
colorBlindMode: boolean;
|
||||
reducedMotion: boolean;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
// Default theme settings
|
||||
export const DEFAULT_THEME_SETTINGS: Omit<ThemeSettings, "id" | "updatedAt"> = {
|
||||
theme: "dark",
|
||||
fontSize: "medium",
|
||||
colorBlindMode: false,
|
||||
reducedMotion: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Service for managing theme settings using IndexedDB
|
||||
*/
|
||||
export class ThemeService {
|
||||
private static instance: ThemeService;
|
||||
private dexieService: DexieService;
|
||||
|
||||
private constructor() {
|
||||
this.dexieService = DexieService.getInstance();
|
||||
|
||||
// Initialize the theme table if it doesn't exist
|
||||
if (isBrowser) {
|
||||
const db = this.dexieService.getDB();
|
||||
|
||||
// Add theme table if it doesn't exist in the schema
|
||||
if (!db.tables.some(table => table.name === "themeSettings")) {
|
||||
db.version(db.verno + 1).stores({
|
||||
themeSettings: "id, theme, fontSize, updatedAt"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static getInstance(): ThemeService {
|
||||
if (!ThemeService.instance) {
|
||||
ThemeService.instance = new ThemeService();
|
||||
}
|
||||
return ThemeService.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current theme settings
|
||||
* @returns The current theme settings or default settings if none exist
|
||||
*/
|
||||
public async getThemeSettings(): Promise<ThemeSettings> {
|
||||
if (!isBrowser) {
|
||||
return {
|
||||
id: "default",
|
||||
...DEFAULT_THEME_SETTINGS,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const db = this.dexieService.getDB();
|
||||
|
||||
// Check if themeSettings table exists
|
||||
if (!db.tables.some(table => table.name === "themeSettings")) {
|
||||
return {
|
||||
id: "default",
|
||||
...DEFAULT_THEME_SETTINGS,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
// Get the theme settings
|
||||
const settings = await db.table("themeSettings").get("current");
|
||||
|
||||
if (!settings) {
|
||||
// If no settings exist, create default settings
|
||||
const defaultSettings: ThemeSettings = {
|
||||
id: "current",
|
||||
...DEFAULT_THEME_SETTINGS,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
await this.saveThemeSettings(defaultSettings);
|
||||
return defaultSettings;
|
||||
}
|
||||
|
||||
return settings;
|
||||
} catch (error) {
|
||||
console.error("Error getting theme settings:", error);
|
||||
|
||||
// Return default settings if there's an error
|
||||
return {
|
||||
id: "default",
|
||||
...DEFAULT_THEME_SETTINGS,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save theme settings to IndexedDB
|
||||
* @param settings The theme settings to save
|
||||
*/
|
||||
public async saveThemeSettings(settings: ThemeSettings): Promise<void> {
|
||||
if (!isBrowser) return;
|
||||
|
||||
try {
|
||||
const db = this.dexieService.getDB();
|
||||
|
||||
// Check if themeSettings table exists
|
||||
if (!db.tables.some(table => table.name === "themeSettings")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the updatedAt timestamp
|
||||
settings.updatedAt = Date.now();
|
||||
|
||||
// Save the settings
|
||||
await db.table("themeSettings").put(settings);
|
||||
|
||||
// Apply the theme to the document
|
||||
this.applyThemeToDocument(settings);
|
||||
} catch (error) {
|
||||
console.error("Error saving theme settings:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a specific theme setting
|
||||
* @param key The setting key to update
|
||||
* @param value The new value
|
||||
*/
|
||||
public async updateThemeSetting<K extends keyof Omit<ThemeSettings, "id" | "updatedAt">>(
|
||||
key: K,
|
||||
value: ThemeSettings[K]
|
||||
): Promise<void> {
|
||||
if (!isBrowser) return;
|
||||
|
||||
try {
|
||||
// Get current settings
|
||||
const currentSettings = await this.getThemeSettings();
|
||||
|
||||
// Update the specific setting
|
||||
const updatedSettings: ThemeSettings = {
|
||||
...currentSettings,
|
||||
[key]: value,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
// Save the updated settings
|
||||
await this.saveThemeSettings(updatedSettings);
|
||||
} catch (error) {
|
||||
console.error(`Error updating theme setting ${key}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply theme settings to the document
|
||||
* @param settings The theme settings to apply
|
||||
*/
|
||||
public applyThemeToDocument(settings: ThemeSettings): void {
|
||||
if (!isBrowser) return;
|
||||
|
||||
// Get current theme before applying new one
|
||||
const oldTheme = document.documentElement.getAttribute("data-theme");
|
||||
|
||||
// Apply theme
|
||||
document.documentElement.setAttribute("data-theme", settings.theme);
|
||||
|
||||
// Apply font size
|
||||
document.documentElement.classList.remove("text-sm", "text-base", "text-lg", "text-xl");
|
||||
switch (settings.fontSize) {
|
||||
case "small":
|
||||
document.documentElement.classList.add("text-sm");
|
||||
break;
|
||||
case "medium":
|
||||
document.documentElement.classList.add("text-base");
|
||||
break;
|
||||
case "large":
|
||||
document.documentElement.classList.add("text-lg");
|
||||
break;
|
||||
case "extra-large":
|
||||
document.documentElement.classList.add("text-xl");
|
||||
break;
|
||||
}
|
||||
|
||||
// Apply accessibility settings
|
||||
if (settings.colorBlindMode) {
|
||||
document.documentElement.classList.add("color-blind-mode");
|
||||
} else {
|
||||
document.documentElement.classList.remove("color-blind-mode");
|
||||
}
|
||||
|
||||
if (settings.reducedMotion) {
|
||||
document.documentElement.classList.add("reduced-motion");
|
||||
} else {
|
||||
document.documentElement.classList.remove("reduced-motion");
|
||||
}
|
||||
|
||||
// Dispatch theme change event if theme changed
|
||||
if (oldTheme !== settings.theme) {
|
||||
const event = new CustomEvent('themechange', {
|
||||
detail: {
|
||||
oldTheme,
|
||||
newTheme: settings.theme
|
||||
}
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize theme from IndexedDB or create default settings
|
||||
* This should be called on app startup
|
||||
*/
|
||||
public async initializeTheme(): Promise<void> {
|
||||
if (!isBrowser) return;
|
||||
|
||||
try {
|
||||
const settings = await this.getThemeSettings();
|
||||
this.applyThemeToDocument(settings);
|
||||
} catch (error) {
|
||||
console.error("Error initializing theme:", error);
|
||||
|
||||
// Apply default theme if there's an error
|
||||
document.documentElement.setAttribute("data-theme", DEFAULT_THEME_SETTINGS.theme);
|
||||
}
|
||||
}
|
||||
}
|
50
src/utils/themeUtils.ts
Normal file
50
src/utils/themeUtils.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { ThemeService } from "../scripts/database/ThemeService";
|
||||
|
||||
/**
|
||||
* Initialize theme settings from IndexedDB
|
||||
* This function can be used in client-side scripts
|
||||
*/
|
||||
export const initializeTheme = async (): Promise<void> => {
|
||||
// Check if we're in a browser environment
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
try {
|
||||
const themeService = ThemeService.getInstance();
|
||||
await themeService.initializeTheme();
|
||||
} catch (error) {
|
||||
console.error("Error initializing theme:", error);
|
||||
// Apply default theme if there's an error
|
||||
if (typeof document !== "undefined") {
|
||||
document.documentElement.setAttribute("data-theme", "dark");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current theme
|
||||
* @returns The current theme ('light' or 'dark')
|
||||
*/
|
||||
export const getCurrentTheme = (): 'light' | 'dark' => {
|
||||
// Check if we're in a browser environment
|
||||
if (typeof document === "undefined") return 'dark';
|
||||
|
||||
const theme = document.documentElement.getAttribute("data-theme");
|
||||
return (theme === 'light' ? 'light' : 'dark');
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle between light and dark themes
|
||||
*/
|
||||
export const toggleTheme = async (): Promise<void> => {
|
||||
// Check if we're in a browser environment
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
try {
|
||||
const themeService = ThemeService.getInstance();
|
||||
const settings = await themeService.getThemeSettings();
|
||||
const newTheme = settings.theme === 'light' ? 'dark' : 'light';
|
||||
await themeService.updateThemeSetting('theme', newTheme);
|
||||
} catch (error) {
|
||||
console.error("Error toggling theme:", error);
|
||||
}
|
||||
};
|
|
@ -47,4 +47,36 @@ export default {
|
|||
},
|
||||
heroui(),
|
||||
],
|
||||
daisyui: {
|
||||
themes: [
|
||||
{
|
||||
light: {
|
||||
primary: "#06659d",
|
||||
secondary: "#4b92db",
|
||||
accent: "#F3C135",
|
||||
neutral: "#2a323c",
|
||||
"base-100": "#ffffff",
|
||||
"base-200": "#f8f9fa",
|
||||
"base-300": "#e9ecef",
|
||||
info: "#3abff8",
|
||||
success: "#36d399",
|
||||
warning: "#fbbd23",
|
||||
error: "#f87272",
|
||||
},
|
||||
dark: {
|
||||
primary: "#88BFEC",
|
||||
secondary: "#4b92db",
|
||||
accent: "#F3C135",
|
||||
neutral: "#191D24",
|
||||
"base-100": "#0A0E1A",
|
||||
"base-200": "#0d1324",
|
||||
"base-300": "#1a2035",
|
||||
info: "#3abff8",
|
||||
success: "#36d399",
|
||||
warning: "#fbbd23",
|
||||
error: "#f87272",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue