feat: multiple columns
This commit is contained in:
parent
566c1cc125
commit
de794b3c45
2 changed files with 139 additions and 130 deletions
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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)}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue