Refactor code structure for improved readability and maintainability
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m27s
58
.github/specs/feat-nav-selector-icons.md
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
# Feature: Change navigation selector icons
|
||||
|
||||
## Overview
|
||||
|
||||
**Goal:** The current view navigation that is displayed in parent mode currently displays custom svg drawing for Children, Tasks, Rewards, and Notifications. I have new png image files that I'd like to replace them with.
|
||||
|
||||
**User Story:**
|
||||
As a user, when I am in parent mode, I should be able to use the navigation selector as usual but see the updated images in the buttons.
|
||||
|
||||
**Rules:**
|
||||
The size of the navigation view-selector should still remain the same for both desktop and mobile. The images should replace the svg drawings.
|
||||
This will most likely only affect ParentView.vue / ParentLayout.vue
|
||||
|
||||
**Drawing to image replacements:**
|
||||
aria-label: "Children" -> frontend/vue-app/public/nav/children.png
|
||||
aria-label: "Tasks" -> frontend/vue-app/public/nav/chores.png
|
||||
aria-label: "Rewards" -> frontend/vue-app/public/nav/rewards.png
|
||||
aria-label: "Notifications" -> frontend/vue-app/public/nav/notifications.png
|
||||
|
||||
---
|
||||
|
||||
## Data Model Changes
|
||||
|
||||
### Backend Model
|
||||
|
||||
### Frontend Model
|
||||
|
||||
---
|
||||
|
||||
## Backend Implementation
|
||||
|
||||
## Backend Tests
|
||||
|
||||
- [ ]
|
||||
|
||||
---
|
||||
|
||||
## Frontend Implementation
|
||||
|
||||
## Frontend Tests
|
||||
|
||||
- [ ]
|
||||
|
||||
---
|
||||
|
||||
## Future Considerations
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria (Definition of Done)
|
||||
|
||||
### Backend
|
||||
|
||||
- [ ]
|
||||
|
||||
### Frontend
|
||||
|
||||
- [ ]
|
||||
2
.gitignore
vendored
@@ -6,3 +6,5 @@ frontend/vue-app/playwright-report/
|
||||
frontend/vue-app/test-results/
|
||||
backend/test-results/
|
||||
.vscode/keybindings.json
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
|
||||
BIN
frontend/vue-app/public/nav/children.png
Normal file
|
After Width: | Height: | Size: 905 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
@@ -301,7 +301,10 @@ function handleChoreConfirmation(event: Event) {
|
||||
function isChoreCompletedToday(item: ChildTask): boolean {
|
||||
if (item.pending_status !== 'approved' || !item.approved_at) return false
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
return item.approved_at.slice(0, 10) === today
|
||||
if (item.approved_at.slice(0, 10) !== today) return false
|
||||
// If the task has a schedule and today is not a scheduled day, don't show as completed
|
||||
if (item.schedule && !isScheduledToday(item.schedule, new Date())) return false
|
||||
return true
|
||||
}
|
||||
|
||||
function isChorePending(item: ChildTask): boolean {
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
||||
import LoginButton from '../components/shared/LoginButton.vue'
|
||||
import { eventBus } from '@/common/eventBus'
|
||||
import type {
|
||||
Event,
|
||||
ChildRewardRequestEventPayload,
|
||||
ChildChoreConfirmationPayload,
|
||||
} from '@/common/models'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -30,9 +36,42 @@ const showBack = computed(
|
||||
),
|
||||
)
|
||||
|
||||
// Notification badge count
|
||||
const notificationCount = ref(0)
|
||||
|
||||
async function fetchNotificationCount() {
|
||||
try {
|
||||
const resp = await fetch('/api/pending-confirmations')
|
||||
if (resp.ok) {
|
||||
const data = await resp.json()
|
||||
notificationCount.value = Array.isArray(data.confirmations) ? data.confirmations.length : 0
|
||||
}
|
||||
} catch {
|
||||
notificationCount.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
function handleRewardRequestBadge(event: Event) {
|
||||
const payload = event.payload as ChildRewardRequestEventPayload
|
||||
if (['CREATED', 'CANCELLED', 'GRANTED'].includes(payload.operation)) {
|
||||
fetchNotificationCount()
|
||||
}
|
||||
}
|
||||
|
||||
function handleChoreConfirmationBadge(event: Event) {
|
||||
const payload = event.payload as ChildChoreConfirmationPayload
|
||||
if (['CONFIRMED', 'APPROVED', 'REJECTED', 'CANCELLED'].includes(payload.operation)) {
|
||||
fetchNotificationCount()
|
||||
}
|
||||
}
|
||||
|
||||
// Version fetching
|
||||
const appVersion = ref('')
|
||||
onMounted(async () => {
|
||||
await fetchNotificationCount()
|
||||
eventBus.on('child_reward_request', handleRewardRequestBadge)
|
||||
eventBus.on('child_chore_confirmation', handleChoreConfirmationBadge)
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/version')
|
||||
if (resp.ok) {
|
||||
@@ -43,6 +82,11 @@ onMounted(async () => {
|
||||
appVersion.value = ''
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
eventBus.off('child_reward_request', handleRewardRequestBadge)
|
||||
eventBus.off('child_chore_confirmation', handleChoreConfirmationBadge)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -69,22 +113,7 @@ onMounted(async () => {
|
||||
aria-label="Children"
|
||||
title="Children"
|
||||
>
|
||||
<!-- Children Icon -->
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="8" cy="10" r="3" />
|
||||
<circle cx="16" cy="10" r="3" />
|
||||
<path d="M2 20c0-2.5 3-4.5 6-4.5s6 2 6 4.5" />
|
||||
<path d="M10 20c0-2 2-3.5 6-3.5s6 1.5 6 3.5" />
|
||||
</svg>
|
||||
<img src="/nav/children.png" class="nav-icon" alt="Children" />
|
||||
</button>
|
||||
<button
|
||||
:class="{
|
||||
@@ -105,21 +134,7 @@ onMounted(async () => {
|
||||
aria-label="Tasks"
|
||||
title="Tasks"
|
||||
>
|
||||
<!-- Book Icon -->
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
|
||||
<path d="M20 22V6a2 2 0 0 0-2-2H6.5A2.5 2.5 0 0 0 4 6.5v13" />
|
||||
<path d="M16 2v4" />
|
||||
</svg>
|
||||
<img src="/nav/chores.png" class="nav-icon" alt="Tasks" />
|
||||
</button>
|
||||
<button
|
||||
:class="{
|
||||
@@ -129,23 +144,7 @@ onMounted(async () => {
|
||||
aria-label="Rewards"
|
||||
title="Rewards"
|
||||
>
|
||||
<!-- Trophy Icon -->
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M8 21h8" />
|
||||
<path d="M12 17v4" />
|
||||
<path d="M17 17a5 5 0 0 0 5-5V7h-4" />
|
||||
<path d="M7 17a5 5 0 0 1-5-5V7h4" />
|
||||
<rect x="7" y="2" width="10" height="15" rx="5" />
|
||||
</svg>
|
||||
<img src="/nav/rewards.png" class="nav-icon" alt="Rewards" />
|
||||
</button>
|
||||
<button
|
||||
:class="{ active: ['NotificationView'].includes(String(route.name)) }"
|
||||
@@ -153,22 +152,10 @@ onMounted(async () => {
|
||||
aria-label="Notifications"
|
||||
title="Notifications"
|
||||
>
|
||||
<!-- Notification/Bell Icon -->
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M18 16v-5a6 6 0 1 0-12 0v5" />
|
||||
<path d="M2 16h20" />
|
||||
<path d="M8 20a4 4 0 0 0 8 0" />
|
||||
<circle cx="19" cy="7" r="2" fill="#ef4444" stroke="none" />
|
||||
</svg>
|
||||
<span class="nav-icon-wrap">
|
||||
<img src="/nav/notifications.png" class="nav-icon" alt="Notifications" />
|
||||
<span v-if="notificationCount > 0" class="notification-badge"></span>
|
||||
</span>
|
||||
</button>
|
||||
</nav>
|
||||
<div v-else class="spacer"></div>
|
||||
@@ -293,8 +280,37 @@ onMounted(async () => {
|
||||
color: var(--button-active-text);
|
||||
}
|
||||
|
||||
.view-selector button.active svg {
|
||||
stroke: var(--button-active-text);
|
||||
.nav-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
object-fit: contain;
|
||||
opacity: 0.55;
|
||||
transition: opacity 0.18s;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-icon-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.notification-badge {
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
right: -3px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--btn-danger, #ef4444);
|
||||
border-radius: 50%;
|
||||
border: 1.5px solid var(--button-bg, #fff);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.view-selector button.active .nav-icon,
|
||||
.view-selector button:hover .nav-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.view-selector button:hover:not(.active) {
|
||||
|
||||