feat(ui): add sidebar and flesh out app routes

This commit is contained in:
Brian McGee
2025-07-21 13:32:57 +01:00
parent c880ab7cc1
commit 38d62af1ba
21 changed files with 510 additions and 338 deletions

View File

@@ -1,5 +1,5 @@
div.sidebar {
@apply h-full w-auto max-w-60 border-none;
@apply w-60 border-none;
& > div.header {
}

View File

@@ -0,0 +1,157 @@
import type { Meta, StoryObj } from "@kachurun/storybook-solid";
import {
createMemoryHistory,
MemoryRouter,
Route,
RouteSectionProps,
} from "@solidjs/router";
import { Sidebar } from "@/src/components/Sidebar/Sidebar";
import { Suspense } from "solid-js";
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
import { addClanURI, resetStore } from "@/src/stores/clan";
import { SolidQueryDevtools } from "@tanstack/solid-query-devtools";
import { encodeBase64 } from "@/src/hooks/clan";
const defaultClanURI = "/home/brian/clans/my-clan";
const queryData = {
"/home/brian/clans/my-clan": {
details: {
name: "Brian's Clan",
uri: "/home/brian/clans/my-clan",
},
machines: {
europa: {
name: "Europa",
machineClass: "nixos",
},
ganymede: {
name: "Ganymede",
machineClass: "nixos",
},
},
},
"/home/brian/clans/davhau": {
details: {
name: "Dave's Clan",
uri: "/home/brian/clans/davhau",
},
machines: {
callisto: {
name: "Callisto",
machineClass: "nixos",
},
amalthea: {
name: "Amalthea",
machineClass: "nixos",
},
},
},
"/home/brian/clans/mic92": {
details: {
name: "Mic92's Clan",
uri: "/home/brian/clans/mic92",
},
machines: {
thebe: {
name: "Thebe",
machineClass: "nixos",
},
sponde: {
name: "Sponde",
machineClass: "nixos",
},
},
},
};
const staticSections = [
{
title: "Links",
links: [
{ label: "GitHub", path: "https://github.com/brian-the-dev" },
{ label: "Twitter", path: "https://twitter.com/brian_the_dev" },
{
label: "LinkedIn",
path: "https://www.linkedin.com/in/brian-the-dev/",
},
{
label: "Instagram",
path: "https://www.instagram.com/brian_the_dev/",
},
],
},
];
const meta: Meta<RouteSectionProps> = {
title: "Components/Sidebar",
component: Sidebar,
render: () => {
// set history to point to our test clan
const history = createMemoryHistory();
history.set({ value: `/clans/${encodeBase64(defaultClanURI)}` });
// reset local storage and then add each clan
resetStore();
Object.keys(queryData).forEach((uri) => addClanURI(uri));
return (
<div style="height: 670px;">
<MemoryRouter
history={history}
root={(props) => <Suspense>{props.children}</Suspense>}
>
<Route
path="/clans/:clanURI"
component={() => <Sidebar staticSections={staticSections} />}
>
<Route path="/" />
<Route
path="/machines/:machineID"
component={() => <h1>Machine</h1>}
/>
</Route>
</MemoryRouter>
<SolidQueryDevtools initialIsOpen={true} />
</div>
);
},
};
export default meta;
type Story = StoryObj<RouteSectionProps>;
export const Default: Story = {
args: {},
decorators: [
(Story: StoryObj) => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
staleTime: Infinity,
},
},
});
Object.entries(queryData).forEach(([clanURI, clan]) => {
queryClient.setQueryData(
["clans", encodeBase64(clanURI), "details"],
clan.details,
);
queryClient.setQueryData(
["clans", encodeBase64(clanURI), "machines"],
clan.machines || {},
);
});
return (
<QueryClientProvider client={queryClient}>
<Story />
</QueryClientProvider>
);
},
],
};

View File

@@ -0,0 +1,28 @@
import "./Sidebar.css";
import { SidebarHeader } from "@/src/components/Sidebar/SidebarHeader";
import { SidebarBody } from "@/src/components/Sidebar/SidebarBody";
export interface LinkProps {
path: string;
label?: string;
}
export interface SectionProps {
title: string;
links: LinkProps[];
}
export interface SidebarProps {
staticSections?: SectionProps[];
}
export const Sidebar = (props: SidebarProps) => {
return (
<>
<div class="sidebar">
<SidebarHeader />
<SidebarBody {...props} />
</div>
</>
);
};

View File

@@ -1,46 +1,60 @@
import "./SidebarNavBody.css";
import "./SidebarBody.css";
import { A } from "@solidjs/router";
import { Accordion } from "@kobalte/core/accordion";
import Icon from "../Icon/Icon";
import { Typography } from "@/src/components/Typography/Typography";
import {
MachineProps,
SidebarNavProps,
} from "@/src/components/Sidebar/SidebarNav";
import { For } from "solid-js";
import { For, Suspense } from "solid-js";
import { MachineStatus } from "@/src/components/MachineStatus/MachineStatus";
import { buildMachinePath, useClanURI } from "@/src/hooks/clan";
import { useMachinesQuery } from "@/src/queries/queries";
import { SidebarProps } from "./Sidebar";
interface MachineProps {
clanURI: string;
machineID: string;
name: string;
status: MachineStatus;
serviceCount: number;
}
const MachineRoute = (props: MachineProps) => (
<div class="flex w-full flex-col gap-2">
<div class="flex flex-row items-center justify-between">
<Typography
hierarchy="label"
size="xs"
weight="bold"
color="primary"
inverted={true}
>
{props.label}
</Typography>
<MachineStatus status={props.status} />
<A href={buildMachinePath(props.clanURI, props.machineID)}>
<div class="flex w-full flex-col gap-2">
<div class="flex flex-row items-center justify-between">
<Typography
hierarchy="label"
size="xs"
weight="bold"
color="primary"
inverted={true}
>
{props.name}
</Typography>
<MachineStatus status={props.status} />
</div>
<div class="flex w-full flex-row items-center gap-1">
<Icon icon="Flash" size="0.75rem" inverted={true} color="tertiary" />
<Typography
hierarchy="label"
family="mono"
size="s"
inverted={true}
color="primary"
>
{props.serviceCount}
</Typography>
</div>
</div>
<div class="flex w-full flex-row items-center gap-1">
<Icon icon="Flash" size="0.75rem" inverted={true} color="tertiary" />
<Typography
hierarchy="label"
family="mono"
size="s"
inverted={true}
color="primary"
>
{props.serviceCount}
</Typography>
</div>
</div>
</A>
);
export const SidebarNavBody = (props: SidebarNavProps) => {
const sectionLabels = props.extraSections.map((section) => section.label);
export const SidebarBody = (props: SidebarProps) => {
const clanURI = useClanURI();
const machineList = useMachinesQuery(clanURI);
const sectionLabels = (props.staticSections || []).map(
(section) => section.title,
);
// controls which sections are open by default
// we want them all to be open by default
@@ -75,21 +89,27 @@ export const SidebarNavBody = (props: SidebarNavProps) => {
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content class="content">
<nav>
<For each={props.clanDetail.machines}>
{(machine) => (
<A href={machine.path}>
<MachineRoute {...machine} />
</A>
)}
</For>
</nav>
<Suspense fallback={"Loading..."}>
<nav>
<For each={Object.entries(machineList.data || {})}>
{([id, machine]) => (
<MachineRoute
clanURI={clanURI}
machineID={id}
name={machine.name || id}
status="Not Installed"
serviceCount={0}
/>
)}
</For>
</nav>
</Suspense>
</Accordion.Content>
</Accordion.Item>
<For each={props.extraSections}>
<For each={props.staticSections}>
{(section) => (
<Accordion.Item class="item" value={section.label}>
<Accordion.Item class="item" value={section.title}>
<Accordion.Header class="header">
<Accordion.Trigger class="trigger">
<Typography
@@ -100,7 +120,7 @@ export const SidebarNavBody = (props: SidebarNavProps) => {
inverted={true}
color="tertiary"
>
{section.label}
{section.title}
</Typography>
<Icon
icon="CaretDown"

View File

@@ -15,10 +15,11 @@ div.sidebar-header {
transition: all 250ms ease-in-out;
div.title {
div.clan-label {
@apply flex items-center gap-2 justify-start;
& > .clan-icon {
@apply flex justify-center items-center;
@apply rounded-full bg-inv-4 w-7 h-7;
}
}

View File

@@ -0,0 +1,107 @@
import "./SidebarHeader.css";
import Icon from "@/src/components/Icon/Icon";
import { DropdownMenu } from "@kobalte/core/dropdown-menu";
import { useNavigate } from "@solidjs/router";
import { Typography } from "../Typography/Typography";
import { createSignal, For, Suspense } from "solid-js";
import { useAllClanDetailsQuery } from "@/src/queries/queries";
import { navigateToClan, useClanURI } from "@/src/hooks/clan";
import { clanURIs } from "@/src/stores/clan";
export const SidebarHeader = () => {
const navigate = useNavigate();
const [open, setOpen] = createSignal(false);
// get information about the current active clan
const clanURI = useClanURI();
const allClans = useAllClanDetailsQuery(clanURIs());
const activeClan = () => allClans.find(({ data }) => data?.uri === clanURI);
return (
<div class="sidebar-header">
<Suspense fallback={"Loading..."}>
<DropdownMenu open={open()} onOpenChange={setOpen} sameWidth={true}>
<DropdownMenu.Trigger class="dropdown-trigger">
<div class="clan-label">
<div class="clan-icon">
<Typography
hierarchy="label"
size="s"
weight="bold"
inverted={true}
>
{activeClan()?.data?.name.charAt(0).toUpperCase()}
</Typography>
</div>
<Typography
hierarchy="label"
size="s"
weight="bold"
inverted={!open()}
>
{activeClan()?.data?.name}
</Typography>
</div>
<DropdownMenu.Icon>
<Icon icon={"CaretDown"} inverted={!open()} size="0.75rem" />
</DropdownMenu.Icon>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content class="sidebar-dropdown-content">
<DropdownMenu.Item
class="dropdown-item"
onSelect={() => navigateToClan(navigate, clanURI)}
>
<Icon
icon="Settings"
size="0.75rem"
inverted={true}
color="tertiary"
/>
<Typography hierarchy="label" size="xs" weight="medium">
Settings
</Typography>
</DropdownMenu.Item>
<DropdownMenu.Group class="dropdown-group">
<DropdownMenu.GroupLabel class="dropdown-group-label">
<Typography
hierarchy="label"
family="mono"
size="xs"
color="tertiary"
>
YOUR CLANS
</Typography>
</DropdownMenu.GroupLabel>
<div class="dropdown-group-items">
<For each={allClans}>
{(clan) => (
<Suspense fallback={"Loading..."}>
<DropdownMenu.Item
class="dropdown-item"
onSelect={() =>
navigateToClan(navigate, clan.data!.uri)
}
>
<Typography
hierarchy="label"
size="xs"
weight="medium"
>
{clan.data?.name}
</Typography>
</DropdownMenu.Item>
</Suspense>
)}
</For>
</div>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
</Suspense>
</div>
);
};

View File

@@ -1,109 +0,0 @@
import type { Meta, StoryObj } from "@kachurun/storybook-solid";
import {
createMemoryHistory,
MemoryRouter,
Route,
RouteSectionProps,
} from "@solidjs/router";
import {
SidebarNav,
SidebarNavProps,
} from "@/src/components/Sidebar/SidebarNav";
import { Suspense } from "solid-js";
import { StoryContext } from "@kachurun/storybook-solid-vite";
const sidebarNavProps: SidebarNavProps = {
clanLinks: [
{ label: "Brian's Clan", path: "/clans/1" },
{ label: "Dave's Clan", path: "/clans/2" },
{ label: "Mic92's Clan", path: "/clans/3" },
],
clanDetail: {
label: "Brian's Clan",
settingsPath: "/clans/1/settings",
machines: [
{
label: "Backup & Home",
path: "/clans/1/machine/backup",
serviceCount: 3,
status: "Online",
},
{
label: "Raspberry Pi",
path: "/clans/1/machine/pi",
serviceCount: 1,
status: "Offline",
},
{
label: "Mom's Laptop",
path: "/clans/1/machine/moms-laptop",
serviceCount: 2,
status: "Installed",
},
{
label: "Dad's Laptop",
path: "/clans/1/machine/dads-laptop",
serviceCount: 4,
status: "Not Installed",
},
],
},
extraSections: [
{
label: "Tools",
links: [
{ label: "Borgbackup", path: "/clans/1/service/borgbackup" },
{ label: "Syncthing", path: "/clans/1/service/syncthing" },
{ label: "Mumble", path: "/clans/1/service/mumble" },
{ label: "Minecraft", path: "/clans/1/service/minecraft" },
],
},
{
label: "Links",
links: [
{ label: "GitHub", path: "https://github.com/brian-the-dev" },
{ label: "Twitter", path: "https://twitter.com/brian_the_dev" },
{
label: "LinkedIn",
path: "https://www.linkedin.com/in/brian-the-dev/",
},
{
label: "Instagram",
path: "https://www.instagram.com/brian_the_dev/",
},
],
},
],
};
const meta: Meta<RouteSectionProps> = {
title: "Components/Sidebar/Nav",
component: SidebarNav,
render: (_: never, context: StoryContext<SidebarNavProps>) => {
const history = createMemoryHistory();
history.set({ value: "/clans/1/machine/backup" });
return (
<div style="height: 670px;">
<MemoryRouter
history={history}
root={(props) => (
<Suspense>
<SidebarNav {...sidebarNavProps} />
</Suspense>
)}
>
<Route path="/clans/1/machine/backup" component={(props) => <></>} />
</MemoryRouter>
</div>
);
},
};
export default meta;
type Story = StoryObj<RouteSectionProps>;
export const Default: Story = {
args: {},
};

View File

@@ -1,47 +0,0 @@
import "./SidebarNav.css";
import { SidebarNavHeader } from "@/src/components/Sidebar/SidebarNavHeader";
import { SidebarNavBody } from "@/src/components/Sidebar/SidebarNavBody";
import { MachineStatus } from "@/src/components/MachineStatus/MachineStatus";
export interface LinkProps {
path: string;
label?: string;
}
export interface SectionProps {
label: string;
links: LinkProps[];
}
export interface MachineProps {
label: string;
path: string;
status: MachineStatus;
serviceCount: number;
}
export interface ClanLinkProps {
label: string;
path: string;
}
export interface ClanProps {
label: string;
settingsPath: string;
machines: MachineProps[];
}
export interface SidebarNavProps {
clanDetail: ClanProps;
clanLinks: ClanLinkProps[];
extraSections: SectionProps[];
}
export const SidebarNav = (props: SidebarNavProps) => {
return (
<div class="sidebar">
<SidebarNavHeader {...props} />
<SidebarNavBody {...props} />
</div>
);
};

View File

@@ -1,96 +0,0 @@
import "./SidebarNavHeader.css";
import Icon from "@/src/components/Icon/Icon";
import { DropdownMenu } from "@kobalte/core/dropdown-menu";
import { useNavigate } from "@solidjs/router";
import { Typography } from "../Typography/Typography";
import { createSignal, For } from "solid-js";
import { ClanLinkProps, ClanProps } from "@/src/components/Sidebar/SidebarNav";
export interface SidebarHeaderProps {
clanDetail: ClanProps;
clanLinks: ClanLinkProps[];
}
export const SidebarNavHeader = (props: SidebarHeaderProps) => {
const navigate = useNavigate();
const [open, setOpen] = createSignal(false);
const firstChar = props.clanDetail.label.charAt(0);
return (
<div class="sidebar-header">
<DropdownMenu open={open()} onOpenChange={setOpen} sameWidth={true}>
<DropdownMenu.Trigger class="dropdown-trigger">
<div class="title">
<div class="clan-icon">
<Typography
hierarchy="label"
size="s"
weight="bold"
inverted={true}
>
{firstChar.toUpperCase()}
</Typography>
</div>
<Typography
hierarchy="label"
size="s"
weight="bold"
inverted={!open()}
>
{props.clanDetail.label}
</Typography>
</div>
<DropdownMenu.Icon>
<Icon icon={"CaretDown"} inverted={!open()} size="0.75rem" />
</DropdownMenu.Icon>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content class="sidebar-dropdown-content">
<DropdownMenu.Item
class="dropdown-item"
onSelect={() => navigate(props.clanDetail.settingsPath)}
>
<Icon
icon="Settings"
size="0.75rem"
inverted={true}
color="tertiary"
/>
<Typography hierarchy="label" size="xs" weight="medium">
Settings
</Typography>
</DropdownMenu.Item>
<DropdownMenu.Group class="dropdown-group">
<DropdownMenu.GroupLabel class="dropdown-group-label">
<Typography
hierarchy="label"
family="mono"
size="xs"
color="tertiary"
>
YOUR CLANS
</Typography>
</DropdownMenu.GroupLabel>
<div class="dropdown-group-items">
<For each={props.clanLinks}>
{(clan) => (
<DropdownMenu.Item
class="dropdown-item"
onSelect={() => navigate(clan.path)}
>
<Typography hierarchy="label" size="xs" weight="medium">
{clan.label}
</Typography>
</DropdownMenu.Item>
)}
</For>
</div>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
</div>
);
};

View File

@@ -1,5 +1,5 @@
div.sidebar-pane {
@apply h-full w-auto max-w-60 border-none;
@apply w-full max-w-60 border-none;
& > div.header {
@apply flex items-center justify-between px-3 py-2 rounded-t-[0.5rem];

View File

@@ -11,7 +11,7 @@ import { Checkbox } from "@/src/components/Form/Checkbox";
import { Combobox } from "../Form/Combobox";
const meta: Meta<SidebarPaneProps> = {
title: "Components/Sidebar/Pane",
title: "Components/SidebarPane",
component: SidebarPane,
};

View File

@@ -2,6 +2,9 @@ import { callApi } from "@/src/hooks/api";
import { addClanURI, setActiveClanURI } from "@/src/stores/clan";
import { Params, Navigator, useParams } from "@solidjs/router";
export const encodeBase64 = (value: string) => window.btoa(value);
export const decodeBase64 = (value: string) => window.atob(value);
export const selectClanFolder = async () => {
const req = callApi("get_clan_folder", {});
const res = await req.result;
@@ -20,12 +23,43 @@ export const selectClanFolder = async () => {
throw new Error("Illegal state exception");
};
export const navigateToClan = (navigate: Navigator, uri: string) => {
navigate("/clans/" + window.btoa(uri));
export const buildClanPath = (clanURI: string) => {
return "/clans/" + encodeBase64(clanURI);
};
export const buildMachinePath = (clanURI: string, machineID: string) => {
return (
"/clans/" + encodeBase64(clanURI) + "/machines/" + encodeBase64(machineID)
);
};
export const navigateToClan = (navigate: Navigator, clanURI: string) => {
const path = buildClanPath(clanURI);
console.log("Navigating to clan", clanURI, path);
navigate(path);
};
export const navigateToMachine = (
navigate: Navigator,
clanURI: string,
machineID: string,
) => {
const path = buildMachinePath(clanURI, machineID);
console.log("Navigating to machine", clanURI, machineID, path);
navigate(path);
};
export const clanURIParam = (params: Params) => {
return window.atob(params.clanURI);
return decodeBase64(params.clanURI);
};
export const useClanURI = () => clanURIParam(useParams());
export const machineIDParam = (params: Params) => {
return decodeBase64(params.machineID);
};
export const useMachineID = (): string => {
const params = useParams();
return machineIDParam(params);
};

View File

@@ -17,11 +17,14 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
"Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
);
}
if (import.meta.env.DEV) {
console.log("Development mode");
}
render(
() => (
<QueryClientProvider client={client}>
{import.meta.env.DEV && <SolidQueryDevtools />}
{import.meta.env.DEV && <SolidQueryDevtools initialIsOpen={true} />}
<Router root={Layout}>{Routes}</Router>
</QueryClientProvider>
),

View File

@@ -1,20 +1,18 @@
import { useQuery, UseQueryResult } from "@tanstack/solid-query";
import { useQueries, useQuery, UseQueryResult } from "@tanstack/solid-query";
import { callApi, SuccessData } from "../hooks/api";
import { encodeBase64 } from "@/src/hooks/clan";
export type ClanDetails = SuccessData<"get_clan_details">;
export type ListMachines = SuccessData<"list_machines">;
export type MachinesQueryResult = UseQueryResult<ListMachines>;
interface MachinesQueryParams {
clanURI: string;
}
export const useMachinesQuery = (props: MachinesQueryParams) =>
export const useMachinesQuery = (clanURI: string) =>
useQuery<ListMachines>(() => ({
queryKey: ["clans", props.clanURI, "machines"],
queryKey: ["clans", encodeBase64(clanURI), "machines"],
queryFn: async () => {
const api = callApi("list_machines", {
flake: {
identifier: props.clanURI,
identifier: clanURI,
},
});
const result = await api.result;
@@ -25,3 +23,54 @@ export const useMachinesQuery = (props: MachinesQueryParams) =>
return result.data;
},
}));
export const useClanDetailsQuery = (clanURI: string) =>
useQuery<ClanDetails>(() => ({
queryKey: ["clans", encodeBase64(clanURI), "details"],
queryFn: async () => {
const call = callApi("get_clan_details", {
flake: {
identifier: clanURI,
},
});
const result = await call.result;
if (result.status === "error") {
// todo should we create some specific error types?
console.error("Error fetching clan details:", result.errors);
throw new Error(result.errors[0].message);
}
return {
uri: clanURI,
...result.data,
};
},
}));
export const useAllClanDetailsQuery = (clanURIs: string[]) =>
useQueries(() => ({
queries: clanURIs.map((clanURI) => ({
queryKey: ["clans", encodeBase64(clanURI), "details"],
enabled: !!clanURI,
queryFn: async () => {
const call = callApi("get_clan_details", {
flake: {
identifier: clanURI,
},
});
const result = await call.result;
if (result.status === "error") {
// todo should we create some specific error types?
console.error("Error fetching clan details:", result.errors);
throw new Error(result.errors[0].message);
}
return {
uri: clanURI,
...result.data,
};
},
})),
}));

View File

@@ -11,3 +11,14 @@
.create-modal {
@apply min-w-96;
}
.sidebar-container {
}
div.sidebar {
@apply absolute top-10 bottom-20 left-4 w-60;
}
div.sidebar-pane {
@apply absolute top-12 bottom-20 left-[16.5rem] w-64;
}

View File

@@ -13,19 +13,14 @@ import "./Clan.css";
import { Modal } from "@/src/components/Modal/Modal";
import { TextInput } from "@/src/components/Form/TextInput";
import { createForm, FieldValues, reset } from "@modular-forms/solid";
import { Sidebar } from "@/src/components/Sidebar/Sidebar";
export const Clan: Component<RouteSectionProps> = (props) => {
return (
<>
<div
style={{
position: "absolute",
top: 0,
}}
>
{props.children}
</div>
<ClanSceneController />
<Sidebar />
{props.children}
<ClanSceneController {...props} />
</>
);
};
@@ -89,7 +84,7 @@ const MockCreateMachine = (props: MockProps) => {
);
};
const ClanSceneController = () => {
const ClanSceneController = (props: RouteSectionProps) => {
const clanURI = useClanURI();
const [dialogHandlers, setDialogHandlers] = createSignal<{
@@ -232,7 +227,7 @@ const SceneDataProvider = (props: {
clanURI: string;
children: (sceneData: { query: MachinesQueryResult }) => JSX.Element;
}) => {
const machinesQuery = useMachinesQuery({ clanURI: props.clanURI });
const machinesQuery = useMachinesQuery(props.clanURI);
// This component can be used to provide scene data or context if needed
return props.children({ query: machinesQuery });

View File

@@ -0,0 +1,19 @@
import { RouteSectionProps, useNavigate } from "@solidjs/router";
import { SidebarPane } from "@/src/components/Sidebar/SidebarPane";
import { navigateToClan, useClanURI, useMachineID } from "@/src/hooks/clan";
export const Machine = (props: RouteSectionProps) => {
const navigate = useNavigate();
const clanURI = useClanURI();
const onClose = () => {
// go back to clan route
navigateToClan(navigate, clanURI);
};
return (
<SidebarPane title={useMachineID()} onClose={onClose}>
<h1>Hello world</h1>
</SidebarPane>
);
};

View File

@@ -1,6 +1,7 @@
import type { RouteDefinition } from "@solidjs/router/dist/types";
import { Onboarding } from "@/src/routes/Onboarding/Onboarding";
import { Clan } from "@/src/routes/Clan/Clan";
import { Machine } from "@/src/routes/Machine/Machine";
export const Routes: RouteDefinition[] = [
{
@@ -21,25 +22,14 @@ export const Routes: RouteDefinition[] = [
},
{
path: "/:clanURI",
component: Clan,
children: [
{
path: "/",
component: Clan,
},
{
path: "/machines",
children: [
{
path: "/",
component: () => <h1>Machines (Index)</h1>,
},
{
path: "/:machineID",
component: (props) => (
<h1>Machine ID: {props.params.machineID}</h1>
),
},
],
path: "/machines/:machineID",
component: Machine,
},
],
},

View File

@@ -20,6 +20,14 @@ const [store, setStore] = makePersisted(
},
);
const resetStore = () => {
setStore({
clanURIs: [],
activeClanURI: undefined,
sceneData: {},
});
};
/**
* Retrieves the active clan URI from the store.
*
@@ -92,4 +100,5 @@ export {
clanURIs,
addClanURI,
removeClanURI,
resetStore,
};

View File

@@ -0,0 +1 @@
/nix/store/q012qk78pwldxl3qjy09nwrx9jlamivm-nixpkgs