feat: extract TripItemList to component and add TripEdit page
This commit is contained in:
parent
c7b720ba4a
commit
14905dd033
4 changed files with 379 additions and 166 deletions
|
|
@ -3,6 +3,7 @@ import { BrowserRouter as Router, Routes, Route, Link, useLocation, Navigate, us
|
|||
import { getSeed, getTrips, getNextTripId } from "./api";
|
||||
import ItemsPage from "./pages/ItemsPage";
|
||||
import TripChecklist from "./pages/TripChecklist";
|
||||
import TripEdit from "./pages/TripEdit";
|
||||
import TripsPage from "./pages/TripsPage";
|
||||
import TagsPage from "./pages/TagsPage";
|
||||
import logo from "./assets/logo.svg";
|
||||
|
|
@ -173,6 +174,7 @@ export default function App() {
|
|||
<Routes>
|
||||
<Route path="/trips" element={<TripsPage />} />
|
||||
<Route path="/trips/:id" element={<TripChecklist trips={trips} />} />
|
||||
<Route path="/trips/:id/edit" element={<TripEdit trips={trips} />} />
|
||||
<Route path="/items" element={<ItemsPage />} />
|
||||
<Route path="/tags" element={<TagsPage />} />
|
||||
<Route path="/" element={<NextTripRedirect trips={trips} />} />
|
||||
|
|
|
|||
172
frontend/src/components/TripItemList.tsx
Normal file
172
frontend/src/components/TripItemList.tsx
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
import React from "react";
|
||||
import { toggleTripItem, getTripItems } from "../api";
|
||||
|
||||
export function TripItemList({
|
||||
tripId,
|
||||
items,
|
||||
markedTags,
|
||||
reloadItems,
|
||||
}: {
|
||||
tripId: string;
|
||||
items: any[];
|
||||
markedTags: any[];
|
||||
reloadItems: () => Promise<void>;
|
||||
}) {
|
||||
// 1. Items ohne Tag
|
||||
const itemsWithoutTag = items.filter((item) => !item.tag);
|
||||
|
||||
// 2. Items mit Tag, gruppiert nach tag.id
|
||||
const itemsByTag: Record<string, { tag: any; items: any[] }> = {};
|
||||
items
|
||||
.filter((item) => item.tag)
|
||||
.forEach((item) => {
|
||||
const tagId = item.tag.id;
|
||||
if (!itemsByTag[tagId]) {
|
||||
itemsByTag[tagId] = { tag: item.tag, items: [] };
|
||||
}
|
||||
itemsByTag[tagId].items.push(item);
|
||||
});
|
||||
|
||||
// Nur markierte Tag-Gruppen
|
||||
const markedTagIds = new Set(markedTags.map((t: any) => t.id));
|
||||
const markedTagGroups = Object.values(itemsByTag)
|
||||
.filter((g) => markedTagIds.has(g.tag.id))
|
||||
.sort((a, b) => a.tag.name.localeCompare(b.tag.name));
|
||||
|
||||
function formatAsHeading(str: string) {
|
||||
if (!str) return "";
|
||||
// Replace hyphens with spaces and capitalize first letter
|
||||
const withSpaces = str.replace(/-/g, " ");
|
||||
return withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1);
|
||||
}
|
||||
|
||||
// Hilfsfunktion für Kategorie-Gruppierung
|
||||
function renderItemsWithCategories(items: any[]) {
|
||||
const slashCategoryMap: Record<string, any[]> = {};
|
||||
const normalItems: any[] = [];
|
||||
items.forEach((item) => {
|
||||
const name = item.name_calculated || "";
|
||||
if (name.includes("/")) {
|
||||
const [cat, sub] = name.split("/", 2);
|
||||
if (!slashCategoryMap[cat]) slashCategoryMap[cat] = [];
|
||||
slashCategoryMap[cat].push({ ...item, _sub: sub });
|
||||
} else {
|
||||
normalItems.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Haupt-Items als Grid */}
|
||||
{normalItems.length > 0 && (
|
||||
<ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-4 mb-4">
|
||||
{normalItems.map((item) => (
|
||||
<li
|
||||
key={item.id}
|
||||
className={
|
||||
"flex items-center gap-2 cursor-pointer select-none px-2 py-1 rounded group " +
|
||||
(item.checked ? "bg-green-100" : "hover:bg-gray-100")
|
||||
}
|
||||
onClick={async () => {
|
||||
await toggleTripItem(item.id);
|
||||
const updated = await getTripItems(id!);
|
||||
setItems(updated);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={item.checked}
|
||||
readOnly
|
||||
tabIndex={-1}
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
<span
|
||||
className={item.checked ? "line-through text-gray-400" : ""}
|
||||
title={
|
||||
item.item && item.item.tags && item.item.tags.length > 0
|
||||
? item.item.tags
|
||||
.map(
|
||||
(tag: any) =>
|
||||
`#${tag.name}${tag.mandatory ? " *" : ""}`
|
||||
)
|
||||
.join(" ")
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{item.name_calculated}
|
||||
</span>
|
||||
{/* Tags NICHT mehr direkt anzeigen */}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{/* Slash-Kategorien wie gehabt */}
|
||||
{Object.entries(slashCategoryMap).map(([cat, catItems]) => (
|
||||
<React.Fragment key={cat}>
|
||||
<li className="mt-2 mb-1 flex items-center gap-2">
|
||||
<h3 className="text-base font-semibold text-gray-600 m-0">
|
||||
{cat.charAt(0).toUpperCase() + cat.slice(1)}
|
||||
</h3>
|
||||
</li>
|
||||
<ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-4">
|
||||
{catItems.map((item) => (
|
||||
<li
|
||||
key={item.id}
|
||||
className={
|
||||
"flex items-center gap-2 cursor-pointer select-none px-2 py-1 rounded group " +
|
||||
(item.checked ? "bg-green-100" : "hover:bg-gray-100")
|
||||
}
|
||||
onClick={async () => {
|
||||
await toggleTripItem(item.id);
|
||||
const updated = await getTripItems(id!);
|
||||
setItems(updated);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={item.checked}
|
||||
readOnly
|
||||
tabIndex={-1}
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
<span
|
||||
className={item.checked ? "line-through text-gray-400" : ""}
|
||||
title={
|
||||
item.item && item.item.tags && item.item.tags.length > 0
|
||||
? item.item.tags
|
||||
.map(
|
||||
(tag: any) =>
|
||||
`#${tag.name}${tag.mandatory ? " *" : ""}`
|
||||
)
|
||||
.join(" ")
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{item._sub}
|
||||
</span>
|
||||
{/* Tags NICHT mehr direkt anzeigen */}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="grid grid-cols-1 gap-x-8 gap-y-2">
|
||||
{renderItemsWithCategories(itemsWithoutTag)}
|
||||
{markedTagGroups.map(({ tag, items }) => (
|
||||
<li key={tag.id} className="mt-6 mb-6 p-4 rounded-lg border-2 border-yellow-200 bg-yellow-50 shadow-sm">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<h2 className="text-lg font-bold text-yellow-700 m-0">{formatAsHeading(tag.name)}</h2>
|
||||
</div>
|
||||
<ul>
|
||||
{renderItemsWithCategories(items)}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import { getTripItems, toggleTripItem, updateTrip, getTags } from "../api";
|
|||
import TagAutocompleteInput from "../components/TagAutocompleteInput";
|
||||
import TripTag from "../components/Tag";
|
||||
import Tag from "../components/Tag";
|
||||
import { TripItemList } from "../components/TripItemList";
|
||||
|
||||
export default function TripChecklist({ trips }: { trips: any[] }) {
|
||||
const { id } = useParams();
|
||||
|
|
@ -94,148 +95,6 @@ export default function TripChecklist({ trips }: { trips: any[] }) {
|
|||
|
||||
if (!trip) return <div>Trip not found</div>;
|
||||
|
||||
// 1. Items ohne Tag
|
||||
const itemsWithoutTag = items.filter((item) => !item.tag);
|
||||
|
||||
// 2. Items mit Tag, gruppiert nach tag.id
|
||||
const itemsByTag: Record<string, { tag: any; items: any[] }> = {};
|
||||
items
|
||||
.filter((item) => item.tag)
|
||||
.forEach((item) => {
|
||||
const tagId = item.tag.id;
|
||||
if (!itemsByTag[tagId]) {
|
||||
itemsByTag[tagId] = { tag: item.tag, items: [] };
|
||||
}
|
||||
itemsByTag[tagId].items.push(item);
|
||||
});
|
||||
|
||||
// Nur markierte Tag-Gruppen
|
||||
const markedTagIds = new Set(markedTags.map((t: any) => t.id));
|
||||
const markedTagGroups = Object.values(itemsByTag)
|
||||
.filter((g) => markedTagIds.has(g.tag.id))
|
||||
.sort((a, b) => a.tag.name.localeCompare(b.tag.name));
|
||||
|
||||
// Hilfsfunktion für Kategorie-Gruppierung
|
||||
function renderItemsWithCategories(items: any[]) {
|
||||
const slashCategoryMap: Record<string, any[]> = {};
|
||||
const normalItems: any[] = [];
|
||||
items.forEach((item) => {
|
||||
const name = item.name_calculated || "";
|
||||
if (name.includes("/")) {
|
||||
const [cat, sub] = name.split("/", 2);
|
||||
if (!slashCategoryMap[cat]) slashCategoryMap[cat] = [];
|
||||
slashCategoryMap[cat].push({ ...item, _sub: sub });
|
||||
} else {
|
||||
normalItems.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Haupt-Items als Grid */}
|
||||
{normalItems.length > 0 && (
|
||||
<ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-4 mb-4">
|
||||
{normalItems.map((item) => (
|
||||
<li
|
||||
key={item.id}
|
||||
className={
|
||||
"flex items-center gap-2 cursor-pointer select-none px-2 py-1 rounded group " +
|
||||
(item.checked ? "bg-green-100" : "hover:bg-gray-100")
|
||||
}
|
||||
onClick={async () => {
|
||||
await toggleTripItem(item.id);
|
||||
const updated = await getTripItems(id!);
|
||||
setItems(updated);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={item.checked}
|
||||
readOnly
|
||||
tabIndex={-1}
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
<span
|
||||
className={item.checked ? "line-through text-gray-400" : ""}
|
||||
title={
|
||||
item.item && item.item.tags && item.item.tags.length > 0
|
||||
? item.item.tags
|
||||
.map(
|
||||
(tag: any) =>
|
||||
`#${tag.name}${tag.mandatory ? " *" : ""}`
|
||||
)
|
||||
.join(" ")
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{item.name_calculated}
|
||||
</span>
|
||||
{/* Tags NICHT mehr direkt anzeigen */}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{/* Slash-Kategorien wie gehabt */}
|
||||
{Object.entries(slashCategoryMap).map(([cat, catItems]) => (
|
||||
<React.Fragment key={cat}>
|
||||
<li className="mt-2 mb-1 flex items-center gap-2">
|
||||
<h3 className="text-base font-semibold text-gray-600 m-0">
|
||||
{cat.charAt(0).toUpperCase() + cat.slice(1)}
|
||||
</h3>
|
||||
</li>
|
||||
<ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-4">
|
||||
{catItems.map((item) => (
|
||||
<li
|
||||
key={item.id}
|
||||
className={
|
||||
"flex items-center gap-2 cursor-pointer select-none px-2 py-1 rounded group " +
|
||||
(item.checked ? "bg-green-100" : "hover:bg-gray-100")
|
||||
}
|
||||
onClick={async () => {
|
||||
await toggleTripItem(item.id);
|
||||
const updated = await getTripItems(id!);
|
||||
setItems(updated);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={item.checked}
|
||||
readOnly
|
||||
tabIndex={-1}
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
<span
|
||||
className={item.checked ? "line-through text-gray-400" : ""}
|
||||
title={
|
||||
item.item && item.item.tags && item.item.tags.length > 0
|
||||
? item.item.tags
|
||||
.map(
|
||||
(tag: any) =>
|
||||
`#${tag.name}${tag.mandatory ? " *" : ""}`
|
||||
)
|
||||
.join(" ")
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{item._sub}
|
||||
</span>
|
||||
{/* Tags NICHT mehr direkt anzeigen */}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function formatAsHeading(str: string) {
|
||||
if (!str) return "";
|
||||
// Replace hyphens with spaces and capitalize first letter
|
||||
const withSpaces = str.replace(/-/g, " ");
|
||||
return withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1);
|
||||
}
|
||||
|
||||
// Progress calculation
|
||||
const totalItems = items.length;
|
||||
const checkedItems = items.filter((item) => item.checked).length;
|
||||
|
|
@ -335,30 +194,16 @@ export default function TripChecklist({ trips }: { trips: any[] }) {
|
|||
</div>
|
||||
</div>
|
||||
{/* ...restliche Seite... */}
|
||||
<ul
|
||||
className="grid grid-cols-1 gap-x-8 gap-y-2"
|
||||
>
|
||||
{/* 1. Ohne Tag */}
|
||||
{renderItemsWithCategories(itemsWithoutTag)}
|
||||
|
||||
{/* 2. Markierte Tag-Gruppen */}
|
||||
{markedTagGroups.map(({ tag, items }) => (
|
||||
<li
|
||||
key={tag.id}
|
||||
className="mt-6 mb-6 p-4 rounded-lg border-2 border-yellow-200 bg-yellow-50 shadow-sm"
|
||||
style={{ listStyle: "none" }}
|
||||
>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<h2 className="text-lg font-bold text-yellow-700 m-0">
|
||||
{formatAsHeading(tag.name)}
|
||||
</h2>
|
||||
</div>
|
||||
<ul>
|
||||
{renderItemsWithCategories(items)}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<TripItemList
|
||||
tripId={trip.id}
|
||||
items={items}
|
||||
markedTags={markedTags}
|
||||
reloadItems={async () => {
|
||||
const updated = await getTripItems(id!);
|
||||
setItems(updated);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
194
frontend/src/pages/TripEdit.tsx
Normal file
194
frontend/src/pages/TripEdit.tsx
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { getTrips, updateTrip, getTags, getTripItems } from "../api";
|
||||
import TagAutocompleteInput from "../components/TagAutocompleteInput";
|
||||
import Tag from "../components/Tag";
|
||||
import { TripItemList } from "../components/TripItemList";
|
||||
|
||||
export default function TripEdit({ trips }: { trips: any[] }) {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const trip = trips.find((t) => t.id === id);
|
||||
|
||||
const [name, setName] = useState(trip?.name || "");
|
||||
const [startDate, setStartDate] = useState(trip?.start_date || "");
|
||||
const [endDate, setEndDate] = useState(trip?.end_date || "");
|
||||
const [selectedTags, setSelectedTags] = useState<any[]>(trip?.selected_tags || []);
|
||||
const [markedTags, setMarkedTags] = useState<any[]>(trip?.marked_tags || []);
|
||||
const [allTags, setAllTags] = useState<any[]>([]);
|
||||
const [addingTag, setAddingTag] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [items, setItems] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
getTags().then(setAllTags);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (trip?.id) {
|
||||
getTripItems(trip.id).then(setItems);
|
||||
}
|
||||
}, [trip]);
|
||||
|
||||
async function handleSave() {
|
||||
await updateTrip(trip.id, {
|
||||
name,
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
selected_tag_ids: selectedTags.map((t) => t.id),
|
||||
marked_tag_ids: markedTags.map((t) => t.id),
|
||||
});
|
||||
navigate(`/trips/${trip.id}`);
|
||||
}
|
||||
|
||||
async function handleToggleMark(tagId: string) {
|
||||
const isMarked = markedTags.some((t) => t.id === tagId);
|
||||
const newMarked = isMarked
|
||||
? markedTags.filter((t) => t.id !== tagId)
|
||||
: [...markedTags, selectedTags.find((t) => t.id === tagId)];
|
||||
setMarkedTags(newMarked);
|
||||
}
|
||||
|
||||
async function handleRemoveTag(tagId: string) {
|
||||
setSelectedTags(selectedTags.filter((t) => t.id !== tagId));
|
||||
setMarkedTags(markedTags.filter((t) => t.id !== tagId));
|
||||
}
|
||||
|
||||
async function handleAddTag(tagId: string) {
|
||||
if (selectedTags.some((t) => t.id === tagId)) return;
|
||||
const tagObj = allTags.find((t) => t.id === tagId);
|
||||
setSelectedTags([...selectedTags, tagObj]);
|
||||
}
|
||||
|
||||
const itemsWithoutTag = items.filter((item) => !item.tag);
|
||||
const itemsByTag: Record<string, { tag: any; items: any[] }> = {};
|
||||
items
|
||||
.filter((item) => item.tag)
|
||||
.forEach((item) => {
|
||||
const tagId = item.tag.id;
|
||||
if (!itemsByTag[tagId]) {
|
||||
itemsByTag[tagId] = { tag: item.tag, items: [] };
|
||||
}
|
||||
itemsByTag[tagId].items.push(item);
|
||||
});
|
||||
const markedTagIds = new Set(markedTags.map((t: any) => t.id));
|
||||
const markedTagGroups = Object.values(itemsByTag)
|
||||
.filter((g) => markedTagIds.has(g.tag.id))
|
||||
.sort((a, b) => a.tag.name.localeCompare(b.tag.name));
|
||||
|
||||
function renderItemsWithCategories(items: any[]) {
|
||||
return items.map((item) => (
|
||||
<div key={item.id} className="py-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-gray-700">{item.name}</div>
|
||||
<div className="text-xs text-gray-500">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
async function reloadItems() {
|
||||
if (trip?.id) {
|
||||
const updated = await getTripItems(trip.id);
|
||||
setItems(updated);
|
||||
}
|
||||
}
|
||||
|
||||
if (!trip) return <div>Trip not found</div>;
|
||||
|
||||
return (
|
||||
<div className="py-4 max-w-5xl mx-auto">
|
||||
<div className="mb-6 px-4 py-3 pb-6 rounded-xl border shadow flex flex-wrap items-center gap-4 relative">
|
||||
<div className="flex flex-col flex-1 min-w-[180px]" style={{ position: "relative", zIndex: 2 }}>
|
||||
<div className="flex flex-nowrap items-end gap-2">
|
||||
<input
|
||||
className="text-xl font-bold text-gray-900 bg-transparent border-b border-gray-300 px-2 py-1 mr-2"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder="Trip Name"
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
className="text-xs text-gray-500 px-2 py-0.5 rounded font-semibold border border-gray-300"
|
||||
value={startDate}
|
||||
onChange={e => setStartDate(e.target.value)}
|
||||
/>
|
||||
<span className="mx-1 text-gray-500">–</span>
|
||||
<input
|
||||
type="date"
|
||||
className="text-xs text-gray-500 px-2 py-0.5 rounded font-semibold border border-gray-300"
|
||||
value={endDate}
|
||||
onChange={e => setEndDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Tags kompakt rechts */}
|
||||
<div className="flex flex-wrap gap-2 items-center min-w-[180px]" style={{ position: "relative", zIndex: 2 }}>
|
||||
{selectedTags.length === 0 ? (
|
||||
<span className="text-gray-400 text-sm">keine</span>
|
||||
) : (
|
||||
selectedTags.map((tag: any) => (
|
||||
<Tag
|
||||
key={tag.id}
|
||||
tag={tag}
|
||||
isMarked={markedTags.some((mt: any) => mt.id === tag.id)}
|
||||
onToggleMark={handleToggleMark}
|
||||
onRemoveTag={handleRemoveTag}
|
||||
onMouseEnter={() => {}}
|
||||
onMouseLeave={() => {}}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
{!addingTag ? (
|
||||
<button
|
||||
className="text-blue-500 underline text-xs ml-2"
|
||||
onClick={() => {
|
||||
setAddingTag(true);
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 0);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
+ Tag
|
||||
</button>
|
||||
) : (
|
||||
<TagAutocompleteInput
|
||||
ref={inputRef}
|
||||
allTags={allTags}
|
||||
selectedTags={selectedTags}
|
||||
onAddTag={(tagId, viaTab) => {
|
||||
handleAddTag(tagId);
|
||||
if (viaTab) {
|
||||
setTimeout(() => {
|
||||
setAddingTag(true);
|
||||
inputRef.current?.focus();
|
||||
}, 0);
|
||||
} else {
|
||||
setAddingTag(false);
|
||||
}
|
||||
}}
|
||||
onEscape={() => setAddingTag(false)}
|
||||
placeholder="Tag suchen..."
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className="ml-4 px-4 py-2 rounded bg-green-500 text-white font-semibold shadow"
|
||||
onClick={handleSave}
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
{/* Optional: Vorschau der Items wie in TripChecklist */}
|
||||
<TripItemList
|
||||
tripId={trip.id}
|
||||
items={items}
|
||||
renderItemsWithCategories={renderItemsWithCategories}
|
||||
markedTagGroups={markedTagGroups}
|
||||
itemsWithoutTag={itemsWithoutTag}
|
||||
reloadItems={reloadItems}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue