wip
This commit is contained in:
92
pkgs/clan-app/ui/src/hooks/api.ts
Normal file
92
pkgs/clan-app/ui/src/hooks/api.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { API } from "@/api/API";
|
||||
import { Schema as Inventory } from "@/api/Inventory";
|
||||
|
||||
type OperationNames = keyof API;
|
||||
type Services = NonNullable<Inventory["services"]>;
|
||||
type ServiceNames = keyof Services;
|
||||
|
||||
export type OperationArgs<T extends OperationNames> = API[T]["arguments"];
|
||||
export type OperationResponse<T extends OperationNames> = API[T]["return"];
|
||||
|
||||
export type ClanServiceInstance<T extends ServiceNames> = NonNullable<
|
||||
Services[T]
|
||||
>[string];
|
||||
|
||||
export type SuccessQuery<T extends OperationNames> = Extract<
|
||||
OperationResponse<T>,
|
||||
{ status: "success" }
|
||||
>;
|
||||
export type SuccessData<T extends OperationNames> = SuccessQuery<T>["data"];
|
||||
|
||||
interface SendHeaderType {
|
||||
logging?: { group_path: string[] };
|
||||
}
|
||||
interface BackendSendType<K extends OperationNames> {
|
||||
body: OperationArgs<K>;
|
||||
header?: SendHeaderType;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
interface ReceiveHeaderType {}
|
||||
interface BackendReturnType<K extends OperationNames> {
|
||||
body: OperationResponse<K>;
|
||||
header: ReceiveHeaderType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface representing an API call with a unique identifier, result promise, and cancellation capability.
|
||||
*
|
||||
* @template K - A generic type parameter extending the set of operation names.
|
||||
*
|
||||
* @property {string} uuid - A unique identifier for the API call.
|
||||
* @property {Promise<BackendReturnType<K>>} result - A promise that resolves to the return type of the backend operation.
|
||||
* @property {() => Promise<void>} cancel - A function to cancel the API call, returning a promise that resolves when cancellation is completed.
|
||||
*/
|
||||
interface ApiCall<K extends OperationNames> {
|
||||
uuid: string;
|
||||
result: Promise<OperationResponse<K>>;
|
||||
cancel: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const callApi = <K extends OperationNames>(
|
||||
method: K,
|
||||
args: OperationArgs<K>,
|
||||
backendOpts?: SendHeaderType,
|
||||
): ApiCall<K> => {
|
||||
// if window[method] does not exist, throw an error
|
||||
if (!(method in window)) {
|
||||
console.error(`Method ${method} not found on window object`);
|
||||
|
||||
return {
|
||||
uuid: "",
|
||||
result: Promise.reject(`Method ${method} not found on window object`),
|
||||
cancel: () => Promise.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
const req: BackendSendType<OperationNames> = {
|
||||
body: args,
|
||||
header: backendOpts,
|
||||
};
|
||||
|
||||
const result = (
|
||||
window as unknown as Record<
|
||||
OperationNames,
|
||||
(
|
||||
args: BackendSendType<OperationNames>,
|
||||
) => Promise<BackendReturnType<OperationNames>>
|
||||
>
|
||||
)[method](req) as Promise<BackendReturnType<K>>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const op_key = (result as any)._webviewMessageId as string;
|
||||
|
||||
return {
|
||||
uuid: op_key,
|
||||
result: result.then(({ body }) => body),
|
||||
cancel: async () => {
|
||||
console.log("Cancelling api call: ", op_key);
|
||||
await callApi("delete_task", { task_id: op_key }).result;
|
||||
}
|
||||
};
|
||||
};
|
||||
29
pkgs/clan-app/ui/src/hooks/clan.ts
Normal file
29
pkgs/clan-app/ui/src/hooks/clan.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { callApi } from "@/src/hooks/api";
|
||||
import { addClanURI, setActiveClanURI } from "@/src/stores/clan";
|
||||
import { Params, Navigator } from "@solidjs/router";
|
||||
|
||||
export const selectClanFolder = async () => {
|
||||
const req = callApi("open_file", { file_request: { mode: "select_folder" } });
|
||||
const res = await req.result;
|
||||
|
||||
if (res.status === "error") {
|
||||
throw new Error(res.errors[0].message);
|
||||
}
|
||||
|
||||
if (res.status === "success" && res.data) {
|
||||
const [uri] = res.data;
|
||||
addClanURI(uri);
|
||||
setActiveClanURI(uri);
|
||||
return uri;
|
||||
}
|
||||
|
||||
throw new Error("Illegal state exception");
|
||||
};
|
||||
|
||||
export const navigateToClan = (navigate: Navigator, uri: string) => {
|
||||
navigate("/clan/" + encodeURIComponent(uri));
|
||||
};
|
||||
|
||||
export const clanURIParam = (params: Params) => {
|
||||
return decodeURIComponent(params.clanURI);
|
||||
};
|
||||
9
pkgs/clan-app/ui/src/routes/Clan/Clan.tsx
Normal file
9
pkgs/clan-app/ui/src/routes/Clan/Clan.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { RouteSectionProps, useParams } from "@solidjs/router";
|
||||
import { Component } from "solid-js";
|
||||
import { clanURIParam } from "@/src/hooks/clan";
|
||||
|
||||
export const Clan: Component<RouteSectionProps> = (props) => {
|
||||
const params = useParams();
|
||||
const clanURI = clanURIParam(params);
|
||||
return <h1>{clanURI}</h1>;
|
||||
};
|
||||
@@ -1,51 +1,61 @@
|
||||
import { Component } from "solid-js";
|
||||
import { RouteSectionProps } from "@solidjs/router";
|
||||
import { RouteSectionProps, useNavigate } from "@solidjs/router";
|
||||
import "./Onboarding.css";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
import { Button } from "@/src/components/Button/Button";
|
||||
import { Divider } from "@/src/components/Divider/Divider";
|
||||
import { Logo } from "@/src/components/Logo/Logo";
|
||||
import { navigateToClan, selectClanFolder } from "@/src/hooks/clan";
|
||||
|
||||
export const Onboarding: Component<RouteSectionProps> = (props) => (
|
||||
<main id="welcome">
|
||||
<div class="background">
|
||||
<div class="layer-1" />
|
||||
<div class="layer-2" />
|
||||
<div class="layer-3" />
|
||||
</div>
|
||||
<div class="choose">
|
||||
<Logo variant="Darknet" inverted={true}/>
|
||||
export const Onboarding: Component<RouteSectionProps> = (props) => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
<div class="controls">
|
||||
<Typography
|
||||
hierarchy="headline"
|
||||
size="xxl"
|
||||
weight="bold"
|
||||
align="center"
|
||||
inverted={true}
|
||||
>
|
||||
Build your <br />
|
||||
own darknet
|
||||
</Typography>
|
||||
<Button hierarchy="secondary">Start building</Button>
|
||||
<div class="separator">
|
||||
<Divider orientation="horizontal" inverted={true} />
|
||||
<Typography
|
||||
hierarchy="body"
|
||||
size="s"
|
||||
weight="medium"
|
||||
inverted={true}
|
||||
align="center"
|
||||
>
|
||||
or
|
||||
</Typography>
|
||||
<Divider orientation="horizontal" inverted={true} />
|
||||
</div>
|
||||
<Button hierarchy="primary" ghost={true}>
|
||||
Select folder
|
||||
</Button>
|
||||
const selectFolder = async () => {
|
||||
const uri = await selectClanFolder();
|
||||
navigateToClan(navigate, uri);
|
||||
};
|
||||
|
||||
return (
|
||||
<main id="welcome">
|
||||
<div class="background">
|
||||
<div class="layer-1" />
|
||||
<div class="layer-2" />
|
||||
<div class="layer-3" />
|
||||
</div>
|
||||
<Logo variant="Clan" inverted={true}/>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
<div class="choose">
|
||||
<Logo variant="Darknet" inverted={true} />
|
||||
|
||||
<div class="controls">
|
||||
<Typography
|
||||
hierarchy="headline"
|
||||
size="xxl"
|
||||
weight="bold"
|
||||
align="center"
|
||||
inverted={true}
|
||||
>
|
||||
Build your <br />
|
||||
own darknet
|
||||
</Typography>
|
||||
<Button hierarchy="secondary">Start building</Button>
|
||||
<div class="separator">
|
||||
<Divider orientation="horizontal" inverted={true} />
|
||||
<Typography
|
||||
hierarchy="body"
|
||||
size="s"
|
||||
weight="medium"
|
||||
inverted={true}
|
||||
align="center"
|
||||
>
|
||||
or
|
||||
</Typography>
|
||||
<Divider orientation="horizontal" inverted={true} />
|
||||
</div>
|
||||
<Button hierarchy="primary" ghost={true} onAction={selectFolder}>
|
||||
Select folder
|
||||
</Button>
|
||||
</div>
|
||||
<Logo variant="Clan" inverted={true} />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import type { RouteDefinition } from "@solidjs/router/dist/types";
|
||||
import { Onboarding } from "@/src/routes/Onboarding/Onboarding";
|
||||
import { Clan } from "@/src/routes/Clan/Clan";
|
||||
|
||||
export const Routes: RouteDefinition[] = [
|
||||
{
|
||||
path: "/",
|
||||
component: Onboarding,
|
||||
},
|
||||
{
|
||||
path: "/clan/:clanURI",
|
||||
component: Clan
|
||||
}
|
||||
];
|
||||
88
pkgs/clan-app/ui/src/stores/clan.ts
Normal file
88
pkgs/clan-app/ui/src/stores/clan.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { createStore, produce } from "solid-js/store";
|
||||
import { makePersisted } from "@solid-primitives/storage";
|
||||
|
||||
interface ClanStoreType {
|
||||
clanURIs: string[];
|
||||
activeClanURI?: string;
|
||||
}
|
||||
|
||||
const [store, setStore] = makePersisted(
|
||||
createStore<ClanStoreType>({
|
||||
clanURIs: [],
|
||||
}),
|
||||
{
|
||||
name: "clanStore",
|
||||
storage: localStorage,
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieves the active clan URI from the store.
|
||||
*
|
||||
* @function
|
||||
* @returns {string} The URI of the active clan.
|
||||
*/
|
||||
const activeClanURI = (): string | undefined => store.activeClanURI;
|
||||
|
||||
/**
|
||||
* Updates the active Clan URI in the store.
|
||||
*
|
||||
* @param {string} uri - The URI to be set as the active Clan URI.
|
||||
*/
|
||||
const setActiveClanURI = (uri: string) => setStore("activeClanURI", uri);
|
||||
|
||||
/**
|
||||
* Retrieves the current list of clan URIs from the store.
|
||||
*
|
||||
* @function clanURIs
|
||||
* @returns {*} The clan URIs from the store.
|
||||
*/
|
||||
const clanURIs = (): string[] => store.clanURIs;
|
||||
|
||||
/**
|
||||
* Adds a new clan URI to the list of clan URIs in the store.
|
||||
*
|
||||
* @param {string} uri - The URI of the clan to be added.
|
||||
*
|
||||
*/
|
||||
const addClanURI = (uri: string) =>
|
||||
setStore("clanURIs", store.clanURIs.length, uri);
|
||||
|
||||
/**
|
||||
* Removes a specified URI from the clan URI list and updates the active clan URI.
|
||||
*
|
||||
* This function modifies the store in the following ways:
|
||||
* - Removes the specified URI from the `clanURIs` array.
|
||||
* - Clears the `activeClanURI` if the removed URI matches the currently active URI.
|
||||
* - Sets a new active clan URI to the last URI in the `clanURIs` array if the active clan URI is undefined
|
||||
* and there are remaining clan URIs in the list.
|
||||
*
|
||||
* @param {string} uri - The URI to be removed from the clan list.
|
||||
*/
|
||||
const removeClanURI = (uri: string) => {
|
||||
setStore(
|
||||
produce((state) => {
|
||||
// remove from the clan list
|
||||
state.clanURIs = state.clanURIs.filter((el) => el !== uri);
|
||||
|
||||
// clear active clan uri if it's the one being removed
|
||||
if (state.activeClanURI === uri) {
|
||||
state.activeClanURI = undefined;
|
||||
}
|
||||
|
||||
// select a new active URI if at least one remains
|
||||
if (!state.activeClanURI && state.clanURIs.length > 0) {
|
||||
state.activeClanURI = state.clanURIs[state.clanURIs.length - 1];
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
store,
|
||||
activeClanURI,
|
||||
setActiveClanURI,
|
||||
clanURIs,
|
||||
addClanURI,
|
||||
removeClanURI,
|
||||
};
|
||||
Reference in New Issue
Block a user