refactoring
All checks were successful
Gitea Actions Demo / build-and-push (push) Successful in 12s

This commit is contained in:
2026-01-15 16:42:01 -05:00
parent dcac2742e9
commit 904185e5c8
9 changed files with 276 additions and 238 deletions

View File

@@ -285,7 +285,7 @@ def list_all_tasks(id):
tasks.append(task_dict) tasks.append(task_dict)
tasks.sort(key=lambda t: (not t['assigned'], t['name'].lower())) tasks.sort(key=lambda t: (not t['assigned'], t['name'].lower()))
return jsonify({ 'tasks': tasks }), 200 return jsonify({ 'tasks': tasks, 'count': len(tasks), 'list_type': 'task' }), 200
@child_api.route('/child/<id>/trigger-task', methods=['POST']) @child_api.route('/child/<id>/trigger-task', methods=['POST'])
@@ -373,7 +373,8 @@ def list_all_rewards(id):
return jsonify({ return jsonify({
'rewards': rewards, 'rewards': rewards,
'rewards_count': len(rewards) 'rewards_count': len(rewards),
'list_type': 'reward'
}), 200 }), 200
@@ -680,5 +681,5 @@ def list_pending_rewards():
) )
reward_responses.append(response.to_dict()) reward_responses.append(response.to_dict())
return jsonify({'rewards': reward_responses}), 200 return jsonify({'rewards': reward_responses, 'count': len(reward_responses), 'list_type': 'notification'}), 200

View File

@@ -6,6 +6,7 @@ export interface Task {
image_id: string | null image_id: string | null
image_url?: string | null // optional, for resolved URLs image_url?: string | null // optional, for resolved URLs
} }
export const TASK_FIELDS = ['id', 'name', 'points', 'is_good', 'image_id'] as const
export interface Child { export interface Child {
id: string id: string
@@ -17,6 +18,7 @@ export interface Child {
image_id: string | null image_id: string | null
image_url?: string | null // optional, for resolved URLs image_url?: string | null // optional, for resolved URLs
} }
export const CHILD_FIELDS = ['id', 'name', 'age', 'tasks', 'rewards', 'points', 'image_id'] as const
export interface Reward { export interface Reward {
id: string id: string
@@ -26,6 +28,7 @@ export interface Reward {
image_id: string | null image_id: string | null
image_url?: string | null // optional, for resolved URLs image_url?: string | null // optional, for resolved URLs
} }
export const REWARD_FIELDS = ['id', 'name', 'cost', 'points_needed', 'image_id'] as const
export interface RewardStatus { export interface RewardStatus {
id: string id: string
@@ -36,6 +39,14 @@ export interface RewardStatus {
image_id: string | null image_id: string | null
image_url?: string | null // optional, for resolved URLs image_url?: string | null // optional, for resolved URLs
} }
export const REWARD_STATUS_FIELDS = [
'id',
'name',
'points_needed',
'cost',
'redeeming',
'image_id',
] as const
export interface PendingReward { export interface PendingReward {
id: string id: string
@@ -48,6 +59,15 @@ export interface PendingReward {
reward_image_id: string | null reward_image_id: string | null
reward_image_url?: string | null // optional, for resolved URLs reward_image_url?: string | null // optional, for resolved URLs
} }
export const PENDING_REWARD_FIELDS = [
'id',
'child_id',
'child_name',
'child_image_id',
'reward_id',
'reward_name',
'reward_image_id',
] as const
export interface Event { export interface Event {
type: string type: string

View File

@@ -10,11 +10,18 @@
ref="rewardListRef" ref="rewardListRef"
:fetchUrl="`/api/child/${childId}/list-all-rewards`" :fetchUrl="`/api/child/${childId}/list-all-rewards`"
itemKey="rewards" itemKey="rewards"
:itemFields="['id', 'name', 'cost', 'image_id']" :itemFields="REWARD_FIELDS"
imageField="image_id" imageField="image_id"
selectable selectable
@loading-complete="(count) => (rewardCountRef = count)" @loading-complete="(count) => (rewardCountRef = count)"
/> :getItemClass="(item) => `reward`"
>
<template #item="{ item }">
<img v-if="item.image_url" :src="item.image_url" />
<span class="name">{{ item.name }}</span>
<span class="value">{{ item.cost }} pts</span>
</template>
</ItemList>
</div> </div>
<div class="actions" v-if="rewardCountRef != 0"> <div class="actions" v-if="rewardCountRef != 0">
<button class="btn btn-secondary" @click="onCancel">Cancel</button> <button class="btn btn-secondary" @click="onCancel">Cancel</button>
@@ -29,6 +36,7 @@ import { useRoute, useRouter } from 'vue-router'
import ItemList from '../shared/ItemList.vue' import ItemList from '../shared/ItemList.vue'
import MessageBlock from '../shared/MessageBlock.vue' import MessageBlock from '../shared/MessageBlock.vue'
import '@/assets/actions-shared.css' import '@/assets/actions-shared.css'
import { REWARD_FIELDS } from '@/common/models'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -90,4 +98,21 @@ function onCancel() {
padding: 0; padding: 0;
min-height: 0; min-height: 0;
} }
.name {
flex: 1;
text-align: left;
font-weight: 600;
}
.value {
min-width: 60px;
text-align: right;
font-weight: 600;
}
:deep(.reward) {
border-color: var(--list-item-border-reward);
background: var(--list-item-bg-reward);
}
</style> </style>

View File

@@ -10,11 +10,18 @@
ref="taskListRef" ref="taskListRef"
:fetchUrl="`/api/child/${childId}/list-all-tasks?type=${typeFilter}`" :fetchUrl="`/api/child/${childId}/list-all-tasks?type=${typeFilter}`"
itemKey="tasks" itemKey="tasks"
:itemFields="['id', 'name', 'points', 'is_good', 'image_id']" :itemFields="TASK_FIELDS"
imageField="image_id" imageField="image_id"
selectable selectable
@loading-complete="(count) => (taskCountRef = count)" @loading-complete="(count) => (taskCountRef = count)"
/> :getItemClass="(item) => ({ bad: !item.is_good, good: item.is_good })"
>
<template #item="{ item }">
<img v-if="item.image_url" :src="item.image_url" />
<span class="name">{{ item.name }}</span>
<span class="value">{{ item.points }} pts</span>
</template>
</ItemList>
</div> </div>
<div class="actions" v-if="taskCountRef > 0"> <div class="actions" v-if="taskCountRef > 0">
<button class="btn btn-secondary" @click="onCancel">Cancel</button> <button class="btn btn-secondary" @click="onCancel">Cancel</button>
@@ -29,6 +36,7 @@ import { useRoute, useRouter } from 'vue-router'
import ItemList from '../shared/ItemList.vue' import ItemList from '../shared/ItemList.vue'
import MessageBlock from '../shared/MessageBlock.vue' import MessageBlock from '../shared/MessageBlock.vue'
import '@/assets/actions-shared.css' import '@/assets/actions-shared.css'
import { TASK_FIELDS } from '@/common/models'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -96,4 +104,25 @@ function onCancel() {
padding: 0; padding: 0;
min-height: 0; min-height: 0;
} }
:deep(.good) {
border-color: var(--list-item-border-good);
background: var(--list-item-bg-good);
}
:deep(.bad) {
border-color: var(--list-item-border-bad);
background: var(--list-item-bg-bad);
}
.name {
flex: 1;
text-align: left;
font-weight: 600;
}
.value {
min-width: 60px;
text-align: right;
font-weight: 600;
}
</style> </style>

View File

@@ -1,141 +0,0 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { getCachedImageUrl } from '../../common/imageCache'
import type { PendingReward, Event, ChildRewardRequestEventPayload } from '@/common/models'
import { eventBus } from '@/common/eventBus'
const emit = defineEmits(['item-clicked'])
const loading = ref(true)
const error = ref<string | null>(null)
const notifications = ref<PendingReward[]>([])
const fetchNotifications = async () => {
loading.value = true
error.value = null
try {
const resp = await fetch('/api/pending-rewards')
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
const data = await resp.json()
const rewards: PendingReward[] = data.rewards || []
// Fetch images for child and reward
await Promise.all(
rewards.map(async (item) => {
if (item.child_image_id) {
try {
item.child_image_url = await getCachedImageUrl(item.child_image_id)
} catch (e) {
item.child_image_url = null
}
}
if (item.reward_image_id) {
try {
item.reward_image_url = await getCachedImageUrl(item.reward_image_id)
} catch (e) {
item.reward_image_url = null
}
}
}),
)
notifications.value = rewards
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to fetch notifications'
notifications.value = []
} finally {
loading.value = false
}
}
function handleRewardRequest(event: Event) {
const payload = event.payload as ChildRewardRequestEventPayload
const childId = payload.child_id
const rewardId = payload.reward_id
// Todo: Have event carry more info to avoid full refresh
fetchNotifications()
}
onMounted(async () => {
eventBus.on('child_reward_request', handleRewardRequest)
await fetchNotifications()
})
onUnmounted(() => {
eventBus.off('child_reward_request', handleRewardRequest)
})
function handleItemClick(item: PendingReward) {
emit('item-clicked', item)
}
</script>
<template>
<div class="centered-list-container">
<div v-if="loading" class="loading">Loading notifications...</div>
<div v-else-if="error" class="error">{{ error }}</div>
<div v-else-if="notifications.length === 0" class="empty">No notifications</div>
<div v-else class="listbox">
<div v-for="(item, idx) in notifications" :key="item.id">
<div class="list-item notification-centered" @click="handleItemClick(item)">
<div class="child-info">
<img
v-if="item.child_image_url"
:src="item.child_image_url"
alt="Child"
class="list-image"
/>
<span class="child-name">{{ item.child_name }}</span>
</div>
<span class="requested-text">requested</span>
<div class="reward-info">
<span class="reward-name">{{ item.reward_name }}</span>
<img
v-if="item.reward_image_url"
:src="item.reward_image_url"
alt="Reward"
class="list-image"
/>
</div>
</div>
<div v-if="idx < notifications.length - 1" class="list-separator"></div>
</div>
</div>
</div>
</template>
<style scoped>
.centered-list-container {
width: 100%;
display: flex;
justify-content: center;
}
.notification-centered {
justify-content: center;
}
.child-info {
display: flex;
align-items: center;
gap: 0.7rem;
}
.child-name {
font-weight: 600;
color: var(--dialog-child-name);
}
.reward-info {
display: flex;
align-items: center;
gap: 0.7rem;
margin-bottom: 0rem;
}
.reward-name {
font-weight: 600;
color: var(--notification-reward-name);
}
.requested-text {
margin: 0 0.7rem;
font-weight: 500;
color: var(--dialog-message);
font-size: 1.05rem;
white-space: nowrap;
}
</style>

View File

@@ -1,20 +1,43 @@
<template> <template>
<div class="notification-view"> <div class="notification-view">
<NotificationList @item-clicked="handleNotificationClick" /> <ItemList
:fetchUrl="`/api/pending-rewards`"
itemKey="rewards"
:itemFields="PENDING_REWARD_FIELDS"
:imageFields="['child_image_id', 'reward_image_id']"
@clicked="handleNotificationClick"
@loading-complete="(count) => (notificationListCountRef = count)"
>
<template #item="{ item }">
<div class="notification-centered">
<div class="child-info">
<img v-if="item.child_image_url" :src="item.child_image_url" alt="Child" />
<span>{{ item.child_name }}</span>
</div>
<span class="requested-text">requested</span>
<div class="reward-info">
<span>{{ item.reward_name }}</span>
<img v-if="item.reward_image_url" :src="item.reward_image_url" alt="Reward" />
</div>
</div>
</template>
</ItemList>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import NotificationList from './NotificationList.vue' import ItemList from '../shared/ItemList.vue'
import type { PendingReward } from '@/common/models' import type { PendingReward } from '@/common/models'
import { PENDING_REWARD_FIELDS } from '@/common/models'
const router = useRouter() const router = useRouter()
const notificationListCountRef = ref(-1)
function handleNotificationClick(item: PendingReward) { function handleNotificationClick(item: PendingReward) {
if (item.child_id) { router.push({ name: 'ParentView', params: { id: item.child_id } })
router.push({ name: 'ParentView', params: { id: item.child_id } })
}
} }
</script> </script>
@@ -29,4 +52,34 @@ function handleNotificationClick(item: PendingReward) {
padding: 0; padding: 0;
min-height: 0; min-height: 0;
} }
.notification-centered {
display: flex;
justify-content: center;
margin-inline: auto;
align-items: center;
}
.child-info {
display: flex;
align-items: center;
gap: 0.4rem;
font-weight: 600;
color: var(--dialog-child-name);
}
.reward-info {
display: flex;
align-items: center;
gap: 0.4rem;
margin-bottom: 0rem;
font-weight: 600;
color: var(--notification-reward-name);
}
.requested-text {
margin: 0 0.7rem;
font-weight: 500;
color: var(--dialog-message);
font-size: 1.05rem;
white-space: nowrap;
}
</style> </style>

View File

@@ -8,13 +8,20 @@
v-else v-else
fetchUrl="/api/reward/list" fetchUrl="/api/reward/list"
itemKey="rewards" itemKey="rewards"
:itemFields="['id', 'name', 'cost', 'description', 'image_id']" :itemFields="REWARD_FIELDS"
imageField="image_id" imageField="image_id"
deletable deletable
@edit="(rewardId) => $router.push({ name: 'EditReward', params: { id: rewardId } })" @clicked="(reward: Reward) => $router.push({ name: 'EditReward', params: { id: reward.id } })"
@delete="confirmDeleteReward" @delete="confirmDeleteReward"
@loading-complete="(count) => (rewardCountRef = count)" @loading-complete="(count) => (rewardCountRef = count)"
/> :getItemClass="(item) => `reward`"
>
<template #item="{ item }">
<img v-if="item.image_url" :src="item.image_url" />
<span class="name">{{ item.name }}</span>
<span class="value">{{ item.cost }} pts</span>
</template>
</ItemList>
<FloatingActionButton aria-label="Create Reward" @click="createReward" /> <FloatingActionButton aria-label="Create Reward" @click="createReward" />
@@ -35,6 +42,8 @@ import MessageBlock from '@/components/shared/MessageBlock.vue'
import '@/assets/button-shared.css' import '@/assets/button-shared.css'
import FloatingActionButton from '../shared/FloatingActionButton.vue' import FloatingActionButton from '../shared/FloatingActionButton.vue'
import DeleteModal from '../shared/DeleteModal.vue' import DeleteModal from '../shared/DeleteModal.vue'
import type { Reward } from '@/common/models'
import { REWARD_FIELDS } from '@/common/models'
import '@/assets/view-shared.css' import '@/assets/view-shared.css'
@@ -83,4 +92,21 @@ const createReward = () => {
padding: 0; padding: 0;
min-height: 0; min-height: 0;
} }
.name {
flex: 1;
text-align: left;
font-weight: 600;
}
.value {
min-width: 60px;
text-align: right;
font-weight: 600;
}
:deep(.reward) {
border-color: var(--list-item-border-reward);
background: var(--list-item-bg-reward);
}
</style> </style>

View File

@@ -1,20 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, onMounted, computed } from 'vue' import { ref, watch, onMounted } from 'vue'
import { getCachedImageUrl } from '@/common/imageCache' import { getCachedImageUrl } from '@/common/imageCache'
const props = defineProps<{ const props = defineProps<{
fetchUrl: string fetchUrl: string
itemKey: string itemKey: string
itemFields: string[] itemFields: readonly string[]
imageField?: string imageFields?: readonly string[]
selectable?: boolean selectable?: boolean
deletable?: boolean deletable?: boolean
onEdit?: (id: string) => void onClicked?: (item: any) => void
onDelete?: (id: string) => void onDelete?: (id: string) => void
filterFn?: (item: any) => boolean filterFn?: (item: any) => boolean
getItemClass?: (item: any) => string | string[] | Record<string, boolean>
}>() }>()
const emit = defineEmits(['edit', 'delete', 'loading-complete']) const emit = defineEmits(['clicked', 'delete', 'loading-complete'])
const items = ref<any[]>([]) const items = ref<any[]>([])
const loading = ref(true) const loading = ref(true)
@@ -32,24 +33,35 @@ const fetchItems = async () => {
console.log(`Fetching items from: ${props.fetchUrl}`) console.log(`Fetching items from: ${props.fetchUrl}`)
try { try {
const resp = await fetch(props.fetchUrl) const resp = await fetch(props.fetchUrl)
console.log(`Fetch response status: ${resp.status}`)
if (!resp.ok) throw new Error(`HTTP ${resp.status}`) if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
const data = await resp.json() const data = await resp.json()
//console log all data //console log all data
console.log('Fetched data:', data) console.log('Fetched data:', data)
let itemList = data[props.itemKey] || [] let itemList = data[props.itemKey || 'items'] || []
if (props.filterFn) itemList = itemList.filter(props.filterFn) if (props.filterFn) itemList = itemList.filter(props.filterFn)
await Promise.all( await Promise.all(
itemList.map(async (item: any) => { itemList.map(async (item: any) => {
if (props.imageField && item[props.imageField]) { if (props.imageFields) {
for (const field of props.imageFields) {
if (item[field]) {
try {
item[`${field.replace('_id', '_url')}`] = await getCachedImageUrl(item[field])
} catch {
item[`${field.replace('_id', '_url')}`] = null
}
}
}
} else if (item.image_id) {
try { try {
item.image_url = await getCachedImageUrl(item[props.imageField]) item.image_url = await getCachedImageUrl(item.image_id)
} catch { } catch {
item.image_url = null item.image_url = null
} }
//for each item see it there is an 'assigned' field that is true. if so check the item's selectable checkbox }
if (props.selectable && item.assigned === true) { //for each item see it there is an 'assigned' field that is true. if so check the item's selectable checkbox
selectedItems.value.push(item.id) if (props.selectable && item.assigned === true) {
} selectedItems.value.push(item.id)
} }
}), }),
) )
@@ -64,25 +76,16 @@ const fetchItems = async () => {
} }
} }
const getItemType = (itemList) => {
if (itemList.length === 0) return 'reward'
if (items.value[0].points !== undefined) {
return items.value[0].is_good ? 'good' : 'bad'
}
return 'reward'
}
onMounted(fetchItems) onMounted(fetchItems)
watch(() => props.fetchUrl, fetchItems) watch(() => props.fetchUrl, fetchItems)
const itemType = computed(() => getItemType(items))
const handleEdit = (id: string) => { const handleClicked = (item: any) => {
emit('edit', id) emit('clicked', item)
props.onEdit?.(id) props.onClicked?.(item)
} }
const handleDelete = (id: string) => { const handleDelete = (item: any) => {
emit('delete', id) emit('delete', item)
props.onDelete?.(id) props.onDelete?.(item)
} }
</script> </script>
@@ -92,44 +95,49 @@ const handleDelete = (id: string) => {
<div v-else-if="error" class="error">{{ error }}</div> <div v-else-if="error" class="error">{{ error }}</div>
<div v-else-if="items.length === 0" class="empty">No items found.</div> <div v-else-if="items.length === 0" class="empty">No items found.</div>
<div v-else> <div v-else>
<div v-for="(item, idx) in items" :key="item.id"> <div v-for="(item, idx) in items" :key="item.id" class="list-row">
<div class="list-item" :class="['list-item', itemType]" @click="handleEdit(item.id)"> <div :class="['list-item', props.getItemClass?.(item)]" @click.stop="handleClicked(item)">
<img v-if="item.image_url" :src="item.image_url" alt="Item" class="list-image" /> <slot name="item" :item="item">
<span class="list-name">{{ item.name }}</span> <!-- Default rendering if no slot is provided -->
<span v-if="item.points !== undefined" class="value">{{ item.points }} pts</span> <img v-if="item.image_url" :src="item.image_url" />
<span v-if="item.cost !== undefined" class="value">{{ item.cost }} pts</span> <span class="list-name">List Item</span>
<input <span class="list-value">1</span>
v-if="props.selectable" </slot>
type="checkbox" <div v-if="props.selectable || props.deletable" class="interact">
class="list-checkbox" <input
v-model="selectedItems" v-if="props.selectable"
:value="item.id" type="checkbox"
@click.stop class="list-checkbox"
/> v-model="selectedItems"
<button :value="item"
v-if="props.deletable" @click.stop
class="delete-btn" />
@click.stop="handleDelete(item.id)" <button
aria-label="Delete item" v-if="props.deletable"
type="button" class="delete-btn"
> @click.stop="handleDelete(item)"
<!-- SVG icon here --> aria-label="Delete item"
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true"> type="button"
<circle cx="10" cy="10" r="9" fill="#fff" stroke="#ef4444" stroke-width="2" /> >
<path <!-- SVG icon here -->
d="M7 7l6 6M13 7l-6 6" <svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
stroke="#ef4444" <circle cx="10" cy="10" r="9" fill="#fff" stroke="#ef4444" stroke-width="2" />
stroke-width="2" <path
stroke-linecap="round" d="M7 7l6 6M13 7l-6 6"
/> stroke="#ef4444"
</svg> stroke-width="2"
</button> stroke-linecap="round"
/>
</svg>
</button>
</div>
<div v-if="idx < items.length - 1" class="list-separator"></div>
</div> </div>
<div v-if="idx < items.length - 1" class="list-separator"></div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.listbox { .listbox {
flex: 0 1 auto; flex: 0 1 auto;
@@ -160,22 +168,17 @@ const handleDelete = (id: string) => {
margin-right: 0.2rem; margin-right: 0.2rem;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.03); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.03);
box-sizing: border-box; box-sizing: border-box;
justify-content: space-between;
cursor: pointer;
} }
.list-item.bad { .list-item .interact {
border-color: var(--list-item-border-bad); display: flex;
background: var(--list-item-bg-bad); align-items: center;
} gap: 0.5rem;
.list-item.reward {
border-color: var(--list-item-border-reward);
background: var(--list-item-bg-reward);
}
.list-item.good {
border-color: var(--list-item-border-good);
background: var(--list-item-bg-good);
} }
/* Image styles */ /* Image styles */
.list-image { :deep(.list-item img) {
width: 36px; width: 36px;
height: 36px; height: 36px;
object-fit: cover; object-fit: cover;
@@ -202,7 +205,6 @@ const handleDelete = (id: string) => {
background: transparent; background: transparent;
border: none; border: none;
border-radius: 50%; border-radius: 50%;
padding: 0.15rem;
margin-left: 0.7rem; margin-left: 0.7rem;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
@@ -250,10 +252,6 @@ const handleDelete = (id: string) => {
border-radius: 8px; border-radius: 8px;
padding: 1rem; padding: 1rem;
} }
.value {
font-weight: 600;
margin-left: 1rem;
}
/* Separator (if needed) */ /* Separator (if needed) */
.list-separator { .list-separator {
height: 0px; height: 0px;

View File

@@ -8,13 +8,20 @@
v-else v-else
fetchUrl="/api/task/list" fetchUrl="/api/task/list"
itemKey="tasks" itemKey="tasks"
:itemFields="['id', 'name', 'points', 'is_good', 'image_id']" :itemFields="TASK_FIELDS"
imageField="image_id" imageField="image_id"
deletable deletable
@edit="(taskId) => $router.push({ name: 'EditTask', params: { id: taskId } })" @clicked="(task: Task) => $router.push({ name: 'EditTask', params: { id: task.id } })"
@delete="confirmDeleteTask" @delete="confirmDeleteTask"
@loading-complete="(count) => (taskCountRef = count)" @loading-complete="(count) => (taskCountRef = count)"
/> :getItemClass="(item) => ({ bad: !item.is_good, good: item.is_good })"
>
<template #item="{ item }">
<img v-if="item.image_url" :src="item.image_url" />
<span class="name">{{ item.name }}</span>
<span class="value">{{ item.points }} pts</span>
</template>
</ItemList>
<FloatingActionButton aria-label="Create Task" @click="createTask" /> <FloatingActionButton aria-label="Create Task" @click="createTask" />
@@ -35,8 +42,8 @@ import MessageBlock from '@/components/shared/MessageBlock.vue'
import '@/assets/button-shared.css' import '@/assets/button-shared.css'
import FloatingActionButton from '../shared/FloatingActionButton.vue' import FloatingActionButton from '../shared/FloatingActionButton.vue'
import DeleteModal from '../shared/DeleteModal.vue' import DeleteModal from '../shared/DeleteModal.vue'
import type { Task } from '@/common/models'
import '@/assets/view-shared.css' import { TASK_FIELDS } from '@/common/models'
const $router = useRouter() const $router = useRouter()
@@ -86,4 +93,24 @@ const createTask = () => {
padding: 0; padding: 0;
min-height: 0; min-height: 0;
} }
.name {
flex: 1;
text-align: left;
font-weight: 600;
}
.value {
min-width: 60px;
text-align: right;
font-weight: 600;
}
:deep(.good) {
border-color: var(--list-item-border-good);
background: var(--list-item-bg-good);
}
:deep(.bad) {
border-color: var(--list-item-border-bad);
background: var(--list-item-bg-bad);
}
</style> </style>