218 lines
6.2 KiB
Svelte
218 lines
6.2 KiB
Svelte
<script>
|
|
import { addEntry, updateEntry } from '../../stores/entriesStore.svelte.js';
|
|
import PhotoEditor from './PhotoEditor.svelte';
|
|
import StepNav from './StepNavbar.svelte';
|
|
import TripBasicInfo from './TripBasicInfo.svelte';
|
|
|
|
/**
|
|
* entry = null → "new entry" mode
|
|
* entry = {...} → "edit" mode
|
|
* @type {{ entry?: import('../shared/types.js').JournalEntry | null, initialCountry?: string, onBack: () => void }}
|
|
*/
|
|
let { entry = null, initialCountry = '', onBack } = $props();
|
|
|
|
// svelte-ignore state_referenced_locally
|
|
let isNew = !entry;
|
|
|
|
// svelte-ignore state_referenced_locally
|
|
let cities = $state([...(entry?.location.cities ?? [])]);
|
|
// svelte-ignore state_referenced_locally
|
|
let country = $state(entry?.location.country ?? initialCountry);
|
|
// svelte-ignore state_referenced_locally
|
|
let date = $state(entry?.date ?? new Date().toISOString().slice(0, 10));
|
|
// svelte-ignore state_referenced_locally
|
|
let days = $state(String(entry?.days ?? ''));
|
|
// svelte-ignore state_referenced_locally
|
|
let tripType = $state(entry?.tripType ?? '');
|
|
// svelte-ignore state_referenced_locally
|
|
let photos = $state([...(entry?.photos ?? [])]);
|
|
// svelte-ignore state_referenced_locally
|
|
let memo = $state(entry?.memo ?? '');
|
|
// svelte-ignore state_referenced_locally
|
|
let transport = $state(entry?.transport ?? '');
|
|
|
|
let step = $state(1);
|
|
|
|
let errors = $state({
|
|
country: '', cities: '', date: '', days: '', tripType: '', transport: ''
|
|
});
|
|
|
|
function clearErrors() {
|
|
errors = { country: '', cities: '', date: '', days: '', tripType: '', transport: '' };
|
|
}
|
|
|
|
const MEMO_MAX = 100;
|
|
let wordCount = $derived(memo.trim() === '' ? 0 : memo.trim().split(/\s+/).length);
|
|
let memoOverLimit = $derived(wordCount > MEMO_MAX);
|
|
|
|
function onMemoInput(e) {
|
|
const raw = e.currentTarget.value;
|
|
const words = raw.trim() === '' ? [] : raw.trim().split(/\s+/);
|
|
if (words.length > MEMO_MAX) {
|
|
memo = words.slice(0, MEMO_MAX).join(' ');
|
|
e.currentTarget.value = memo;
|
|
} else {
|
|
memo = raw;
|
|
}
|
|
}
|
|
|
|
function nextStep() {
|
|
if (step === 1) {
|
|
clearErrors();
|
|
let hasError = false;
|
|
if (!country.trim()) { errors.country = 'Country is required.'; hasError = true; }
|
|
if (cities.length === 0) { errors.cities = 'Add at least one city.'; hasError = true; }
|
|
if (!date) { errors.date = 'Date is required.'; hasError = true; }
|
|
if (!days || Number(days) < 1) { errors.days = 'Enter a valid number of days.'; hasError = true; }
|
|
if (!tripType) { errors.tripType = 'Select a trip type.'; hasError = true; }
|
|
if (!transport) { errors.transport = 'Select how you got there.'; hasError = true; }
|
|
if (hasError) return;
|
|
}
|
|
step++;
|
|
}
|
|
|
|
function prevStep() {
|
|
if (step === 1) onBack();
|
|
else step--;
|
|
}
|
|
|
|
async function save() {
|
|
try {
|
|
if (isNew) {
|
|
await addEntry({
|
|
title: `${cities.join(', ')}, ${country}`,
|
|
date,
|
|
days: Number(days),
|
|
tripType,
|
|
memo,
|
|
photos,
|
|
transport,
|
|
location: { cities, country },
|
|
});
|
|
} else {
|
|
await updateEntry(entry.id, {
|
|
date,
|
|
days: Number(days),
|
|
tripType,
|
|
transport,
|
|
memo,
|
|
photos,
|
|
location: { cities, country },
|
|
});
|
|
}
|
|
onBack();
|
|
} catch (err) {
|
|
console.error('Save failed:', err);
|
|
}
|
|
}
|
|
|
|
let next = $derived(step < 3 ? nextStep : save);
|
|
</script>
|
|
|
|
<div class="layout">
|
|
<StepNav {step} totalSteps={3} onback={prevStep} onnext={next} />
|
|
|
|
<div class="scroll">
|
|
<div class="form">
|
|
|
|
{#if step === 1}
|
|
<TripBasicInfo
|
|
bind:country bind:cities
|
|
bind:date bind:days bind:tripType bind:transport
|
|
bind:errors {isNew}
|
|
/>
|
|
|
|
{:else if step === 2}
|
|
<h2 class="step-title">Photos</h2>
|
|
<p class="step-sub">Optional — add or update photos from your trip</p>
|
|
<PhotoEditor {photos} onchange={(p) => (photos = p)} />
|
|
|
|
{:else}
|
|
<h2 class="step-title">How was it?</h2>
|
|
<p class="step-sub">Optional — write a note about your trip</p>
|
|
<div class="field">
|
|
<div class="label-row">
|
|
<label class="label" for="edit-memo">Your notes</label>
|
|
<span class="char-count" class:over={memoOverLimit}>{wordCount} / {MEMO_MAX} words</span>
|
|
</div>
|
|
<textarea id="edit-memo" class="input textarea" class:input-over={memoOverLimit} rows="8" value={memo} oninput={onMemoInput}></textarea>
|
|
</div>
|
|
{/if}
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.layout {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
background: var(--bg);
|
|
font-family: var(--sans);
|
|
}
|
|
|
|
.scroll { flex: 1; overflow-y: auto; }
|
|
|
|
.form {
|
|
max-width: 560px;
|
|
margin: 0 auto;
|
|
padding: 36px 48px 80px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 18px;
|
|
}
|
|
|
|
.step-title {
|
|
font-size: 20px;
|
|
font-weight: 400;
|
|
color: var(--text-h);
|
|
letter-spacing: -0.3px;
|
|
margin: 0 0 2px;
|
|
}
|
|
.step-sub {
|
|
font-size: 13px;
|
|
font-weight: 300;
|
|
color: var(--text-sub);
|
|
margin: -10px 0 4px;
|
|
}
|
|
|
|
.field { display: flex; flex-direction: column; gap: 6px; }
|
|
|
|
.label-row {
|
|
display: flex;
|
|
align-items: baseline;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.label {
|
|
font-size: 11px;
|
|
font-weight: 400;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
color: var(--text-h);
|
|
}
|
|
|
|
.char-count { font-size: 11px; font-weight: 300; color: var(--text-sub); transition: color 0.15s; }
|
|
.char-count.over { color: #dc2626; }
|
|
.input-over { border-color: #fca5a5; }
|
|
|
|
.input {
|
|
font-family: var(--sans);
|
|
font-size: 14px;
|
|
font-weight: 300;
|
|
color: var(--text-h);
|
|
background: var(--bg-subtle);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 8px 12px;
|
|
outline: none;
|
|
transition: border-color 0.15s;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
}
|
|
.input:focus { border-color: var(--accent-border); }
|
|
|
|
.textarea { resize: vertical; line-height: 1.6; }
|
|
</style>
|