339 lines
12 KiB
Svelte
339 lines
12 KiB
Svelte
<script>
|
|
import { flagEmoji } from '../shared/countries.js';
|
|
|
|
/** @type {{ entry: import('../stores/journalStore.js').JournalEntry, onClick: () => void }} */
|
|
let { entry, onClick } = $props();
|
|
|
|
function formatDate(/** @type {string} */ iso) {
|
|
return new Date(iso).toLocaleDateString('en-US', {
|
|
month: 'short', day: 'numeric', year: 'numeric',
|
|
});
|
|
}
|
|
|
|
let mainPhoto = $derived(entry.photos[0] ?? null);
|
|
let thumbPhotos = $derived(entry.photos.slice(1, 4));
|
|
let extraCount = $derived(entry.photos.length > 4 ? entry.photos.length - 4 : 0);
|
|
|
|
const transportIcons = {
|
|
flight: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.18 10a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3.09 0h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.09 8a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 21 15z"/></svg>`,
|
|
train: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="2" width="16" height="16" rx="2"/><path d="M4 12h16M8 20l-2 2M16 20l2 2M12 12v6"/><circle cx="8.5" cy="7.5" r="1"/><circle cx="15.5" cy="7.5" r="1"/></svg>`,
|
|
bus: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 6v6M15 6v6M2 12h19.6M18 18h2a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h2m0 0v2m8-2v2M6 18h12"/><circle cx="8" cy="18" r="2"/><circle cx="16" cy="18" r="2"/></svg>`,
|
|
car: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 17H3a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v9a2 2 0 0 1-2 2h-2"/><circle cx="7.5" cy="17.5" r="2.5"/><circle cx="17.5" cy="17.5" r="2.5"/></svg>`,
|
|
ship: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 21c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1 .6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"/><path d="M19.38 20A11.6 11.6 0 0 0 21 14l-9-4-9 4c0 2.9.94 5.34 2.81 7.76"/><path d="M19 13V7a1 1 0 0 0-1-1H6a1 1 0 0 0-1 1v6"/><path d="M12 10v4"/><path d="M12 3v4"/></svg>`,
|
|
walk: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="4" r="2"/><path d="m9 22 1-7-2.5-2.5L9 9l6 1-1 4.5L16 22"/><path d="M6.5 11.5 4 12l2 6"/><path d="M17.5 11.5 20 12l-2 6"/></svg>`,
|
|
};
|
|
let transportLabel = $derived({ flight: 'Flight', train: 'Train', bus: 'Bus', car: 'Car', ship: 'Ship', walk: 'Walk' }[entry.transport] ?? '');
|
|
</script>
|
|
|
|
<li class="v-item">
|
|
<div class="v-dot" aria-hidden="true"></div>
|
|
|
|
<div class="v-content">
|
|
<!-- Country above card -->
|
|
<div class="above-card">
|
|
<span class="flag">{flagEmoji(entry.location.country)}</span>
|
|
<span class="country-name">{entry.location.country}</span>
|
|
</div>
|
|
|
|
<!-- Card -->
|
|
<div class="entry-card" role="button" tabindex="0"
|
|
onclick={onClick}
|
|
onkeydown={(e) => e.key === 'Enter' && onClick()}>
|
|
|
|
<!-- Trip badge — top-right of card, outside photo -->
|
|
<span class="trip-badge trip-badge--{entry.tripType}">
|
|
{entry.tripType === 'solo' ? 'Solo' : entry.tripType === 'family' ? 'Family' : 'Friends'}
|
|
</span>
|
|
|
|
<!-- Photos -->
|
|
<div class="photo-grid" class:has-thumbs={thumbPhotos.length > 0}>
|
|
<div class="photo-main">
|
|
{#if mainPhoto}
|
|
<img src={mainPhoto} alt="" loading="lazy"
|
|
onerror={(e) => {
|
|
e.currentTarget.style.display = 'none';
|
|
e.currentTarget.nextElementSibling.style.display = 'flex';
|
|
}} />
|
|
<div class="photo-fallback" style="display:none">
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2">
|
|
<rect x="3" y="3" width="18" height="18" rx="3"/>
|
|
<circle cx="8.5" cy="8.5" r="1.5"/>
|
|
<path d="M21 15l-5-5L5 21"/>
|
|
</svg>
|
|
</div>
|
|
{:else}
|
|
<div class="photo-fallback">
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2">
|
|
<rect x="3" y="3" width="18" height="18" rx="3"/>
|
|
<circle cx="8.5" cy="8.5" r="1.5"/>
|
|
<path d="M21 15l-5-5L5 21"/>
|
|
</svg>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if thumbPhotos.length > 0}
|
|
<div class="photo-thumbs">
|
|
{#each thumbPhotos as photo, i}
|
|
<div class="photo-thumb">
|
|
<img src={photo} alt="" loading="lazy"
|
|
onerror={(e) => {
|
|
e.currentTarget.style.display = 'none';
|
|
e.currentTarget.nextElementSibling.style.display = 'flex';
|
|
}} />
|
|
<div class="thumb-fallback" style="display:none">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2">
|
|
<rect x="3" y="3" width="18" height="18" rx="3"/>
|
|
<circle cx="8.5" cy="8.5" r="1.5"/>
|
|
<path d="M21 15l-5-5L5 21"/>
|
|
</svg>
|
|
</div>
|
|
{#if i === 2 && extraCount > 0}
|
|
<div class="extra-overlay">+{extraCount}</div>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Info bar -->
|
|
<div class="card-info">
|
|
<span class="city">{entry.location.cities.join(', ')}</span>
|
|
<div class="meta">
|
|
{#if entry.transport}
|
|
<span class="transport-chip transport-chip--{entry.transport}">
|
|
{@html transportIcons[entry.transport] ?? ''}
|
|
{transportLabel}
|
|
</span>
|
|
<span class="dot-sep">·</span>
|
|
{/if}
|
|
<span>{formatDate(entry.date)}</span>
|
|
<span class="dot-sep">·</span>
|
|
<span>{entry.days} {entry.days === 1 ? 'day' : 'days'}</span>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</li>
|
|
|
|
<style>
|
|
/* ── Timeline row ── */
|
|
.v-item {
|
|
display: flex;
|
|
gap: 14px;
|
|
align-items: flex-start;
|
|
padding-bottom: 48px;
|
|
position: relative;
|
|
}
|
|
.v-item:last-child { padding-bottom: 0; }
|
|
.v-item:not(:last-child)::after {
|
|
content: '';
|
|
position: absolute;
|
|
left: 4px;
|
|
top: 34px;
|
|
bottom: 0;
|
|
width: 1px;
|
|
background: var(--border);
|
|
}
|
|
|
|
.v-dot {
|
|
flex-shrink: 0;
|
|
width: 9px;
|
|
height: 9px;
|
|
border-radius: 50%;
|
|
background: var(--bg);
|
|
border: 1.5px solid var(--accent);
|
|
margin-top: 6px;
|
|
z-index: 1;
|
|
}
|
|
|
|
/* ── Content column (above-card + card) ── */
|
|
.v-content {
|
|
flex: 1;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
|
|
/* ── Country above card ── */
|
|
.above-card {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding-left: 2px;
|
|
}
|
|
.flag { font-size: 16px; line-height: 1; }
|
|
.country-name {
|
|
font-size: 15px;
|
|
font-weight: 400;
|
|
color: var(--text-h);
|
|
letter-spacing: -0.2px;
|
|
}
|
|
|
|
/* ── Card ── */
|
|
.entry-card {
|
|
position: relative;
|
|
border-radius: 12px;
|
|
border: 1px solid var(--border);
|
|
background: var(--bg-raised);
|
|
box-shadow: 0 1px 4px rgba(0,0,0,0.05);
|
|
cursor: pointer;
|
|
overflow: hidden;
|
|
transition: box-shadow 0.2s, transform 0.15s;
|
|
container-type: inline-size;
|
|
container-name: card;
|
|
}
|
|
.entry-card:hover {
|
|
box-shadow: 0 6px 20px rgba(0,0,0,0.09);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
/* ── Trip badge — absolute top-right of card ── */
|
|
.trip-badge {
|
|
position: absolute;
|
|
top: 10px;
|
|
right: 10px;
|
|
z-index: 2;
|
|
font-size: 11px;
|
|
font-weight: 300;
|
|
padding: 3px 10px;
|
|
border-radius: 20px;
|
|
letter-spacing: 0.04em;
|
|
backdrop-filter: blur(6px);
|
|
}
|
|
.trip-badge--solo { background: rgba(245,158,11,0.85); color: #fff; }
|
|
.trip-badge--friends { background: rgba(124,58,237,0.85); color: #fff; }
|
|
.trip-badge--family { background: rgba(16,185,129,0.85); color: #fff; }
|
|
|
|
/* ── Photo grid — fixed height, always consistent ── */
|
|
.photo-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
height: 180px;
|
|
background: var(--bg-subtle);
|
|
}
|
|
.photo-grid.has-thumbs {
|
|
grid-template-columns: 2fr 1fr;
|
|
gap: 2px;
|
|
}
|
|
|
|
@container card (max-width: 300px) {
|
|
.photo-grid.has-thumbs { grid-template-columns: 1fr; }
|
|
.photo-thumbs { display: none; }
|
|
}
|
|
|
|
/* ── Main photo ── */
|
|
.photo-main {
|
|
overflow: hidden;
|
|
height: 100%;
|
|
background: var(--bg-subtle);
|
|
}
|
|
.photo-main img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
display: block;
|
|
}
|
|
.photo-fallback {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--text-sub);
|
|
}
|
|
|
|
/* ── Thumbs ── */
|
|
.photo-thumbs {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
.photo-thumb {
|
|
position: relative;
|
|
flex: 1;
|
|
overflow: hidden;
|
|
min-height: 0;
|
|
background: var(--bg-subtle);
|
|
}
|
|
.photo-thumb img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
display: block;
|
|
}
|
|
.thumb-fallback {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--text-sub);
|
|
background: var(--bg-subtle);
|
|
}
|
|
.extra-overlay {
|
|
position: absolute;
|
|
inset: 0;
|
|
background: rgba(0,0,0,0.4);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 14px;
|
|
font-weight: 400;
|
|
color: #fff;
|
|
}
|
|
|
|
/* ── Info bar ── */
|
|
.card-info {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 10px 14px;
|
|
background: var(--bg);
|
|
border-top: 1px solid var(--border);
|
|
gap: 8px;
|
|
min-height: 44px;
|
|
}
|
|
.city {
|
|
font-size: 13px;
|
|
font-weight: 300;
|
|
color: var(--text);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.meta {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
font-size: 12px;
|
|
font-weight: 300;
|
|
color: var(--text-sub);
|
|
white-space: nowrap;
|
|
flex-shrink: 0;
|
|
}
|
|
.dot-sep { color: var(--border-bright); }
|
|
|
|
.transport-chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
font-size: 11px;
|
|
font-weight: 400;
|
|
padding: 2px 7px;
|
|
border-radius: 20px;
|
|
border: 1px solid var(--border);
|
|
background: var(--bg-subtle);
|
|
color: var(--text-sub);
|
|
}
|
|
.transport-chip--flight { color: #7c3aed; background: rgba(124,58,237,0.07); border-color: rgba(124,58,237,0.2); }
|
|
.transport-chip--train { color: #0369a1; background: rgba(3,105,161,0.07); border-color: rgba(3,105,161,0.2); }
|
|
.transport-chip--bus { color: #15803d; background: rgba(21,128,61,0.07); border-color: rgba(21,128,61,0.2); }
|
|
.transport-chip--car { color: #b45309; background: rgba(180,83,9,0.07); border-color: rgba(180,83,9,0.2); }
|
|
.transport-chip--ship { color: #0e7490; background: rgba(14,116,144,0.07); border-color: rgba(14,116,144,0.2); }
|
|
.transport-chip--walk { color: #65a30d; background: rgba(101,163,13,0.07); border-color: rgba(101,163,13,0.2); }
|
|
</style>
|