UI/refactor: unify clan details and edit

This commit is contained in:
Johannes Kirschbauer
2024-09-04 10:16:08 +02:00
parent 7da164a5d1
commit ace1598977
11 changed files with 470 additions and 494 deletions

View File

@@ -0,0 +1,21 @@
import { callApi } from "../api";
import { setActiveURI, setClanList } from "../App";
export const registerClan = async () => {
try {
const loc = await callApi("open_file", {
file_request: { mode: "select_folder" },
});
if (loc.status === "success" && loc.data) {
const data = loc.data[0];
setClanList((s) => {
const res = new Set([...s, data]);
return Array.from(res);
});
setActiveURI(data);
return data;
}
} catch (e) {
//
}
};

View File

@@ -7,15 +7,12 @@ import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
import { MachineDetails } from "./routes/machines/[name]/view"; import { MachineDetails } from "./routes/machines/[name]/view";
import { Layout } from "./layout/layout"; import { Layout } from "./layout/layout";
import { MachineListView } from "./routes/machines/view"; import { MachineListView } from "./routes/machines/view";
import { CreateClan } from "./routes/clan/view"; import { ClanList, CreateClan, ClanDetails } from "./routes/clans";
import { Settings } from "./routes/settings";
import { EditClanForm } from "./routes/clan/editClan";
import { Flash } from "./routes/flash/view"; import { Flash } from "./routes/flash/view";
import { CreateMachine } from "./routes/machines/create"; import { CreateMachine } from "./routes/machines/create";
import { HostList } from "./routes/hosts/view"; import { HostList } from "./routes/hosts/view";
import { Welcome } from "./routes/welcome"; import { Welcome } from "./routes/welcome";
import { Toaster } from "solid-toast"; import { Toaster } from "solid-toast";
import { Details } from "./routes/clan/details";
const client = new QueryClient(); const client = new QueryClient();
@@ -74,7 +71,7 @@ export const routes: AppRoute[] = [
{ {
path: "/", path: "/",
label: "Overview", label: "Overview",
component: () => <Settings />, component: () => <ClanList />,
}, },
{ {
path: "/create", path: "/create",
@@ -85,7 +82,7 @@ export const routes: AppRoute[] = [
path: "/:id", path: "/:id",
label: "Details", label: "Details",
hidden: true, hidden: true,
component: () => <Details />, component: () => <ClanDetails />,
}, },
], ],
}, },

View File

@@ -64,11 +64,18 @@ export const Header = (props: HeaderProps) => {
</span> </span>
</div> </div>
<div class="flex-none"> <div class="flex-none">
<span class="tooltip tooltip-bottom" data-tip="Settings"> <Show when={activeURI()}>
<button class="link" onClick={() => navigate("/clan")}> {(d) => (
<span class="tooltip tooltip-bottom" data-tip="Clan Settings">
<button
class="link"
onClick={() => navigate(`/clans/${window.btoa(d())}`)}
>
<span class="material-icons">settings</span> <span class="material-icons">settings</span>
</button> </button>
</span> </span>
)}
</Show>
</div> </div>
</div> </div>
); );

View File

@@ -1,210 +0,0 @@
import { callApi, SuccessQuery } from "@/src/api";
import { BackButton } from "@/src/components/BackButton";
import { useParams } from "@solidjs/router";
import { createQuery } from "@tanstack/solid-query";
import { createSignal, For, Match, Switch } from "solid-js";
import { Show } from "solid-js";
import {
createForm,
FieldValues,
getValue,
getValues,
setValue,
} from "@modular-forms/solid";
import { TextInput } from "@/src/components/TextInput";
import toast from "solid-toast";
type AdminData = SuccessQuery<"get_admin_service">["data"];
interface ClanDetailsProps {
admin: AdminData;
base_url: string;
}
interface AdminSettings extends FieldValues {
allowedKeys: { name: string; value: string }[];
}
const ClanDetails = (props: ClanDetailsProps) => {
const items = () =>
Object.entries<string>(
(props.admin?.config?.allowedKeys as Record<string, string>) || {},
);
const [formStore, { Form, Field }] = createForm<AdminSettings>({
initialValues: {
allowedKeys: items().map(([name, value]) => ({ name, value })),
},
});
const [keys, setKeys] = createSignal<1[]>(
new Array(items().length || 1).fill(1),
);
const handleSubmit = async (values: AdminSettings) => {
console.log("submitting", values, getValues(formStore));
const r = await callApi("set_admin_service", {
base_url: props.base_url,
allowed_keys: values.allowedKeys.reduce(
(acc, curr) => ({ ...acc, [curr.name]: curr.value }),
{},
),
});
if (r.status === "success") {
toast.success("Successfully updated admin settings");
}
if (r.status === "error") {
toast.error(`Failed to update admin settings: ${r.errors[0].message}`);
}
};
return (
<div>
<span class="text-xl text-primary">Clan Admin Settings</span>
<Form onSubmit={handleSubmit}>
<div class="grid grid-cols-12 gap-2">
<span class="col-span-12 text-lg text-neutral">
Each of the following keys can be used to authenticate on any
machine
</span>
<For each={keys()}>
{(name, idx) => (
<>
<Field name={`allowedKeys.${idx()}.name`}>
{(field, props) => (
<TextInput
formStore={formStore}
inputProps={props}
label={"Name"}
adornment={{
position: "start",
content: (
<span class="material-icons text-gray-400">key</span>
),
}}
value={field.value ?? ""}
error={field.error}
class="col-span-4"
required
/>
)}
</Field>
<Field name={`allowedKeys.${idx()}.value`}>
{(field, props) => (
<>
<TextInput
formStore={formStore}
inputProps={props}
label={"Value"}
value={field.value ?? ""}
error={field.error}
class="col-span-6"
required
/>
<span class="tooltip mt-auto" data-tip="Select file">
<label
class={"form-control w-full"}
aria-disabled={formStore.submitting}
>
<div class="input input-bordered relative flex items-center gap-2">
<input
value=""
// Disable drag n drop
onDrop={(e) => e.preventDefault()}
class="absolute -ml-4 size-full cursor-pointer opacity-0"
type="file"
onInput={async (e) => {
console.log(e.target.files);
if (!e.target.files) return;
const content = await e.target.files[0].text();
console.log(content);
setValue(
formStore,
`allowedKeys.${idx()}.value`,
content,
);
if (
!getValue(
formStore,
`allowedKeys.${idx()}.name`,
)
) {
setValue(
formStore,
`allowedKeys.${idx()}.name`,
e.target.files[0].name,
);
}
}}
/>
<span class="material-icons">file_open</span>
</div>
</label>
</span>
</>
)}
</Field>
<button
class="btn btn-ghost col-span-1 self-end"
onClick={(e) => {
e.preventDefault();
setKeys((c) => c.filter((_, i) => i !== idx()));
setValue(formStore, `allowedKeys.${idx()}.name`, "");
setValue(formStore, `allowedKeys.${idx()}.value`, "");
}}
>
<span class="material-icons">delete</span>
</button>
</>
)}
</For>
</div>
<div class="my-2 flex w-full gap-2">
<button
class="btn btn-square btn-ghost"
onClick={(e) => {
e.preventDefault();
setKeys((c) => [...c, 1]);
}}
>
<span class="material-icons">add</span>
</button>
<button class="btn" type="submit">
Submit
</button>
</div>
</Form>
</div>
);
};
export const Details = () => {
const params = useParams();
const clan_dir = window.atob(params.id);
const query = createQuery(() => ({
queryKey: [clan_dir, "get_admin_service"],
queryFn: async () => {
const result = await callApi("get_admin_service", {
base_url: clan_dir,
});
if (result.status === "error") throw new Error("Failed to fetch data");
return result.data || null;
},
}));
return (
<div class="p-2">
<BackButton />
<Show
when={!query.isLoading}
fallback={<span class="loading loading-lg"></span>}
>
<Switch>
<Match when={query.data}>
{(d) => <ClanDetails admin={query.data} base_url={clan_dir} />}
</Match>
</Switch>
</Show>
</div>
);
};

View File

@@ -1,175 +0,0 @@
import { callApi, OperationResponse, pyApi } from "@/src/api";
import { Accessor, Match, Show, Switch } from "solid-js";
import {
createForm,
required,
reset,
SubmitHandler,
} from "@modular-forms/solid";
import toast from "solid-toast";
import { createQuery } from "@tanstack/solid-query";
type CreateForm = Meta;
interface EditClanFormProps {
directory: Accessor<string>;
done: () => void;
}
export const EditClanForm = (props: EditClanFormProps) => {
const { directory } = props;
const details = createQuery(() => ({
queryKey: [directory(), "meta"],
queryFn: async () => {
const result = await callApi("show_clan_meta", { uri: directory() });
if (result.status === "error") throw new Error("Failed to fetch data");
return result.data;
},
}));
return (
<Switch>
<Match when={details?.data}>
{(data) => (
<FinalEditClanForm
initial={data()}
directory={directory()}
done={props.done}
/>
)}
</Match>
</Switch>
);
};
interface FinalEditClanFormProps {
initial: CreateForm;
directory: string;
done: () => void;
}
export const FinalEditClanForm = (props: FinalEditClanFormProps) => {
const [formStore, { Form, Field }] = createForm<CreateForm>({
initialValues: props.initial,
});
const handleSubmit: SubmitHandler<CreateForm> = async (values, event) => {
await toast.promise(
(async () => {
await callApi("update_clan_meta", {
options: {
directory: props.directory,
meta: values,
},
});
})(),
{
loading: "Updating clan...",
success: "Clan Successfully updated",
error: "Failed to update clan",
},
);
props.done();
};
return (
<div class="card card-normal">
<Form onSubmit={handleSubmit} shouldActive>
<Field name="icon">
{(field, props) => (
<>
<figure>
<Show
when={field.value}
fallback={
<span class="material-icons aspect-square size-60 rounded-lg text-[18rem]">
group
</span>
}
>
{(icon) => (
<img
class="aspect-square size-60 rounded-lg"
src={icon()}
alt="Clan Logo"
/>
)}
</Show>
</figure>
</>
)}
</Field>
<div class="card-body">
<Field
name="name"
validate={[required("Please enter a unique name for the clan.")]}
>
{(field, props) => (
<label class="form-control w-full">
<div class="label">
<span class="label-text block after:ml-0.5 after:text-primary after:content-['*']">
Name
</span>
</div>
<input
{...props}
disabled={formStore.submitting}
required
placeholder="Clan Name"
class="input input-bordered"
classList={{ "input-error": !!field.error }}
value={field.value}
/>
<div class="label">
{field.error && (
<span class="label-text-alt">{field.error}</span>
)}
</div>
</label>
)}
</Field>
<Field name="description">
{(field, props) => (
<label class="form-control w-full">
<div class="label">
<span class="label-text">Description</span>
</div>
<input
{...props}
disabled={formStore.submitting}
required
type="text"
placeholder="Some words about your clan"
class="input input-bordered"
classList={{ "input-error": !!field.error }}
value={field.value || ""}
/>
<div class="label">
{field.error && (
<span class="label-text-alt">{field.error}</span>
)}
</div>
</label>
)}
</Field>
{
<div class="card-actions justify-end">
<button
class="btn btn-primary"
type="submit"
disabled={formStore.submitting}
>
Save
</button>
</div>
}
</div>
</Form>
</div>
);
};
type Meta = Extract<
OperationResponse<"show_clan_meta">,
{ status: "success" }
>["data"];

View File

@@ -1,9 +0,0 @@
import { ClanForm } from "./clanDetails";
export const CreateClan = () => {
return (
<div>
<ClanForm />
</div>
);
};

View File

@@ -15,7 +15,7 @@ type CreateForm = Meta & {
template: string; template: string;
}; };
export const ClanForm = () => { export const CreateClan = () => {
const [formStore, { Form, Field }] = createForm<CreateForm>({ const [formStore, { Form, Field }] = createForm<CreateForm>({
initialValues: { initialValues: {
name: "", name: "",

View File

@@ -0,0 +1,394 @@
import { callApi, SuccessQuery } from "@/src/api";
import { BackButton } from "@/src/components/BackButton";
import { useParams } from "@solidjs/router";
import {
createQuery,
QueryClient,
useQueryClient,
} from "@tanstack/solid-query";
import { createSignal, For, Match, Switch } from "solid-js";
import { Show } from "solid-js";
import {
createForm,
FieldValues,
getValue,
getValues,
required,
setValue,
SubmitHandler,
} from "@modular-forms/solid";
import { TextInput } from "@/src/components/TextInput";
import toast from "solid-toast";
interface AdminModuleFormProps {
admin: AdminData;
base_url: string;
}
interface AdminSettings extends FieldValues {
allowedKeys: { name: string; value: string }[];
}
interface EditClanFormProps {
initial: GeneralData;
directory: string;
}
const EditClanForm = (props: EditClanFormProps) => {
const [formStore, { Form, Field }] = createForm<GeneralData>({
initialValues: props.initial,
});
const queryClient = useQueryClient();
const handleSubmit: SubmitHandler<GeneralData> = async (values, event) => {
await toast.promise(
(async () => {
await callApi("update_clan_meta", {
options: {
directory: props.directory,
meta: values,
},
});
})(),
{
loading: "Updating clan...",
success: "Clan Successfully updated",
error: "Failed to update clan",
},
);
queryClient.invalidateQueries({
queryKey: [props.directory, "meta"],
});
};
const curr_name = () => props.initial.name;
return (
<Form onSubmit={handleSubmit} shouldActive>
<Field name="icon">
{(field) => (
<>
<figure class="p-1">
<div class="flex flex-col items-center">
<div class="text-3xl text-primary">{curr_name()}'s</div>
<div class="text-secondary">Wide settings</div>
</div>
</figure>
<figure>
<Show
when={field.value}
fallback={
<span class="material-icons aspect-square size-60 rounded-lg text-[18rem]">
group
</span>
}
>
{(icon) => (
<img
class="aspect-square size-60 rounded-lg"
src={icon()}
alt="Clan Logo"
/>
)}
</Show>
</figure>
</>
)}
</Field>
<div class="card-body">
<span class="text-xl text-primary">General</span>
<Field
name="name"
validate={[required("Please enter a unique name for the clan.")]}
>
{(field, props) => (
<label class="form-control w-full">
<div class="label">
<span class="label-text block after:ml-0.5 after:text-primary after:content-['*']">
Name
</span>
</div>
<input
{...props}
disabled={formStore.submitting}
required
placeholder="Clan Name"
class="input input-bordered"
classList={{ "input-error": !!field.error }}
value={field.value}
/>
<div class="label">
{field.error && (
<span class="label-text-alt">{field.error}</span>
)}
</div>
</label>
)}
</Field>
<Field name="description">
{(field, props) => (
<label class="form-control w-full">
<div class="label">
<span class="label-text">Description</span>
</div>
<input
{...props}
disabled={formStore.submitting}
required
type="text"
placeholder="Some words about your clan"
class="input input-bordered"
classList={{ "input-error": !!field.error }}
value={field.value || ""}
/>
<div class="label">
{field.error && (
<span class="label-text-alt">{field.error}</span>
)}
</div>
</label>
)}
</Field>
{
<div class="card-actions justify-end">
<button
class="btn btn-primary"
type="submit"
disabled={formStore.submitting || !formStore.dirty}
>
Save
</button>
</div>
}
</div>
</Form>
);
};
const AdminModuleForm = (props: AdminModuleFormProps) => {
const items = () =>
Object.entries<string>(
(props.admin?.config?.allowedKeys as Record<string, string>) || {},
);
const [formStore, { Form, Field }] = createForm<AdminSettings>({
initialValues: {
allowedKeys: items().map(([name, value]) => ({ name, value })),
},
});
const queryClient = useQueryClient();
const [keys, setKeys] = createSignal<1[]>(
new Array(items().length || 1).fill(1),
);
const handleSubmit = async (values: AdminSettings) => {
console.log("submitting", values, getValues(formStore));
const r = await callApi("set_admin_service", {
base_url: props.base_url,
allowed_keys: values.allowedKeys.reduce(
(acc, curr) => ({ ...acc, [curr.name]: curr.value }),
{},
),
});
if (r.status === "success") {
toast.success("Successfully updated admin settings");
}
if (r.status === "error") {
toast.error(`Failed to update admin settings: ${r.errors[0].message}`);
}
queryClient.invalidateQueries({
queryKey: [props.base_url, "get_admin_service"],
});
};
return (
<Form onSubmit={handleSubmit}>
<div class="card-body">
<span class="text-xl text-primary">Administration</span>
<div class="grid grid-cols-12 gap-2">
<span class="col-span-12 text-lg text-neutral">
Each of the following keys can be used to authenticate on machines
</span>
<For each={keys()}>
{(name, idx) => (
<>
<Field name={`allowedKeys.${idx()}.name`}>
{(field, props) => (
<TextInput
formStore={formStore}
inputProps={props}
label={"Name"}
adornment={{
position: "start",
content: (
<span class="material-icons text-gray-400">key</span>
),
}}
value={field.value ?? ""}
error={field.error}
class="col-span-4"
required
/>
)}
</Field>
<Field name={`allowedKeys.${idx()}.value`}>
{(field, props) => (
<>
<TextInput
formStore={formStore}
inputProps={props}
label={"Value"}
value={field.value ?? ""}
error={field.error}
class="col-span-6"
required
/>
<span class="tooltip mt-auto" data-tip="Select file">
<label
class={"form-control w-full"}
aria-disabled={formStore.submitting}
>
<div class="btn btn-secondary relative flex items-center justify-center">
<input
value=""
// Disable drag n drop
onDrop={(e) => e.preventDefault()}
class="absolute -ml-4 size-full cursor-pointer opacity-0"
type="file"
onInput={async (e) => {
console.log(e.target.files);
if (!e.target.files) return;
const content = await e.target.files[0].text();
console.log(content);
setValue(
formStore,
`allowedKeys.${idx()}.value`,
content,
);
if (
!getValue(
formStore,
`allowedKeys.${idx()}.name`,
)
) {
setValue(
formStore,
`allowedKeys.${idx()}.name`,
e.target.files[0].name,
);
}
}}
/>
<span class="material-icons">file_open</span>
</div>
</label>
</span>
</>
)}
</Field>
<button
class="btn btn-ghost col-span-1 self-end"
onClick={(e) => {
e.preventDefault();
setKeys((c) => c.filter((_, i) => i !== idx()));
setValue(formStore, `allowedKeys.${idx()}.name`, "");
setValue(formStore, `allowedKeys.${idx()}.value`, "");
}}
>
<span class="material-icons">delete</span>
</button>
</>
)}
</For>
<div class="my-2 flex w-full gap-2">
<button
class="btn btn-square btn-ghost"
onClick={(e) => {
e.preventDefault();
setKeys((c) => [...c, 1]);
}}
>
<span class="material-icons">add</span>
</button>
</div>
</div>
{
<div class="card-actions justify-end">
<button
class="btn btn-primary"
type="submit"
disabled={formStore.submitting || !formStore.dirty}
>
Save
</button>
</div>
}
</div>
</Form>
);
};
type GeneralData = SuccessQuery<"show_clan_meta">["data"];
type AdminData = SuccessQuery<"get_admin_service">["data"];
export const ClanDetails = () => {
const params = useParams();
const clan_dir = window.atob(params.id);
// Fetch general meta data
const clanQuery = createQuery(() => ({
queryKey: [clan_dir, "meta"],
queryFn: async () => {
const result = await callApi("show_clan_meta", { uri: clan_dir });
if (result.status === "error") throw new Error("Failed to fetch data");
return result.data;
},
}));
// Fetch admin settings
const adminQuery = createQuery(() => ({
queryKey: [clan_dir, "get_admin_service"],
queryFn: async () => {
const result = await callApi("get_admin_service", {
base_url: clan_dir,
});
if (result.status === "error") throw new Error("Failed to fetch data");
return result.data || null;
},
}));
return (
<div class="card card-normal">
<BackButton />
<Show
when={!adminQuery.isLoading}
fallback={
<div>
<span class="loading loading-lg"></span>
</div>
}
>
<Switch>
<Match when={clanQuery.data}>
{(d) => <EditClanForm initial={d()} directory={clan_dir} />}
</Match>
</Switch>
</Show>
<div class="divider"></div>
<Show
when={!adminQuery.isLoading}
fallback={
<div>
<span class="loading loading-lg"></span>
</div>
}
>
<Switch>
<Match when={adminQuery.data}>
{(d) => <AdminModuleForm admin={d()} base_url={clan_dir} />}
</Match>
</Switch>
</Show>
</div>
);
};

View File

@@ -0,0 +1,3 @@
export * from "./list";
export * from "./create";
export * from "./details";

View File

@@ -4,39 +4,14 @@ import { createSignal, For, Match, Setter, Show, Switch } from "solid-js";
import { createQuery } from "@tanstack/solid-query"; import { createQuery } from "@tanstack/solid-query";
import { useFloating } from "@/src/floating"; import { useFloating } from "@/src/floating";
import { autoUpdate, flip, hide, offset, shift } from "@floating-ui/dom"; import { autoUpdate, flip, hide, offset, shift } from "@floating-ui/dom";
import { EditClanForm } from "../clan/editClan";
import { useNavigate, A } from "@solidjs/router"; import { useNavigate, A } from "@solidjs/router";
import { fileURLToPath } from "url"; import { registerClan } from "@/src/hooks";
export const registerClan = async () => { interface ClanItemProps {
try {
const loc = await callApi("open_file", {
file_request: { mode: "select_folder" },
});
if (loc.status === "success" && loc.data) {
const data = loc.data[0];
setClanList((s) => {
const res = new Set([...s, data]);
return Array.from(res);
});
setActiveURI(data);
// setRoute((r) => {
// if (r === "welcome") return "machines";
// return r;
// });
return data;
}
} catch (e) {
//
}
};
interface ClanDetailsProps {
clan_dir: string; clan_dir: string;
setEditURI: Setter<string | null>;
} }
const ClanDetails = (props: ClanDetailsProps) => { const ClanItem = (props: ClanItemProps) => {
const { clan_dir, setEditURI } = props; const { clan_dir } = props;
const details = createQuery(() => ({ const details = createQuery(() => ({
queryKey: [clan_dir, "meta"], queryKey: [clan_dir, "meta"],
@@ -46,7 +21,7 @@ const ClanDetails = (props: ClanDetailsProps) => {
return result.data; return result.data;
}, },
})); }));
const navigate = useNavigate();
const [reference, setReference] = createSignal<HTMLElement>(); const [reference, setReference] = createSignal<HTMLElement>();
const [floating, setFloating] = createSignal<HTMLElement>(); const [floating, setFloating] = createSignal<HTMLElement>();
@@ -88,9 +63,7 @@ const ClanDetails = (props: ClanDetailsProps) => {
<div class="join"> <div class="join">
<button <button
class="join-item btn-sm" class="join-item btn-sm"
onClick={() => { onClick={() => navigate(`/clans/${window.btoa(clan_dir)}`)}
setEditURI(clan_dir);
}}
> >
<span class="material-icons">edit</span> <span class="material-icons">edit</span>
</button> </button>
@@ -157,37 +130,16 @@ const ClanDetails = (props: ClanDetailsProps) => {
); );
}; };
export const Settings = () => { export const ClanList = () => {
const [editURI, setEditURI] = createSignal<string | null>(null);
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<div class="card card-normal"> <div class="card card-normal">
<Switch>
<Match when={editURI()}>
{(uri) => (
<EditClanForm
directory={uri}
done={() => {
setEditURI(null);
}}
/>
)}
</Match>
<Match when={!editURI()}>
<div class="card-body"> <div class="card-body">
<div class="label"> <div class="label">
<div class="label-text text-2xl text-neutral"> <div class="label-text text-2xl text-neutral">Registered Clans</div>
Registered Clans
</div>
<div class="flex gap-2"> <div class="flex gap-2">
<span class="tooltip tooltip-top" data-tip="Register clan"> <span class="tooltip tooltip-top" data-tip="Register clan">
<button <button class="btn btn-square btn-ghost" onClick={registerClan}>
class="btn btn-square btn-ghost"
onClick={() => {
registerClan();
}}
>
<span class="material-icons">post_add</span> <span class="material-icons">post_add</span>
</button> </button>
</span> </span>
@@ -205,14 +157,10 @@ export const Settings = () => {
</div> </div>
<div class="stats stats-vertical shadow"> <div class="stats stats-vertical shadow">
<For each={clanList()}> <For each={clanList()}>
{(value) => ( {(value) => <ClanItem clan_dir={value} />}
<ClanDetails clan_dir={value} setEditURI={setEditURI} />
)}
</For> </For>
</div> </div>
</div> </div>
</Match>
</Switch>
</div> </div>
); );
}; };

View File

@@ -1,5 +1,5 @@
import { setActiveURI, setClanList } from "@/src/App"; import { setActiveURI } from "@/src/App";
import { registerClan } from "../settings"; import { registerClan } from "@/src/hooks";
import { useNavigate } from "@solidjs/router"; import { useNavigate } from "@solidjs/router";
export const Welcome = () => { export const Welcome = () => {