Skip to content

Commit 955381d

Browse files
committed
refactor: split user-preferences composables and streamline sync flow
1 parent 6108722 commit 955381d

23 files changed

Lines changed: 264 additions & 209 deletions

app/components/Settings/AccentColorPicker.vue

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
<script setup lang="ts">
2-
import { useAccentColor } from '~/composables/useUserPreferences'
3-
42
const { accentColors, selectedAccentColor, setAccentColor } = useAccentColor()
53
64
onPrehydrate(el => {

app/composables/npm/useSearch.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { NpmSearchResponse, NpmSearchResult } from '#shared/types'
2-
import type { SearchProvider } from '~/composables/useSettings'
2+
import type { SearchProvider } from '#shared/schemas/userPreferences'
33
import type { AlgoliaMultiSearchChecks } from './useAlgoliaSearch'
44
import { type SearchSuggestion, emptySearchResponse, parseSuggestionIntent } from './search-utils'
55
import { isValidNewPackageName, checkPackageExists } from '~/utils/package-name'

app/composables/useInstallCommand.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function useInstallCommand(
1212
installVersionOverride?: MaybeRefOrGetter<string | null>,
1313
) {
1414
const selectedPM = useSelectedPackageManager()
15-
const { preferences } = useUserPreferences()
15+
const { preferences } = useUserPreferencesState()
1616

1717
// Check if we should show @types in install command
1818
const showTypesInInstall = computed(() => {

app/composables/useUserPreferences.ts

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

app/composables/useUserPreferencesProvider.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,15 @@
1313

1414
import type { RemovableRef } from '@vueuse/core'
1515
import { useLocalStorage } from '@vueuse/core'
16-
import type { UserPreferences } from '#shared/schemas/userPreferences'
16+
import { DEFAULT_USER_PREFERENCES, type UserPreferences } from '#shared/schemas/userPreferences'
1717

1818
const STORAGE_KEY = 'npmx-user-preferences'
1919

20+
function arePreferencesEqual(a: UserPreferences, b: UserPreferences): boolean {
21+
const keys = Object.keys(DEFAULT_USER_PREFERENCES) as (keyof typeof DEFAULT_USER_PREFERENCES)[]
22+
return keys.every(key => a[key] === b[key])
23+
}
24+
2025
export type HydratedUserPreferences = Required<Omit<UserPreferences, 'updatedAt'>> &
2126
Pick<UserPreferences, 'updatedAt'>
2227

@@ -26,7 +31,9 @@ let syncInitialized = false
2631
/**
2732
* User preferences provider with server sync support.
2833
*/
29-
export function useUserPreferencesProvider(defaultValue: HydratedUserPreferences) {
34+
export function useUserPreferencesProvider(
35+
defaultValue: HydratedUserPreferences = DEFAULT_USER_PREFERENCES,
36+
) {
3037
if (!dataRef) {
3138
dataRef = useLocalStorage<HydratedUserPreferences>(STORAGE_KEY, defaultValue, {
3239
mergeDefaults: true,
@@ -63,7 +70,10 @@ export function useUserPreferencesProvider(defaultValue: HydratedUserPreferences
6370
if (isAuthenticated.value) {
6471
const serverPrefs = await loadFromServer()
6572
if (serverPrefs) {
66-
preferences.value = { ...preferences.value, ...serverPrefs }
73+
const merged = { ...preferences.value, ...serverPrefs }
74+
if (!arePreferencesEqual(preferences.value, merged)) {
75+
preferences.value = merged
76+
}
6777
}
6878
}
6979

@@ -81,7 +91,10 @@ export function useUserPreferencesProvider(defaultValue: HydratedUserPreferences
8191
if (newIsAuth) {
8292
const serverPrefs = await loadFromServer()
8393
if (serverPrefs) {
84-
preferences.value = { ...defaultValue, ...preferences.value, ...serverPrefs }
94+
const merged = { ...defaultValue, ...preferences.value, ...serverPrefs }
95+
if (!arePreferencesEqual(preferences.value, merged)) {
96+
preferences.value = merged
97+
}
8598
}
8699
}
87100
})

app/composables/useUserPreferencesSync.client.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { UserPreferences } from '#shared/schemas/userPreferences'
22
import { DEFAULT_USER_PREFERENCES } from '#shared/schemas/userPreferences'
33

44
const SYNC_DEBOUNCE_MS = 2000
5+
const SYNCED_DISPLAY_MS = 3000
56

67
type SyncStatus = 'idle' | 'syncing' | 'synced' | 'error'
78

@@ -15,6 +16,7 @@ let syncStateInstance: PreferencesSyncState | null = null
1516
let pendingSavePromise: Promise<boolean> | null = null
1617
let hasPendingChanges = false
1718
let debounceTimeoutId: ReturnType<typeof setTimeout> | null = null
19+
let syncedResetTimeoutId: ReturnType<typeof setTimeout> | null = null
1820

1921
function getSyncState(): PreferencesSyncState {
2022
if (!syncStateInstance) {
@@ -38,6 +40,23 @@ async function fetchServerPreferences(): Promise<UserPreferences | null> {
3840
}
3941
}
4042

43+
/** Show 'synced' status briefly, then reset to 'idle'. */
44+
function showSyncedStatus(): void {
45+
const state = getSyncState()
46+
if (syncedResetTimeoutId) {
47+
clearTimeout(syncedResetTimeoutId)
48+
}
49+
state.status.value = 'synced'
50+
state.lastSyncedAt.value = new Date()
51+
syncedResetTimeoutId = setTimeout(() => {
52+
syncedResetTimeoutId = null
53+
54+
if (state.status.value === 'synced') {
55+
state.status.value = 'idle'
56+
}
57+
}, SYNCED_DISPLAY_MS)
58+
}
59+
4160
async function saveToServer(preferences: UserPreferences): Promise<boolean> {
4261
const state = getSyncState()
4362
state.status.value = 'syncing'
@@ -48,8 +67,7 @@ async function saveToServer(preferences: UserPreferences): Promise<boolean> {
4867
method: 'PUT',
4968
body: preferences,
5069
})
51-
state.status.value = 'synced'
52-
state.lastSyncedAt.value = new Date()
70+
showSyncedStatus()
5371
hasPendingChanges = false
5472
return true
5573
} catch (err) {
@@ -96,8 +114,7 @@ export function useUserPreferencesSync() {
96114
const serverPreferences = await fetchServerPreferences()
97115

98116
if (serverPreferences) {
99-
state.status.value = 'synced'
100-
state.lastSyncedAt.value = new Date()
117+
showSyncedStatus()
101118
return serverPreferences
102119
}
103120

@@ -120,7 +137,7 @@ export function useUserPreferencesSync() {
120137
function setupRouteGuard(getPreferences: () => UserPreferences): void {
121138
router.beforeEach(async (_to, _from, next) => {
122139
if (hasPendingChanges && isAuthenticated.value) {
123-
await flushPendingSync(getPreferences())
140+
void flushPendingSync(getPreferences())
124141
}
125142
next()
126143
})
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { ACCENT_COLORS } from '#shared/utils/constants'
2+
import type { AccentColorId } from '#shared/schemas/userPreferences'
3+
4+
export function useAccentColor() {
5+
const { preferences } = useUserPreferencesState()
6+
const colorMode = useColorMode()
7+
8+
const accentColors = computed(() => {
9+
const isDark = colorMode.value === 'dark'
10+
const colors = isDark ? ACCENT_COLORS.dark : ACCENT_COLORS.light
11+
12+
return Object.entries(colors).map(([id, value]) => ({
13+
id: id as AccentColorId,
14+
name: id,
15+
value,
16+
}))
17+
})
18+
19+
function setAccentColor(id: AccentColorId | null) {
20+
if (id) {
21+
document.documentElement.style.setProperty('--accent-color', `var(--swatch-${id})`)
22+
} else {
23+
document.documentElement.style.removeProperty('--accent-color')
24+
}
25+
preferences.value.accentColorId = id
26+
}
27+
28+
return {
29+
accentColors,
30+
selectedAccentColor: computed(() => preferences.value.accentColorId),
31+
setAccentColor,
32+
}
33+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { BACKGROUND_THEMES } from '#shared/utils/constants'
2+
import type { BackgroundThemeId } from '#shared/schemas/userPreferences'
3+
4+
export function useBackgroundTheme() {
5+
const backgroundThemes = Object.entries(BACKGROUND_THEMES).map(([id, value]) => ({
6+
id: id as BackgroundThemeId,
7+
name: id,
8+
value,
9+
}))
10+
11+
const { preferences } = useUserPreferencesState()
12+
13+
function setBackgroundTheme(id: BackgroundThemeId | null) {
14+
if (id) {
15+
document.documentElement.dataset.bgTheme = id
16+
} else {
17+
document.documentElement.removeAttribute('data-bg-theme')
18+
}
19+
preferences.value.preferredBackgroundTheme = id
20+
}
21+
22+
return {
23+
backgroundThemes,
24+
selectedBackgroundTheme: computed(() => preferences.value.preferredBackgroundTheme),
25+
setBackgroundTheme,
26+
}
27+
}

0 commit comments

Comments
 (0)