fix(journey): a dead translation key, and two cards that spoke differently

`common.errorOccurred` is in no locale. It sat in the reorder failure toast
already and I copied it into three more places, so a failed dismiss, restore or
reorder printed the key at the reader. They all say `common.errorTitle` now.

The suggestion mark on a phone card was the word spelled out, which took a third
of a 136px card and pushed the date off it; it is the same plus the desktop
suggestion card offers, and the card's italic dimmed title carries the rest.
Dismissing a suggestion from the entry sheet has its own full-width row above
the buttons rather than a two-line label wedged between Delete and Cancel.

And two cards that should have been one design: the dashboard's spotlight trip
wrote its facts in capitals on a panel of dark glass, the journey hero wrote
them in sentence case on a gradient with its badge loose in a corner. The
journey card is built the way the trip card is now, and the trip card's three
pills are set like the DAY x OF y badge beside them, a size down and on the
same ground.
This commit is contained in:
Maurice
2026-09-13 20:32:49 +02:00
parent 15aefa449e
commit cbc28fad88
11 changed files with 95 additions and 55 deletions
@@ -412,9 +412,16 @@ function MSpotlightCard({ trip, t, onOpen, actions }: {
)
}
/**
* One fact about the spotlight trip: how long, how many places, how many people.
*
* Set like the DAY x OF y badge above it, a size down. The three used to be
* sentence case while the two badges over them were capitals, so the card had two
* voices in the same corner.
*/
function SpotlightPill({ icon, label }: { icon: React.ReactElement; label: string }): React.ReactElement {
return (
<span className="inline-flex items-center gap-1 rounded-full bg-white/20 px-[9px] py-[3px] font-geist text-[0.625rem] font-bold text-white backdrop-blur-[6px]">
<span className="inline-flex items-center gap-1 rounded-full bg-white/[.28] px-[9px] py-[3px] font-geist text-[0.5625rem] font-bold tracking-[.07em] text-white uppercase backdrop-blur-[6px]">
{icon}
{label}
</span>
+27 -21
View File
@@ -103,41 +103,47 @@ function HeroCard({ journey, onOpen }: { journey: JourneyListItem; onOpen: () =>
const src = journeyCoverSrc(journey.cover_image)
return (
// Built like the dashboard's spotlight trip: the photo carries the card and
// everything written sits on one panel of dark glass at its foot, rather than
// a badge floating in one corner and text bleeding into a gradient at the
// other. Two hero cards, two different languages, was the complaint.
<button
type="button"
onClick={onOpen}
className="relative mb-3 block h-[180px] w-full overflow-hidden rounded-[22px] text-left shadow-[0_16px_38px_-20px_rgba(0,0,0,.5)]"
className="relative mb-3 block h-[220px] w-full overflow-hidden rounded-[26px] text-left shadow-[0_24px_56px_-22px_rgba(0,0,0,.5)]"
>
{src ? (
<img src={src} alt="" className="h-full w-full object-cover" />
<img src={src} alt="" className="absolute inset-0 h-full w-full object-cover" />
) : (
<div className="h-full w-full" style={journeyCoverStyle(journey)} />
<div className="absolute inset-0" style={journeyCoverStyle(journey)} />
)}
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(0,0,0,.05)_40%,rgba(0,0,0,.66))]" />
<span className="absolute left-[11px] top-[11px] flex items-center gap-1 rounded-full bg-white/90 px-[10px] py-1 font-geist text-[0.59375rem] font-extrabold uppercase tracking-[.04em] text-[#101013]">
{t('mobileJourney.latestJourney')}
</span>
<div className="absolute bottom-[13px] left-[14px] right-[14px] text-white">
<div className="truncate text-[1.25rem] font-extrabold">{journey.title}</div>
<div className="absolute right-[10px] bottom-[10px] left-[10px] rounded-[18px] border border-white/[.16] bg-[rgba(14,14,17,.52)] p-[12px_14px] text-white backdrop-blur-[22px] backdrop-saturate-[1.6]">{/* theme-lint-disable — fixed dark glass on the cover photo */}
<span className="flex gap-[6px]">
<span className="rounded-full bg-white/[.92] px-2 py-[3px] text-[0.625rem] font-bold tracking-[.07em] text-[#101013] uppercase">{/* theme-lint-disable — fixed on-photo badge */}
{t('mobileJourney.latestJourney')}
</span>
</span>
<div className="mt-[7px] truncate text-[1.4375rem] font-bold">{journey.title}</div>
<div className="mt-[9px] flex gap-[6px]">
<span className="inline-flex items-center gap-1 rounded-full bg-white/20 px-[9px] py-[3px] font-geist text-[0.59375rem] font-bold backdrop-blur-[6px]">
<NotebookPen size={10} strokeWidth={2.2} />
{t('mobileJourney.entriesCount', { count: journey.entry_count ?? 0 })}
</span>
<span className="inline-flex items-center gap-1 rounded-full bg-white/20 px-[9px] py-[3px] font-geist text-[0.59375rem] font-bold backdrop-blur-[6px]">
<Image size={10} strokeWidth={2.2} />
{t('mobileJourney.photosCount', { count: journey.photo_count ?? 0 })}
</span>
<span className="inline-flex items-center gap-1 rounded-full bg-white/20 px-[9px] py-[3px] font-geist text-[0.59375rem] font-bold backdrop-blur-[6px]">
<MapPin size={10} strokeWidth={2.2} />
{t('mobileJourney.placesCount', { count: journey.place_count ?? 0 })}
</span>
<HeroPill icon={<NotebookPen size={10} strokeWidth={2.2} />} label={t('mobileJourney.entriesCount', { count: journey.entry_count ?? 0 })} />
<HeroPill icon={<Image size={10} strokeWidth={2.2} />} label={t('mobileJourney.photosCount', { count: journey.photo_count ?? 0 })} />
<HeroPill icon={<MapPin size={10} strokeWidth={2.2} />} label={t('mobileJourney.placesCount', { count: journey.place_count ?? 0 })} />
</div>
</div>
</button>
)
}
/** One fact about the journey, set like the spotlight trip's pills on the dashboard. */
function HeroPill({ icon, label }: { icon: React.ReactElement; label: string }) {
return (
<span className="inline-flex items-center gap-1 rounded-full bg-white/[.28] px-[9px] py-[3px] font-geist text-[0.5625rem] font-bold tracking-[.07em] uppercase backdrop-blur-[6px]">
{icon}
{label}
</span>
)
}
function GridCard({ journey, onOpen }: { journey: JourneyListItem; onOpen: () => void }) {
const entries = journey.entry_count ?? 0
const photos = journey.photo_count ?? 0
@@ -158,7 +158,7 @@ export default function MJourneyDetail() {
await reorderEntries(current.id, reordered.map(e => e.id))
await loadJourney(current.id)
} catch {
toast.error(t('common.errorOccurred'))
toast.error(t('common.errorTitle'))
}
}, [current, entries, reorderEntries, loadJourney, toast, t])
@@ -963,6 +963,24 @@ export default function MJourneyEntrySheet({
)}
</div>
{/* A suggestion is not deleted, it is put down: the row survives so the trip
sync does not offer the same place again (discussion #2299).
Its own row above the buttons rather than among them: the sentence is
longer than a button label and wrapped into two lines between Delete and
Cancel. Full width, it reads as what it is, an alternative to saving this
suggestion rather than a fourth thing competing with them. */}
{!readOnly && onDismiss && (
<button
type="button"
onClick={onDismiss}
className="mx-[18px] mt-1 mb-[10px] flex flex-none items-center gap-[9px] rounded-[14px] border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] px-[13px] py-[10px] text-left"
>
<EyeOff size={15} strokeWidth={2} className="flex-none text-m-muted" />
<span className="min-w-0 flex-1 text-[0.8125rem] font-semibold">{t('journey.suggestions.dismiss')}</span>
</button>
)}
<div className="flex flex-none items-center gap-2 border-t border-[color:var(--m-rowbr)] px-[18px] pb-4 pt-3">
{!readOnly && onDelete && (
<button
@@ -974,18 +992,6 @@ export default function MJourneyEntrySheet({
{t('common.delete')}
</button>
)}
{/* A suggestion is not deleted, it is put down: the row survives so the trip
sync does not offer the same place again (discussion #2299). */}
{!readOnly && onDismiss && (
<button
type="button"
onClick={onDismiss}
className="flex items-center gap-[5px] text-[0.75rem] font-bold text-m-muted"
>
<EyeOff size={13} strokeWidth={2} />
{t('journey.suggestions.dismiss')}
</button>
)}
{!readOnly && captureOnly && (
<button
type="button"