Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d2833f7664 | |||
| 2a14080b5b | |||
| 2457fa41e3 |
@@ -3632,6 +3632,59 @@ html.theme-cupertino .events-scroll-container {
|
||||
.stats-kpi-value {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.profile-stats-section.form-card {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.profile-stats-section .form-header {
|
||||
margin-bottom: 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.profile-stats-section .form-header h2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.profile-stats-section .stats-subtitle {
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.profile-stats-kpi-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.profile-stats-kpi-grid .stats-kpi-card {
|
||||
padding: 10px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.profile-stats-kpi-grid .stats-kpi-icon {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.profile-stats-kpi-grid .stats-kpi-icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.profile-stats-kpi-grid .stats-kpi-label {
|
||||
font-size: 11px;
|
||||
margin-bottom: 2px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.profile-stats-kpi-grid .stats-kpi-value {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.profile-stats-kpi-grid .stats-kpi-unit {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.signature-grid {
|
||||
|
||||
@@ -726,7 +726,7 @@ export default function UserProfilePage({ onBack, onLogout }: UserProfilePagePro
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="form-card">
|
||||
<section className="form-card profile-stats-section">
|
||||
<div className="form-header">
|
||||
<BarChart2 size={24} className="form-icon" />
|
||||
<div>
|
||||
@@ -736,7 +736,7 @@ export default function UserProfilePage({ onBack, onLogout }: UserProfilePagePro
|
||||
</div>
|
||||
|
||||
{(statsTotals || profile) && (
|
||||
<div className="stats-kpi-grid">
|
||||
<div className="stats-kpi-grid profile-stats-kpi-grid">
|
||||
<KpiCard
|
||||
icon={<BookOpen size={20} />}
|
||||
label={t('profile.stats_logbooks')}
|
||||
|
||||
@@ -69,4 +69,28 @@ describe('appearancePrefs', () => {
|
||||
await saveAppearancePrefsToServer('ocean', 'light')
|
||||
expect(mockedApiJson).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('syncAppearancePrefs skips server sync when userId does not match active session', async () => {
|
||||
localStorage.setItem('active_userid', 'session-user')
|
||||
setThemePreference('other-user', 'ocean')
|
||||
mockedApiJson.mockResolvedValue({
|
||||
theme: 'material',
|
||||
colorScheme: 'dark',
|
||||
persisted: true
|
||||
})
|
||||
|
||||
await syncAppearancePrefs('other-user')
|
||||
|
||||
expect(mockedApiJson).not.toHaveBeenCalled()
|
||||
expect(localStorage.getItem('user_pref_theme_other-user')).toBe('ocean')
|
||||
})
|
||||
|
||||
it('syncAppearancePrefs skips server sync when active session is missing', async () => {
|
||||
setThemePreference(USER_ID, 'ocean')
|
||||
|
||||
await syncAppearancePrefs(USER_ID)
|
||||
|
||||
expect(mockedApiJson).not.toHaveBeenCalled()
|
||||
expect(localStorage.getItem(`user_pref_theme_${USER_ID}`)).toBe('ocean')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -23,16 +23,30 @@ function hasLocalAppearancePrefs(userId: string): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
export async function fetchAppearancePrefs(): Promise<AppearancePrefs> {
|
||||
if (!getActiveUserId()) {
|
||||
function resolveSyncedUserId(userId?: string | null): string | null {
|
||||
const id = userId?.trim() || getActiveUserId()?.trim() || null
|
||||
if (!id) return null
|
||||
|
||||
const activeId = getActiveUserId()?.trim() || null
|
||||
if (!activeId || activeId !== id) return null
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
export async function fetchAppearancePrefs(userId?: string | null): Promise<AppearancePrefs> {
|
||||
if (!resolveSyncedUserId(userId)) {
|
||||
return { theme: 'auto', colorScheme: 'auto', persisted: false }
|
||||
}
|
||||
|
||||
return apiJson<AppearancePrefs>(API_BASE)
|
||||
}
|
||||
|
||||
export async function saveAppearancePrefsToServer(theme: string, colorScheme: string): Promise<void> {
|
||||
if (!getActiveUserId()) return
|
||||
export async function saveAppearancePrefsToServer(
|
||||
theme: string,
|
||||
colorScheme: string,
|
||||
userId?: string | null
|
||||
): Promise<void> {
|
||||
if (!resolveSyncedUserId(userId)) return
|
||||
|
||||
await apiJson<AppearancePrefs>(API_BASE, {
|
||||
method: 'PUT',
|
||||
@@ -42,17 +56,17 @@ export async function saveAppearancePrefsToServer(theme: string, colorScheme: st
|
||||
|
||||
/** Merge server-stored appearance with local cache (server wins after cache wipe). */
|
||||
export async function syncAppearancePrefs(userId?: string | null): Promise<void> {
|
||||
const id = userId?.trim() || getActiveUserId()
|
||||
const id = resolveSyncedUserId(userId)
|
||||
if (!id) return
|
||||
|
||||
try {
|
||||
const server = await fetchAppearancePrefs()
|
||||
const server = await fetchAppearancePrefs(id)
|
||||
|
||||
if (server.persisted) {
|
||||
setThemePreference(id, server.theme)
|
||||
setColorSchemePreference(id, server.colorScheme)
|
||||
} else if (hasLocalAppearancePrefs(id)) {
|
||||
await saveAppearancePrefsToServer(getThemePreference(id), getColorSchemePreference(id))
|
||||
await saveAppearancePrefsToServer(getThemePreference(id), getColorSchemePreference(id), id)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to sync appearance preferences:', err)
|
||||
|
||||
Reference in New Issue
Block a user