diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e93297c..937e094 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,8 +12,6 @@ "@tailwindcss/vite": "^4.1.5", "axios": "^1.9.0", "framer-motion": "^12.23.5", - "i18next": "^25.1.1", - "i18next-browser-languagedetector": "^8.1.0", "masonic": "^4.1.0", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -3690,6 +3688,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.26.10" }, @@ -3702,15 +3701,6 @@ } } }, - "node_modules/i18next-browser-languagedetector": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.1.0.tgz", - "integrity": "sha512-mHZxNx1Lq09xt5kCauZ/4bsXOEA2pfpwSoU11/QTJB+pD94iONFwp+ohqi///PwiFvjFOxe1akYCdHyFo1ng5Q==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.2" - } - }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index 53c3855..aff2fc2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,8 +19,6 @@ "@tailwindcss/vite": "^4.1.5", "axios": "^1.9.0", "framer-motion": "^12.23.5", - "i18next": "^25.1.1", - "i18next-browser-languagedetector": "^8.1.0", "masonic": "^4.1.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/frontend/src/app.tsx b/frontend/src/app.tsx index 8d48b3d..f07bbb2 100644 --- a/frontend/src/app.tsx +++ b/frontend/src/app.tsx @@ -15,29 +15,43 @@ import TagsPage from "./pages/tags_page.tsx"; import RandomPage from "./pages/random_page.tsx"; import ActivitiesPage from "./pages/activities_page.tsx"; import CommentPage from "./pages/comment_page.tsx"; +import CreateCollectionPage from "./pages/create_collection_page.tsx"; +import CollectionPage from "./pages/collection_page.tsx"; +import { i18nData } from "./i18n.ts"; +import { i18nContext } from "./utils/i18n.ts"; export default function App() { return ( - - - } /> - } /> - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - + + + + } /> + } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } + /> + } /> + } /> + } /> + } /> + } /> + } + /> + } /> + + + + ); } diff --git a/frontend/src/components/comment_input.tsx b/frontend/src/components/comment_input.tsx index 26b7f41..a9951b8 100644 --- a/frontend/src/components/comment_input.tsx +++ b/frontend/src/components/comment_input.tsx @@ -1,5 +1,5 @@ import { useState, useRef, useEffect } from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "../utils/i18n"; import showToast from "./toast"; import { network } from "../network/network"; import { InfoAlert } from "./alert"; diff --git a/frontend/src/components/comment_tile.tsx b/frontend/src/components/comment_tile.tsx index 35e3c38..8c912c1 100644 --- a/frontend/src/components/comment_tile.tsx +++ b/frontend/src/components/comment_tile.tsx @@ -1,4 +1,4 @@ -import { useTranslation } from "react-i18next"; +import { useTranslation } from "../utils/i18n"; import { useNavigate } from "react-router"; import { MdOutlineComment } from "react-icons/md"; import { Comment } from "../network/models"; diff --git a/frontend/src/components/image_selector.tsx b/frontend/src/components/image_selector.tsx index 895bc49..c518771 100644 --- a/frontend/src/components/image_selector.tsx +++ b/frontend/src/components/image_selector.tsx @@ -1,5 +1,5 @@ import { MdAdd } from "react-icons/md"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "../utils/i18n"; import { network } from "../network/network.ts"; import showToast from "./toast.ts"; import { useState } from "react"; diff --git a/frontend/src/components/loading.tsx b/frontend/src/components/loading.tsx index 210e3ce..09cb2f7 100644 --- a/frontend/src/components/loading.tsx +++ b/frontend/src/components/loading.tsx @@ -1,4 +1,4 @@ -import { useTranslation } from "react-i18next"; +import { useTranslation } from "../utils/i18n"; export default function Loading() { const { t } = useTranslation(); diff --git a/frontend/src/components/navigator.tsx b/frontend/src/components/navigator.tsx index ce0752f..9e1a8fb 100644 --- a/frontend/src/components/navigator.tsx +++ b/frontend/src/components/navigator.tsx @@ -3,7 +3,7 @@ import { network } from "../network/network.ts"; import { useNavigate, useOutlet } from "react-router"; import { createContext, useContext, useEffect, useState } from "react"; import { MdArrowUpward, MdOutlinePerson, MdSearch } from "react-icons/md"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "../utils/i18n"; import UploadingSideBar from "./uploading_side_bar.tsx"; import { ThemeSwitcher } from "./theme_switcher.tsx"; import { IoLogoGithub } from "react-icons/io"; diff --git a/frontend/src/components/resource_card.tsx b/frontend/src/components/resource_card.tsx index b6a0d38..873228f 100644 --- a/frontend/src/components/resource_card.tsx +++ b/frontend/src/components/resource_card.tsx @@ -2,8 +2,15 @@ import { Resource } from "../network/models.ts"; import { network } from "../network/network.ts"; import { useNavigate } from "react-router"; import Badge from "./badge.tsx"; +import React from "react"; -export default function ResourceCard({ resource }: { resource: Resource }) { +export default function ResourceCard({ + resource, + action, +}: { + resource: Resource; + action?: React.ReactNode; +}) { const navigate = useNavigate(); let tags = resource.tags; @@ -58,6 +65,8 @@ export default function ResourceCard({ resource }: { resource: Resource }) {
{resource.author.username}
+
+ {action} diff --git a/frontend/src/components/resources_view.tsx b/frontend/src/components/resources_view.tsx index b3d102b..ebbd672 100644 --- a/frontend/src/components/resources_view.tsx +++ b/frontend/src/components/resources_view.tsx @@ -9,9 +9,11 @@ import { useAppContext } from "./AppContext.tsx"; export default function ResourcesView({ loader, storageKey, + actionBuilder, }: { loader: (page: number) => Promise>; storageKey?: string; + actionBuilder?: (resource: Resource) => React.ReactNode; }) { const [data, setData] = useState([]); const pageRef = useRef(1); @@ -54,7 +56,8 @@ export default function ResourcesView({ isLoadingRef.current = false; pageRef.current = pageRef.current + 1; totalPagesRef.current = res.totalPages ?? 1; - setData((prev) => [...prev, ...res.data!]); + let data = res.data ?? []; + setData((prev) => [...prev, ...data]); } }, [loader]); @@ -71,7 +74,13 @@ export default function ResourcesView({ columnWidth={300} items={data} render={(e) => { - return ; + return ( + + ); }} > {pageRef.current <= totalPagesRef.current && } diff --git a/frontend/src/components/tag_input.tsx b/frontend/src/components/tag_input.tsx index 0290f66..fc50661 100644 --- a/frontend/src/components/tag_input.tsx +++ b/frontend/src/components/tag_input.tsx @@ -1,12 +1,13 @@ import { Tag } from "../network/models.ts"; import { useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "../utils/i18n"; import { network } from "../network/network.ts"; import { LuInfo } from "react-icons/lu"; import { MdSearch } from "react-icons/md"; import Button from "./button.tsx"; import Input, { TextArea } from "./input.tsx"; import { ErrorAlert } from "./alert.tsx"; +import { Debounce } from "../utils/debounce.ts"; export default function TagInput({ onAdd, @@ -177,31 +178,6 @@ export default function TagInput({ ); } -class Debounce { - private timer: number | null = null; - private readonly delay: number; - - constructor(delay: number) { - this.delay = delay; - } - - run(callback: () => void) { - if (this.timer) { - clearTimeout(this.timer); - } - this.timer = setTimeout(() => { - callback(); - }, this.delay); - } - - cancel() { - if (this.timer) { - clearTimeout(this.timer); - this.timer = null; - } - } -} - export function QuickAddTagDialog({ onAdded, }: { diff --git a/frontend/src/components/theme_switcher.tsx b/frontend/src/components/theme_switcher.tsx index c94a24b..126c038 100644 --- a/frontend/src/components/theme_switcher.tsx +++ b/frontend/src/components/theme_switcher.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "../utils/i18n"; import { MdPalette } from "react-icons/md"; interface ThemeOption { diff --git a/frontend/src/components/toast.ts b/frontend/src/components/toast.ts index 5a9b47e..4d46a1d 100644 --- a/frontend/src/components/toast.ts +++ b/frontend/src/components/toast.ts @@ -10,7 +10,7 @@ export default function showToast({ type = type || "info"; const div = document.createElement("div"); div.innerHTML = ` -
+
${message}
diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index 99f2902..a5e2815 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -1,243 +1,4 @@ export const i18nData = { - "en": { - translation: { - "My Profile": "My Profile", - "Publish": "Publish", - "Log out": "Log out", - "Are you sure you want to log out?": "Are you sure you want to log out?", - "Cancel": "Cancel", - "Confirm": "Confirm", - "Search": "Search", - "Login": "Login", - "Register": "Register", - "Username": "Username", - "Password": "Password", - "Confirm Password": "Confirm Password", - "Username and password cannot be empty": - "Username and password cannot be empty", - "Passwords do not match": "Passwords do not match", - "Continue": "Continue", - "Don't have an account? Register": "Don't have an account? Register", - "Already have an account? Login": "Already have an account? Login", - "Publish Resource": "Publish Resource", - "All information can be modified after publishing": - "All information can be modified after publishing", - "Title": "Title", - "Alternative Titles": "Alternative Titles", - "Add Alternative Title": "Add Alternative Title", - "Tags": "Tags", - "Description": "Description", - "Use Markdown format": "Use Markdown format", - "Images": "Images", - "Images will not be displayed automatically, you need to reference them in the description": - "Images will not be displayed automatically, you need to reference them in the description", - "Preview": "Preview", - "Link": "Link", - "Action": "Action", - "Upload Image": "Upload Image", - "Error": "Error", - "Title cannot be empty": "Title cannot be empty", - "Alternative title cannot be empty": "Alternative title cannot be empty", - "At least one tag required": "At least one tag required", - "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 - "Settings": "Settings", - "Manage": "Manage", - "Storage": "Storage", - "Users": "Users", - "You are not logged in. Please log in to access this page.": - "You are not logged in. Please log in to access this page.", - "You are not authorized to access this page.": - "You are not authorized to access this page.", - - // Storage management - "No storage found. Please create a new storage.": - "No storage found. Please create a new storage.", - "Name": "Name", - "Created At": "Created At", - "Actions": "Actions", - "Delete Storage": "Delete Storage", - "Are you sure you want to delete this storage? This action cannot be undone.": - "Are you sure you want to delete this storage? This action cannot be undone.", - "Delete": "Delete", - "Storage deleted successfully": "Storage deleted successfully", - "New Storage": "New Storage", - "Type": "Type", - "Local": "Local", - "S3": "S3", - "Path": "Path", - "Max Size (MB)": "Max Size (MB)", - "Endpoint": "Endpoint", - "Access Key ID": "Access Key ID", - "Secret Access Key": "Secret Access Key", - "Bucket Name": "Bucket Name", - "All fields are required": "All fields are required", - "Storage created successfully": "Storage created successfully", - "Close": "Close", - "Submit": "Submit", - - // User management - "Admin": "Admin", - "Can Upload": "Can Upload", - "Yes": "Yes", - "No": "No", - "Delete User": "Delete User", - "Are you sure you want to delete user": - "Are you sure you want to delete user", - "This action cannot be undone.": "This action cannot be undone.", - "User deleted successfully": "User deleted successfully", - "Set as user": "Set as user", - "Set as admin": "Set as admin", - "Remove upload permission": "Remove upload permission", - "Grant upload permission": "Grant upload permission", - "User set as admin successfully": "User set as admin successfully", - "User set as user successfully": "User set as user successfully", - "User set as upload permission successfully": - "User set as upload permission successfully", - "User removed upload permission successfully": - "User removed upload permission successfully", - - // Resource details page - "Resource ID is required": "Resource ID is required", - "Files": "Files", - "Comments": "Comments", - "Upload": "Upload", - "Create File": "Create File", - "Please select a file type": "Please select a file type", - "Please fill in all fields": "Please fill in all fields", - "File created successfully": "File created successfully", - "Successfully create uploading task.": - "Successfully create uploading task.", - "Please select a file and storage": "Please select a file and storage", - "Redirect": "Redirect", - "User who click the file will be redirected to the URL": - "User who click the file will be redirected to the URL", - "File Name": "File Name", - "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", - - // 评论删除相关 - "Delete Comment": "Delete Comment", - "Are you sure you want to delete this comment? This action cannot be undone.": - "Are you sure you want to delete this comment? This action cannot be undone.", - "Comment deleted successfully": "Comment deleted successfully", - - // 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.", - "The first image will be used as the cover image": - "The first image will be used as the cover image", - "Please enter a search keyword": "Please enter a search keyword", - "Searching...": "Searching...", - "Create Tag": "Create Tag", - "Search Tags": "Search Tags", - "Edit Resource": "Edit Resource", - "Change Bio": "Change Bio", - "About this site": "About this site", - "Tag not found": "Tag not found", - "Description is too long": "Description is too long", - "Unknown error": "Unknown error", - "Edit": "Edit", - "Edit Tag": "Edit Tag", - "Set the description of the tag.": "Set the description of the tag.", - "Tag: ": "Tag: ", - "Select a Order": "Select a Order", - "Time Ascending": "Time Ascending", - "Time Descending": "Time Descending", - "Views Ascending": "Views Ascending", - "Views Descending": "Views Descending", - "Downloads Ascending": "Downloads Ascending", - "Downloads Descending": "Downloads Descending", - "File Url": "File Url", - "Provide a file url for the server to download, and the file will be moved to the selected storage.": - "Provide a file url for the server to download, and the file will be moved to the selected storage.", - "Verifying your request": "Verifying your request", - "Please check your network if the verification takes too long or the captcha does not appear.": - "Please check your network if the verification takes too long or the captcha does not appear.", - "About": "About", - "Home": "Home", - "Other": "Other", - "Quick Add": "Quick Add", - "Add Tags": "Add Tags", - "Input tags separated by separator.": - "Input tags separated by separator.", - "If the tag does not exist, it will be created automatically.": - "If the tag does not exist, it will be created automatically.", - "Optionally, you can specify a type for the new tags.": - "Optionally, you can specify a type for the new tags.", - "Upload Clipboard Image": "Upload Clipboard Image", - "Show more": "Show more", - "Show less": "Show less", - "You need to log in to comment": "You need to log in to comment", - - // Color Scheme Translation - "Light Pink": "Light Pink", - "Ocean Breeze": "Ocean Breeze", - "Mint Leaf": "Mint Leaf", - "Golden Glow": "Golden Glow", - "Random": "Random", - - // Activity Page - "Activity": "Activity", - "Published a resource": "Published a resource", - "Updated a resource": "Updated a resource", - "Commented on a resource": "Commented on a resource", - - "Comment": "Comment", - "Replies": "Replies", - "Reply": "Reply", - "Commented on": "Commented on", - "Write down your comment": "Write down your comment", - "Click to view more": "Click to view more", - "Comment Details": "Comment Details", - "Posted a comment": "Posted a comment", - "Resources": "Resources", - "Added a new file": "Added a new file", - "Data from": "Data from", - }, - }, "zh-CN": { translation: { "My Profile": "我的资料", @@ -466,6 +227,29 @@ export const i18nData = { "Added a new file": "添加了新文件", "Data from": "数据来源", + + "Create Collection": "创建合集", + "Create": "创建", + "Image size exceeds 5MB limit": "图片大小超过5MB限制", + "Title and description cannot be empty": "标题和描述不能为空", + "Collection created successfully": "合集创建成功", + "Collection deleted successfully": "合集删除成功", + "Remove Resource": "移除资源", + "Are you sure you want to remove this resource?": "您确定要移除此资源吗?", + "Resource deleted successfully": "资源移除成功", + "Edit Collection": "编辑合集", + "Edit successful": "编辑成功", + "Failed to save changes": "保存更改失败", + + "Collect": "收藏", + "Add to Collection": "添加到合集", + "Add": "添加", + "Resource added to collection successfully": "资源已成功添加到合集", + "No patches found for this VN.": "未找到该作品的补丁。", + "Update File Info": "更新文件信息", + "File info updated successfully": "文件信息更新成功", + "File URL": "文件URL", + "You do not have permission to upload files, please contact the administrator.": "您没有上传文件的权限,请联系管理员。", }, }, "zh-TW": { @@ -696,6 +480,29 @@ export const i18nData = { "Added a new file": "添加了新檔案", "Data from": "數據來源", + + "Create Collection": "創建合集", + "Create": "創建", + "Image size exceeds 5MB limit": "圖片大小超過5MB限制", + "Title and description cannot be empty": "標題和描述不能為空", + "Collection created successfully": "合集創建成功", + "Collection deleted successfully": "合集刪除成功", + "Remove Resource": "移除資源", + "Are you sure you want to remove this resource?": "您確定要移除此資源嗎?", + "Resource deleted successfully": "資源移除成功", + "Edit Collection": "編輯合集", + "Edit successful": "編輯成功", + "Failed to save changes": "保存更改失敗", + + "Collect": "收藏", + "Add to Collection": "添加到合集", + "Add": "添加", + "Resource added to collection successfully": "資源已成功添加到合集", + "No patches found for this VN.": "未找到該作品的補丁。", + "Update File Info": "更新檔案信息", + "File info updated successfully": "檔案信息更新成功", + "File URL": "檔案URL", + "You do not have permission to upload files, please contact the administrator.": "您沒有上傳檔案的權限,請聯繫管理員。", }, }, }; diff --git a/frontend/src/index.css b/frontend/src/index.css index 7325673..423dc77 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -325,4 +325,4 @@ body { .lg\:bg-base-100-tr82 { background-color: rgb(var(--color-base-100-rgb) / 0.82); } -} \ No newline at end of file +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 62c12ee..58ba29a 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -2,29 +2,12 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import "./index.css"; import App from "./app.tsx"; -import i18n from "i18next"; -import { initReactI18next } from "react-i18next"; -import LanguageDetector from "i18next-browser-languagedetector"; -import { i18nData } from "./i18n.ts"; import AppContext from "./components/AppContext.tsx"; -i18n - .use(initReactI18next) - .use(LanguageDetector) - .init({ - resources: i18nData, - debug: true, - fallbackLng: "en", - interpolation: { - escapeValue: false, - }, - }) - .then(() => { - createRoot(document.getElementById("root")!).render( - - - - - , - ); - }); +createRoot(document.getElementById("root")!).render( + + + + + , +); diff --git a/frontend/src/pages/activities_page.tsx b/frontend/src/pages/activities_page.tsx index ea329ff..e71e73e 100644 --- a/frontend/src/pages/activities_page.tsx +++ b/frontend/src/pages/activities_page.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { Activity, ActivityType } from "../network/models.ts"; import { network } from "../network/network.ts"; import showToast from "../components/toast.ts"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "../utils/i18n"; import { useNavigate } from "react-router"; import Loading from "../components/loading.tsx"; import { CommentContent } from "../components/comment_tile.tsx"; diff --git a/frontend/src/pages/collection_page.tsx b/frontend/src/pages/collection_page.tsx new file mode 100644 index 0000000..7279979 --- /dev/null +++ b/frontend/src/pages/collection_page.tsx @@ -0,0 +1,341 @@ +import { useEffect, useRef, useState } from "react"; +import { useParams, useNavigate } from "react-router"; // 新增 useNavigate +import showToast from "../components/toast"; +import { network } from "../network/network"; +import { Collection } from "../network/models"; +import Markdown from "react-markdown"; +import ResourcesView from "../components/resources_view"; +import Loading from "../components/loading"; +import { MdOutlineDelete, MdOutlineEdit } from "react-icons/md"; +import { app } from "../app"; +import { useTranslation } from "../utils/i18n"; +import Button from "../components/button"; + +export default function CollectionPage() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const [collection, setCollection] = useState(null); + const [resourcesKey, setResourcesKey] = useState(0); + const { t } = useTranslation(); + const [isDeleting, setIsDeleting] = useState(false); + + const [editOpen, setEditOpen] = useState(false); + + const [deleteOpen, setDeleteOpen] = useState(false); + + useEffect(() => { + const idInt = parseInt(id || "0", 10); + if (isNaN(idInt)) { + showToast({ + type: "error", + message: "Invalid collection ID", + }); + return; + } + + network.getCollection(idInt).then((res) => { + if (res.success) { + setCollection(res.data!); + } else { + showToast({ + type: "error", + message: res.message || "Failed to load collection", + }); + } + }); + }, [resourcesKey]); + + const toBeDeletedRID = useRef(null); + + const handleDeleteResource = (resourceId: number) => { + toBeDeletedRID.current = resourceId; + const dialog = document.getElementById( + "deleteResourceDialog", + ) as HTMLDialogElement | null; + if (dialog) { + dialog.showModal(); + } + }; + + const handleDeletedResourceConfirmed = () => { + if (toBeDeletedRID.current === null) return; + network + .removeResourceFromCollection(collection!.id, toBeDeletedRID.current) + .then((res) => { + if (res.success) { + showToast({ + type: "success", + message: "Resource deleted successfully", + }); + setResourcesKey((prev) => prev + 1); // Trigger re-render of ResourcesView + } else { + showToast({ + type: "error", + message: res.message || "Failed to delete resource", + }); + } + }); + toBeDeletedRID.current = null; + const dialog = document.getElementById( + "deleteResourceDialog", + ) as HTMLDialogElement | null; + if (dialog) { + dialog.close(); + } + }; + + const handleDeleteCollection = () => setDeleteOpen(true); + const handleDeleteCollectionConfirmed = async () => { + if (!collection) return; + setIsDeleting(true); + const res = await network.deleteCollection(collection.id); + setIsDeleting(false); + if (res.success) { + showToast({ + type: "success", + message: "Collection deleted successfully", + }); + setDeleteOpen(false); + if (window.history.length > 1) { + navigate(-1); + } else { + navigate("/", { replace: true }); + } + } else { + showToast({ + type: "error", + message: res.message || "Failed to delete collection", + }); + setDeleteOpen(false); + } + }; + + const isOwner = collection?.user?.id === app?.user?.id; + + const openEditDialog = () => setEditOpen(true); + + const handleEditSaved = (newCollection: Collection) => { + setCollection(newCollection); + setEditOpen(false); + }; + + if (!collection) { + return ; + } + + return ( + <> +
+

{collection?.title}

+
+ +
+
+ {isOwner && ( + <> + + + + )} +
+
+ { + return network.listCollectionResources(collection!.id); + }} + actionBuilder={ + isOwner + ? (r) => { + return ( + + ); + } + : undefined + } + key={resourcesKey} + /> + +
+

Remove Resource

+

Are you sure you want to remove this resource?

+
+ + +
+
+
+ {deleteOpen && ( +
+
+

{t("Delete Collection")}

+

+ {t( + "Are you sure you want to delete this collection? This action cannot be undone.", + )} +

+
+ + +
+
+
+ )} + {editOpen && collection && ( + setEditOpen(false)} + onSaved={handleEditSaved} + /> + )} + + ); +} + +function CollectionContent({ content }: { content: string }) { + const lines = content.split("\n"); + for (let i = 0; i < lines.length; i++) { + let line = lines[i]; + if (!line.endsWith(" ")) { + // Ensure that each line ends with two spaces for Markdown to recognize it as a line break + lines[i] = line + " "; + } + } + content = lines.join("\n"); + + return {content}; +} + +function EditCollectionDialog({ + open, + collection, + onClose, + onSaved, +}: { + open: boolean; + collection: Collection; + onClose: () => void; + onSaved: (newCollection: Collection) => void; +}) { + const [editTitle, setEditTitle] = useState(collection.title); + const [editArticle, setEditArticle] = useState(collection.article); + const [editLoading, setEditLoading] = useState(false); + + const { t } = useTranslation(); + + const handleEditSave = async () => { + if (editTitle.trim() === "" || editArticle.trim() === "") { + showToast({ + type: "error", + message: t("Title and description cannot be empty"), + }); + return; + } + setEditLoading(true); + const res = await network.updateCollection( + collection.id, + editTitle, + editArticle, + ); + setEditLoading(false); + if (res.success) { + showToast({ type: "success", message: t("Edit successful") }); + const getRes = await network.getCollection(collection.id); + if (getRes.success) { + onSaved(getRes.data!); + } else { + onSaved({ ...collection, title: editTitle, article: editArticle }); + } + } else { + showToast({ + type: "error", + message: res.message || t("Failed to save changes"), + }); + } + }; + + if (!open) return null; + + return ( +
+
+

{t("Edit Collection")}

+ + setEditTitle(e.target.value)} + disabled={editLoading} + /> + +