feat: Firebase integration, 3-step new trip form, full country list, and UI polish
- Connect Firestore for journal entries and visited countries (real-time onSnapshot) - Connect Firebase Storage for photo uploads - Add NewEntryForm: 3-step flow (trip details → photos → reflection questions) - Expand country list to full world-atlas dataset (~240 countries) matching the map - Filter city suggestions by selected country in both NewEntryForm and EditForm - Redesign StatsPanel as floating horizontal card with donut chart and progress bar - Center timeline layout with responsive side margins - Replace "entry" language with "trip" throughout (Add trip, Save trip, Delete trip) - Remove footer from Layout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,11 +18,19 @@
|
||||
let cityInput = $state('');
|
||||
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 days = $state(String(entry?.days ?? ''));
|
||||
let tripType = $state(entry?.tripType ?? '');
|
||||
let photos = $state([...(entry?.photos ?? [])]);
|
||||
let memo = $state(entry?.memo ?? '');
|
||||
let transport = $state(entry?.transport ?? 'flight');
|
||||
let transport = $state(entry?.transport ?? '');
|
||||
|
||||
let errors = $state({
|
||||
country: '', cities: '', date: '', days: '', tripType: '', transport: ''
|
||||
});
|
||||
|
||||
function clearErrors() {
|
||||
errors = { country: '', cities: '', date: '', days: '', tripType: '', transport: '' };
|
||||
}
|
||||
|
||||
const transportOptions = [
|
||||
{ value: 'flight', label: '✈ Flight' },
|
||||
@@ -49,8 +57,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Suggest cities — when a country is selected show only cities from that country.
|
||||
let cityOptions = $derived(
|
||||
[...new Set(get(journals).flatMap(e => e.location.cities))].sort()
|
||||
country.trim()
|
||||
? [...new Set(get(journals).filter(j => (j.location.country || '').toLowerCase() === country.trim().toLowerCase()).flatMap(e => e.location.cities))].sort()
|
||||
: [...new Set(get(journals).flatMap(e => e.location.cities))].sort()
|
||||
);
|
||||
|
||||
function addCity(val) {
|
||||
@@ -65,31 +76,45 @@
|
||||
cities = cities.filter(x => x !== c);
|
||||
}
|
||||
|
||||
function save() {
|
||||
if (isNew) {
|
||||
addJournal({
|
||||
title: `${cities.join(', ')}, ${country}`,
|
||||
date,
|
||||
days: Number(days),
|
||||
tripType,
|
||||
memo,
|
||||
photos,
|
||||
transport,
|
||||
location: { cities, country },
|
||||
});
|
||||
} else {
|
||||
updateJournal({
|
||||
...entry,
|
||||
date,
|
||||
days: Number(days),
|
||||
tripType,
|
||||
transport,
|
||||
memo,
|
||||
photos,
|
||||
location: { cities, country },
|
||||
});
|
||||
async function save() {
|
||||
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;
|
||||
|
||||
try {
|
||||
if (isNew) {
|
||||
await addJournal({
|
||||
title: `${cities.join(', ')}, ${country}`,
|
||||
date,
|
||||
days: Number(days),
|
||||
tripType,
|
||||
memo,
|
||||
photos,
|
||||
transport,
|
||||
location: { cities, country },
|
||||
});
|
||||
} else {
|
||||
await updateJournal({
|
||||
...entry,
|
||||
date,
|
||||
days: Number(days),
|
||||
tripType,
|
||||
transport,
|
||||
memo,
|
||||
photos,
|
||||
location: { cities, country },
|
||||
});
|
||||
}
|
||||
onBack();
|
||||
} catch (err) {
|
||||
showToast('Failed to save. Please try again.');
|
||||
}
|
||||
onBack();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -104,7 +129,7 @@
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
<span class="topbar-title">{isNew ? 'New entry' : 'Edit'}</span>
|
||||
<span class="topbar-title">{isNew ? 'New trip' : 'Edit'}</span>
|
||||
<div class="topbar-right">
|
||||
<button class="topbar-btn topbar-btn--save" onclick={save}>Save changes</button>
|
||||
</div>
|
||||
@@ -117,12 +142,14 @@
|
||||
<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 />
|
||||
{#if errors.country}<span class="field-error">{errors.country}</span>{/if}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="edit-city">Cities <span class="req">*</span></label>
|
||||
<div class="city-input-row">
|
||||
<SearchInput id="edit-city" bind:value={cityInput} options={cityOptions} onselect={addCity} />
|
||||
</div>
|
||||
{#if errors.cities}<span class="field-error">{errors.cities}</span>{/if}
|
||||
{#if cities.length > 0}
|
||||
<div class="city-tags">
|
||||
{#each cities as c}
|
||||
@@ -140,10 +167,12 @@
|
||||
<div class="field">
|
||||
<label class="label" for="edit-date">Date <span class="req">*</span></label>
|
||||
<input id="edit-date" class="input" type="date" bind:value={date} required />
|
||||
{#if errors.date}<span class="field-error">{errors.date}</span>{/if}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="edit-days">Days <span class="req">*</span></label>
|
||||
<input id="edit-days" class="input" type="number" min="1" bind:value={days} required />
|
||||
{#if errors.days}<span class="field-error">{errors.days}</span>{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -160,6 +189,7 @@
|
||||
<input type="radio" name="tripType" value="family" bind:group={tripType} /> With family
|
||||
</label>
|
||||
</div>
|
||||
{#if errors.tripType}<span class="field-error">{errors.tripType}</span>{/if}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
@@ -172,6 +202,7 @@
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
{#if errors.transport}<span class="field-error">{errors.transport}</span>{/if}
|
||||
</div>
|
||||
|
||||
<PhotoEditor {photos} onchange={(p) => (photos = p)} />
|
||||
@@ -190,6 +221,12 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.field-error {
|
||||
font-size: 11px;
|
||||
color: #dc2626;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.edit-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user