- Implement ESLint with @antfu/eslint-config and apply consistent formatting across the codebase.

- Refactor `useTasks` to improve task fetching, creation, and updating logic.
- Enhance `TodoItem` and `ListScreen` with improved bindings, sorting, and category handling.
- Update dependencies and adjust task grouping in various components.
This commit is contained in:
2026-02-22 16:24:55 +01:00
parent 395129abb1
commit ec76a52fdd
8 changed files with 2863 additions and 149 deletions

6
eslint.config.js Normal file
View File

@@ -0,0 +1,6 @@
import antfu from '@antfu/eslint-config'
export default antfu({
formatters: true,
vue: true,
})

View File

@@ -37,7 +37,10 @@
"tailwindcss": "^4.2.0", "tailwindcss": "^4.2.0",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"vite": "^7.3.1", "vite": "^7.3.1",
"vue-tsc": "^2.1.10" "vue-tsc": "^2.1.10",
"@antfu/eslint-config": "^7.4.3",
"eslint": "^9.39.2",
"eslint-plugin-format": "^1.4.0"
}, },
"packageManager": "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a" "packageManager": "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a"
} }

2705
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { PhCheckSquareOffset, PhListChecks, PhSliders } from '@phosphor-icons/vue'; import { PhCheckSquareOffset, PhListChecks, PhSliders } from '@phosphor-icons/vue'
import { useRouter } from 'vue-router'; import { computed, onMounted } from 'vue'
import { computed } from 'vue'; import { useRouter } from 'vue-router'
const router = useRouter(); import { useTasks } from './composables/useTasks.ts'
const currentPath = computed(() => router.currentRoute.value.path);
const { fetchTasks } = useTasks()
const router = useRouter()
const currentPath = computed(() => router.currentRoute.value.path)
onMounted(fetchTasks)
</script> </script>
<template> <template>
<div class="overflow-hidden"> <div class="overflow-hidden">
<main class="pb-40 overflow-y-scroll h-screen"> <main class="pb-40 overflow-y-scroll h-screen">
<RouterView /> <RouterView />
</main> </main>
@@ -26,5 +29,4 @@ const currentPath = computed(() => router.currentRoute.value.path);
</RouterLink> </RouterLink>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -1,41 +1,46 @@
<script setup lang="ts"> <script setup lang="ts">
import { PhCheckSquare, PhDotsThree, PhFlag, PhPause, PhPlay, PhSquare, PhX } from '@phosphor-icons/vue'; import type { Task } from '../types.ts'
import { TaskStatus, Task } from '../types.ts'; import { PhCheckSquare, PhDotsThree, PhFlag, PhPause, PhPlay, PhSquare, PhX } from '@phosphor-icons/vue'
import { DateTime } from 'luxon'; import { DateTime } from 'luxon'
import { computed, ref } from 'vue'; import { computed, ref } from 'vue'
import { useTasks } from '../composables/useTasks.ts'; import { useTasks } from '../composables/useTasks.ts'
import { TaskStatus } from '../types.ts'
const {updateTask} = useTasks() const { task } = defineProps<{ task: Task }>()
const {task} = defineProps<{task: Task}>() const { updateTask } = useTasks()
const dueColor = computed(() => { const dueColor = computed(() => {
const dueDiff = task.dueDate ? DateTime.fromMillis(task.dueDate).diffNow('days').days : undefined; const dueDiff = task.dueDate ? DateTime.fromMillis(task.dueDate).diffNow('days').days : undefined
if (!dueDiff) return ''; if (!dueDiff)
return ''
if (dueDiff < 0) { if (dueDiff < 0) {
return 'text-error' return 'text-error'
} else if (dueDiff < 2) { }
return 'text-warning'; else if (dueDiff < 2) {
} else if (dueDiff < 7) { return 'text-warning'
return 'text-success'; }
} else { else if (dueDiff < 7) {
return 'text-neutral'; return 'text-success'
}
else {
return 'text-neutral'
} }
}) })
const statusSelectVisible = ref(false); const statusSelectVisible = ref(false)
const handleClick = async(update: Partial<Task>) => { async function handleClick(update: Partial<Task>) {
updateTask({...task, ...update}) updateTask({ ...task, ...update })
statusSelectVisible.value = false; statusSelectVisible.value = false
} }
</script> </script>
<template> <template>
<li class="list-row" > <li class="list-row">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<button class="btn btn-square btn-ghost" @click="statusSelectVisible = !statusSelectVisible"> <button class="btn btn-square btn-ghost" @click="statusSelectVisible = !statusSelectVisible">
<PhX :size="20" v-if="statusSelectVisible" /> <PhX v-if="statusSelectVisible" :size="20" />
<template v-else> <template v-else>
<PhSquare v-if="task.status === TaskStatus.WAIT" :size="20" /> <PhSquare v-if="task.status === TaskStatus.WAIT" :size="20" />
<PhCheckSquare v-else-if="task.status === TaskStatus.DONE" :size="20" weight="fill" class="text-success" /> <PhCheckSquare v-else-if="task.status === TaskStatus.DONE" :size="20" weight="fill" class="text-success" />
@@ -46,28 +51,30 @@ const handleClick = async(update: Partial<Task>) => {
<Transition> <Transition>
<div v-if="statusSelectVisible" class=""> <div v-if="statusSelectVisible" class="">
<template v-if="task.status !== TaskStatus.WIP"> <template v-if="task.status !== TaskStatus.WIP">
<button v-if="task.status !== TaskStatus.DONE" class="btn btn-square btn-ghost" @click="handleClick({status: TaskStatus.DONE})"> <button v-if="task.status !== TaskStatus.DONE" class="btn btn-square btn-ghost" @click="handleClick({ status: TaskStatus.DONE })">
<PhCheckSquare :size="24" weight="regular" class="text-success" /> <PhCheckSquare :size="24" weight="regular" class="text-success" />
</button> </button>
<button v-if="task.status !== TaskStatus.WAIT" class="btn btn-square btn-ghost" @click="handleClick({status: TaskStatus.WAIT})"> <button v-if="task.status !== TaskStatus.WAIT" class="btn btn-square btn-ghost" @click="handleClick({ status: TaskStatus.WAIT })">
<PhSquare :size="24" weight="regular" /> <PhSquare :size="24" weight="regular" />
</button> </button>
<button v-if="task.status !== TaskStatus.FLAG" class="btn btn-square btn-ghost" @click="handleClick({status: TaskStatus.FLAG})"> <button v-if="task.status !== TaskStatus.FLAG" class="btn btn-square btn-ghost" @click="handleClick({ status: TaskStatus.FLAG })">
<PhFlag :size="24" weight="fill" class="text-warning" /> <PhFlag :size="24" weight="fill" class="text-warning" />
</button> </button>
<button class="btn btn-square btn-ghost" @click="handleClick({status: TaskStatus.WIP})"> <button class="btn btn-square btn-ghost" @click="handleClick({ status: TaskStatus.WIP })">
<PhPlay :size="24" weight="fill" class="text-info" /> <PhPlay :size="24" weight="fill" class="text-info" />
</button> </button>
</template> </template>
<button v-else class="btn btn-square btn-ghost" @click="handleClick({status: TaskStatus.WAIT})"> <button v-else class="btn btn-square btn-ghost" @click="handleClick({ status: TaskStatus.WAIT })">
<PhPause :size="24" weight="fill" class="text-info" /> <PhPause :size="24" weight="fill" class="text-info" />
</button> </button>
</div> </div>
</Transition> </Transition>
</div> </div>
<div class="flex flex-col justify-center"> <div class="flex flex-col justify-center">
<div>{{task.title}}</div> <div>{{ task.id_ }} {{ task.title }}</div>
<div :class="dueColor" v-if="task.dueDate">{{DateTime.fromMillis(task.dueDate).toFormat('dd/MM/yyyy')}}</div> <div v-if="task.dueDate" :class="dueColor">
{{ DateTime.fromMillis(task.dueDate).toFormat('dd/MM/yyyy') }}
</div>
</div> </div>
<button class="btn btn-square btn-ghost"> <button class="btn btn-square btn-ghost">
<PhDotsThree :size="24" weight="regular" /> <PhDotsThree :size="24" weight="regular" />

View File

@@ -1,89 +1,88 @@
import { ref } from 'vue'; import type { Task } from '../types.ts'
import { useApi } from './useApi.ts'; import { useArrayUnique } from '@vueuse/core'
import { Task } from '../types.ts'; import { ref } from 'vue'
import { useArrayReduce, useArrayUnique } from '@vueuse/core' import { useApi } from './useApi.ts'
const tasks = ref<Task[]>([]); const tasks = ref<Task[]>([])
const isLoading = ref(false); const isLoading = ref(false)
const error = ref<string | null>(null); const error = ref<string | null>(null)
export function useTasks() { export function useTasks() {
const api = useApi(); const api = useApi()
const fetchTasks = async (force = false) => { const fetchTasks = async (force = false) => {
if (tasks.value.length > 0 && !force) return; if (tasks.value.length > 0 && !force)
return
isLoading.value = true; isLoading.value = true
error.value = null; error.value = null
try { try {
const data = await api.get('e5880167-9322-4d7b-8a38-e06bae8a7734/list').then((res) => res.json()); const data = await api.get('e5880167-9322-4d7b-8a38-e06bae8a7734/list').then(res => res.json())
tasks.value = data.tasks ?? []; tasks.value = data.tasks ?? []
} catch (e: any) { }
error.value = e.message || 'Failed to fetch tasks'; catch (e: any) {
console.error('Error fetching tasks:', e); error.value = e.message || 'Failed to fetch tasks'
} finally { console.error('Error fetching tasks:', e)
isLoading.value = false; }
finally {
isLoading.value = false
}
} }
};
const createTask = async (taskData: Partial<Task>) => { const createTask = async (taskData: Partial<Task>) => {
isLoading.value = true; isLoading.value = true
error.value = null; error.value = null
try { try {
// Get next ID as per current logic in CreateScreen.vue // Get next ID as per current logic in CreateScreen.vue
const nextId = await api.get('d49dde4c-530d-46ee-8205-d1357563ac16') const nextId = tasks.value.sort((a, b) => b.id_ - a.id_).reduce((acc, task) => {
.then((response) => response.json()) if (task.id_ === acc + 1)
.then((json) => json.nextId as number) return acc + 1
.catch(() => null); return task.id_ + 1
}, 0)
if (!nextId) throw new Error('Could not get next task ID');
const newTask: Partial<Task> = { const newTask: Partial<Task> = {
...taskData, ...taskData,
id_: nextId, id_: nextId,
logs: [], logs: [],
archived: false, archived: false,
}; }
const data = await api.put('e5880167-9322-4d7b-8a38-e06bae8a7734/list', { tasks: [newTask] }).then((res) => res.json()); const data = await api.put('e5880167-9322-4d7b-8a38-e06bae8a7734/list', { tasks: [newTask] }).then(res => res.json())
if (data.tasks) { if (data.tasks) {
tasks.value = data.tasks; tasks.value = data.tasks
}
}
catch (e: any) {
error.value = e.message || 'Failed to create task'
console.error('Error creating task:', e)
throw e
}
finally {
isLoading.value = false
} }
} catch (e: any) {
error.value = e.message || 'Failed to create task';
console.error('Error creating task:', e);
throw e;
} finally {
isLoading.value = false;
} }
};
const updateTask = async (task: Task) => { const updateTask = async (task: Task) => {
console.log('updateTask',task); console.log('updateTask', task)
isLoading.value = true; isLoading.value = true
error.value = null; error.value = null
try { try {
const data = await api.put('e5880167-9322-4d7b-8a38-e06bae8a7734/list', { tasks: [task] }).then((res) => res.json()); const data = await api.put('e5880167-9322-4d7b-8a38-e06bae8a7734/list', { tasks: [task] }).then(res => res.json())
if (data.tasks) { if (data.tasks) {
tasks.value = data.tasks; tasks.value = data.tasks
} }
} catch (e: any) { }
error.value = e.message || 'Failed to update task'; catch (e: any) {
console.error('Error updating task:', e); error.value = e.message || 'Failed to update task'
throw e; console.error('Error updating task:', e)
} finally { throw e
isLoading.value = false; }
finally {
isLoading.value = false
} }
} }
const tasksByCategory = useArrayReduce(tasks.value.sort((a,b) => a.id_ - b.id_), (acc, task) => { const categories = useArrayUnique(tasks.value.map(task => task.tag))
const tag = task.tag ?? 'Uncategorized';
acc[tag] = acc[tag] ?? [];
acc[tag].push(task);
return acc;
}, {} as Record<string, Task[]>)
const categories = useArrayUnique(Object.keys(tasksByCategory.value))
return { return {
tasks, tasks,
@@ -93,5 +92,5 @@ export function useTasks() {
createTask, createTask,
updateTask, updateTask,
categories, categories,
}; }
} }

View File

@@ -1,16 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { router } from '../router.ts'; import type { Task } from '../types.ts'
import { useTasks } from '../composables/useTasks.ts'; import { useTasks } from '../composables/useTasks.ts'
import { Task } from '../types.ts'; import { router } from '../router.ts'
const { createTask } = useTasks(); const { createTask, tasks } = useTasks()
const handleSubmit = async(e: Event) => { async function handleSubmit(e: Event) {
const data = new FormData(e.target as HTMLFormElement); const data = new FormData(e.target as HTMLFormElement)
const task: Partial<Task> = Object.fromEntries(data) const task: Partial<Task> = Object.fromEntries(data)
await createTask(task); await createTask(task)
await router.push('/'); await router.push('/')
} }
</script> </script>
@@ -18,10 +18,14 @@ const handleSubmit = async(e: Event) => {
<div> <div>
<form @submit.prevent="handleSubmit"> <form @submit.prevent="handleSubmit">
<fieldset class="fieldset"> <fieldset class="fieldset">
<legend class="fieldset-legend">What is your name?</legend> <legend class="fieldset-legend">
<input type="text" class="input" name="title" placeholder="Type here" /> What is your name?
</legend>
<input type="text" class="input" name="title" placeholder="Type here">
</fieldset> </fieldset>
<button class="btn btn-primary">Submit</button> <button class="btn btn-primary">
Submit
</button>
</form> </form>
</div> </div>
</template> </template>

View File

@@ -1,49 +1,53 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue'; import type { Task } from '../types.ts'
import { useTasks } from '../composables/useTasks.ts'; import { PhCaretDown, PhCaretUp } from '@phosphor-icons/vue'
import { Task } from '../types.ts'; import { computed, onMounted, ref } from 'vue'
import { PhCaretDown, PhCaretUp } from '@phosphor-icons/vue'; import TodoItem from '../components/TodoItem.vue'
import TodoItem from '../components/TodoItem.vue'; import { useTasks } from '../composables/useTasks.ts'
const { tasks, fetchTasks } = useTasks(); const { tasks, fetchTasks } = useTasks()
onMounted(async () => { onMounted(async () => {
await fetchTasks(); await fetchTasks()
}) })
const visibleTasks = computed<Task[]>(() => tasks.value.filter(task => !task.archived)) const visibleTasks = computed<Task[]>(() => tasks.value.filter(task => !task.archived).sort((a, b) => a.id_ - b.id_))
const categorizedTasks = computed(() => visibleTasks.value.reduce()) const categorizedTasks = computed(() => visibleTasks.value.reduce((acc, task) => {
const tag = task.tag ?? '@uncategorized'
const collapsed = ref<string[]>([]); acc[tag] = acc[tag] ?? []
acc[tag].push(task)
return acc
}, {} as Record<string, Task[]>))
const collapsed = ref<string[]>([])
</script> </script>
<template> <template>
<div> <div>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="m-4 rounded-box border border-neutral-100 shadow-md" v-for="(tasks, category) in categorizedTasks" :key="category"> <div v-for="(tasks, category) in categorizedTasks" :key="category" class="m-4 rounded-box border border-neutral-100 shadow-md">
<div class="m-4 flex justify-between items-center"> <div class="m-4 flex justify-between items-center">
<div class="badge badge-xl badge-primary">{{ category }}</div> <div class="badge badge-xl badge-primary">
{{ category }}
</div>
<button <button
@click="collapsed.includes(category) ? collapsed.splice(collapsed.indexOf(category), 1) : collapsed.push(category)"
class="btn btn-square btn-sm" class="btn btn-square btn-sm"
@click="collapsed.includes(category) ? collapsed.splice(collapsed.indexOf(category), 1) : collapsed.push(category)"
> >
<PhCaretDown :size="20" v-if="collapsed.includes(category)" /> <PhCaretDown v-if="collapsed.includes(category)" :size="20" />
<PhCaretUp :size="20" v-else /> <PhCaretUp v-else :size="20" />
</button> </button>
</div> </div>
<Transition name="fade"> <Transition name="fade">
<ul v-if="!collapsed.includes(category)" class="list bg-base-100 rounded-box" > <ul v-if="!collapsed.includes(category)" class="list bg-base-100 rounded-box">
<TodoItem v-for="task in tasks.sort((a, b) => a.id_ - b.id_)" :key="task.id" :task /> <TodoItem v-for="task in tasks.sort((a, b) => a.id_ - b.id_)" :key="task.id" :task />
</ul> </ul>
</Transition> </Transition>
</div> </div>
</div> </div>
</div>
</div>
</template> </template>
<style scoped> <style scoped>