diff --git a/frontend/src/app.tsx b/frontend/src/app.tsx index c01f753..24e8bc0 100644 --- a/frontend/src/app.tsx +++ b/frontend/src/app.tsx @@ -9,6 +9,7 @@ import ResourcePage from "./pages/resource_details_page.tsx"; import ManagePage from "./pages/manage_page.tsx"; import TaggedResourcesPage from "./pages/tagged_resources_page.tsx"; import UserPage from "./pages/user_page.tsx"; +import EditResourcePage from "./pages/edit_resource_page.tsx"; export default function App() { return ( @@ -24,6 +25,7 @@ export default function App() { }/> }/> }/> + }/> diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index 99a19c4..4c008a4 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -147,6 +147,7 @@ export const i18nData = { "Searching...": "Searching...", "Create Tag": "Create Tag", "Search Tags": "Search Tags", + "Edit Resource": "Edit Resource", } }, "zh-CN": { @@ -297,6 +298,7 @@ export const i18nData = { "Searching...": "搜索中...", "Create Tag": "创建标签", "Search Tags": "搜索标签", + "Edit Resource": "编辑资源", } }, "zh-TW": { @@ -447,6 +449,7 @@ export const i18nData = { "Searching...": "搜尋中...", "Create Tag": "創建標籤", "Search Tags": "搜尋標籤", + "Edit Resource": "編輯資源", } } } diff --git a/frontend/src/network/network.ts b/frontend/src/network/network.ts index 8576426..8cebcf9 100644 --- a/frontend/src/network/network.ts +++ b/frontend/src/network/network.ts @@ -340,6 +340,19 @@ class Network { } } + async editResource(id: number, params: CreateResourceParams): Promise> { + try { + const response = await axios.post(`${this.apiBaseUrl}/resource/${id}`, params) + return response.data + } catch (e: any) { + console.error(e) + return { + success: false, + message: e.toString(), + } + } + } + async getResources(page: number): Promise> { try { const response = await axios.get(`${this.apiBaseUrl}/resource`, { diff --git a/frontend/src/pages/edit_resource_page.tsx b/frontend/src/pages/edit_resource_page.tsx new file mode 100644 index 0000000..fb2db5d --- /dev/null +++ b/frontend/src/pages/edit_resource_page.tsx @@ -0,0 +1,394 @@ +import { useEffect, useRef, useState } from "react"; +import {MdAdd, MdClose, MdDelete, MdOutlineInfo, MdSearch} from "react-icons/md"; +import { Tag } from "../network/models.ts"; +import { network } from "../network/network.ts"; +import { LuInfo } from "react-icons/lu"; +import {useNavigate, useParams} from "react-router"; +import showToast from "../components/toast.ts"; +import { useTranslation } from "react-i18next"; +import { app } from "../app.ts"; +import { ErrorAlert } from "../components/alert.tsx"; +import Loading from "../components/loading.tsx"; + +export default function EditResourcePage() { + const [title, setTitle] = useState("") + const [altTitles, setAltTitles] = useState([]) + const [tags, setTags] = useState([]) + const [article, setArticle] = useState("") + const [images, setImages] = useState([]) + const [isUploading, setUploading] = useState(false) + const [error, setError] = useState(null) + const [isSubmitting, setSubmitting] = useState(false) + const [isLoading, setLoading] = useState(true) + + const navigate = useNavigate() + const { t } = useTranslation(); + + useEffect(() => { + document.title = t("Edit Resource"); + }, [t]) + + const {rid} = useParams() + const id = parseInt(rid || "") + + useEffect(() => { + if (isNaN(id)) { + return + } + network.getResourceDetails(id).then((res) => { + if (res.success) { + const data = res.data! + setTitle(data.title) + setAltTitles(data.alternativeTitles) + setTags(data.tags) + setArticle(data.article) + setImages(data.images.map(i => i.id)) + setLoading(false) + } else { + showToast({ message: t("Failed to load resource"), type: "error" }) + } + }) + }, [id, t]); + + const handleSubmit = async () => { + if (isSubmitting) { + return + } + if (!title) { + setError(t("Title cannot be empty")) + return + } + for (let i = 0; i < altTitles.length; i++) { + if (!altTitles[i]) { + setError(t("Alternative title cannot be empty")) + return + } + } + if (!tags || tags.length === 0) { + setError(t("At least one tag required")) + return + } + if (!article) { + setError(t("Description cannot be empty")) + return + } + const res = await network.editResource(id, { + title: title, + alternative_titles: altTitles, + tags: tags.map((tag) => tag.id), + article: article, + images: images, + }) + if (res.success) { + setSubmitting(false) + navigate("/resources/" + id.toString(), { replace: true }) + } else { + setSubmitting(false) + setError(res.message) + } + } + + const addImage = () => { + if (isUploading) { + return + } + const input = document.createElement("input") + input.type = "file" + input.accept = "image/*" + input.onchange = async () => { + const files = input.files + if (!files || files.length === 0) { + return + } + const image = files[0] + setUploading(true) + const res = await network.uploadImage(image) + if (res.success) { + setUploading(false) + setImages([...images, res.data!]) + } else { + setUploading(false) + showToast({ message: t("Failed to upload image"), type: "error" }) + } + } + input.click() + } + + if (isNaN(id)) { + return + } + + if (!app.user) { + return + } + + if (isLoading) { + return + } + + return
+

{t("Edit Resource")}

+
+ + {t("All information can be modified after publishing")} +
+

{t("Title")}

+ setTitle(e.target.value)} /> +
+

{t("Alternative Titles")}

+ { + altTitles.map((title, index) => { + return
+ { + const newAltTitles = [...altTitles] + newAltTitles[index] = e.target.value + setAltTitles(newAltTitles) + }} /> + +
+ }) + } + +
+

{t("Tags")}

+

+ { + tags.map((tag, index) => { + return + {tag.name} + { + const newTags = [...tags] + newTags.splice(index, 1) + setTags(newTags) + }}> + + + + }) + } +

+ { + setTags([...tags, tag]) + }} /> +
+

{t("Description")}

+