mirror of
https://github.com/wgh136/nysoure.git
synced 2025-09-27 12:17:24 +00:00
Update translations
This commit is contained in:
@@ -39,6 +39,8 @@ export const i18nData = {
|
|||||||
"Description cannot be empty": "Description cannot be empty",
|
"Description cannot be empty": "Description cannot be empty",
|
||||||
"Loading": "Loading",
|
"Loading": "Loading",
|
||||||
"Enter a search keyword to continue": "Enter a search keyword to continue",
|
"Enter a search keyword to continue": "Enter a search keyword to continue",
|
||||||
|
"My Info": "My Info",
|
||||||
|
"Server": "Server",
|
||||||
|
|
||||||
// Management page translations
|
// Management page translations
|
||||||
"Manage": "Manage",
|
"Manage": "Manage",
|
||||||
@@ -106,6 +108,40 @@ export const i18nData = {
|
|||||||
"URL": "URL",
|
"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.",
|
"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",
|
"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": {
|
"zh-CN": {
|
||||||
@@ -148,6 +184,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": "个人信息",
|
||||||
|
"Server": "服务器",
|
||||||
|
|
||||||
// Management page translations
|
// Management page translations
|
||||||
"Manage": "管理",
|
"Manage": "管理",
|
||||||
@@ -215,6 +253,40 @@ export const i18nData = {
|
|||||||
"URL": "URL",
|
"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": "资源详情",
|
||||||
|
"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": {
|
"zh-TW": {
|
||||||
@@ -257,6 +329,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": "個人信息",
|
||||||
|
"Server": "伺服器",
|
||||||
|
|
||||||
// Management page translations
|
// Management page translations
|
||||||
"Manage": "管理",
|
"Manage": "管理",
|
||||||
@@ -324,6 +398,40 @@ export const i18nData = {
|
|||||||
"URL": "URL",
|
"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": "資源詳情",
|
||||||
|
"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 密鑰,將在註冊和下載時啟用驗證",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -408,6 +408,19 @@ class Network {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteResource(id: number): Promise<Response<void>> {
|
||||||
|
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,
|
async createS3Storage(name: string, endPoint: string, accessKeyID: string,
|
||||||
secretAccessKey: string, bucketName: string, maxSizeInMB: number): Promise<Response<any>> {
|
secretAccessKey: string, bucketName: string, maxSizeInMB: number): Promise<Response<any>> {
|
||||||
try {
|
try {
|
||||||
@@ -587,7 +600,7 @@ class Network {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteFile(fileId: number): Promise<Response<void>> {
|
async deleteFile(fileId: string): Promise<Response<void>> {
|
||||||
try {
|
try {
|
||||||
const response = await axios.delete(`${this.apiBaseUrl}/files/${fileId}`);
|
const response = await axios.delete(`${this.apiBaseUrl}/files/${fileId}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
|
@@ -42,6 +42,8 @@ function ChangeAvatarDialog() {
|
|||||||
|
|
||||||
const navigator = useNavigator();
|
const navigator = useNavigator();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const selectAvatar = () => {
|
const selectAvatar = () => {
|
||||||
const input = document.createElement("input");
|
const input = document.createElement("input");
|
||||||
input.type = "file";
|
input.type = "file";
|
||||||
@@ -67,7 +69,7 @@ function ChangeAvatarDialog() {
|
|||||||
app.user = res.data!;
|
app.user = res.data!;
|
||||||
navigator.refresh();
|
navigator.refresh();
|
||||||
showToast({
|
showToast({
|
||||||
message: "Avatar changed successfully",
|
message: t("Avatar changed successfully"),
|
||||||
type: "success",
|
type: "success",
|
||||||
})
|
})
|
||||||
const dialog = document.getElementById("change_avatar_dialog") as HTMLDialogElement;
|
const dialog = document.getElementById("change_avatar_dialog") as HTMLDialogElement;
|
||||||
@@ -78,7 +80,7 @@ function ChangeAvatarDialog() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<ListTile icon={<MdOutlineAccountCircle />} title="Change Avatar" onClick={() => {
|
<ListTile icon={<MdOutlineAccountCircle />} title={t("Change Avatar")} onClick={() => {
|
||||||
const dialog = document.getElementById("change_avatar_dialog") as HTMLDialogElement;
|
const dialog = document.getElementById("change_avatar_dialog") as HTMLDialogElement;
|
||||||
if (dialog) {
|
if (dialog) {
|
||||||
dialog.showModal();
|
dialog.showModal();
|
||||||
@@ -86,7 +88,7 @@ function ChangeAvatarDialog() {
|
|||||||
}} />
|
}} />
|
||||||
<dialog id="change_avatar_dialog" className="modal">
|
<dialog id="change_avatar_dialog" className="modal">
|
||||||
<div className="modal-box">
|
<div className="modal-box">
|
||||||
<h3 className="font-bold text-lg">Change Avatar</h3>
|
<h3 className="font-bold text-lg">{t("Change Avatar")}</h3>
|
||||||
<div className="h-48 flex items-center justify-center">
|
<div className="h-48 flex items-center justify-center">
|
||||||
<div className="avatar">
|
<div className="avatar">
|
||||||
<div className="w-28 rounded-full cursor-pointer" onClick={selectAvatar}>
|
<div className="w-28 rounded-full cursor-pointer" onClick={selectAvatar}>
|
||||||
@@ -97,9 +99,9 @@ function ChangeAvatarDialog() {
|
|||||||
{error && <ErrorAlert message={error} className={"m-4"} />}
|
{error && <ErrorAlert message={error} className={"m-4"} />}
|
||||||
<div className="modal-action">
|
<div className="modal-action">
|
||||||
<form method="dialog">
|
<form method="dialog">
|
||||||
<Button>Close</Button>
|
<Button>{t("Close")}</Button>
|
||||||
</form>
|
</form>
|
||||||
<Button className="btn-primary" onClick={handleSubmit} isLoading={isLoading} disabled={avatar == null}>Save</Button>
|
<Button className="btn-primary" onClick={handleSubmit} isLoading={isLoading} disabled={avatar == null}>{t("Save")}</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
@@ -112,9 +114,11 @@ function ChangeUsernameDialog() {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const navigator = useNavigator();
|
const navigator = useNavigator();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!newUsername.trim()) {
|
if (!newUsername.trim()) {
|
||||||
setError("Username cannot be empty");
|
setError(t("Username cannot be empty"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -126,7 +130,7 @@ function ChangeUsernameDialog() {
|
|||||||
app.user = res.data!;
|
app.user = res.data!;
|
||||||
navigator.refresh();
|
navigator.refresh();
|
||||||
showToast({
|
showToast({
|
||||||
message: "Username changed successfully",
|
message: t("Username changed successfully"),
|
||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
const dialog = document.getElementById("change_username_dialog") as HTMLDialogElement;
|
const dialog = document.getElementById("change_username_dialog") as HTMLDialogElement;
|
||||||
@@ -139,7 +143,7 @@ function ChangeUsernameDialog() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<ListTile icon={<MdOutlineEditNote />} title="Change Username" onClick={() => {
|
<ListTile icon={<MdOutlineEditNote />} title={t("Change Username")} onClick={() => {
|
||||||
const dialog = document.getElementById("change_username_dialog") as HTMLDialogElement;
|
const dialog = document.getElementById("change_username_dialog") as HTMLDialogElement;
|
||||||
if (dialog) {
|
if (dialog) {
|
||||||
dialog.showModal();
|
dialog.showModal();
|
||||||
@@ -147,14 +151,14 @@ function ChangeUsernameDialog() {
|
|||||||
}} />
|
}} />
|
||||||
<dialog id="change_username_dialog" className="modal">
|
<dialog id="change_username_dialog" className="modal">
|
||||||
<div className="modal-box">
|
<div className="modal-box">
|
||||||
<h3 className="font-bold text-lg">Change Username</h3>
|
<h3 className="font-bold text-lg">{t("Change Username")}</h3>
|
||||||
<div className="input mt-4 w-full">
|
<div className="input mt-4 w-full">
|
||||||
<label className="label">
|
<label className="label">
|
||||||
New Username
|
{t("New Username")}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter new username"
|
placeholder={t("Enter new username")}
|
||||||
value={newUsername}
|
value={newUsername}
|
||||||
onChange={(e) => setNewUsername(e.target.value)}
|
onChange={(e) => setNewUsername(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -162,7 +166,7 @@ function ChangeUsernameDialog() {
|
|||||||
{error && <ErrorAlert message={error} className={"mt-4"} />}
|
{error && <ErrorAlert message={error} className={"mt-4"} />}
|
||||||
<div className="modal-action">
|
<div className="modal-action">
|
||||||
<form method="dialog">
|
<form method="dialog">
|
||||||
<Button>Close</Button>
|
<Button>{t("Close")}</Button>
|
||||||
</form>
|
</form>
|
||||||
<Button
|
<Button
|
||||||
className="btn-primary"
|
className="btn-primary"
|
||||||
@@ -170,7 +174,7 @@ function ChangeUsernameDialog() {
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
disabled={!newUsername.trim()}
|
disabled={!newUsername.trim()}
|
||||||
>
|
>
|
||||||
Save
|
{t("Save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -185,20 +189,22 @@ function ChangePasswordDialog() {
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
// Validate input
|
// Validate input
|
||||||
if (!oldPassword || !newPassword || !confirmPassword) {
|
if (!oldPassword || !newPassword || !confirmPassword) {
|
||||||
setError("All fields are required");
|
setError(t("All fields are required"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newPassword !== confirmPassword) {
|
if (newPassword !== confirmPassword) {
|
||||||
setError("New passwords don't match");
|
setError(t("New passwords don't match"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newPassword.length < 6) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +220,7 @@ function ChangePasswordDialog() {
|
|||||||
app.user = res.data!;
|
app.user = res.data!;
|
||||||
|
|
||||||
showToast({
|
showToast({
|
||||||
message: "Password changed successfully",
|
message: t("Password changed successfully"),
|
||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -232,7 +238,7 @@ function ChangePasswordDialog() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<ListTile icon={<MdLockOutline />} title="Change Password" onClick={() => {
|
<ListTile icon={<MdLockOutline />} title={t("Change Password")} onClick={() => {
|
||||||
const dialog = document.getElementById("change_password_dialog") as HTMLDialogElement;
|
const dialog = document.getElementById("change_password_dialog") as HTMLDialogElement;
|
||||||
if (dialog) {
|
if (dialog) {
|
||||||
dialog.showModal();
|
dialog.showModal();
|
||||||
@@ -240,13 +246,13 @@ function ChangePasswordDialog() {
|
|||||||
}} />
|
}} />
|
||||||
<dialog id="change_password_dialog" className="modal">
|
<dialog id="change_password_dialog" className="modal">
|
||||||
<div className="modal-box">
|
<div className="modal-box">
|
||||||
<h3 className="font-bold text-lg mb-2">Change Password</h3>
|
<h3 className="font-bold text-lg mb-2">{t("Change Password")}</h3>
|
||||||
|
|
||||||
<fieldset className="fieldset w-full">
|
<fieldset className="fieldset w-full">
|
||||||
<legend className="fieldset-legend">Current Password</legend>
|
<legend className="fieldset-legend">{t("Current Password")}</legend>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Enter current password"
|
placeholder={t("Enter current password")}
|
||||||
value={oldPassword}
|
value={oldPassword}
|
||||||
className="input w-full"
|
className="input w-full"
|
||||||
onChange={(e) => setOldPassword(e.target.value)}
|
onChange={(e) => setOldPassword(e.target.value)}
|
||||||
@@ -254,10 +260,10 @@ function ChangePasswordDialog() {
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset className="fieldset w-full">
|
<fieldset className="fieldset w-full">
|
||||||
<legend className="fieldset-legend">New Password</legend>
|
<legend className="fieldset-legend">{t("New Password")}</legend>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Enter new password"
|
placeholder={t("Enter new password")}
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
className="input w-full"
|
className="input w-full"
|
||||||
onChange={(e) => setNewPassword(e.target.value)}
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
@@ -265,10 +271,10 @@ function ChangePasswordDialog() {
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset className="fieldset w-full">
|
<fieldset className="fieldset w-full">
|
||||||
<legend className="fieldset-legend">Confirm New Password</legend>
|
<legend className="fieldset-legend">{t("Confirm New Password")}</legend>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Confirm new password"
|
placeholder={t("Confirm new password")}
|
||||||
value={confirmPassword}
|
value={confirmPassword}
|
||||||
className="input w-full"
|
className="input w-full"
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
@@ -279,7 +285,7 @@ function ChangePasswordDialog() {
|
|||||||
|
|
||||||
<div className="modal-action">
|
<div className="modal-action">
|
||||||
<form method="dialog">
|
<form method="dialog">
|
||||||
<Button>Close</Button>
|
<Button>{t("Close")}</Button>
|
||||||
</form>
|
</form>
|
||||||
<Button
|
<Button
|
||||||
className="btn-primary"
|
className="btn-primary"
|
||||||
@@ -287,7 +293,7 @@ function ChangePasswordDialog() {
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
disabled={!oldPassword || !newPassword || !confirmPassword}
|
disabled={!oldPassword || !newPassword || !confirmPassword}
|
||||||
>
|
>
|
||||||
Save
|
{t("Save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -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 {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";
|
||||||
import Loading from "../components/loading.tsx";
|
import Loading from "../components/loading.tsx";
|
||||||
import { MdAdd, MdOutlineArticle, MdOutlineComment, MdOutlineDataset, MdOutlineDownload } from "react-icons/md";
|
import {
|
||||||
import { app } from "../app.ts";
|
MdAdd,
|
||||||
import { uploadingManager } from "../network/uploading.ts";
|
MdOutlineArticle,
|
||||||
import { ErrorAlert } from "../components/alert.tsx";
|
MdOutlineComment,
|
||||||
import { useTranslation } from "react-i18next";
|
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 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";
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -34,7 +42,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])
|
||||||
@@ -50,7 +58,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"})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -67,7 +75,7 @@ export default function ResourcePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
return <Loading />
|
return <Loading/>
|
||||||
}
|
}
|
||||||
|
|
||||||
return <context.Provider value={reload}>
|
return <context.Provider value={reload}>
|
||||||
@@ -86,7 +94,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>
|
||||||
@@ -106,57 +114,111 @@ 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 article={resource.article}/>
|
||||||
</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>
|
||||||
|
<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 }) {
|
||||||
|
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 <>
|
||||||
|
<Button className={"btn-error btn-ghost"} onClick={() => {
|
||||||
|
const dialog = document.getElementById("delete_resource_dialog") as HTMLDialogElement
|
||||||
|
dialog.showModal()
|
||||||
|
}}>
|
||||||
|
<MdOutlineDelete size={20} className={"inline-block"}/>
|
||||||
|
</Button>
|
||||||
|
<dialog id={`delete_resource_dialog`} className="modal">
|
||||||
|
<div className="modal-box">
|
||||||
|
<h3 className="font-bold text-lg">{t("Delete Resource")}</h3>
|
||||||
|
<p
|
||||||
|
className="py-4">{t("Are you sure you want to delete the resource")}? {t("This action cannot be undone.")}</p>
|
||||||
|
<div className="modal-action">
|
||||||
|
<form method="dialog">
|
||||||
|
<button className="btn btn-ghost">{t("Close")}</button>
|
||||||
|
</form>
|
||||||
|
<Button className="btn btn-error" isLoading={isLoading} onClick={handleDelete}>{t("Delete")}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
const context = createContext<() => void>(() => {
|
const context = createContext<() => void>(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
function Article({ article }: { article: string }) {
|
function Article({article}: { article: string }) {
|
||||||
return <article>
|
return <article>
|
||||||
<Markdown>{article}</Markdown>
|
<Markdown>{article}</Markdown>
|
||||||
</article>
|
</article>
|
||||||
}
|
}
|
||||||
|
|
||||||
function FileTile({ file }: { file: RFile }) {
|
function FileTile({file}: { file: RFile }) {
|
||||||
const buttonRef = createRef<HTMLButtonElement>()
|
const buttonRef = createRef<HTMLButtonElement>()
|
||||||
|
|
||||||
return <div className={"card card-border border-base-300 my-2"}>
|
return <div className={"card card-border border-base-300 my-2"}>
|
||||||
@@ -174,14 +236,15 @@ function FileTile({ file }: { file: RFile }) {
|
|||||||
showPopup(<CloudflarePopup file={file}/>, buttonRef.current!)
|
showPopup(<CloudflarePopup file={file}/>, buttonRef.current!)
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<MdOutlineDownload size={24} />
|
<MdOutlineDownload size={24}/>
|
||||||
</button>
|
</button>
|
||||||
|
<DeleteFileDialog fileId={file.id}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
function CloudflarePopup({ file }: { file: RFile }) {
|
function CloudflarePopup({file}: { file: RFile }) {
|
||||||
const closePopup = useClosePopup()
|
const closePopup = useClosePopup()
|
||||||
|
|
||||||
return <div className={"menu bg-base-100 rounded-box z-1 w-80 p-2 shadow-sm h-20"}>
|
return <div className={"menu bg-base-100 rounded-box z-1 w-80 p-2 shadow-sm h-20"}>
|
||||||
@@ -193,7 +256,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) => {
|
||||||
@@ -214,8 +277,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)
|
||||||
@@ -261,7 +324,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)
|
||||||
@@ -282,7 +345,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)
|
||||||
@@ -302,7 +365,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)
|
||||||
@@ -316,7 +379,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")}
|
||||||
@@ -330,13 +393,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>
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -344,13 +407,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)
|
||||||
}} />
|
}}/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,15 +446,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">
|
||||||
@@ -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 [page, setPage] = useState(1);
|
||||||
|
|
||||||
const [maxPage, setMaxPage] = useState(0);
|
const [maxPage, setMaxPage] = useState(0);
|
||||||
@@ -418,6 +481,8 @@ function Comments({ resourceId }: { resourceId: number }) {
|
|||||||
|
|
||||||
const [isLoading, setLoading] = useState(false);
|
const [isLoading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const {t} = useTranslation();
|
||||||
|
|
||||||
const reload = useCallback(() => {
|
const reload = useCallback(() => {
|
||||||
setPage(1);
|
setPage(1);
|
||||||
setMaxPage(0);
|
setMaxPage(0);
|
||||||
@@ -429,25 +494,25 @@ function Comments({ resourceId }: { resourceId: number }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (commentContent === "") {
|
if (commentContent === "") {
|
||||||
showToast({ message: "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: "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);
|
||||||
}
|
}
|
||||||
|
|
||||||
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={"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"}`}>
|
||||||
@@ -456,14 +521,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
|
||||||
@@ -486,25 +551,25 @@ 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 }) {
|
||||||
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">
|
<div className="avatar">
|
||||||
<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>
|
||||||
@@ -517,3 +582,55 @@ function CommentTile({ comment }: { comment: Comment }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function DeleteFileDialog({fileId}: { fileId: string }) {
|
||||||
|
const [isLoading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
const id = `delete_file_dialog_${fileId}`
|
||||||
|
|
||||||
|
const reload = useContext(context)
|
||||||
|
|
||||||
|
const {t} = useTranslation();
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (isLoading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setLoading(true)
|
||||||
|
const res = await network.deleteFile(fileId)
|
||||||
|
const dialog = document.getElementById(id) as HTMLDialogElement
|
||||||
|
dialog.close()
|
||||||
|
if (res.success) {
|
||||||
|
showToast({message: t("File deleted successfully"), type: "success"})
|
||||||
|
reload()
|
||||||
|
} else {
|
||||||
|
showToast({message: res.message, type: "error"})
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!app.isAdmin()) {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<button className={"btn btn-error btn-ghost btn-circle ml-1"} onClick={() => {
|
||||||
|
const dialog = document.getElementById(id) as HTMLDialogElement
|
||||||
|
dialog.showModal()
|
||||||
|
}}>
|
||||||
|
<MdOutlineDelete size={20} className={"inline-block"}/>
|
||||||
|
</button>
|
||||||
|
<dialog id={id} className="modal">
|
||||||
|
<div className="modal-box">
|
||||||
|
<h3 className="font-bold text-lg">{t("Delete File")}</h3>
|
||||||
|
<p className="py-4">{t("Are you sure you want to delete the file? This action cannot be undone.")}</p>
|
||||||
|
<div className="modal-action">
|
||||||
|
<form method="dialog">
|
||||||
|
<button className="btn btn-ghost">{t("Close")}</button>
|
||||||
|
</form>
|
||||||
|
<button className="btn btn-error" onClick={handleDelete}>{t("Delete")}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user