Refactor components to use ModalDialog and StatusMessage; update styles and remove unused files
Some checks failed
Gitea Actions Demo / build-and-push (push) Failing after 9s

- Replaced inline modal dialogs in ParentView with a reusable ModalDialog component.
- Introduced StatusMessage component for loading and error states in ParentView.
- Updated styles to use new colors.css and styles.css for consistent theming.
- Removed ChildRewardList.vue and ChildTaskList.vue components as they were no longer needed.
- Adjusted RewardAssignView and TaskAssignView to use new styles and shared button styles.
- Cleaned up imports across components to reflect the new styles and removed unused CSS files.
This commit is contained in:
2026-01-22 16:37:53 -05:00
parent a0a059472b
commit 63769fbe32
20 changed files with 438 additions and 674 deletions

View File

@@ -1,180 +0,0 @@
<script setup lang="ts">
import { ref, onBeforeUnmount, watch, nextTick, computed } from 'vue'
import { defineProps, defineEmits, defineExpose } from 'vue'
import { getCachedImageUrl, revokeAllImageUrls } from '../../common/imageCache'
import type { RewardStatus } from '@/common/models'
const imageCacheName = 'images-v1'
const props = defineProps<{
childId: string | null
childPoints: number
isParentAuthenticated: boolean
}>()
const emit = defineEmits(['trigger-reward'])
const rewards = ref<RewardStatus[]>([])
const loading = ref(true)
const error = ref<string | null>(null)
const scrollWrapper = ref<HTMLDivElement | null>(null)
const rewardRefs = ref<Record<string, HTMLElement | null>>({})
const lastCenteredRewardId = ref<string | null>(null)
const readyRewardId = ref<string | null>(null)
const fetchRewards = async (id: string | number | null) => {
if (!id) {
rewards.value = []
loading.value = false
return
}
loading.value = true
error.value = null
try {
const resp = await fetch(`/api/child/${id}/reward-status`)
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
const data = await resp.json()
rewards.value = data.reward_status
// Fetch images for each reward using shared utility
await Promise.all(rewards.value.map(fetchImage))
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to fetch rewards'
console.error('Error fetching rewards:', err)
} finally {
loading.value = false
}
}
const fetchImage = async (reward: RewardStatus) => {
if (!reward.image_id) {
console.log(`No image ID for reward: ${reward.id}`)
return
}
try {
const url = await getCachedImageUrl(reward.image_id, imageCacheName)
reward.image_id = url
} catch (err) {
console.error('Error fetching image for reward', reward.id, err)
}
}
const centerReward = async (rewardId: string) => {
await nextTick()
const wrapper = scrollWrapper.value
const card = rewardRefs.value[rewardId]
if (wrapper && card) {
const wrapperRect = wrapper.getBoundingClientRect()
const cardRect = card.getBoundingClientRect()
const wrapperScrollLeft = wrapper.scrollLeft
const cardCenter = cardRect.left + cardRect.width / 2
const wrapperCenter = wrapperRect.left + wrapperRect.width / 2
const scrollOffset = cardCenter - wrapperCenter
wrapper.scrollTo({
left: wrapperScrollLeft + scrollOffset,
behavior: 'smooth',
})
}
}
const handleRewardClick = async (rewardId: string) => {
await nextTick()
const wrapper = scrollWrapper.value
const card = rewardRefs.value[rewardId]
if (!wrapper || !card) return
const wrapperRect = wrapper.getBoundingClientRect()
const cardRect = card.getBoundingClientRect()
const cardCenter = cardRect.left + cardRect.width / 2
const cardFullyVisible = cardCenter >= wrapperRect.left && cardCenter <= wrapperRect.right
if (!cardFullyVisible || lastCenteredRewardId.value !== rewardId) {
// Center the reward, but don't trigger
await centerReward(rewardId)
lastCenteredRewardId.value = rewardId
readyRewardId.value = rewardId
return
}
// If already centered and visible, trigger the reward
await triggerReward(rewardId)
readyRewardId.value = null
}
const triggerReward = (rewardId: string) => {
const reward = rewards.value.find((rew) => rew.id === rewardId)
if (!reward) return // Don't trigger if not allowed
emit('trigger-reward', reward, reward.points_needed <= 0, reward.redeeming)
}
watch(
() => props.childId,
(newId) => {
fetchRewards(newId)
},
{ immediate: true },
)
watch(
() => props.childPoints,
() => {
rewards.value.forEach((reward) => {
reward.points_needed = Math.max(0, reward.cost - props.childPoints)
})
},
)
function getPendingRewards(): string[] {
return rewards.value.filter((r) => r.redeeming).map((r) => r.id)
}
// revoke created object URLs when component unmounts to avoid memory leaks
onBeforeUnmount(() => {
revokeAllImageUrls()
})
// expose refresh method for parent component
defineExpose({ refresh: () => fetchRewards(props.childId), getPendingRewards })
const isAnyPending = computed(() => rewards.value.some((r) => r.redeeming))
</script>
<template>
<div class="child-list-container">
<h3>Rewards</h3>
<div v-if="loading" class="loading">Loading rewards...</div>
<div v-else-if="error" class="error">Error: {{ error }}</div>
<div v-else-if="rewards.length === 0" class="empty">No rewards available</div>
<div v-else class="scroll-wrapper" ref="scrollWrapper">
<div class="item-scroll">
<div
v-for="r in rewards"
:key="r.id"
class="item-card"
:class="{
ready: readyRewardId === r.id,
disabled: isAnyPending && !r.redeeming,
}"
:ref="(el) => (rewardRefs[r.id] = el)"
@click="() => handleRewardClick(r.id)"
>
<div class="item-name">{{ r.name }}</div>
<img v-if="r.image_id" :src="r.image_id" alt="Reward Image" class="item-image" />
<div class="item-points" :class="{ ready: r.points_needed === 0 }">
<template v-if="r.points_needed === 0"> REWARD READY </template>
<template v-else> {{ r.points_needed }} more points </template>
</div>
<!-- PENDING block if redeeming is true -->
<div v-if="r.redeeming" class="pending-block">PENDING</div>
</div>
</div>
</div>
</div>
</template>
<style scoped></style>