diff --git a/pkgs/clan-app/ui/src/components/Modal/Modal.tsx b/pkgs/clan-app/ui/src/components/Modal/Modal.tsx index dc64d9b94..a4618c369 100644 --- a/pkgs/clan-app/ui/src/components/Modal/Modal.tsx +++ b/pkgs/clan-app/ui/src/components/Modal/Modal.tsx @@ -3,6 +3,7 @@ import { Dialog as KDialog } from "@kobalte/core/dialog"; import "./Modal.css"; import { Typography } from "../Typography/Typography"; import Icon from "../Icon/Icon"; +import cx from "classnames"; export interface ModalContext { close(): void; @@ -13,6 +14,8 @@ export interface ModalProps { title: string; onClose: () => void; children: (ctx: ModalContext) => JSX.Element; + mount?: Node; + class?: string; } export const Modal = (props: ModalProps) => { @@ -20,18 +23,28 @@ export const Modal = (props: ModalProps) => { return ( - - + +
{props.title} - setOpen(false)}> + { + setOpen(false); + props.onClose(); + }} + >
- {props.children({ close: () => setOpen(false) })} + {props.children({ + close: () => { + setOpen(false); + props.onClose(); + }, + })}
diff --git a/pkgs/clan-app/ui/src/routes/Clan/Clan.css b/pkgs/clan-app/ui/src/routes/Clan/Clan.css index 977797521..d74a2c313 100644 --- a/pkgs/clan-app/ui/src/routes/Clan/Clan.css +++ b/pkgs/clan-app/ui/src/routes/Clan/Clan.css @@ -2,3 +2,12 @@ opacity: 0; transition: opacity 0.5s ease; } + +.create-backdrop { + @apply absolute top-0 left-0 w-full h-svh flex justify-center items-center backdrop-blur-0 z-50; + -webkit-backdrop-filter: blur(4px); +} + +.create-modal { + @apply min-w-96; +} diff --git a/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx b/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx index d9383dcff..cd47110a4 100644 --- a/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx +++ b/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx @@ -1,5 +1,5 @@ import { RouteSectionProps } from "@solidjs/router"; -import { Component, JSX } from "solid-js"; +import { Component, JSX, Show, createSignal } from "solid-js"; import { useClanURI } from "@/src/hooks/clan"; import { CubeScene } from "@/src/scene/cubes"; import { MachinesQueryResult, useMachinesQuery } from "@/src/queries/queries"; @@ -10,6 +10,9 @@ import { Button } from "@/src/components/Button/Button"; import { Splash } from "@/src/scene/splash"; import cx from "classnames"; import "./Clan.css"; +import { Modal } from "@/src/components/Modal/Modal"; +import { TextInput } from "@/src/components/Form/TextInput"; +import { createForm, FieldValues, reset } from "@modular-forms/solid"; export const Clan: Component = (props) => { return ( @@ -27,17 +30,88 @@ export const Clan: Component = (props) => { ); }; +interface CreateFormValues extends FieldValues { + name: string; +} +interface MockProps { + onClose: () => void; + onSubmit: (formValues: CreateFormValues) => void; +} +const MockCreateMachine = (props: MockProps) => { + let container: Node; + + const [form, { Form, Field, FieldArray }] = createForm(); + + return ( +
(container = el)} class="create-backdrop"> + { + reset(form); + props.onClose(); + }} + class="create-modal" + title="Create Machine" + > + {() => ( +
+ + {(field, props) => ( + <> + + + )} + + +
+ + +
+
+ )} +
+
+ ); +}; + const ClanSceneController = () => { const clanURI = useClanURI({ force: true }); - const onCreate = async (id: string) => { + const [dialogHandlers, setDialogHandlers] = createSignal<{ + resolve: ({ id }: { id: string }) => void; + reject: (err: unknown) => void; + } | null>(null); + + const onCreate = async (): Promise<{ id: string }> => { + return new Promise((resolve, reject) => { + setShowModal(true); + setDialogHandlers({ resolve, reject }); + }); + }; + + const sendCreate = async (values: CreateFormValues) => { const api = callApi("create_machine", { opts: { clan_dir: { identifier: clanURI, }, machine: { - name: id, + name: values.name, }, }, }); @@ -49,14 +123,34 @@ const ClanSceneController = () => { // Important: rejects the promise throw new Error(res.errors[0].message); } - return; + return { id: values.name }; }; + const [showModal, setShowModal] = createSignal(false); + return ( {({ query }) => { return ( <> + + { + 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); + } + }} + /> +
{ produce((s) => { for (const machineId in s.sceneData[clanURI]) { // Reset the position of each machine to [0, 0] + s.sceneData[clanURI] = {}; // Clear the entire object // delete s.sceneData[clanURI][machineId]; } }), @@ -90,6 +185,7 @@ const ClanSceneController = () => {
+ Promise; + onCreate: () => Promise<{ id: string }>; sceneStore: Accessor; setMachinePos: (machineId: string, pos: [number, number]) => void; isLoading: boolean; @@ -251,7 +251,10 @@ export function CubeScene(props: { // Reactive cubes memo - this recalculates whenever data changes const cubes = createMemo(() => { console.log("Calculating cubes..."); - const currentIds = Object.keys(unwrap(props.sceneStore())); + const sceneData = props.sceneStore(); // keep it reactive + if (!sceneData) return []; + + const currentIds = Object.keys(sceneData); console.log("Current IDs:", currentIds); let cameraTarget = [0, 0, 0] as [number, number, number]; @@ -302,6 +305,7 @@ export function CubeScene(props: { if (progress < 1) { requestAnimationFrame(animate); + requestRenderIfNotRequested(); } } @@ -599,6 +603,7 @@ export function CubeScene(props: { CREATE_BASE_COLOR, CREATE_BASE_EMISSIVE, ); + initBase.visible = false; scene.add(initBase); @@ -631,14 +636,25 @@ export function CubeScene(props: { // - Creates a new cube in "create" mode const onClick = (event: MouseEvent) => { if (worldMode() === "create") { - setWorldMode("view"); + props + .onCreate() + .then(({ id }) => { + //Successfully created machine + const pos = cursorPosition(); + if (!pos) { + console.warn("No position set for new cube"); + return; + } + props.setMachinePos(id, pos); + }) + .catch((error) => { + console.error("Error creating cube:", error); + }) + .finally(() => { + if (initBase) initBase.visible = false; - // res.result.then(() => { - // props.cubesQuery.refetch(); - - // positionMap.set("sara", pos); - // addCube("sara"); - // }); + setWorldMode("view"); + }); } const rect = renderer.domElement.getBoundingClientRect(); @@ -828,6 +844,8 @@ export function CubeScene(props: { } } }); + + requestRenderIfNotRequested(); }); createEffect( @@ -856,8 +874,10 @@ export function CubeScene(props: { const pos = nextGridPos(); if (!initBase) return; - initBase.position.set(pos[0], BASE_HEIGHT / 2, pos[1]); - + if (initBase.visible === false && inside) { + initBase.position.set(pos[0], BASE_HEIGHT / 2, pos[1]); + initBase.visible = true; + } requestRenderIfNotRequested(); }; @@ -869,6 +889,8 @@ export function CubeScene(props: { if (worldMode() !== "create") return; if (!initBase) return; + initBase.visible = true; + const rect = renderer.domElement.getBoundingClientRect(); const mouse = new THREE.Vector2( ((event.clientX - rect.left) / rect.width) * 2 - 1, @@ -923,8 +945,10 @@ export function CubeScene(props: { onClick={() => { if (positionMode() === "grid") { setPositionMode("circle"); + grid.visible = false; } else { setPositionMode("grid"); + grid.visible = true; } }} />