diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index 7d390eb..df0b3b6 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -39,6 +39,8 @@ export const i18nData = { "Description cannot be empty": "Description cannot be empty", "Loading": "Loading", "Enter a search keyword to continue": "Enter a search keyword to continue", + "My Info": "My Info", + "Server": "Server", // Management page translations "Manage": "Manage", @@ -106,6 +108,40 @@ export const i18nData = { "URL": "URL", "Upload a file to server, then the file will be moved to the selected storage.": "Upload a file to server, then the file will be moved to the selected storage.", "Select Storage": "Select Storage", + "Resource Details": "Resource Details", + "Delete Resource": "Delete Resource", + "Are you sure you want to delete the resource": "Are you sure you want to delete the resource", + "Delete File": "Delete File", + "Are you sure you want to delete the file": "Are you sure you want to delete the file", + + // New translations + "Change Avatar": "Change Avatar", + "Change Username": "Change Username", + "Change Password": "Change Password", + "New Username": "New Username", + "Enter new username": "Enter new username", + "Save": "Save", + "Current Password": "Current Password", + "Enter current password": "Enter current password", + "New Password": "New Password", + "Enter new password": "Enter new password", + "Confirm New Password": "Confirm New Password", + "Confirm new password": "Confirm new password", + "Avatar changed successfully": "Avatar changed successfully", + "Username changed successfully": "Username changed successfully", + "Password changed successfully": "Password changed successfully", + + // Manage server config page translations + "Update server config successfully": "Update server config successfully", + "Max uploading size (MB)": "Max uploading size (MB)", + "Max file size (MB)": "Max file size (MB)", + "Max downloads per day for single IP": "Max downloads per day for single IP", + "Allow register": "Allow register", + "Server name": "Server name", + "Server description": "Server description", + "Cloudflare Turnstile Site Key": "Cloudflare Turnstile Site Key", + "Cloudflare Turnstile Secret Key": "Cloudflare Turnstile Secret Key", + "If the cloudflare turnstile keys are not empty, the turnstile will be used for register and download.": "If the cloudflare turnstile keys are not empty, the turnstile will be used for register and download.", } }, "zh-CN": { @@ -148,6 +184,8 @@ export const i18nData = { "Description cannot be empty": "介绍不能为空", "Loading": "加载中", "Enter a search keyword to continue": "输入搜索关键词以继续", + "My Info": "个人信息", + "Server": "服务器", // Management page translations "Manage": "管理", @@ -215,6 +253,40 @@ export const i18nData = { "URL": "URL", "Upload a file to server, then the file will be moved to the selected storage.": "将文件上传到服务器,然后文件将被移动到选定的存储中。", "Select Storage": "选择存储", + "Resource Details": "资源详情", + "Delete Resource": "删除资源", + "Are you sure you want to delete the resource": "您确定要删除此资源吗", + "Delete File": "删除文件", + "Are you sure you want to delete the file": "您确定要删除此文件吗", + + // New translations + "Change Avatar": "更改头像", + "Change Username": "更改用户名", + "Change Password": "更改密码", + "New Username": "新用户名", + "Enter new username": "输入新用户名", + "Save": "保存", + "Current Password": "当前密码", + "Enter current password": "输入当前密码", + "New Password": "新密码", + "Enter new password": "输入新密码", + "Confirm New Password": "确认新密码", + "Confirm new password": "确认新密码", + "Avatar changed successfully": "头像更改成功", + "Username changed successfully": "用户名更改成功", + "Password changed successfully": "密码更改成功", + + // Manage server config page translations + "Update server config successfully": "成功更新服务器配置", + "Max uploading size (MB)": "最大上传大小 (MB)", + "Max file size (MB)": "最大文件大小 (MB)", + "Max downloads per day for single IP": "单个IP每日最大下载次数", + "Allow register": "允许注册", + "Server name": "服务器名称", + "Server description": "服务器描述", + "Cloudflare Turnstile Site Key": "Cloudflare Turnstile 站点密钥", + "Cloudflare Turnstile Secret Key": "Cloudflare Turnstile 密钥", + "If the cloudflare turnstile keys are not empty, the turnstile will be used for register and download.": "如果设置了 Cloudflare Turnstile 密钥,将在注册和下载时启用验证", } }, "zh-TW": { @@ -257,6 +329,8 @@ export const i18nData = { "Description cannot be empty": "介紹不能為空", "Loading": "載入中", "Enter a search keyword to continue": "輸入搜尋關鍵字以繼續", + "My Info": "個人信息", + "Server": "伺服器", // Management page translations "Manage": "管理", @@ -324,6 +398,40 @@ export const i18nData = { "URL": "URL", "Upload a file to server, then the file will be moved to the selected storage.": "將檔案上傳到伺服器,然後檔案將被移動到選定的儲存中。", "Select Storage": "選擇儲存", + "Resource Details": "資源詳情", + "Delete Resource": "刪除資源", + "Are you sure you want to delete the resource": "您確定要刪除此資源嗎", + "Delete File": "刪除檔案", + "Are you sure you want to delete the file": "您確定要刪除此檔案嗎", + + // New translations + "Change Avatar": "更改頭像", + "Change Username": "更改用戶名", + "Change Password": "更改密碼", + "New Username": "新用戶名", + "Enter new username": "輸入新用戶名", + "Save": "儲存", + "Current Password": "當前密碼", + "Enter current password": "輸入當前密碼", + "New Password": "新密碼", + "Enter new password": "輸入新密碼", + "Confirm New Password": "確認新密碼", + "Confirm new password": "確認新密碼", + "Avatar changed successfully": "頭像更改成功", + "Username changed successfully": "用戶名更改成功", + "Password changed successfully": "密碼更改成功", + + // Manage server config page translations + "Update server config successfully": "成功更新伺服器配置", + "Max uploading size (MB)": "最大上傳大小 (MB)", + "Max file size (MB)": "最大檔案大小 (MB)", + "Max downloads per day for single IP": "單個IP每日最大下載次數", + "Allow register": "允許註冊", + "Server name": "伺服器名稱", + "Server description": "伺服器描述", + "Cloudflare Turnstile Site Key": "Cloudflare Turnstile 網站密鑰", + "Cloudflare Turnstile Secret Key": "Cloudflare Turnstile 密鑰", + "If the cloudflare turnstile keys are not empty, the turnstile will be used for register and download.": "如果設置了 Cloudflare Turnstile 密鑰,將在註冊和下載時啟用驗證", } } } diff --git a/frontend/src/network/network.ts b/frontend/src/network/network.ts index 0814946..7e9fe82 100644 --- a/frontend/src/network/network.ts +++ b/frontend/src/network/network.ts @@ -408,6 +408,19 @@ class Network { } } + async deleteResource(id: number): Promise> { + try { + const response = await axios.delete(`${this.apiBaseUrl}/resource/${id}`) + return response.data + } catch (e: any) { + console.error(e) + return { + success: false, + message: e.toString(), + } + } + } + async createS3Storage(name: string, endPoint: string, accessKeyID: string, secretAccessKey: string, bucketName: string, maxSizeInMB: number): Promise> { try { @@ -587,7 +600,7 @@ class Network { } } - async deleteFile(fileId: number): Promise> { + async deleteFile(fileId: string): Promise> { try { const response = await axios.delete(`${this.apiBaseUrl}/files/${fileId}`); return response.data; diff --git a/frontend/src/pages/manage_me_page.tsx b/frontend/src/pages/manage_me_page.tsx index d3e10a6..77db5a2 100644 --- a/frontend/src/pages/manage_me_page.tsx +++ b/frontend/src/pages/manage_me_page.tsx @@ -42,6 +42,8 @@ function ChangeAvatarDialog() { const navigator = useNavigator(); + const { t } = useTranslation(); + const selectAvatar = () => { const input = document.createElement("input"); input.type = "file"; @@ -67,7 +69,7 @@ function ChangeAvatarDialog() { app.user = res.data!; navigator.refresh(); showToast({ - message: "Avatar changed successfully", + message: t("Avatar changed successfully"), type: "success", }) const dialog = document.getElementById("change_avatar_dialog") as HTMLDialogElement; @@ -78,7 +80,7 @@ function ChangeAvatarDialog() { } return <> - } title="Change Avatar" onClick={() => { + } title={t("Change Avatar")} onClick={() => { const dialog = document.getElementById("change_avatar_dialog") as HTMLDialogElement; if (dialog) { dialog.showModal(); @@ -86,7 +88,7 @@ function ChangeAvatarDialog() { }} />
-

Change Avatar

+

{t("Change Avatar")}

@@ -97,9 +99,9 @@ function ChangeAvatarDialog() { {error && }
- +
- +
@@ -112,9 +114,11 @@ function ChangeUsernameDialog() { const [error, setError] = useState(null); const navigator = useNavigator(); + const { t } = useTranslation(); + const handleSubmit = async () => { if (!newUsername.trim()) { - setError("Username cannot be empty"); + setError(t("Username cannot be empty")); return; } setIsLoading(true); @@ -126,7 +130,7 @@ function ChangeUsernameDialog() { app.user = res.data!; navigator.refresh(); showToast({ - message: "Username changed successfully", + message: t("Username changed successfully"), type: "success", }); const dialog = document.getElementById("change_username_dialog") as HTMLDialogElement; @@ -139,7 +143,7 @@ function ChangeUsernameDialog() { }; return <> - } title="Change Username" onClick={() => { + } title={t("Change Username")} onClick={() => { const dialog = document.getElementById("change_username_dialog") as HTMLDialogElement; if (dialog) { dialog.showModal(); @@ -147,14 +151,14 @@ function ChangeUsernameDialog() { }} />
-

Change Username

+

{t("Change Username")}

setNewUsername(e.target.value)} /> @@ -162,7 +166,7 @@ function ChangeUsernameDialog() { {error && }
- +
@@ -185,20 +189,22 @@ function ChangePasswordDialog() { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); + const { t } = useTranslation(); + const handleSubmit = async () => { // Validate input if (!oldPassword || !newPassword || !confirmPassword) { - setError("All fields are required"); + setError(t("All fields are required")); return; } if (newPassword !== confirmPassword) { - setError("New passwords don't match"); + setError(t("New passwords don't match")); return; } if (newPassword.length < 6) { - setError("New password must be at least 6 characters long"); + setError(t("New password must be at least 6 characters long")); return; } @@ -214,7 +220,7 @@ function ChangePasswordDialog() { app.user = res.data!; showToast({ - message: "Password changed successfully", + message: t("Password changed successfully"), type: "success", }); @@ -232,7 +238,7 @@ function ChangePasswordDialog() { }; return <> - } title="Change Password" onClick={() => { + } title={t("Change Password")} onClick={() => { const dialog = document.getElementById("change_password_dialog") as HTMLDialogElement; if (dialog) { dialog.showModal(); @@ -240,13 +246,13 @@ function ChangePasswordDialog() { }} />
-

Change Password

+

{t("Change Password")}

- Current Password + {t("Current Password")} setOldPassword(e.target.value)} @@ -254,10 +260,10 @@ function ChangePasswordDialog() {
- New Password + {t("New Password")} setNewPassword(e.target.value)} @@ -265,10 +271,10 @@ function ChangePasswordDialog() {
- Confirm New Password + {t("Confirm New Password")} setConfirmPassword(e.target.value)} @@ -279,7 +285,7 @@ function ChangePasswordDialog() {
- +
diff --git a/frontend/src/pages/resource_details_page.tsx b/frontend/src/pages/resource_details_page.tsx index 1cb06a2..1aa2e2c 100644 --- a/frontend/src/pages/resource_details_page.tsx +++ b/frontend/src/pages/resource_details_page.tsx @@ -1,23 +1,31 @@ -import { useNavigate, useParams } from "react-router"; +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 {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, + MdOutlineDelete, + 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"; import showPopup, {useClosePopup} from "../components/popup.tsx"; import {Turnstile} from "@marsidev/react-turnstile"; +import Button from "../components/button.tsx"; export default function ResourcePage() { const params = useParams() - const { t } = useTranslation(); + const {t} = useTranslation(); const idStr = params.id @@ -34,7 +42,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]) @@ -50,7 +58,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"}) } }) } @@ -67,7 +75,7 @@ export default function ResourcePage() { } if (!resource) { - return + return } return @@ -86,7 +94,7 @@ export default function ResourcePage() {
- {"avatar"} + {"avatar"}/
@@ -106,57 +114,111 @@ export default function ResourcePage() {
-
+
- +
- +
+ +
+
} +function DeleteResourceDialog({resourceId, uploaderId}: { resourceId: number, uploaderId?: number }) { + const [isLoading, setLoading] = useState(false) + + const navigate = useNavigate() + + const {t} = useTranslation() + + const handleDelete = async () => { + if (isLoading) { + return + } + setLoading(true) + const res = await network.deleteResource(resourceId) + 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}) + } else { + showToast({message: res.message, type: "error"}) + } + setLoading(false) + } + + if (!app.isAdmin() && app.user?.id !== uploaderId) { + return <> + } + + return <> + + +
+

{t("Delete Resource")}

+

{t("Are you sure you want to delete the resource")}? {t("This action cannot be undone.")}

+
+
+ +
+ +
+
+
+ +} + const context = createContext<() => void>(() => { }) -function Article({ article }: { article: string }) { +function Article({article}: { article: string }) { return
{article}
} -function FileTile({ file }: { file: RFile }) { +function FileTile({file}: { file: RFile }) { const buttonRef = createRef() return
@@ -174,14 +236,15 @@ function FileTile({ file }: { file: RFile }) { showPopup(, buttonRef.current!) } }}> - + +
} -function CloudflarePopup({ file }: { file: RFile }) { +function CloudflarePopup({file}: { file: RFile }) { const closePopup = useClosePopup() return
@@ -193,7 +256,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) => { @@ -214,8 +277,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) @@ -261,7 +324,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) @@ -282,7 +345,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) @@ -302,7 +365,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) @@ -316,7 +379,7 @@ function CreateFileDialog({ resourceId }: { resourceId: number }) { dialog.showModal() }}> { - isLoading ? : + isLoading ? : } {t("Upload")} @@ -330,13 +393,13 @@ function CreateFileDialog({ resourceId }: { resourceId: number }) {
{ setFileType(null); - }} /> + }}/> { setFileType(FileType.redirect); - }} /> + }}/> { setFileType(FileType.upload); - }} /> + }}/>
{ @@ -344,13 +407,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) - }} /> + }}/> } @@ -373,25 +436,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 && }
@@ -407,7 +470,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); @@ -418,6 +481,8 @@ function Comments({ resourceId }: { resourceId: number }) { const [isLoading, setLoading] = useState(false); + const {t} = useTranslation(); + const reload = useCallback(() => { setPage(1); setMaxPage(0); @@ -429,41 +494,41 @@ function Comments({ resourceId }: { resourceId: number }) { return; } if (commentContent === "") { - showToast({ message: "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: "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); } return
-