Render internal links to card.

This commit is contained in:
nyne
2025-05-23 19:30:46 +08:00
parent 0bc97b1db5
commit 8a7af82b6c
5 changed files with 159 additions and 96 deletions

View File

@@ -67,6 +67,7 @@ export interface ResourceDetails {
author: User; author: User;
views: number; views: number;
downloads: number; downloads: number;
related: Resource[];
} }
export interface Storage { export interface Storage {

View File

@@ -27,10 +27,6 @@ class Network {
} }
init() { init() {
if (import.meta.env.MODE === 'development') {
this.baseUrl = 'http://localhost:3000';
this.apiBaseUrl = 'http://localhost:3000/api';
}
axios.defaults.validateStatus = _ => true axios.defaults.validateStatus = _ => true
axios.interceptors.request.use((config) => { axios.interceptors.request.use((config) => {
if (app.token) { if (app.token) {

View File

@@ -1,7 +1,7 @@
import {useNavigate, useParams} from "react-router"; import { useNavigate, useParams } from "react-router";
import {createContext, createRef, useCallback, useContext, useEffect, useRef, useState} from "react"; import { createContext, createRef, useCallback, useContext, useEffect, useRef, useState } from "react";
import {ResourceDetails, RFile, Storage, Comment} from "../network/models.ts"; import { ResourceDetails, RFile, Storage, Comment } from "../network/models.ts";
import {network} from "../network/network.ts"; import { network } from "../network/network.ts";
import showToast from "../components/toast.ts"; import showToast from "../components/toast.ts";
import Markdown from "react-markdown"; import Markdown from "react-markdown";
import "../markdown.css"; import "../markdown.css";
@@ -14,20 +14,20 @@ import {
MdOutlineDelete, MdOutlineDelete,
MdOutlineDownload, MdOutlineEdit MdOutlineDownload, MdOutlineEdit
} from "react-icons/md"; } from "react-icons/md";
import {app} from "../app.ts"; import { app } from "../app.ts";
import {uploadingManager} from "../network/uploading.ts"; import { uploadingManager } from "../network/uploading.ts";
import {ErrorAlert} from "../components/alert.tsx"; import { ErrorAlert } from "../components/alert.tsx";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import Pagination from "../components/pagination.tsx"; import Pagination from "../components/pagination.tsx";
import showPopup, {useClosePopup} from "../components/popup.tsx"; import showPopup, { useClosePopup } from "../components/popup.tsx";
import {Turnstile} from "@marsidev/react-turnstile"; import { Turnstile } from "@marsidev/react-turnstile";
import Button from "../components/button.tsx"; 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"; import Input from "../components/input.tsx";
export default function ResourcePage() { export default function ResourcePage() {
const params = useParams() const params = useParams()
const {t} = useTranslation(); const { t } = useTranslation();
const idStr = params.id const idStr = params.id
@@ -44,7 +44,7 @@ export default function ResourcePage() {
if (res.success) { if (res.success) {
setResource(res.data!) setResource(res.data!)
} else { } else {
showToast({message: res.message, type: "error"}) showToast({ message: res.message, type: "error" })
} }
} }
}, [id]) }, [id])
@@ -60,7 +60,7 @@ export default function ResourcePage() {
setResource(res.data!) setResource(res.data!)
document.title = res.data!.title document.title = res.data!.title
} else { } else {
showToast({message: res.message, type: "error"}) showToast({ message: res.message, type: "error" })
} }
}) })
} }
@@ -77,7 +77,7 @@ export default function ResourcePage() {
} }
if (!resource) { if (!resource) {
return <Loading/> return <Loading />
} }
return <context.Provider value={reload}> return <context.Provider value={reload}>
@@ -96,7 +96,7 @@ export default function ResourcePage() {
<div className="flex items-center "> <div className="flex items-center ">
<div className="avatar"> <div className="avatar">
<div className="w-6 rounded-full"> <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> </div>
<div className="w-2"></div> <div className="w-2"></div>
@@ -116,63 +116,63 @@ export default function ResourcePage() {
<label className="tab transition-all"> <label className="tab transition-all">
<input type="radio" name="my_tabs" checked={page === 0} onChange={() => { <input type="radio" name="my_tabs" checked={page === 0} onChange={() => {
setPage(0) setPage(0)
}}/> }} />
<MdOutlineArticle className="text-xl mr-2"/> <MdOutlineArticle className="text-xl mr-2" />
<span className="text-sm"> <span className="text-sm">
{t("Description")} {t("Description")}
</span> </span>
</label> </label>
<div key={"article"} className="tab-content p-2"> <div key={"article"} className="tab-content p-2">
<Article article={resource.article}/> <Article resource={resource} />
</div> </div>
<label className="tab transition-all"> <label className="tab transition-all">
<input type="radio" name="my_tabs" checked={page === 1} onChange={() => { <input type="radio" name="my_tabs" checked={page === 1} onChange={() => {
setPage(1) setPage(1)
}}/> }} />
<MdOutlineDataset className="text-xl mr-2"/> <MdOutlineDataset className="text-xl mr-2" />
<span className="text-sm"> <span className="text-sm">
{t("Files")} {t("Files")}
</span> </span>
</label> </label>
<div key={"files"} className="tab-content p-2"> <div key={"files"} className="tab-content p-2">
<Files files={resource.files} resourceID={resource.id}/> <Files files={resource.files} resourceID={resource.id} />
</div> </div>
<label className="tab transition-all"> <label className="tab transition-all">
<input type="radio" name="my_tabs" checked={page === 2} onChange={() => { <input type="radio" name="my_tabs" checked={page === 2} onChange={() => {
setPage(2) setPage(2)
}}/> }} />
<MdOutlineComment className="text-xl mr-2"/> <MdOutlineComment className="text-xl mr-2" />
<span className="text-sm"> <span className="text-sm">
{t("Comments")} {t("Comments")}
</span> </span>
</label> </label>
<div key={"comments"} className="tab-content p-2"> <div key={"comments"} className="tab-content p-2">
<Comments resourceId={resource.id}/> <Comments resourceId={resource.id} />
</div> </div>
<div className={"grow"}></div> <div className={"grow"}></div>
{ {
app.isAdmin() || app.user?.id === resource.author.id ? <Button className={"btn-ghost btn-circle"} onClick={() => { app.isAdmin() || app.user?.id === resource.author.id ? <Button className={"btn-ghost btn-circle"} onClick={() => {
navigate(`/resource/edit/${resource.id}`, {replace: true}) navigate(`/resource/edit/${resource.id}`, { replace: true })
}}> }}>
<MdOutlineEdit size={20}/> <MdOutlineEdit size={20} />
</Button> : null </Button> : null
} }
<DeleteResourceDialog resourceId={resource.id} uploaderId={resource.author.id}/> <DeleteResourceDialog resourceId={resource.id} uploaderId={resource.author.id} />
</div> </div>
<div className="h-4"></div> <div className="h-4"></div>
</div> </div>
</context.Provider> </context.Provider>
} }
function DeleteResourceDialog({resourceId, uploaderId}: { resourceId: number, uploaderId?: number }) { function DeleteResourceDialog({ resourceId, uploaderId }: { resourceId: number, uploaderId?: number }) {
const [isLoading, setLoading] = useState(false) const [isLoading, setLoading] = useState(false)
const navigate = useNavigate() const navigate = useNavigate()
const {t} = useTranslation() const { t } = useTranslation()
const handleDelete = async () => { const handleDelete = async () => {
if (isLoading) { if (isLoading) {
@@ -183,10 +183,10 @@ function DeleteResourceDialog({resourceId, uploaderId}: { resourceId: number, up
const dialog = document.getElementById("delete_resource_dialog") as HTMLDialogElement const dialog = document.getElementById("delete_resource_dialog") as HTMLDialogElement
dialog.close() dialog.close()
if (res.success) { if (res.success) {
showToast({message: t("Resource deleted successfully"), type: "success"}) showToast({ message: t("Resource deleted successfully"), type: "success" })
navigate("/", {replace: true}) navigate("/", { replace: true })
} else { } else {
showToast({message: res.message, type: "error"}) showToast({ message: res.message, type: "error" })
} }
setLoading(false) setLoading(false)
} }
@@ -200,7 +200,7 @@ function DeleteResourceDialog({resourceId, uploaderId}: { resourceId: number, up
const dialog = document.getElementById("delete_resource_dialog") as HTMLDialogElement const dialog = document.getElementById("delete_resource_dialog") as HTMLDialogElement
dialog.showModal() dialog.showModal()
}}> }}>
<MdOutlineDelete size={20} className={"inline-block"}/> <MdOutlineDelete size={20} className={"inline-block"} />
</Button> </Button>
<dialog id={`delete_resource_dialog`} className="modal"> <dialog id={`delete_resource_dialog`} className="modal">
<div className="modal-box"> <div className="modal-box">
@@ -221,9 +221,67 @@ function DeleteResourceDialog({resourceId, uploaderId}: { resourceId: number, up
const context = createContext<() => void>(() => { const context = createContext<() => void>(() => {
}) })
function Article({article}: { article: string }) { function Article({ resource }: { resource: ResourceDetails }) {
return <article> const articleRef = useRef<HTMLDivElement>(null)
<Markdown>{article}</Markdown>
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}
<div class="card card-border max-w-72 sm:max-w-full border-base-300 my-2 sm:card-side">
${r.image ? `
<figure>
<img
class="w-full h-40 sm:h-full sm:w-32 object-cover"
src="${network.getImageUrl(r.image!.id)}"
alt="Cover" />
</figure>
` : ""}
<div class="card-body" style="padding: 1rem">
<h3>${r.title}</h4>
<p class="text-sm">${content}</p>
</div>
</div>
`
child.appendChild(div)
}
(child as HTMLParagraphElement).onclick = (e) => {
e.stopPropagation()
e.preventDefault()
navigate(`/resources/${r.id}`)
}
}
}
}
}
}
}
}, [resource])
return <article ref={articleRef}>
<Markdown>{resource.article}</Markdown>
</article> </article>
} }
@@ -239,10 +297,10 @@ function fileSizeToString(size: number) {
} }
} }
function FileTile({file}: { file: RFile }) { function FileTile({ file }: { file: RFile }) {
const buttonRef = createRef<HTMLButtonElement>() const buttonRef = createRef<HTMLButtonElement>()
const {t} = useTranslation() const { t } = useTranslation()
return <div className={"card card-border border-base-300 my-2"}> return <div className={"card card-border border-base-300 my-2"}>
<div className={"p-4 flex flex-row items-center"}> <div className={"p-4 flex flex-row items-center"}>
@@ -259,19 +317,19 @@ function FileTile({file}: { file: RFile }) {
const link = network.getFileDownloadLink(file.id, ""); const link = network.getFileDownloadLink(file.id, "");
window.open(link, "_blank"); window.open(link, "_blank");
} else { } else {
showPopup(<CloudflarePopup file={file}/>, buttonRef.current!) showPopup(<CloudflarePopup file={file} />, buttonRef.current!)
} }
}}> }}>
<MdOutlineDownload size={24}/> <MdOutlineDownload size={24} />
</button> </button>
<DeleteFileDialog fileId={file.id} uploaderId={file.user_id}/> <DeleteFileDialog fileId={file.id} uploaderId={file.user_id} />
<UpdateFileInfoDialog file={file}/> <UpdateFileInfoDialog file={file} />
</div> </div>
</div> </div>
</div> </div>
} }
function CloudflarePopup({file}: { file: RFile }) { function CloudflarePopup({ file }: { file: RFile }) {
const closePopup = useClosePopup() const closePopup = useClosePopup()
const [isLoading, setLoading] = useState(true) const [isLoading, setLoading] = useState(true)
@@ -292,7 +350,7 @@ function CloudflarePopup({file}: { file: RFile }) {
</div> </div>
} }
function Files({files, resourceID}: { files: RFile[], resourceID: number }) { function Files({ files, resourceID }: { files: RFile[], resourceID: number }) {
return <div> return <div>
{ {
files.map((file) => { files.map((file) => {
@@ -313,8 +371,8 @@ enum FileType {
upload = "upload", upload = "upload",
} }
function CreateFileDialog({resourceId}: { resourceId: number }) { function CreateFileDialog({ resourceId }: { resourceId: number }) {
const {t} = useTranslation(); const { t } = useTranslation();
const [isLoading, setLoading] = useState(false) const [isLoading, setLoading] = useState(false)
const storages = useRef<Storage[] | null>(null) const storages = useRef<Storage[] | null>(null)
const mounted = useRef(true) const mounted = useRef(true)
@@ -360,7 +418,7 @@ function CreateFileDialog({resourceId}: { resourceId: number }) {
setSubmitting(false) setSubmitting(false)
const dialog = document.getElementById("upload_dialog") as HTMLDialogElement const dialog = document.getElementById("upload_dialog") as HTMLDialogElement
dialog.close() dialog.close()
showToast({message: t("File created successfully"), type: "success"}) showToast({ message: t("File created successfully"), type: "success" })
reload() reload()
} else { } else {
setError(res.message) setError(res.message)
@@ -381,7 +439,7 @@ function CreateFileDialog({resourceId}: { resourceId: number }) {
setSubmitting(false) setSubmitting(false)
const dialog = document.getElementById("upload_dialog") as HTMLDialogElement const dialog = document.getElementById("upload_dialog") as HTMLDialogElement
dialog.close() dialog.close()
showToast({message: t("Successfully create uploading task."), type: "success"}) showToast({ message: t("Successfully create uploading task."), type: "success" })
} else { } else {
setError(res.message) setError(res.message)
setSubmitting(false) setSubmitting(false)
@@ -401,7 +459,7 @@ function CreateFileDialog({resourceId}: { resourceId: number }) {
return; return;
} }
if (!res.success) { if (!res.success) {
showToast({message: res.message, type: "error"}) showToast({ message: res.message, type: "error" })
} else { } else {
storages.current = res.data! storages.current = res.data!
setLoading(false) setLoading(false)
@@ -415,7 +473,7 @@ function CreateFileDialog({resourceId}: { resourceId: number }) {
dialog.showModal() 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"}> <span className={"text-sm"}>
{t("Upload")} {t("Upload")}
@@ -429,13 +487,13 @@ function CreateFileDialog({resourceId}: { resourceId: number }) {
<form className="filter mb-2"> <form className="filter mb-2">
<input className="btn btn-square" type="reset" value="×" onClick={() => { <input className="btn btn-square" type="reset" value="×" onClick={() => {
setFileType(null); setFileType(null);
}}/> }} />
<input className="btn text-sm" type="radio" name="type" aria-label={t("Redirect")} onInput={() => { <input className="btn text-sm" type="radio" name="type" aria-label={t("Redirect")} onInput={() => {
setFileType(FileType.redirect); setFileType(FileType.redirect);
}}/> }} />
<input className="btn text-sm" type="radio" name="type" aria-label={t("Upload")} onInput={() => { <input className="btn text-sm" type="radio" name="type" aria-label={t("Upload")} onInput={() => {
setFileType(FileType.upload); setFileType(FileType.upload);
}}/> }} />
</form> </form>
{ {
@@ -443,13 +501,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> <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) => { <input type="text" className="input w-full my-2" placeholder={t("File Name")} onChange={(e) => {
setFilename(e.target.value) setFilename(e.target.value)
}}/> }} />
<input type="text" className="input w-full my-2" placeholder={t("URL")} onChange={(e) => { <input type="text" className="input w-full my-2" placeholder={t("URL")} onChange={(e) => {
setRedirectUrl(e.target.value) setRedirectUrl(e.target.value)
}}/> }} />
<input type="text" className="input w-full my-2" placeholder={t("Description")} onChange={(e) => { <input type="text" className="input w-full my-2" placeholder={t("Description")} onChange={(e) => {
setDescription(e.target.value) setDescription(e.target.value)
}}/> }} />
</> </>
} }
@@ -482,15 +540,15 @@ function CreateFileDialog({resourceId}: { resourceId: number }) {
if (e.target.files) { if (e.target.files) {
setFile(e.target.files[0]) setFile(e.target.files[0])
} }
}}/> }} />
<input type="text" className="input w-full my-2" placeholder={t("Description")} onChange={(e) => { <input type="text" className="input w-full my-2" placeholder={t("Description")} onChange={(e) => {
setDescription(e.target.value) setDescription(e.target.value)
}}/> }} />
</> </>
} }
{error && <ErrorAlert className={"my-2"} message={error}/>} {error && <ErrorAlert className={"my-2"} message={error} />}
<div className="modal-action"> <div className="modal-action">
<form method="dialog"> <form method="dialog">
@@ -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 [isLoading, setLoading] = useState(false)
const [filename, setFilename] = useState(file.filename) const [filename, setFilename] = useState(file.filename)
const [description, setDescription] = useState(file.description) const [description, setDescription] = useState(file.description)
const {t} = useTranslation() const { t } = useTranslation()
const reload = useContext(context) const reload = useContext(context)
@@ -525,11 +583,11 @@ function UpdateFileInfoDialog({file}: { file: RFile }) {
const res = await network.updateFile(file.id, filename, description); const res = await network.updateFile(file.id, filename, description);
const dialog = document.getElementById(`update_file_info_dialog_${file.id}`) as HTMLDialogElement const dialog = document.getElementById(`update_file_info_dialog_${file.id}`) as HTMLDialogElement
dialog.close() dialog.close()
if (res.success){ if (res.success) {
showToast({message: t("File info updated successfully"), type: "success"}) showToast({ message: t("File info updated successfully"), type: "success" })
reload() reload()
} else { } else {
showToast({message: res.message, type: "error"}) showToast({ message: res.message, type: "error" })
} }
setLoading(false) setLoading(false)
} }
@@ -543,13 +601,13 @@ function UpdateFileInfoDialog({file}: { file: RFile }) {
const dialog = document.getElementById(`update_file_info_dialog_${file.id}`) as HTMLDialogElement const dialog = document.getElementById(`update_file_info_dialog_${file.id}`) as HTMLDialogElement
dialog.showModal() dialog.showModal()
}}> }}>
<MdOutlineEdit size={20} className={"inline-block"}/> <MdOutlineEdit size={20} className={"inline-block"} />
</button> </button>
<dialog id={`update_file_info_dialog_${file.id}`} className="modal"> <dialog id={`update_file_info_dialog_${file.id}`} className="modal">
<div className="modal-box"> <div className="modal-box">
<h3 className="font-bold text-lg">{t("Update File Info")}</h3> <h3 className="font-bold text-lg">{t("Update File Info")}</h3>
<Input type={"text"} label={t("File Name")} value={filename} onChange={(e) => setFilename(e.target.value)}/> <Input type={"text"} label={t("File Name")} value={filename} onChange={(e) => setFilename(e.target.value)} />
<Input type={"text"} label={t("Description")} value={description} onChange={(e) => setDescription(e.target.value)}/> <Input type={"text"} label={t("Description")} value={description} onChange={(e) => setDescription(e.target.value)} />
<div className="modal-action"> <div className="modal-action">
<form method="dialog"> <form method="dialog">
<button className="btn btn-ghost">{t("Close")}</button> <button className="btn btn-ghost">{t("Close")}</button>
@@ -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 [page, setPage] = useState(1);
const [maxPage, setMaxPage] = useState(0); const [maxPage, setMaxPage] = useState(0);
@@ -572,7 +630,7 @@ function Comments({resourceId}: { resourceId: number }) {
const [isLoading, setLoading] = useState(false); const [isLoading, setLoading] = useState(false);
const {t} = useTranslation(); const { t } = useTranslation();
const reload = useCallback(() => { const reload = useCallback(() => {
setPage(1); setPage(1);
@@ -585,17 +643,17 @@ function Comments({resourceId}: { resourceId: number }) {
return; return;
} }
if (commentContent === "") { if (commentContent === "") {
showToast({message: t("Comment content cannot be empty"), type: "error"}); showToast({ message: t("Comment content cannot be empty"), type: "error" });
return; return;
} }
setLoading(true); setLoading(true);
const res = await network.createComment(resourceId, commentContent); const res = await network.createComment(resourceId, commentContent);
if (res.success) { if (res.success) {
setCommentContent(""); setCommentContent("");
showToast({message: t("Comment created successfully"), type: "success"}); showToast({ message: t("Comment created successfully"), type: "success" });
reload(); reload();
} else { } else {
showToast({message: res.message, type: "error"}); showToast({ message: res.message, type: "error" });
} }
setLoading(false); setLoading(false);
} }
@@ -603,7 +661,7 @@ function Comments({resourceId}: { resourceId: number }) {
return <div> return <div>
<div className={"mt-4 mb-6 textarea w-full p-4 h-28 flex flex-col"}> <div className={"mt-4 mb-6 textarea w-full p-4 h-28 flex flex-col"}>
<textarea placeholder={t("Write down your comment")} className={"w-full resize-none grow"} value={commentContent} <textarea placeholder={t("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"}> <div className={"flex flex-row-reverse"}>
<button onClick={sendComment} <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"}`}>
@@ -612,14 +670,14 @@ function Comments({resourceId}: { resourceId: number }) {
</button> </button>
</div> </div>
</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"}> {maxPage && <div className={"w-full flex justify-center"}>
<Pagination page={page} setPage={setPage} totalPages={maxPage}/> <Pagination page={page} setPage={setPage} totalPages={maxPage} />
</div>} </div>}
</div> </div>
} }
function CommentsList({resourceId, page, maxPageCallback}: { function CommentsList({ resourceId, page, maxPageCallback }: {
resourceId: number, resourceId: number,
page: number, page: number,
maxPageCallback: (maxPage: number) => void maxPageCallback: (maxPage: number) => void
@@ -642,26 +700,26 @@ function CommentsList({resourceId, page, maxPageCallback}: {
if (comments == null) { if (comments == null) {
return <div className={"w-full"}> return <div className={"w-full"}>
<Loading/> <Loading />
</div> </div>
} }
return <> return <>
{ {
comments.map((comment) => { 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 }) {
const navigate = useNavigate(); const navigate = useNavigate();
return <div className={"card card-border border-base-300 p-2 my-3"}> 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={"flex flex-row items-center my-1 mx-1"}>
<div className="avatar cursor-pointer" onClick={() => navigate(`/user/${comment.user.username}`)}> <div className="avatar cursor-pointer" onClick={() => navigate(`/user/${comment.user.username}`)}>
<div className="w-8 rounded-full"> <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> </div>
<div className={"w-2"}></div> <div className={"w-2"}></div>
@@ -680,14 +738,14 @@ function CommentTile({comment}: { comment: Comment }) {
</div> </div>
} }
function DeleteFileDialog({fileId, uploaderId}: { fileId: string, uploaderId: number }) { function DeleteFileDialog({ fileId, uploaderId }: { fileId: string, uploaderId: number }) {
const [isLoading, setLoading] = useState(false) const [isLoading, setLoading] = useState(false)
const id = `delete_file_dialog_${fileId}` const id = `delete_file_dialog_${fileId}`
const reload = useContext(context) const reload = useContext(context)
const {t} = useTranslation(); const { t } = useTranslation();
const handleDelete = async () => { const handleDelete = async () => {
if (isLoading) { if (isLoading) {
@@ -698,10 +756,10 @@ function DeleteFileDialog({fileId, uploaderId}: { fileId: string, uploaderId: nu
const dialog = document.getElementById(id) as HTMLDialogElement const dialog = document.getElementById(id) as HTMLDialogElement
dialog.close() dialog.close()
if (res.success) { if (res.success) {
showToast({message: t("File deleted successfully"), type: "success"}) showToast({ message: t("File deleted successfully"), type: "success" })
reload() reload()
} else { } else {
showToast({message: res.message, type: "error"}) showToast({ message: res.message, type: "error" })
} }
setLoading(false) setLoading(false)
} }
@@ -715,7 +773,7 @@ function DeleteFileDialog({fileId, uploaderId}: { fileId: string, uploaderId: nu
const dialog = document.getElementById(id) as HTMLDialogElement const dialog = document.getElementById(id) as HTMLDialogElement
dialog.showModal() dialog.showModal()
}}> }}>
<MdOutlineDelete size={20} className={"inline-block"}/> <MdOutlineDelete size={20} className={"inline-block"} />
</button> </button>
<dialog id={id} className="modal"> <dialog id={id} className="modal">
<div className="modal-box"> <div className="modal-box">

View File

@@ -5,4 +5,12 @@ import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react(), tailwindcss(),], plugins: [react(), tailwindcss(),],
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
},
},
},
}) })

View File

@@ -91,7 +91,7 @@ func parseResourceIfPresent(line string, host string) *model.ResourceView {
if err != nil { if err != nil {
return nil return nil
} }
if parsed.Hostname() != host { if parsed.IsAbs() && parsed.Hostname() != host {
return nil return nil
} }
path := parsed.Path path := parsed.Path