Sync with solid pod
Tim Izzo tim@octree.ch
Fri, 31 Jan 2025 17:37:54 +0100
13 files changed,
174 insertions(+),
47 deletions(-)
jump to
M
deno.lock
→
deno.lock
@@ -3,6 +3,7 @@ "version": "4",
"specifiers": { "npm:@inrupt/solid-client-authn-browser@^2.3.0": "2.3.0", "npm:@inrupt/solid-client@^2.1.2": "2.1.2", + "npm:@inrupt/vocab-common-rdf@^1.0.5": "1.0.5", "npm:@sveltejs/vite-plugin-svelte@^5.0.3": "5.0.3_svelte@5.17.3__acorn@8.14.0_vite@6.0.7", "npm:@tailwindcss/vite@^4.0.1": "4.0.1_vite@6.0.7_lightningcss@1.29.1", "npm:@tsconfig/svelte@^5.0.4": "5.0.4",@@ -155,6 +156,9 @@ "jsonld-streaming-parser",
"n3", "uuid@10.0.0" ] + }, + "@inrupt/vocab-common-rdf@1.0.5": { + "integrity": "sha512-onrehQte8m0XW83WwM6T4o5WgmYGzdYUCef6FDjMKfQCF64FnARFNn5fYhKodimBaAOFKhuJ3vw1NBZV/EYqJw==" }, "@jridgewell/gen-mapping@0.3.8": { "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",@@ -810,6 +814,7 @@ "packageJson": {
"dependencies": [ "npm:@inrupt/solid-client-authn-browser@^2.3.0", "npm:@inrupt/solid-client@^2.1.2", + "npm:@inrupt/vocab-common-rdf@^1.0.5", "npm:@sveltejs/vite-plugin-svelte@^5.0.3", "npm:@tailwindcss/vite@^4.0.1", "npm:@tsconfig/svelte@^5.0.4",
M
package.json
→
package.json
@@ -21,6 +21,7 @@ "packageManager": "yarn@4.6.0",
"dependencies": { "@inrupt/solid-client": "^2.1.2", "@inrupt/solid-client-authn-browser": "^2.3.0", + "@inrupt/vocab-common-rdf": "^1.0.5", "@tailwindcss/vite": "^4.0.1", "daisyui": "^5.0.0-beta.6", "tailwindcss": "^4.0.1"
M
src/App.svelte
→
src/App.svelte
@@ -1,6 +1,7 @@
<script lang="ts"> import Home from "./pages/Home.svelte"; import Login from "./pages/Login.svelte"; + import ToastContainer from "./lib/components/ToastContainer.svelte"; let currentPage = $state(window.location.pathname);@@ -17,3 +18,5 @@ <Login {navigate} />
{:else} <Home {navigate} /> {/if} + +<ToastContainer />
M
src/lib/components/TaskItem.svelte
→
src/lib/components/TaskItem.svelte
@@ -29,15 +29,20 @@ tasksStore.updateTask(task.id, { dueDate });
}; </script> -<div class="flex items-center gap-4 rounded border p-2" id={task.id}> +<li class="list-row flex items-center gap-4 p-2" id={task.id}> <input type="checkbox" - class="checkbox mt-1" + class="checkbox" checked={task.done} onchange={onUpdateDone} /> <div class="flex w-full justify-between"> - <div contenteditable oninput={debounce(onUpdateName, 750)}> + <div + contenteditable + autocorrect="off" + oninput={debounce(onUpdateName, 750)} + class:line-through={task.done} + > {@html parseContent(task.name)} </div> <input@@ -46,6 +51,7 @@ 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()} + class="rounded px-1" /> </div> -</div> +</li>
M
src/lib/components/Tasks.svelte
→
src/lib/components/Tasks.svelte
@@ -4,8 +4,8 @@
let { tasks = [] }: { tasks: Task[] } = $props(); </script> -<div class="flex flex-col gap-4 py-4"> +<ul class="py-4 list"> {#each tasks as task} <TaskItem {task} /> {/each} -</div> +</ul>
A
src/lib/components/Toast.svelte
@@ -0,0 +1,25 @@
+<script lang="ts"> + import { removeToast, type Toast } from "../store/toastStore"; + + const TOAST_TIME = 4000; + + let { toast }: { toast: Toast } = $props(); + + setTimeout(() => { + removeToast(toast.id); + }, TOAST_TIME); + + const colorNames = { + info: "alert-info", + error: "alert-error", + success: "alert-success", + warning: "alert-warning", + }; +</script> + +<button + class={`alert ${colorNames[toast.type]} min-w-40`} + onclick={() => removeToast(toast.id)} +> + <span>{toast.message}</span> +</button>
A
src/lib/components/ToastContainer.svelte
@@ -0,0 +1,20 @@
+<script lang="ts"> + import Toast from "./Toast.svelte"; + import { addToast, toastStore } from "../store/toastStore"; + import { onMount } from "svelte"; + + onMount(() => { + const storedToast = sessionStorage.getItem("toast"); + if (storedToast) { + const { message, type } = JSON.parse(storedToast); + addToast(message, type); + sessionStorage.removeItem("toast"); + } + }); +</script> + +<div class="toast toast-end"> + {#each $toastStore as toast} + <Toast {toast} /> + {/each} +</div>
M
src/lib/solid.ts
→
src/lib/solid.ts
@@ -6,15 +6,6 @@ getUrl,
} from "@inrupt/solid-client"; import { fetch, getDefaultSession } from "@inrupt/solid-client-authn-browser"; import { FOAF, VCARD } from "@inrupt/vocab-common-rdf"; -import { thingToTask } from "./models/task"; - -export const getPodTasks = async (podUrl: string) => { - const tasksDataset = await getSolidDataset(`${podUrl}/tasks/default`, { - fetch: fetch, - }); - const things = getThingAll(tasksDataset); - return things.map(thingToTask); -}; export const getProfile = async () => { const session = getDefaultSession();
M
src/lib/store/podInfoStore.ts
→
src/lib/store/podInfoStore.ts
@@ -1,7 +1,12 @@
+import type { + SolidDataset, + WithServerResourceInfo, +} from "@inrupt/solid-client"; import { writable } from "svelte/store"; interface PodInfo { podUrl: string | null; + tasksDataset?: SolidDataset & WithServerResourceInfo; } const { set, update, subscribe } = writable<PodInfo>({
M
src/lib/store/tasksStore.ts
→
src/lib/store/tasksStore.ts
@@ -1,11 +1,20 @@
-import { writable } from "svelte/store"; +import { get, writable } from "svelte/store"; +import { debounce } from "../utils"; +import { taskToThing } from "../models/task"; +import { saveSolidDatasetAt, setThing } from "@inrupt/solid-client"; +import { fetch } from "@inrupt/solid-client-authn-browser"; +import podInfo from "./podInfoStore"; +import { addToast } from "./toastStore"; + +const DEBOUNCE_TIME_SYNC = 2000; const { set, update, subscribe } = writable<Task[]>([]); const addTask = (task: Task) => { + const podUrl = get(podInfo).podUrl; update(tasks => [ ...tasks, - { ...task, id: `https://pod.5ika.ch/tasks/default#${Date.now()}` }, + { ...task, id: `${podUrl}/tasks/default#${Date.now()}` }, ]); };@@ -21,5 +30,29 @@
const deleteTask = (taskId: string) => { update(tasks => tasks.filter(task => task.id !== taskId)); }; + +let init = false; +subscribe( + debounce(async (tasks: Task[]) => { + if (!init) return (init = true); + + let taskDataset = get(podInfo).tasksDataset; + if (!taskDataset) throw new Error("No tasks dataset. Can't write to pod."); + + for (const task of tasks) { + const thing = taskToThing(task); + taskDataset = setThing(taskDataset, thing); + } + + const podUrl = get(podInfo).podUrl; + const savedSolidDataset = await saveSolidDatasetAt( + `${podUrl}/tasks/default`, + taskDataset, + { fetch: fetch } + ); + podInfo.update(store => ({ ...store, tasksDataset: savedSolidDataset })); + addToast("Tâches enregistrées vers le pod."); + }, DEBOUNCE_TIME_SYNC) +); export default { set, update, subscribe, addTask, updateTask, deleteTask };
A
src/lib/store/toastStore.ts
@@ -0,0 +1,29 @@
+import { writable } from "svelte/store"; + +type ToastType = "info" | "error" | "success" | "warning"; + +export interface Toast { + id: number; + message: string; + type: ToastType; +} + +export const toastStore = writable(<Toast[]>[]); + +// Display toast +export const addToast = (message: string, type: ToastType = "info") => { + const id = Math.random(); + const newToast = { id, message, type }; + toastStore.update((toasts: Toast[]) => [...toasts, newToast]); +}; + +// Store toast in session storage and display it at next page load +export const addSessionToast = (message: string, type: ToastType = "info") => { + const toast = JSON.stringify({ message, type }); + sessionStorage.setItem("toast", toast); +}; + +export const removeToast = (id: number) => + toastStore.update((toasts: Toast[]) => + toasts.filter(toast => toast.id !== id) + );
M
src/pages/Home.svelte
→
src/pages/Home.svelte
@@ -7,11 +7,15 @@ } from "@inrupt/solid-client-authn-browser";
import { onMount } from "svelte"; import Tasks from "../lib/components/Tasks.svelte"; import NewTask from "../lib/components/NewTask.svelte"; - import { getPodTasks } from "../lib/solid"; - import tasks from "../lib/store/tasksStore"; + import tasksStore from "../lib/store/tasksStore"; import podInfo from "../lib/store/podInfoStore"; - import { getPodUrlAll } from "@inrupt/solid-client"; + import { + getPodUrlAll, + getSolidDataset, + getThingAll, + } from "@inrupt/solid-client"; import Profile from "../lib/components/Profile.svelte"; + import { thingToTask } from "../lib/models/task"; let { navigate }: PageProps = $props(); let sessionInfo = $state(getDefaultSession()?.info);@@ -31,9 +35,16 @@ const pods = await getPodUrlAll(sessionInfo?.webId, { fetch: fetch });
const firstPod = pods?.[0]; if (firstPod) $podInfo.podUrl = firstPod; - // Load tasks from pod - const podTasks = await getPodTasks(firstPod); - tasks.set(podTasks); + // Load tasks dataset from pod + const tasksDataset = await getSolidDataset(`${firstPod}/tasks/default`, { + fetch: fetch, + }); + $podInfo.tasksDataset = tasksDataset; + + // Init tasks store + const things = getThingAll(tasksDataset); + const tasks = things.map(thingToTask); + tasksStore.set(tasks); } }); </script>@@ -44,9 +55,12 @@ <div class="flex justify-between">
<h1 class="text-2xl mb-4">kōi</h1> <Profile /> </div> - <Tasks tasks={$tasks} /> + <Tasks tasks={$tasksStore} /> <NewTask /> {:else} - <span class="loading loading-spinner loading-xl"></span> + <div class="flex flex-col items-center mt-8 gap-4"> + <span class="loading loading-spinner loading-xl"></span> + <span>Connexion au pod</span> + </div> {/if} </main>
M
src/pages/Login.svelte
→
src/pages/Login.svelte
@@ -1,37 +1,32 @@
<script lang="ts"> - import { - login, - getDefaultSession, - fetch, - } from "@inrupt/solid-client-authn-browser"; - import { getSolidDataset } from "@inrupt/solid-client"; + import { login, getDefaultSession } from "@inrupt/solid-client-authn-browser"; let { navigate }: PageProps = $props(); + let session = getDefaultSession(); - let session = getDefaultSession(); + let oidcIssuer = $state(""); const startLogin = async () => { if (!getDefaultSession().info.isLoggedIn) { await login({ - oidcIssuer: "https://pod.5ika.ch", + oidcIssuer, redirectUrl: new URL("/", window.location.href).toString(), clientName: "kōi SPA", }); } }; - - if (session.isLoggedIn) { - console.log("Fetch dataset"); - getSolidDataset("https://pod.5ika.ch/tasks/", { fetch: fetch }).then( - console.log - ); - } </script> -{#if session.info.isLoggedIn} - {JSON.stringify(session.info, null, 4)} -{:else} - <button onclick={startLogin}>Connexion</button> -{/if} - -<button onclick={session?.logout}>Déconnexion</button> +<main class="w-xl max-w-full mx-auto p-4"> + <h1 class="text-2xl">Connexion</h1> + {#if session.info.isLoggedIn} + {JSON.stringify(session.info, null, 4)} + {:else} + <input + class="input input-bordered" + bind:value={oidcIssuer} + placeholder="Pod URL" + /> + <button class="btn btn-primary" onclick={startLogin}>Connexion</button> + {/if} +</main>