chore: update project files
This commit is contained in:
214
web/src/views/TodoView.vue
Normal file
214
web/src/views/TodoView.vue
Normal file
@@ -0,0 +1,214 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import type { Task } from '../types'
|
||||
import * as api from '../services/api'
|
||||
import PaginationControl from '../components/PaginationControl.vue'
|
||||
import TodoEditorModal from '../components/TodoEditorModal.vue'
|
||||
import { t } from '../i18n'
|
||||
import { useToast } from '../composables/useToast'
|
||||
|
||||
const { success, error: showError } = useToast()
|
||||
|
||||
const tasks = ref<Task[]>([])
|
||||
const loading = ref(false)
|
||||
const modalOpen = ref(false)
|
||||
const editingTask = ref<Task | null>(null)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(6)
|
||||
const filter = ref<'all' | 'todo' | 'done'>('all')
|
||||
|
||||
const filteredTasks = computed(() => {
|
||||
if (filter.value === 'all') return tasks.value
|
||||
return tasks.value.filter((t) => t.status === filter.value)
|
||||
})
|
||||
|
||||
const total = computed(() => filteredTasks.value.length)
|
||||
const pages = computed(() => Math.max(Math.ceil(total.value / pageSize.value), 1))
|
||||
const pagedTasks = computed(() => {
|
||||
const start = (page.value - 1) * pageSize.value
|
||||
return filteredTasks.value.slice(start, start + pageSize.value)
|
||||
})
|
||||
|
||||
async function loadTasks() {
|
||||
loading.value = true
|
||||
try {
|
||||
const list = await api.listTasks()
|
||||
tasks.value = list.sort((a, b) => Number(new Date(b.updated_at)) - Number(new Date(a.updated_at)))
|
||||
if (page.value > pages.value) page.value = pages.value
|
||||
} catch {
|
||||
showError(t('load_tasks_failed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editingTask.value = null
|
||||
modalOpen.value = true
|
||||
}
|
||||
|
||||
function openEdit(task: Task) {
|
||||
editingTask.value = task
|
||||
modalOpen.value = true
|
||||
}
|
||||
|
||||
async function saveTask(payload: Partial<Task>) {
|
||||
try {
|
||||
if (editingTask.value?.id) {
|
||||
await api.updateTask(editingTask.value.id, payload)
|
||||
success(t('task_updated_success') || 'Task updated')
|
||||
} else {
|
||||
await api.createTask({ ...payload, status: 'todo' })
|
||||
success(t('task_created_success') || 'Task created')
|
||||
}
|
||||
modalOpen.value = false
|
||||
editingTask.value = null
|
||||
await loadTasks()
|
||||
} catch {
|
||||
showError(t('save_task_failed'))
|
||||
}
|
||||
}
|
||||
|
||||
async function toggle(task: Task) {
|
||||
const originalStatus = task.status
|
||||
const newStatus = task.status === 'done' ? 'todo' : 'done'
|
||||
|
||||
// Optimistic update
|
||||
task.status = newStatus
|
||||
|
||||
try {
|
||||
await api.updateTask(task.id, { status: newStatus })
|
||||
// Background refresh to ensure consistency
|
||||
// await loadTasks() // Optional: might cause jumping if list sort changes
|
||||
} catch {
|
||||
// Revert on failure
|
||||
task.status = originalStatus
|
||||
showError(t('update_task_failed'))
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(id: number) {
|
||||
if (!confirm(t('confirm_delete') || 'Are you sure?')) return
|
||||
|
||||
const originalTasks = [...tasks.value]
|
||||
tasks.value = tasks.value.filter(t => t.id !== id)
|
||||
|
||||
try {
|
||||
await api.deleteTask(id)
|
||||
success(t('task_deleted_success') || 'Task deleted')
|
||||
} catch {
|
||||
tasks.value = originalTasks
|
||||
showError(t('delete_task_failed'))
|
||||
}
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
if (page.value < pages.value) page.value += 1
|
||||
}
|
||||
|
||||
function prevPage() {
|
||||
if (page.value > 1) page.value -= 1
|
||||
}
|
||||
|
||||
function formatDue(value: string) {
|
||||
return value ? new Date(value).toLocaleString() : t('due_empty')
|
||||
}
|
||||
|
||||
function setFilter(f: 'all' | 'todo' | 'done') {
|
||||
filter.value = f
|
||||
page.value = 1
|
||||
}
|
||||
|
||||
onMounted(loadTasks)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page">
|
||||
<header class="page-head">
|
||||
<div>
|
||||
<h2>{{ t('todo_title') }}</h2>
|
||||
<p>{{ t('todo_subtitle') }}</p>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<div class="filter-group">
|
||||
<button
|
||||
class="btn sm"
|
||||
:class="{ primary: filter === 'all' }"
|
||||
@click="setFilter('all')"
|
||||
>All</button>
|
||||
<button
|
||||
class="btn sm"
|
||||
:class="{ primary: filter === 'todo' }"
|
||||
@click="setFilter('todo')"
|
||||
>Active</button>
|
||||
<button
|
||||
class="btn sm"
|
||||
:class="{ primary: filter === 'done' }"
|
||||
@click="setFilter('done')"
|
||||
>Done</button>
|
||||
</div>
|
||||
<button class="btn" @click="loadTasks">{{ t('refresh') }}</button>
|
||||
<button class="btn primary" @click="openCreate">{{ t('new_todo') }}</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="cards" v-if="!loading">
|
||||
<div v-if="pagedTasks.length === 0" class="empty-state">
|
||||
<p>No tasks found.</p>
|
||||
</div>
|
||||
<article v-for="task in pagedTasks" :key="task.id" class="task-row" :class="{ done: task.status === 'done' }">
|
||||
<div class="task-row-main">
|
||||
<input
|
||||
class="task-check"
|
||||
type="checkbox"
|
||||
:checked="task.status === 'done'"
|
||||
@change="toggle(task)"
|
||||
:aria-label="task.title"
|
||||
/>
|
||||
<div class="task-row-content">
|
||||
<h3>{{ task.title }}</h3>
|
||||
<p>{{ task.description || t('no_description') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-row-meta">
|
||||
<div class="task-meta">
|
||||
<small>{{ t('priority') }} P{{ task.priority || 2 }}</small>
|
||||
<small>{{ t('due_at') }} {{ formatDue(task.due_at) }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-row-actions">
|
||||
<button class="btn" @click="openEdit(task)">{{ t('edit') }}</button>
|
||||
<button class="btn danger" @click="remove(task.id)">{{ t('delete') }}</button>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<p v-else>{{ t('loading_tasks') }}</p>
|
||||
|
||||
<PaginationControl :page="page" :page-size="pageSize" :total="total" @next="nextPage" @prev="prevPage" />
|
||||
|
||||
<TodoEditorModal v-model="modalOpen" :editing-task="editingTask" @submit="saveTask" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.filter-group {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: #666;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 8px;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.btn.sm {
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user