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

View File

@@ -15,10 +15,11 @@ div.sidebar-header {
transition: all 250ms ease-in-out; transition: all 250ms ease-in-out;
div.title { div.clan-label {
@apply flex items-center gap-2 justify-start; @apply flex items-center gap-2 justify-start;
& > .clan-icon { & > .clan-icon {
@apply flex justify-center items-center;
@apply rounded-full bg-inv-4 w-7 h-7; @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 { div.sidebar-pane {
@apply h-full w-auto max-w-60 border-none; @apply w-full max-w-60 border-none;
& > div.header { & > div.header {
@apply flex items-center justify-between px-3 py-2 rounded-t-[0.5rem]; @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"; import { Combobox } from "../Form/Combobox";
const meta: Meta<SidebarPaneProps> = { const meta: Meta<SidebarPaneProps> = {
title: "Components/Sidebar/Pane", title: "Components/SidebarPane",
component: SidebarPane, component: SidebarPane,
}; };

View File

@@ -2,6 +2,9 @@ import { callApi } from "@/src/hooks/api";
import { addClanURI, setActiveClanURI } from "@/src/stores/clan"; import { addClanURI, setActiveClanURI } from "@/src/stores/clan";
import { Params, Navigator, useParams } from "@solidjs/router"; 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 () => { export const selectClanFolder = async () => {
const req = callApi("get_clan_folder", {}); const req = callApi("get_clan_folder", {});
const res = await req.result; const res = await req.result;
@@ -20,12 +23,43 @@ export const selectClanFolder = async () => {
throw new Error("Illegal state exception"); throw new Error("Illegal state exception");
}; };
export const navigateToClan = (navigate: Navigator, uri: string) => { export const buildClanPath = (clanURI: string) => {
navigate("/clans/" + window.btoa(uri)); 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) => { export const clanURIParam = (params: Params) => {
return window.atob(params.clanURI); return decodeBase64(params.clanURI);
}; };
export const useClanURI = () => clanURIParam(useParams()); 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?", "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( render(
() => ( () => (
<QueryClientProvider client={client}> <QueryClientProvider client={client}>
{import.meta.env.DEV && <SolidQueryDevtools />} {import.meta.env.DEV && <SolidQueryDevtools initialIsOpen={true} />}
<Router root={Layout}>{Routes}</Router> <Router root={Layout}>{Routes}</Router>
</QueryClientProvider> </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 { 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 ListMachines = SuccessData<"list_machines">;
export type MachinesQueryResult = UseQueryResult<ListMachines>; export type MachinesQueryResult = UseQueryResult<ListMachines>;
interface MachinesQueryParams { export const useMachinesQuery = (clanURI: string) =>
clanURI: string;
}
export const useMachinesQuery = (props: MachinesQueryParams) =>
useQuery<ListMachines>(() => ({ useQuery<ListMachines>(() => ({
queryKey: ["clans", props.clanURI, "machines"], queryKey: ["clans", encodeBase64(clanURI), "machines"],
queryFn: async () => { queryFn: async () => {
const api = callApi("list_machines", { const api = callApi("list_machines", {
flake: { flake: {
identifier: props.clanURI, identifier: clanURI,
}, },
}); });
const result = await api.result; const result = await api.result;
@@ -25,3 +23,54 @@ export const useMachinesQuery = (props: MachinesQueryParams) =>
return result.data; 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 { .create-modal {
@apply min-w-96; @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 { Modal } from "@/src/components/Modal/Modal";
import { TextInput } from "@/src/components/Form/TextInput"; import { TextInput } from "@/src/components/Form/TextInput";
import { createForm, FieldValues, reset } from "@modular-forms/solid"; import { createForm, FieldValues, reset } from "@modular-forms/solid";
import { Sidebar } from "@/src/components/Sidebar/Sidebar";
export const Clan: Component<RouteSectionProps> = (props) => { export const Clan: Component<RouteSectionProps> = (props) => {
return ( return (
<> <>
<div <Sidebar />
style={{ {props.children}
position: "absolute", <ClanSceneController {...props} />
top: 0,
}}
>
{props.children}
</div>
<ClanSceneController />
</> </>
); );
}; };
@@ -89,7 +84,7 @@ const MockCreateMachine = (props: MockProps) => {
); );
}; };
const ClanSceneController = () => { const ClanSceneController = (props: RouteSectionProps) => {
const clanURI = useClanURI(); const clanURI = useClanURI();
const [dialogHandlers, setDialogHandlers] = createSignal<{ const [dialogHandlers, setDialogHandlers] = createSignal<{
@@ -232,7 +227,7 @@ const SceneDataProvider = (props: {
clanURI: string; clanURI: string;
children: (sceneData: { query: MachinesQueryResult }) => JSX.Element; 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 // This component can be used to provide scene data or context if needed
return props.children({ query: machinesQuery }); 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 type { RouteDefinition } from "@solidjs/router/dist/types";
import { Onboarding } from "@/src/routes/Onboarding/Onboarding"; import { Onboarding } from "@/src/routes/Onboarding/Onboarding";
import { Clan } from "@/src/routes/Clan/Clan"; import { Clan } from "@/src/routes/Clan/Clan";
import { Machine } from "@/src/routes/Machine/Machine";
export const Routes: RouteDefinition[] = [ export const Routes: RouteDefinition[] = [
{ {
@@ -21,25 +22,14 @@ export const Routes: RouteDefinition[] = [
}, },
{ {
path: "/:clanURI", path: "/:clanURI",
component: Clan,
children: [ children: [
{ {
path: "/", path: "/",
component: Clan,
}, },
{ {
path: "/machines", path: "/machines/:machineID",
children: [ component: Machine,
{
path: "/",
component: () => <h1>Machines (Index)</h1>,
},
{
path: "/:machineID",
component: (props) => (
<h1>Machine ID: {props.params.machineID}</h1>
),
},
],
}, },
], ],
}, },

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. * Retrieves the active clan URI from the store.
* *
@@ -92,4 +100,5 @@ export {
clanURIs, clanURIs,
addClanURI, addClanURI,
removeClanURI, removeClanURI,
resetStore,
}; };

View File

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