This commit is contained in:
2025-12-02 17:02:20 -05:00
parent f82ba25160
commit 6423d1c1a2
49 changed files with 2320 additions and 349 deletions

View File

@@ -1,17 +1,10 @@
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router'
import { getCachedImageUrl, revokeAllImageUrls } from '../common/imageCache'
import { isParentAuthenticated } from '../stores/auth'
import ChildForm from './child/ChildForm.vue'
interface Child {
id: string | number
name: string
age: number
points?: number
image_id: string | null
}
import { eventBus } from '@/common/eventBus'
import type { Child, Event } from '@/common/models'
const router = useRouter()
const children = ref<Child[]>([])
@@ -27,19 +20,13 @@ const confirmDeleteVisible = ref(false)
const deletingChildId = ref<string | number | null>(null)
const deleting = ref(false)
const showEditDialog = ref(false)
const editingChild = ref<Child | null>(null)
const openEditDialog = (child: Child, evt?: Event) => {
const openChildEditor = (child: Child, evt?: Event) => {
evt?.stopPropagation()
editingChild.value = { ...child } // shallow copy for editing
showEditDialog.value = true
closeMenu()
router.push({ name: 'ChildEditView', params: { id: child.id } })
}
const closeEditDialog = () => {
showEditDialog.value = false
editingChild.value = null
function handleServerChange(event: Event) {
fetchChildren()
}
// points update state
@@ -85,12 +72,28 @@ const fetchChildren = async () => {
}
}
const createChild = () => {
router.push({ name: 'CreateChild' })
}
onMounted(async () => {
eventBus.on('child_update', handleServerChange)
eventBus.on('task_update', handleServerChange)
eventBus.on('reward_update', handleServerChange)
eventBus.on('child_delete', handleServerChange)
await fetchChildren()
// 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)
})
const shouldIgnoreNextCardClick = ref(false)
const onDocClick = (e: MouseEvent) => {
@@ -194,11 +197,6 @@ const deletePoints = async (childId: string | number, evt?: Event) => {
}
}
const gridColumns = computed(() => {
const n = Math.min(children.value.length, 3)
return `repeat(${n || 1}, minmax(var(--card-width, 289px), 1fr))`
})
onBeforeUnmount(() => {
document.removeEventListener('click', onDocClick, true)
revokeAllImageUrls()
@@ -238,7 +236,7 @@ onBeforeUnmount(() => {
<button
class="menu-item"
@mousedown.stop.prevent
@click="openEditDialog(child, $event)"
@click="openChildEditor(child, $event)"
>
Edit Child
</button>
@@ -269,18 +267,6 @@ onBeforeUnmount(() => {
</div>
</div>
<ChildForm
v-if="showEditDialog"
:child="editingChild"
@close="closeEditDialog"
@updated="
async () => {
closeEditDialog()
await fetchChildren()
}
"
/>
<!-- confirmation modal -->
<div
v-if="confirmDeleteVisible"
@@ -308,6 +294,14 @@ onBeforeUnmount(() => {
</div>
</div>
</div>
<!-- Add Child button (FAB) -->
<button v-if="isParentAuthenticated" class="fab" @click="createChild" aria-label="Add Child">
<svg width="28" height="28" viewBox="0 0 28 28" fill="none">
<circle cx="14" cy="14" r="14" fill="#667eea" />
<path d="M14 8v12M8 14h12" stroke="#fff" stroke-width="2" stroke-linecap="round" />
</svg>
</button>
</div>
</template>
@@ -525,4 +519,32 @@ h1 {
font-weight: 600;
text-align: center;
}
/* Floating Action Button (FAB) */
.fab {
position: fixed;
bottom: 2rem;
right: 2rem;
background: #667eea;
color: #fff;
border: none;
border-radius: 50%;
width: 56px;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
cursor: pointer;
font-size: 24px;
z-index: 1300;
}
.fab:hover {
background: #5a67d8;
}
.fab:active {
background: #4c51bf;
}
</style>