feat: pack creating

This commit is contained in:
Kentai Radiquum 2025-05-05 01:33:58 +05:00
parent 5be021789b
commit af7b8a6fea
Signed by: Radiquum
GPG key ID: 858E8EE696525EED
27 changed files with 677 additions and 130 deletions

43
gui/app/App.tsx Normal file
View file

@ -0,0 +1,43 @@
"use client";
import { Geist, Geist_Mono } from "next/font/google";
import { Menu } from "./components/Sidebar";
import { Bounce, ToastContainer } from "react-toastify";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
type APPProps = {
children: React.ReactNode;
};
export const App = ({ children }: APPProps) => {
return (
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased flex h-screen overflow-hidden`}
>
<Menu></Menu>
<div className="p-2 overflow-auto w-full">{children}</div>
<ToastContainer
position="bottom-right"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick={false}
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme="colored"
transition={Bounce}
/>
</body>
);
};

View file

@ -0,0 +1,56 @@
import {
Sidebar,
SidebarItem,
SidebarItemGroup,
SidebarItems,
} from "flowbite-react";
import {
// HiArrowSmRight,
HiChartPie,
HiPlusCircle
// HiInbox,
// HiShoppingBag,
// HiTable,
// HiUser,
// HiViewBoards,
} from "react-icons/hi";
export const Menu = () => {
return (
<Sidebar aria-label="Default sidebar example">
<SidebarItems>
<SidebarItemGroup>
<SidebarItem href="/" icon={HiChartPie}>
Dashboard
</SidebarItem>
<SidebarItem href="/pack/new" icon={HiPlusCircle}>
New mod pack
</SidebarItem>
{/* <SidebarItem
href="#"
icon={HiViewBoards}
label="Pro"
labelColor="dark"
>
Kanban
</SidebarItem>
<SidebarItem href="#" icon={HiInbox} label="3">
Inbox
</SidebarItem>
<SidebarItem href="#" icon={HiUser}>
Users
</SidebarItem>
<SidebarItem href="#" icon={HiShoppingBag}>
Products
</SidebarItem>
<SidebarItem href="#" icon={HiArrowSmRight}>
Sign In
</SidebarItem>
<SidebarItem href="#" icon={HiTable}>
Sign Up
</SidebarItem> */}
</SidebarItemGroup>
</SidebarItems>
</Sidebar>
);
};

View file

@ -1,4 +1,6 @@
@import "tailwindcss";
@plugin "flowbite-react/plugin/tailwindcss";
@source "../.flowbite-react/class-list.json";
:root {
--background: #ffffff;
@ -14,7 +16,7 @@
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--background: #111827;
--foreground: #ededed;
}
}
@ -22,5 +24,5 @@
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
font-family: var(--font-sans), Helvetica, sans-serif;
}

View file

@ -1,16 +1,6 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
import { App } from "./App";
export const metadata: Metadata = {
title: "Create Next App",
@ -24,11 +14,7 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
<App>{children}</App>
</html>
);
}

221
gui/app/pack/new/page.tsx Normal file
View file

@ -0,0 +1,221 @@
"use client";
import { Card, FileInput } from "flowbite-react";
import { Label, TextInput, Select } from "flowbite-react";
import { useState } from "react";
import { HiUser, HiAnnotation } from "react-icons/hi";
import { Button } from "flowbite-react";
import { useRouter } from "next/navigation";
import mc from "../../../api/mc_version.json";
import { ENDPOINTS, PACK_IMG_ENDPOINTS } from "@/api/ENDPOINTS";
import { toast } from "react-toastify";
const mcr = mc.reverse();
export default function PackNew() {
const router = useRouter()
const [image, setImage] = useState<null | string>(null);
const [imageMime, setImageMime] = useState<null | string>(null);
const [packInfo, setPackInfo] = useState({
title: "",
author: "",
modloader: "Forge",
version: "1.21.5",
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleImagePreview = (e: any) => {
const file = e.target.files[0];
const fileReader = new FileReader();
fileReader.onloadend = () => {
const content = fileReader.result;
setImage(content as string);
setImageMime(file.type);
e.target.value = "";
};
fileReader.readAsDataURL(file);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function handleInput(e: any) {
const regex = /[^a-zA-Zа-яА-Я0-9_.()\- \[\]]/g;
setPackInfo({
...packInfo,
[e.target.name]: e.target.value.replace(regex, ""),
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function submit(e: any) {
e.preventDefault();
async function _submit() {
const tid = toast.loading(`Creating Pack "${packInfo.title}"`)
const res = await fetch(`${ENDPOINTS.createPack}`, {
method: "POST",
body: JSON.stringify(packInfo),
headers: {
"content-type": "application/json",
accept: "application/json",
},
});
const data = await res.json();
if (data.status != "ok") {
toast.update(tid, {render: data.message, type: "error", isLoading: false})
return;
}
if (image) {
await fetch(`${PACK_IMG_ENDPOINTS("editPackImage", data.id)}`, {
method: "POST",
body: JSON.stringify({
image: image,
mimetype: imageMime
}),
headers: {
"content-type": "application/json",
accept: "application/json",
},
});
}
toast.update(tid, {render: data.message, type: "success", isLoading: false})
router.push(`/pack/${data.id}`)
}
_submit();
}
return (
<Card className="w-full">
<form
className="flex flex-col gap-4"
encType="multipart/form-data"
onSubmit={(e) => submit(e)}
>
<div className="flex w-full items-center justify-center">
<Label
htmlFor="dropzone-file"
className="flex h-64 w-full cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:hover:border-gray-500 dark:hover:bg-gray-600"
>
{image ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={image} alt="preview" className="overflow-hidden" />
) : (
<div className="flex flex-col items-center justify-center pb-6 pt-5">
<svg
className="mb-4 h-8 w-8 text-gray-500 dark:text-gray-400"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 20 16"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"
/>
</svg>
<p className="mb-2 text-sm text-gray-500 dark:text-gray-400">
<span className="font-semibold">Click to upload</span> or drag
and drop
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
PNG or JPG
</p>
</div>
)}
<FileInput
id="dropzone-file"
className="hidden"
name="image"
onChange={(e) => handleImagePreview(e)}
/>
</Label>
</div>
<div className="flex gap-4">
<div className="flex-1">
<div className="mb-2 block">
<Label htmlFor="base" className="text-lg">
Title
</Label>
</div>
<TextInput
id="base"
type="text"
sizing="md"
name="title"
onChange={(e) => handleInput(e)}
value={packInfo.title}
icon={HiAnnotation}
required
/>
</div>
<div className="flex-1">
<div className="mb-2 block">
<Label htmlFor="base" className="text-lg">
Author
</Label>
</div>
<TextInput
id="base"
type="text"
sizing="md"
name="author"
onChange={(e) => handleInput(e)}
value={packInfo.author}
icon={HiUser}
required
/>
</div>
</div>
<div className="flex gap-4">
<div className="flex-1">
<div className="mb-2 block">
<Label htmlFor="base" className="text-lg">
Mod Loader
</Label>
</div>
<Select
id="modloader"
name="modloader"
required
onChange={(e) => handleInput(e)}
>
<option value="Forge">Forge</option>
<option value="Fabric">Fabric</option>
<option value="NeoForge">NeoForge</option>
<option value="Quilt">Quilt</option>
</Select>
</div>
<div className="flex-1">
<div className="mb-2 block">
<Label htmlFor="base" className="text-lg">
Game Version
</Label>
</div>
<Select
id="version"
name="version"
required
onChange={(e) => handleInput(e)}
>
{mcr.map((version) => (
<option key={version} value={version}>
{version}
</option>
))}
</Select>
</div>
</div>
<div className="flex justify-end">
<Button type="submit">Create</Button>
</div>
</form>
</Card>
);
}

View file

@ -1,103 +1,5 @@
import Image from "next/image";
"use client";
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div>
);
return <></>
}