feat: add PendingRewardDialog, RewardConfirmDialog, and TaskConfirmDialog components
All checks were successful
Gitea Actions Demo / build-and-push (push) Successful in 25s

- Implemented PendingRewardDialog for handling pending reward requests.
- Created RewardConfirmDialog for confirming reward redemption.
- Developed TaskConfirmDialog for task confirmation with child name display.

test: add unit tests for ChildView and ParentView components

- Added comprehensive tests for ChildView including task triggering and SSE event handling.
- Implemented tests for ParentView focusing on override modal and SSE event management.

test: add ScrollingList component tests

- Created tests for ScrollingList to verify item fetching, loading states, and custom item classes.
- Included tests for two-step click interactions and edit button display logic.
- Moved toward hashed passwords.
This commit is contained in:
2026-02-10 20:21:05 -05:00
parent 3dee8b80a2
commit 401c21ad82
45 changed files with 4353 additions and 441 deletions

View File

@@ -0,0 +1,205 @@
<template>
<ModalDialog v-if="isOpen" @backdrop-click="$emit('close')">
<div class="override-edit-modal">
<h3>Edit {{ entityName }}</h3>
<div class="modal-body">
<label :for="`override-input-${entityId}`">
{{ entityType === 'task' ? 'New Points' : 'New Cost' }}:
</label>
<input
:id="`override-input-${entityId}`"
v-model.number="inputValue"
type="number"
min="0"
max="10000"
:disabled="loading"
@input="validateInput"
/>
<div v-if="errorMessage" class="error-message">{{ errorMessage }}</div>
<div class="default-hint">Default: {{ defaultValue }}</div>
</div>
<div class="modal-actions">
<button @click="$emit('close')" :disabled="loading" class="btn-secondary">Cancel</button>
<button @click="save" :disabled="!isValid || loading" class="btn-primary">Save</button>
</div>
</div>
</ModalDialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import ModalDialog from './shared/ModalDialog.vue'
import { setChildOverride, parseErrorResponse } from '@/common/api'
const props = defineProps<{
isOpen: boolean
childId: string
entityId: string
entityType: 'task' | 'reward'
entityName: string
defaultValue: number
currentOverride?: number
}>()
const emit = defineEmits<{
close: []
saved: []
}>()
const inputValue = ref<number>(0)
const errorMessage = ref<string>('')
const isValid = ref<boolean>(true)
const loading = ref<boolean>(false)
// Initialize input value when modal opens
watch(
() => props.isOpen,
(newVal) => {
if (newVal) {
inputValue.value = props.currentOverride ?? props.defaultValue
validateInput()
}
},
{ immediate: true },
)
function validateInput() {
const value = inputValue.value
if (value === null || value === undefined || isNaN(value)) {
errorMessage.value = 'Please enter a valid number'
isValid.value = false
return
}
if (value < 0 || value > 10000) {
errorMessage.value = 'Value must be between 0 and 10000'
isValid.value = false
return
}
errorMessage.value = ''
isValid.value = true
}
async function save() {
if (!isValid.value) {
return
}
loading.value = true
try {
const response = await setChildOverride(
props.childId,
props.entityId,
props.entityType,
inputValue.value,
)
if (!response.ok) {
const { msg } = parseErrorResponse(response)
alert(`Error: ${msg}`)
loading.value = false
return
}
emit('saved')
emit('close')
} catch (error) {
alert(`Error: ${error}`)
} finally {
loading.value = false
}
}
</script>
<style scoped>
.override-edit-modal {
background: var(--modal-bg);
padding: var(--spacing-md);
border-radius: var(--border-radius-md);
min-width: 300px;
}
.override-edit-modal h3 {
margin: 0 0 var(--spacing-md) 0;
color: var(--text-primary);
font-size: var(--font-size-lg);
}
.modal-body {
margin-bottom: var(--spacing-md);
}
.modal-body label {
display: block;
margin-bottom: var(--spacing-xs);
color: var(--text-secondary);
font-weight: 500;
}
.modal-body input[type='number'] {
width: 100%;
padding: var(--spacing-sm);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
font-size: var(--font-size-md);
box-sizing: border-box;
}
.modal-body input[type='number']:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.error-message {
color: var(--error-color);
font-size: var(--font-size-sm);
margin-top: var(--spacing-xs);
}
.default-hint {
color: var(--text-muted);
font-size: var(--font-size-sm);
margin-top: var(--spacing-xs);
}
.modal-actions {
display: flex;
gap: var(--spacing-sm);
justify-content: flex-end;
}
.modal-actions button {
padding: var(--spacing-sm) var(--spacing-md);
border: none;
border-radius: var(--border-radius-sm);
font-size: var(--font-size-md);
cursor: pointer;
transition: opacity 0.2s;
}
.modal-actions button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-primary {
background: var(--btn-primary);
color: white;
}
.btn-primary:hover:not(:disabled) {
opacity: 0.9;
}
.btn-secondary {
background: var(--btn-secondary);
color: var(--text-primary);
}
.btn-secondary:hover:not(:disabled) {
opacity: 0.8;
}
</style>