From 0517e7c2e2e1e661ca8b65dac74ceecc372edcad Mon Sep 17 00:00:00 2001 From: Emma Date: Sat, 19 Oct 2024 12:40:53 -0600 Subject: [PATCH] namespace tracking and creation --- server/main.ts | 27 +++++++++++ src/app.tsx | 2 +- src/atoms/namespace.ts | 3 ++ src/components/editor/namespaceModal.tsx | 62 ++++++++++++++++++++++++ src/components/editor/selector.tsx | 3 ++ src/components/loader.tsx | 2 +- src/{ => util}/classes.ts | 0 src/util/fetchJson.ts | 2 + src/views/editor.tsx | 44 ++++++++++++++++- 9 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 src/atoms/namespace.ts create mode 100644 src/components/editor/namespaceModal.tsx create mode 100644 src/components/editor/selector.tsx rename src/{ => util}/classes.ts (100%) create mode 100644 src/util/fetchJson.ts diff --git a/server/main.ts b/server/main.ts index 5d19ec5..2de66f6 100644 --- a/server/main.ts +++ b/server/main.ts @@ -120,6 +120,33 @@ router.route("/api/pack") return new Response(JSON.stringify({ packName }), { status: 200 }); }); +router.route("/api/pack/namespaces") + .get(async () => { + using store = new BearMetalStore(); + + const namespaces = Array.from(Deno.readDirSync(store.get("packlocation"))) + .filter((dir) => dir.isDirectory).map((dir) => dir.name); + return new Response(JSON.stringify(namespaces), { status: 200 }); + }) + .post(async (req) => { + using store = new BearMetalStore(); + + const namespace = await req.text(); + if (!namespace) { + return new Response("Namespace is required", { status: 400 }); + } + + const namespaceRx = /^[a-zA-Z0-9_\-]+$/; + if (!namespaceRx.test(namespace)) { + return new Response( + "Namespace must only contain letters, numbers, underscores, and dashes", + { status: 400 }, + ); + } + await ensureDir(store.get("packlocation") + "/" + namespace); + return new Response(null, { status: 200 }); + }); + router.route("/api/packs") .get(async () => { using store = new BearMetalStore(); diff --git a/src/app.tsx b/src/app.tsx index 02f867b..859574e 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -7,7 +7,7 @@ export function App() { - + ); diff --git a/src/atoms/namespace.ts b/src/atoms/namespace.ts new file mode 100644 index 0000000..cde3e5e --- /dev/null +++ b/src/atoms/namespace.ts @@ -0,0 +1,3 @@ +import { atom } from "jotai"; + +export const namespaceAtom = atom(""); diff --git a/src/components/editor/namespaceModal.tsx b/src/components/editor/namespaceModal.tsx new file mode 100644 index 0000000..110f7fb --- /dev/null +++ b/src/components/editor/namespaceModal.tsx @@ -0,0 +1,62 @@ +import { useState } from "preact/hooks"; +import { fetchJson } from "../../util/fetchJson.ts"; +import { Loader } from "../loader.tsx"; +import { Modal } from "../modal.tsx"; +import useSwr from "swr"; + +export const NamespaceModal = ({ close }: { close: () => void }) => { + const { data: namespaces, isLoading } = useSwr( + "/api/pack/namespaces", + fetchJson, + ); + const [namespace, setNamespace] = useState(""); + const [invalid, setInvalid] = useState(""); + + const createNamespace = async (ns?: string) => { + const res = await fetch("/api/pack/namespaces", { + method: "POST", + body: ns ?? namespace, + }); + + if (res.status === 200) { + return close(); + } + + setInvalid(await res.text()); + }; + + return ( + + {isLoading + ? + : ( +
+

Create a new namespace

+ {!namespaces?.includes("minecraft") && + ( + + )} +
{ + e.preventDefault(); + createNamespace(); + }} + class="flex gap-2" + > + setNamespace((e.target as any).value)} + /> + +
+
+ )} +
+ ); +}; diff --git a/src/components/editor/selector.tsx b/src/components/editor/selector.tsx new file mode 100644 index 0000000..1d4b6d3 --- /dev/null +++ b/src/components/editor/selector.tsx @@ -0,0 +1,3 @@ +export const Selector = () => { + return
Selector
; +}; diff --git a/src/components/loader.tsx b/src/components/loader.tsx index d2cf620..b288fb6 100644 --- a/src/components/loader.tsx +++ b/src/components/loader.tsx @@ -1,4 +1,4 @@ -import { classList } from "../classes.ts"; +import { classList } from "../util/classes.ts"; interface IProps { msg?: string; diff --git a/src/classes.ts b/src/util/classes.ts similarity index 100% rename from src/classes.ts rename to src/util/classes.ts diff --git a/src/util/fetchJson.ts b/src/util/fetchJson.ts new file mode 100644 index 0000000..d95bcbd --- /dev/null +++ b/src/util/fetchJson.ts @@ -0,0 +1,2 @@ +export const fetchJson = (url: string, init?: RequestInit) => + fetch(url, init).then((res) => res.json()); diff --git a/src/views/editor.tsx b/src/views/editor.tsx index e1dae76..b3e15a5 100644 --- a/src/views/editor.tsx +++ b/src/views/editor.tsx @@ -3,11 +3,23 @@ import { Modal } from "../components/modal.tsx"; import { LabelledHr } from "../components/labelledHr.tsx"; import { PacksList } from "../components/packsList.tsx"; import { PackInfo } from "../components/packInfo.tsx"; +import { useAtom } from "jotai"; +import { namespaceAtom } from "../atoms/namespace.ts"; +import { Route, Routes } from "react-router-dom"; +import { Selector } from "../components/editor/selector.tsx"; +import { NamespaceModal } from "../components/editor/namespaceModal.tsx"; export const Editor = () => { const [packName, setPackName] = useState(""); const [loading, setLoading] = useState(true); const [namespaces, setNamespaces] = useState([]); + const [namespace, setNamespace] = useAtom(namespaceAtom); + + const fetchNamespaces = async () => { + const res = await fetch("/api/pack/namespaces"); + const json = await res.json(); + setNamespaces(json); + }; useEffect(() => { document.title = "BearMetal Packer"; @@ -15,6 +27,7 @@ export const Editor = () => { setPackName(json.packName); setLoading(false); }); + fetchNamespaces(); }, []); const setPackNameThing = async (event: SubmitEvent) => { @@ -56,9 +69,11 @@ export const Editor = () => { ); } + const [showNamespaceModal, setShowNamespaceModal] = useState(false); + return (
-
+

BearMetalPacker

@@ -67,14 +82,39 @@ export const Editor = () => {
    {namespaces.map((namespace) => ( -
  • setNamespace(namespace)} +
  • setNamespace(namespace)} > {namespace}
  • ))} +
  • + +
+ {showNamespaceModal && ( + { + setShowNamespaceModal(false); + fetchNamespaces(); + }} + /> + )}
+
+ {!namespace ?
No namespace set
: ( +
+

Namespace: {namespace}

+ + + +
+ )} +
); };