import { useNavigate, useParams } from "react-router"; import { createContext, createRef, useCallback, useContext, useEffect, useRef, useState } from "react"; import { ResourceDetails, RFile, Storage, Comment } from "../network/models.ts"; import { network } from "../network/network.ts"; import showToast from "../components/toast.ts"; import Markdown from "react-markdown"; import "../markdown.css"; import Loading from "../components/loading.tsx"; import { MdAdd, MdOutlineArticle, MdOutlineComment, MdOutlineDataset, MdOutlineDelete, MdOutlineDownload, MdOutlineEdit } from "react-icons/md"; import { app } from "../app.ts"; import { uploadingManager } from "../network/uploading.ts"; import { ErrorAlert } from "../components/alert.tsx"; import { useTranslation } from "react-i18next"; import Pagination from "../components/pagination.tsx"; import showPopup, { useClosePopup } from "../components/popup.tsx"; import { Turnstile } from "@marsidev/react-turnstile"; import Button from "../components/button.tsx"; import Badge, { BadgeAccent } from "../components/badge.tsx"; import Input from "../components/input.tsx"; export default function ResourcePage() { const params = useParams() const { t } = useTranslation(); const idStr = params.id const id = idStr ? parseInt(idStr) : NaN const [resource, setResource] = useState(null) const [page, setPage] = useState(0) const reload = useCallback(async () => { if (!isNaN(id)) { setResource(null) const res = await network.getResourceDetails(id) if (res.success) { setResource(res.data!) } else { showToast({ message: res.message, type: "error" }) } } }, [id]) useEffect(() => { document.title = t("Resource Details"); }, [t]) useEffect(() => { setResource(null); if (!isNaN(id)) { network.getResourceDetails(id).then((res) => { if (res.success) { setResource(res.data!) document.title = res.data!.title } else { showToast({ message: res.message, type: "error" }) } }) } }, [id]) const navigate = useNavigate() if (isNaN(id)) { return
{t("Resource ID is required")}
} if (!resource) { return } return

{resource.title}

{ resource.alternativeTitles.map((e, i) => { return

{e}

}) }

{ resource.tags.map((e) => { return { navigate(`/tag/${e.name}`); }}>{e.name} }) }

{ app.isAdmin() || app.user?.id === resource.author.id ? : null }
} function DeleteResourceDialog({ resourceId, uploaderId }: { resourceId: number, uploaderId?: number }) { const [isLoading, setLoading] = useState(false) const navigate = useNavigate() const { t } = useTranslation() const handleDelete = async () => { if (isLoading) { return } setLoading(true) const res = await network.deleteResource(resourceId) const dialog = document.getElementById("delete_resource_dialog") as HTMLDialogElement dialog.close() if (res.success) { showToast({ message: t("Resource deleted successfully"), type: "success" }) navigate("/", { replace: true }) } else { showToast({ message: res.message, type: "error" }) } setLoading(false) } if (!app.isAdmin() && app.user?.id !== uploaderId) { return <> } return <>

{t("Delete Resource")}

{t("Are you sure you want to delete the resource")}? {t("This action cannot be undone.")}

} const context = createContext<() => void>(() => { }) function Article({ resource }: { resource: ResourceDetails }) { const articleRef = useRef(null) const navigate = useNavigate() useEffect(() => { if (articleRef.current) { console.log("render") for (let child of articleRef.current.children) { console.log("child", child) if (child.tagName === "P" && child.children.length === 1 && child.children[0].tagName === "A") { const href = (child.children[0] as HTMLAnchorElement).href as string console.log("href", href) console.log("origin", window.location.origin) if (href.startsWith(window.location.origin) || href.startsWith("/")) { console.log("href starts with origin") let path = href if (path.startsWith(window.location.origin)) { path = path.substring(window.location.origin.length) } if (path.startsWith("/resources/")) { const content = child.children[0].innerHTML const id = path.substring("/resources/".length) for (let r of resource.related) { if (r.id.toString() === id) { child.children[0].classList.add("hidden") let div = document.createElement("div") div.innerHTML = ` ${child.innerHTML}
${r.image ? `
Cover
` : ""}

${r.title}

${content}

` child.appendChild(div); (child as HTMLParagraphElement).onclick = (e) => { e.stopPropagation() e.preventDefault() navigate(`/resources/${r.id}`) div.remove(); } } } } } } } } }, [resource]) return
{resource.article}
} function fileSizeToString(size: number) { if (size < 1024) { return size + "B" } else if (size < 1024 * 1024) { return (size / 1024).toFixed(2) + "KB" } else if (size < 1024 * 1024 * 1024) { return (size / 1024 / 1024).toFixed(2) + "MB" } else { return (size / 1024 / 1024 / 1024).toFixed(2) + "GB" } } function FileTile({ file }: { file: RFile }) { const buttonRef = createRef() const { t } = useTranslation() return

{file.filename}

{file.description}

{file.is_redirect ? t("Redirect") : fileSizeToString(file.size)}

} function CloudflarePopup({ file }: { file: RFile }) { const closePopup = useClosePopup() const [isLoading, setLoading] = useState(true) return
{ isLoading ?
: null } { setLoading(false) }} onSuccess={(token) => { closePopup(); const link = network.getFileDownloadLink(file.id, token); window.open(link, "_blank"); }}>
} function Files({ files, resourceID }: { files: RFile[], resourceID: number }) { return
{ files.map((file) => { return }) }
{ app.canUpload() &&
}
} enum FileType { redirect = "redirect", upload = "upload", } function CreateFileDialog({ resourceId }: { resourceId: number }) { const { t } = useTranslation(); const [isLoading, setLoading] = useState(false) const storages = useRef(null) const mounted = useRef(true) const [fileType, setFileType] = useState(null) const [filename, setFilename] = useState("") const [redirectUrl, setRedirectUrl] = useState("") const [storage, setStorage] = useState(null) const [file, setFile] = useState(null) const [description, setDescription] = useState("") const reload = useContext(context) const [isSubmitting, setSubmitting] = useState(false) const [error, setError] = useState(null) useEffect(() => { mounted.current = true return () => { mounted.current = false } }, []); const submit = async () => { if (isSubmitting) { return } if (!fileType) { setError(t("Please select a file type")) return } setSubmitting(true) if (fileType === FileType.redirect) { if (!redirectUrl || !filename || !description) { setError(t("Please fill in all fields")); setSubmitting(false); return; } const res = await network.createRedirectFile(filename, description, resourceId, redirectUrl); if (res.success) { setSubmitting(false) const dialog = document.getElementById("upload_dialog") as HTMLDialogElement dialog.close() showToast({ message: t("File created successfully"), type: "success" }) reload() } else { setError(res.message) setSubmitting(false) } } else { if (!file || !storage) { setError(t("Please select a file and storage")) setSubmitting(false) return } const res = await uploadingManager.addTask(file, resourceId, storage.id, description, () => { if (mounted.current) { reload(); } }); if (res.success) { setSubmitting(false) const dialog = document.getElementById("upload_dialog") as HTMLDialogElement dialog.close() showToast({ message: t("Successfully create uploading task."), type: "success" }) } else { setError(res.message) setSubmitting(false) } } } return <>

{t("Create File")}

{t("Type")}

{ setFileType(null); }} /> { setFileType(FileType.redirect); }} /> { setFileType(FileType.upload); }} />
{ fileType === FileType.redirect && <>

{t("User who click the file will be redirected to the URL")}

{ setFilename(e.target.value) }} /> { setRedirectUrl(e.target.value) }} /> { setDescription(e.target.value) }} /> } { fileType === FileType.upload && <>

{t("Upload a file to server, then the file will be moved to the selected storage.")}

{ if (e.target.files) { setFile(e.target.files[0]) } }} /> { setDescription(e.target.value) }} /> } {error && }
} function UpdateFileInfoDialog({ file }: { file: RFile }) { const [isLoading, setLoading] = useState(false) const [filename, setFilename] = useState(file.filename) const [description, setDescription] = useState(file.description) const { t } = useTranslation() const reload = useContext(context) const handleUpdate = async () => { if (isLoading) { return } setLoading(true) const res = await network.updateFile(file.id, filename, description); const dialog = document.getElementById(`update_file_info_dialog_${file.id}`) as HTMLDialogElement dialog.close() if (res.success) { showToast({ message: t("File info updated successfully"), type: "success" }) reload() } else { showToast({ message: res.message, type: "error" }) } setLoading(false) } if (!app.isAdmin() && app.user?.id !== file.user_id) { return <> } return <>

{t("Update File Info")}

setFilename(e.target.value)} /> setDescription(e.target.value)} />
} function Comments({ resourceId }: { resourceId: number }) { const [page, setPage] = useState(1); const [maxPage, setMaxPage] = useState(0); const [listKey, setListKey] = useState(0); const [commentContent, setCommentContent] = useState(""); const [isLoading, setLoading] = useState(false); const { t } = useTranslation(); const reload = useCallback(() => { setPage(1); setMaxPage(0); setListKey(prev => prev + 1); }, []) const sendComment = async () => { if (isLoading) { return; } if (commentContent === "") { showToast({ message: t("Comment content cannot be empty"), type: "error" }); return; } setLoading(true); const res = await network.createComment(resourceId, commentContent); if (res.success) { setCommentContent(""); showToast({ message: t("Comment created successfully"), type: "success" }); reload(); } else { showToast({ message: res.message, type: "error" }); } setLoading(false); } return