✨ Add scheduling on tasks
jump to
@@ -3,7 +3,7 @@
## Tâches - [x] Améliorer l'input pour l'ajout d'une tâche -- [ ] Permettre d'entrer des tâches récurrentes +- [x] Permettre d'entrer des tâches récurrentes - [ ] Ajouter un raccourci clavier pour checker une tâche - [ ] Essayer une connexion OIDC sur ActivityPods - [ ] Champ de recherche
@@ -1,9 +1,11 @@
-<!doctype html> -<html lang="en"> +<!DOCTYPE html> +<html lang="fr" data-theme="catppuccin-mocha"> <head> <meta charset="UTF-8" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <meta + name="viewport" + content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content" /> <title>kōi</title> </head> <body>
@@ -21,7 +21,7 @@ </script>
<div class="join w-full"> <input - class="input input-bordered w-full join-item" + class="input w-full join-item" type="text" placeholder="Ajouter une tâche" onkeydown={onKeyDown}
@@ -0,0 +1,93 @@
+<script lang="ts"> + import { DateTime } from "luxon"; + import tasksStore from "../store/tasksStore"; + + let { task }: { task: Task } = $props(); + + let modalRef = $state<HTMLDialogElement>(); + let datetime = $derived( + task.dueDate ? DateTime.fromJSDate(task.dueDate) : DateTime.now() + ); + let hasSchedule = $derived( + !!task.byDay || !!task.byMonth || !!task.byMonthDay || !!task.byMonthWeek + ); + let weekNumberInMonth = $derived( + Math.ceil((datetime.day + datetime.startOf("month").weekday) / 7) + ); + + const onRemove = () => { + tasksStore.updateTask(task.id, { + byDay: undefined, + byMonth: undefined, + byMonthDay: undefined, + byMonthWeek: undefined, + }); + modalRef?.close(); + }; + + const onPickWeekDay = () => { + tasksStore.updateTask(task.id, { + byDay: datetime.toFormat("EEEE"), + byMonth: undefined, + byMonthDay: undefined, + byMonthWeek: undefined, + }); + modalRef?.close(); + }; + + const onPickMonthDay = () => { + tasksStore.updateTask(task.id, { + byDay: undefined, + byMonth: undefined, + byMonthDay: datetime.day, + byMonthWeek: undefined, + }); + modalRef?.close(); + }; + + const onPickMonthWeek = () => { + tasksStore.updateTask(task.id, { + byDay: datetime.toFormat("EEEE"), + byMonth: undefined, + byMonthDay: undefined, + byMonthWeek: weekNumberInMonth, + }); + modalRef?.close(); + }; +</script> + +<button + onclick={() => modalRef?.showModal()} + class="btn btn-ghost btn-circle btn-sm" + class:text-secondary={hasSchedule}>⟳</button +> +<dialog bind:this={modalRef} class="modal"> + <div class="dropdown-content menu w-96 rounded-box bg-base-100 shadow gap-1"> + <button + class="btn btn-soft" + class:btn-primary={task.byDay && !task.byMonthWeek} + onclick={onPickWeekDay} + >Chaque {datetime.setLocale("fr").toFormat("EEEE")}</button + > + <button + class="btn btn-soft" + class:btn-primary={task.byMonthDay} + onclick={onPickMonthDay}>Tous les {datetime.day} du mois</button + > + <button + class="btn btn-soft" + class:btn-primary={task.byDay && task.byMonthWeek} + onclick={onPickMonthWeek} + >Chaque {weekNumberInMonth}e {datetime.setLocale("fr").toFormat("EEEE")} du + mois</button + > + {#if hasSchedule} + <button class="btn btn-soft" onclick={onRemove} + >Supprimer la récurrence</button + > + {/if} + <button class="btn btn-ghost" onclick={() => modalRef?.close()} + >Annuler</button + > + </div> +</dialog>
@@ -0,0 +1,12 @@
+<script lang="ts"> + import filterStore from "../store/filterStore"; +</script> + +<div class="py-6"> + <input + type="text" + class="input w-full" + placeholder="Recherche" + bind:value={$filterStore.search} + /> +</div>
@@ -3,6 +3,8 @@ import type { FormEventHandler } from "svelte/elements";
import tasksStore from "../store/tasksStore"; import { debounce } from "../utils"; import { getShortcutActions } from "./taskShortcuts.svelte"; + import Scheduler from "./Scheduler.svelte"; + import { DateTime } from "luxon"; let { task }: { task: Task } = $props();@@ -62,16 +64,20 @@ onkeyup={onKeyUp}
> {@html parseContent(task.name)} </div> - <input - type="date" - value={task.dueDate?.toISOString().split("T")[0]} - onchange={onUpdateDueDate} - disabled={task.done} - class:text-red-400={!task.done && - task.dueDate && - task.dueDate < new Date()} - class:text-primary={task.dueDate && task.dueDate >= new Date()} - class="rounded px-1 h-fit" - /> + <div class="flex items-center gap-2"> + <input + type="date" + value={task.dueDate?.toISOString().split("T")[0]} + onchange={onUpdateDueDate} + disabled={task.done} + class:text-red-400={!task.done && + task.dueDate && + DateTime.fromJSDate(task.dueDate).endOf("day") <= + DateTime.now().startOf("day")} + class:text-primary={task.dueDate && task.dueDate >= new Date()} + class="rounded px-1 h-fit" + /> + <Scheduler {task} /> + </div> </div> </li>
@@ -26,7 +26,7 @@ •{/if}
{title}</summary > <ul class="py-4 list collapse-content"> - {#each sortedTasks as task} + {#each sortedTasks as task (task.id)} <TaskItem {task} /> {/each} </ul>
@@ -4,6 +4,7 @@ getUrl,
getDatetime, type Thing, createThing, + getInteger, } from "@inrupt/solid-client"; import { RDF, SCHEMA_INRUPT } from "@inrupt/vocab-common-rdf"; import { buildThing } from "@inrupt/solid-client";@@ -11,10 +12,15 @@
const SCHEMA_CUSTOM = { // Types ScheduleAction: "http://schema.org/ScheduleAction", + Schedule: "http://schema.org/Schedule", // Properties scheduledTime: "http://schema.org/scheduledTime", actionStatus: "http://schema.org/actionStatus", + byDay: "http://schema.org/byDay", + byMonth: "http://schema.org/byMonth", + byMonthDay: "http://schema.org/byMonthDay", + byMonthWeek: "http://schema.org/byMonthWeek", // Enumeration types ActionStatusType: {@@ -24,7 +30,7 @@ },
}; export const thingToTask = (taskThing: Thing) => ({ - type: SCHEMA_CUSTOM.ScheduleAction, + type: [SCHEMA_CUSTOM.ScheduleAction, SCHEMA_CUSTOM.Schedule], id: taskThing.url, name: getStringNoLocale(taskThing, SCHEMA_INRUPT.name) || "", done:@@ -33,12 +39,19 @@ SCHEMA_CUSTOM.ActionStatusType.done,
dueDate: getDatetime(taskThing, SCHEMA_CUSTOM.scheduledTime), startTime: getDatetime(taskThing, SCHEMA_INRUPT.startTime), endTime: getDatetime(taskThing, SCHEMA_INRUPT.endTime), + + // Periodicity + byDay: getStringNoLocale(taskThing, SCHEMA_CUSTOM.byDay), + byMonth: getInteger(taskThing, SCHEMA_CUSTOM.byDay), + byMonthDay: getInteger(taskThing, SCHEMA_CUSTOM.byDay), + byMonthWeek: getInteger(taskThing, SCHEMA_CUSTOM.byDay), }); export const taskToThing = (task: Task) => { const taskThing = buildThing(createThing({ url: task.id })) .addStringNoLocale(SCHEMA_INRUPT.name, task.name) .addUrl(RDF.type, SCHEMA_CUSTOM.ScheduleAction) + .addUrl(RDF.type, SCHEMA_CUSTOM.Schedule) .addUrl( SCHEMA_CUSTOM.actionStatus, task.done@@ -50,5 +63,11 @@ taskThing.addDatetime(SCHEMA_CUSTOM.scheduledTime, task.dueDate);
if (task.startTime) taskThing.addDatetime(SCHEMA_INRUPT.startTime, task.startTime); if (task.endTime) taskThing.addDatetime(SCHEMA_INRUPT.endTime, task.endTime); + if (task.byDay) taskThing.addStringNoLocale(SCHEMA_CUSTOM.byDay, task.byDay); + if (task.byMonth) taskThing.addInteger(SCHEMA_CUSTOM.byMonth, task.byMonth); + if (task.byMonthDay) + taskThing.addInteger(SCHEMA_CUSTOM.byMonthDay, task.byMonthDay); + if (task.byMonthWeek) + taskThing.addInteger(SCHEMA_CUSTOM.byMonthWeek, task.byMonthWeek); return taskThing.build(); };
@@ -0,0 +1,11 @@
+import { writable } from "svelte/store"; + +interface Filters { + search: string; +} + +const filterStore = writable<Filters>({ + search: "", +}); + +export default filterStore;
@@ -1,5 +1,5 @@
import { get, writable } from "svelte/store"; -import { debounce } from "../utils"; +import { debounce, WEEKDAYS } from "../utils"; import { taskToThing } from "../models/task"; import { getThingAll,@@ -10,10 +10,12 @@ } from "@inrupt/solid-client";
import { fetch } from "@inrupt/solid-client-authn-browser"; import podInfo from "./podInfoStore"; import { addToast } from "./toastStore"; +import { DateTime } from "luxon"; const DEBOUNCE_TIME_SYNC = 2000; -const { set, update, subscribe } = writable<Task[]>([]); +const taskStore = writable<Task[]>([]); +const { update, set, subscribe } = taskStore; const addTask = (task: Omit<Task, "id">) => { const podUrl = get(podInfo).podUrl;@@ -30,12 +32,40 @@ if (task.id === taskId) return { ...task, ...taskUpdate };
return task; }) ); + + if (taskUpdate.done) { + const task = get(taskStore).find(item => item.id === taskId); + const now = DateTime.now(); + const datetime = task?.dueDate + ? DateTime.fromJSDate(task.dueDate) + : now + let weekday = WEEKDAYS[task?.byDay as string]; + let dueDate: DateTime | null = null; + if (task?.byDay && !task.byMonthWeek) { + dueDate = datetime.set({ weekday }); + if (dueDate <= now) dueDate = dueDate.plus({ week: 1 }) + } + else if (task?.byMonthDay) { + dueDate = datetime.set({ day: task.byMonthDay }) + if (dueDate <= now) dueDate = dueDate.plus({ month: 1 }) + } + else if (task?.byDay && task?.byMonthWeek) { + dueDate = datetime.set({ weekday, weekNumber: task.byMonthWeek }) + if (dueDate <= now) dueDate = dueDate.plus({ month: 1 }) + } + + if (dueDate) { + console.log(`Tâche replanifiée pour le ${dueDate.toISODate()}`) + addTask({ ...task, dueDate: dueDate.toJSDate(), done: false, endTime: undefined }); + } + } }; const deleteTask = (taskId: string) => { update(tasks => tasks.filter(task => task.id !== taskId)); }; +// Sync tasks with pod data let init = false; subscribe( debounce(async (tasks: Task[]) => {
@@ -1,3 +1,14 @@
+export const WEEKDAYS = { + "Monday": 1, + "Tuesday": 2, + "Wednesday": 3, + "Thursday": 4, + "Friday": 5, + "Saturday": 6, + "Sunday": 7 +} + + export const debounce = (callback: Function, wait = 300) => { let timeout: ReturnType<typeof setTimeout>;
@@ -8,6 +8,7 @@ import { onMount } from "svelte";
import TasksFeed from "../lib/components/TasksFeed.svelte"; import NewTask from "../lib/components/NewTask.svelte"; import tasksStore from "../lib/store/tasksStore"; + import filterStore from "../lib/store/filterStore"; import podInfo from "../lib/store/podInfoStore"; import { getPodUrlAll,@@ -16,9 +17,17 @@ getThingAll,
} from "@inrupt/solid-client"; import Profile from "../lib/components/Profile.svelte"; import { thingToTask } from "../lib/models/task"; + import Search from "../lib/components/Search.svelte"; let { navigate }: PageProps = $props(); let sessionInfo = $state(getDefaultSession()?.info); + let filteredTasks = $derived( + $tasksStore.filter(task => + $filterStore.search + ? task.name?.toLowerCase().includes($filterStore.search) + : true + ) + ); onMount(async () => { const info = await handleIncomingRedirect({@@ -54,7 +63,8 @@ <div class="flex justify-between">
<h1 class="text-2xl mb-4">kōi</h1> <Profile /> </div> - <TasksFeed tasks={$tasksStore} /> + <Search /> + <TasksFeed tasks={filteredTasks} /> </main> <div class="fixed bottom-0 left-0 right-0 p-4">
@@ -12,4 +12,10 @@ done?: boolean;
startTime: Date; dueDate?: Date | null; endTime?: Date | null; + + // Periodicity + byDay?: WeekDay; + byMonth?: number; + byMonthDay?: number; + byMonthWeek?: number; }