Sync tasks with pod
Tim Izzo tim@octree.ch
Sat, 11 Jan 2025 20:04:34 +0100
13 files changed,
132 insertions(+),
130 deletions(-)
M
package.json
→
package.json
@@ -16,7 +16,6 @@ "@inrupt/solid-client": "^2.1.2",
"@inrupt/solid-client-authn-node": "^2.3.0", "@inrupt/vocab-common-rdf": "^1.0.5", "daisyui": "^4.12.23", - "dexie": "^4.0.10", "uuid": "^11.0.4" }, "devDependencies": {
M
src/app.d.ts
→
src/app.d.ts
@@ -12,10 +12,10 @@ // interface Platform {}
} interface Task { - id: number; + id: string; + name: string; done?: boolean; - dueDate?: Date; - content: string; + dueDate?: Date | null; } }
D
src/lib/db.ts
@@ -1,13 +0,0 @@
-import Dexie, { type EntityTable } from 'dexie' - -export const db = new Dexie('tasks') as Dexie & { - tasks: EntityTable<Task, 'id'> -} - -db.version(1).stores({ - tasks: '++id, content, done, dueDate' -}) - -db.open().catch(err => { - console.error('Failed to open db: ' + err.stack) -})
M
src/lib/models/task.ts
→
src/lib/models/task.ts
@@ -1,5 +1,12 @@
-import { getStringNoLocale, getUrl, getDatetime, type Thing } from '@inrupt/solid-client'; -import { SCHEMA_INRUPT } from '@inrupt/vocab-common-rdf'; +import { + getStringNoLocale, + getUrl, + getDatetime, + type Thing, + createThing +} from '@inrupt/solid-client'; +import { RDF, SCHEMA_INRUPT } from '@inrupt/vocab-common-rdf'; +import { buildThing } from '@inrupt/solid-client'; const SCHEMA_CUSTOM = { // Types@@ -16,21 +23,23 @@ done: 'http://schema.org/CompletedActionStatus'
} }; -export class Task { - type = SCHEMA_CUSTOM.ScheduleAction; - done: boolean; - name: string | null; - datetime: Date | null; +export const thingToTask = (taskThing: Thing) => ({ + type: SCHEMA_CUSTOM.ScheduleAction, + id: taskThing.url, + name: getStringNoLocale(taskThing, SCHEMA_INRUPT.name) || '', + done: getUrl(taskThing, SCHEMA_CUSTOM.actionStatus) === SCHEMA_CUSTOM.ActionStatusType.done, + dueDate: getDatetime(taskThing, SCHEMA_CUSTOM.scheduledTime) +}); - constructor(taskInput: Thing) { - this.name = getStringNoLocale(taskInput, SCHEMA_INRUPT.name); - this.done = - getUrl(taskInput, SCHEMA_CUSTOM.actionStatus) === SCHEMA_CUSTOM.ActionStatusType.done; - this.datetime = getDatetime(taskInput, SCHEMA_CUSTOM.scheduledTime); - } +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( + SCHEMA_CUSTOM.actionStatus, + task.done ? SCHEMA_CUSTOM.ActionStatusType.done : SCHEMA_CUSTOM.ActionStatusType.active + ); + if (task.dueDate) taskThing.addDatetime(SCHEMA_CUSTOM.scheduledTime, task.dueDate); - serialize() { - // TODO could be improved - return JSON.parse(JSON.stringify(this)); - } -} + return taskThing.build(); +};
M
src/lib/solid.ts
→
src/lib/solid.ts
@@ -1,14 +1,5 @@
-import { - getSolidDataset, - getStringNoLocale, - getThingAll, - getUrl, - toRdfJsDataset -} from '@inrupt/solid-client'; +import { getSolidDataset, getStringNoLocale, getThingAll, getUrl } from '@inrupt/solid-client'; import { FOAF, VCARD } from '@inrupt/vocab-common-rdf'; -import jsonld from 'jsonld'; -import { JsonLdSerializer } from 'jsonld-streaming-serializer'; -import d from 'stream-to-string'; export const fetchPod: typeof fetch = async (resource: URL | RequestInfo) => { const response = await fetch(`/solid/fetch?resource=${encodeURIComponent(resource.toString())}`);
M
src/routes/(auth)/+page.server.ts
→
src/routes/(auth)/+page.server.ts
@@ -1,6 +1,6 @@
import { getSolidDataset, getThingAll } from '@inrupt/solid-client'; import type { PageServerLoad } from './$types'; -import { Task } from '$lib/models/task'; +import { thingToTask } from '$lib/models/task'; export const load: PageServerLoad = async (event) => { const session = await event.locals.getSession();@@ -9,9 +9,6 @@ const dataset = await getSolidDataset(iri, {
fetch: session.fetch }); const taskThings = getThingAll(dataset); - const tasks = taskThings.map((thing) => new Task(thing)).map((task) => task.serialize()); - - console.log(tasks); - - return { tasks }; + const tasks = taskThings.map(thingToTask); + return { tasks, iri }; };
M
src/routes/(auth)/+page.svelte
→
src/routes/(auth)/+page.svelte
@@ -1,26 +1,42 @@
<script lang="ts"> - import { db } from '$lib/db'; - import { liveQuery } from 'dexie'; import NewTask from './NewTask.svelte'; - import Task from './Task.svelte'; + import TaskItem from './TaskItem.svelte'; import type { PageData } from './$types'; + import tasksStore from './tasksStore'; + import { browser } from '$app/environment'; + import { createSolidDataset, saveSolidDatasetAt, setThing } from '@inrupt/solid-client'; + import { taskToThing } from '$lib/models/task'; + import { fetchPod } from '$lib/solid'; let { data }: { data: PageData } = $props(); - let tasks = liveQuery(() => db.tasks.toArray()); + tasksStore.set( + data?.tasks?.map((task) => ({ + ...task, + dueDate: task.dueDate ? new Date(task.dueDate) : null + })) || [] + ); + + // Sync with SOLID pod + if (browser) + tasksStore.subscribe(async (tasks) => { + let dataset = createSolidDataset(); + for (const task of tasks) { + const taskThing = taskToThing(task); + dataset = setThing(dataset, taskThing); + } + const savedSolidDataset = await saveSolidDatasetAt(data.iri, dataset, { + fetch: fetchPod + }); + console.log({ savedSolidDataset }); + }); </script> <div class="flex flex-col gap-4 py-4"> - {#each $tasks as task (task.id)} - <Task {task} /> + {#each $tasksStore as task (task.id)} + <TaskItem {task} /> {:else} <p class="text-center">Aucune tâche actuellement</p> {/each} </div> - -<ul> - {#each data.tasks as task} - <li>{task.name} {task.done} {task.datetime}</li> - {/each} -</ul> <NewTask />
M
src/routes/(auth)/NewTask.svelte
→
src/routes/(auth)/NewTask.svelte
@@ -1,12 +1,12 @@
<script lang="ts"> - import { db } from '$lib/db'; + import tasksStore from './tasksStore'; const onKeyDown = (e: KeyboardEvent) => { const element = e.target as HTMLInputElement; const inputValue = element?.value; const enterKeys = ['Enter', 'NumpadEnter']; if (inputValue && enterKeys.includes(e.code)) { - db.tasks.add({ content: inputValue }); + tasksStore.addTask({ name: inputValue }); element.value = ''; } };
D
src/routes/(auth)/Task.svelte
@@ -1,57 +0,0 @@
-<script lang="ts"> - import { db } from '$lib/db'; - import { debounce } from '$lib/utils'; - import type { FormEventHandler } from 'svelte/elements'; - - let { task }: { task: Task } = $props(); - - const parseContent = (content: string) => - content.replace(/#((\w|-)+)/g, '<span class="hashtag">#$1</span>'); - - const onUpdateContent: FormEventHandler<HTMLDivElement> = (e) => { - const element = e.target as HTMLElement; - const content = element.innerText?.trim(); - if (content) db.tasks.update(task.id, { content }); - else db.tasks.delete(task.id); - }; - - const onUpdateDone: FormEventHandler<HTMLInputElement> = (e) => { - const element = e.target as HTMLInputElement; - const done = element.checked; - db.tasks.update(task.id, { done }); - }; - - const onUpdateDueDate: FormEventHandler<HTMLInputElement> = (e) => { - const element = e.target as HTMLInputElement; - const rawDate = element.value; - let dueDate; - if (rawDate) dueDate = new Date(rawDate); - db.tasks.update(task.id, { dueDate }); - }; -</script> - -<div class="flex items-center justify-between gap-2"> - <div class="flex items-center gap-2"> - <input - type="checkbox" - class="checkbox-primary checkbox checkbox-sm" - checked={task.done} - onchange={onUpdateDone} - /> - <div - contenteditable - oninput={debounce(onUpdateContent, 1000)} - class:opacity-50={task.done} - class:line-through={task.done} - > - {@html parseContent(task.content)} - </div> - </div> - <input - type="date" - value={task.dueDate?.toISOString().split('T')[0]} - onchange={onUpdateDueDate} - class:text-red-400={task.dueDate && task.dueDate < new Date()} - class:text-primary={task.dueDate && task.dueDate >= new Date()} - /> -</div>
A
src/routes/(auth)/TaskItem.svelte
@@ -0,0 +1,46 @@
+<script lang="ts"> + import { debounce } from '$lib/utils'; + import type { FormEventHandler } from 'svelte/elements'; + import tasksStore from './tasksStore'; + + let { task }: { task: Task } = $props(); + + const parseContent = (content: string) => + content.replace(/#((\w|-)+)/g, '<span class="hashtag">#$1</span>'); + + const onUpdateName: FormEventHandler<HTMLDivElement> = (event) => { + const element = event.target as HTMLElement; + const newValue = element.innerText?.trim(); + tasksStore.updateTask(task.id, { name: newValue }); + }; + + const onUpdateDone: FormEventHandler<HTMLInputElement> = (e) => { + const element = e.target as HTMLInputElement; + const done = element.checked; + tasksStore.updateTask(task.id, { done }); + }; + + const onUpdateDueDate: FormEventHandler<HTMLInputElement> = (e) => { + const element = e.target as HTMLInputElement; + const rawDate = element.value; + let dueDate; + if (rawDate) dueDate = new Date(rawDate); + tasksStore.updateTask(task.id, { dueDate }); + }; +</script> + +<div class="flex items-center gap-4 rounded border p-2" id={task.id}> + <input type="checkbox" class="checkbox mt-1" checked={task.done} onchange={onUpdateDone} /> + <div class="flex w-full justify-between"> + <div contenteditable oninput={debounce(onUpdateName, 750)}> + {@html parseContent(task.name)} + </div> + <input + type="date" + value={task.dueDate?.toISOString().split('T')[0]} + onchange={onUpdateDueDate} + class:text-red-400={task.dueDate && task.dueDate < new Date()} + class:text-primary={task.dueDate && task.dueDate >= new Date()} + /> + </div> +</div>
A
src/routes/(auth)/tasksStore.ts
@@ -0,0 +1,22 @@
+import { writable } from 'svelte/store'; + +const { set, update, subscribe } = writable<Task[]>([]); + +const addTask = (task: Task) => { + update((tasks) => [...tasks, { ...task, id: `https://pod.5ika.ch/tasks/default#${Date.now()}` }]); +}; + +const updateTask = (taskId: string, taskUpdate: Partial<Task>) => { + update((tasks) => + tasks.map((task) => { + if (task.id === taskId) return { ...task, ...taskUpdate }; + return task; + }) + ); +}; + +const deleteTask = (taskId: string) => { + update((tasks) => tasks.filter((task) => task.id !== taskId)); +}; + +export default { set, update, subscribe, addTask, updateTask, deleteTask };
M
yarn.lock
→
yarn.lock
@@ -1365,13 +1365,6 @@ checksum: 10c0/f6717a856fd54216959abd341cb189e47a9b37d72d8419e055ae77567ff4ed0fb683b1ffb6a71067f645adae5991bffabe6468a3e2385937bff49273e71c1f51
languageName: node linkType: hard -"dexie@npm:^4.0.10": - version: 4.0.10 - resolution: "dexie@npm:4.0.10" - checksum: 10c0/7e5cbc79947fd830918679f8621bceb4e543f139eb8ec73d5a9605927e5d659ea306a9649bbac7c37d55e623888daaf416f9868422badebc26049c9c2ebf1dfb - languageName: node - linkType: hard - "didyoumean@npm:^1.2.2": version: 1.2.2 resolution: "didyoumean@npm:1.2.2"@@ -2314,7 +2307,6 @@ "@sveltejs/vite-plugin-svelte": "npm:^5.0.3"
"@types/eslint": "npm:^9.6.1" autoprefixer: "npm:^10.4.20" daisyui: "npm:^4.12.23" - dexie: "npm:^4.0.10" eslint: "npm:^9.17.0" eslint-config-prettier: "npm:^9.1.0" eslint-plugin-svelte: "npm:^2.46.1"