- graph blueprint: GET /api/graph resolves note_links to target notes by
normalized title (owner-scoped, non-trashed, self-excluded) → {nodes, edges}
of connected notes.
- GraphView: hand-rolled force simulation (repulsion + edge springs + centering,
cooling over ~400 frames), SVG nodes/edges, click a node to open it (reuses the
editor with link navigation). Sidebar Graph entry + route; empty state.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
66 lines
2.1 KiB
TypeScript
66 lines
2.1 KiB
TypeScript
import { createRouter, createWebHistory } from "vue-router";
|
|
import { useSessionStore } from "../stores/session";
|
|
import { useConfigStore } from "../stores/config";
|
|
|
|
const router = createRouter({
|
|
history: createWebHistory(),
|
|
routes: [
|
|
{
|
|
// Persistent authed shell (sidebar + top bar + search); children render in it.
|
|
path: "/",
|
|
component: () => import("../components/AppShell.vue"),
|
|
meta: { requiresAuth: true },
|
|
children: [
|
|
{ path: "", name: "board", component: () => import("../views/BoardView.vue") },
|
|
{ path: "archive", name: "archive", component: () => import("../views/BoardView.vue") },
|
|
{ path: "trash", name: "trash", component: () => import("../views/BoardView.vue") },
|
|
{ path: "label/:id", name: "label", component: () => import("../views/BoardView.vue") },
|
|
{ path: "search", name: "search", component: () => import("../views/SearchView.vue") },
|
|
{ path: "graph", name: "graph", component: () => import("../views/GraphView.vue") },
|
|
],
|
|
},
|
|
{
|
|
path: "/settings",
|
|
name: "settings",
|
|
component: () => import("../views/SettingsView.vue"),
|
|
meta: { requiresAuth: true, requiresAdmin: true },
|
|
},
|
|
{
|
|
path: "/login",
|
|
name: "login",
|
|
component: () => import("../views/LoginView.vue"),
|
|
meta: { guestOnly: true },
|
|
},
|
|
{
|
|
path: "/register",
|
|
name: "register",
|
|
component: () => import("../views/RegisterView.vue"),
|
|
meta: { guestOnly: true },
|
|
},
|
|
],
|
|
});
|
|
|
|
router.beforeEach(async (to) => {
|
|
const session = useSessionStore();
|
|
const config = useConfigStore();
|
|
await config.load();
|
|
if (!session.loaded) {
|
|
await session.fetchMe();
|
|
}
|
|
if (to.meta.requiresAuth && !session.user) {
|
|
return { name: "login", query: to.fullPath !== "/" ? { redirect: to.fullPath } : undefined };
|
|
}
|
|
if (to.meta.requiresAdmin && !session.user?.is_admin) {
|
|
return { name: "board" };
|
|
}
|
|
if (to.name === "register" && !config.allowRegistration) {
|
|
return { name: "login" };
|
|
}
|
|
if (to.meta.guestOnly && session.user) {
|
|
return { name: "board" };
|
|
}
|
|
return true;
|
|
});
|
|
|
|
export default router;
|