round 4
This commit is contained in:
@@ -1,10 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { ref, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getCachedImageUrl, revokeAllImageUrls } from '../common/imageCache'
|
||||
import { isParentAuthenticated } from '../stores/auth'
|
||||
import { eventBus } from '@/common/eventBus'
|
||||
import type { Child, Event } from '@/common/models'
|
||||
import type {
|
||||
Child,
|
||||
ChildModifiedEventPayload,
|
||||
ChildTaskTriggeredEventPayload,
|
||||
ChildRewardTriggeredEventPayload,
|
||||
Event,
|
||||
} from '@/common/models'
|
||||
|
||||
const router = useRouter()
|
||||
const children = ref<Child[]>([])
|
||||
@@ -25,8 +31,70 @@ const openChildEditor = (child: Child, evt?: Event) => {
|
||||
router.push({ name: 'ChildEditView', params: { id: child.id } })
|
||||
}
|
||||
|
||||
function handleServerChange(event: Event) {
|
||||
fetchChildren()
|
||||
async function handleChildModified(event: Event) {
|
||||
const payload = event.payload as ChildModifiedEventPayload
|
||||
const childId = payload.child_id
|
||||
|
||||
switch (payload.operation) {
|
||||
case 'DELETE':
|
||||
children.value = children.value.filter((c) => c.id !== childId)
|
||||
break
|
||||
|
||||
case 'ADD':
|
||||
try {
|
||||
const list = await fetchChildren()
|
||||
children.value = list
|
||||
} catch (err) {
|
||||
console.warn('Failed to fetch children after ADD operation:', err)
|
||||
}
|
||||
break
|
||||
|
||||
case 'EDIT':
|
||||
try {
|
||||
const list = await fetchChildren()
|
||||
const updatedChild = list.find((c) => c.id === childId)
|
||||
if (updatedChild) {
|
||||
const idx = children.value.findIndex((c) => c.id === childId)
|
||||
if (idx !== -1) {
|
||||
children.value[idx] = updatedChild
|
||||
} else {
|
||||
console.warn(`EDIT operation: child with id ${childId} not found in current list.`)
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
`EDIT operation: updated child with id ${childId} not found in fetched list.`,
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to fetch children after EDIT operation:', err)
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
console.warn(`Unknown operation: ${payload.operation}`)
|
||||
}
|
||||
}
|
||||
|
||||
function handleChildTaskTriggered(event: Event) {
|
||||
const payload = event.payload as ChildTaskTriggeredEventPayload
|
||||
const childId = payload.child_id
|
||||
const child = children.value.find((c) => c.id === childId)
|
||||
if (child) {
|
||||
child.points = payload.points
|
||||
} else {
|
||||
console.warn(`Child with id ${childId} not found when updating points.`)
|
||||
}
|
||||
}
|
||||
|
||||
function handleChildRewardTriggered(event: Event) {
|
||||
const payload = event.payload as ChildRewardTriggeredEventPayload
|
||||
const childId = payload.child_id
|
||||
const child = children.value.find((c) => c.id === childId)
|
||||
if (child) {
|
||||
child.points = payload.points
|
||||
} else {
|
||||
console.warn(`Child with id ${childId} not found when updating points.`)
|
||||
}
|
||||
}
|
||||
|
||||
// points update state
|
||||
@@ -41,8 +109,7 @@ const fetchImage = async (imageId: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
// extracted fetch so we can refresh after delete / points edit
|
||||
const fetchChildren = async () => {
|
||||
const fetchChildren = async (): Promise<Child[]> => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
images.value.clear()
|
||||
@@ -53,20 +120,22 @@ const fetchChildren = async () => {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
const data = await response.json()
|
||||
children.value = data.children || []
|
||||
const childList = data.children || []
|
||||
|
||||
// Fetch images for each child (shared cache util)
|
||||
await Promise.all(
|
||||
children.value.map((child) => {
|
||||
childList.map((child) => {
|
||||
if (child.image_id) {
|
||||
return fetchImage(child.image_id)
|
||||
}
|
||||
return Promise.resolve()
|
||||
}),
|
||||
)
|
||||
return childList
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to fetch children'
|
||||
console.error('Error fetching children:', err)
|
||||
return []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -77,21 +146,22 @@ const createChild = () => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
eventBus.on('child_update', handleServerChange)
|
||||
eventBus.on('task_update', handleServerChange)
|
||||
eventBus.on('reward_update', handleServerChange)
|
||||
eventBus.on('child_delete', handleServerChange)
|
||||
eventBus.on('child_modified', handleChildModified)
|
||||
eventBus.on('child_task_triggered', handleChildTaskTriggered)
|
||||
eventBus.on('child_reward_triggered', handleChildRewardTriggered)
|
||||
|
||||
await fetchChildren()
|
||||
const listPromise = fetchChildren()
|
||||
listPromise.then((list) => {
|
||||
children.value = list
|
||||
})
|
||||
// listen for outside clicks to auto-close any open kebab menu
|
||||
document.addEventListener('click', onDocClick, true)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
eventBus.off('child_update', handleServerChange)
|
||||
eventBus.off('task_update', handleServerChange)
|
||||
eventBus.off('reward_update', handleServerChange)
|
||||
eventBus.off('child_delete', handleServerChange)
|
||||
onUnmounted(() => {
|
||||
eventBus.off('child_modified', handleChildModified)
|
||||
eventBus.off('child_task_triggered', handleChildTaskTriggered)
|
||||
eventBus.off('child_reward_triggered', handleChildRewardTriggered)
|
||||
})
|
||||
|
||||
const shouldIgnoreNextCardClick = ref(false)
|
||||
@@ -188,8 +258,7 @@ const deletePoints = async (childId: string | number, evt?: Event) => {
|
||||
if (!resp.ok) {
|
||||
throw new Error(`Failed to update points: ${resp.status}`)
|
||||
}
|
||||
// refresh the list so points reflect the change
|
||||
await fetchChildren()
|
||||
// no need to refresh since we update optimistically via eventBus
|
||||
} catch (err) {
|
||||
console.error('Failed to delete points for child', childId, err)
|
||||
} finally {
|
||||
@@ -204,13 +273,22 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<div v-if="loading" class="loading">Loading...</div>
|
||||
<div>
|
||||
<div v-if="children.length === 0" class="no-children-message">
|
||||
<div>No children</div>
|
||||
<div class="sub-message">
|
||||
<template v-if="!isParentAuthenticated">
|
||||
<button class="sign-in-btn" @click="eventBus.emit('open-login')">Sign in</button> to
|
||||
create a child
|
||||
</template>
|
||||
<span v-else><button class="sign-in-btn" @click="createChild">Create</button> a child</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="loading" class="loading">Loading...</div>
|
||||
|
||||
<div v-else-if="error" class="error">Error: {{ error }}</div>
|
||||
|
||||
<div v-else-if="children.length === 0" class="empty">No children found</div>
|
||||
|
||||
<div v-else class="grid">
|
||||
<div v-for="child in children" :key="child.id" class="card" @click="selectChild(child.id)">
|
||||
<!-- kebab menu shown only for authenticated parent -->
|
||||
@@ -547,4 +625,37 @@ h1 {
|
||||
.fab:active {
|
||||
background: #4c51bf;
|
||||
}
|
||||
|
||||
.no-children-message {
|
||||
margin: 2rem 0;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
color: #fdfdfd;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.sub-message {
|
||||
margin-top: 0.3rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
color: #b5ccff;
|
||||
}
|
||||
.sign-in-btn {
|
||||
background: #fff;
|
||||
color: #2563eb;
|
||||
border: 2px solid #2563eb;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
padding: 0.2rem 0.5rem;
|
||||
margin-right: 0.1rem;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.18s,
|
||||
color 0.18s;
|
||||
}
|
||||
.sign-in-btn:hover {
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user