round 3
This commit is contained in:
247
web/vue-app/src/components/child/ChildEditView.vue
Normal file
247
web/vue-app/src/components/child/ChildEditView.vue
Normal file
@@ -0,0 +1,247 @@
|
||||
<template>
|
||||
<div class="child-edit-view">
|
||||
<h2>{{ isEdit ? 'Edit Child' : 'Create Child' }}</h2>
|
||||
<div v-if="loading" class="loading-message">Loading child...</div>
|
||||
<form v-else @submit.prevent="submit" class="form">
|
||||
<div class="form-group">
|
||||
<label for="child-name">Name</label>
|
||||
<input id="child-name" ref="nameInput" v-model="name" required maxlength="64" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="child-age">Age</label>
|
||||
<input id="child-age" v-model.number="age" type="number" min="0" max="120" required />
|
||||
</div>
|
||||
<div class="form-group image-picker-group">
|
||||
<label for="child-image">Image</label>
|
||||
<ImagePicker
|
||||
id="child-image"
|
||||
v-model="selectedImageId"
|
||||
:image-type="1"
|
||||
@add-image="onAddImage"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="error" class="error">{{ error }}</div>
|
||||
<div class="actions">
|
||||
<button type="button" class="btn cancel" @click="onCancel" :disabled="loading">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" class="btn save" :disabled="loading">
|
||||
{{ isEdit ? 'Save' : 'Create' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import ImagePicker from '../ImagePicker.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// Accept id as a prop for edit mode
|
||||
const props = defineProps<{ id?: string }>()
|
||||
|
||||
const isEdit = computed(() => !!props.id)
|
||||
const name = ref('')
|
||||
const age = ref<number | null>(null)
|
||||
const selectedImageId = ref<string | null>(null)
|
||||
const localImageFile = ref<File | null>(null)
|
||||
const nameInput = ref<HTMLInputElement | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
onMounted(async () => {
|
||||
if (isEdit.value && props.id) {
|
||||
loading.value = true
|
||||
try {
|
||||
const resp = await fetch(`/api/child/${props.id}`)
|
||||
if (!resp.ok) throw new Error('Failed to load child')
|
||||
const data = await resp.json()
|
||||
name.value = data.name ?? ''
|
||||
age.value = Number(data.age) ?? null
|
||||
selectedImageId.value = data.image_id ?? null
|
||||
} catch (e) {
|
||||
error.value = 'Could not load child.'
|
||||
} finally {
|
||||
loading.value = false
|
||||
await nextTick()
|
||||
nameInput.value?.focus()
|
||||
}
|
||||
} else {
|
||||
await nextTick()
|
||||
nameInput.value?.focus()
|
||||
}
|
||||
})
|
||||
|
||||
function onAddImage({ id, file }: { id: string; file: File }) {
|
||||
if (id === 'local-upload') {
|
||||
localImageFile.value = file
|
||||
}
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
let imageId = selectedImageId.value
|
||||
error.value = null
|
||||
if (!name.value.trim()) {
|
||||
error.value = 'Child name is required.'
|
||||
return
|
||||
}
|
||||
if (age.value === null || age.value < 0) {
|
||||
error.value = 'Age must be a non-negative number.'
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
|
||||
// If the selected image is a local upload, upload it first
|
||||
if (imageId === 'local-upload' && localImageFile.value) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', localImageFile.value)
|
||||
formData.append('type', '1')
|
||||
formData.append('permanent', 'false')
|
||||
try {
|
||||
const resp = await fetch('/api/image/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
if (!resp.ok) throw new Error('Image upload failed')
|
||||
const data = await resp.json()
|
||||
imageId = data.id
|
||||
} catch (err) {
|
||||
alert('Failed to upload image.')
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Now update or create the child
|
||||
try {
|
||||
let resp
|
||||
if (isEdit.value && props.id) {
|
||||
resp = await fetch(`/api/child/${props.id}/edit`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: name.value,
|
||||
age: age.value,
|
||||
image_id: imageId,
|
||||
}),
|
||||
})
|
||||
} else {
|
||||
resp = await fetch('/api/child/add', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: name.value,
|
||||
age: age.value,
|
||||
image_id: imageId,
|
||||
}),
|
||||
})
|
||||
}
|
||||
if (!resp.ok) throw new Error('Failed to save child')
|
||||
await router.push({ name: 'ChildrenListView' })
|
||||
} catch (err) {
|
||||
alert('Failed to save child.')
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
router.back()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.child-edit-view {
|
||||
max-width: 400px;
|
||||
margin: 2rem auto;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 24px #667eea22;
|
||||
padding: 2rem 2.2rem 1.5rem 2.2rem;
|
||||
}
|
||||
h2 {
|
||||
margin-bottom: 1.2rem;
|
||||
font-size: 1.15rem;
|
||||
color: #667eea;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.1rem;
|
||||
}
|
||||
.form-group {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.35rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
label {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 1rem;
|
||||
}
|
||||
input[type='text'],
|
||||
input[type='number'] {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 0.5rem 0.7rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e6e6e6;
|
||||
font-size: 1rem;
|
||||
background: #fafbff;
|
||||
color: #222;
|
||||
transition: border 0.2s;
|
||||
}
|
||||
input:focus {
|
||||
outline: none;
|
||||
border: 1.5px solid #667eea;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.7rem;
|
||||
justify-content: center;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.btn {
|
||||
padding: 0.5rem 1.1rem;
|
||||
border-radius: 8px;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
transition: background 0.18s;
|
||||
}
|
||||
.btn.cancel {
|
||||
background: #f3f3f3;
|
||||
color: #666;
|
||||
}
|
||||
.btn.save {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
.btn.save:hover {
|
||||
background: #5a67d8;
|
||||
}
|
||||
.form-group.image-picker-group {
|
||||
display: block;
|
||||
text-align: left;
|
||||
}
|
||||
.error {
|
||||
color: #e53e3e;
|
||||
margin-top: 0.7rem;
|
||||
text-align: center;
|
||||
}
|
||||
.loading-message {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user