initial commit
This commit is contained in:
123
web/vue-app/src/components/ChildDetailCard.vue
Normal file
123
web/vue-app/src/components/ChildDetailCard.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
import { defineProps, toRefs, ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { getCachedImageUrl, revokeAllImageUrls } from '../common/imageCache'
|
||||
|
||||
interface Child {
|
||||
id: string | number
|
||||
name: string
|
||||
age: number
|
||||
points?: number
|
||||
image_id: string | null
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
child: Child | null
|
||||
}>()
|
||||
|
||||
const { child } = toRefs(props)
|
||||
const imageUrl = ref<string | null>(null)
|
||||
|
||||
const imageCacheName = 'images-v1'
|
||||
|
||||
const fetchImage = async (imageId: string) => {
|
||||
try {
|
||||
const url = await getCachedImageUrl(imageId, imageCacheName)
|
||||
imageUrl.value = url
|
||||
} catch (err) {
|
||||
console.error('Error fetching child image:', err)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (child.value && child.value.image_id) {
|
||||
fetchImage(child.value.image_id)
|
||||
}
|
||||
})
|
||||
|
||||
// Revoke created object URLs when component unmounts to avoid memory leaks
|
||||
onBeforeUnmount(() => {
|
||||
revokeAllImageUrls()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="child" class="detail-card">
|
||||
<h1>{{ child.name }}</h1>
|
||||
<img v-if="imageUrl" :src="imageUrl" alt="Child Image" class="child-image" />
|
||||
<div class="info">
|
||||
<div class="info-item">
|
||||
<span class="label">Age:</span>
|
||||
<span class="value">{{ child.age }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">Points:</span>
|
||||
<span class="value">{{ child.points ?? '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.detail-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.18);
|
||||
padding: 1.2rem 1rem;
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.detail-card h1 {
|
||||
font-size: 1.3rem;
|
||||
color: #333;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.child-image {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
margin: 0 auto 0.7rem auto;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0.7rem;
|
||||
background: #f5f5f5;
|
||||
border-radius: 7px;
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
|
||||
/* Even more compact on small screens */
|
||||
@media (max-width: 480px) {
|
||||
.detail-card {
|
||||
padding: 0.7rem 0.4rem;
|
||||
max-width: 98vw;
|
||||
}
|
||||
.detail-card h1 {
|
||||
font-size: 1.05rem;
|
||||
margin-bottom: 0.7rem;
|
||||
}
|
||||
.child-image {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.info-item {
|
||||
padding: 0.38rem 0.5rem;
|
||||
font-size: 0.93rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user