Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 1x 1x 1x 1x 1x 10x 10x 10x 10x 11x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 6x 10x 2x | <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useListsStore } from '../stores/lists'
import ListSection from '../components/list/ListSection.vue'
import ListCard from '../components/list/ListCard.vue'
import FloatingActionButton from '../components/common/FloatingActionButton.vue'
import AddListModal from '../components/common/AddListModal.vue'
import { connectWebSocket, subscribe } from '../services/websocket'
import { getUserId } from '../services/userId'
import { useNotificationsStore } from '../stores/notifications'
const route = useRoute()
const router = useRouter()
const listsStore = useListsStore()
const notificationsStore = useNotificationsStore()
const showAddModal = ref(false)
const initialPresetId = ref<string | null>(null)
const initialPresetEmoji = ref<string | null>(null)
const initialPresetName = ref<string | null>(null)
let unsubscribe: (() => void) | null = null
onMounted(async () => {
listsStore.fetchAll()
const userId = getUserId()
Eif (userId) {
await connectWebSocket()
unsubscribe = subscribe(`/topic/user/${userId}`, (msg: unknown) => {
const body = msg as { type?: string; listId?: string; listName?: string; conflictCount?: number }
if (body?.type === 'LIST_ADDED') {
listsStore.fetchAll()
} else if (body?.type === 'CONFLICT_DETECTED' && body.listId && body.listName) {
notificationsStore.add({
listId: body.listId,
listName: body.listName,
message: 'Konflikt automatisch gelöst',
})
}
})
}
})
onUnmounted(() => {
unsubscribe?.()
})
// Open AddListModal pre-filled when coming from LibraryView via query params
watch(() => route.query, (q) => {
if (q.presetId) {
initialPresetId.value = q.presetId as string
initialPresetEmoji.value = (q.presetEmoji as string) || null
initialPresetName.value = (q.presetName as string) || null
showAddModal.value = true
// Clear query params without navigating away — keep userId in URL
router.replace({ name: 'home', params: { userId: getUserId()! }, query: {} })
}
}, { immediate: true })
const lists = computed(() => listsStore.lists)
const totalDone = computed(() => lists.value.reduce((a, l) => a + l.checkedCount, 0))
const totalRemaining = computed(() => lists.value.reduce((a, l) => a + (l.itemCount - l.checkedCount), 0))
const sharedCount = computed(() => lists.value.filter(l => l.participantCount > 1).length)
async function handleCreate(name: string, emoji: string, presetId: string | null) {
await listsStore.create({ name, emoji, presetId })
}
</script>
<template>
<div class="pt-16 pb-24 px-5 max-w-lg mx-auto">
<!-- Greeting -->
<div class="mt-4 mb-6 animate-fade-up">
<p class="text-ctp-overlay1 text-sm">Willkommen zurück</p>
<h2 class="text-2xl font-bold text-ctp-text mt-0.5">
Deine Listen
<span class="text-ctp-overlay0 font-normal text-base ml-1">
({{ lists.length }})
</span>
</h2>
</div>
<!-- Quick stats -->
<div class="grid grid-cols-3 gap-3 mb-8 animate-fade-up" style="animation-delay: 60ms">
<div class="bg-ctp-surface0/60 border border-ctp-surface1/40 rounded-2xl p-3 text-center">
<p class="text-xl font-bold text-ctp-green tabular-nums">{{ totalDone }}</p>
<p class="text-[10px] text-ctp-overlay0 mt-0.5 uppercase tracking-wider">Erledigt</p>
</div>
<div class="bg-ctp-surface0/60 border border-ctp-surface1/40 rounded-2xl p-3 text-center">
<p class="text-xl font-bold text-ctp-teal tabular-nums">{{ totalRemaining }}</p>
<p class="text-[10px] text-ctp-overlay0 mt-0.5 uppercase tracking-wider">Offen</p>
</div>
<div class="bg-ctp-surface0/60 border border-ctp-surface1/40 rounded-2xl p-3 text-center">
<p class="text-xl font-bold text-ctp-sapphire tabular-nums">{{ sharedCount }}</p>
<p class="text-[10px] text-ctp-overlay0 mt-0.5 uppercase tracking-wider">Geteilt</p>
</div>
</div>
<!-- Loading skeletons -->
<div v-if="listsStore.loading" class="space-y-3 animate-fade-up">
<div v-for="n in 3" :key="n" class="h-24 bg-ctp-surface0 rounded-2xl skeleton" />
</div>
<!-- Error -->
<div v-else-if="listsStore.error" class="text-center py-12 text-ctp-red text-sm animate-fade-up">
{{ listsStore.error }}
</div>
<!-- Lists -->
<template v-else>
<ListSection title="Meine Listen" :count="lists.length" class="mb-8">
<ListCard
v-for="(list, i) in lists"
:key="list.id"
:list="list"
:index="i"
/>
<div v-if="lists.length === 0" class="text-center py-12 animate-fade-up">
<p class="text-4xl mb-3">🛒</p>
<p class="text-ctp-overlay0 text-sm">Noch keine Listen. Erstelle deine erste!</p>
</div>
</ListSection>
</template>
<!-- FAB -->
<FloatingActionButton @click="showAddModal = true" />
<!-- Add modal -->
<AddListModal
:open="showAddModal"
:initial-preset-id="initialPresetId"
:initial-preset-emoji="initialPresetEmoji"
:initial-preset-name="initialPresetName"
@close="showAddModal = false"
@create="handleCreate"
/>
</div>
</template>
|