feat: add caching for backend objects
This commit is contained in:
75
app/app.vue
75
app/app.vue
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<IonApp>
|
||||
<!-- Offline indicator — floats above all pages -->
|
||||
<div v-if="!isOnline" class="offline-chip">
|
||||
<IonIcon :icon="cloudOfflineOutline" />
|
||||
<span>Offline</span>
|
||||
</div>
|
||||
|
||||
<IonSplitPane content-id="main-content">
|
||||
<IonMenu v-if="authStore.user" content-id="main-content" menu-id="main-menu">
|
||||
<IonHeader>
|
||||
@@ -91,11 +97,15 @@ import {
|
||||
import {
|
||||
homeOutline, calendarOutline, boatOutline, personOutline,
|
||||
bookOutline, calendarNumberOutline, peopleOutline, constructOutline, logOutOutline,
|
||||
layersOutline, settingsOutline, bookmarkOutline,
|
||||
layersOutline, settingsOutline, bookmarkOutline, cloudOfflineOutline,
|
||||
} from 'ionicons/icons'
|
||||
import { useAuthStore } from '~/stores/auth'
|
||||
import { useOfflineStatus } from '~/composables/useOfflineStatus'
|
||||
import { useAppCache } from '~/composables/useAppCache'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const { isOnline } = useOfflineStatus()
|
||||
const cache = useAppCache()
|
||||
|
||||
async function closeMenu() {
|
||||
await menuController.close('main-menu')
|
||||
@@ -116,4 +126,67 @@ watch(() => authStore.user, (u) => {
|
||||
if (u) authStore.fetchMember()
|
||||
else authStore.member = null
|
||||
})
|
||||
|
||||
// ── Realtime: keep cache fresh while online ──────────────────────────────────
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const supabase = useSupabaseClient() as any
|
||||
|
||||
type ReservationPayload = {
|
||||
eventType: 'INSERT' | 'UPDATE' | 'DELETE'
|
||||
new: { id: string; boat_id: string; start_time: string; end_time: string; status: string } | null
|
||||
old: { id: string; boat_id: string; start_time: string; end_time: string; status: string } | null
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
supabase
|
||||
.channel('app-cache-sync')
|
||||
.on('postgres_changes', { event: '*', schema: 'public', table: 'reservations' },
|
||||
(payload: ReservationPayload) => {
|
||||
const row = payload.new ?? payload.old
|
||||
if (!row?.start_time) return
|
||||
const wk = cache.weekKey(row.start_time)
|
||||
const slotsKey = `slots:${wk}`
|
||||
type SlotRow = { id: string; boat_id: string; start_time: string; end_time: string; status: string }
|
||||
const cached = cache.peek<SlotRow[]>(slotsKey) ?? []
|
||||
let updated: SlotRow[]
|
||||
if (payload.eventType === 'INSERT' && payload.new) {
|
||||
const { id, boat_id, start_time, end_time, status } = payload.new
|
||||
updated = [...cached, { id, boat_id, start_time, end_time, status }]
|
||||
} else if (payload.eventType === 'UPDATE' && payload.new) {
|
||||
updated = cached.map(s => s.id === payload.new!.id ? { ...s, status: payload.new!.status } : s)
|
||||
} else {
|
||||
updated = cached.filter(s => s.id !== (payload.old?.id))
|
||||
}
|
||||
cache.set(slotsKey, updated)
|
||||
})
|
||||
.on('postgres_changes', { event: '*', schema: 'public', table: 'intervals' },
|
||||
() => cache.invalidatePrefix('intervals:'))
|
||||
.on('postgres_changes', { event: '*', schema: 'public', table: 'boats' },
|
||||
async () => {
|
||||
cache.invalidate('boats')
|
||||
const { data } = await supabase.from('boats').select('*').order('name')
|
||||
if (data) cache.set('boats', data)
|
||||
})
|
||||
.subscribe()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.offline-chip {
|
||||
position: fixed;
|
||||
top: calc(env(safe-area-inset-top, 0px) + 8px);
|
||||
right: 12px;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
background: var(--ion-color-warning);
|
||||
color: var(--ion-color-warning-contrast);
|
||||
border-radius: 12px;
|
||||
padding: 4px 10px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user