"use client"; import { DOWNLOAD_ENDPOINT, MOD_ENDPOINT, PACK_ENDPOINT, PACKS_ENDPOINT, } from "@/api/ENDPOINTS"; import { Pack } from "@/types/pack"; import { Button, Card, Label, Progress, 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"; import { io } from "socket.io-client"; const socketURL = process.env.NEXT_PUBLIC_SOCKET_URL ? process.env.NEXT_PUBLIC_SOCKET_URL : undefined; const socket = io(socketURL); export default function PackPage() { const [packData, setPackData] = useState(null); const [packDataLoading, setPackDataLoading] = useState(true); const router = useRouter(); const id = useSearchParams().get("id") || ""; const [addModModalOpen, setAddModModalOpen] = useState(false); const [modUrl, setModUrl] = useState(""); const [downloadModalOpen, setdownloadModalOpen] = useState(false); const [downloadProgressFile, setDownloadProgressFile] = useState({ filename: "filename", total: 1, current: 1, }); const [downloadProgressTotal, setDownloadProgressTotal] = useState(100); const [downloadProgressCurrent, setDownloadProgressCurrent] = useState(100); 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 }, []); useEffect(() => { function percentage(partialValue: number, totalValue: number) { const flo = (100 * partialValue) / totalValue; return Number(flo.toFixed(2)); } // eslint-disable-next-line @typescript-eslint/no-explicit-any function onDownloadCurrent(data: any) { switch (data.status) { case "pending": const progress = percentage(data.download_bytes, data.total_bytes); setDownloadProgressCurrent(progress); break; case "ok": setDownloadProgressCurrent(0); break; case "error": toast.error(data.message, { autoClose: 2500, closeOnClick: true, draggable: true, }); break; default: console.log(`GET UNKNOWN WS STATUS: ${data.status}`); break; } } // eslint-disable-next-line @typescript-eslint/no-explicit-any function onDownloadTotal(data: any) { const progress = percentage(data.current, data.total); setDownloadProgressTotal(progress); setDownloadProgressFile({ filename: data.filename, total: data.total, current: data.current, }); } socket.on("download_current", (data) => onDownloadCurrent(data)); socket.on("download_total", (data) => onDownloadTotal(data)); return () => { socket.off("download_current", (data) => onDownloadCurrent(data)); socket.off("download_total", (data) => onDownloadTotal(data)); }; }, []); // 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(); } async function downloadPack() { if (!packData) return; fetch(`${DOWNLOAD_ENDPOINT["downloadPack"]}`, { method: "POST", body: JSON.stringify({ pack_id: packData._id, }), headers: { "content-type": "application/json", accept: "application/json", }, }); setdownloadModalOpen(true); } async function downloadMods(mods: string[]) { if (!packData) return; fetch(`${DOWNLOAD_ENDPOINT["downloadMods"]}`, { method: "POST", body: JSON.stringify({ pack_id: packData._id, mods: mods, }), headers: { "content-type": "application/json", accept: "application/json", }, }); setdownloadModalOpen(true); } return (
{packDataLoading && (
)} {packData && (

{packData.version}

{packData.modloader}

handleImageUpdate(e)} id="pack-icon" className="hidden" name="image" type="file" />

{packData.title}

by {packData.author}

{packData.mods.length} mods

v{packData.modpackVersion}

)} setAddModModalOpen(false)} > Add a mod
setModUrl(e.target.value)} value={modUrl} required />

Can be one of:

  • https://modrinth.com/mod/[slug][/version/file_id]
  • https://www.curseforge.com/minecraft/mc-mods/[slug][/files/file_id]
{ if (downloadProgressFile.current == downloadProgressFile.total) { setdownloadModalOpen(false); } }} > Download progress

{downloadProgressFile.current} / {downloadProgressFile.total}

{downloadProgressFile.current == downloadProgressFile.total ? (

Mods downloaded, You can find them in path: packs/{packData?._id} /mods

) : ( "" )}
); }