This commit is contained in:
parent
e3b2edd214
commit
b316680114
4 changed files with 113 additions and 20 deletions
48
src/lib/breadcrumbs.ts
Normal file
48
src/lib/breadcrumbs.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import { derived } from "svelte/store";
|
||||||
|
import IdbValStore from "./idbValStore";
|
||||||
|
|
||||||
|
export type PageParams = {
|
||||||
|
worldId: string;
|
||||||
|
pageId: string;
|
||||||
|
};
|
||||||
|
type WorldId = string;
|
||||||
|
type PageId = string;
|
||||||
|
|
||||||
|
class BreadcrumbsStore {
|
||||||
|
idb: IdbValStore<Map<WorldId, PageId[]>>;
|
||||||
|
idbLastWorld: IdbValStore<WorldId>;
|
||||||
|
constructor() {
|
||||||
|
this.idb = new IdbValStore("schreiben-breadcrumbs");
|
||||||
|
this.idbLastWorld = new IdbValStore("schreiben-last-world");
|
||||||
|
}
|
||||||
|
|
||||||
|
async newCrumb({ worldId, pageId }: PageParams) {
|
||||||
|
await this.idbLastWorld.set(worldId);
|
||||||
|
await this.idb.update((map) => {
|
||||||
|
if (!map) map = new Map();
|
||||||
|
let crumbs = map.get(worldId) || [];
|
||||||
|
if (crumbs.includes(pageId)) {
|
||||||
|
crumbs = crumbs.slice(0, crumbs.indexOf(pageId) + 1);
|
||||||
|
} else {
|
||||||
|
crumbs.push(pageId);
|
||||||
|
}
|
||||||
|
map.set(worldId, crumbs);
|
||||||
|
return map;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async lastWorldBreadcrumbs() {
|
||||||
|
const map = await this.idb.get();
|
||||||
|
const worldId = await this.idbLastWorld.get();
|
||||||
|
if (!worldId) return;
|
||||||
|
const breadcrumbs = map?.get(worldId);
|
||||||
|
if (!breadcrumbs) return;
|
||||||
|
return { worldId, breadcrumbs };
|
||||||
|
}
|
||||||
|
|
||||||
|
worldStore(worldId: WorldId) {
|
||||||
|
return derived(this.idb, (map) => map?.get(worldId) || []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new BreadcrumbsStore();
|
|
@ -1,14 +1,37 @@
|
||||||
import { get, set } from "idb-keyval";
|
import { get, set, update } from "idb-keyval";
|
||||||
|
|
||||||
export default class IdbValStore<T> {
|
export default class IdbValStore<T> {
|
||||||
name: string;
|
name: string;
|
||||||
|
subscriptors: ((val: T) => void)[];
|
||||||
constructor(name: string) {
|
constructor(name: string) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
this.subscriptors = [];
|
||||||
}
|
}
|
||||||
get(): Promise<T | undefined> {
|
get(): Promise<T | undefined> {
|
||||||
return get<T>(this.name);
|
return get<T>(this.name);
|
||||||
}
|
}
|
||||||
set(val: T): Promise<void> {
|
async set(val: T): Promise<void> {
|
||||||
return set(this.name, val);
|
await set(this.name, val);
|
||||||
|
this.push(val);
|
||||||
|
}
|
||||||
|
async update(updater: (val: T | undefined) => T): Promise<void> {
|
||||||
|
let newVal: T | undefined = undefined;
|
||||||
|
await update(this.name, (val) => (newVal = updater(val)));
|
||||||
|
if (newVal === undefined) throw new Error("what the fuck");
|
||||||
|
this.push(newVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private push(val: T) {
|
||||||
|
for (const sub of this.subscriptors) sub(val);
|
||||||
|
}
|
||||||
|
subscribe(subscription: (value: T | undefined) => void): () => void {
|
||||||
|
this.subscriptors.push(subscription);
|
||||||
|
(async () => {
|
||||||
|
const val = await this.get();
|
||||||
|
if (this.subscriptors.includes(subscription)) subscription(val);
|
||||||
|
})();
|
||||||
|
return () => {
|
||||||
|
this.subscriptors = this.subscriptors.filter((s) => s !== subscription);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import navaid from "navaid";
|
import navaid from "navaid";
|
||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
import { inject } from "regexparam";
|
|
||||||
|
|
||||||
import ChooseWorld from "../views/ChooseWorld.svelte";
|
import ChooseWorld from "../views/ChooseWorld.svelte";
|
||||||
import CreateWorld from "../views/CreateWorld.svelte";
|
import CreateWorld from "../views/CreateWorld.svelte";
|
||||||
|
@ -9,9 +8,8 @@ import NotFound from "../views/NotFound.svelte";
|
||||||
import Page from "../views/Page.svelte";
|
import Page from "../views/Page.svelte";
|
||||||
import ShareWorld from "../views/ShareWorld.svelte";
|
import ShareWorld from "../views/ShareWorld.svelte";
|
||||||
import { routes } from "./routes";
|
import { routes } from "./routes";
|
||||||
import IdbValStore from "./idbValStore";
|
import breadcrumbs, { type PageParams } from "./breadcrumbs";
|
||||||
|
import { inject } from "regexparam";
|
||||||
export const lastPageStore = new IdbValStore("schreiben-last-page");
|
|
||||||
|
|
||||||
export const currentRoute = writable<{
|
export const currentRoute = writable<{
|
||||||
// XXX: in lack of a better type for Svelte components
|
// XXX: in lack of a better type for Svelte components
|
||||||
|
@ -34,15 +32,19 @@ router.on(routes.ShareWorld, (params) =>
|
||||||
router.on(routes.JoinWorld, (params) =>
|
router.on(routes.JoinWorld, (params) =>
|
||||||
currentRoute.set({ component: JoinWorld, params }),
|
currentRoute.set({ component: JoinWorld, params }),
|
||||||
);
|
);
|
||||||
router.on(routes.Page, (params) =>
|
router.on(routes.Page, (params) => {
|
||||||
currentRoute.set({ component: Page, params }),
|
currentRoute.set({ component: Page, params });
|
||||||
);
|
breadcrumbs.newCrumb(params as PageParams);
|
||||||
|
});
|
||||||
|
|
||||||
async function setRouteToLastPage() {
|
async function setRouteToLastPage() {
|
||||||
if (location.pathname === "/") {
|
if (location.pathname === "/") {
|
||||||
const lastPage = await lastPageStore.get();
|
const lastWorldBreadcrumbs = await breadcrumbs.lastWorldBreadcrumbs();
|
||||||
if (lastPage) {
|
if (lastWorldBreadcrumbs) {
|
||||||
router.route(inject(routes.Page, lastPage));
|
const { worldId, breadcrumbs } = lastWorldBreadcrumbs;
|
||||||
|
for (const crumb of breadcrumbs) {
|
||||||
|
router.route(inject(routes.Page, { worldId, pageId: crumb }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,21 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { XmlFragment } from "yjs";
|
import type { XmlFragment } from "yjs";
|
||||||
import { inject } from "regexparam";
|
import { inject } from "regexparam";
|
||||||
|
import ChevronRight from "eva-icons/fill/svg/chevron-right.svg";
|
||||||
import Editor from "../editor/Editor.svelte";
|
import Editor from "../editor/Editor.svelte";
|
||||||
import { getWorldPage, getWorldY, type WorldY } from "../lib/doc";
|
import { getWorldPage, getWorldY, type WorldY } from "../lib/doc";
|
||||||
import { routes } from "../lib/routes";
|
import { routes } from "../lib/routes";
|
||||||
import { loadWorlds } from "../lib/worldStorage";
|
import { loadWorlds } from "../lib/worldStorage";
|
||||||
import { lastPageStore } from "../lib/router";
|
import breadcrumbs from "../lib/breadcrumbs";
|
||||||
|
|
||||||
export let worldId: string;
|
export let worldId: string;
|
||||||
export let pageId: string;
|
export let pageId: string;
|
||||||
|
|
||||||
|
$: pageBreadcrumbs = breadcrumbs.worldStore(worldId);
|
||||||
|
|
||||||
async function loadDoc(
|
async function loadDoc(
|
||||||
worldId: string,
|
worldId: string,
|
||||||
pageId: string
|
pageId: string,
|
||||||
): Promise<{ worldY: WorldY; doc: XmlFragment }> {
|
): Promise<{ worldY: WorldY; doc: XmlFragment }> {
|
||||||
const worlds = await loadWorlds();
|
const worlds = await loadWorlds();
|
||||||
const worldIdentifier = worlds.find((w) => w.room === worldId);
|
const worldIdentifier = worlds.find((w) => w.room === worldId);
|
||||||
|
@ -33,11 +36,6 @@
|
||||||
})
|
})
|
||||||
.catch((error) => (state = { error }));
|
.catch((error) => (state = { error }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveLastPage() {
|
|
||||||
await lastPageStore.set({ worldId, pageId });
|
|
||||||
}
|
|
||||||
saveLastPage();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav>
|
<nav>
|
||||||
|
@ -52,6 +50,28 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<!-- https://devdojo.com/pines/docs/breadcrumbs -->
|
||||||
|
<div
|
||||||
|
class="flex justify-between rounded-md border border-neutral-200/60 px-3.5 py-1"
|
||||||
|
>
|
||||||
|
<ol
|
||||||
|
class="mb-3 inline-flex items-center space-x-1 text-xs text-neutral-500 sm:mb-0 [&_.active-breadcrumb]:font-medium [&_.active-breadcrumb]:text-neutral-600"
|
||||||
|
>
|
||||||
|
{#each $pageBreadcrumbs as crumb, index}
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href={inject(routes.Page, { worldId, pageId: crumb })}
|
||||||
|
class="inline-flex items-center py-1 font-normal hover:text-neutral-900 focus:outline-none"
|
||||||
|
class:active-breadcrumb={crumb === pageId}>{crumb}</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
{#if index !== $pageBreadcrumbs.length - 1}
|
||||||
|
<ChevronRight class="h-5 w-5 fill-current text-gray-400/70" />
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{#if state === "loading"}Cargando...{:else if "doc" in state}
|
{#if state === "loading"}Cargando...{:else if "doc" in state}
|
||||||
|
|
Loading…
Reference in a new issue