mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 21:46:49 +00:00
68e101ed37
Dashboard widget (B): the collections tool now shows the user's LISTS as compact colour-washed badges (cover image tinted with the list colour, or a gradient) that jump to the list — one list() call, no N+1. List details (C): lists gain a description, a custom cover image (uploaded to /uploads/covers, tinted with the list colour in the hero) and links. A shared ListEditorModal handles both create and edit; the hero shows the description + link chips. New `links` JSON column on collections + collection_places (migration 151) with parse/serialize in the service; a POST :id/cover upload endpoint mirroring trips; cover-file cleanup path-confined locally. Place detail (D): clicking a place opens a bottom sheet (no backdrop, so the map stays visible) — status cycle, copy-to-trip, remove, and an edit mode with a markdown description + links editor (collectionsApi.updatePlace, now wired via a store action). A "+" next to the search adds a place to the list via the maps search. Fusion + fixes (E): the owner can now remove an accepted member (kick) — new removeMember service/route/store + a button in ShareCollectionModal, with a collections:removed WS bounce. findMembership no longer matches on name alone (coordinate proximity required, killing "Starbucks everywhere" false positives). loadCollection swallows a 403/404 after a leave/remove so the URL sync can't throw uncaught. Grid remnants gone; the map select toggle moved onto the map. New strings across all 22 locales; i18n parity strict passes.
71 lines
2.7 KiB
TypeScript
71 lines
2.7 KiB
TypeScript
import React, { useEffect, useState } from 'react'
|
|
import { Bookmark, ArrowRight, MapPin } from 'lucide-react'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import { useTranslation } from '../../i18n'
|
|
import { collectionsApi } from '../../api/collections'
|
|
import { entityGradient } from '../../utils/gradients'
|
|
import type { Collection } from '@trek/shared'
|
|
|
|
/**
|
|
* Dashboard sidebar widget — a glassy `.tool` card that surfaces the user's
|
|
* saved-place LISTS as compact colour-washed badges (a mini version of the
|
|
* collections hero): each badge shows the list's cover image (tinted with its
|
|
* colour) or a colour gradient, its name and place count, and jumps to that
|
|
* list. Fetches only list() (per-list place_count), so no N+1.
|
|
*/
|
|
export default function CollectionsWidget({ onOpen }: { onOpen: () => void }): React.ReactElement {
|
|
const { t } = useTranslation()
|
|
const navigate = useNavigate()
|
|
const [lists, setLists] = useState<Collection[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
;(async () => {
|
|
try {
|
|
const data = await collectionsApi.list()
|
|
if (!cancelled) setLists(data.collections)
|
|
} catch {
|
|
if (!cancelled) setLists([])
|
|
} finally {
|
|
if (!cancelled) setLoading(false)
|
|
}
|
|
})()
|
|
return () => { cancelled = true }
|
|
}, [])
|
|
|
|
return (
|
|
<div className="tool">
|
|
<div className="tool-head">
|
|
<div className="tool-title"><Bookmark size={14} /> {t('collections.widget.title')}</div>
|
|
<button className="tool-action" aria-label={t('collections.widget.title')} onClick={onOpen}>
|
|
<ArrowRight size={14} />
|
|
</button>
|
|
</div>
|
|
{loading ? null : lists.length === 0 ? (
|
|
<div className="col-empty">{t('collections.widget.empty')}</div>
|
|
) : (
|
|
<div className="col-badges">
|
|
{lists.slice(0, 6).map(list => (
|
|
<button
|
|
key={list.id}
|
|
className="col-badge"
|
|
style={{ ['--badge-color' as string]: list.color || '#6366f1' }}
|
|
onClick={() => navigate(`/collections/${list.id}`)}
|
|
>
|
|
{list.cover_image
|
|
? <img className="col-badge-media" src={list.cover_image} alt="" />
|
|
: <div className="col-badge-media" style={{ backgroundImage: entityGradient(list.id) }} />}
|
|
<div className="col-badge-tint" />
|
|
<div className="col-badge-body">
|
|
<span className="col-badge-name">{list.name}</span>
|
|
<span className="col-badge-count"><MapPin size={11} /> {list.place_count ?? 0}</span>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|