
This commit includes several improvements and new features: - Updated the WebSocket connection logic in `websocket.py` to include connection status messages and periodic pings for maintaining the connection. - Introduced new skeleton components (`Skeleton.vue`, `SkeletonDashboard.vue`, `SkeletonList.vue`) for improved loading states in the UI, enhancing user experience during data fetching. - Refactored the Vite configuration to support advanced code splitting and caching strategies, optimizing the build process. - Enhanced ESLint configuration for better compatibility with project structure. These changes aim to improve real-time communication, user interface responsiveness, and overall application performance.
144 lines
4.1 KiB
Vue
144 lines
4.1 KiB
Vue
<template>
|
||
<main class="p-4 max-w-screen-md mx-auto space-y-6">
|
||
<!-- Loading -->
|
||
<Spinner v-if="isLoading" :label="$t('listDetailPage.loading.list')" />
|
||
|
||
<!-- Error -->
|
||
<Alert v-else-if="error" type="error" :message="error" />
|
||
|
||
<!-- Main Content -->
|
||
<template v-else-if="list">
|
||
<div class="flex items-center justify-between gap-4">
|
||
<Heading :level="1">{{ list.name }}</Heading>
|
||
<Switch v-model="supermarktMode" />
|
||
</div>
|
||
|
||
<ProgressBar v-if="supermarktMode" :value="completion" />
|
||
|
||
<ItemsList :list="list" :items="itemsWithUI" :is-online="isOnline" :supermarkt-mode="supermarktMode"
|
||
:category-options="categoryOptions" :new-item="newItem" :categories="categories" @add-item="handleAddItem"
|
||
@checkbox-change="handleCheckboxChange" @delete-item="handleDeleteItem"
|
||
@update:newItemName="val => (newItem.name = val)"
|
||
@update:newItemCategoryId="val => (newItem.category_id = val)" />
|
||
</template>
|
||
</main>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
|
||
import { useRoute } from 'vue-router'
|
||
import { useI18n } from 'vue-i18n'
|
||
import { useNetwork } from '@vueuse/core'
|
||
import { storeToRefs } from 'pinia'
|
||
|
||
// Stores
|
||
import { useListsStore } from '@/stores/listsStore'
|
||
import { useAuthStore } from '@/stores/auth'
|
||
import { useCategoryStore } from '@/stores/categoryStore'
|
||
|
||
// UI Primitives
|
||
import { Heading, Spinner, Alert, ProgressBar, Switch } from '@/components/ui'
|
||
|
||
// Components
|
||
import ItemsList from '@/components/list-detail/ItemsList.vue'
|
||
|
||
const { t } = useI18n()
|
||
const { isOnline } = useNetwork()
|
||
const route = useRoute()
|
||
|
||
// Pinia stores
|
||
const listsStore = useListsStore()
|
||
const authStore = useAuthStore()
|
||
const categoryStore = useCategoryStore()
|
||
|
||
// Store refs
|
||
const { currentList: list, isLoading, error } = storeToRefs(listsStore)
|
||
const { categories } = storeToRefs(categoryStore)
|
||
|
||
// Local state
|
||
const supermarktMode = ref(false)
|
||
const newItem = reactive<{ name: string; category_id: number | null }>({ name: '', category_id: null })
|
||
|
||
// Derived data
|
||
const completion = computed(() => {
|
||
if (!list.value?.items.length) return 0
|
||
const done = list.value.items.filter(i => i.is_complete).length
|
||
return (done / list.value.items.length) * 100
|
||
})
|
||
|
||
const itemsWithUI = computed(() => {
|
||
return (list.value?.items || []).map(i => ({
|
||
...i,
|
||
updating: false,
|
||
deleting: false,
|
||
priceInput: i.price ?? null,
|
||
swiped: false,
|
||
}))
|
||
})
|
||
|
||
const categoryOptions = computed(() => [
|
||
{ label: t('listDetailPage.noCategory'), value: null },
|
||
...categories.value.map((c: { id: number; name: string }) => ({ label: c.name, value: c.id })),
|
||
])
|
||
|
||
// Lifecycle
|
||
onMounted(() => {
|
||
const listId = String(route.params.id)
|
||
listsStore.fetchListDetails(listId)
|
||
categoryStore.fetchCategories()
|
||
|
||
let stop: (() => void) | null = null
|
||
stop = watch(
|
||
() => authStore.accessToken,
|
||
token => {
|
||
if (token) {
|
||
listsStore.connectWebSocket(Number(listId), token)
|
||
if (stop) stop()
|
||
}
|
||
},
|
||
{ immediate: true },
|
||
)
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
listsStore.disconnectWebSocket()
|
||
})
|
||
|
||
// Stub handlers (to be implemented)
|
||
function handleAddItem() {
|
||
if (!list.value) return
|
||
const nameTrimmed = newItem.name.trim()
|
||
if (!nameTrimmed) return
|
||
|
||
listsStore
|
||
.addItem(list.value.id, {
|
||
name: nameTrimmed,
|
||
category_id: newItem.category_id ?? undefined,
|
||
})
|
||
.then(() => {
|
||
newItem.name = ''
|
||
newItem.category_id = null
|
||
})
|
||
.catch(err => {
|
||
console.error('Add item failed', err)
|
||
})
|
||
}
|
||
|
||
function handleCheckboxChange(item: any, checked: boolean) {
|
||
if (!list.value) return
|
||
listsStore
|
||
.updateItem(list.value.id, item.id, { is_complete: checked, version: item.version })
|
||
.catch(err => console.error('Update item failed', err))
|
||
}
|
||
|
||
function handleDeleteItem(item: any) {
|
||
if (!list.value) return
|
||
listsStore
|
||
.deleteItem(list.value.id, item.id, item.version)
|
||
.catch(err => console.error('Delete item failed', err))
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* No extra styles – Tailwind utility classes cover layout */
|
||
</style> |