feat: multiple columns

This commit is contained in:
Felix Zett 2025-09-15 22:19:52 +02:00
parent 566c1cc125
commit de794b3c45
2 changed files with 139 additions and 130 deletions

View file

@ -44,7 +44,7 @@ export default function App() {
} }
return ( return (
<div className="p-4 max-w-2xl mx-auto"> <div className="p-4 max-w-5xl mx-auto">
<h1 className="text-2xl font-bold mb-4">Packlist</h1> <h1 className="text-2xl font-bold mb-4">Packlist</h1>
<div className="flex gap-2 mb-4"> <div className="flex gap-2 mb-4">

View file

@ -127,54 +127,10 @@ export default function TripChecklist({ trips }: { trips: any[] }) {
return ( return (
<> <>
{normalItems.map((item) => ( {/* Haupt-Items als Grid */}
<li {normalItems.length > 0 && (
key={item.id} <ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-4 gap-y-2 mb-4">
className={ {normalItems.map((item) => (
"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" : ""}>
{item.name_calculated}
</span>
{item.item && item.item.tags && item.item.tags.length > 0 && (
<span className="ml-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
{[...item.item.tags]
.slice()
.sort((a, b) => a.name.localeCompare(b.name))
.map((tag: any) => (
<span
key={tag.id}
className="text-xs text-gray-400 rounded px-1 py-0.5"
>
#{tag.name}
</span>
))}
</span>
)}
</li>
))}
{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>
{catItems.map((item) => (
<li <li
key={item.id} key={item.id}
className={ className={
@ -195,7 +151,7 @@ export default function TripChecklist({ trips }: { trips: any[] }) {
className="pointer-events-none" className="pointer-events-none"
/> />
<span className={item.checked ? "line-through text-gray-400" : ""}> <span className={item.checked ? "line-through text-gray-400" : ""}>
{item._sub} {item.name_calculated}
</span> </span>
{item.item && item.item.tags && item.item.tags.length > 0 && ( {item.item && item.item.tags && item.item.tags.length > 0 && (
<span className="ml-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity"> <span className="ml-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
@ -214,6 +170,58 @@ export default function TripChecklist({ trips }: { trips: any[] }) {
)} )}
</li> </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 gap-y-2">
{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" : ""}>
{item._sub}
</span>
{item.item && item.item.tags && item.item.tags.length > 0 && (
<span className="ml-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
{[...item.item.tags]
.slice()
.sort((a, b) => a.name.localeCompare(b.name))
.map((tag: any) => (
<span
key={tag.id}
className="text-xs text-gray-400 rounded px-1 py-0.5"
>
#{tag.name}
</span>
))}
</span>
)}
</li>
))}
</ul>
</React.Fragment> </React.Fragment>
))} ))}
</> </>
@ -228,93 +236,94 @@ export default function TripChecklist({ trips }: { trips: any[] }) {
} }
return ( return (
<div> <div className="py-4 max-w-5xl mx-auto">
{/* Trip-Titel und Zeitraum */} {/* Trip-Titel und Zeitraum */}
<div className="mb-2"> <div className="mb-6 p-6 rounded-xl border-2 border-blue-200 bg-blue-50 shadow flex flex-col gap-2">
<h2 className="text-xl font-bold">{trip.name}</h2> <h2 className="text-2xl font-bold text-blue-900">{trip.name}</h2>
<div className="text-gray-600 text-sm"> <div className="text-gray-600 text-base">
{trip.start_date} {trip.end_date} {trip.start_date} {trip.end_date}
</div> </div>
</div> <div className="flex flex-wrap gap-2 items-center">
{/* Tag-Liste */} <span className="font-semibold">Tags: </span>
<div className="mb-2 flex flex-wrap gap-2 items-center"> {selectedTags.length === 0 ? (
<span className="font-semibold">Tags: </span> <span className="text-gray-400">keine</span>
{selectedTags.length === 0 ? ( ) : (
<span className="text-gray-400">keine</span> selectedTags.map((tag: any) => {
) : ( const isMarked = markedTags.some((mt: any) => mt.id === tag.id);
selectedTags.map((tag: any) => { return (
const isMarked = markedTags.some((mt: any) => mt.id === tag.id); <span
return ( key={tag.id}
<span className={
key={tag.id} "relative px-2 py-0.5 rounded mr-1 text-sm cursor-pointer transition " +
className={ (isMarked
"relative px-2 py-0.5 rounded mr-1 text-sm cursor-pointer transition " + ? "bg-yellow-200 text-yellow-900 font-bold"
(isMarked : "bg-blue-100 text-blue-800")
? "bg-yellow-200 text-yellow-900 font-bold" }
: "bg-blue-100 text-blue-800") onClick={() => handleToggleMark(tag.id)}
} onMouseEnter={() => setHoveredTag(tag.id)}
onClick={() => handleToggleMark(tag.id)} onMouseLeave={() => setHoveredTag(null)}
onMouseEnter={() => setHoveredTag(tag.id)}
onMouseLeave={() => setHoveredTag(null)}
>
#{tag.name}
{hoveredTag === tag.id && (
<button
className="absolute -top-2 -right-2 text-xs text-red-500 bg-white rounded-full px-1 shadow"
onClick={e => {
e.stopPropagation();
handleRemoveTag(tag.id);
}}
>
×
</button>
)}
</span>
);
})
)}
{/* Tag hinzufügen */}
<div className="relative">
<input
type="text"
placeholder="+ Tag"
value={tagInput}
onChange={e => setTagInput(e.target.value)}
className="border rounded px-2 py-0.5 text-sm ml-2"
list="tag-suggestions"
/>
<datalist id="tag-suggestions">
{allTags
.filter(
(t) =>
t.name.toLowerCase().includes(tagInput.toLowerCase()) &&
!selectedTags.some((st) => st.id === t.id)
)
.map((t) => (
<option key={t.id} value={t.name} />
))}
</datalist>
{tagInput &&
allTags
.filter(
(t) =>
t.name.toLowerCase() === tagInput.toLowerCase() &&
!selectedTags.some((st) => st.id === t.id)
)
.map((t) => (
<button
key={t.id}
className="ml-2 text-xs text-blue-500 underline"
onClick={() => handleAddTag(t.id)}
type="button"
> >
Hinzufügen #{tag.name}
</button> {hoveredTag === tag.id && (
))} <button
className="absolute -top-2 -right-2 text-xs text-red-500 bg-white rounded-full px-1 shadow"
onClick={e => {
e.stopPropagation();
handleRemoveTag(tag.id);
}}
>
×
</button>
)}
</span>
);
})
)}
{/* Tag hinzufügen */}
<div className="relative">
<input
type="text"
placeholder="+ Tag"
value={tagInput}
onChange={e => setTagInput(e.target.value)}
className="border rounded px-2 py-0.5 text-sm ml-2"
list="tag-suggestions"
/>
<datalist id="tag-suggestions">
{allTags
.filter(
(t) =>
t.name.toLowerCase().includes(tagInput.toLowerCase()) &&
!selectedTags.some((st) => st.id === t.id)
)
.map((t) => (
<option key={t.id} value={t.name} />
))}
</datalist>
{tagInput &&
allTags
.filter(
(t) =>
t.name.toLowerCase() === tagInput.toLowerCase() &&
!selectedTags.some((st) => st.id === t.id)
)
.map((t) => (
<button
key={t.id}
className="ml-2 text-xs text-blue-500 underline"
onClick={() => handleAddTag(t.id)}
type="button"
>
Hinzufügen
</button>
))}
</div>
</div> </div>
</div> </div>
{/* ...Rest der Checklist... */} {/* ...Rest der Checklist... */}
<ul> <ul
className="grid grid-cols-1 gap-x-8 gap-y-2"
>
{/* 1. Ohne Tag */} {/* 1. Ohne Tag */}
{renderItemsWithCategories(itemsWithoutTag)} {renderItemsWithCategories(itemsWithoutTag)}