diff --git a/app/client/package.json b/app/client/package.json index 48fabfe..ad7ef4c 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -21,7 +21,8 @@ "react-dom": "18.3.1", "react-router": "6.26.0", "react-router-dom": "6.26.0", - "sass": "1.77.8" + "sass": "1.77.8", + "yaml": "^2.5.0" }, "packageManager": "yarn@1.22.22" } diff --git a/app/client/src/index.html b/app/client/src/index.html index 51c4b87..ae625fc 100644 --- a/app/client/src/index.html +++ b/app/client/src/index.html @@ -3,11 +3,11 @@ + Open Hotel Asset Editor diff --git a/app/client/src/main.module.scss b/app/client/src/main.module.scss index cae9d53..c41f0d1 100644 --- a/app/client/src/main.module.scss +++ b/app/client/src/main.module.scss @@ -1,23 +1,40 @@ :root { --color-primary: #f0ebe3; + --color-secondary: #b4b0ad; --color-background: #121212; } * { font-size: 1.6rem; - font-family: ui-sans-serif, system-ui, sans-serif; + font-family: monospace; box-sizing: border-box; button { border-radius: 0; - padding: 0; + border: 0; + padding: 0 .5rem; + background: white; + cursor: pointer; + label { + cursor: pointer; + } + } + + select { + border-radius: 0; border: 0; } input { border: 0 solid; padding: 0; - border: 0; + width: 100%; + } + + form { + background-color: gray; + display: flex; + gap: .25rem; } img { @@ -37,6 +54,9 @@ h4 { font-size: 2rem; } + hr { + color: var(--color-primary); + } } html { @@ -55,7 +75,6 @@ body { background-attachment: fixed; background-size: cover; - overflow-x: hidden !important; } a { &:visited, diff --git a/app/client/src/modules/application/components/content/content.module.scss b/app/client/src/modules/application/components/content/content.module.scss index 6f6ff94..7009b2b 100644 --- a/app/client/src/modules/application/components/content/content.module.scss +++ b/app/client/src/modules/application/components/content/content.module.scss @@ -4,7 +4,6 @@ display: flex; flex-direction: column; height: 100%; - max-width: 200px; .outlet { flex: 1; diff --git a/app/client/src/modules/application/components/footer/footer.component.tsx b/app/client/src/modules/application/components/footer/footer.component.tsx index c9e2e3a..ab10c65 100644 --- a/app/client/src/modules/application/components/footer/footer.component.tsx +++ b/app/client/src/modules/application/components/footer/footer.component.tsx @@ -5,7 +5,18 @@ import { ContainerComponent } from "shared/components"; export const FooterComponent = () => { return ( ); }; diff --git a/app/client/src/modules/application/components/footer/footer.module.scss b/app/client/src/modules/application/components/footer/footer.module.scss index 95ca357..2d5c827 100644 --- a/app/client/src/modules/application/components/footer/footer.module.scss +++ b/app/client/src/modules/application/components/footer/footer.module.scss @@ -2,4 +2,24 @@ @import "../../../../shared/styles/mixins.module"; .footer { + margin: 2rem 0; + color: var(--color-secondary); + + .container { + display: flex; + flex-direction: row; + gap: 1rem; + line-height: 3rem; + + label { + flex: 1; + } + img { + height: 3rem; + opacity: .5; + &:hover { + opacity: 1; + } + } + } } diff --git a/app/client/src/modules/application/components/header/header.component.tsx b/app/client/src/modules/application/components/header/header.component.tsx index a7f7709..7c3252c 100644 --- a/app/client/src/modules/application/components/header/header.component.tsx +++ b/app/client/src/modules/application/components/header/header.component.tsx @@ -1,11 +1,20 @@ import React from "react"; import styles from "./header.module.scss"; -import { ContainerComponent } from "shared/components"; +import { ContainerComponent, LinkComponent } from "shared/components"; export const HeaderComponent = () => { return (
- header + + Open Hotel Asset Editor +
+ Home + File Manager + Sprite Sheets + Furniture + Human and Clothes +
+
); }; diff --git a/app/client/src/modules/application/components/header/header.module.scss b/app/client/src/modules/application/components/header/header.module.scss index e6ebfb9..2987b28 100644 --- a/app/client/src/modules/application/components/header/header.module.scss +++ b/app/client/src/modules/application/components/header/header.module.scss @@ -2,4 +2,24 @@ @import "../../../../shared/styles/mixins.module"; .header { + margin: 2rem 0; + color: var(--color-secondary); + + .container { + display: flex; + flex-direction: column; + gap: 1rem; + .items { + display: flex; + flex-direction: row; + gap: 2rem; + a { + color: var(--color-secondary); + + &:hover { + color: var(--color-primary); + } + } + } + } } diff --git a/app/client/src/modules/application/components/router/router.component.tsx b/app/client/src/modules/application/components/router/router.component.tsx index 4284495..f3e5a8b 100644 --- a/app/client/src/modules/application/components/router/router.component.tsx +++ b/app/client/src/modules/application/components/router/router.component.tsx @@ -3,13 +3,23 @@ import React from "react"; import { LayoutComponent } from "../layout"; import { NotFoundComponent } from "../not-found"; import { HomeComponent } from "modules/home"; +import { FileManagerComponent } from "modules/file-manager"; import { RedirectComponent } from "shared/components"; +import { SpriteSheetsComponent } from "modules/sprite-sheets"; const router = createBrowserRouter([ { element: , path: "/", children: [ + { + path: "/sprite-sheets", + element: , + }, + { + path: "/file-manager", + element: , + }, { path: "/", Component: () => , diff --git a/app/client/src/modules/file-manager/file-manager.component.tsx b/app/client/src/modules/file-manager/file-manager.component.tsx new file mode 100644 index 0000000..9572320 --- /dev/null +++ b/app/client/src/modules/file-manager/file-manager.component.tsx @@ -0,0 +1,173 @@ +import React, { + useEffect, + useState, + ChangeEvent, + useRef, + FormEvent, + useMemo, +} from "react"; +import { useData } from "shared/hooks"; +import { File } from "shared/types"; +import { getBase64FromBody } from "shared/utils"; +import { parse } from "yaml"; +import styles from "./file-manager.module.scss"; + +export const FileManagerComponent: React.FC = () => { + const { readDirectory, getFile, addFiles, remove, createDirectory } = + useData(); + + const uploadRef = useRef(); + + const [path, setPath] = useState(""); + const [files, setFiles] = useState([]); + + const [currentFile, setCurrentFile] = useState(); + const [currentFileData, setCurrentFileData] = useState<{ + data: string; + format: "image" | "text" | "json"; + }>(); + + useEffect(() => { + if (!currentFile) return setCurrentFileData(null); + + getFile(path + "/" + currentFile.name).then(async (data) => { + if (currentFile.name.endsWith(".png")) { + return setCurrentFileData({ + data: await getBase64FromBody(data), + format: "image", + }); + } + if (currentFile.name.endsWith(".yml")) { + return setCurrentFileData({ + data: parse(await data.text()), + format: "json", + }); + } + if (currentFile.name.endsWith(".json")) { + return setCurrentFileData({ + data: await data.json(), + format: "json", + }); + } + return setCurrentFileData({ + data: await data.text(), + format: "text", + }); + }); + }, [currentFile]); + + const loadFiles = () => { + setCurrentFile(null); + setCurrentFileData(null); + readDirectory(path).then(setFiles); + }; + + useEffect(() => { + loadFiles(); + }, [path]); + + const onClickBack = () => { + if (path === "/") return; + const pathArr = path.split("/"); + setPath(pathArr.slice(0, pathArr.length - 1).join("/")); + }; + + const onClickFile = (file: File) => async () => { + if (file.isDirectory) setPath((path) => path + "/" + file.name); + + if (file.isFile) setCurrentFile(file); + }; + const onClickDeleteFile = (file: File) => async () => { + await remove(path + `/${file.name}`); + loadFiles(); + }; + + const onUploadFiles = async (event: ChangeEvent) => { + const { files } = event.target; + await addFiles(path, files); + loadFiles(); + //@ts-ignore + uploadRef.current.value = ""; + }; + + const onCreateDirectory = async (event: FormEvent) => { + event.preventDefault(); + + const data = new FormData(event.target as unknown as HTMLFormElement); + const name = data.get("name") as string; + + if (!name.length) return; + + const targetPath = path + `/${name}`; + //@ts-ignore + event.target.reset(); + await createDirectory(targetPath); + loadFiles(); + setPath(targetPath); + }; + + const sortedFiles = useMemo( + () => files.toSorted((fileA, fileB) => (fileA.isDirectory ? -1 : 1)), + [files], + ); + + return ( +
+
+ {path || "/"} + {currentFile?.name ? `/${currentFile?.name}` : ""} +
+
+
+
+ {path ? "⪻" : "|"} +
+ {sortedFiles.map((file) => ( +
+ + +
+ ))} +
+
+
+ +
+ + +
+
+ + +
+
+
+ {currentFileData ? ( + <> + {currentFileData.format === "json" ? ( +