feat(ui): enable switching between clans
This commit is contained in:
@@ -18,7 +18,6 @@ import {
|
||||
OperationNames,
|
||||
OperationResponse,
|
||||
} from "@/src/hooks/api";
|
||||
import { mac } from "valibot";
|
||||
|
||||
const defaultClanURI = "/home/brian/clans/my-clan";
|
||||
|
||||
|
||||
@@ -5,8 +5,12 @@ import { useNavigate } from "@solidjs/router";
|
||||
import { Typography } from "../Typography/Typography";
|
||||
import { createSignal, For, Suspense } from "solid-js";
|
||||
import { useClanListQuery } from "@/src/hooks/queries";
|
||||
import { navigateToClan, useClanURI } from "@/src/hooks/clan";
|
||||
import { clanURIs } from "@/src/stores/clan";
|
||||
import {
|
||||
navigateToClan,
|
||||
navigateToOnboarding,
|
||||
useClanURI,
|
||||
} from "@/src/hooks/clan";
|
||||
import { clanURIs, setActiveClanURI } from "@/src/stores/clan";
|
||||
import { Button } from "../Button/Button";
|
||||
|
||||
export const SidebarHeader = () => {
|
||||
@@ -81,7 +85,7 @@ export const SidebarHeader = () => {
|
||||
ghost
|
||||
size="xs"
|
||||
startIcon="Plus"
|
||||
onClick={() => navigate("/?addClan=true")}
|
||||
onClick={() => navigateToOnboarding(navigate, true)}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
@@ -92,9 +96,9 @@ export const SidebarHeader = () => {
|
||||
<Suspense fallback={"Loading..."}>
|
||||
<DropdownMenu.Item
|
||||
class="dropdown-item"
|
||||
onSelect={() =>
|
||||
navigateToClan(navigate, clan.data!.uri)
|
||||
}
|
||||
onSelect={() => {
|
||||
setActiveClanURI(clan.data!.uri);
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
|
||||
@@ -36,6 +36,9 @@ export const navigateToClan = (navigate: Navigator, clanURI: string) => {
|
||||
navigate(path);
|
||||
};
|
||||
|
||||
export const navigateToOnboarding = (navigate: Navigator, addClan: boolean) =>
|
||||
navigate(`/${addClan ? "?addClan=true" : ""}`);
|
||||
|
||||
export const navigateToMachine = (
|
||||
navigate: Navigator,
|
||||
clanURI: string,
|
||||
|
||||
@@ -27,10 +27,6 @@ export type ClanListQueryResult = UseQueryResult<ClanDetailsWithURI>[];
|
||||
export const useMachinesQuery = (clanURI: string) => {
|
||||
const client = useApiClient();
|
||||
|
||||
if (!clanURI) {
|
||||
throw new Error("useMachinesQuery: clanURI is undefined");
|
||||
}
|
||||
|
||||
return useQuery<ListMachines>(() => ({
|
||||
queryKey: ["clans", encodeBase64(clanURI), "machines"],
|
||||
queryFn: async () => {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
.modal {
|
||||
@apply w-screen max-w-2xl h-fit flex flex-col;
|
||||
|
||||
.content {
|
||||
@apply flex flex-col gap-2;
|
||||
}
|
||||
|
||||
.header {
|
||||
@apply flex items-center justify-between;
|
||||
}
|
||||
|
||||
.clans {
|
||||
@apply flex flex-col gap-2;
|
||||
}
|
||||
|
||||
.clan {
|
||||
@apply flex items-center justify-between;
|
||||
@apply px-4 py-5 bg-def-2;
|
||||
|
||||
.meta {
|
||||
@apply flex flex-col gap-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
91
pkgs/clan-app/ui/src/routes/Onboarding/ListClansModal.tsx
Normal file
91
pkgs/clan-app/ui/src/routes/Onboarding/ListClansModal.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import { Modal } from "../../components/Modal/Modal";
|
||||
import cx from "classnames";
|
||||
import styles from "./ListClansModal.module.css";
|
||||
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 { activeClanURI, clanURIs, setActiveClanURI } from "@/src/stores/clan";
|
||||
import { useClanListQuery } from "@/src/hooks/queries";
|
||||
|
||||
export interface ClansModalProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const ListClansModal = (props: ClansModalProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const query = useClanListQuery(clanURIs());
|
||||
|
||||
// we only want clans we could interrogate successfully
|
||||
// todo how to surface the ones that failed to users?
|
||||
const clanList = () => query.filter((it) => it.isSuccess);
|
||||
|
||||
const selectClan = (uri: string) => () => {
|
||||
if (uri == activeClanURI()) {
|
||||
navigateToClan(navigate, uri);
|
||||
} else {
|
||||
setActiveClanURI(uri);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Select Clan"
|
||||
open
|
||||
onClose={props.onClose}
|
||||
class={cx(styles.modal)}
|
||||
>
|
||||
<div class={cx(styles.content)}>
|
||||
<div class={cx(styles.header)}>
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
family="mono"
|
||||
size="xs"
|
||||
color="tertiary"
|
||||
transform="uppercase"
|
||||
>
|
||||
Your Clans
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
hierarchy="secondary"
|
||||
ghost
|
||||
size="s"
|
||||
startIcon="Plus"
|
||||
onClick={() => {
|
||||
props.onClose();
|
||||
navigateToOnboarding(navigate, true);
|
||||
}}
|
||||
>
|
||||
Add Clan
|
||||
</Button>
|
||||
</div>
|
||||
<ul class={cx(styles.clans)}>
|
||||
<For each={clanList()}>
|
||||
{(clan) => (
|
||||
<li class={cx(styles.clan)}>
|
||||
<div class={cx(styles.meta)}>
|
||||
<Typography hierarchy="label" weight="bold" size="default">
|
||||
{clan.data.name}
|
||||
</Typography>
|
||||
<Typography hierarchy="body" size="s">
|
||||
{clan.data.description}
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
hierarchy="secondary"
|
||||
ghost
|
||||
icon="CaretRight"
|
||||
onClick={selectClan(clan.data.uri)}
|
||||
/>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -33,6 +33,10 @@ main#welcome {
|
||||
@apply w-16;
|
||||
@apply absolute bottom-28 left-1/2 transform -translate-x-1/2;
|
||||
}
|
||||
|
||||
button.list-clans {
|
||||
@apply absolute bottom-28 right-0 transform -translate-x-1/2;
|
||||
}
|
||||
}
|
||||
|
||||
& > div.container {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
createSignal,
|
||||
Match,
|
||||
Setter,
|
||||
Show,
|
||||
Switch,
|
||||
} from "solid-js";
|
||||
import {
|
||||
@@ -36,6 +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";
|
||||
|
||||
type State = "welcome" | "setup" | "creating";
|
||||
|
||||
@@ -59,14 +61,32 @@ const SetupSchema = v.object({
|
||||
|
||||
type SetupForm = v.InferInput<typeof SetupSchema>;
|
||||
|
||||
const background = (props: { state: State; form: FormStore<SetupForm> }) => (
|
||||
<div class="background">
|
||||
<div class="layer-1" />
|
||||
<div class="layer-2" />
|
||||
<div class="layer-3" />
|
||||
<Logo variant="Clan" inverted={true} />
|
||||
</div>
|
||||
);
|
||||
const background = (props: { state: State; form: FormStore<SetupForm> }) => {
|
||||
// controls whether the list clans modal is displayed
|
||||
const [showModal, setShowModal] = createSignal(false);
|
||||
|
||||
return (
|
||||
<div class="background">
|
||||
<div class="layer-1" />
|
||||
<div class="layer-2" />
|
||||
<div class="layer-3" />
|
||||
<Logo variant="Clan" inverted />
|
||||
<Button
|
||||
class="list-clans"
|
||||
hierarchy="primary"
|
||||
ghost
|
||||
size="s"
|
||||
startIcon="Grid"
|
||||
onClick={() => setShowModal(true)}
|
||||
>
|
||||
All Clans
|
||||
</Button>
|
||||
<Show when={showModal()}>
|
||||
<ListClansModal onClose={() => setShowModal(false)} />
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const welcome = (props: {
|
||||
setState: Setter<State>;
|
||||
@@ -147,17 +167,13 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
|
||||
|
||||
const activeURI = activeClanURI();
|
||||
|
||||
console.log("onboarding params", searchParams.addClan);
|
||||
|
||||
if (!searchParams.addClan && activeURI) {
|
||||
// the user has already selected a clan, so we should navigate to it
|
||||
console.log("active clan detected, navigating to it", activeURI);
|
||||
navigateToClan(navigate, activeURI);
|
||||
}
|
||||
|
||||
const [state, setState] = createSignal<State>(
|
||||
searchParams.addClan ? "setup" : "welcome",
|
||||
);
|
||||
const [state, setState] = createSignal<State>("welcome");
|
||||
|
||||
// used to display an error in the welcome screen in the event of a failed
|
||||
// clan creation
|
||||
@@ -280,15 +296,7 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
|
||||
hierarchy="secondary"
|
||||
ghost={true}
|
||||
icon="ArrowLeft"
|
||||
onClick={() => {
|
||||
// if we arrived here by way of adding a clan then we return to the active clan
|
||||
if (searchParams.addClan && activeURI) {
|
||||
navigateToClan(navigate, activeURI);
|
||||
}
|
||||
|
||||
// otherwise we return to the initial welcome screen
|
||||
setState("welcome");
|
||||
}}
|
||||
onClick={() => setState("welcome")}
|
||||
/>
|
||||
<Typography hierarchy="headline" size="default" weight="bold">
|
||||
Setup
|
||||
|
||||
Reference in New Issue
Block a user