changed app name, added new journal entry from map and timeline

This commit is contained in:
Haeri Kim
2026-06-14 10:59:59 +09:00
parent cdf3643622
commit 8422c6e34f
6 changed files with 162 additions and 42 deletions

View File

@@ -1,39 +1,73 @@
<script>
import { get } from 'svelte/store';
import { journals, updateJournal } from '../stores/journalStore.js';
import { journals, addJournal, updateJournal } from '../stores/journalStore.js';
import { countryNames } from '../shared/countries.js';
import SearchInput from '../shared/SearchInput.svelte';
import PhotoEditor from './PhotoEditor.svelte';
/** @type {{ entry: import('../stores/journalStore.js').JournalEntry, onBack: () => void }} */
let { entry, onBack } = $props();
/**
* entry = null → "new entry" mode
* entry = {...} → "edit" mode
* @type {{ entry?: import('../stores/journalStore.js').JournalEntry | null, initialCountry?: string, onBack: () => void }}
*/
let { entry = null, initialCountry = '', onBack } = $props();
let city = $state(entry.location.city);
let country = $state(entry.location.country);
let date = $state(entry.date);
let days = $state(String(entry.days));
let tripType = $state(entry.tripType);
let photos = $state([...entry.photos]);
let memo = $state(entry.memo);
let songTitle = $state(entry.song.title);
let songArtist = $state(entry.song.artist);
let isNew = !entry;
let city = $state(entry?.location.city ?? '');
let country = $state(entry?.location.country ?? initialCountry);
let date = $state(entry?.date ?? new Date().toISOString().slice(0, 10));
let days = $state(String(entry?.days ?? 1));
let tripType = $state(entry?.tripType ?? 'solo');
let photos = $state([...(entry?.photos ?? [])]);
let memo = $state(entry?.memo ?? '');
let songTitle = $state(entry?.song.title ?? '');
let songArtist = $state(entry?.song.artist ?? '');
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) {
// keep first 100 words, preserve trailing space if user is mid-word
memo = words.slice(0, MEMO_MAX).join(' ');
e.currentTarget.value = memo;
} else {
memo = raw;
}
}
// City suggestions from existing entries (deduplicated)
let cityOptions = $derived(
[...new Set(get(journals).map(e => e.location.city))].sort()
);
function save() {
updateJournal({
...entry,
date,
days: Number(days),
tripType,
memo,
photos,
location: { city, country },
song: { title: songTitle, artist: songArtist },
});
if (isNew) {
addJournal({
title: `${city}, ${country}`,
date,
days: Number(days),
tripType,
memo,
photos,
location: { city, country },
song: { title: songTitle, artist: songArtist },
});
} else {
updateJournal({
...entry,
date,
days: Number(days),
tripType,
memo,
photos,
location: { city, country },
song: { title: songTitle, artist: songArtist },
});
}
onBack();
}
</script>
@@ -49,7 +83,7 @@
Back
</button>
</div>
<span class="topbar-title">Edit</span>
<span class="topbar-title">{isNew ? 'New entry' : 'Edit'}</span>
<div class="topbar-right">
<button class="topbar-btn topbar-btn--save" onclick={save}>Save changes</button>
</div>
@@ -59,14 +93,14 @@
<form class="form" onsubmit={(e) => { e.preventDefault(); save(); }}>
<div class="row">
<div class="field">
<label class="label" for="edit-city">City <span class="req">*</span></label>
<SearchInput id="edit-city" bind:value={city} options={cityOptions} required />
</div>
<div class="field">
<label class="label" for="edit-country">Country <span class="req">*</span></label>
<SearchInput id="edit-country" bind:value={country} options={countryNames} required />
</div>
<div class="field">
<label class="label" for="edit-city">City <span class="req">*</span></label>
<SearchInput id="edit-city" bind:value={city} options={cityOptions} required />
</div>
</div>
<div class="row">
@@ -95,8 +129,11 @@
<PhotoEditor {photos} onchange={(p) => (photos = p)} />
<div class="field">
<label class="label" for="edit-memo">How was it?</label>
<textarea id="edit-memo" class="input textarea" rows="4" bind:value={memo}></textarea>
<div class="label-row">
<label class="label" for="edit-memo">How was it?</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="4" value={memo} oninput={onMemoInput}></textarea>
</div>
<div class="row">
@@ -206,6 +243,12 @@
gap: 6px;
}
.label-row {
display: flex;
align-items: baseline;
justify-content: space-between;
}
.label {
font-size: 11px;
font-weight: 400;
@@ -214,6 +257,15 @@
color: var(--text-sub);
}
.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; }
.req {
color: var(--accent);
font-size: 11px;