Add TodoItem component, enhance task list, and improve API data handling
- Introduced `TodoItem.vue`, a reusable component for task items. - Refactored `ListScreen` to use `TodoItem` for better modularity. - Added new animations and styles for smooth transitions. - Updated `useTasks` with `updateTask` method to sync task updates via API. - Improved type definitions for `Task` and added nullable fields for flexibility. - Added dependencies: `luxon`, `@types/luxon`, `uuid`, and `@vueuse/core`.
This commit is contained in:
@@ -21,8 +21,12 @@
|
|||||||
"@tauri-apps/plugin-http": "~2.5.7",
|
"@tauri-apps/plugin-http": "~2.5.7",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"@tauri-apps/plugin-store": "~2.4.2",
|
"@tauri-apps/plugin-store": "~2.4.2",
|
||||||
|
"@types/luxon": "^3.7.1",
|
||||||
|
"@vueuse/core": "^14.2.1",
|
||||||
"daisyui": "^5.5.18",
|
"daisyui": "^5.5.18",
|
||||||
"jsencrypt": "^3.5.4",
|
"jsencrypt": "^3.5.4",
|
||||||
|
"luxon": "^3.7.2",
|
||||||
|
"uuid": "^13.0.0",
|
||||||
"vue": "^3.5.28",
|
"vue": "^3.5.28",
|
||||||
"vue-router": "^4.6.4"
|
"vue-router": "^4.6.4"
|
||||||
},
|
},
|
||||||
|
|||||||
60
pnpm-lock.yaml
generated
60
pnpm-lock.yaml
generated
@@ -23,12 +23,24 @@ importers:
|
|||||||
'@tauri-apps/plugin-store':
|
'@tauri-apps/plugin-store':
|
||||||
specifier: ~2.4.2
|
specifier: ~2.4.2
|
||||||
version: 2.4.2
|
version: 2.4.2
|
||||||
|
'@types/luxon':
|
||||||
|
specifier: ^3.7.1
|
||||||
|
version: 3.7.1
|
||||||
|
'@vueuse/core':
|
||||||
|
specifier: ^14.2.1
|
||||||
|
version: 14.2.1(vue@3.5.28(typescript@5.9.3))
|
||||||
daisyui:
|
daisyui:
|
||||||
specifier: ^5.5.18
|
specifier: ^5.5.18
|
||||||
version: 5.5.18
|
version: 5.5.18
|
||||||
jsencrypt:
|
jsencrypt:
|
||||||
specifier: ^3.5.4
|
specifier: ^3.5.4
|
||||||
version: 3.5.4
|
version: 3.5.4
|
||||||
|
luxon:
|
||||||
|
specifier: ^3.7.2
|
||||||
|
version: 3.7.2
|
||||||
|
uuid:
|
||||||
|
specifier: ^13.0.0
|
||||||
|
version: 13.0.0
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.5.28
|
specifier: ^3.5.28
|
||||||
version: 3.5.28(typescript@5.9.3)
|
version: 3.5.28(typescript@5.9.3)
|
||||||
@@ -615,6 +627,12 @@ packages:
|
|||||||
'@types/estree@1.0.8':
|
'@types/estree@1.0.8':
|
||||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||||
|
|
||||||
|
'@types/luxon@3.7.1':
|
||||||
|
resolution: {integrity: sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==}
|
||||||
|
|
||||||
|
'@types/web-bluetooth@0.0.21':
|
||||||
|
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
|
||||||
|
|
||||||
'@vitejs/plugin-vue@6.0.4':
|
'@vitejs/plugin-vue@6.0.4':
|
||||||
resolution: {integrity: sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ==}
|
resolution: {integrity: sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -674,6 +692,19 @@ packages:
|
|||||||
'@vue/shared@3.5.28':
|
'@vue/shared@3.5.28':
|
||||||
resolution: {integrity: sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==}
|
resolution: {integrity: sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==}
|
||||||
|
|
||||||
|
'@vueuse/core@14.2.1':
|
||||||
|
resolution: {integrity: sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.5.0
|
||||||
|
|
||||||
|
'@vueuse/metadata@14.2.1':
|
||||||
|
resolution: {integrity: sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==}
|
||||||
|
|
||||||
|
'@vueuse/shared@14.2.1':
|
||||||
|
resolution: {integrity: sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.5.0
|
||||||
|
|
||||||
alien-signals@1.0.13:
|
alien-signals@1.0.13:
|
||||||
resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==}
|
resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==}
|
||||||
|
|
||||||
@@ -810,6 +841,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==}
|
resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
|
luxon@3.7.2:
|
||||||
|
resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
@@ -864,6 +899,10 @@ packages:
|
|||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
uuid@13.0.0:
|
||||||
|
resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
vite@7.3.1:
|
vite@7.3.1:
|
||||||
resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
|
resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -1285,6 +1324,10 @@ snapshots:
|
|||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
|
|
||||||
|
'@types/luxon@3.7.1': {}
|
||||||
|
|
||||||
|
'@types/web-bluetooth@0.0.21': {}
|
||||||
|
|
||||||
'@vitejs/plugin-vue@6.0.4(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1))(vue@3.5.28(typescript@5.9.3))':
|
'@vitejs/plugin-vue@6.0.4(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1))(vue@3.5.28(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rolldown/pluginutils': 1.0.0-rc.2
|
'@rolldown/pluginutils': 1.0.0-rc.2
|
||||||
@@ -1377,6 +1420,19 @@ snapshots:
|
|||||||
|
|
||||||
'@vue/shared@3.5.28': {}
|
'@vue/shared@3.5.28': {}
|
||||||
|
|
||||||
|
'@vueuse/core@14.2.1(vue@3.5.28(typescript@5.9.3))':
|
||||||
|
dependencies:
|
||||||
|
'@types/web-bluetooth': 0.0.21
|
||||||
|
'@vueuse/metadata': 14.2.1
|
||||||
|
'@vueuse/shared': 14.2.1(vue@3.5.28(typescript@5.9.3))
|
||||||
|
vue: 3.5.28(typescript@5.9.3)
|
||||||
|
|
||||||
|
'@vueuse/metadata@14.2.1': {}
|
||||||
|
|
||||||
|
'@vueuse/shared@14.2.1(vue@3.5.28(typescript@5.9.3))':
|
||||||
|
dependencies:
|
||||||
|
vue: 3.5.28(typescript@5.9.3)
|
||||||
|
|
||||||
alien-signals@1.0.13: {}
|
alien-signals@1.0.13: {}
|
||||||
|
|
||||||
balanced-match@1.0.2: {}
|
balanced-match@1.0.2: {}
|
||||||
@@ -1495,6 +1551,8 @@ snapshots:
|
|||||||
lightningcss-win32-arm64-msvc: 1.31.1
|
lightningcss-win32-arm64-msvc: 1.31.1
|
||||||
lightningcss-win32-x64-msvc: 1.31.1
|
lightningcss-win32-x64-msvc: 1.31.1
|
||||||
|
|
||||||
|
luxon@3.7.2: {}
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
@@ -1563,6 +1621,8 @@ snapshots:
|
|||||||
|
|
||||||
typescript@5.9.3: {}
|
typescript@5.9.3: {}
|
||||||
|
|
||||||
|
uuid@13.0.0: {}
|
||||||
|
|
||||||
vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1):
|
vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.27.3
|
esbuild: 0.27.3
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const currentPath = computed(() => router.currentRoute.value.path);
|
|||||||
<main class="pb-40 overflow-y-scroll h-screen">
|
<main class="pb-40 overflow-y-scroll h-screen">
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</main>
|
</main>
|
||||||
<div class="dock dock-xl bg-neutral-400">
|
<div class="dock dock-xl inset-shadow-sm">
|
||||||
<RouterLink to="/create" :class="currentPath === '/create' ? 'dock-active' : ''">
|
<RouterLink to="/create" :class="currentPath === '/create' ? 'dock-active' : ''">
|
||||||
<PhCheckSquareOffset :size="32" />
|
<PhCheckSquareOffset :size="32" />
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|||||||
80
src/components/TodoItem.vue
Normal file
80
src/components/TodoItem.vue
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { PhCheckSquare, PhDotsThree, PhFlag, PhPause, PhPlay, PhSquare, PhX } from '@phosphor-icons/vue';
|
||||||
|
import { TaskStatus, Task } from '../types.ts';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useTasks } from '../composables/useTasks.ts';
|
||||||
|
|
||||||
|
const {updateTask} = useTasks()
|
||||||
|
|
||||||
|
const {task} = defineProps<{task: Task}>()
|
||||||
|
|
||||||
|
const dueColor = computed(() => {
|
||||||
|
const dueDiff = task.dueDate ? DateTime.fromMillis(task.dueDate).diffNow('days').days : undefined;
|
||||||
|
if (!dueDiff) return '';
|
||||||
|
if (dueDiff < 0) {
|
||||||
|
return 'text-error'
|
||||||
|
} else if (dueDiff < 2) {
|
||||||
|
return 'text-warning';
|
||||||
|
} else if (dueDiff < 7) {
|
||||||
|
return 'text-success';
|
||||||
|
} else {
|
||||||
|
return 'text-neutral';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusSelectVisible = ref(false);
|
||||||
|
|
||||||
|
const handleClick = async(update: Partial<Task>) => {
|
||||||
|
updateTask({...task, ...update})
|
||||||
|
statusSelectVisible.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<li class="list-row" >
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<button class="btn btn-square btn-ghost" @click="statusSelectVisible = !statusSelectVisible">
|
||||||
|
<PhX :size="20" v-if="statusSelectVisible" />
|
||||||
|
<template v-else>
|
||||||
|
<PhSquare v-if="task.status === TaskStatus.WAIT" :size="20" />
|
||||||
|
<PhCheckSquare v-else-if="task.status === TaskStatus.DONE" :size="20" weight="fill" class="text-success" />
|
||||||
|
<PhFlag v-else-if="task.status === TaskStatus.FLAG" :size="20" weight="fill" class="text-warning" />
|
||||||
|
<PhPlay v-else-if="task.status === TaskStatus.WIP" :size="20" weight="fill" class="text-info" />
|
||||||
|
</template>
|
||||||
|
</button>
|
||||||
|
<Transition>
|
||||||
|
<div v-if="statusSelectVisible" class="">
|
||||||
|
<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})">
|
||||||
|
<PhCheckSquare :size="24" weight="regular" class="text-success" />
|
||||||
|
</button>
|
||||||
|
<button v-if="task.status !== TaskStatus.WAIT" class="btn btn-square btn-ghost" @click="handleClick({status: TaskStatus.WAIT})">
|
||||||
|
<PhSquare :size="24" weight="regular" />
|
||||||
|
</button>
|
||||||
|
<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" />
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-square btn-ghost" @click="handleClick({status: TaskStatus.WIP})">
|
||||||
|
<PhPlay :size="24" weight="fill" class="text-info" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<button v-else class="btn btn-square btn-ghost" @click="handleClick({status: TaskStatus.WAIT})">
|
||||||
|
<PhPause :size="24" weight="fill" class="text-info" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col justify-center">
|
||||||
|
<div>{{task.title}}</div>
|
||||||
|
<div :class="dueColor" v-if="task.dueDate">{{DateTime.fromMillis(task.dueDate).toFormat('dd/MM/yyyy')}}</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-square btn-ghost">
|
||||||
|
<PhDotsThree :size="24" weight="regular" />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -51,10 +51,10 @@ export function useApi() {
|
|||||||
const get = (endpoint: string, options: RequestInit = {}) =>
|
const get = (endpoint: string, options: RequestInit = {}) =>
|
||||||
apiFetch(endpoint, { ...options, method: 'GET' });
|
apiFetch(endpoint, { ...options, method: 'GET' });
|
||||||
|
|
||||||
const post = (endpoint: string, body: BodyInit | null, options: RequestInit = {}) =>
|
const post = (endpoint: string, body: unknown, options: RequestInit = {}) =>
|
||||||
apiFetch(endpoint, { ...options, method: 'POST', body: JSON.stringify(body) });
|
apiFetch(endpoint, { ...options, method: 'POST', body: JSON.stringify(body) });
|
||||||
|
|
||||||
const put = (endpoint: string, body: BodyInit | null, options: RequestInit = {}) =>
|
const put = (endpoint: string, body: unknown, options: RequestInit = {}) =>
|
||||||
apiFetch(endpoint, { ...options, method: 'PUT', body: JSON.stringify(body) });
|
apiFetch(endpoint, { ...options, method: 'PUT', body: JSON.stringify(body) });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function useSettings() {
|
|||||||
let password = readSettings.password ?? '';
|
let password = readSettings.password ?? '';
|
||||||
if (password) {
|
if (password) {
|
||||||
try {
|
try {
|
||||||
password = decrypt(password);
|
password = decrypt(password) as string;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to decrypt stored password:', error);
|
console.warn('Failed to decrypt stored password:', error);
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@ export function useSettings() {
|
|||||||
|
|
||||||
const saveSettings = async () => {
|
const saveSettings = async () => {
|
||||||
const encryptedPassword = settings.value.password
|
const encryptedPassword = settings.value.password
|
||||||
? encrypt(settings.value.password)
|
? encrypt(settings.value.password) as string
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
await setValue<Settings>('settings', {
|
await setValue<Settings>('settings', {
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ export function useTasks() {
|
|||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
try {
|
try {
|
||||||
const response = await api.get('e5880167-9322-4d7b-8a38-e06bae8a7734/list');
|
const data = await api.get('e5880167-9322-4d7b-8a38-e06bae8a7734/list').then((res) => res.json());
|
||||||
const data = await response.json();
|
|
||||||
tasks.value = data.tasks ?? [];
|
tasks.value = data.tasks ?? [];
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error.value = e.message || 'Failed to fetch tasks';
|
error.value = e.message || 'Failed to fetch tasks';
|
||||||
@@ -45,15 +44,10 @@ export function useTasks() {
|
|||||||
archived: false,
|
archived: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
await api.put('e5880167-9322-4d7b-8a38-e06bae8a7734/list', { tasks: [newTask] });
|
const data = await api.put('e5880167-9322-4d7b-8a38-e06bae8a7734/list', { tasks: [newTask] }).then((res) => res.json());
|
||||||
|
if (data.tasks) {
|
||||||
// Update local store (optimistic update or refetch)
|
tasks.value = data.tasks;
|
||||||
// Since it's a PUT to '.../list' with {tasks: [task]}, it seems to add/update?
|
}
|
||||||
// Based on CreateScreen.vue, it just navigates back.
|
|
||||||
// To keep store in sync without full refetch, we could add it locally if we knew the full structure
|
|
||||||
// But maybe it's safer to refetch or at least push to local state if we have the full object.
|
|
||||||
// Let's refetch to be sure it's in sync with server.
|
|
||||||
await fetchTasks(true);
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error.value = e.message || 'Failed to create task';
|
error.value = e.message || 'Failed to create task';
|
||||||
console.error('Error creating task:', e);
|
console.error('Error creating task:', e);
|
||||||
@@ -63,11 +57,30 @@ export function useTasks() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateTask = async (task: Task) => {
|
||||||
|
console.log('updateTask',task);
|
||||||
|
isLoading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
try {
|
||||||
|
const data = await api.put('e5880167-9322-4d7b-8a38-e06bae8a7734/list', { tasks: [task] }).then((res) => res.json());
|
||||||
|
if (data.tasks) {
|
||||||
|
tasks.value = data.tasks;
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
error.value = e.message || 'Failed to update task';
|
||||||
|
console.error('Error updating task:', e);
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tasks,
|
tasks,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
fetchTasks,
|
fetchTasks,
|
||||||
createTask,
|
createTask,
|
||||||
|
updateTask,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { useTasks } from '../composables/useTasks.ts';
|
import { useTasks } from '../composables/useTasks.ts';
|
||||||
import { TaskStatus } from '../types.ts';
|
|
||||||
import { PhCaretDown, PhCaretUp, PhCheckSquare, PhDotsThree, PhPlay, PhSquare } from '@phosphor-icons/vue';
|
import { Task } from '../types.ts';
|
||||||
|
import { PhCaretDown, PhCaretUp } from '@phosphor-icons/vue';
|
||||||
|
import TodoItem from '../components/TodoItem.vue';
|
||||||
|
|
||||||
const { tasks, fetchTasks } = useTasks();
|
const { tasks, fetchTasks } = useTasks();
|
||||||
|
|
||||||
@@ -10,7 +12,7 @@ onMounted(async () => {
|
|||||||
await fetchTasks();
|
await fetchTasks();
|
||||||
})
|
})
|
||||||
|
|
||||||
const visibleTasks = computed(() => tasks.value.filter(task => !task.archived))
|
const visibleTasks = computed<Task[]>(() => tasks.value.filter(task => !task.archived))
|
||||||
|
|
||||||
const categorizedTasks = computed(() => visibleTasks.value.reduce((acc, task) => {
|
const categorizedTasks = computed(() => visibleTasks.value.reduce((acc, task) => {
|
||||||
const tag = task.tag ?? 'Uncategorized';
|
const tag = task.tag ?? 'Uncategorized';
|
||||||
@@ -39,23 +41,7 @@ const collapsed = ref<string[]>([]);
|
|||||||
</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" >
|
||||||
<li class="list-row" v-for="task in tasks" :key="task.id">
|
<TodoItem v-for="task in tasks.sort((a, b) => a.id_ - b.id_)" :key="task.id" :task />
|
||||||
<div class="flex items-center justify-center">
|
|
||||||
<PhSquare v-if="task.status === TaskStatus.WAIT" :size="20" />
|
|
||||||
<PhCheckSquare v-else-if="task.status === TaskStatus.DONE" :size="20" weight="fill" class="text-success" />
|
|
||||||
<PhSquare v-else-if="task.status === TaskStatus.FLAG" :size="20" weight="fill" class="text-warning" />
|
|
||||||
<PhPlay v-else-if="task.status === TaskStatus.WIP" :size="20" weight="fill" class="text-info" />
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div>{{task.title}}</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-square btn-ghost btn-sm">
|
|
||||||
<PhDotsThree :size="24" weight="regular" />
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -43,8 +43,16 @@
|
|||||||
.fade-leave-active {
|
.fade-leave-active {
|
||||||
transition: opacity 0.25s ease;
|
transition: opacity 0.25s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-from,
|
.fade-enter-from,
|
||||||
.fade-leave-to {
|
.fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.xExpand-enter-active,
|
||||||
|
.xExpand-leave-active {
|
||||||
|
transition: all 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
.xExpand-enter-from,
|
||||||
|
.xExpand-leave-to {
|
||||||
|
transform: scaleX(0);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
export enum TaskStatus {
|
export enum TaskStatus {
|
||||||
NONE,
|
NONE,
|
||||||
DONE,
|
DONE,
|
||||||
@@ -6,6 +7,7 @@ export enum TaskStatus {
|
|||||||
FLAG,
|
FLAG,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export type Worklog = {
|
export type Worklog = {
|
||||||
start: number;
|
start: number;
|
||||||
end: number;
|
end: number;
|
||||||
@@ -17,9 +19,9 @@ export type Task = {
|
|||||||
"tag": string,
|
"tag": string,
|
||||||
"title": string,
|
"title": string,
|
||||||
"status": TaskStatus,
|
"status": TaskStatus,
|
||||||
"lastaction": number,
|
"lastaction": number | null,
|
||||||
"logs": Worklog[],
|
"logs": Worklog[],
|
||||||
"dueDate": number,
|
"dueDate": number | null,
|
||||||
"id_": number,
|
"id_": number,
|
||||||
"id": number
|
"id": number
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user