serve frontend

This commit is contained in:
2025-05-15 12:47:15 +08:00
parent d6fea884cf
commit 578aab36c3
24 changed files with 400 additions and 155 deletions

View File

@@ -1,21 +1,21 @@
import {useNavigate, useParams} from "react-router";
import {createContext, useCallback, useContext, useEffect, useRef, useState} from "react";
import {ResourceDetails, RFile, Storage, Comment} from "../network/models.ts";
import {network} from "../network/network.ts";
import { useNavigate, useParams } from "react-router";
import { createContext, 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, MdOutlineDownload} 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 { MdAdd, MdOutlineArticle, MdOutlineComment, MdOutlineDataset, MdOutlineDownload } 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";
export default function ResourcePage() {
const params = useParams()
const {t} = useTranslation();
const { t } = useTranslation();
const idStr = params.id
@@ -32,18 +32,23 @@ export default function ResourcePage() {
if (res.success) {
setResource(res.data!)
} else {
showToast({message: res.message, type: "error"})
showToast({ message: res.message, type: "error" })
}
}
}, [id])
useEffect(() => {
document.title = t("Resource Details");
}, [])
useEffect(() => {
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"})
showToast({ message: res.message, type: "error" })
}
})
}
@@ -60,7 +65,7 @@ export default function ResourcePage() {
}
if (!resource) {
return <Loading/>
return <Loading />
}
return <context.Provider value={reload}>
@@ -79,7 +84,7 @@ export default function ResourcePage() {
<div className="flex items-center ">
<div className="avatar">
<div className="w-6 rounded-full">
<img src={network.getUserAvatar(resource.author)} alt={"avatar"}/>
<img src={network.getUserAvatar(resource.author)} alt={"avatar"} />
</div>
</div>
<div className="w-2"></div>
@@ -99,40 +104,40 @@ export default function ResourcePage() {
<label className="tab transition-all">
<input type="radio" name="my_tabs" checked={page === 0} onChange={() => {
setPage(0)
}}/>
<MdOutlineArticle className="text-xl mr-2"/>
}} />
<MdOutlineArticle className="text-xl mr-2" />
<span className="text-sm">
{t("Description")}
</span>
{t("Description")}
</span>
</label>
<div key={"article"} className="tab-content p-2">
<Article article={resource.article}/>
<Article article={resource.article} />
</div>
<label className="tab transition-all">
<input type="radio" name="my_tabs" checked={page === 1} onChange={() => {
setPage(1)
}}/>
<MdOutlineDataset className="text-xl mr-2"/>
}} />
<MdOutlineDataset className="text-xl mr-2" />
<span className="text-sm">
{t("Files")}
</span>
{t("Files")}
</span>
</label>
<div key={"files"} className="tab-content p-2">
<Files files={resource.files} resourceID={resource.id}/>
<Files files={resource.files} resourceID={resource.id} />
</div>
<label className="tab transition-all">
<input type="radio" name="my_tabs" checked={page === 2} onChange={() => {
setPage(2)
}}/>
<MdOutlineComment className="text-xl mr-2"/>
}} />
<MdOutlineComment className="text-xl mr-2" />
<span className="text-sm">
{t("Comments")}
</span>
{t("Comments")}
</span>
</label>
<div key={"comments"} className="tab-content p-2">
<Comments resourceId={resource.id}/>
<Comments resourceId={resource.id} />
</div>
</div>
<div className="h-4"></div>
@@ -143,13 +148,13 @@ export default function ResourcePage() {
const context = createContext<() => void>(() => {
})
function Article({article}: { article: string }) {
function Article({ article }: { article: string }) {
return <article>
<Markdown>{article}</Markdown>
</article>
}
function FileTile({file}: { file: RFile }) {
function FileTile({ file }: { file: RFile }) {
return <div className={"card card-border border-base-300 my-2"}>
<div className={"p-4 flex flex-row items-center"}>
<div className={"grow"}>
@@ -161,14 +166,14 @@ function FileTile({file}: { file: RFile }) {
const link = network.getFileDownloadLink(file.id);
window.open(link, "_blank");
}}>
<MdOutlineDownload size={24}/>
<MdOutlineDownload size={24} />
</button>
</div>
</div>
</div>
}
function Files({files, resourceID}: { files: RFile[], resourceID: number }) {
function Files({ files, resourceID }: { files: RFile[], resourceID: number }) {
return <div>
{
files.map((file) => {
@@ -189,8 +194,8 @@ enum FileType {
upload = "upload",
}
function CreateFileDialog({resourceId}: { resourceId: number }) {
const {t} = useTranslation();
function CreateFileDialog({ resourceId }: { resourceId: number }) {
const { t } = useTranslation();
const [isLoading, setLoading] = useState(false)
const storages = useRef<Storage[] | null>(null)
const mounted = useRef(true)
@@ -236,7 +241,7 @@ function CreateFileDialog({resourceId}: { resourceId: number }) {
setSubmitting(false)
const dialog = document.getElementById("upload_dialog") as HTMLDialogElement
dialog.close()
showToast({message: t("File created successfully"), type: "success"})
showToast({ message: t("File created successfully"), type: "success" })
reload()
} else {
setError(res.message)
@@ -257,7 +262,7 @@ function CreateFileDialog({resourceId}: { resourceId: number }) {
setSubmitting(false)
const dialog = document.getElementById("upload_dialog") as HTMLDialogElement
dialog.close()
showToast({message: t("Successfully create uploading task."), type: "success"})
showToast({ message: t("Successfully create uploading task."), type: "success" })
} else {
setError(res.message)
setSubmitting(false)
@@ -277,7 +282,7 @@ function CreateFileDialog({resourceId}: { resourceId: number }) {
return;
}
if (!res.success) {
showToast({message: res.message, type: "error"})
showToast({ message: res.message, type: "error" })
} else {
storages.current = res.data!
setLoading(false)
@@ -291,7 +296,7 @@ function CreateFileDialog({resourceId}: { resourceId: number }) {
dialog.showModal()
}}>
{
isLoading ? <span className={"loading loading-spinner loading-sm"}></span> : <MdAdd size={24}/>
isLoading ? <span className={"loading loading-spinner loading-sm"}></span> : <MdAdd size={24} />
}
<span className={"text-sm"}>
{t("Upload")}
@@ -305,13 +310,13 @@ function CreateFileDialog({resourceId}: { resourceId: number }) {
<form className="filter mb-2">
<input className="btn btn-square" type="reset" value="×" onClick={() => {
setFileType(null);
}}/>
}} />
<input className="btn text-sm" type="radio" name="type" aria-label={t("Redirect")} onInput={() => {
setFileType(FileType.redirect);
}}/>
}} />
<input className="btn text-sm" type="radio" name="type" aria-label={t("Upload")} onInput={() => {
setFileType(FileType.upload);
}}/>
}} />
</form>
{
@@ -319,13 +324,13 @@ function CreateFileDialog({resourceId}: { resourceId: number }) {
<p className={"text-sm p-2"}>{t("User who click the file will be redirected to the URL")}</p>
<input type="text" className="input w-full my-2" placeholder={t("File Name")} onChange={(e) => {
setFilename(e.target.value)
}}/>
}} />
<input type="text" className="input w-full my-2" placeholder={t("URL")} onChange={(e) => {
setRedirectUrl(e.target.value)
}}/>
}} />
<input type="text" className="input w-full my-2" placeholder={t("Description")} onChange={(e) => {
setDescription(e.target.value)
}}/>
}} />
</>
}
@@ -348,25 +353,25 @@ function CreateFileDialog({resourceId}: { resourceId: number }) {
{
storages.current?.map((s) => {
return <option key={s.id}
value={s.id}>{s.name}({(s.currentSize / 1024 / 1024).toFixed(2)}/{s.maxSize / 1024 / 1024}MB)</option>
value={s.id}>{s.name}({(s.currentSize / 1024 / 1024).toFixed(2)}/{s.maxSize / 1024 / 1024}MB)</option>
})
}
</select>
<input
type="file" className="file-input w-full my-2" onChange={(e) => {
if (e.target.files) {
setFile(e.target.files[0])
}
}}/>
if (e.target.files) {
setFile(e.target.files[0])
}
}} />
<input type="text" className="input w-full my-2" placeholder={t("Description")} onChange={(e) => {
setDescription(e.target.value)
}}/>
}} />
</>
}
{error && <ErrorAlert className={"my-2"} message={error}/>}
{error && <ErrorAlert className={"my-2"} message={error} />}
<div className="modal-action">
<form method="dialog">
@@ -382,7 +387,7 @@ function CreateFileDialog({resourceId}: { resourceId: number }) {
</>
}
function Comments({resourceId}: { resourceId: number }) {
function Comments({ resourceId }: { resourceId: number }) {
const [page, setPage] = useState(1);
const [maxPage, setMaxPage] = useState(0);
@@ -404,17 +409,17 @@ function Comments({resourceId}: { resourceId: number }) {
return;
}
if (commentContent === "") {
showToast({message: "Comment content cannot be empty", type: "error"});
showToast({ message: "Comment content cannot be empty", type: "error" });
return;
}
setLoading(true);
const res = await network.createComment(resourceId, commentContent);
if (res.success) {
setCommentContent("");
showToast({message: "Comment created successfully", type: "success"});
showToast({ message: "Comment created successfully", type: "success" });
reload();
} else {
showToast({message: res.message, type: "error"});
showToast({ message: res.message, type: "error" });
}
setLoading(false);
}
@@ -422,23 +427,23 @@ function Comments({resourceId}: { resourceId: number }) {
return <div>
<div className={"mt-4 mb-6 textarea w-full p-4 h-28 flex flex-col"}>
<textarea placeholder={"Write down your comment"} className={"w-full resize-none grow"} value={commentContent}
onChange={(e) => setCommentContent(e.target.value)}/>
onChange={(e) => setCommentContent(e.target.value)} />
<div className={"flex flex-row-reverse"}>
<button onClick={sendComment}
className={`btn btn-primary h-8 text-sm mx-2 ${commentContent === "" && "btn-disabled"}`}>
className={`btn btn-primary h-8 text-sm mx-2 ${commentContent === "" && "btn-disabled"}`}>
{isLoading ? <span className={"loading loading-spinner loading-sm"}></span> : null}
Submit
</button>
</div>
</div>
<CommentsList resourceId={resourceId} page={page} maxPageCallback={setMaxPage} key={listKey}/>
<CommentsList resourceId={resourceId} page={page} maxPageCallback={setMaxPage} key={listKey} />
{maxPage && <div className={"w-full flex justify-center"}>
<Pagination page={page} setPage={setPage} totalPages={maxPage}/>
<Pagination page={page} setPage={setPage} totalPages={maxPage} />
</div>}
</div>
}
function CommentsList({resourceId, page, maxPageCallback}: {
function CommentsList({ resourceId, page, maxPageCallback }: {
resourceId: number,
page: number,
maxPageCallback: (maxPage: number) => void
@@ -461,25 +466,25 @@ function CommentsList({resourceId, page, maxPageCallback}: {
if (comments == null) {
return <div className={"w-full"}>
<Loading/>
<Loading />
</div>
}
return <>
{
comments.map((comment) => {
return <CommentTile comment={comment} key={comment.id}/>
return <CommentTile comment={comment} key={comment.id} />
})
}
</>
}
function CommentTile({comment}: { comment: Comment }) {
function CommentTile({ comment }: { comment: Comment }) {
return <div className={"card card-border border-base-300 p-2 my-3"}>
<div className={"flex flex-row items-center my-1 mx-1"}>
<div className="avatar">
<div className="w-8 rounded-full">
<img src={network.getUserAvatar(comment.user)} alt={"avatar"}/>
<img src={network.getUserAvatar(comment.user)} alt={"avatar"} />
</div>
</div>
<div className={"w-2"}></div>