Fix tags was not committed
This commit is contained in:
243
Client/app/routes/tags.tsx
Normal file
243
Client/app/routes/tags.tsx
Normal file
@@ -0,0 +1,243 @@
|
||||
import RootLayout from "~/layout/RootLayout";
|
||||
import type { Route } from "../+types/root";
|
||||
import type { PaginatedResponse } from "~/types/PaginatedResponse";
|
||||
import { TagType, type Tag } from "~/types/tag";
|
||||
import { useEffect, useState, type FormEventHandler } from "react";
|
||||
import { API } from "~/api/api";
|
||||
import Icon, { ICONS } from "~/components/Icon";
|
||||
import Loading from "~/components/Loading";
|
||||
import { useAuth } from "~/context/AuthContext";
|
||||
import Modal from "~/components/Modal";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "ImgBoard - Tag List" },
|
||||
];
|
||||
}
|
||||
|
||||
export default function Tags() {
|
||||
|
||||
const { user } = useAuth();
|
||||
|
||||
const [paginatedData, setTags] = useState<PaginatedResponse<Tag> | null>(null);
|
||||
const [isEditTagModalOpen, setIsEditTagModalOpen] = useState<boolean>(false);
|
||||
const [isCreateTagModalOpen, setIsCreateTagModalOpen] = useState<boolean>(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
const [tagToEdit, setTagToEdit] = useState<Tag|null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
const tags = await API.fetchTags(currentPage);
|
||||
setTags(tags);
|
||||
}
|
||||
fetchData();
|
||||
}, [currentPage]);
|
||||
|
||||
if (!paginatedData) {
|
||||
return <RootLayout>
|
||||
<Loading />
|
||||
</RootLayout>;
|
||||
}
|
||||
|
||||
const Pagination = ({
|
||||
itemsPerPage,
|
||||
totalItems,
|
||||
paginate,
|
||||
currentPage,
|
||||
}: {
|
||||
itemsPerPage: number;
|
||||
totalItems: number;
|
||||
paginate: (pageNumber: number) => void;
|
||||
currentPage: number;
|
||||
}) => {
|
||||
const pageNumbers = [];
|
||||
for (let i = 1; i <= Math.ceil(totalItems / itemsPerPage); i++) { pageNumbers.push(i); }
|
||||
if(pageNumbers.length <= 1) return null;
|
||||
return (
|
||||
<nav className="mt-6 flex justify-center">
|
||||
<ul className="flex items-center space-x-2">
|
||||
{pageNumbers.map(number => (
|
||||
<li key={number}>
|
||||
<button onClick={() => paginate(number)} className={`px-4 py-2 rounded-md transition-colors ${ currentPage === number ? 'bg-blue-500 text-white' : 'bg-white text-gray-700 hover:bg-gray-100'}`}>
|
||||
{number}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
const CreateTagModal = ({ isOpen, onClose, onSave }: { isOpen: boolean; onClose: () => void; onSave: (name: string, type: TagType) => void }) => {
|
||||
const [newTagName, setNewTagName] = useState("");
|
||||
const [newTagType, setNewTagType] = useState<TagType>(TagType.General);
|
||||
|
||||
const handleSubmit: FormEventHandler = (e) => {
|
||||
e.preventDefault();
|
||||
if (newTagName) {
|
||||
onSave(newTagName, newTagType);
|
||||
setNewTagName("");
|
||||
setNewTagType(TagType.General);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<h2 className="text-2xl font-bold mb-4">Create Tag</h2>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="new-tag-name">Tag Name</label>
|
||||
<input id="new-tag-name" type="text" value={newTagName} onChange={(e) => setNewTagName(e.target.value)} className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" required placeholder="e.g., game" />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="new-tag-type">Tag Type</label>
|
||||
<select id="new-tag-type" value={newTagType} onChange={(e) => setNewTagType(e.target.value as TagType)} className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
||||
{Object.values(TagType).map(type => (
|
||||
<option key={type} value={type}>{type}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center justify-end">
|
||||
<button type="submit" className="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">Create Tag</button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const EditTagModal = ({ isOpen, onClose, tag, onSave }: { isOpen: boolean; onClose: () => void; tag: Tag | null; onSave: (tag: Tag, newType: TagType) => void }) => {
|
||||
const [newTagType, setNewTagType] = useState<TagType>(tag?.type || TagType.General);
|
||||
|
||||
useEffect(() => {
|
||||
if (tag) {
|
||||
setNewTagType(tag.type);
|
||||
}
|
||||
}, [tag]);
|
||||
|
||||
const handleSubmit: FormEventHandler = (e) => {
|
||||
e.preventDefault();
|
||||
if (tag) {
|
||||
onSave(tag, newTagType);
|
||||
}
|
||||
};
|
||||
|
||||
if (!tag) return null;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<h2 className="text-2xl font-bold mb-4">Edit Tag</h2>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2">Tag Name</label>
|
||||
<p className="text-gray-800 font-semibold text-lg">{tag.name}</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="tag-type">Tag Type</label>
|
||||
<select id="tag-type" value={newTagType} onChange={(e) => setNewTagType(e.target.value as TagType)} className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
||||
{Object.values(TagType).map(type => (
|
||||
<option key={type} value={type}>{type}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center justify-end">
|
||||
<button type="submit" className="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
function onEditTag(tag: Tag) {
|
||||
setTagToEdit(tag);
|
||||
setIsEditTagModalOpen(true);
|
||||
}
|
||||
|
||||
async function onUpdateTag(tag: Tag, newType: TagType) {
|
||||
if (!user || newType === tag.type) {
|
||||
setIsEditTagModalOpen(false);
|
||||
return;
|
||||
};
|
||||
const res = await API.updateTag(tag.name, newType, user);
|
||||
if (!res.ok) {
|
||||
alert(`Failed to update tag: ${await res.text()}`);
|
||||
return;
|
||||
}
|
||||
const updatedTag = await res.json();
|
||||
setTags(prev => prev ? { ...prev, items: prev.items.map(t => t.name === updatedTag.name ? updatedTag : t) } : null);
|
||||
setIsEditTagModalOpen(false);
|
||||
}
|
||||
|
||||
async function onCreateTag(name: string, type: TagType) {
|
||||
if (!user) return;
|
||||
const res = await API.createTag(name, type, user);
|
||||
if (!res.ok) {
|
||||
alert(`Failed to create tag: ${await res.text()}`);
|
||||
return;
|
||||
}
|
||||
const newTag = await res.json();
|
||||
// Reset to first page to see the new tag
|
||||
if (currentPage !== 1) {
|
||||
setCurrentPage(1);
|
||||
} else {
|
||||
setTags(prev => prev ? { ...prev, items: [newTag, ...prev.items] } : { items: [newTag], totalCount: 1, currentPage: 1, pageSize: 20 });
|
||||
}
|
||||
setIsCreateTagModalOpen(false);
|
||||
}
|
||||
|
||||
async function onDeleteTag(tag: Tag) {
|
||||
if (!user) return
|
||||
const res = await API.deleteTag(tag.name, user);
|
||||
if (!res.ok) {
|
||||
alert("Failed to delete tag");
|
||||
return;
|
||||
}
|
||||
setTags(prev => {
|
||||
if (!prev) return null;
|
||||
const newItems = prev.items.filter(t => t.name !== tag.name);
|
||||
// If the page is now empty and it's not the first page, go to the previous page
|
||||
if (newItems.length === 0 && currentPage > 1) {
|
||||
setCurrentPage(currentPage - 1);
|
||||
}
|
||||
return { ...prev, items: newItems, total: prev.totalCount - 1 };
|
||||
});
|
||||
}
|
||||
|
||||
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
|
||||
|
||||
return (
|
||||
<RootLayout>
|
||||
<div className="p-4 md:p-6 max-w-4xl mx-auto">
|
||||
<div className="bg-white rounded-xl shadow-lg p-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-3xl font-bold text-gray-800">Tag List</h2>
|
||||
{user && user.isAdmin && (
|
||||
<button onClick={() => setIsCreateTagModalOpen(true)} className="bg-blue-500 text-white font-semibold px-4 py-2 rounded-md hover:bg-blue-600 transition-colors flex items-center space-x-2">
|
||||
<Icon path={ICONS.add} className="w-5 h-5"/>
|
||||
<span>Create Tag</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{paginatedData.items.map(tag => (
|
||||
<div key={tag.name} className="flex items-center justify-between bg-gray-50 p-3 rounded-lg">
|
||||
<span className="font-semibold text-gray-700">{tag.name}</span>
|
||||
{user && user.isAdmin && <div className="space-x-2">
|
||||
<button onClick={() => onEditTag(tag)} className="bg-yellow-500 text-white font-semibold px-3 py-1 rounded-md hover:bg-yellow-600 transition-colors flex items-center space-x-1">
|
||||
<Icon path={ICONS.edit} className="w-4 h-4"/><span>Edit</span>
|
||||
</button>
|
||||
<button onClick={() => onDeleteTag(tag)} className="bg-red-500 text-white font-semibold px-3 py-1 rounded-md hover:bg-red-600 transition-colors flex items-center space-x-1">
|
||||
<Icon path={ICONS.delete} className="w-4 h-4"/><span>Delete</span>
|
||||
</button>
|
||||
</div>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Pagination itemsPerPage={paginatedData.pageSize} totalItems={paginatedData.totalCount} paginate={paginate} currentPage={currentPage} />
|
||||
</div>
|
||||
</div>
|
||||
<CreateTagModal isOpen={isCreateTagModalOpen} onClose={() => setIsCreateTagModalOpen(false)} onSave={onCreateTag} />
|
||||
<EditTagModal isOpen={isEditTagModalOpen} onClose={() => setIsEditTagModalOpen(false)} tag={tagToEdit} onSave={onUpdateTag} />
|
||||
</RootLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user