From 8340b1d4891ec26c154b91fc304fe96a8b0eca0e Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Tue, 10 Mar 2026 12:22:39 -0700 Subject: [PATCH] feat(web): align notes runtime with backend --- web/src/app/(app)/dashboard/page.tsx | 33 +++++- web/src/app/(app)/notes/[noteId]/page.tsx | 42 +++++-- web/src/app/(app)/search/page.tsx | 39 +++++- web/src/app/(app)/workspaces/page.tsx | 48 +++++++- web/src/lib/auth.ts | 32 +---- web/src/lib/notes-client.ts | 137 ++++++++++++++++++++++ 6 files changed, 284 insertions(+), 47 deletions(-) create mode 100644 web/src/lib/notes-client.ts diff --git a/web/src/app/(app)/dashboard/page.tsx b/web/src/app/(app)/dashboard/page.tsx index 0381ee2..8cbe352 100644 --- a/web/src/app/(app)/dashboard/page.tsx +++ b/web/src/app/(app)/dashboard/page.tsx @@ -1,8 +1,32 @@ +"use client"; + +import { useEffect, useState } from "react"; import { AppShell } from "@/components/AppShell"; -import { mockNotes, mockOperatorWorkflows, mockSavedViews, mockWorkspaces } from "@/lib/mock-data"; +import { listNoteSummaries, listWorkspaceSummaries } from "@/lib/notes-client"; +import { mockOperatorWorkflows, mockSavedViews } from "@/lib/mock-data"; +import type { NoteSummary, WorkspaceSummary } from "@/lib/types"; export default function DashboardPage() { - const recentNotes = mockNotes.slice(0, 3); + const [notes, setNotes] = useState([]); + const [workspaces, setWorkspaces] = useState([]); + const [error, setError] = useState(null); + + useEffect(() => { + void (async () => { + try { + const [nextNotes, nextWorkspaces] = await Promise.all([ + listNoteSummaries(), + listWorkspaceSummaries(), + ]); + setNotes(nextNotes); + setWorkspaces(nextWorkspaces); + } catch (err) { + setError(err instanceof Error ? err.message : "Unable to load dashboard data"); + } + })(); + }, []); + + const recentNotes = notes.slice(0, 3); return (
Active workspaces
-
{mockWorkspaces.length}
+
{workspaces.length}
Tracked notes
-
{mockNotes.length}
+
{notes.length}
Pending review surfaces
@@ -67,6 +91,7 @@ export default function DashboardPage() {
Recent note activity
+ {error ?
{error}
: null}
{recentNotes.map((note) => (
diff --git a/web/src/app/(app)/notes/[noteId]/page.tsx b/web/src/app/(app)/notes/[noteId]/page.tsx index 403cf7e..61569f3 100644 --- a/web/src/app/(app)/notes/[noteId]/page.tsx +++ b/web/src/app/(app)/notes/[noteId]/page.tsx @@ -1,4 +1,7 @@ -import { notFound } from "next/navigation"; +"use client"; + +import { useEffect, useState } from "react"; +import { useParams } from "next/navigation"; import { AppShell } from "@/components/AppShell"; import { NoteEditor } from "@/components/NoteEditor"; import { MetadataPanel } from "@/components/MetadataPanel"; @@ -6,19 +9,38 @@ import { LinkedNotesPanel } from "@/components/LinkedNotesPanel"; import { TaskReviewPanel } from "@/components/TaskReviewPanel"; import { ArtifactPanel } from "@/components/ArtifactPanel"; import { AgentTimeline } from "@/components/AgentTimeline"; -import { getNoteById } from "@/lib/mock-data"; +import { getNoteDetail } from "@/lib/notes-client"; import { mockAgentTimeline } from "@/lib/review-data"; +import type { NoteDetail } from "@/lib/types"; -export default async function NoteDetailPage({ - params, -}: { - params: Promise<{ noteId: string }>; -}) { - const { noteId } = await params; - const note = getNoteById(noteId); +export default function NoteDetailPage() { + const params = useParams<{ noteId: string }>(); + const noteId = params.noteId; + const [note, setNote] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + void (async () => { + try { + setNote(await getNoteDetail(noteId)); + } catch (err) { + setError(err instanceof Error ? err.message : "Unable to load note"); + } + })(); + }, [noteId]); if (!note) { - notFound(); + return ( + Loading
} + > +
+ {error ?? "Loading note…"} +
+ + ); } return ( diff --git a/web/src/app/(app)/search/page.tsx b/web/src/app/(app)/search/page.tsx index dc85c87..cc02507 100644 --- a/web/src/app/(app)/search/page.tsx +++ b/web/src/app/(app)/search/page.tsx @@ -1,8 +1,40 @@ +"use client"; + import Link from "next/link"; +import { useEffect, useMemo, useState } from "react"; import { AppShell } from "@/components/AppShell"; -import { mockNotes, mockSavedViews } from "@/lib/mock-data"; +import { listNoteSummaries } from "@/lib/notes-client"; +import { mockSavedViews } from "@/lib/mock-data"; +import type { NoteSummary } from "@/lib/types"; export default function SearchPage() { + const [notes, setNotes] = useState([]); + const [query, setQuery] = useState(""); + const [error, setError] = useState(null); + + useEffect(() => { + void (async () => { + try { + setNotes(await listNoteSummaries()); + } catch (err) { + setError(err instanceof Error ? err.message : "Unable to load notes"); + } + })(); + }, []); + + const filteredNotes = useMemo(() => { + const normalized = query.trim().toLowerCase(); + if (!normalized) { + return notes; + } + + return notes.filter((note) => + note.title.toLowerCase().includes(normalized) || + note.excerpt.toLowerCase().includes(normalized) || + note.tags.some((tag) => tag.toLowerCase().includes(normalized)), + ); + }, [notes, query]); + return ( setQuery(event.target.value)} />
workspace:all @@ -45,8 +79,9 @@ export default function SearchPage() { source:manual+agent matched:title+tags
+ {error ?
{error}
: null}
- {mockNotes.map((note) => ( + {filteredNotes.map((note) => (
diff --git a/web/src/app/(app)/workspaces/page.tsx b/web/src/app/(app)/workspaces/page.tsx index 19228b9..deb9630 100644 --- a/web/src/app/(app)/workspaces/page.tsx +++ b/web/src/app/(app)/workspaces/page.tsx @@ -1,8 +1,43 @@ +"use client"; + import Link from "next/link"; +import { useEffect, useMemo, useState } from "react"; import { AppShell } from "@/components/AppShell"; -import { getNotesForWorkspace, mockSavedViews, mockWorkspaces } from "@/lib/mock-data"; +import { listNoteSummaries, listWorkspaceSummaries } from "@/lib/notes-client"; +import { mockSavedViews } from "@/lib/mock-data"; +import type { NoteSummary, WorkspaceSummary } from "@/lib/types"; export default function WorkspacesPage() { + const [notes, setNotes] = useState([]); + const [workspaces, setWorkspaces] = useState([]); + const [error, setError] = useState(null); + + useEffect(() => { + void (async () => { + try { + const [nextNotes, nextWorkspaces] = await Promise.all([ + listNoteSummaries(), + listWorkspaceSummaries(), + ]); + setNotes(nextNotes); + setWorkspaces(nextWorkspaces); + } catch (err) { + setError(err instanceof Error ? err.message : "Unable to load workspaces"); + } + })(); + }, []); + + const notesByWorkspace = useMemo( + () => + new Map( + workspaces.map((workspace) => [ + workspace.id, + notes.filter((note) => note.workspaceId === workspace.id), + ]), + ), + [notes, workspaces], + ); + return (
+ {error ? ( +
+ {error} +
+ ) : null}
- {mockWorkspaces.map((workspace) => { - const notes = getNotesForWorkspace(workspace.id); + {workspaces.map((workspace) => { + const workspaceNotes = notesByWorkspace.get(workspace.id) ?? []; return (
@@ -62,7 +102,7 @@ export default function WorkspacesPage() { ))}
- {notes.map((note) => ( + {workspaceNotes.map((note) => ( {note.title} {note.excerpt} diff --git a/web/src/lib/auth.ts b/web/src/lib/auth.ts index e4916e0..cd30e8d 100644 --- a/web/src/lib/auth.ts +++ b/web/src/lib/auth.ts @@ -2,7 +2,7 @@ import { createAuthProvider } from "@bytelyst/react-auth"; import type { ProductUser } from "@/lib/types"; -import { PRODUCT_ID } from "@/lib/product-config"; +import { PLATFORM_SERVICE_URL, PRODUCT_ID } from "@/lib/product-config"; interface LoginResponse { user?: ProductUser; @@ -10,14 +10,8 @@ interface LoginResponse { refreshToken?: string; } -const demoUser: ProductUser = { - email: "operator@bytelyst.dev", - name: "ByteLyst Operator", - role: "admin", - workspaceId: "workspace-product", -}; - export const { AuthProvider, useAuth } = createAuthProvider({ + baseUrl: PLATFORM_SERVICE_URL, storagePrefix: PRODUCT_ID, loginEndpoint: "/auth/login", registerEndpoint: "/auth/register", @@ -28,25 +22,9 @@ export const { AuthProvider, useAuth } = createAuthProvider({ mapLoginResponse: (data: unknown) => { const result = data as LoginResponse; return { - user: result.user ?? demoUser, - accessToken: result.accessToken ?? "demo-access-token", - refreshToken: result.refreshToken ?? "demo-refresh-token", - }; - }, - onLoginFallback: async () => ({ - user: demoUser, - accessToken: "demo-access-token", - refreshToken: "demo-refresh-token", - }), - onInit: () => { - if (typeof window === "undefined") { - return null; - } - - return { - user: demoUser, - accessToken: "demo-access-token", - refreshToken: "demo-refresh-token", + user: result.user as ProductUser, + accessToken: result.accessToken ?? "", + refreshToken: result.refreshToken ?? "", }; }, }); diff --git a/web/src/lib/notes-client.ts b/web/src/lib/notes-client.ts new file mode 100644 index 0000000..b7379e7 --- /dev/null +++ b/web/src/lib/notes-client.ts @@ -0,0 +1,137 @@ +import { createApiClient } from "@bytelyst/api-client"; +import { NOTES_API_URL, PRODUCT_ID } from "@/lib/product-config"; +import type { NoteDetail, NoteSummary, WorkspaceSummary } from "@/lib/types"; + +type NoteDoc = { + id: string; + workspaceId: string; + title: string; + body: string; + status: "draft" | "active" | "archived"; + tags: string[]; + updatedAt: string; + updatedBy: string; + createdBy: string; + sourceType?: string; +}; + +type WorkspaceDoc = { + id: string; + name: string; + description?: string; + members: Array<{ userId: string; role: string }>; + updatedAt: string; + updatedBy: string; +}; + +type NoteListResponse = { + items: NoteDoc[]; +}; + +type WorkspaceListResponse = { + items: WorkspaceDoc[]; +}; + +function getAccessToken(): string | null { + if (typeof window === "undefined") { + return null; + } + + return localStorage.getItem(`${PRODUCT_ID}_access_token`); +} + +function createNotesApiClient() { + return createApiClient({ + baseUrl: NOTES_API_URL, + getToken: getAccessToken, + defaultHeaders: { + "x-product-id": PRODUCT_ID, + }, + }); +} + +function buildWorkspaceMap(workspaces: WorkspaceDoc[]) { + return new Map(workspaces.map((workspace) => [workspace.id, workspace])); +} + +function toNoteSummary(note: NoteDoc): NoteSummary { + return { + id: note.id, + workspaceId: note.workspaceId, + title: note.title, + excerpt: note.body.slice(0, 160), + status: note.status, + tags: note.tags, + updatedAt: note.updatedAt, + updatedBy: note.updatedBy, + }; +} + +function toWorkspaceSummary(workspace: WorkspaceDoc, notes: NoteDoc[]): WorkspaceSummary { + const owner = workspace.members.find((member) => member.role === "owner")?.userId ?? workspace.updatedBy; + const noteCount = notes.filter((note) => note.workspaceId === workspace.id).length; + + return { + id: workspace.id, + name: workspace.name, + description: workspace.description ?? "", + owner, + noteCount, + visibility: workspace.members.length > 1 ? "shared" : "private", + updatedAt: workspace.updatedAt, + tags: [], + }; +} + +export async function listWorkspaceSummaries(): Promise { + const api = createNotesApiClient(); + const [workspaceResponse, noteResponse] = await Promise.all([ + api.fetch("/workspaces"), + api.fetch("/notes"), + ]); + + return workspaceResponse.items.map((workspace) => toWorkspaceSummary(workspace, noteResponse.items)); +} + +export async function listNoteSummaries(): Promise { + const api = createNotesApiClient(); + const response = await api.fetch("/notes"); + return response.items.map(toNoteSummary); +} + +export async function listNotesForWorkspace(workspaceId: string): Promise { + const api = createNotesApiClient(); + const response = await api.fetch(`/notes?workspaceId=${encodeURIComponent(workspaceId)}`); + return response.items.map(toNoteSummary); +} + +export async function getNoteDetail(noteId: string): Promise { + const api = createNotesApiClient(); + const [workspaceResponse, noteResponse] = await Promise.all([ + api.fetch("/workspaces"), + api.fetch("/notes"), + ]); + + const note = noteResponse.items.find((item) => item.id === noteId); + if (!note) { + return null; + } + + const workspaceMap = buildWorkspaceMap(workspaceResponse.items); + const workspace = workspaceMap.get(note.workspaceId); + + return { + ...toNoteSummary(note), + body: note.body, + metadata: { + owner: workspace?.members.find((member) => member.role === "owner")?.userId ?? note.updatedBy, + source: note.sourceType ?? "manual", + reviewState: "none", + taskCount: 0, + artifactCount: 0, + }, + linkedNotes: [], + tasks: [], + artifacts: [], + }; +}