Skip to content

Commit 5b380e4

Browse files
committed
feat: add server-synced user preferences infrastructure (#484)
- introduce the foundational layer for persisting user preferences to the server - add UserPreferencesSchema and shared types for user preferences - add client-only sync composable with debounced saves, route guard flush, and sendBeacon fallback - integrate server sync into useSettings and migrate to shared UserPreferences type - extract generic localStorage helpers, migrate consumers, remove usePreferencesProvider
1 parent 145cc8d commit 5b380e4

12 files changed

Lines changed: 431 additions & 156 deletions
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { createLocalStorageProvider } from '~/utils/storage'
2+
3+
export function useLocalStorageHashProvider<T extends object>(key: string, defaultValue: T) {
4+
const provider = createLocalStorageProvider<T>(key)
5+
const data = ref<T>(defaultValue)
6+
7+
onMounted(() => {
8+
const stored = provider.get()
9+
if (stored) {
10+
data.value = { ...defaultValue, ...stored }
11+
}
12+
})
13+
14+
function save() {
15+
provider.set(data.value)
16+
}
17+
18+
function reset() {
19+
data.value = { ...defaultValue }
20+
provider.remove()
21+
}
22+
23+
function update<K extends keyof T>(key: K, value: T[K]) {
24+
data.value[key] = value
25+
save()
26+
}
27+
28+
return {
29+
data,
30+
save,
31+
reset,
32+
update,
33+
}
34+
}

app/composables/usePackageListPreferences.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,18 @@ import type {
1111
} from '#shared/types/preferences'
1212
import { DEFAULT_COLUMNS, DEFAULT_PREFERENCES } from '#shared/types/preferences'
1313

14+
const STORAGE_KEY = 'npmx-list-prefs'
15+
1416
/**
1517
* Composable for managing package list display preferences
1618
* Persists to localStorage and provides reactive state
17-
*
1819
*/
1920
export function usePackageListPreferences() {
2021
const {
2122
data: preferences,
22-
isHydrated,
2323
save,
2424
reset,
25-
} = usePreferencesProvider<PackageListPreferences>(DEFAULT_PREFERENCES)
25+
} = useLocalStorageHashProvider<PackageListPreferences>(STORAGE_KEY, DEFAULT_PREFERENCES)
2626

2727
// Computed accessors for common properties
2828
const viewMode = computed({
@@ -90,7 +90,6 @@ export function usePackageListPreferences() {
9090
return {
9191
// Raw preferences
9292
preferences,
93-
isHydrated,
9493

9594
// Individual properties with setters
9695
viewMode,

app/composables/usePreferencesProvider.ts

Lines changed: 0 additions & 100 deletions
This file was deleted.

app/composables/useSettings.ts

Lines changed: 71 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,23 @@
11
import type { RemovableRef } from '@vueuse/core'
22
import { useLocalStorage } from '@vueuse/core'
33
import { ACCENT_COLORS } from '#shared/utils/constants'
4-
import type { LocaleObject } from '@nuxtjs/i18n'
54
import { BACKGROUND_THEMES } from '#shared/utils/constants'
6-
7-
type BackgroundThemeId = keyof typeof BACKGROUND_THEMES
8-
9-
type AccentColorId = keyof typeof ACCENT_COLORS.light
10-
11-
/**
12-
* Application settings stored in localStorage
13-
*/
14-
export interface AppSettings {
15-
/** Display dates as relative (e.g., "3 days ago") instead of absolute */
16-
relativeDates: boolean
17-
/** Include @types/* package in install command for packages without built-in types */
18-
includeTypesInInstall: boolean
19-
/** Accent color theme */
20-
accentColorId: AccentColorId | null
21-
/** Preferred background shade */
22-
preferredBackgroundTheme: BackgroundThemeId | null
23-
/** Hide platform-specific packages (e.g., @scope/pkg-linux-x64) from search results */
24-
hidePlatformPackages: boolean
25-
/** User-selected locale */
26-
selectedLocale: LocaleObject['code'] | null
27-
sidebar: {
28-
collapsed: string[]
29-
}
30-
}
31-
32-
const DEFAULT_SETTINGS: AppSettings = {
33-
relativeDates: false,
34-
includeTypesInInstall: true,
35-
accentColorId: null,
36-
hidePlatformPackages: true,
37-
selectedLocale: null,
38-
preferredBackgroundTheme: null,
39-
sidebar: {
40-
collapsed: [],
41-
},
42-
}
5+
import {
6+
DEFAULT_USER_PREFERENCES,
7+
type AccentColorId,
8+
type BackgroundThemeId,
9+
type UserPreferences,
10+
} from '#shared/schemas/userPreferences'
4311

4412
const STORAGE_KEY = 'npmx-settings'
4513

46-
// Shared settings instance (singleton per app)
47-
let settingsRef: RemovableRef<AppSettings> | null = null
14+
let settingsRef: RemovableRef<UserPreferences> | null = null
15+
let syncInitialized = false
4816

49-
/**
50-
* Composable for managing application settings with localStorage persistence.
51-
* Settings are shared across all components that use this composable.
52-
*/
17+
// TODO: After discussion with the team, this will be replaced with a proper persistent solution (LS + server sync)
5318
export function useSettings() {
5419
if (!settingsRef) {
55-
settingsRef = useLocalStorage<AppSettings>(STORAGE_KEY, DEFAULT_SETTINGS, {
20+
settingsRef = useLocalStorage<UserPreferences>(STORAGE_KEY, DEFAULT_USER_PREFERENCES, {
5621
mergeDefaults: true,
5722
})
5823
}
@@ -62,18 +27,72 @@ export function useSettings() {
6227
}
6328
}
6429

65-
/**
66-
* Composable for accessing just the relative dates setting.
67-
* Useful for components that only need to read this specific setting.
68-
*/
30+
// TODO: Name to be changed
31+
export function useSettingsSync() {
32+
const { settings } = useSettings()
33+
const {
34+
isAuthenticated,
35+
status,
36+
lastSyncedAt,
37+
error,
38+
loadFromServer,
39+
scheduleSync,
40+
setupRouteGuard,
41+
setupBeforeUnload,
42+
} = useUserPreferencesSync()
43+
44+
const isSyncing = computed(() => status.value === 'syncing')
45+
const isSynced = computed(() => status.value === 'synced')
46+
const hasError = computed(() => status.value === 'error')
47+
48+
async function initializeSync(): Promise<void> {
49+
if (syncInitialized || import.meta.server) return
50+
51+
setupRouteGuard(() => settings.value)
52+
setupBeforeUnload(() => settings.value)
53+
54+
if (isAuthenticated.value) {
55+
const serverPrefs = await loadFromServer()
56+
Object.assign(settings.value, serverPrefs)
57+
}
58+
59+
watch(
60+
settings,
61+
newSettings => {
62+
if (isAuthenticated.value) {
63+
scheduleSync(newSettings)
64+
}
65+
},
66+
{ deep: true },
67+
)
68+
69+
watch(isAuthenticated, async newIsAuth => {
70+
if (newIsAuth) {
71+
const serverPrefs = await loadFromServer()
72+
Object.assign(settings.value, serverPrefs)
73+
}
74+
})
75+
76+
syncInitialized = true
77+
}
78+
79+
return {
80+
settings,
81+
isAuthenticated,
82+
isSyncing,
83+
isSynced,
84+
hasError,
85+
syncError: error,
86+
lastSyncedAt,
87+
initializeSync,
88+
}
89+
}
90+
6991
export function useRelativeDates() {
7092
const { settings } = useSettings()
7193
return computed(() => settings.value.relativeDates)
7294
}
7395

74-
/**
75-
* Composable for managing accent color.
76-
*/
7796
export function useAccentColor() {
7897
const { settings } = useSettings()
7998
const colorMode = useColorMode()

0 commit comments

Comments
 (0)