feat(ui): introduce a top-level Clan context
This commit is contained in:
@@ -5,15 +5,20 @@ import { Typography } from "@/src/components/Typography/Typography";
|
||||
import { Button } from "@/src/components/Button/Button";
|
||||
import { navigateToClan, navigateToOnboarding } from "@/src/hooks/clan";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { For } from "solid-js";
|
||||
import { For, Show } from "solid-js";
|
||||
import { activeClanURI, clanURIs, setActiveClanURI } from "@/src/stores/clan";
|
||||
import { useClanListQuery } from "@/src/hooks/queries";
|
||||
import { Alert } from "@/src/components/Alert/Alert";
|
||||
|
||||
export interface ClansModalProps {
|
||||
export interface ListClansModalProps {
|
||||
onClose: () => void;
|
||||
error?: {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const ListClansModal = (props: ClansModalProps) => {
|
||||
export const ListClansModal = (props: ListClansModalProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const query = useClanListQuery(clanURIs());
|
||||
@@ -38,6 +43,14 @@ export const ListClansModal = (props: ClansModalProps) => {
|
||||
class={cx(styles.modal)}
|
||||
>
|
||||
<div class={cx(styles.content)}>
|
||||
<Show when={props.error}>
|
||||
<Alert
|
||||
type="error"
|
||||
title={props.error?.title || ""}
|
||||
description={props.error?.description}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<div class={cx(styles.header)}>
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
@@ -3,11 +3,12 @@ import { A } from "@solidjs/router";
|
||||
import { Accordion } from "@kobalte/core/accordion";
|
||||
import Icon from "../Icon/Icon";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
import { For } from "solid-js";
|
||||
import { For, useContext } from "solid-js";
|
||||
import { MachineStatus } from "@/src/components/MachineStatus/MachineStatus";
|
||||
import { buildMachinePath, useClanURI } from "@/src/hooks/clan";
|
||||
import { useMachinesQuery, useMachineStateQuery } from "@/src/hooks/queries";
|
||||
import { useMachineStateQuery } from "@/src/hooks/queries";
|
||||
import { SidebarProps } from "./Sidebar";
|
||||
import { ClanContext } from "@/src/routes/Clan/Clan";
|
||||
|
||||
interface MachineProps {
|
||||
clanURI: string;
|
||||
@@ -56,7 +57,11 @@ const MachineRoute = (props: MachineProps) => {
|
||||
|
||||
export const SidebarBody = (props: SidebarProps) => {
|
||||
const clanURI = useClanURI();
|
||||
const machineList = useMachinesQuery(clanURI);
|
||||
|
||||
const ctx = useContext(ClanContext);
|
||||
if (!ctx) {
|
||||
throw new Error("ClanContext not found");
|
||||
}
|
||||
|
||||
const sectionLabels = (props.staticSections || []).map(
|
||||
(section) => section.title,
|
||||
@@ -96,7 +101,7 @@ export const SidebarBody = (props: SidebarProps) => {
|
||||
</Accordion.Header>
|
||||
<Accordion.Content class="content">
|
||||
<nav>
|
||||
<For each={Object.entries(machineList.data || {})}>
|
||||
<For each={Object.entries(ctx.machinesQuery.data || {})}>
|
||||
{([id, machine]) => (
|
||||
<MachineRoute
|
||||
clanURI={clanURI}
|
||||
|
||||
@@ -3,15 +3,15 @@ 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 { useClanListQuery } from "@/src/hooks/queries";
|
||||
import { createSignal, For, Suspense, useContext } from "solid-js";
|
||||
import {
|
||||
navigateToClan,
|
||||
navigateToOnboarding,
|
||||
useClanURI,
|
||||
} from "@/src/hooks/clan";
|
||||
import { clanURIs, setActiveClanURI } from "@/src/stores/clan";
|
||||
import { setActiveClanURI } from "@/src/stores/clan";
|
||||
import { Button } from "../Button/Button";
|
||||
import { ClanContext } from "@/src/routes/Clan/Clan";
|
||||
|
||||
export const SidebarHeader = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -19,10 +19,19 @@ export const SidebarHeader = () => {
|
||||
const [open, setOpen] = createSignal(false);
|
||||
|
||||
// get information about the current active clan
|
||||
const clanURI = useClanURI();
|
||||
const allClans = useClanListQuery(clanURIs());
|
||||
const ctx = useContext(ClanContext);
|
||||
|
||||
const activeClan = () => allClans.find(({ data }) => data?.uri === clanURI);
|
||||
if (!ctx) {
|
||||
throw new Error("SidebarContext not found");
|
||||
}
|
||||
|
||||
const clanURI = useClanURI();
|
||||
|
||||
const clanChar = () =>
|
||||
ctx?.activeClanQuery?.data?.name.charAt(0).toUpperCase();
|
||||
const clanName = () => ctx?.activeClanQuery?.data?.name;
|
||||
|
||||
const clans = () => ctx.otherClanQueries.filter((clan) => !clan.isError);
|
||||
|
||||
return (
|
||||
<div class="sidebar-header">
|
||||
@@ -37,7 +46,7 @@ export const SidebarHeader = () => {
|
||||
weight="bold"
|
||||
inverted={true}
|
||||
>
|
||||
{activeClan()?.data?.name.charAt(0).toUpperCase()}
|
||||
{clanChar()}
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography
|
||||
@@ -46,7 +55,7 @@ export const SidebarHeader = () => {
|
||||
weight="bold"
|
||||
inverted={!open()}
|
||||
>
|
||||
{activeClan()?.data?.name}
|
||||
{clanName()}
|
||||
</Typography>
|
||||
</div>
|
||||
<DropdownMenu.Icon>
|
||||
@@ -91,7 +100,7 @@ export const SidebarHeader = () => {
|
||||
</Button>
|
||||
</DropdownMenu.GroupLabel>
|
||||
<div class="dropdown-group-items">
|
||||
<For each={allClans}>
|
||||
<For each={clans()}>
|
||||
{(clan) => (
|
||||
<Suspense fallback={"Loading..."}>
|
||||
<DropdownMenu.Item
|
||||
|
||||
@@ -157,7 +157,7 @@ export const useMachineDetailsQuery = (
|
||||
|
||||
export const useClanDetailsQuery = (clanURI: string) => {
|
||||
const client = useApiClient();
|
||||
return useQuery<ClanDetails>(() => ({
|
||||
return useQuery<ClanDetailsWithURI>(() => ({
|
||||
queryKey: ["clans", encodeBase64(clanURI), "details"],
|
||||
queryFn: async () => {
|
||||
const call = client.fetch("get_clan_details", {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { RouteSectionProps } from "@solidjs/router";
|
||||
import { RouteSectionProps, useNavigate } from "@solidjs/router";
|
||||
import {
|
||||
Component,
|
||||
JSX,
|
||||
Show,
|
||||
createContext,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
on,
|
||||
onMount,
|
||||
Show,
|
||||
useContext,
|
||||
} from "solid-js";
|
||||
import {
|
||||
buildMachinePath,
|
||||
@@ -16,13 +17,14 @@ import {
|
||||
} from "@/src/hooks/clan";
|
||||
import { CubeScene } from "@/src/scene/cubes";
|
||||
import {
|
||||
ClanListQueryResult,
|
||||
ClanDetailsWithURI,
|
||||
MachinesQueryResult,
|
||||
useClanDetailsQuery,
|
||||
useClanListQuery,
|
||||
useMachinesQuery,
|
||||
} from "@/src/hooks/queries";
|
||||
import { callApi } from "@/src/hooks/api";
|
||||
import { store, setStore, clanURIs } from "@/src/stores/clan";
|
||||
import { clanURIs, setStore, store } from "@/src/stores/clan";
|
||||
import { produce } from "solid-js/store";
|
||||
import { Button } from "@/src/components/Button/Button";
|
||||
import { Splash } from "@/src/scene/splash";
|
||||
@@ -32,15 +34,36 @@ 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";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { UseQueryResult } from "@tanstack/solid-query";
|
||||
|
||||
export const ClanContext = createContext<{
|
||||
machinesQuery: MachinesQueryResult;
|
||||
activeClanQuery: UseQueryResult<ClanDetailsWithURI>;
|
||||
otherClanQueries: UseQueryResult<ClanDetailsWithURI>[];
|
||||
}>();
|
||||
|
||||
export const Clan: Component<RouteSectionProps> = (props) => {
|
||||
const clanURI = useClanURI();
|
||||
const activeClanQuery = useClanDetailsQuery(clanURI);
|
||||
|
||||
const otherClanQueries = useClanListQuery(
|
||||
clanURIs().filter((uri) => uri !== clanURI),
|
||||
);
|
||||
|
||||
const machinesQuery = useMachinesQuery(clanURI);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ClanContext.Provider
|
||||
value={{
|
||||
machinesQuery,
|
||||
activeClanQuery,
|
||||
otherClanQueries,
|
||||
}}
|
||||
>
|
||||
<Sidebar class={cx(styles.sidebar)} />
|
||||
{props.children}
|
||||
<ClanSceneController {...props} />
|
||||
</>
|
||||
</ClanContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -98,7 +121,10 @@ const ClanSceneController = (props: RouteSectionProps) => {
|
||||
const clanURI = useClanURI();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const machinesQuery = useMachinesQuery(clanURI);
|
||||
const ctx = useContext(ClanContext);
|
||||
if (!ctx) {
|
||||
throw new Error("ClanContext not found");
|
||||
}
|
||||
|
||||
const [dialogHandlers, setDialogHandlers] = createSignal<{
|
||||
resolve: ({ id }: { id: string }) => void;
|
||||
@@ -133,7 +159,7 @@ const ClanSceneController = (props: RouteSectionProps) => {
|
||||
}
|
||||
|
||||
// trigger a refetch of the machines query
|
||||
machinesQuery.refetch();
|
||||
ctx.machinesQuery.refetch();
|
||||
|
||||
return { id: values.name };
|
||||
};
|
||||
@@ -177,102 +203,87 @@ const ClanSceneController = (props: RouteSectionProps) => {
|
||||
}),
|
||||
);
|
||||
|
||||
// a combination of the individual clan details query status and the machines query status
|
||||
// the cube scene needs the machines query, the sidebar needs the clans query and machines query results
|
||||
// so we wait on both before removing the loader to avoid any loading artefacts
|
||||
const isLoading = (): boolean => {
|
||||
// check if the active clan query is still loading
|
||||
if (ctx.activeClanQuery.isLoading) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// check the machines query first
|
||||
if (ctx.machinesQuery.isLoading) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// otherwise iterate the clans query and return early if we find a queries that is still loading
|
||||
for (const query of ctx.otherClanQueries) {
|
||||
if (query.isLoading) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
return (
|
||||
<SceneDataProvider clanURI={clanURI}>
|
||||
{({ clansQuery, machinesQuery }) => {
|
||||
// a combination of the individual clan details query status and the machines query status
|
||||
// the cube scene needs the machines query, the sidebar needs the clans query and machines query results
|
||||
// so we wait on both before removing the loader to avoid any loading artefacts
|
||||
const isLoading = (): boolean => {
|
||||
// check the machines query first
|
||||
if (machinesQuery.isLoading) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// otherwise iterate the clans query and return early if we find a queries that is still loading
|
||||
for (const query of clansQuery) {
|
||||
if (query.isLoading) {
|
||||
return true;
|
||||
<>
|
||||
<Show when={showModal()}>
|
||||
<MockCreateMachine
|
||||
onClose={() => {
|
||||
setShowModal(false);
|
||||
dialogHandlers()?.reject(new Error("User cancelled"));
|
||||
}}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
const result = await sendCreate(values);
|
||||
dialogHandlers()?.resolve(result);
|
||||
setShowModal(false);
|
||||
} catch (err) {
|
||||
dialogHandlers()?.reject(err);
|
||||
setShowModal(false);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
<div
|
||||
class={cx({
|
||||
[styles.fadeOut]: !ctx.machinesQuery.isLoading && loadingCooldown(),
|
||||
})}
|
||||
>
|
||||
<Splash />
|
||||
</div>
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Show when={showModal()}>
|
||||
<MockCreateMachine
|
||||
onClose={() => {
|
||||
setShowModal(false);
|
||||
dialogHandlers()?.reject(new Error("User cancelled"));
|
||||
}}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
const result = await sendCreate(values);
|
||||
dialogHandlers()?.resolve(result);
|
||||
setShowModal(false);
|
||||
} catch (err) {
|
||||
dialogHandlers()?.reject(err);
|
||||
setShowModal(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
<div
|
||||
class={cx({
|
||||
[styles.fadeOut]: !machinesQuery.isLoading && loadingCooldown(),
|
||||
})}
|
||||
>
|
||||
<Splash />
|
||||
</div>
|
||||
|
||||
<CubeScene
|
||||
selectedIds={selectedIds}
|
||||
onSelect={onMachineSelect}
|
||||
isLoading={isLoading()}
|
||||
cubesQuery={machinesQuery}
|
||||
onCreate={onCreate}
|
||||
sceneStore={() => {
|
||||
const clanURI = useClanURI();
|
||||
return store.sceneData?.[clanURI];
|
||||
}}
|
||||
setMachinePos={(machineId: string, pos: [number, number]) => {
|
||||
console.log("calling setStore", machineId, pos);
|
||||
setStore(
|
||||
produce((s) => {
|
||||
if (!s.sceneData) {
|
||||
s.sceneData = {};
|
||||
}
|
||||
if (!s.sceneData[clanURI]) {
|
||||
s.sceneData[clanURI] = {};
|
||||
}
|
||||
if (!s.sceneData[clanURI][machineId]) {
|
||||
s.sceneData[clanURI][machineId] = { position: pos };
|
||||
} else {
|
||||
s.sceneData[clanURI][machineId].position = pos;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</SceneDataProvider>
|
||||
<CubeScene
|
||||
selectedIds={selectedIds}
|
||||
onSelect={onMachineSelect}
|
||||
isLoading={isLoading()}
|
||||
cubesQuery={ctx.machinesQuery}
|
||||
onCreate={onCreate}
|
||||
sceneStore={() => {
|
||||
const clanURI = useClanURI();
|
||||
return store.sceneData?.[clanURI];
|
||||
}}
|
||||
setMachinePos={(machineId: string, pos: [number, number]) => {
|
||||
console.log("calling setStore", machineId, pos);
|
||||
setStore(
|
||||
produce((s) => {
|
||||
if (!s.sceneData) {
|
||||
s.sceneData = {};
|
||||
}
|
||||
if (!s.sceneData[clanURI]) {
|
||||
s.sceneData[clanURI] = {};
|
||||
}
|
||||
if (!s.sceneData[clanURI][machineId]) {
|
||||
s.sceneData[clanURI][machineId] = { position: pos };
|
||||
} else {
|
||||
s.sceneData[clanURI][machineId].position = pos;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SceneDataProvider = (props: {
|
||||
clanURI: string;
|
||||
children: (sceneData: {
|
||||
clansQuery: ClanListQueryResult;
|
||||
machinesQuery: MachinesQueryResult;
|
||||
}) => JSX.Element;
|
||||
}) => {
|
||||
const clansQuery = useClanListQuery(clanURIs());
|
||||
const machinesQuery = useMachinesQuery(props.clanURI);
|
||||
|
||||
// This component can be used to provide scene data or context if needed
|
||||
return props.children({ clansQuery, machinesQuery });
|
||||
};
|
||||
|
||||
@@ -37,7 +37,7 @@ import { HostFileInput } from "@/src/components/Form/HostFileInput";
|
||||
import { callApi } from "@/src/hooks/api";
|
||||
import { Creating } from "./Creating";
|
||||
import { useApiClient } from "@/src/hooks/ApiClient";
|
||||
import { ListClansModal } from "@/src/routes/Onboarding/ListClansModal";
|
||||
import { ListClansModal } from "@/src/components/ListClansModal/ListClansModal";
|
||||
|
||||
type State = "welcome" | "setup" | "creating";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user