kiwi/frontend/src/stores/settings.ts
pyr0ball c3e7dc1ea4 feat: time-first recipe entry (kiwi#52)
- Add max_total_min to RecipeRequest schema and TypeScript interface
- Add _within_time() helper to recipe_engine using parse_time_effort()
  with graceful degradation (empty directions or no signals -> pass)
- Wire max_total_min filter into suggest() loop after max_time_min
- Add time_first_layout to allowed settings keys
- Add timeFirstLayout ref to settings store (preserves sensoryPreferences)
- Add maxTotalMin ref to recipes store, wired into _buildRequest()
- Add time bucket selector UI (15/30/45/60/90 min) in RecipesView
  Find tab, gated by timeFirstLayout != 'normal'
- Add time-first layout selector section in SettingsView
- Add 5 _within_time unit tests and 2 settings key tests
2026-04-24 10:15:58 -07:00

115 lines
3.5 KiB
TypeScript

/**
* Settings Store
*
* Manages user settings (cooking equipment, preferences) using Pinia.
*/
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { settingsAPI } from '../services/api'
import type { UnitSystem } from '../utils/units'
import type { SensoryPreferences } from '../services/api'
import { DEFAULT_SENSORY_PREFERENCES } from '../services/api'
export type TimeFirstLayout = 'auto' | 'time_first' | 'normal'
export const useSettingsStore = defineStore('settings', () => {
// State
const cookingEquipment = ref<string[]>([])
const unitSystem = ref<UnitSystem>('metric')
const shoppingLocale = ref<string>('us')
const sensoryPreferences = ref<SensoryPreferences>({ ...DEFAULT_SENSORY_PREFERENCES })
const timeFirstLayout = ref<TimeFirstLayout>('auto')
const loading = ref(false)
const saved = ref(false)
// Actions
async function load() {
loading.value = true
try {
const [rawEquipment, rawUnits, rawLocale, rawSensory, rawTimeFirst] = await Promise.allSettled([
settingsAPI.getSetting('cooking_equipment'),
settingsAPI.getSetting('unit_system'),
settingsAPI.getSetting('shopping_locale'),
settingsAPI.getSetting('sensory_preferences'),
settingsAPI.getSetting('time_first_layout'),
])
if (rawEquipment.status === 'fulfilled' && rawEquipment.value) {
cookingEquipment.value = JSON.parse(rawEquipment.value)
}
if (rawUnits.status === 'fulfilled' && rawUnits.value) {
unitSystem.value = rawUnits.value as UnitSystem
}
if (rawLocale.status === 'fulfilled' && rawLocale.value) {
shoppingLocale.value = rawLocale.value
}
if (rawSensory.status === 'fulfilled' && rawSensory.value) {
try {
sensoryPreferences.value = JSON.parse(rawSensory.value)
} catch {
sensoryPreferences.value = { ...DEFAULT_SENSORY_PREFERENCES }
}
}
if (rawTimeFirst.status === 'fulfilled' && rawTimeFirst.value) {
timeFirstLayout.value = rawTimeFirst.value as TimeFirstLayout
}
} catch (err: unknown) {
console.error('Failed to load settings:', err)
} finally {
loading.value = false
}
}
async function save() {
loading.value = true
try {
await Promise.all([
settingsAPI.setSetting('cooking_equipment', JSON.stringify(cookingEquipment.value)),
settingsAPI.setSetting('unit_system', unitSystem.value),
settingsAPI.setSetting('shopping_locale', shoppingLocale.value),
settingsAPI.setSetting('sensory_preferences', JSON.stringify(sensoryPreferences.value)),
settingsAPI.setSetting('time_first_layout', timeFirstLayout.value),
])
saved.value = true
setTimeout(() => {
saved.value = false
}, 2000)
} catch (err: unknown) {
console.error('Failed to save settings:', err)
} finally {
loading.value = false
}
}
async function saveSensory() {
loading.value = true
try {
await settingsAPI.setSetting(
'sensory_preferences',
JSON.stringify(sensoryPreferences.value),
)
saved.value = true
setTimeout(() => { saved.value = false }, 2000)
} catch (err: unknown) {
console.error('Failed to save sensory preferences:', err)
} finally {
loading.value = false
}
}
return {
// State
cookingEquipment,
unitSystem,
shoppingLocale,
sensoryPreferences,
timeFirstLayout,
loading,
saved,
// Actions
load,
save,
saveSensory,
}
})