"use client"; import { MOD_ENDPOINT, PACK_ENDPOINT, PACKS_ENDPOINT } from "@/api/ENDPOINTS"; import { Pack } from "@/types/pack"; import { Button, Card, Label, Spinner, TextInput } from "flowbite-react"; import { useSearchParams } from "next/navigation"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { Modal, ModalBody, ModalFooter, ModalHeader } from "flowbite-react"; import { HiDownload, HiPlusCircle, HiTrash } from "react-icons/hi"; import { ModTable } from "../components/ModTable"; import { toast } from "react-toastify"; export default function PackPage() { const [packData, setPackData] = useState<Pack | null>(null); const [packDataLoading, setPackDataLoading] = useState(true); const router = useRouter(); const id = useSearchParams().get("id") || ""; const [addModModalOpen, setAddModModalOpen] = useState(true); const [modUrl, setModUrl] = useState(""); async function _getPacksData() { const res = await fetch(PACK_ENDPOINT("getPack", id)); if (!res.ok) router.push("/404"); const data: Pack = await res.json(); setPackData(data); setPackDataLoading(false); } useEffect(() => { if (id) { _getPacksData(); } else { router.push("/404"); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // eslint-disable-next-line @typescript-eslint/no-explicit-any const handleImageUpdate = (e: any) => { if (!packData || !window) return; const file = e.target.files[0]; const fileReader = new FileReader(); fileReader.onloadend = () => { const content = fileReader.result; fetch(`${PACK_ENDPOINT("editPackImage", packData._id)}`, { method: "POST", body: JSON.stringify({ image: content, mimetype: file.type, }), headers: { "content-type": "application/json", accept: "application/json", }, }).then(() => location.reload()); e.target.value = ""; }; fileReader.readAsDataURL(file); }; function deletePack() { if (!packData || !window) return; if (window.confirm(`Delete pack ${packData.title}?`)) { fetch(`${PACKS_ENDPOINT("deletePack", packData._id)}`); const ur = new URL(window.location.href); ur.searchParams.delete("id"); ur.pathname = "/"; window.location.href = ur.href; } } async function addMod() { let slug = null; let version = null; if (!modUrl) { toast.error("Mod url is required", { autoClose: 2500, closeOnClick: true, draggable: true, }); return; } let source = null; if (modUrl.includes("modrinth.com")) { source = "Modrinth"; } else if (modUrl.includes("curseforge.com")) { source = "CurseForge"; } else { toast.error("unknown source", { autoClose: 2500, closeOnClick: true, draggable: true, }); return; } switch (source) { case "Modrinth": const _tmp = modUrl.split("/mod/"); if (_tmp.length == 1) { toast.error("invalid Modrinth url", { autoClose: 2500, closeOnClick: true, draggable: true, }); return; } const _tmp2 = _tmp[1].split("/version/"); slug = _tmp2[0]; if (_tmp2.length > 1) { version = _tmp2[1]; } break; case "CurseForge": const _tmp3 = modUrl.split("/mc-mods/"); if (_tmp3.length == 1) { toast.error("invalid CurseForge url", { autoClose: 2500, closeOnClick: true, draggable: true, }); return; } const _tmp4 = _tmp3[1].split("/files/"); slug = _tmp4[0]; if (_tmp4.length > 1) { version = _tmp4[1]; } break; default: toast.error("failed to parse url", { autoClose: 2500, closeOnClick: true, draggable: true, }); return; } slug = slug.replace("/", ""); version = version ? version.replace("/", "") : null; if (!packData) return; const tid = toast.loading(`Adding mod`); const res = await fetch(MOD_ENDPOINT("addMod", packData._id), { method: "POST", body: JSON.stringify({ slug, version, source, }), 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, autoClose: 2500, closeOnClick: true, draggable: true, }); return; } toast.update(tid, { render: data.message, type: "success", isLoading: false, autoClose: 2500, closeOnClick: true, draggable: true, }); // setModUrl({ ...modUrl, value: "" }); _getPacksData(); } return ( <div> {packDataLoading && ( <div className="w-full flex justify-center items-center"> <Spinner></Spinner> </div> )} {packData && ( <div> <Card className="sticky top-0 left-0 right-0 z-10"> <div className="flex gap-4 items-center justify-between"> <div> <p className="text-xl font-semibold">{packData.version}</p> <p className="text-sm text-gray-400">{packData.modloader}</p> </div> <div className="flex gap-2 items-center"> <label htmlFor="pack-icon" className="cursor-pointer"> {/* eslint-disable-next-line @next/next/no-img-element */} <img alt="" src={PACK_ENDPOINT("getPackImage", packData._id)} className="w-12 h-12 rounded-md" /> </label> <input onChange={(e) => handleImageUpdate(e)} id="pack-icon" className="hidden" name="image" type="file" /> <div> <p className="text-xl font-semibold">{packData.title}</p> <p className="text-sm text-gray-400">by {packData.author}</p> </div> </div> <div className="flex gap-2 items-center"> <div> <p className="text-lg font-semibold"> {packData.mods.length} mods </p> <p className="text-sm text-gray-400"> v{packData.modpackVersion} </p> </div> <Button onClick={() => setAddModModalOpen(true)}> Add mod <HiPlusCircle className="ml-2 h-5 w-5" /> </Button> <Button> Download <HiDownload className="ml-2 h-5 w-5" /> </Button> <Button color={"red"} onClick={() => deletePack()}> Delete <HiTrash className="ml-2 h-5 w-5" /> </Button> </div> </div> </Card> <div className="mt-4"> <ModTable mods={packData.mods} updatePack={_getPacksData} packID={id} /> </div> </div> )} <Modal dismissible show={addModModalOpen} onClose={() => setAddModModalOpen(false)} > <ModalHeader>Add a mod</ModalHeader> <ModalBody> <div className="space-y-6"> <div className="flex-1"> <div className="mb-2 block"> <Label htmlFor="mod_url" className="text-lg"> Link </Label> </div> <TextInput id="mod_url" type="text" sizing="md" name="mod_url" onChange={(e) => setModUrl(e.target.value)} value={modUrl} required /> <p className="mt-2 text-gray-400">Can be one of:</p> <ul className="text-gray-400 list-disc"> <li className="ml-4"> https://modrinth.com/mod/[slug][/version/file_id] </li> <li className="ml-4"> https://www.curseforge.com/minecraft/mc-mods/[slug][/files/file_id] </li> </ul> </div> </div> </ModalBody> <ModalFooter> <Button onClick={() => addMod()}>Save</Button> </ModalFooter> </Modal> </div> ); }