fix colors

This commit is contained in:
chark1es 2025-03-28 14:34:22 -07:00
parent 0ca36b69eb
commit 1cbc9d7b8e
6 changed files with 651 additions and 312 deletions

View file

@ -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>

View file

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

View file

@ -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 = [

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

View file

@ -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",
},
},
],
},
};