feat(ui): enable switching between clans
This commit is contained in:
@@ -18,7 +18,6 @@ import {
|
|||||||
OperationNames,
|
OperationNames,
|
||||||
OperationResponse,
|
OperationResponse,
|
||||||
} from "@/src/hooks/api";
|
} from "@/src/hooks/api";
|
||||||
import { mac } from "valibot";
|
|
||||||
|
|
||||||
const defaultClanURI = "/home/brian/clans/my-clan";
|
const defaultClanURI = "/home/brian/clans/my-clan";
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,12 @@ import { useNavigate } from "@solidjs/router";
|
|||||||
import { Typography } from "../Typography/Typography";
|
import { Typography } from "../Typography/Typography";
|
||||||
import { createSignal, For, Suspense } from "solid-js";
|
import { createSignal, For, Suspense } from "solid-js";
|
||||||
import { useClanListQuery } from "@/src/hooks/queries";
|
import { useClanListQuery } from "@/src/hooks/queries";
|
||||||
import { navigateToClan, useClanURI } from "@/src/hooks/clan";
|
import {
|
||||||
import { clanURIs } from "@/src/stores/clan";
|
navigateToClan,
|
||||||
|
navigateToOnboarding,
|
||||||
|
useClanURI,
|
||||||
|
} from "@/src/hooks/clan";
|
||||||
|
import { clanURIs, setActiveClanURI } from "@/src/stores/clan";
|
||||||
import { Button } from "../Button/Button";
|
import { Button } from "../Button/Button";
|
||||||
|
|
||||||
export const SidebarHeader = () => {
|
export const SidebarHeader = () => {
|
||||||
@@ -81,7 +85,7 @@ export const SidebarHeader = () => {
|
|||||||
ghost
|
ghost
|
||||||
size="xs"
|
size="xs"
|
||||||
startIcon="Plus"
|
startIcon="Plus"
|
||||||
onClick={() => navigate("/?addClan=true")}
|
onClick={() => navigateToOnboarding(navigate, true)}
|
||||||
>
|
>
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
@@ -92,9 +96,9 @@ export const SidebarHeader = () => {
|
|||||||
<Suspense fallback={"Loading..."}>
|
<Suspense fallback={"Loading..."}>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="dropdown-item"
|
class="dropdown-item"
|
||||||
onSelect={() =>
|
onSelect={() => {
|
||||||
navigateToClan(navigate, clan.data!.uri)
|
setActiveClanURI(clan.data!.uri);
|
||||||
}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
hierarchy="label"
|
hierarchy="label"
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ export const navigateToClan = (navigate: Navigator, clanURI: string) => {
|
|||||||
navigate(path);
|
navigate(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const navigateToOnboarding = (navigate: Navigator, addClan: boolean) =>
|
||||||
|
navigate(`/${addClan ? "?addClan=true" : ""}`);
|
||||||
|
|
||||||
export const navigateToMachine = (
|
export const navigateToMachine = (
|
||||||
navigate: Navigator,
|
navigate: Navigator,
|
||||||
clanURI: string,
|
clanURI: string,
|
||||||
|
|||||||
@@ -27,10 +27,6 @@ export type ClanListQueryResult = UseQueryResult<ClanDetailsWithURI>[];
|
|||||||
export const useMachinesQuery = (clanURI: string) => {
|
export const useMachinesQuery = (clanURI: string) => {
|
||||||
const client = useApiClient();
|
const client = useApiClient();
|
||||||
|
|
||||||
if (!clanURI) {
|
|
||||||
throw new Error("useMachinesQuery: clanURI is undefined");
|
|
||||||
}
|
|
||||||
|
|
||||||
return useQuery<ListMachines>(() => ({
|
return useQuery<ListMachines>(() => ({
|
||||||
queryKey: ["clans", encodeBase64(clanURI), "machines"],
|
queryKey: ["clans", encodeBase64(clanURI), "machines"],
|
||||||
queryFn: async () => {
|
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 w-16;
|
||||||
@apply absolute bottom-28 left-1/2 transform -translate-x-1/2;
|
@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 {
|
& > div.container {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
createSignal,
|
createSignal,
|
||||||
Match,
|
Match,
|
||||||
Setter,
|
Setter,
|
||||||
|
Show,
|
||||||
Switch,
|
Switch,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import {
|
import {
|
||||||
@@ -36,6 +37,7 @@ import { HostFileInput } from "@/src/components/Form/HostFileInput";
|
|||||||
import { callApi } from "@/src/hooks/api";
|
import { callApi } from "@/src/hooks/api";
|
||||||
import { Creating } from "./Creating";
|
import { Creating } from "./Creating";
|
||||||
import { useApiClient } from "@/src/hooks/ApiClient";
|
import { useApiClient } from "@/src/hooks/ApiClient";
|
||||||
|
import { ListClansModal } from "@/src/routes/Onboarding/ListClansModal";
|
||||||
|
|
||||||
type State = "welcome" | "setup" | "creating";
|
type State = "welcome" | "setup" | "creating";
|
||||||
|
|
||||||
@@ -59,14 +61,32 @@ const SetupSchema = v.object({
|
|||||||
|
|
||||||
type SetupForm = v.InferInput<typeof SetupSchema>;
|
type SetupForm = v.InferInput<typeof SetupSchema>;
|
||||||
|
|
||||||
const background = (props: { state: State; form: FormStore<SetupForm> }) => (
|
const background = (props: { state: State; form: FormStore<SetupForm> }) => {
|
||||||
<div class="background">
|
// controls whether the list clans modal is displayed
|
||||||
<div class="layer-1" />
|
const [showModal, setShowModal] = createSignal(false);
|
||||||
<div class="layer-2" />
|
|
||||||
<div class="layer-3" />
|
return (
|
||||||
<Logo variant="Clan" inverted={true} />
|
<div class="background">
|
||||||
</div>
|
<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: {
|
const welcome = (props: {
|
||||||
setState: Setter<State>;
|
setState: Setter<State>;
|
||||||
@@ -147,17 +167,13 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
|
|||||||
|
|
||||||
const activeURI = activeClanURI();
|
const activeURI = activeClanURI();
|
||||||
|
|
||||||
console.log("onboarding params", searchParams.addClan);
|
|
||||||
|
|
||||||
if (!searchParams.addClan && activeURI) {
|
if (!searchParams.addClan && activeURI) {
|
||||||
// the user has already selected a clan, so we should navigate to it
|
// the user has already selected a clan, so we should navigate to it
|
||||||
console.log("active clan detected, navigating to it", activeURI);
|
console.log("active clan detected, navigating to it", activeURI);
|
||||||
navigateToClan(navigate, activeURI);
|
navigateToClan(navigate, activeURI);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [state, setState] = createSignal<State>(
|
const [state, setState] = createSignal<State>("welcome");
|
||||||
searchParams.addClan ? "setup" : "welcome",
|
|
||||||
);
|
|
||||||
|
|
||||||
// used to display an error in the welcome screen in the event of a failed
|
// used to display an error in the welcome screen in the event of a failed
|
||||||
// clan creation
|
// clan creation
|
||||||
@@ -280,15 +296,7 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
|
|||||||
hierarchy="secondary"
|
hierarchy="secondary"
|
||||||
ghost={true}
|
ghost={true}
|
||||||
icon="ArrowLeft"
|
icon="ArrowLeft"
|
||||||
onClick={() => {
|
onClick={() => setState("welcome")}
|
||||||
// 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");
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Typography hierarchy="headline" size="default" weight="bold">
|
<Typography hierarchy="headline" size="default" weight="bold">
|
||||||
Setup
|
Setup
|
||||||
|
|||||||
Reference in New Issue
Block a user