mirror of
https://github.com/wgh136/nysoure.git
synced 2025-09-27 12:17:24 +00:00
378 lines
11 KiB
TypeScript
378 lines
11 KiB
TypeScript
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, MdOutlineLock } from "react-icons/md";
|
|
import { app } from "../app";
|
|
import { useTranslation } from "../utils/i18n";
|
|
import Button from "../components/button";
|
|
import Badge from "../components/badge";
|
|
|
|
export default function CollectionPage() {
|
|
const { id } = useParams<{ id: string }>();
|
|
const navigate = useNavigate();
|
|
const [collection, setCollection] = useState<Collection | null>(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;
|
|
}
|
|
|
|
const prefetchData = app.getPreFetchData();
|
|
if (prefetchData?.collection?.id === idInt) {
|
|
setCollection(prefetchData.collection);
|
|
return;
|
|
}
|
|
|
|
network.getCollection(idInt).then((res) => {
|
|
if (res.success) {
|
|
setCollection(res.data!);
|
|
} else {
|
|
showToast({
|
|
type: "error",
|
|
message: res.message || "Failed to load collection",
|
|
});
|
|
}
|
|
});
|
|
}, [id]);
|
|
|
|
useEffect(() => {
|
|
if (!collection) return;
|
|
document.title = collection.title;
|
|
}, [collection]);
|
|
|
|
const toBeDeletedRID = useRef<number | null>(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 <Loading />;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="mx-4 mt-4 p-4 bg-base-100-tr82 shadow rounded-xl">
|
|
<h1 className="text-2xl font-bold">{collection?.title}</h1>
|
|
<article>
|
|
<CollectionContent content={collection?.article || ""} />
|
|
</article>
|
|
<div className="flex items-center flex-row-reverse">
|
|
{isOwner && (
|
|
<>
|
|
<button
|
|
className="btn btn-sm btn-ghost ml-2"
|
|
onClick={openEditDialog}
|
|
>
|
|
<MdOutlineEdit size={16} />
|
|
{t("Edit")}
|
|
</button>
|
|
<button
|
|
className="btn btn-sm btn-error btn-ghost ml-2"
|
|
onClick={handleDeleteCollection}
|
|
>
|
|
<MdOutlineDelete size={16} />
|
|
{t("Delete")}
|
|
</button>
|
|
</>
|
|
)}
|
|
<span className="flex-1" />
|
|
{!collection.isPublic && (
|
|
<Badge className="badge-soft badge-error text-xs mr-2 shadow-xs">
|
|
<MdOutlineLock size={16} className="inline-block" />{" "}
|
|
{t("Private")}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<ResourcesView
|
|
loader={(page) => {
|
|
return network.listCollectionResources(collection!.id, page);
|
|
}}
|
|
actionBuilder={
|
|
isOwner
|
|
? (r) => {
|
|
return (
|
|
<button
|
|
className="btn btn-sm btn-rounded btn-error btn-ghost"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
handleDeleteResource(r.id);
|
|
}}
|
|
>
|
|
<MdOutlineDelete size={16} />
|
|
</button>
|
|
);
|
|
}
|
|
: undefined
|
|
}
|
|
key={resourcesKey}
|
|
/>
|
|
<dialog id="deleteResourceDialog" className="modal">
|
|
<div className="modal-box">
|
|
<h2 className="font-bold text-lg">Remove Resource</h2>
|
|
<p>Are you sure you want to remove this resource?</p>
|
|
<div className="modal-action">
|
|
<Button
|
|
onClick={() => {
|
|
const dialog = document.getElementById(
|
|
"deleteResourceDialog",
|
|
) as HTMLDialogElement | null;
|
|
if (dialog) {
|
|
dialog.close();
|
|
}
|
|
}}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
className="btn-error"
|
|
onClick={handleDeletedResourceConfirmed}
|
|
>
|
|
Delete
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</dialog>
|
|
{deleteOpen && (
|
|
<div className="modal modal-open">
|
|
<div className="modal-box">
|
|
<h2 className="font-bold text-lg mb-2">{t("Delete Collection")}</h2>
|
|
<p>
|
|
{t(
|
|
"Are you sure you want to delete this collection? This action cannot be undone.",
|
|
)}
|
|
</p>
|
|
<div className="modal-action">
|
|
<Button className="btn" onClick={() => setDeleteOpen(false)}>
|
|
{t("Cancel")}
|
|
</Button>
|
|
<Button
|
|
className="btn btn-error"
|
|
onClick={handleDeleteCollectionConfirmed}
|
|
isLoading={isDeleting}
|
|
>
|
|
{t("Delete")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{editOpen && collection && (
|
|
<EditCollectionDialog
|
|
open={editOpen}
|
|
collection={collection}
|
|
onClose={() => 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 <Markdown>{content}</Markdown>;
|
|
}
|
|
|
|
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 [editIsPublic, setEditIsPublic] = useState(collection.isPublic);
|
|
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,
|
|
editIsPublic,
|
|
);
|
|
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,
|
|
isPublic: editIsPublic,
|
|
});
|
|
}
|
|
} else {
|
|
showToast({
|
|
type: "error",
|
|
message: res.message || t("Failed to save changes"),
|
|
});
|
|
}
|
|
};
|
|
|
|
if (!open) return null;
|
|
|
|
return (
|
|
<div className="modal modal-open">
|
|
<div className="modal-box">
|
|
<h2 className="font-bold text-lg mb-2">{t("Edit Collection")}</h2>
|
|
<label className="block mb-1">{t("Title")}</label>
|
|
<input
|
|
className="input w-full mb-2"
|
|
value={editTitle}
|
|
onChange={(e) => setEditTitle(e.target.value)}
|
|
disabled={editLoading}
|
|
/>
|
|
<label className="block mb-1">{t("Description")}</label>
|
|
<textarea
|
|
className="textarea w-full min-h-32 mb-2"
|
|
value={editArticle}
|
|
onChange={(e) => setEditArticle(e.target.value)}
|
|
disabled={editLoading}
|
|
/>
|
|
<label className="flex items-center mb-4 mt-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={!editIsPublic}
|
|
onChange={(e) => setEditIsPublic(!e.target.checked)}
|
|
className="checkbox mr-2"
|
|
disabled={editLoading}
|
|
/>
|
|
{t("Private")}
|
|
</label>
|
|
<div className="modal-action">
|
|
<button className="btn" onClick={onClose} disabled={editLoading}>
|
|
{t("Cancel")}
|
|
</button>
|
|
<button
|
|
className="btn btn-primary"
|
|
onClick={handleEditSave}
|
|
disabled={editLoading}
|
|
>
|
|
{editLoading ? (
|
|
<span className="loading loading-spinner loading-sm"></span>
|
|
) : (
|
|
t("Save")
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|