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 DisplaySettings from "./SettingsSection/DisplaySettings";
|
||||||
import ResumeSettings from "./SettingsSection/ResumeSettings";
|
import ResumeSettings from "./SettingsSection/ResumeSettings";
|
||||||
import EmailRequestSettings from "./SettingsSection/EmailRequestSettings";
|
import EmailRequestSettings from "./SettingsSection/EmailRequestSettings";
|
||||||
|
import ThemeToggle from "./universal/ThemeToggle";
|
||||||
|
|
||||||
// Import environment variables
|
// Import environment variables
|
||||||
const logtoAppId = import.meta.env.LOGTO_APP_ID;
|
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 safeLogtoAppSecret = logtoAppSecret || "missing_app_secret";
|
||||||
const safeLogtoEndpoint = logtoEndpoint || "https://auth.ieeeucsd.org";
|
const safeLogtoEndpoint = logtoEndpoint || "https://auth.ieeeucsd.org";
|
||||||
const safeLogtoTokenEndpoint =
|
const safeLogtoTokenEndpoint =
|
||||||
logtoTokenEndpoint || "https://auth.ieeeucsd.org/oidc/token";
|
logtoTokenEndpoint || "https://auth.ieeeucsd.org/oidc/token";
|
||||||
const safeLogtoApiEndpoint = logtoApiEndpoint || "";
|
const safeLogtoApiEndpoint = logtoApiEndpoint || "";
|
||||||
---
|
---
|
||||||
|
|
||||||
<div id="settings-section" class="">
|
<div id="settings-section" class="">
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h2 class="text-2xl font-bold">Settings</h2>
|
<h2 class="text-2xl font-bold">Settings</h2>
|
||||||
<p class="opacity-70">Manage your account settings and preferences</p>
|
<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>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Resume Settings Card -->
|
<!-- Debug Environment Variables (Only visible in development) -->
|
||||||
<div
|
{
|
||||||
id="resume-management-section"
|
import.meta.env.DEV && (
|
||||||
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 bg-card shadow-xl border border-warning mb-6 p-4">
|
||||||
>
|
<h3 class="text-lg font-bold text-warning">
|
||||||
<div class="card-body">
|
Debug Environment Variables
|
||||||
<h3 class="card-title flex items-center gap-3">
|
</h3>
|
||||||
<div class="badge badge-primary p-3">
|
<p class="text-sm mb-2">
|
||||||
<Icon name="heroicons:document-text" class="h-5 w-5" />
|
This section is only visible in development mode
|
||||||
</div>
|
</p>
|
||||||
Resume Management
|
<div class="overflow-x-auto">
|
||||||
</h3>
|
<table class="table-auto w-full text-xs">
|
||||||
<p class="text-sm opacity-70 mb-4">
|
<thead>
|
||||||
Upload and manage your resume for recruiters and career opportunities
|
<tr>
|
||||||
</p>
|
<th>Variable</th>
|
||||||
<div class="divider"></div>
|
<th>Value</th>
|
||||||
<ResumeSettings client:load />
|
<th>Status</th>
|
||||||
</div>
|
</tr>
|
||||||
</div>
|
</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 -->
|
<!-- 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: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 -->
|
|
||||||
<div
|
<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">
|
<div class="card-body">
|
||||||
<h4 class="text-xl font-bold">Coming Soon</h4>
|
<h3 class="card-title flex items-center gap-3">
|
||||||
<p class="text-sm opacity-70">
|
<div
|
||||||
Notification settings will be available in a future update
|
class="inline-flex items-center justify-center p-3 rounded-full bg-primary text-primary-foreground"
|
||||||
</p>
|
>
|
||||||
</div>
|
<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>
|
||||||
|
|
||||||
<div class="card-body">
|
<!-- Resume Settings Card -->
|
||||||
<h3 class="card-title flex items-center gap-3">
|
<div
|
||||||
<div class="badge badge-primary p-3">
|
id="resume-management-section"
|
||||||
<Icon name="heroicons:bell" class="h-5 w-5" />
|
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>
|
</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>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Display Settings Card -->
|
<!-- IEEE Email Request Card -->
|
||||||
<div
|
<div
|
||||||
class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform"
|
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">
|
<div class="card-body">
|
||||||
<h3 class="card-title flex items-center gap-3">
|
<h3 class="card-title flex items-center gap-3">
|
||||||
<div class="badge badge-primary p-3">
|
<div
|
||||||
<Icon name="heroicons:computer-desktop" class="h-5 w-5" />
|
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>
|
</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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,59 +3,44 @@ import { Authentication } from '../../../scripts/pocketbase/Authentication';
|
||||||
import { Update } from '../../../scripts/pocketbase/Update';
|
import { Update } from '../../../scripts/pocketbase/Update';
|
||||||
import { Collections } from '../../../schemas/pocketbase/schema';
|
import { Collections } from '../../../schemas/pocketbase/schema';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
|
import { ThemeService, DEFAULT_THEME_SETTINGS, type ThemeSettings } from '../../../scripts/database/ThemeService';
|
||||||
// Default display preferences
|
|
||||||
const DEFAULT_DISPLAY_PREFERENCES = {
|
|
||||||
theme: 'dark',
|
|
||||||
fontSize: 'medium'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Default accessibility settings
|
|
||||||
const DEFAULT_ACCESSIBILITY_SETTINGS = {
|
|
||||||
colorBlindMode: false,
|
|
||||||
reducedMotion: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function DisplaySettings() {
|
export default function DisplaySettings() {
|
||||||
const auth = Authentication.getInstance();
|
const auth = Authentication.getInstance();
|
||||||
const update = Update.getInstance();
|
const update = Update.getInstance();
|
||||||
const [theme, setTheme] = useState(DEFAULT_DISPLAY_PREFERENCES.theme);
|
const themeService = ThemeService.getInstance();
|
||||||
const [fontSize, setFontSize] = useState(DEFAULT_DISPLAY_PREFERENCES.fontSize);
|
|
||||||
const [colorBlindMode, setColorBlindMode] = useState(DEFAULT_ACCESSIBILITY_SETTINGS.colorBlindMode);
|
// Current applied settings
|
||||||
const [reducedMotion, setReducedMotion] = useState(DEFAULT_ACCESSIBILITY_SETTINGS.reducedMotion);
|
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);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
// Track if form has unsaved changes
|
||||||
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
|
||||||
// Load saved preferences on component mount
|
// Load saved preferences on component mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadPreferences = async () => {
|
const loadPreferences = async () => {
|
||||||
try {
|
try {
|
||||||
// First check localStorage for immediate UI updates
|
// First load theme settings from IndexedDB
|
||||||
const savedTheme = localStorage.getItem('theme') || DEFAULT_DISPLAY_PREFERENCES.theme;
|
const themeSettings = await themeService.getThemeSettings();
|
||||||
// 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';
|
|
||||||
|
|
||||||
setTheme(validTheme);
|
// Store current settings
|
||||||
setFontSize(savedFontSize);
|
setCurrentSettings(themeSettings);
|
||||||
setColorBlindMode(savedColorBlindMode);
|
|
||||||
setReducedMotion(savedReducedMotion);
|
|
||||||
|
|
||||||
// Apply theme to document
|
// Set form state from theme settings
|
||||||
document.documentElement.setAttribute('data-theme', validTheme);
|
setTheme(themeSettings.theme);
|
||||||
|
setFontSize(themeSettings.fontSize);
|
||||||
|
setColorBlindMode(themeSettings.colorBlindMode);
|
||||||
|
setReducedMotion(themeSettings.reducedMotion);
|
||||||
|
|
||||||
// Apply font size
|
// Reset changes flag
|
||||||
applyFontSize(savedFontSize);
|
setHasChanges(false);
|
||||||
|
|
||||||
// Apply accessibility settings
|
|
||||||
if (savedColorBlindMode) {
|
|
||||||
document.documentElement.classList.add('color-blind-mode');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (savedReducedMotion) {
|
|
||||||
document.documentElement.classList.add('reduced-motion');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then check if user has saved preferences in their profile
|
// Then check if user has saved preferences in their profile
|
||||||
const user = auth.getCurrentUser();
|
const user = auth.getCurrentUser();
|
||||||
|
@ -68,20 +53,20 @@ export default function DisplaySettings() {
|
||||||
try {
|
try {
|
||||||
const userPrefs = JSON.parse(user.display_preferences);
|
const userPrefs = JSON.parse(user.display_preferences);
|
||||||
|
|
||||||
// Only update if values exist and are different from localStorage
|
// Only update if values exist and are different from IndexedDB
|
||||||
if (userPrefs.theme && ['light', 'dark'].includes(userPrefs.theme) && userPrefs.theme !== validTheme) {
|
if (userPrefs.theme && ['light', 'dark'].includes(userPrefs.theme) && userPrefs.theme !== themeSettings.theme) {
|
||||||
setTheme(userPrefs.theme);
|
setTheme(userPrefs.theme as 'light' | 'dark');
|
||||||
localStorage.setItem('theme', userPrefs.theme);
|
// Don't update theme service yet, wait for save
|
||||||
document.documentElement.setAttribute('data-theme', userPrefs.theme);
|
setHasChanges(true);
|
||||||
} else if (!['light', 'dark'].includes(userPrefs.theme)) {
|
} else if (!['light', 'dark'].includes(userPrefs.theme)) {
|
||||||
// If theme is not valid, mark for update
|
// If theme is not valid, mark for update
|
||||||
needsDisplayPrefsUpdate = true;
|
needsDisplayPrefsUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userPrefs.fontSize && userPrefs.fontSize !== savedFontSize) {
|
if (userPrefs.fontSize && userPrefs.fontSize !== themeSettings.fontSize) {
|
||||||
setFontSize(userPrefs.fontSize);
|
setFontSize(userPrefs.fontSize);
|
||||||
localStorage.setItem('fontSize', userPrefs.fontSize);
|
// Don't update theme service yet, wait for save
|
||||||
applyFontSize(userPrefs.fontSize);
|
setHasChanges(true);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error parsing display preferences:', e);
|
console.error('Error parsing display preferences:', e);
|
||||||
|
@ -97,27 +82,17 @@ export default function DisplaySettings() {
|
||||||
const accessibilityPrefs = JSON.parse(user.accessibility_settings);
|
const accessibilityPrefs = JSON.parse(user.accessibility_settings);
|
||||||
|
|
||||||
if (typeof accessibilityPrefs.colorBlindMode === 'boolean' &&
|
if (typeof accessibilityPrefs.colorBlindMode === 'boolean' &&
|
||||||
accessibilityPrefs.colorBlindMode !== savedColorBlindMode) {
|
accessibilityPrefs.colorBlindMode !== themeSettings.colorBlindMode) {
|
||||||
setColorBlindMode(accessibilityPrefs.colorBlindMode);
|
setColorBlindMode(accessibilityPrefs.colorBlindMode);
|
||||||
localStorage.setItem('colorBlindMode', accessibilityPrefs.colorBlindMode.toString());
|
// Don't update theme service yet, wait for save
|
||||||
|
setHasChanges(true);
|
||||||
if (accessibilityPrefs.colorBlindMode) {
|
|
||||||
document.documentElement.classList.add('color-blind-mode');
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('color-blind-mode');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof accessibilityPrefs.reducedMotion === 'boolean' &&
|
if (typeof accessibilityPrefs.reducedMotion === 'boolean' &&
|
||||||
accessibilityPrefs.reducedMotion !== savedReducedMotion) {
|
accessibilityPrefs.reducedMotion !== themeSettings.reducedMotion) {
|
||||||
setReducedMotion(accessibilityPrefs.reducedMotion);
|
setReducedMotion(accessibilityPrefs.reducedMotion);
|
||||||
localStorage.setItem('reducedMotion', accessibilityPrefs.reducedMotion.toString());
|
// Don't update theme service yet, wait for save
|
||||||
|
setHasChanges(true);
|
||||||
if (accessibilityPrefs.reducedMotion) {
|
|
||||||
document.documentElement.classList.add('reduced-motion');
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('reduced-motion');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error parsing accessibility settings:', e);
|
console.error('Error parsing accessibility settings:', e);
|
||||||
|
@ -141,6 +116,23 @@ export default function DisplaySettings() {
|
||||||
loadPreferences();
|
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
|
// Initialize default settings if not set
|
||||||
const initializeDefaultSettings = async (userId: string, updateDisplayPrefs: boolean, updateAccessibility: boolean) => {
|
const initializeDefaultSettings = async (userId: string, updateDisplayPrefs: boolean, updateAccessibility: boolean) => {
|
||||||
try {
|
try {
|
||||||
|
@ -162,91 +154,38 @@ export default function DisplaySettings() {
|
||||||
|
|
||||||
if (Object.keys(updateData).length > 0) {
|
if (Object.keys(updateData).length > 0) {
|
||||||
await update.updateFields(Collections.USERS, userId, updateData);
|
await update.updateFields(Collections.USERS, userId, updateData);
|
||||||
// console.log('Initialized default display and accessibility settings');
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error initializing default settings:', 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
|
// Handle theme change
|
||||||
const handleThemeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
const handleThemeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
const newTheme = e.target.value;
|
const newTheme = e.target.value as 'light' | 'dark';
|
||||||
setTheme(newTheme);
|
setTheme(newTheme);
|
||||||
|
// Changes will be applied on save
|
||||||
// Apply theme to document
|
|
||||||
document.documentElement.setAttribute('data-theme', newTheme);
|
|
||||||
|
|
||||||
// Save to localStorage
|
|
||||||
localStorage.setItem('theme', newTheme);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle font size change
|
// Handle font size change
|
||||||
const handleFontSizeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
const handleFontSizeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
const newSize = e.target.value;
|
const newSize = e.target.value as 'small' | 'medium' | 'large' | 'extra-large';
|
||||||
setFontSize(newSize);
|
setFontSize(newSize);
|
||||||
|
// Changes will be applied on save
|
||||||
// Apply font size
|
|
||||||
applyFontSize(newSize);
|
|
||||||
|
|
||||||
// Save to localStorage
|
|
||||||
localStorage.setItem('fontSize', newSize);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle color blind mode toggle
|
// Handle color blind mode toggle
|
||||||
const handleColorBlindModeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleColorBlindModeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const enabled = e.target.checked;
|
const enabled = e.target.checked;
|
||||||
setColorBlindMode(enabled);
|
setColorBlindMode(enabled);
|
||||||
|
// Changes will be applied on save
|
||||||
// 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());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle reduced motion toggle
|
// Handle reduced motion toggle
|
||||||
const handleReducedMotionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleReducedMotionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const enabled = e.target.checked;
|
const enabled = e.target.checked;
|
||||||
setReducedMotion(enabled);
|
setReducedMotion(enabled);
|
||||||
|
// Changes will be applied on save
|
||||||
// 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());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle form submission
|
// Handle form submission
|
||||||
|
@ -270,7 +209,17 @@ export default function DisplaySettings() {
|
||||||
reducedMotion
|
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(
|
await update.updateFields(
|
||||||
Collections.USERS,
|
Collections.USERS,
|
||||||
user.id,
|
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
|
// Show success message
|
||||||
toast.success('Display settings saved successfully!');
|
toast.success('Display settings saved successfully!');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -367,19 +329,26 @@ export default function DisplaySettings() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-sm text-info">
|
<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>
|
</p>
|
||||||
|
|
||||||
<div className="form-control">
|
<div className="form-control">
|
||||||
<button
|
<div className="flex flex-col gap-2">
|
||||||
type="submit"
|
{hasChanges && (
|
||||||
className={`btn btn-primary ${saving ? 'loading' : ''}`}
|
<p className="text-sm text-warning">
|
||||||
disabled={saving}
|
You have unsaved changes. Click "Save Settings" to apply them.
|
||||||
>
|
</p>
|
||||||
{saving ? 'Saving...' : 'Save Settings'}
|
)}
|
||||||
</button>
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={`btn btn-primary ${saving ? 'loading' : ''}`}
|
||||||
|
disabled={saving || !hasChanges}
|
||||||
|
>
|
||||||
|
{saving ? 'Saving...' : 'Save Settings'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -27,6 +27,9 @@ interface OfflineChange {
|
||||||
syncAttempts: number;
|
syncAttempts: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Import ThemeSettings interface
|
||||||
|
import type { ThemeSettings } from "./ThemeService";
|
||||||
|
|
||||||
export class DashboardDatabase extends Dexie {
|
export class DashboardDatabase extends Dexie {
|
||||||
users!: Dexie.Table<User, string>;
|
users!: Dexie.Table<User, string>;
|
||||||
events!: Dexie.Table<Event, string>;
|
events!: Dexie.Table<Event, string>;
|
||||||
|
@ -38,6 +41,7 @@ export class DashboardDatabase extends Dexie {
|
||||||
receipts!: Dexie.Table<Receipt, string>;
|
receipts!: Dexie.Table<Receipt, string>;
|
||||||
sponsors!: Dexie.Table<Sponsor, string>;
|
sponsors!: Dexie.Table<Sponsor, string>;
|
||||||
offlineChanges!: Dexie.Table<OfflineChange, string>;
|
offlineChanges!: Dexie.Table<OfflineChange, string>;
|
||||||
|
themeSettings!: Dexie.Table<ThemeSettings, string>;
|
||||||
|
|
||||||
// Store last sync timestamps
|
// Store last sync timestamps
|
||||||
syncInfo!: Dexie.Table<
|
syncInfo!: Dexie.Table<
|
||||||
|
@ -77,6 +81,11 @@ export class DashboardDatabase extends Dexie {
|
||||||
events:
|
events:
|
||||||
"id, event_name, event_code, start_date, end_date, published, files",
|
"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
|
// Initialize the database with default values
|
||||||
|
@ -178,6 +187,7 @@ export class DexieService {
|
||||||
await db.receipts.clear();
|
await db.receipts.clear();
|
||||||
await db.sponsors.clear();
|
await db.sponsors.clear();
|
||||||
await db.offlineChanges.clear();
|
await db.offlineChanges.clear();
|
||||||
|
// Note: We don't clear themeSettings as they should persist across logins
|
||||||
|
|
||||||
// Reset sync timestamps
|
// Reset sync timestamps
|
||||||
const collections = [
|
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(),
|
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