-
-
-
- {user &&
{user.email_prefix} }
-
-
-
+
+
+ {data?.map((network, index) => (
+
+ navigate(`/${network.id}`)}
+ />
+ {index < data.length - 1 && }
+
+ ))}
-
-
- navigate(`/${value}`)}>
-
-
-
-
- {data?.map((network) => (
-
- {network.name}
-
- ))}
-
-
-
-
+
);
}
diff --git a/js/src/hooks/use-networks.ts b/js/src/hooks/use-networks.ts
new file mode 100644
index 0000000..a770741
--- /dev/null
+++ b/js/src/hooks/use-networks.ts
@@ -0,0 +1,9 @@
+import { useQuery } from "@tanstack/react-query";
+import { apiClient } from "@/api/client";
+
+export function useNetworks() {
+ return useQuery({
+ queryKey: ["networks"],
+ queryFn: () => apiClient.listNetworks(),
+ });
+}
diff --git a/js/src/pages/path-resolver.tsx b/js/src/pages/path-resolver.tsx
index 718a72e..8b785c8 100644
--- a/js/src/pages/path-resolver.tsx
+++ b/js/src/pages/path-resolver.tsx
@@ -1,12 +1,132 @@
-import { useLocation } from "react-router-dom";
+import { useLocation, useNavigate } from "react-router-dom";
+import { Home, Settings } from "lucide-react";
import { NetworkSelector } from "@/features/network-selector";
import { ParticleListView } from "@/features/particles/particle-list-view";
import { ParticleViewResolver } from "@/features/particles/particle-view-resolver";
+import { WindowControls } from "@/components/window-controls";
+import { Button } from "@/components/ui/button";
+import { Muted } from "@/components/ui/typography";
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbList,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+} from "@/components/ui/breadcrumb";
+import { Avatar, AvatarFallback } from "@/components/ui/avatar";
+import { useAuthStore } from "@/stores/auth-store";
+import { useNetworks } from "@/hooks/use-networks";
function parsePathSegments(path: string): string[] {
return path.split("/").filter(Boolean);
}
+function NetworkBreadcrumbContent({ networkId }: { networkId: string }) {
+ const { data: networks } = useNetworks();
+ const network = networks?.find((n) => n.id === networkId);
+ const name = network?.name ?? networkId;
+ const initials = name.slice(0, 2).toUpperCase();
+
+ return (
+
+
+
+ {initials}
+
+
+ {name}
+
+ );
+}
+
+function TopBar({ segments }: { segments: string[] }) {
+ const navigate = useNavigate();
+ const user = useAuthStore((s) => s.user);
+ const networkId = segments[0] ?? null;
+
+ return (
+
+
+
+
+
+
+ {segments.length === 0 ? (
+
+
+ Networks
+
+ ) : (
+ navigate("/")}
+ >
+
+ Networks
+
+ )}
+
+
+ {networkId && (
+
+
+
+ {segments.length === 1 ? (
+
+
+
+ ) : (
+ navigate(`/${networkId}`)}
+ >
+
+
+ )}
+
+
+ )}
+
+ {segments.slice(1).map((segment, index) => {
+ const isLast = index === segments.length - 2;
+ const path = `/${segments.slice(0, index + 2).join("/")}`;
+
+ return (
+
+
+
+ {isLast ? (
+ {segment}
+ ) : (
+ navigate(path)}
+ >
+ {segment}
+
+ )}
+
+
+ );
+ })}
+
+
+
+
+ {user &&
{user.email_prefix} }
+
navigate("/settings")}
+ >
+
+
+
+ );
+}
+
/**
* URL structure:
* / → network selector
@@ -16,18 +136,32 @@ function parsePathSegments(path: string): string[] {
export default function PathResolver() {
const segments = parsePathSegments(useLocation().pathname);
- // No segments → show network selector
- if (segments.length === 0) {
- return
;
- }
+ const content = (() => {
+ // No segments → show network selector
+ if (segments.length === 0) {
+ return
;
+ }
- const [networkId, ...particleSegments] = segments;
+ const [networkId, ...particleSegments] = segments;
- // /:networkId with no particle segments → root particle list
- if (particleSegments.length === 0) {
- return
;
- }
+ // /:networkId with no particle segments → root particle list
+ if (particleSegments.length === 0) {
+ return
;
+ }
- // /:networkId/:p1/:p2/... → resolve and render the container particle
- return
;
+ // /:networkId/:p1/:p2/... → resolve and render the container particle
+ return (
+
+ );
+ })();
+
+ return (
+
+ );
}
diff --git a/js/src/pages/settings-page.tsx b/js/src/pages/settings-page.tsx
new file mode 100644
index 0000000..bd938d7
--- /dev/null
+++ b/js/src/pages/settings-page.tsx
@@ -0,0 +1,139 @@
+import { useNavigate } from "react-router-dom";
+import { ChevronRight, LogOut, User, Info, Shield } from "lucide-react";
+import { Avatar, AvatarFallback } from "@/components/ui/avatar";
+import { Separator } from "@/components/ui/separator";
+import { WindowControls } from "@/components/window-controls";
+import { Button } from "@/components/ui/button";
+import { Muted } from "@/components/ui/typography";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { useAuthStore } from "@/stores/auth-store";
+import { ArrowLeft } from "lucide-react";
+
+interface SettingsRowProps {
+ icon: React.ReactNode;
+ label: string;
+ detail?: string;
+ onClick?: () => void;
+ destructive?: boolean;
+}
+
+function SettingsRow({
+ icon,
+ label,
+ detail,
+ onClick,
+ destructive,
+}: SettingsRowProps) {
+ return (
+
+
+ {icon}
+
+ {label}
+ {detail && {detail} }
+ {onClick && !destructive && (
+
+ )}
+
+ );
+}
+
+function SettingsGroup({
+ title,
+ children,
+}: {
+ title: string;
+ children: React.ReactNode;
+}) {
+ return (
+
+
+ {title}
+
+
{children}
+
+ );
+}
+
+export function SettingsPage() {
+ const navigate = useNavigate();
+ const user = useAuthStore((s) => s.user);
+ const signOut = useAuthStore((s) => s.signOut);
+
+ const initials = user?.email_prefix?.slice(0, 2).toUpperCase() ?? "?";
+
+ return (
+
+
+
+
navigate(-1)}
+ >
+
+
+
Settings
+
+
+
+
+ {/* Profile header */}
+
+
+
+ {initials}
+
+
+
+
+ {user?.email_prefix}
+
+
{user?.email}
+
+
+
+
+
+
+ }
+ label="Profile"
+ detail={user?.email_prefix}
+ />
+
+ }
+ label="Privacy"
+ />
+
+
+
+
+
+ }
+ label="Version"
+ detail="1.0.0"
+ />
+
+
+
+
+
+ }
+ label="Sign out"
+ onClick={signOut}
+ destructive
+ />
+
+
+
+ );
+}