From 8a7af82b6c502482edb7b15525e2dba954b4eba1 Mon Sep 17 00:00:00 2001 From: nyne Date: Fri, 23 May 2025 19:30:46 +0800 Subject: [PATCH] Render internal links to card. --- frontend/src/network/models.ts | 1 + frontend/src/network/network.ts | 4 - frontend/src/pages/resource_details_page.tsx | 240 ++++++++++++------- frontend/vite.config.ts | 8 + server/service/resource.go | 2 +- 5 files changed, 159 insertions(+), 96 deletions(-) diff --git a/frontend/src/network/models.ts b/frontend/src/network/models.ts index f0ec6d2..ea26592 100644 --- a/frontend/src/network/models.ts +++ b/frontend/src/network/models.ts @@ -67,6 +67,7 @@ export interface ResourceDetails { author: User; views: number; downloads: number; + related: Resource[]; } export interface Storage { diff --git a/frontend/src/network/network.ts b/frontend/src/network/network.ts index 7cb6b39..430b419 100644 --- a/frontend/src/network/network.ts +++ b/frontend/src/network/network.ts @@ -27,10 +27,6 @@ class Network { } init() { - if (import.meta.env.MODE === 'development') { - this.baseUrl = 'http://localhost:3000'; - this.apiBaseUrl = 'http://localhost:3000/api'; - } axios.defaults.validateStatus = _ => true axios.interceptors.request.use((config) => { if (app.token) { diff --git a/frontend/src/pages/resource_details_page.tsx b/frontend/src/pages/resource_details_page.tsx index 1db86e6..acaa1bd 100644 --- a/frontend/src/pages/resource_details_page.tsx +++ b/frontend/src/pages/resource_details_page.tsx @@ -1,7 +1,7 @@ -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 { 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"; @@ -14,20 +14,20 @@ import { 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 { 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 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 Badge, { BadgeAccent } from "../components/badge.tsx"; import Input from "../components/input.tsx"; export default function ResourcePage() { const params = useParams() - const {t} = useTranslation(); + const { t } = useTranslation(); const idStr = params.id @@ -44,7 +44,7 @@ export default function ResourcePage() { if (res.success) { setResource(res.data!) } else { - showToast({message: res.message, type: "error"}) + showToast({ message: res.message, type: "error" }) } } }, [id]) @@ -60,7 +60,7 @@ export default function ResourcePage() { setResource(res.data!) document.title = res.data!.title } else { - showToast({message: res.message, type: "error"}) + showToast({ message: res.message, type: "error" }) } }) } @@ -77,7 +77,7 @@ export default function ResourcePage() { } if (!resource) { - return + return } return @@ -96,7 +96,7 @@ export default function ResourcePage() {
- {"avatar"}/ + {"avatar"}
@@ -116,63 +116,63 @@ export default function ResourcePage() {
-
+
- +
- +
{ app.isAdmin() || app.user?.id === resource.author.id ? : null } - +
} -function DeleteResourceDialog({resourceId, uploaderId}: { resourceId: number, uploaderId?: number }) { +function DeleteResourceDialog({ resourceId, uploaderId }: { resourceId: number, uploaderId?: number }) { const [isLoading, setLoading] = useState(false) const navigate = useNavigate() - const {t} = useTranslation() + const { t } = useTranslation() const handleDelete = async () => { if (isLoading) { @@ -183,10 +183,10 @@ function DeleteResourceDialog({resourceId, uploaderId}: { resourceId: number, up 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}) + showToast({ message: t("Resource deleted successfully"), type: "success" }) + navigate("/", { replace: true }) } else { - showToast({message: res.message, type: "error"}) + showToast({ message: res.message, type: "error" }) } setLoading(false) } @@ -200,7 +200,7 @@ function DeleteResourceDialog({resourceId, uploaderId}: { resourceId: number, up const dialog = document.getElementById("delete_resource_dialog") as HTMLDialogElement dialog.showModal() }}> - +
@@ -221,9 +221,67 @@ function DeleteResourceDialog({resourceId, uploaderId}: { resourceId: number, up const context = createContext<() => void>(() => { }) -function Article({article}: { article: string }) { - return
- {article} +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}`) + } + } + } + } + } + } + } + }, [resource]) + + return
+ {resource.article}
} @@ -239,10 +297,10 @@ function fileSizeToString(size: number) { } } -function FileTile({file}: { file: RFile }) { +function FileTile({ file }: { file: RFile }) { const buttonRef = createRef() - const {t} = useTranslation() + const { t } = useTranslation() return
@@ -259,19 +317,19 @@ function FileTile({file}: { file: RFile }) { const link = network.getFileDownloadLink(file.id, ""); window.open(link, "_blank"); } else { - showPopup(, buttonRef.current!) + showPopup(, buttonRef.current!) } }}> - + - - + +
} -function CloudflarePopup({file}: { file: RFile }) { +function CloudflarePopup({ file }: { file: RFile }) { const closePopup = useClosePopup() const [isLoading, setLoading] = useState(true) @@ -292,7 +350,7 @@ function CloudflarePopup({file}: { file: RFile }) { } -function Files({files, resourceID}: { files: RFile[], resourceID: number }) { +function Files({ files, resourceID }: { files: RFile[], resourceID: number }) { return
{ files.map((file) => { @@ -313,8 +371,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(null) const mounted = useRef(true) @@ -360,7 +418,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) @@ -381,7 +439,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) @@ -401,7 +459,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) @@ -415,7 +473,7 @@ function CreateFileDialog({resourceId}: { resourceId: number }) { dialog.showModal() }}> { - isLoading ? : + isLoading ? : } {t("Upload")} @@ -429,13 +487,13 @@ function CreateFileDialog({resourceId}: { resourceId: number }) {
{ setFileType(null); - }}/> + }} /> { setFileType(FileType.redirect); - }}/> + }} /> { setFileType(FileType.upload); - }}/> + }} />
{ @@ -443,13 +501,13 @@ function CreateFileDialog({resourceId}: { resourceId: number }) {

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

{ setFilename(e.target.value) - }}/> + }} /> { setRedirectUrl(e.target.value) - }}/> + }} /> { setDescription(e.target.value) - }}/> + }} /> } @@ -472,25 +530,25 @@ function CreateFileDialog({resourceId}: { resourceId: number }) { { storages.current?.map((s) => { return + value={s.id}>{s.name}({(s.currentSize / 1024 / 1024).toFixed(2)}/{s.maxSize / 1024 / 1024}MB) }) } { - if (e.target.files) { - setFile(e.target.files[0]) - } - }}/> + if (e.target.files) { + setFile(e.target.files[0]) + } + }} /> { setDescription(e.target.value) - }}/> + }} /> } - {error && } + {error && }
@@ -506,14 +564,14 @@ function CreateFileDialog({resourceId}: { resourceId: number }) { } -function UpdateFileInfoDialog({file}: { file: RFile }) { +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 { t } = useTranslation() const reload = useContext(context) @@ -525,11 +583,11 @@ function UpdateFileInfoDialog({file}: { file: RFile }) { 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"}) + if (res.success) { + showToast({ message: t("File info updated successfully"), type: "success" }) reload() } else { - showToast({message: res.message, type: "error"}) + showToast({ message: res.message, type: "error" }) } setLoading(false) } @@ -543,13 +601,13 @@ function UpdateFileInfoDialog({file}: { file: RFile }) { const dialog = document.getElementById(`update_file_info_dialog_${file.id}`) as HTMLDialogElement dialog.showModal() }}> - +

{t("Update File Info")}

- setFilename(e.target.value)}/> - setDescription(e.target.value)}/> + setFilename(e.target.value)} /> + setDescription(e.target.value)} />
@@ -561,7 +619,7 @@ function UpdateFileInfoDialog({file}: { file: RFile }) { } -function Comments({resourceId}: { resourceId: number }) { +function Comments({ resourceId }: { resourceId: number }) { const [page, setPage] = useState(1); const [maxPage, setMaxPage] = useState(0); @@ -572,7 +630,7 @@ function Comments({resourceId}: { resourceId: number }) { const [isLoading, setLoading] = useState(false); - const {t} = useTranslation(); + const { t } = useTranslation(); const reload = useCallback(() => { setPage(1); @@ -585,17 +643,17 @@ function Comments({resourceId}: { resourceId: number }) { return; } if (commentContent === "") { - showToast({message: t("Comment content cannot be empty"), type: "error"}); + 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"}); + showToast({ message: t("Comment created successfully"), type: "success" }); reload(); } else { - showToast({message: res.message, type: "error"}); + showToast({ message: res.message, type: "error" }); } setLoading(false); } @@ -603,23 +661,23 @@ function Comments({resourceId}: { resourceId: number }) { return