Fix option card readability and image loading

This commit is contained in:
TopherMayor
2026-06-12 11:10:27 -07:00
parent fa0a7f44b7
commit 4cce703544
7 changed files with 133 additions and 52 deletions

View File

@@ -24,7 +24,6 @@ const CAT_EMOJI = {
itinerary: '🗺️',
budget: '💸',
}
function normalizeSourceKey(value) {
return String(value || '')
.trim()
@@ -83,6 +82,12 @@ function cx(...classes) {
return classes.filter(Boolean).join(' ')
}
function getOptionImageUrl(option) {
if (option.imageUrl) return option.imageUrl
const bookingUrl = option.bookingUrl || option.url || option.links?.[0]?.url || ''
return bookingUrl ? `/api/preview-image?url=${encodeURIComponent(bookingUrl)}` : ''
}
function useCaboData() {
const [state, setState] = useState({
categories: [],
@@ -367,7 +372,7 @@ function MainRoute({ data, tabs, guest, token, send, mobileView, setMobileView }
</div>
)}
<div className={cx('grid gap-4', showMap && 'md:grid-cols-[minmax(0,1fr)_minmax(360px,42vw)]')}>
<div className={cx('grid gap-4', showMap && 'lg:grid-cols-[minmax(420px,520px)_minmax(0,1fr)] xl:grid-cols-[minmax(520px,640px)_minmax(0,1fr)]')}>
<section className={cx(showMap && mobileView === 'map' && 'hidden md:block')}>
<OptionList tabId={tabId} tab={tab} options={visibleOptions} data={data} guest={guest} token={token} send={send} />
<AddOptionForm categories={data.categories} token={token} guest={guest} />
@@ -438,6 +443,7 @@ function OptionCard({ option, tabId, guest, token, send }) {
const latestPoint = getLatestPoint(option, tabId)
const source = getAvailableSources(option, tabId)[0]
const bookingUrl = getBookingUrl(option, tabId)
const imageUrl = getOptionImageUrl({ ...option, bookingUrl })
const links = getVisibleLinks(option, tabId)
const price = latestPoint?.displayPrice || (typeof latestPoint?.price === 'number' ? formatMoney(latestPoint.price, latestPoint.currency) : source?.latestDisplayPrice || '')
const dates = [formatDate(latestPoint?.tripCheckIn), formatDate(latestPoint?.tripCheckOut)].filter(Boolean).join(' to ')
@@ -453,21 +459,30 @@ function OptionCard({ option, tabId, guest, token, send }) {
}
return (
<article className={cx('overflow-hidden rounded-xl border bg-panel shadow-lg transition hover:-translate-y-0.5 hover:border-aqua/70', hasVoted ? 'border-aqua/70' : 'border-line')}>
<div className="grid gap-0 md:grid-cols-[minmax(132px,34%)_1fr]">
<div className="min-h-44 bg-panel2">
<img src={option.imageUrl} alt="" className="h-full min-h-44 w-full object-cover" loading="lazy" />
<article className={cx('min-w-0 overflow-hidden rounded-xl border bg-panel shadow-lg transition hover:-translate-y-0.5 hover:border-aqua/70', hasVoted ? 'border-aqua/70' : 'border-line')}>
<div className="grid gap-0">
<div className="grid h-44 place-items-center overflow-hidden bg-gradient-to-br from-aqua/10 via-panel2 to-gold/10 sm:h-52">
{imageUrl ? (
<img
src={imageUrl}
alt={`${option.name} booking site preview`}
className="h-full w-full object-cover"
loading="lazy"
/>
) : (
<div className="px-6 text-center text-sm font-black uppercase tracking-wider text-slate-500">No booking image</div>
)}
</div>
<div className="p-4">
<div className="min-w-0 p-4">
<div className="flex items-start justify-between gap-3">
<div>
<h2 className="text-base font-black text-white">{option.name}</h2>
<p className="mt-1 text-sm leading-5 text-slate-400">{option.desc}</p>
<div className="min-w-0">
<h2 className="break-words text-base font-black text-white">{option.name}</h2>
<p className="mt-1 break-words text-sm leading-5 text-slate-400">{option.desc}</p>
</div>
<div className="shrink-0 text-sm font-black text-aqua">{votes.length} vote{votes.length === 1 ? '' : 's'}</div>
</div>
<div className="mt-3 grid gap-2 sm:grid-cols-2">
<div className="mt-3 grid gap-2 min-[560px]:grid-cols-2">
<Fact label="Current price" value={price || 'Not tracked yet'} sub={dates} />
<Fact label="Source" value={source?.sourceLabel || latestPoint?.source || 'Planning data'} />
<Fact label="Booking" value={formatBookingType(source?.bookingType || latestPoint?.bookingType, source?.priceBasis || latestPoint?.priceBasis) || 'Option link'} />
@@ -476,7 +491,7 @@ function OptionCard({ option, tabId, guest, token, send }) {
{!!chips.length && (
<div className="mt-3 flex flex-wrap gap-1.5">
{chips.map((chip) => <span key={chip} className="rounded-full bg-panel2 px-2.5 py-1 text-[11px] text-slate-300">{chip}</span>)}
{chips.map((chip) => <span key={chip} className="max-w-full rounded-full bg-panel2 px-2.5 py-1 text-[11px] text-slate-300">{chip}</span>)}
</div>
)}
@@ -487,7 +502,7 @@ function OptionCard({ option, tabId, guest, token, send }) {
</a>
)}
{links.slice(0, 4).map((link) => (
<a key={`${option.id}-${link.label}`} href={link.url} target="_blank" rel="noreferrer" className="rounded-full border border-aqua/20 bg-aqua/10 px-3 py-2 text-xs font-bold text-aqua hover:bg-aqua/20">
<a key={`${option.id}-${link.label}`} href={link.url} target="_blank" rel="noreferrer" className="max-w-full rounded-full border border-aqua/20 bg-aqua/10 px-3 py-2 text-xs font-bold text-aqua hover:bg-aqua/20">
{link.label}
</a>
))}
@@ -508,10 +523,10 @@ function OptionCard({ option, tabId, guest, token, send }) {
function Fact({ label, value, sub }) {
return (
<div className="rounded-lg border border-white/5 bg-white/[0.03] p-3">
<div className="min-w-0 rounded-lg border border-white/5 bg-white/[0.03] p-3">
<div className="text-[10px] font-black uppercase tracking-wider text-slate-500">{label}</div>
<div className="mt-1 text-sm font-bold text-white">{value}</div>
{sub && <div className="mt-1 text-xs text-slate-500">{sub}</div>}
<div className="mt-1 break-words text-sm font-bold leading-5 text-white">{value}</div>
{sub && <div className="mt-1 break-words text-xs leading-4 text-slate-500">{sub}</div>}
</div>
)
}