feat: add TagsPage component and integrate it into the main application
This commit is contained in:
parent
1cfaf70803
commit
067d8f2a6a
2 changed files with 160 additions and 2 deletions
|
|
@ -4,6 +4,7 @@ import { getSeed, getTrips, getNextTripId } from "./api";
|
||||||
import ItemsPage from "./pages/ItemsPage";
|
import ItemsPage from "./pages/ItemsPage";
|
||||||
import TripChecklist from "./pages/TripChecklist";
|
import TripChecklist from "./pages/TripChecklist";
|
||||||
import TripsPage from "./pages/TripsPage";
|
import TripsPage from "./pages/TripsPage";
|
||||||
|
import TagsPage from "./pages/TagsPage";
|
||||||
|
|
||||||
function NextTripRedirect({ trips }: { trips: any[] }) {
|
function NextTripRedirect({ trips }: { trips: any[] }) {
|
||||||
const [nextTripId, setNextTripId] = React.useState<string | null>(null);
|
const [nextTripId, setNextTripId] = React.useState<string | null>(null);
|
||||||
|
|
@ -55,12 +56,17 @@ export default function App() {
|
||||||
</button>
|
</button>
|
||||||
<Link to="/trips">
|
<Link to="/trips">
|
||||||
<button className="bg-blue-500 text-white px-4 py-2 rounded">
|
<button className="bg-blue-500 text-white px-4 py-2 rounded">
|
||||||
Alle Trips
|
Trips
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/items">
|
<Link to="/items">
|
||||||
<button className="bg-green-500 text-white px-4 py-2 rounded">
|
<button className="bg-green-500 text-white px-4 py-2 rounded">
|
||||||
Alle Items
|
Items
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
<Link to="/tags">
|
||||||
|
<button className="bg-purple-500 text-white px-4 py-2 rounded">
|
||||||
|
Tags
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
<button
|
||||||
|
|
@ -78,6 +84,7 @@ export default function App() {
|
||||||
<Route path="/trips" element={<TripsPage />} />
|
<Route path="/trips" element={<TripsPage />} />
|
||||||
<Route path="/trips/:id" element={<TripChecklist trips={trips} />} />
|
<Route path="/trips/:id" element={<TripChecklist trips={trips} />} />
|
||||||
<Route path="/items" element={<ItemsPage />} />
|
<Route path="/items" element={<ItemsPage />} />
|
||||||
|
<Route path="/tags" element={<TagsPage />} />
|
||||||
<Route path="/" element={<NextTripRedirect trips={trips} />} />
|
<Route path="/" element={<NextTripRedirect trips={trips} />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
151
frontend/src/pages/TagsPage.tsx
Normal file
151
frontend/src/pages/TagsPage.tsx
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { getTags } from "../api";
|
||||||
|
import type { Tag } from "../api";
|
||||||
|
|
||||||
|
const API_BASE = "http://localhost:8000";
|
||||||
|
|
||||||
|
export default function TagsPage() {
|
||||||
|
const [tags, setTags] = useState<Tag[]>([]);
|
||||||
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
|
const [editName, setEditName] = useState("");
|
||||||
|
const [editMandatory, setEditMandatory] = useState(false);
|
||||||
|
const [newName, setNewName] = useState("");
|
||||||
|
const [newMandatory, setNewMandatory] = useState(false);
|
||||||
|
|
||||||
|
async function loadTags() {
|
||||||
|
setTags(await getTags());
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadTags();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function handleSave(id: string) {
|
||||||
|
await fetch(`${API_BASE}/tags/${id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ name: editName, mandatory: editMandatory }),
|
||||||
|
});
|
||||||
|
setEditingId(null);
|
||||||
|
await loadTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(id: string) {
|
||||||
|
if (!window.confirm("Tag wirklich löschen?")) return;
|
||||||
|
await fetch(`${API_BASE}/tags/${id}`, { method: "DELETE" });
|
||||||
|
await loadTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCreate(e: React.FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
await fetch(`${API_BASE}/tags/`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ name: newName, mandatory: newMandatory }),
|
||||||
|
});
|
||||||
|
setNewName("");
|
||||||
|
setNewMandatory(false);
|
||||||
|
await loadTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 max-w-xl mx-auto">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">Alle Tags</h1>
|
||||||
|
<form className="flex gap-2 mb-6" onSubmit={handleCreate}>
|
||||||
|
<input
|
||||||
|
className="border rounded px-2 py-1 flex-1"
|
||||||
|
placeholder="Neuer Tag..."
|
||||||
|
value={newName}
|
||||||
|
onChange={e => setNewName(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<label className="flex items-center gap-1 text-sm">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={newMandatory}
|
||||||
|
onChange={e => setNewMandatory(e.target.checked)}
|
||||||
|
/>
|
||||||
|
mandatory
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
className="bg-green-500 text-white px-3 py-1 rounded"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Hinzufügen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<ul>
|
||||||
|
{tags.map(tag =>
|
||||||
|
editingId === tag.id ? (
|
||||||
|
<li key={tag.id} className="flex gap-2 items-center mb-2">
|
||||||
|
<input
|
||||||
|
className="border rounded px-2 py-1 flex-1"
|
||||||
|
value={editName}
|
||||||
|
onChange={e => setEditName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<label className="flex items-center gap-1 text-sm">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={editMandatory}
|
||||||
|
onChange={e => setEditMandatory(e.target.checked)}
|
||||||
|
/>
|
||||||
|
mandatory
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
className="bg-blue-500 text-white px-2 py-1 rounded"
|
||||||
|
onClick={() => handleSave(tag.id)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="bg-gray-300 px-2 py-1 rounded"
|
||||||
|
onClick={() => setEditingId(null)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
) : (
|
||||||
|
<li
|
||||||
|
key={tag.id}
|
||||||
|
className="flex gap-2 items-center mb-2 group"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
"px-2 py-0.5 rounded text-sm " +
|
||||||
|
(tag.mandatory
|
||||||
|
? "border-2 border-yellow-400 bg-yellow-50 font-bold"
|
||||||
|
: "bg-gray-100")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
#{tag.name}
|
||||||
|
{tag.mandatory && (
|
||||||
|
<span className="ml-1 text-yellow-500 font-bold">*</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
className="text-xs text-blue-500 underline"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingId(tag.id);
|
||||||
|
setEditName(tag.name);
|
||||||
|
setEditMandatory(tag.mandatory);
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Bearbeiten
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="text-xs text-red-500 underline opacity-0 group-hover:opacity-100"
|
||||||
|
onClick={() => handleDelete(tag.id)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue