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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

18
client/dist/assets/index-P6j6uqrQ.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Cabo Bachelor Party</title>
<script type="module" crossorigin src="/assets/index-OJVYK787.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DKrFtyUx.css">
<script type="module" crossorigin src="/assets/index-P6j6uqrQ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B5xoFPr6.css">
</head>
<body>
<div id="root"></div>

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>
)
}