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:
@@ -6,9 +6,11 @@ import CreateInput from './components/CreateInput.vue'
|
|||||||
|
|
||||||
import MobileActions from './components/MobileActions.vue'
|
import MobileActions from './components/MobileActions.vue'
|
||||||
import SettingsModal from './components/SettingsModal.vue'
|
import SettingsModal from './components/SettingsModal.vue'
|
||||||
|
import { useSettings } from './composables/useSettings.ts'
|
||||||
import { useTasks } from './composables/useTasks.ts'
|
import { useTasks } from './composables/useTasks.ts'
|
||||||
|
|
||||||
const isMobile = useMediaQuery('(pointer: coarse)')
|
const isMobile = useMediaQuery('(pointer: coarse)')
|
||||||
|
const { settings } = useSettings()
|
||||||
|
|
||||||
const { fetchTasks } = useTasks()
|
const { fetchTasks } = useTasks()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -17,7 +19,7 @@ onMounted(fetchTasks)
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div :data-theme="settings.theme ?? 'default'" class="flex flex-col">
|
||||||
<main class="h-screen overflow-y-auto overflow-x-none w-screen max-w-screen border-t-2 border-primary">
|
<main class="h-screen overflow-y-auto overflow-x-none w-screen max-w-screen border-t-2 border-primary">
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -120,18 +120,18 @@ watchEffect(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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="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 }}
|
{{ suggestion }}
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
ref="inputComponent"
|
ref="inputComponent"
|
||||||
v-model="value" type="text"
|
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>
|
||||||
<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 />
|
<HelpPanel />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -27,23 +27,23 @@ function showModal(component: ModalShown) {
|
|||||||
<div>
|
<div>
|
||||||
<div class="fab select-none">
|
<div class="fab select-none">
|
||||||
<!-- a focusable div with tabindex is necessary to work on all browsers. role="button" is necessary for accessibility -->
|
<!-- 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" />
|
<PhDotsNine :size="30" weight="bold" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- close button should not be focusable so it can close the FAB when clicked. It's just a visual placeholder -->
|
<!-- close button should not be focusable so it can close the FAB when clicked. It's just a visual placeholder -->
|
||||||
<div class="fab-close">
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- buttons that show up when FAB is open -->
|
<!-- buttons that show up when FAB is open -->
|
||||||
<div>
|
<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" />
|
<PhPlus size="30" weight="bold" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<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" />
|
<PhSliders :size="30" weight="bold" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,11 +53,6 @@ function showModal(component: ModalShown) {
|
|||||||
<button>close</button>
|
<button>close</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="modal-box">
|
<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" />
|
<component :is="componentMap[modalShown]" v-if="modalShown" />
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { TaskStatus } from '../types.ts'
|
|||||||
const { task } = defineProps<{ task: Task }>()
|
const { task } = defineProps<{ task: Task }>()
|
||||||
|
|
||||||
const dueColor = computed(() => {
|
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)
|
if (!dueDiff)
|
||||||
return ''
|
return ''
|
||||||
if (dueDiff < 0) {
|
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" />
|
<PhPlay v-else-if="task.status === TaskStatus.WIP" :size="16" weight="fill" class="text-info" />
|
||||||
</div>
|
</div>
|
||||||
<span>{{ task.title }}</span>
|
<span>{{ task.title }}</span>
|
||||||
<span v-if="task.dueDate" :class="dueColor">
|
<span v-if="task.due_date" :class="dueColor">
|
||||||
{{ DateTime.fromMillis(task.dueDate).toFormat('dd/MM/yyyy') }}
|
{{ DateTime.fromISO(task.due_date).toFormat('dd/MM/yyyy') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { UseSwipeDirection } from '@vueuse/core'
|
import type { UseSwipeDirection } from '@vueuse/core'
|
||||||
import type { Task } from '../types.ts'
|
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 { onClickOutside, useSwipe } from '@vueuse/core'
|
||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
import { computed, shallowRef, useTemplateRef } from 'vue'
|
import { computed, shallowRef, useTemplateRef } from 'vue'
|
||||||
import useActions from '../composables/useActions.ts'
|
import useActions from '../composables/useActions.ts'
|
||||||
import { TaskStatus } from '../types.ts'
|
import { TaskStatus } from '../types.ts'
|
||||||
|
import EditForm from './forms/EditForm.vue'
|
||||||
|
|
||||||
const { task } = defineProps<{ task: Task }>()
|
const { task } = defineProps<{ task: Task }>()
|
||||||
|
|
||||||
const { run } = useActions()
|
const { run } = useActions()
|
||||||
|
|
||||||
const dueColor = computed(() => {
|
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)
|
if (!dueDiff)
|
||||||
return ''
|
return ''
|
||||||
if (dueDiff < 0) {
|
if (dueDiff < 0) {
|
||||||
@@ -36,8 +37,11 @@ const containerWidth = computed(() => container.value?.offsetWidth)
|
|||||||
const left = shallowRef('0')
|
const left = shallowRef('0')
|
||||||
const opacity = shallowRef(1)
|
const opacity = shallowRef(1)
|
||||||
|
|
||||||
|
const modalComponent = useTemplateRef('modalComponent')
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
left.value = '0'
|
left.value = '0'
|
||||||
|
modalComponent.value?.close()
|
||||||
opacity.value = 1
|
opacity.value = 1
|
||||||
}
|
}
|
||||||
const { isSwiping, lengthX } = useSwipe(
|
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_}`)">
|
<button v-else class="btn btn-square btn-ghost" @click="handleClick(`stop ${task.id_}`)">
|
||||||
<PhPause :size="30" weight="fill" class="text-info" />
|
<PhPause :size="30" weight="fill" class="text-info" />
|
||||||
</button>
|
</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">
|
<div class="grow flex justify-end">
|
||||||
<button class="btn btn-square btn-ghost" @click="handleClick(`delete ${task.id_}`)">
|
<button class="btn btn-square btn-ghost" @click="handleClick(`delete ${task.id_}`)">
|
||||||
<PhTrash :size="30" weight="fill" class="text-error" />
|
<PhTrash :size="30" weight="fill" class="text-error" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="flex items-center justify-center">
|
||||||
<PhSquare v-if="task.status === TaskStatus.WAIT" :size="30" />
|
<PhSquare v-if="task.status === TaskStatus.WAIT" :size="30" />
|
||||||
<PhCheckSquare v-else-if="task.status === TaskStatus.DONE" :size="30" weight="fill" class="text-success" />
|
<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">
|
<div class="grow">
|
||||||
{{ task.title }}
|
{{ task.title }}
|
||||||
</div>
|
</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'" />
|
<PhClockCountdown :size="30" :weight="dueColor === 'text-error' ? 'fill' : 'regular'" />
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -6,37 +6,31 @@ const { createTask } = useTasks()
|
|||||||
|
|
||||||
async function handleSubmit(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 = 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)
|
await createTask(task)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="handleSubmit">
|
<form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend class="fieldset-legend">
|
<input type="text" class="input input-xl input-neutral w-full" name="title" placeholder="Task">
|
||||||
What is your name?
|
|
||||||
</legend>
|
|
||||||
<input type="text" class="input" name="title" placeholder="Type here">
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend class="fieldset-legend">
|
<select class="select select-xl select-neutral w-full" name="tag">
|
||||||
Category
|
<option selected value="@uncategorized">
|
||||||
</legend>
|
|
||||||
<select class="select" name="tag">
|
|
||||||
<option disabled selected value="@uncategorized">
|
|
||||||
@uncategorized
|
@uncategorized
|
||||||
</option>
|
</option>
|
||||||
<option value="@home">
|
<option value="@home">
|
||||||
@home
|
@home
|
||||||
</option>
|
</option>
|
||||||
<option value="@work">
|
<option value="@work">
|
||||||
Amber
|
@work
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<button class="btn btn-primary">
|
<button class="btn btn-xl btn-neutral btn-outline w-full">
|
||||||
Submit
|
Submit
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,42 +1,42 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Task } from '../../types.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) {
|
async function handleSubmit(e: Event) {
|
||||||
const data = new FormData(e.target as HTMLFormElement)
|
const formData = new FormData(e.target as HTMLFormElement)
|
||||||
const task = Object.fromEntries(data) as unknown as Pick<Task, 'tag' | 'title' | 'dueDate'>
|
const data = Object.fromEntries(formData) as unknown as Pick<Task, 'tag' | 'title' | 'due_date'>
|
||||||
|
run(`edit ${localTask.value.id_} ${data.tag} ${data.title}`)
|
||||||
await createTask(task)
|
emit('update')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="handleSubmit">
|
<form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend class="fieldset-legend">
|
<input v-model="localTask.title" type="text" class="input input-xl input-neutral w-full" name="title" placeholder="Task">
|
||||||
What is your name?
|
|
||||||
</legend>
|
|
||||||
<input type="text" class="input" name="title" placeholder="Type here">
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend class="fieldset-legend">
|
<select v-model="localTask.tag" class="select select-xl select-neutral w-full" name="tag">
|
||||||
Category
|
<option selected value="@uncategorized">
|
||||||
</legend>
|
|
||||||
<select class="select" name="tag">
|
|
||||||
<option disabled selected value="@uncategorized">
|
|
||||||
@uncategorized
|
@uncategorized
|
||||||
</option>
|
</option>
|
||||||
<option value="@home">
|
<option value="@home">
|
||||||
@home
|
@home
|
||||||
</option>
|
</option>
|
||||||
<option value="@work">
|
<option value="@work">
|
||||||
Amber
|
@work
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<button class="btn btn-primary">
|
<button class="btn btn-xl btn-neutral btn-outline w-full">
|
||||||
Submit
|
Submit
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -2,21 +2,42 @@
|
|||||||
import { useSettings } from '../../composables/useSettings.ts'
|
import { useSettings } from '../../composables/useSettings.ts'
|
||||||
|
|
||||||
const { settings } = useSettings()
|
const { settings } = useSettings()
|
||||||
|
const themes = ['default', 'black', 'cyberpunk', 'forest', 'halloween', 'luxury', 'retro', 'synthwave', 'valentine', 'wireframe', 'aqua']
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<form @submit.prevent>
|
<form @submit.prevent>
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend class="fieldset-legend">
|
<legend class="fieldset-legend">
|
||||||
Username
|
API Key
|
||||||
</legend>
|
</legend>
|
||||||
<input v-model="settings.username" type="text" class="input" name="username">
|
<input v-model="settings.accessKey" type="password" class="input" name="accessKey">
|
||||||
</fieldset>
|
|
||||||
<fieldset class="fieldset">
|
|
||||||
<legend class="fieldset-legend">
|
|
||||||
Password
|
|
||||||
</legend>
|
|
||||||
<input v-model="settings.password" type="password" class="input" name="password">
|
|
||||||
</fieldset>
|
</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>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -19,12 +19,13 @@ import { useSettings } from './useSettings.ts'
|
|||||||
import { useTasks } from './useTasks.ts'
|
import { useTasks } from './useTasks.ts'
|
||||||
|
|
||||||
export default function useActions() {
|
export default function useActions() {
|
||||||
const { tasks: tasksOriginal, createTask, updateTask } = useTasks()
|
const { tasks: tasksOriginal, createTask, updateTask, deleteTask, fetchTasks } = useTasks()
|
||||||
const { settings } = useSettings()
|
const { settings } = useSettings()
|
||||||
const run = async (value: string) => {
|
const run = async (value: string) => {
|
||||||
const cmd = parseCommand(value)
|
const cmd = parseCommand(value)
|
||||||
let tasksToUpdate: Task[] = []
|
let tasksToUpdate: Task[] | null = null
|
||||||
let taskToCreate: Pick<Task, 'tag' | 'title' | 'dueDate'> | null = null
|
let tasksToDelete: Task[] | null = null
|
||||||
|
let taskToCreate: Pick<Task, 'tag' | 'title' | 'due_date'> | null = null
|
||||||
const tasks = JSON.parse(JSON.stringify(tasksOriginal.value)) as Task[]
|
const tasks = JSON.parse(JSON.stringify(tasksOriginal.value)) as Task[]
|
||||||
if (cmd) {
|
if (cmd) {
|
||||||
const ids = cmd.id
|
const ids = cmd.id
|
||||||
@@ -45,7 +46,7 @@ export default function useActions() {
|
|||||||
break
|
break
|
||||||
case 'd':
|
case 'd':
|
||||||
case 'delete':
|
case 'delete':
|
||||||
tasksToUpdate = deleteCommand(ids, cmd, tasks)
|
tasksToDelete = deleteCommand(ids, cmd, tasks)
|
||||||
break
|
break
|
||||||
case 'fl':
|
case 'fl':
|
||||||
case 'flag':
|
case 'flag':
|
||||||
@@ -98,12 +99,16 @@ export default function useActions() {
|
|||||||
// break
|
// break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (tasksToDelete) {
|
||||||
|
await deleteTask(tasksToDelete)
|
||||||
|
}
|
||||||
if (tasksToUpdate) {
|
if (tasksToUpdate) {
|
||||||
await updateTask(tasksToUpdate)
|
await updateTask(tasksToUpdate)
|
||||||
}
|
}
|
||||||
if (taskToCreate) {
|
if (taskToCreate) {
|
||||||
await createTask(taskToCreate)
|
await createTask(taskToCreate)
|
||||||
}
|
}
|
||||||
|
await fetchTasks(true)
|
||||||
}
|
}
|
||||||
return { run }
|
return { run }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,10 +60,14 @@ export function useApi() {
|
|||||||
const patch = (endpoint: string, body: unknown, options: RequestInit = {}) =>
|
const patch = (endpoint: string, body: unknown, options: RequestInit = {}) =>
|
||||||
apiFetch(endpoint, { ...options, method: 'PATCH', body: JSON.stringify(body) })
|
apiFetch(endpoint, { ...options, method: 'PATCH', body: JSON.stringify(body) })
|
||||||
|
|
||||||
|
const delete_ = (endpoint: string, body: unknown, options: RequestInit = {}) =>
|
||||||
|
apiFetch(endpoint, { ...options, method: 'DELETE', body: JSON.stringify(body) })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
get,
|
get,
|
||||||
post,
|
post,
|
||||||
patch,
|
patch,
|
||||||
|
delete: delete_,
|
||||||
fetch: apiFetch,
|
fetch: apiFetch,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import { useStore } from './useStore.ts'
|
|||||||
export interface Settings {
|
export interface Settings {
|
||||||
accessKey: string
|
accessKey: string
|
||||||
todayShown: boolean
|
todayShown: boolean
|
||||||
|
theme: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const settingsDefault: Settings = {
|
const settingsDefault: Settings = {
|
||||||
accessKey: '',
|
accessKey: '',
|
||||||
todayShown: false,
|
todayShown: false,
|
||||||
|
theme: 'default',
|
||||||
}
|
}
|
||||||
const settings = ref<Settings>({ ...settingsDefault })
|
const settings = ref<Settings>({ ...settingsDefault })
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const tasks = ref<Task[]>([])
|
|||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
const endpoint = '/items/pomodays'
|
const endpoint = '/items/pomodays'
|
||||||
|
const refreshInterval = ref<number>()
|
||||||
|
|
||||||
export function useTasks() {
|
export function useTasks() {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
@@ -18,7 +19,7 @@ export function useTasks() {
|
|||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const data = await api.get(`${endpoint}?limit=-1`).then(res => res.json())
|
const data = await api.get(`${endpoint}?limit=-1`).then(res => res.json())
|
||||||
tasks.value = data.tasks ?? []
|
tasks.value = data.data ?? []
|
||||||
}
|
}
|
||||||
catch (e: any) {
|
catch (e: any) {
|
||||||
error.value = e.message || 'Failed to fetch tasks'
|
error.value = e.message || 'Failed to fetch tasks'
|
||||||
@@ -29,12 +30,15 @@ export function useTasks() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (refreshInterval.value === undefined) {
|
||||||
|
refreshInterval.value = setInterval(() => fetchTasks(true), 1000 * 60)
|
||||||
|
}
|
||||||
|
|
||||||
const createTask = async (taskData: Pick<Task, 'title' | 'due_date' | 'tag'>) => {
|
const createTask = async (taskData: Pick<Task, 'title' | 'due_date' | 'tag'>) => {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
await api.post(endpoint, { ...taskData }).then(res => res.json())
|
await api.post(endpoint, { ...taskData }).then(res => res.json())
|
||||||
await fetchTasks()
|
|
||||||
}
|
}
|
||||||
catch (e: any) {
|
catch (e: any) {
|
||||||
error.value = e.message || 'Failed to create task'
|
error.value = e.message || 'Failed to create task'
|
||||||
@@ -46,12 +50,14 @@ export function useTasks() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateTask = async (task: Task) => {
|
const updateTask = async (tasks: Task | Task[]) => {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
|
const tasksToUpdate = Array.isArray(tasks) ? tasks : [tasks]
|
||||||
try {
|
try {
|
||||||
await api.patch(`${endpoint}/${task.id}`, task).then(res => res.json())
|
for (const task of tasksToUpdate) {
|
||||||
await fetchTasks()
|
await api.patch(`${endpoint}/${task.id}`, task)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (e: any) {
|
catch (e: any) {
|
||||||
error.value = e.message || 'Failed to update task'
|
error.value = e.message || 'Failed to update task'
|
||||||
@@ -63,12 +69,12 @@ export function useTasks() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateTasks = async (data: Partial<Task>, keys: Task['id'][]) => {
|
const deleteTask = async (tasks: Task | Task[]) => {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
|
const taskIdsToDelete = (Array.isArray(tasks) ? tasks : [tasks]).map(task => task.id)
|
||||||
try {
|
try {
|
||||||
await api.patch(`${endpoint}`, { data, keys }).then(res => res.json())
|
await api.delete(endpoint, taskIdsToDelete)
|
||||||
await fetchTasks()
|
|
||||||
}
|
}
|
||||||
catch (e: any) {
|
catch (e: any) {
|
||||||
error.value = e.message || 'Failed to update task'
|
error.value = e.message || 'Failed to update task'
|
||||||
@@ -89,7 +95,7 @@ export function useTasks() {
|
|||||||
fetchTasks,
|
fetchTasks,
|
||||||
createTask,
|
createTask,
|
||||||
updateTask,
|
updateTask,
|
||||||
updateTasks,
|
deleteTask,
|
||||||
categories,
|
categories,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
|
|
||||||
@plugin "daisyui/theme" {
|
@plugin "daisyui/theme" {
|
||||||
name: "lofi";
|
name: "default";
|
||||||
default: true;
|
default: true;
|
||||||
prefersdark: false;
|
prefersdark: false;
|
||||||
color-scheme: "light";
|
color-scheme: "light";
|
||||||
@@ -75,6 +75,333 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@plugin "daisyui/theme" {
|
||||||
|
name: "cyberpunk";
|
||||||
|
default: false;
|
||||||
|
prefersdark: false;
|
||||||
|
color-scheme: "light";
|
||||||
|
--color-base-100: oklch(94.51% 0.179 104.32);
|
||||||
|
--color-base-200: oklch(91.51% 0.179 104.32);
|
||||||
|
--color-base-300: oklch(85.51% 0.179 104.32);
|
||||||
|
--color-base-content: oklch(0% 0 0);
|
||||||
|
--color-primary: oklch(74.22% 0.209 6.35);
|
||||||
|
--color-primary-content: oklch(14.844% 0.041 6.35);
|
||||||
|
--color-secondary: oklch(83.33% 0.184 204.72);
|
||||||
|
--color-secondary-content: oklch(16.666% 0.036 204.72);
|
||||||
|
--color-accent: oklch(71.86% 0.217 310.43);
|
||||||
|
--color-accent-content: oklch(14.372% 0.043 310.43);
|
||||||
|
--color-neutral: oklch(23.04% 0.065 269.31);
|
||||||
|
--color-neutral-content: oklch(94.51% 0.179 104.32);
|
||||||
|
--color-info: oklch(72.06% 0.191 231.6);
|
||||||
|
--color-info-content: oklch(0% 0 0);
|
||||||
|
--color-success: oklch(64.8% 0.15 160);
|
||||||
|
--color-success-content: oklch(0% 0 0);
|
||||||
|
--color-warning: oklch(84.71% 0.199 83.87);
|
||||||
|
--color-warning-content: oklch(0% 0 0);
|
||||||
|
--color-error: oklch(71.76% 0.221 22.18);
|
||||||
|
--color-error-content: oklch(0% 0 0);
|
||||||
|
--radius-selector: 0rem;
|
||||||
|
--radius-field: 0rem;
|
||||||
|
--radius-box: 0rem;
|
||||||
|
--size-selector: 0.25rem;
|
||||||
|
--size-field: 0.25rem;
|
||||||
|
--border: 1px;
|
||||||
|
--depth: 0;
|
||||||
|
--noise: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@plugin "daisyui/theme" {
|
||||||
|
name: "forest";
|
||||||
|
default: false;
|
||||||
|
prefersdark: false;
|
||||||
|
color-scheme: "dark";
|
||||||
|
--color-base-100: oklch(20.84% 0.008 17.911);
|
||||||
|
--color-base-200: oklch(18.522% 0.007 17.911);
|
||||||
|
--color-base-300: oklch(16.203% 0.007 17.911);
|
||||||
|
--color-base-content: oklch(83.768% 0.001 17.911);
|
||||||
|
--color-primary: oklch(68.628% 0.185 148.958);
|
||||||
|
--color-primary-content: oklch(0% 0 0);
|
||||||
|
--color-secondary: oklch(69.776% 0.135 168.327);
|
||||||
|
--color-secondary-content: oklch(13.955% 0.027 168.327);
|
||||||
|
--color-accent: oklch(70.628% 0.119 185.713);
|
||||||
|
--color-accent-content: oklch(14.125% 0.023 185.713);
|
||||||
|
--color-neutral: oklch(30.698% 0.039 171.364);
|
||||||
|
--color-neutral-content: oklch(86.139% 0.007 171.364);
|
||||||
|
--color-info: oklch(72.06% 0.191 231.6);
|
||||||
|
--color-info-content: oklch(0% 0 0);
|
||||||
|
--color-success: oklch(64.8% 0.15 160);
|
||||||
|
--color-success-content: oklch(0% 0 0);
|
||||||
|
--color-warning: oklch(84.71% 0.199 83.87);
|
||||||
|
--color-warning-content: oklch(0% 0 0);
|
||||||
|
--color-error: oklch(71.76% 0.221 22.18);
|
||||||
|
--color-error-content: oklch(0% 0 0);
|
||||||
|
--radius-selector: 1rem;
|
||||||
|
--radius-field: 2rem;
|
||||||
|
--radius-box: 1rem;
|
||||||
|
--size-selector: 0.25rem;
|
||||||
|
--size-field: 0.25rem;
|
||||||
|
--border: 1px;
|
||||||
|
--depth: 0;
|
||||||
|
--noise: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@plugin "daisyui/theme" {
|
||||||
|
name: "retro";
|
||||||
|
default: false;
|
||||||
|
prefersdark: false;
|
||||||
|
color-scheme: "light";
|
||||||
|
--color-base-100: oklch(91.637% 0.034 90.515);
|
||||||
|
--color-base-200: oklch(88.272% 0.049 91.774);
|
||||||
|
--color-base-300: oklch(84.133% 0.065 90.856);
|
||||||
|
--color-base-content: oklch(41% 0.112 45.904);
|
||||||
|
--color-primary: oklch(80% 0.114 19.571);
|
||||||
|
--color-primary-content: oklch(39% 0.141 25.723);
|
||||||
|
--color-secondary: oklch(92% 0.084 155.995);
|
||||||
|
--color-secondary-content: oklch(44% 0.119 151.328);
|
||||||
|
--color-accent: oklch(68% 0.162 75.834);
|
||||||
|
--color-accent-content: oklch(41% 0.112 45.904);
|
||||||
|
--color-neutral: oklch(44% 0.011 73.639);
|
||||||
|
--color-neutral-content: oklch(86% 0.005 56.366);
|
||||||
|
--color-info: oklch(58% 0.158 241.966);
|
||||||
|
--color-info-content: oklch(96% 0.059 95.617);
|
||||||
|
--color-success: oklch(51% 0.096 186.391);
|
||||||
|
--color-success-content: oklch(96% 0.059 95.617);
|
||||||
|
--color-warning: oklch(64% 0.222 41.116);
|
||||||
|
--color-warning-content: oklch(96% 0.059 95.617);
|
||||||
|
--color-error: oklch(70% 0.191 22.216);
|
||||||
|
--color-error-content: oklch(40% 0.123 38.172);
|
||||||
|
--radius-selector: 0.25rem;
|
||||||
|
--radius-field: 0.25rem;
|
||||||
|
--radius-box: 0.5rem;
|
||||||
|
--size-selector: 0.25rem;
|
||||||
|
--size-field: 0.25rem;
|
||||||
|
--border: 1px;
|
||||||
|
--depth: 0;
|
||||||
|
--noise: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@plugin "daisyui/theme" {
|
||||||
|
name: "halloween";
|
||||||
|
default: false;
|
||||||
|
prefersdark: false;
|
||||||
|
color-scheme: "dark";
|
||||||
|
--color-base-100: oklch(21% 0.006 56.043);
|
||||||
|
--color-base-200: oklch(14% 0.004 49.25);
|
||||||
|
--color-base-300: oklch(0% 0 0);
|
||||||
|
--color-base-content: oklch(84.955% 0 0);
|
||||||
|
--color-primary: oklch(77.48% 0.204 60.62);
|
||||||
|
--color-primary-content: oklch(19.693% 0.004 196.779);
|
||||||
|
--color-secondary: oklch(45.98% 0.248 305.03);
|
||||||
|
--color-secondary-content: oklch(89.196% 0.049 305.03);
|
||||||
|
--color-accent: oklch(64.8% 0.223 136.073);
|
||||||
|
--color-accent-content: oklch(0% 0 0);
|
||||||
|
--color-neutral: oklch(24.371% 0.046 65.681);
|
||||||
|
--color-neutral-content: oklch(84.874% 0.009 65.681);
|
||||||
|
--color-info: oklch(54.615% 0.215 262.88);
|
||||||
|
--color-info-content: oklch(90.923% 0.043 262.88);
|
||||||
|
--color-success: oklch(62.705% 0.169 149.213);
|
||||||
|
--color-success-content: oklch(12.541% 0.033 149.213);
|
||||||
|
--color-warning: oklch(66.584% 0.157 58.318);
|
||||||
|
--color-warning-content: oklch(13.316% 0.031 58.318);
|
||||||
|
--color-error: oklch(65.72% 0.199 27.33);
|
||||||
|
--color-error-content: oklch(13.144% 0.039 27.33);
|
||||||
|
--radius-selector: 1rem;
|
||||||
|
--radius-field: 0.5rem;
|
||||||
|
--radius-box: 1rem;
|
||||||
|
--size-selector: 0.25rem;
|
||||||
|
--size-field: 0.25rem;
|
||||||
|
--border: 1px;
|
||||||
|
--depth: 1;
|
||||||
|
--noise: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@plugin "daisyui/theme" {
|
||||||
|
name: "luxury";
|
||||||
|
default: false;
|
||||||
|
prefersdark: false;
|
||||||
|
color-scheme: "dark";
|
||||||
|
--color-base-100: oklch(14.076% 0.004 285.822);
|
||||||
|
--color-base-200: oklch(20.219% 0.004 308.229);
|
||||||
|
--color-base-300: oklch(23.219% 0.004 308.229);
|
||||||
|
--color-base-content: oklch(75.687% 0.123 76.89);
|
||||||
|
--color-primary: oklch(100% 0 0);
|
||||||
|
--color-primary-content: oklch(20% 0 0);
|
||||||
|
--color-secondary: oklch(27.581% 0.064 261.069);
|
||||||
|
--color-secondary-content: oklch(85.516% 0.012 261.069);
|
||||||
|
--color-accent: oklch(36.674% 0.051 338.825);
|
||||||
|
--color-accent-content: oklch(87.334% 0.01 338.825);
|
||||||
|
--color-neutral: oklch(24.27% 0.057 59.825);
|
||||||
|
--color-neutral-content: oklch(93.203% 0.089 90.861);
|
||||||
|
--color-info: oklch(79.061% 0.121 237.133);
|
||||||
|
--color-info-content: oklch(15.812% 0.024 237.133);
|
||||||
|
--color-success: oklch(78.119% 0.192 132.154);
|
||||||
|
--color-success-content: oklch(15.623% 0.038 132.154);
|
||||||
|
--color-warning: oklch(86.127% 0.136 102.891);
|
||||||
|
--color-warning-content: oklch(17.225% 0.027 102.891);
|
||||||
|
--color-error: oklch(71.753% 0.176 22.568);
|
||||||
|
--color-error-content: oklch(14.35% 0.035 22.568);
|
||||||
|
--radius-selector: 1rem;
|
||||||
|
--radius-field: 0.5rem;
|
||||||
|
--radius-box: 1rem;
|
||||||
|
--size-selector: 0.25rem;
|
||||||
|
--size-field: 0.25rem;
|
||||||
|
--border: 1px;
|
||||||
|
--depth: 1;
|
||||||
|
--noise: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@plugin "daisyui/theme" {
|
||||||
|
name: "synthwave";
|
||||||
|
default: false;
|
||||||
|
prefersdark: false;
|
||||||
|
color-scheme: "dark";
|
||||||
|
--color-base-100: oklch(15% 0.09 281.288);
|
||||||
|
--color-base-200: oklch(20% 0.09 281.288);
|
||||||
|
--color-base-300: oklch(25% 0.09 281.288);
|
||||||
|
--color-base-content: oklch(78% 0.115 274.713);
|
||||||
|
--color-primary: oklch(71% 0.202 349.761);
|
||||||
|
--color-primary-content: oklch(28% 0.109 3.907);
|
||||||
|
--color-secondary: oklch(82% 0.111 230.318);
|
||||||
|
--color-secondary-content: oklch(29% 0.066 243.157);
|
||||||
|
--color-accent: oklch(75% 0.183 55.934);
|
||||||
|
--color-accent-content: oklch(26% 0.079 36.259);
|
||||||
|
--color-neutral: oklch(45% 0.24 277.023);
|
||||||
|
--color-neutral-content: oklch(87% 0.065 274.039);
|
||||||
|
--color-info: oklch(74% 0.16 232.661);
|
||||||
|
--color-info-content: oklch(29% 0.066 243.157);
|
||||||
|
--color-success: oklch(77% 0.152 181.912);
|
||||||
|
--color-success-content: oklch(27% 0.046 192.524);
|
||||||
|
--color-warning: oklch(90% 0.182 98.111);
|
||||||
|
--color-warning-content: oklch(42% 0.095 57.708);
|
||||||
|
--color-error: oklch(73.7% 0.121 32.639);
|
||||||
|
--color-error-content: oklch(23.501% 0.096 290.329);
|
||||||
|
--radius-selector: 1rem;
|
||||||
|
--radius-field: 0.5rem;
|
||||||
|
--radius-box: 1rem;
|
||||||
|
--size-selector: 0.25rem;
|
||||||
|
--size-field: 0.25rem;
|
||||||
|
--border: 1px;
|
||||||
|
--depth: 0;
|
||||||
|
--noise: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@plugin "daisyui/theme" {
|
||||||
|
name: "valentine";
|
||||||
|
default: false;
|
||||||
|
prefersdark: false;
|
||||||
|
color-scheme: "light";
|
||||||
|
--color-base-100: oklch(97% 0.014 343.198);
|
||||||
|
--color-base-200: oklch(94% 0.028 342.258);
|
||||||
|
--color-base-300: oklch(89% 0.061 343.231);
|
||||||
|
--color-base-content: oklch(52% 0.223 3.958);
|
||||||
|
--color-primary: oklch(65% 0.241 354.308);
|
||||||
|
--color-primary-content: oklch(100% 0 0);
|
||||||
|
--color-secondary: oklch(62% 0.265 303.9);
|
||||||
|
--color-secondary-content: oklch(97% 0.014 308.299);
|
||||||
|
--color-accent: oklch(82% 0.111 230.318);
|
||||||
|
--color-accent-content: oklch(39% 0.09 240.876);
|
||||||
|
--color-neutral: oklch(40% 0.153 2.432);
|
||||||
|
--color-neutral-content: oklch(89% 0.061 343.231);
|
||||||
|
--color-info: oklch(86% 0.127 207.078);
|
||||||
|
--color-info-content: oklch(44% 0.11 240.79);
|
||||||
|
--color-success: oklch(84% 0.143 164.978);
|
||||||
|
--color-success-content: oklch(43% 0.095 166.913);
|
||||||
|
--color-warning: oklch(75% 0.183 55.934);
|
||||||
|
--color-warning-content: oklch(26% 0.079 36.259);
|
||||||
|
--color-error: oklch(63% 0.237 25.331);
|
||||||
|
--color-error-content: oklch(97% 0.013 17.38);
|
||||||
|
--radius-selector: 1rem;
|
||||||
|
--radius-field: 2rem;
|
||||||
|
--radius-box: 1rem;
|
||||||
|
--size-selector: 0.25rem;
|
||||||
|
--size-field: 0.25rem;
|
||||||
|
--border: 1px;
|
||||||
|
--depth: 0;
|
||||||
|
--noise: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@plugin "daisyui/theme" {
|
||||||
|
name: "wireframe";
|
||||||
|
default: false;
|
||||||
|
prefersdark: false;
|
||||||
|
color-scheme: "light";
|
||||||
|
--color-base-100: oklch(100% 0 0);
|
||||||
|
--color-base-200: oklch(97% 0 0);
|
||||||
|
--color-base-300: oklch(94% 0 0);
|
||||||
|
--color-base-content: oklch(20% 0 0);
|
||||||
|
--color-primary: oklch(87% 0 0);
|
||||||
|
--color-primary-content: oklch(26% 0 0);
|
||||||
|
--color-secondary: oklch(87% 0 0);
|
||||||
|
--color-secondary-content: oklch(26% 0 0);
|
||||||
|
--color-accent: oklch(87% 0 0);
|
||||||
|
--color-accent-content: oklch(26% 0 0);
|
||||||
|
--color-neutral: oklch(87% 0 0);
|
||||||
|
--color-neutral-content: oklch(26% 0 0);
|
||||||
|
--color-info: oklch(44% 0.11 240.79);
|
||||||
|
--color-info-content: oklch(90% 0.058 230.902);
|
||||||
|
--color-success: oklch(43% 0.095 166.913);
|
||||||
|
--color-success-content: oklch(90% 0.093 164.15);
|
||||||
|
--color-warning: oklch(47% 0.137 46.201);
|
||||||
|
--color-warning-content: oklch(92% 0.12 95.746);
|
||||||
|
--color-error: oklch(44% 0.177 26.899);
|
||||||
|
--color-error-content: oklch(88% 0.062 18.334);
|
||||||
|
--radius-selector: 0rem;
|
||||||
|
--radius-field: 0.25rem;
|
||||||
|
--radius-box: 0.25rem;
|
||||||
|
--size-selector: 0.25rem;
|
||||||
|
--size-field: 0.25rem;
|
||||||
|
--border: 1px;
|
||||||
|
--depth: 0;
|
||||||
|
--noise: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@plugin "daisyui/theme" {
|
||||||
|
name: "aqua";
|
||||||
|
default: false;
|
||||||
|
prefersdark: false;
|
||||||
|
color-scheme: "dark";
|
||||||
|
--color-base-100: oklch(37% 0.146 265.522);
|
||||||
|
--color-base-200: oklch(28% 0.091 267.935);
|
||||||
|
--color-base-300: oklch(22% 0.091 267.935);
|
||||||
|
--color-base-content: oklch(90% 0.058 230.902);
|
||||||
|
--color-primary: oklch(85.661% 0.144 198.645);
|
||||||
|
--color-primary-content: oklch(40.124% 0.068 197.603);
|
||||||
|
--color-secondary: oklch(60.682% 0.108 309.782);
|
||||||
|
--color-secondary-content: oklch(96% 0.016 293.756);
|
||||||
|
--color-accent: oklch(93.426% 0.102 94.555);
|
||||||
|
--color-accent-content: oklch(18.685% 0.02 94.555);
|
||||||
|
--color-neutral: oklch(27% 0.146 265.522);
|
||||||
|
--color-neutral-content: oklch(80% 0.146 265.522);
|
||||||
|
--color-info: oklch(54.615% 0.215 262.88);
|
||||||
|
--color-info-content: oklch(90.923% 0.043 262.88);
|
||||||
|
--color-success: oklch(62.705% 0.169 149.213);
|
||||||
|
--color-success-content: oklch(12.541% 0.033 149.213);
|
||||||
|
--color-warning: oklch(66.584% 0.157 58.318);
|
||||||
|
--color-warning-content: oklch(27% 0.077 45.635);
|
||||||
|
--color-error: oklch(73.95% 0.19 27.33);
|
||||||
|
--color-error-content: oklch(14.79% 0.038 27.33);
|
||||||
|
--radius-selector: 1rem;
|
||||||
|
--radius-field: 0.5rem;
|
||||||
|
--radius-box: 1rem;
|
||||||
|
--size-selector: 0.25rem;
|
||||||
|
--size-field: 0.25rem;
|
||||||
|
--border: 1px;
|
||||||
|
--depth: 1;
|
||||||
|
--noise: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Transitions */
|
/* Transitions */
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export enum TaskStatus {
|
|||||||
|
|
||||||
export interface Worklog {
|
export interface Worklog {
|
||||||
start: string
|
start: string
|
||||||
end: number
|
end?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Task {
|
export interface Task {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Task } from '../types.ts'
|
import type { Task } from '../types.ts'
|
||||||
import type { Command } from './parser.ts'
|
import type { Command } from './parser.ts'
|
||||||
|
import { DateTime } from 'luxon'
|
||||||
import { TaskStatus } from '../types.ts'
|
import { TaskStatus } from '../types.ts'
|
||||||
import { parseDueDate, stopWorkLogging } from './helpers.ts'
|
import { parseDueDate, stopWorkLogging } from './helpers.ts'
|
||||||
|
|
||||||
@@ -14,8 +15,7 @@ export function beginCommand(tasks: Task[], ids: Task['id_'][]) {
|
|||||||
return tasks.filter(t => ids.includes(t.id_) && t.status !== TaskStatus.WIP).map((t) => {
|
return tasks.filter(t => ids.includes(t.id_) && t.status !== TaskStatus.WIP).map((t) => {
|
||||||
t.status = TaskStatus.WIP
|
t.status = TaskStatus.WIP
|
||||||
t.logs = (t.logs || []).concat({
|
t.logs = (t.logs || []).concat({
|
||||||
start: Date.now(),
|
start: DateTime.now().toUTC().toString(),
|
||||||
end: 0,
|
|
||||||
})
|
})
|
||||||
return t
|
return t
|
||||||
})
|
})
|
||||||
@@ -37,10 +37,7 @@ export function deleteCommand(ids: Task['id_'][], cmd: Command, tasks: Task[]) {
|
|||||||
// Delete by tag
|
// Delete by tag
|
||||||
const tag = (cmd?.id?.match(/^(@.*)/) || []).pop()
|
const tag = (cmd?.id?.match(/^(@.*)/) || []).pop()
|
||||||
if (tag) {
|
if (tag) {
|
||||||
return tasks.filter(t => t.tag === tag).map(t => ({
|
return tasks.filter(t => t.tag === tag)
|
||||||
...t,
|
|
||||||
status: TaskStatus.NONE,
|
|
||||||
} as Task))
|
|
||||||
}
|
}
|
||||||
// Delete by status
|
// Delete by status
|
||||||
const status = (
|
const status = (
|
||||||
@@ -70,20 +67,14 @@ export function deleteCommand(ids: Task['id_'][], cmd: Command, tasks: Task[]) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
if (taskStatus) {
|
if (taskStatus) {
|
||||||
return tasks.filter(t => t.status === taskStatus && !t.archived).map(t => ({
|
return tasks.filter(t => t.status === taskStatus)
|
||||||
...t,
|
|
||||||
status: TaskStatus.NONE,
|
|
||||||
} as Task))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Delete by id
|
// Delete by id
|
||||||
return tasks.filter(t => ids.includes(t.id_)).map(t => ({
|
return tasks.filter(t => ids.includes(t.id_))
|
||||||
...t,
|
|
||||||
status: TaskStatus.NONE,
|
|
||||||
} as Task))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,19 +163,19 @@ export function insertTaskCommand(cmd: Command) {
|
|||||||
return {
|
return {
|
||||||
tag,
|
tag,
|
||||||
title: task,
|
title: task,
|
||||||
dueDate: null,
|
due_date: null,
|
||||||
} as Pick<Task, 'title' | 'dueDate' | 'tag'>
|
} as Pick<Task, 'title' | 'due_date' | 'tag'>
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function editTaskCommand(ids: Task['id_'][], cmd: Command, tasks: Task[]) {
|
export function editTaskCommand(ids: Task['id_'][], cmd: Command, tasks: Task[]) {
|
||||||
const id = ids[0]
|
const id = ids[0]
|
||||||
const task = cmd?.text
|
if (cmd) {
|
||||||
if (task && task.length) {
|
return tasks.filter(t => t.id_ === id).map(t => ({
|
||||||
return tasks.filter(t => t.id === id).map(t => ({
|
|
||||||
...t,
|
...t,
|
||||||
title: task,
|
title: cmd.text?.length ? cmd.text : t.title,
|
||||||
|
tag: cmd.tag ?? t.tag,
|
||||||
} as Task))
|
} as Task))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,10 +188,10 @@ export function dueCommand(ids: Task['id_'][], cmd: Command, tasks: Task[]) {
|
|||||||
if (id) {
|
if (id) {
|
||||||
return tasks.filter(t => t.id_ === id).map((t: Task) => {
|
return tasks.filter(t => t.id_ === id).map((t: Task) => {
|
||||||
if (/^(?:clear|none|remove)$/i.test(text)) {
|
if (/^(?:clear|none|remove)$/i.test(text)) {
|
||||||
t.dueDate = null
|
t.due_date = null
|
||||||
}
|
}
|
||||||
else if (text && text.length) {
|
else if (text && text.length) {
|
||||||
t.dueDate = parseDueDate(text)
|
t.due_date = parseDueDate(text)
|
||||||
}
|
}
|
||||||
return t
|
return t
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,27 +1,29 @@
|
|||||||
import type { Task } from '../types.ts'
|
import type { Task } from '../types.ts'
|
||||||
|
import { DateTime } from 'luxon'
|
||||||
import Sherlock from 'sherlockjs'
|
import Sherlock from 'sherlockjs'
|
||||||
|
|
||||||
export function parseDueDate(text: string): number | null {
|
export function parseDueDate(text: string): Task['due_date'] {
|
||||||
try {
|
try {
|
||||||
const parsed = Sherlock.parse(text)
|
const parsed = Sherlock.parse(text)
|
||||||
if (parsed && parsed.startDate) {
|
if (parsed && parsed.startDate) {
|
||||||
return parsed.startDate.getTime()
|
return DateTime.fromMillis(parsed.startDate.getTime()).toUTC().toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch {}
|
catch {}
|
||||||
const ts = Date.parse(text)
|
const ts = Date.parse(text)
|
||||||
return Number.isNaN(ts) ? null : ts
|
const millis = Number.isNaN(ts) ? null : ts
|
||||||
|
return millis ? DateTime.fromMillis(millis).toUTC().toString() : null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stopWorkLogging(t: Task) {
|
export function stopWorkLogging(t: Task) {
|
||||||
if (t.logs && t.logs.length) {
|
if (t.logs && t.logs.length) {
|
||||||
const lastLog = t.logs[t.logs.length - 1]
|
const lastLog = t.logs[t.logs.length - 1]
|
||||||
if (lastLog.start && !lastLog.end) {
|
if (lastLog.start && !lastLog.end) {
|
||||||
lastLog.end = Date.now()
|
lastLog.end = DateTime.now().toUTC().toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
t.logs = []
|
t.logs = null
|
||||||
}
|
}
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ function parseTaskCommand(str: string) {
|
|||||||
return str.match(/^(t(?:ask)?)\s(@\S*[0-9a-z'-])?([\s\S]*)/i)
|
return str.match(/^(t(?:ask)?)\s(@\S*[0-9a-z'-])?([\s\S]*)/i)
|
||||||
}
|
}
|
||||||
function parseEditCommand(str: string) {
|
function parseEditCommand(str: string) {
|
||||||
return str.match(/^(e(?:dit)?)\s(\d+)([\s\S]*)/i)
|
return str.match(/^(e(?:dit)?)\s(\d+)\s(@\S*[0-9a-z'-])?([\s\S]*)/i)
|
||||||
}
|
}
|
||||||
const parseDueCommand = (str: string) => str.match(/^(due)\s(\d+)([\s\S]*)/i)
|
const parseDueCommand = (str: string) => str.match(/^(due)\s(\d+)([\s\S]*)/i)
|
||||||
function parseMoveCommand(str: string) {
|
function parseMoveCommand(str: string) {
|
||||||
@@ -72,7 +72,8 @@ function compileEditCommand(input: string) {
|
|||||||
return {
|
return {
|
||||||
command: matchEdit[1],
|
command: matchEdit[1],
|
||||||
id: matchEdit[2],
|
id: matchEdit[2],
|
||||||
text: matchEdit[3].trim(),
|
tag: matchEdit[3],
|
||||||
|
text: matchEdit[4].trim(),
|
||||||
} as Command
|
} as Command
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
|||||||
Reference in New Issue
Block a user