Fix tags was not committed

This commit is contained in:
2025-11-06 20:22:29 +02:00
parent a891a686fd
commit b22cc00647

243
Client/app/routes/tags.tsx Normal file
View 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>
);
}