feat: enhance ItemsPage and components with improved item management and tagging functionality
This commit is contained in:
parent
56de7bb167
commit
4e451d751b
6 changed files with 401 additions and 217 deletions
|
|
@ -1,21 +1,102 @@
|
|||
import { BrowserRouter as Router, Routes, Route, Link } from "react-router-dom";
|
||||
import { ItemsPage } from "./pages/ItemsPage";
|
||||
import { TripsPage } from "./pages/TripsPage"; // das ist dein bisheriger Inhalt
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { getSeed, getTrips, getTripItems, toggleTripItem } from "./api";
|
||||
import ItemsPage from "./pages/ItemsPage";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Router>
|
||||
<div className="p-4 max-w-2xl mx-auto">
|
||||
<nav className="flex gap-4 mb-6">
|
||||
<Link to="/" className="text-blue-500 underline">Trips</Link>
|
||||
<Link to="/items" className="text-blue-500 underline">Items</Link>
|
||||
</nav>
|
||||
const [view, setView] = useState<"trips" | "items">("trips");
|
||||
|
||||
<Routes>
|
||||
<Route path="/" element={<TripsPage />} />
|
||||
<Route path="/items" element={<ItemsPage />} />
|
||||
</Routes>
|
||||
const [trips, setTrips] = useState<any[]>([]);
|
||||
const [items, setItems] = useState<Record<string, any[]>>({});
|
||||
|
||||
async function loadTrips() {
|
||||
const data = await getTrips();
|
||||
setTrips(data);
|
||||
}
|
||||
|
||||
async function loadItems(tripId: string) {
|
||||
const data = await getTripItems(tripId);
|
||||
setItems((prev) => ({ ...prev, [tripId]: data }));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (view === "trips") {
|
||||
loadTrips();
|
||||
}
|
||||
}, [view]);
|
||||
|
||||
if (view === "items") {
|
||||
return (
|
||||
<div className="p-4 max-w-4xl mx-auto">
|
||||
<div className="mb-4">
|
||||
<button
|
||||
onClick={() => setView("trips")}
|
||||
className="bg-gray-500 text-white px-3 py-1 rounded"
|
||||
>
|
||||
← Zurück zu Trips
|
||||
</button>
|
||||
</div>
|
||||
<ItemsPage />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 max-w-2xl mx-auto">
|
||||
<h1 className="text-2xl font-bold mb-4">Packlist</h1>
|
||||
|
||||
<div className="flex gap-2 mb-4">
|
||||
<button
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded"
|
||||
onClick={async () => {
|
||||
await getSeed();
|
||||
await loadTrips();
|
||||
}}
|
||||
>
|
||||
Seed-Daten erzeugen
|
||||
</button>
|
||||
<button
|
||||
className="bg-green-500 text-white px-4 py-2 rounded"
|
||||
onClick={() => setView("items")}
|
||||
>
|
||||
Alle Items
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{trips.map((trip) => (
|
||||
<div key={trip.id} className="border rounded p-2 mb-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h2 className="font-bold">{trip.name}</h2>
|
||||
<p>
|
||||
{trip.start_date} – {trip.end_date}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
className="text-sm text-blue-500 underline"
|
||||
onClick={() => loadItems(trip.id)}
|
||||
>
|
||||
Packliste anzeigen
|
||||
</button>
|
||||
</div>
|
||||
{items[trip.id] && (
|
||||
<ul className="mt-2">
|
||||
{items[trip.id].map((item) => (
|
||||
<li key={item.id} className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={item.checked}
|
||||
onChange={async () => {
|
||||
await toggleTripItem(item.id);
|
||||
await loadItems(trip.id);
|
||||
}}
|
||||
/>
|
||||
<span>{item.name_calculated}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,15 @@
|
|||
export const API_BASE = 'http://localhost:8000';
|
||||
export interface Tag {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Item {
|
||||
id: string;
|
||||
name: string;
|
||||
tags: Tag[];
|
||||
}
|
||||
|
||||
const API_BASE = "http://localhost:8000"; // ggf. anpassen
|
||||
|
||||
export async function getSeed() {
|
||||
return fetch(`${API_BASE}/dev/seed`).then(res => res.json());
|
||||
|
|
@ -17,49 +28,52 @@ export async function toggleTripItem(tripItemId: string) {
|
|||
}
|
||||
|
||||
|
||||
export interface Tag {
|
||||
id: string;
|
||||
name: string;
|
||||
export async function getItems(): Promise<Item[]> {
|
||||
const res = await fetch(`${API_BASE}/items/`);
|
||||
if (!res.ok) throw new Error("Failed to fetch items");
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export interface Item {
|
||||
id: string;
|
||||
name: string;
|
||||
tags: Tag[];
|
||||
export async function getTags(): Promise<Tag[]> {
|
||||
const res = await fetch(`${API_BASE}/tags/`);
|
||||
if (!res.ok) throw new Error("Failed to fetch tags");
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export const api = {
|
||||
async getItems(): Promise<Item[]> {
|
||||
// später durch echten fetch ersetzen
|
||||
return [
|
||||
{ id: "1", name: "Ladekabel Mac", tags: [{ id: "t1", name: "kristin" }] },
|
||||
{
|
||||
id: "2",
|
||||
name: "{nights} Unterhosen",
|
||||
tags: [
|
||||
{ id: "t2", name: "felix" },
|
||||
{ id: "t1", name: "kristin" },
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
async getTags(): Promise<Tag[]> {
|
||||
return [
|
||||
{ id: "t1", name: "kristin" },
|
||||
{ id: "t2", name: "felix" },
|
||||
{ id: "t3", name: "sommer" },
|
||||
];
|
||||
},
|
||||
async renameItem(id: string, newName: string) {
|
||||
console.log("rename", id, newName);
|
||||
},
|
||||
async removeTag(itemId: string, tagId: string) {
|
||||
console.log("removeTag", itemId, tagId);
|
||||
},
|
||||
async addTag(itemId: string, tagName: string) {
|
||||
console.log("addTag", itemId, tagName);
|
||||
},
|
||||
async addItem(name: string) {
|
||||
console.log("addItem", name);
|
||||
},
|
||||
};
|
||||
export async function updateItemName(itemId: string, name: string): Promise<Item> {
|
||||
const res = await fetch(`${API_BASE}/items/${itemId}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to update item");
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function deleteItemTag(itemId: string, tagId: string): Promise<Item> {
|
||||
const res = await fetch(`${API_BASE}/items/${itemId}/tags/${tagId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to delete tag from item");
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function addItemTag(itemId: string, tagName: string): Promise<Item> {
|
||||
const res = await fetch(`${API_BASE}/items/${itemId}/tags`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: tagName }),
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to add tag to item");
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function createItem(name: string, tags: string[]): Promise<Item> {
|
||||
const res = await fetch(`${API_BASE}/items/`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name, tags }),
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to create item");
|
||||
return res.json();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,38 @@
|
|||
import { Item } from "../api";
|
||||
import { ItemRow } from "./ItemRow";
|
||||
import React from "react";
|
||||
import { Item, Tag } from "../api";
|
||||
import ItemRow from "./ItemRow";
|
||||
|
||||
interface Props {
|
||||
interface ItemListProps {
|
||||
items: Item[];
|
||||
onRename: (id: string, newName: string) => void;
|
||||
onRemoveTag: (itemId: string, tagId: string) => void;
|
||||
onAddTag: (itemId: string, tagName: string) => void;
|
||||
allTags: Tag[];
|
||||
onUpdateName: (id: string, name: string) => void;
|
||||
onDeleteTag: (itemId: string, tagId: string) => void;
|
||||
onAddTag: (itemId: string, tagId: string) => void;
|
||||
}
|
||||
|
||||
export default function ItemList({
|
||||
items,
|
||||
allTags,
|
||||
onUpdateName,
|
||||
onDeleteTag,
|
||||
onAddTag,
|
||||
}: ItemListProps) {
|
||||
if (items.length === 0) {
|
||||
return <p className="text-gray-500 italic">Keine Items gefunden.</p>;
|
||||
}
|
||||
|
||||
export function ItemList({ items, onRename, onRemoveTag, onAddTag }: Props) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{items.map(item => (
|
||||
<ul className="divide-y">
|
||||
{items.map((item) => (
|
||||
<ItemRow
|
||||
key={item.id}
|
||||
item={item}
|
||||
onRename={onRename}
|
||||
onRemoveTag={onRemoveTag}
|
||||
allTags={allTags}
|
||||
onUpdateName={onUpdateName}
|
||||
onDeleteTag={onDeleteTag}
|
||||
onAddTag={onAddTag}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,79 +1,134 @@
|
|||
import { useState } from "react";
|
||||
import { Item } from "../api";
|
||||
import React, { useState } from "react";
|
||||
import { Item, Tag } from "../api";
|
||||
|
||||
interface Props {
|
||||
interface ItemRowProps {
|
||||
item: Item;
|
||||
onRename: (id: string, newName: string) => void;
|
||||
onRemoveTag: (itemId: string, tagId: string) => void;
|
||||
onAddTag: (itemId: string, tagName: string) => void;
|
||||
allTags: Tag[];
|
||||
onUpdateName: (id: string, name: string) => void;
|
||||
onDeleteTag: (itemId: string, tagId: string) => void;
|
||||
onAddTag: (itemId: string, tagId: string) => void;
|
||||
}
|
||||
|
||||
export function ItemRow({ item, onRename, onRemoveTag, onAddTag }: Props) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [newName, setNewName] = useState(item.name);
|
||||
export default function ItemRow({
|
||||
item,
|
||||
allTags,
|
||||
onUpdateName,
|
||||
onDeleteTag,
|
||||
onAddTag,
|
||||
}: ItemRowProps) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editName, setEditName] = useState(item.name);
|
||||
const [hover, setHover] = useState(false);
|
||||
const [addingTag, setAddingTag] = useState(false);
|
||||
const [newTag, setNewTag] = useState("");
|
||||
const [newTagInput, setNewTagInput] = useState("");
|
||||
|
||||
const filteredSuggestions = allTags.filter(
|
||||
(t) =>
|
||||
t.name.toLowerCase().includes(newTagInput.toLowerCase()) &&
|
||||
!item.tags.some((it) => it.id === t.id)
|
||||
);
|
||||
|
||||
function handleSave() {
|
||||
if (editName.trim() && editName !== item.name) {
|
||||
onUpdateName(item.id, editName.trim());
|
||||
}
|
||||
setIsEditing(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between p-2 hover:bg-gray-50 rounded">
|
||||
{/* Name */}
|
||||
<div className="flex-1">
|
||||
{editing ? (
|
||||
<input
|
||||
value={newName}
|
||||
onChange={e => setNewName(e.target.value)}
|
||||
onBlur={() => {
|
||||
setEditing(false);
|
||||
if (newName !== item.name) onRename(item.id, newName);
|
||||
<li
|
||||
className="flex flex-wrap items-center gap-2 py-1 border-b"
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => {
|
||||
setHover(false);
|
||||
setAddingTag(false);
|
||||
setNewTagInput("");
|
||||
}}
|
||||
>
|
||||
{isEditing ? (
|
||||
<input
|
||||
className="border rounded px-1 py-0.5"
|
||||
value={editName}
|
||||
onChange={(e) => setEditName(e.target.value)}
|
||||
onBlur={handleSave}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleSave();
|
||||
if (e.key === "Escape") {
|
||||
setEditName(item.name);
|
||||
setIsEditing(false);
|
||||
}
|
||||
}}
|
||||
className="border-b border-gray-400 outline-none"
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<span onClick={() => setEditing(true)} className="cursor-pointer">
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
{item.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
<div className="flex gap-2 items-center">
|
||||
{item.tags.map(tag => (
|
||||
{item.tags.map((tag) => (
|
||||
<span
|
||||
key={tag.id}
|
||||
className="bg-gray-200 px-2 py-1 rounded-full text-sm relative group"
|
||||
className="bg-gray-200 text-sm rounded px-1 py-0.5 flex items-center"
|
||||
>
|
||||
#{tag.name}
|
||||
{hover && (
|
||||
<button
|
||||
onClick={() => onRemoveTag(item.id, tag.id)}
|
||||
className="ml-1 text-xs text-red-600 opacity-0 group-hover:opacity-100"
|
||||
onClick={() => onDeleteTag(item.id, tag.id)}
|
||||
className="ml-1 text-xs text-red-500 hover:text-red-700"
|
||||
>
|
||||
✕
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
|
||||
{addingTag ? (
|
||||
<input
|
||||
value={newTag}
|
||||
onChange={e => setNewTag(e.target.value)}
|
||||
onBlur={() => {
|
||||
if (newTag.trim()) onAddTag(item.id, newTag.trim());
|
||||
setAddingTag(false);
|
||||
setNewTag("");
|
||||
}}
|
||||
className="border-b w-20 outline-none"
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
{hover && !addingTag && (
|
||||
<button
|
||||
className="text-xs text-blue-500 hover:underline"
|
||||
onClick={() => setAddingTag(true)}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
+
|
||||
+ Tag
|
||||
</button>
|
||||
)}
|
||||
|
||||
{addingTag && (
|
||||
<div className="relative">
|
||||
<input
|
||||
className="border rounded px-1 py-0.5"
|
||||
placeholder="Tag..."
|
||||
value={newTagInput}
|
||||
onChange={(e) => setNewTagInput(e.target.value)}
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") {
|
||||
setAddingTag(false);
|
||||
setNewTagInput("");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{newTagInput && filteredSuggestions.length > 0 && (
|
||||
<ul className="absolute bg-white border rounded mt-1 shadow-md z-10">
|
||||
{filteredSuggestions.map((tag) => (
|
||||
<li
|
||||
key={tag.id}
|
||||
className="px-2 py-1 hover:bg-gray-100 cursor-pointer"
|
||||
onMouseDown={() => {
|
||||
onAddTag(item.id, tag.id);
|
||||
setAddingTag(false);
|
||||
setNewTagInput("");
|
||||
}}
|
||||
>
|
||||
#{tag.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,31 @@
|
|||
import React from "react";
|
||||
import { Tag } from "../api";
|
||||
|
||||
interface Props {
|
||||
interface TagFilterProps {
|
||||
tags: Tag[];
|
||||
selected: string[];
|
||||
onToggle: (tag: string) => void;
|
||||
onToggle: (tagId: string) => void;
|
||||
}
|
||||
|
||||
export function TagFilter({ tags, selected, onToggle }: Props) {
|
||||
export default function TagFilter({ tags, selected, onToggle }: TagFilterProps) {
|
||||
return (
|
||||
<div className="mb-4 flex flex-wrap gap-2">
|
||||
{tags.map((tag) => {
|
||||
const isSelected = selected.includes(tag.id);
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{tags.map(tag => (
|
||||
<button
|
||||
key={tag.id}
|
||||
onClick={() => onToggle(tag.name)}
|
||||
className={`px-2 py-1 rounded-full text-sm ${
|
||||
selected.includes(tag.name)
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-gray-200 hover:bg-gray-300"
|
||||
onClick={() => onToggle(tag.id)}
|
||||
className={`px-2 py-1 rounded text-sm border ${
|
||||
isSelected
|
||||
? "bg-blue-500 text-white border-blue-600"
|
||||
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
|
||||
}`}
|
||||
>
|
||||
#{tag.name}
|
||||
</button>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,92 +1,109 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { SearchBar } from "../components/SearchBar";
|
||||
import { TagFilter } from "../components/TagFilter";
|
||||
import { ItemList } from "../components/ItemList";
|
||||
import { api, Item, Tag } from "../api";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
getItems,
|
||||
getTags,
|
||||
createItem,
|
||||
updateItemName,
|
||||
deleteItemTag,
|
||||
addItemTag,
|
||||
Item,
|
||||
Tag,
|
||||
} from "../api";
|
||||
import ItemList from "../components/ItemList";
|
||||
import TagFilter from "../components/TagFilter";
|
||||
|
||||
export function ItemsPage() {
|
||||
export default function ItemsPage() {
|
||||
const [items, setItems] = useState<Item[]>([]);
|
||||
const [tags, setTags] = useState<Tag[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [filterText, setFilterText] = useState("");
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function loadData() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [itemsData, tagsData] = await Promise.all([getItems(), getTags()]);
|
||||
setItems(itemsData);
|
||||
setTags(tagsData);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
api.getItems().then(setItems);
|
||||
api.getTags().then(setTags);
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const handleRename = (id: string, newName: string) => {
|
||||
setItems(prev =>
|
||||
prev.map(item => (item.id === id ? { ...item, name: newName } : item))
|
||||
function handleTagToggle(tagId: string) {
|
||||
setSelectedTags((prev) =>
|
||||
prev.includes(tagId) ? prev.filter((id) => id !== tagId) : [...prev, tagId]
|
||||
);
|
||||
api.renameItem(id, newName);
|
||||
};
|
||||
}
|
||||
|
||||
const handleRemoveTag = (itemId: string, tagId: string) => {
|
||||
setItems(prev =>
|
||||
prev.map(item =>
|
||||
item.id === itemId
|
||||
? { ...item, tags: item.tags.filter(t => t.id !== tagId) }
|
||||
: item
|
||||
)
|
||||
);
|
||||
api.removeTag(itemId, tagId);
|
||||
};
|
||||
async function handleAddItem(name: string, tagNames: string[]) {
|
||||
const newItem = await createItem(name, tagNames);
|
||||
setItems((prev) => [...prev, newItem]);
|
||||
}
|
||||
|
||||
const handleAddTag = (itemId: string, tagName: string) => {
|
||||
const newTag: Tag = { id: crypto.randomUUID(), name: tagName };
|
||||
setItems(prev =>
|
||||
prev.map(item =>
|
||||
item.id === itemId ? { ...item, tags: [...item.tags, newTag] } : item
|
||||
)
|
||||
);
|
||||
api.addTag(itemId, tagName);
|
||||
};
|
||||
async function handleRenameItem(itemId: string, name: string) {
|
||||
const updated = await updateItemName(itemId, name);
|
||||
setItems((prev) => prev.map((it) => (it.id === itemId ? updated : it)));
|
||||
}
|
||||
|
||||
const handleAddItem = (name: string) => {
|
||||
const newItem: Item = { id: crypto.randomUUID(), name, tags: [] };
|
||||
setItems(prev => [...prev, newItem]);
|
||||
api.addItem(name);
|
||||
};
|
||||
async function handleDeleteTag(itemId: string, tagId: string) {
|
||||
const updated = await deleteItemTag(itemId, tagId);
|
||||
setItems((prev) => prev.map((it) => (it.id === itemId ? updated : it)));
|
||||
}
|
||||
|
||||
// Filtering
|
||||
const filtered = items.filter(item => {
|
||||
const matchSearch = item.name
|
||||
.toLowerCase()
|
||||
.includes(searchQuery.toLowerCase());
|
||||
const matchTags =
|
||||
async function handleAddTag(itemId: string, tagName: string) {
|
||||
const updated = await addItemTag(itemId, tagName);
|
||||
setItems((prev) => prev.map((it) => (it.id === itemId ? updated : it)));
|
||||
}
|
||||
|
||||
// Filtern
|
||||
const filteredItems = items.filter((item) => {
|
||||
const matchesText =
|
||||
filterText === "" ||
|
||||
item.name.toLowerCase().includes(filterText.toLowerCase());
|
||||
|
||||
const matchesTags =
|
||||
selectedTags.length === 0 ||
|
||||
selectedTags.some(tag =>
|
||||
item.tags.map(t => t.name).includes(tag)
|
||||
);
|
||||
return matchSearch && matchTags;
|
||||
item.tags.some((t) => selectedTags.includes(t.id));
|
||||
|
||||
return matchesText && matchesTags;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<h1 className="text-2xl font-bold">Items</h1>
|
||||
<SearchBar value={searchQuery} onChange={setSearchQuery} />
|
||||
<div className="p-4 max-w-3xl mx-auto">
|
||||
<h1 className="text-2xl font-bold mb-4">Items</h1>
|
||||
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="🔍 Suchen..."
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
className="border rounded px-2 py-1 w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TagFilter
|
||||
tags={tags}
|
||||
selected={selectedTags}
|
||||
onToggle={tag =>
|
||||
setSelectedTags(prev =>
|
||||
prev.includes(tag) ? prev.filter(t => t !== tag) : [...prev, tag]
|
||||
)
|
||||
}
|
||||
onToggle={handleTagToggle}
|
||||
/>
|
||||
|
||||
{loading ? (
|
||||
<p>Lade...</p>
|
||||
) : (
|
||||
<ItemList
|
||||
items={filtered}
|
||||
onRename={handleRename}
|
||||
onRemoveTag={handleRemoveTag}
|
||||
items={filteredItems}
|
||||
allTags={tags}
|
||||
onUpdateName={handleRenameItem}
|
||||
onDeleteTag={handleDeleteTag}
|
||||
onAddTag={handleAddTag}
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleAddItem("Neues Item")}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded-lg"
|
||||
>
|
||||
+ Neues Item
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue