diff --git a/frontend/src/components/image.tsx b/frontend/src/components/image.tsx new file mode 100644 index 0000000..dabeda3 --- /dev/null +++ b/frontend/src/components/image.tsx @@ -0,0 +1,53 @@ +import { Image } from "../network/models.ts"; +import { network } from "../network/network.ts"; + +export function SquareImage({ image }: { image: Image }) { + let cover = false; + const imgAspectRatio = image.width / image.height; + if (imgAspectRatio > 0.8 && imgAspectRatio < 1.2) { + cover = true; + } + + return ( + <> +
{ + const dialog = document.getElementById( + `image-dialog-${image.id}`, + ) as HTMLDialogElement; + dialog.showModal(); + }} + > + {"image"} +
+ +
{ + const dialog = document.getElementById( + `image-dialog-${image.id}`, + ) as HTMLDialogElement; + dialog.close(); + }} + > + {"image"} +
+
+ + ); +} diff --git a/frontend/src/components/toast.ts b/frontend/src/components/toast.ts index e9498dd..5a9b47e 100644 --- a/frontend/src/components/toast.ts +++ b/frontend/src/components/toast.ts @@ -1,9 +1,11 @@ export default function showToast({ message, type, + parent, }: { message: string; type?: "success" | "error" | "warning" | "info"; + parent?: HTMLElement | null; }) { type = type || "info"; const div = document.createElement("div"); @@ -13,7 +15,11 @@ export default function showToast({ ${message} `; - document.body.appendChild(div); + if (parent) { + parent.appendChild(div); + } else { + document.body.appendChild(div); + } setTimeout(() => { div.remove(); }, 3000); diff --git a/frontend/src/network/models.ts b/frontend/src/network/models.ts index eea7ecb..c7d6d38 100644 --- a/frontend/src/network/models.ts +++ b/frontend/src/network/models.ts @@ -111,6 +111,7 @@ export interface Comment { content: string; created_at: string; user: User; + images: Image[]; } export interface CommentWithResource { diff --git a/frontend/src/network/network.ts b/frontend/src/network/network.ts index b60ee8c..088aaa9 100644 --- a/frontend/src/network/network.ts +++ b/frontend/src/network/network.ts @@ -925,11 +925,12 @@ class Network { async createComment( resourceID: number, content: string, + images: number[], ): Promise> { try { - const response = await axios.postForm( + const response = await axios.post( `${this.apiBaseUrl}/comments/${resourceID}`, - { content }, + { content, images }, ); return response.data; } catch (e: any) { @@ -941,11 +942,12 @@ class Network { async updateComment( commentID: number, content: string, + images: number[], ): Promise> { try { - const response = await axios.putForm( + const response = await axios.put( `${this.apiBaseUrl}/comments/${commentID}`, - { content }, + { content, images }, ); return response.data; } catch (e: any) { diff --git a/frontend/src/pages/resource_details_page.tsx b/frontend/src/pages/resource_details_page.tsx index 62b004d..89adfdb 100644 --- a/frontend/src/pages/resource_details_page.tsx +++ b/frontend/src/pages/resource_details_page.tsx @@ -26,12 +26,14 @@ import { MdAdd, MdArrowDownward, MdArrowUpward, + MdClose, MdOutlineArticle, MdOutlineComment, MdOutlineDataset, MdOutlineDelete, MdOutlineDownload, MdOutlineEdit, + MdOutlineImage, MdOutlineOpenInNew, } from "react-icons/md"; import { app } from "../app.ts"; @@ -45,6 +47,7 @@ import Button from "../components/button.tsx"; import Badge, { BadgeAccent } from "../components/badge.tsx"; import Input, { TextArea } from "../components/input.tsx"; import { useAppContext } from "../components/AppContext.tsx"; +import { SquareImage } from "../components/image.tsx"; export default function ResourcePage() { const params = useParams(); @@ -1131,23 +1134,45 @@ function UpdateFileInfoDialog({ file }: { file: RFile }) { 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); }, []); + return ( +
+ + + {maxPage ? ( +
+ +
+ ) : null} +
+ ); +} + +function CommentInput({ + resourceId, + reload, +}: { + resourceId: number; + reload: () => void; +}) { + const [commentContent, setCommentContent] = useState(""); + const [isLoading, setLoading] = useState(false); + const [images, setImages] = useState([]); + const { t } = useTranslation(); + const sendComment = async () => { if (isLoading) { return; @@ -1160,9 +1185,25 @@ function Comments({ resourceId }: { resourceId: number }) { return; } setLoading(true); - const res = await network.createComment(resourceId, commentContent); + const imageIds: number[] = []; + for (const image of images) { + const res = await network.uploadImage(image); + if (res.success) { + imageIds.push(res.data!); + } else { + showToast({ message: res.message, type: "error" }); + setLoading(false); + return; + } + } + const res = await network.createComment( + resourceId, + commentContent, + imageIds, + ); if (res.success) { setCommentContent(""); + setImages([]); showToast({ message: t("Comment created successfully"), type: "success", @@ -1174,45 +1215,89 @@ function Comments({ resourceId }: { resourceId: number }) { setLoading(false); }; - return ( -
- {app.isLoggedIn() ? ( -
-