Files
Map-Jurnal/src/lib/timeline/JournalDetail.svelte

353 lines
8.6 KiB
Svelte

<script>
import { removeEntry } from '../stores/entriesStore.svelte.js';
import { flagEmoji } from '../shared/countries.js';
import DeleteConfirm from './DeleteConfirm.svelte';
/** @type {{ entry: import('../shared/types.js').JournalEntry, onBack: () => void, onEdit: () => void }} */
let { entry, onBack, onEdit } = $props();
let showDeleteConfirm = $state(false);
function handleDelete() {
removeEntry(entry.id);
onBack();
}
function formatDate(iso) {
return new Date(iso).toLocaleDateString('en-US', {
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
});
}
let lightboxSrc = $state(null);
</script>
{#if showDeleteConfirm}
<DeleteConfirm {entry} onConfirm={handleDelete} onCancel={() => showDeleteConfirm = false} />
{/if}
<!-- Lightbox -->
{#if lightboxSrc}
<div class="lightbox" onclick={() => lightboxSrc = null} role="button" tabindex="0"
onkeydown={(e) => e.key === 'Escape' && (lightboxSrc = null)}>
<img src={lightboxSrc} alt="" />
</div>
{/if}
<div class="detail-layout">
<!-- ── Full-width top bar ── -->
<header class="detail-topbar">
<div class="topbar-left">
<button class="topbar-btn" onclick={onBack}>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 12H5M12 5l-7 7 7 7"/>
</svg>
Back
</button>
<div class="topbar-divider"></div>
<span class="topbar-flag">{flagEmoji(entry.location.country)}</span>
<div class="topbar-place">
<span class="topbar-city">{entry.location.cities.join(', ')}</span>
<span class="topbar-country">{entry.location.country}</span>
</div>
</div>
<div class="topbar-right">
<button class="topbar-btn" title="Edit entry" onclick={onEdit}>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
Edit
</button>
<button class="topbar-btn topbar-btn--danger" title="Delete entry" onclick={() => showDeleteConfirm = true}>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6"/>
</svg>
Delete
</button>
</div>
</header>
<!-- ── Body: photo left + Q&A right ── -->
<div class="detail-body">
<!-- Left: photos -->
<div class="photo-col">
<div class="photo-scroll">
{#if entry.photos.length === 0}
<div class="no-photos">No photos</div>
{:else}
<div class="photo-grid">
{#each entry.photos as photo, i}
<div class="photo-cell" class:cell-wide={i === 0 && entry.photos.length > 1}>
<img src={photo} alt=""
onclick={() => lightboxSrc = photo}
onerror={(e) => e.currentTarget.parentElement.classList.add('cell-broken')} />
</div>
{/each}
</div>
{/if}
</div>
</div>
<!-- Right: Q&A -->
<div class="info-col">
<div class="info-inner">
<div class="qa-list">
<div class="qa-item">
<p class="question">When did you go?</p>
<p class="answer">{formatDate(entry.date)}</p>
</div>
<div class="qa-item">
<p class="question">How long did you stay?</p>
<p class="answer">{entry.days} {entry.days === 1 ? 'day' : 'days'}</p>
</div>
<div class="qa-item">
<p class="question">Who did you go with?</p>
<p class="answer">
{#if entry.tripType === 'solo'}
Just me — solo trip
{:else if entry.tripType === 'family'}
With family
{:else}
With friends
{/if}
</p>
</div>
<div class="qa-item">
<p class="question">How was it?</p>
<p class="answer memo">{entry.memo}</p>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
/* ── Outer layout: column (topbar + body) ── */
.detail-layout {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
/* ── Full-width top bar ── */
.detail-topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
height: 60px;
flex-shrink: 0;
background: var(--bg);
border-bottom: 1px solid var(--border);
}
.topbar-left {
display: flex;
align-items: center;
gap: 12px;
}
.topbar-right {
display: flex;
align-items: center;
gap: 4px;
}
.topbar-divider {
width: 1px;
height: 20px;
background: var(--border);
}
.topbar-flag { font-size: 20px; line-height: 1; }
.topbar-place {
display: flex;
align-items: baseline;
gap: 6px;
}
.topbar-city {
font-size: 13px;
font-weight: 300;
color: var(--text-sub);
}
.topbar-country {
font-size: 17px;
font-weight: 400;
color: var(--text-h);
letter-spacing: -0.3px;
}
.topbar-btn {
display: inline-flex;
align-items: center;
gap: 6px;
font-family: var(--sans);
font-size: 15px;
font-weight: 400;
color: var(--text);
background: none;
border: 1px solid transparent;
border-radius: 10px;
padding: 8px 14px;
cursor: pointer;
transition: background 0.15s, color 0.15s, border-color 0.15s;
white-space: nowrap;
}
.topbar-btn:hover {
background: var(--bg-subtle);
border-color: var(--border);
color: var(--text-h);
}
.topbar-btn--danger:hover { color: #dc2626; background: #fff1f1; border-color: #fca5a5; }
/* ── Body row ── */
.detail-body {
flex: 1;
display: flex;
flex-direction: row;
overflow: hidden;
}
/* ── Left: photos ── */
.photo-col {
flex: 1;
overflow: hidden;
background: #f0f0f0;
display: flex;
flex-direction: column;
}
.photo-scroll {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.photo-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
grid-auto-rows: 200px;
}
.photo-cell {
overflow: hidden;
background: var(--bg-subtle);
border-radius: 4px;
cursor: zoom-in;
}
.photo-cell.cell-wide {
grid-column: 1 / -1;
grid-row: span 2;
}
.photo-cell img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform 0.2s ease;
}
.photo-cell:hover img { transform: scale(1.03); }
.photo-cell.cell-broken {
display: flex;
align-items: center;
justify-content: center;
color: var(--text-sub);
font-size: 12px;
}
.no-photos {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-sub);
font-size: 14px;
}
/* ── Right: Q&A ── */
.info-col {
width: 440px;
flex-shrink: 0;
overflow-y: auto;
border-left: 1px solid var(--border);
background: var(--bg);
}
.info-inner {
padding: 36px 32px 80px;
}
.qa-list {
display: flex;
flex-direction: column;
}
.qa-item {
padding: 20px 0;
border-bottom: 1px solid var(--border);
}
.qa-item:first-child { padding-top: 0; }
.qa-item:last-child { border-bottom: none; }
.question {
font-size: 11px;
font-weight: 400;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--accent);
margin-bottom: 8px;
}
.answer {
font-size: 15px;
font-weight: 300;
color: var(--text-h);
line-height: 1.5;
}
.answer.memo {
font-size: 14px;
color: var(--text);
line-height: 1.75;
}
/* ── Lightbox ── */
.lightbox {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.9);
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
cursor: zoom-out;
}
.lightbox img {
max-width: 90vw;
max-height: 90vh;
object-fit: contain;
border-radius: 4px;
}
/* ── Responsive ── */
@media (max-width: 700px) {
.detail-body { flex-direction: column; overflow-y: auto; }
.photo-col { height: 260px; flex: none; }
.info-col { width: 100%; border-left: none; border-top: 1px solid var(--border); }
}
</style>