Refactor task management, UI interactions, and theme support
- Added task editing functionality with `EditForm` and integrated it with `TodoItemTouch`. - Switched `dueDate` fields to `due_date` for consistency with API. - Updated `useTasks` to handle dynamic task updates, deletions, and periodic refresh. - Enhanced `useApi` with a dedicated delete method. - Improved UI responsiveness and styling with multi-theme support in DaisyUI. - Simplified modal handling for task interactions (`EditForm`, `CreateForm`). - Improved `useSettings` with a `theme` field to support theme switching. - Extended task commands for tagging, text, and due date updates. - Optimized `parser` for extended edit command parsing. - Updated helpers and actions to use `luxon` for date manipulations. - Streamlined task and view logic for improved usability and extensibility.
This commit is contained in:
@@ -120,18 +120,18 @@ watchEffect(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="flex bottom-0 right-0 left-0 top-0 justify-center items-center flex-col gap-6 transition-opacity duration-500 bg-primary/10 " :class=" inputActive ? 'fixed' : 'opacity-0' " @submit.prevent="handleSubmit">
|
||||
<form class="flex bottom-0 right-0 left-0 top-0 justify-center items-center flex-col gap-6 transition-opacity duration-500 bg-primary/30 " :class=" inputActive ? 'fixed' : 'opacity-0' " @submit.prevent="handleSubmit">
|
||||
<div class="font-mono w-10/12 max-w-md relative h-10">
|
||||
<div class="absolute top-0 left-0 right-0 bottom-0 bg-white px-4 py-2">
|
||||
<div class="absolute top-0 left-0 right-0 bottom-0 bg-neutral text-neutral-content px-4 py-2">
|
||||
{{ suggestion }}
|
||||
</div>
|
||||
<input
|
||||
ref="inputComponent"
|
||||
v-model="value" type="text"
|
||||
class="absolute top-0 left-0 right-0 bottom-0 bg-white/40 px-4 py-2 rounded-lg shadow-2xl focus:outline-none focus:ring-2 focus:ring-primary h-full w-full"
|
||||
class="absolute top-0 left-0 right-0 bottom-0 bg-neutral/70 text-neutral-content px-4 py-2 shadow-2xl focus:outline-none focus:ring-2 focus:ring-primary h-full w-full"
|
||||
>
|
||||
</div>
|
||||
<div v-if="showHelp" class="bg-white/70 backdrop-blur-xs p-6 rounded-lg shadow-xl text-center">
|
||||
<div v-if="showHelp" class="bg-neutral/70 backdrop-blur-xs p-6 text-neutral-content shadow-xl text-center">
|
||||
<HelpPanel />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -27,23 +27,23 @@ function showModal(component: ModalShown) {
|
||||
<div>
|
||||
<div class="fab select-none">
|
||||
<!-- a focusable div with tabindex is necessary to work on all browsers. role="button" is necessary for accessibility -->
|
||||
<div tabindex="0" role="button" class="btn btn-xl btn-circle bg-white border border-black border-2">
|
||||
<div tabindex="0" role="button" class="btn btn-xl btn-circle btn-neutral btn-outline">
|
||||
<PhDotsNine :size="30" weight="bold" />
|
||||
</div>
|
||||
|
||||
<!-- close button should not be focusable so it can close the FAB when clicked. It's just a visual placeholder -->
|
||||
<div class="fab-close">
|
||||
<span class="btn btn-xl btn-circle bg-white border border-black border-2"><PhX size="30" weight="bold" /></span>
|
||||
<span class="btn btn-xl btn-circle btn-neutral btn-outline"><PhX size="30" weight="bold" /></span>
|
||||
</div>
|
||||
|
||||
<!-- buttons that show up when FAB is open -->
|
||||
<div>
|
||||
<button class="btn btn-xl btn-circle bg-white border border-black border-2" @click="showModal('create')">
|
||||
<button class="btn btn-xl btn-circle btn-neutral btn-outline" @click="showModal('create')">
|
||||
<PhPlus size="30" weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-xl btn-circle bg-white border border-black border-2" @click="showModal('settings')">
|
||||
<button class="btn btn-xl btn-circle btn-neutral btn-outline" @click="showModal('settings')">
|
||||
<PhSliders :size="30" weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -53,11 +53,6 @@ function showModal(component: ModalShown) {
|
||||
<button>close</button>
|
||||
</form>
|
||||
<div class="modal-box">
|
||||
<form method="dialog">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">
|
||||
<PhX size="24" />
|
||||
</button>
|
||||
</form>
|
||||
<component :is="componentMap[modalShown]" v-if="modalShown" />
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { TaskStatus } from '../types.ts'
|
||||
const { task } = defineProps<{ task: Task }>()
|
||||
|
||||
const dueColor = computed(() => {
|
||||
const dueDiff = task.dueDate ? DateTime.fromMillis(task.dueDate).diffNow('days').days : undefined
|
||||
const dueDiff = task.due_date ? DateTime.fromISO(task.due_date).diffNow('days').days : undefined
|
||||
if (!dueDiff)
|
||||
return ''
|
||||
if (dueDiff < 0) {
|
||||
@@ -37,8 +37,8 @@ const dueColor = computed(() => {
|
||||
<PhPlay v-else-if="task.status === TaskStatus.WIP" :size="16" weight="fill" class="text-info" />
|
||||
</div>
|
||||
<span>{{ task.title }}</span>
|
||||
<span v-if="task.dueDate" :class="dueColor">
|
||||
{{ DateTime.fromMillis(task.dueDate).toFormat('dd/MM/yyyy') }}
|
||||
<span v-if="task.due_date" :class="dueColor">
|
||||
{{ DateTime.fromISO(task.due_date).toFormat('dd/MM/yyyy') }}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import type { UseSwipeDirection } from '@vueuse/core'
|
||||
import type { Task } from '../types.ts'
|
||||
import { PhCheckSquare, PhClockCountdown, PhFlag, PhPause, PhPlay, PhSquare, PhTrash } from '@phosphor-icons/vue'
|
||||
import { PhCheckSquare, PhClockCountdown, PhFlag, PhPause, PhPen, PhPlay, PhSquare, PhTrash } from '@phosphor-icons/vue'
|
||||
import { onClickOutside, useSwipe } from '@vueuse/core'
|
||||
import { DateTime } from 'luxon'
|
||||
import { computed, shallowRef, useTemplateRef } from 'vue'
|
||||
import useActions from '../composables/useActions.ts'
|
||||
import { TaskStatus } from '../types.ts'
|
||||
import EditForm from './forms/EditForm.vue'
|
||||
|
||||
const { task } = defineProps<{ task: Task }>()
|
||||
|
||||
const { run } = useActions()
|
||||
|
||||
const dueColor = computed(() => {
|
||||
const dueDiff = task.dueDate ? DateTime.fromMillis(task.dueDate).diffNow('days').days : undefined
|
||||
const dueDiff = task.due_date ? DateTime.fromISO(task.due_date).diffNow('days').days : undefined
|
||||
if (!dueDiff)
|
||||
return ''
|
||||
if (dueDiff < 0) {
|
||||
@@ -36,8 +37,11 @@ const containerWidth = computed(() => container.value?.offsetWidth)
|
||||
const left = shallowRef('0')
|
||||
const opacity = shallowRef(1)
|
||||
|
||||
const modalComponent = useTemplateRef('modalComponent')
|
||||
|
||||
function reset() {
|
||||
left.value = '0'
|
||||
modalComponent.value?.close()
|
||||
opacity.value = 1
|
||||
}
|
||||
const { isSwiping, lengthX } = useSwipe(
|
||||
@@ -94,13 +98,16 @@ onClickOutside(container, reset)
|
||||
<button v-else class="btn btn-square btn-ghost" @click="handleClick(`stop ${task.id_}`)">
|
||||
<PhPause :size="30" weight="fill" class="text-info" />
|
||||
</button>
|
||||
<button class="btn btn-square btn-ghost" @click="modalComponent?.showModal()">
|
||||
<PhPen :size="30" weight="fill" class="text-info" />
|
||||
</button>
|
||||
<div class="grow flex justify-end">
|
||||
<button class="btn btn-square btn-ghost" @click="handleClick(`delete ${task.id_}`)">
|
||||
<PhTrash :size="30" weight="fill" class="text-error" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="target" :class="{ 'transition-all': !isSwiping }" :style="{ left, opacity }" class="top-0 left-0 w-full h-full absolute rounded bg-white font-mono text-md font-semibold flex flex-row justify-start items-center gap-2 min-h-10 px-2">
|
||||
<div ref="target" :class="{ 'transition-all': !isSwiping }" :style="{ left, opacity }" class="top-0 left-0 w-full h-full absolute bg-base-100 text-base-content font-mono text-md font-semibold flex flex-row justify-start items-center gap-2 min-h-10 px-2">
|
||||
<div class="flex items-center justify-center">
|
||||
<PhSquare v-if="task.status === TaskStatus.WAIT" :size="30" />
|
||||
<PhCheckSquare v-else-if="task.status === TaskStatus.DONE" :size="30" weight="fill" class="text-success" />
|
||||
@@ -110,10 +117,18 @@ onClickOutside(container, reset)
|
||||
<div class="grow">
|
||||
{{ task.title }}
|
||||
</div>
|
||||
<div v-if="task.dueDate" :class="dueColor" class="text-right">
|
||||
<div v-if="task.due_date" :class="dueColor" class="text-right">
|
||||
<PhClockCountdown :size="30" :weight="dueColor === 'text-error' ? 'fill' : 'regular'" />
|
||||
</div>
|
||||
</div>
|
||||
<dialog ref="modalComponent" class="modal">
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
<div class="modal-box">
|
||||
<EditForm :task="task" @update="reset" />
|
||||
</div>
|
||||
</dialog>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -6,37 +6,31 @@ const { createTask } = useTasks()
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
const data = new FormData(e.target as HTMLFormElement)
|
||||
const task = Object.fromEntries(data) as unknown as Pick<Task, 'tag' | 'title' | 'dueDate'>
|
||||
const task = Object.fromEntries(data) as unknown as Pick<Task, 'tag' | 'title' | 'due_date'>
|
||||
|
||||
await createTask(task)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend">
|
||||
What is your name?
|
||||
</legend>
|
||||
<input type="text" class="input" name="title" placeholder="Type here">
|
||||
<input type="text" class="input input-xl input-neutral w-full" name="title" placeholder="Task">
|
||||
</fieldset>
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend">
|
||||
Category
|
||||
</legend>
|
||||
<select class="select" name="tag">
|
||||
<option disabled selected value="@uncategorized">
|
||||
<select class="select select-xl select-neutral w-full" name="tag">
|
||||
<option selected value="@uncategorized">
|
||||
@uncategorized
|
||||
</option>
|
||||
<option value="@home">
|
||||
@home
|
||||
</option>
|
||||
<option value="@work">
|
||||
Amber
|
||||
@work
|
||||
</option>
|
||||
</select>
|
||||
</fieldset>
|
||||
<button class="btn btn-primary">
|
||||
<button class="btn btn-xl btn-neutral btn-outline w-full">
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import type { Task } from '../../types.ts'
|
||||
import { useTasks } from '../../composables/useTasks.ts'
|
||||
import { ref } from 'vue'
|
||||
import useActions from '../../composables/useActions.ts'
|
||||
|
||||
const { createTask } = useTasks()
|
||||
const { task } = defineProps<{ task: Task }>()
|
||||
|
||||
const emit = defineEmits(['update'])
|
||||
|
||||
const localTask = ref(task)
|
||||
|
||||
const { run } = useActions()
|
||||
async function handleSubmit(e: Event) {
|
||||
const data = new FormData(e.target as HTMLFormElement)
|
||||
const task = Object.fromEntries(data) as unknown as Pick<Task, 'tag' | 'title' | 'dueDate'>
|
||||
|
||||
await createTask(task)
|
||||
const formData = new FormData(e.target as HTMLFormElement)
|
||||
const data = Object.fromEntries(formData) as unknown as Pick<Task, 'tag' | 'title' | 'due_date'>
|
||||
run(`edit ${localTask.value.id_} ${data.tag} ${data.title}`)
|
||||
emit('update')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend">
|
||||
What is your name?
|
||||
</legend>
|
||||
<input type="text" class="input" name="title" placeholder="Type here">
|
||||
<input v-model="localTask.title" type="text" class="input input-xl input-neutral w-full" name="title" placeholder="Task">
|
||||
</fieldset>
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend">
|
||||
Category
|
||||
</legend>
|
||||
<select class="select" name="tag">
|
||||
<option disabled selected value="@uncategorized">
|
||||
<select v-model="localTask.tag" class="select select-xl select-neutral w-full" name="tag">
|
||||
<option selected value="@uncategorized">
|
||||
@uncategorized
|
||||
</option>
|
||||
<option value="@home">
|
||||
@home
|
||||
</option>
|
||||
<option value="@work">
|
||||
Amber
|
||||
@work
|
||||
</option>
|
||||
</select>
|
||||
</fieldset>
|
||||
<button class="btn btn-primary">
|
||||
<button class="btn btn-xl btn-neutral btn-outline w-full">
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -2,21 +2,42 @@
|
||||
import { useSettings } from '../../composables/useSettings.ts'
|
||||
|
||||
const { settings } = useSettings()
|
||||
const themes = ['default', 'black', 'cyberpunk', 'forest', 'halloween', 'luxury', 'retro', 'synthwave', 'valentine', 'wireframe', 'aqua']
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent>
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend">
|
||||
Username
|
||||
API Key
|
||||
</legend>
|
||||
<input v-model="settings.username" type="text" class="input" name="username">
|
||||
</fieldset>
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend">
|
||||
Password
|
||||
</legend>
|
||||
<input v-model="settings.password" type="password" class="input" name="password">
|
||||
<input v-model="settings.accessKey" type="password" class="input" name="accessKey">
|
||||
</fieldset>
|
||||
<div class="dropdown mb-72">
|
||||
<div tabindex="0" role="button" class="btn m-1">
|
||||
Theme
|
||||
<svg
|
||||
width="12px"
|
||||
height="12px"
|
||||
class="inline-block h-2 w-2 fill-current opacity-60"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 2048 2048"
|
||||
>
|
||||
<path d="M1799 349l242 241-1017 1017L7 590l242-241 775 775 775-775z" />
|
||||
</svg>
|
||||
</div>
|
||||
<ul tabindex="-1" class="dropdown-content bg-base-300 rounded-box z-1 w-52 p-2 shadow-2xl">
|
||||
<li v-for="theme in themes" :key="theme">
|
||||
<input
|
||||
v-model="settings.theme"
|
||||
type="radio"
|
||||
name="theme-dropdown"
|
||||
class="theme-controller w-full btn btn-sm btn-block btn-ghost justify-start"
|
||||
:aria-label="theme.charAt(0).toUpperCase() + theme.slice(1)"
|
||||
:value="theme"
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user