diff --git a/public/plane 2.webp b/public/plane 2.webp new file mode 100644 index 0000000..6003f20 Binary files /dev/null and b/public/plane 2.webp differ diff --git a/src/App.svelte b/src/App.svelte index 6aa1ff8..ec56a01 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -62,13 +62,14 @@ {/if} - + {#if !journeyActive}{/if} {:else} (inDetail = v)} {pendingCountry} onNewEntryClear={() => (pendingCountry = '')} + onGoToMap={() => { screen = 'worldmap'; }} /> {/if} @@ -139,4 +140,5 @@ .journey-play-btn:active { transform: scale(0.92); } + diff --git a/src/assets 2/airplane 2.png b/src/assets 2/airplane 2.png new file mode 100644 index 0000000..f645cfe Binary files /dev/null and b/src/assets 2/airplane 2.png differ diff --git a/src/assets 2/bus 2.png b/src/assets 2/bus 2.png new file mode 100644 index 0000000..c667c20 Binary files /dev/null and b/src/assets 2/bus 2.png differ diff --git a/src/assets 2/car 2.png b/src/assets 2/car 2.png new file mode 100644 index 0000000..e3e2522 Binary files /dev/null and b/src/assets 2/car 2.png differ diff --git a/src/assets 2/ship 2.png b/src/assets 2/ship 2.png new file mode 100644 index 0000000..d16da5f Binary files /dev/null and b/src/assets 2/ship 2.png differ diff --git a/src/assets 2/train 2.png b/src/assets 2/train 2.png new file mode 100644 index 0000000..9392332 Binary files /dev/null and b/src/assets 2/train 2.png differ diff --git a/src/assets 2/walk 2.png b/src/assets 2/walk 2.png new file mode 100644 index 0000000..3630327 Binary files /dev/null and b/src/assets 2/walk 2.png differ diff --git a/src/assets/logo-1.png b/src/assets/logo-1.png new file mode 100644 index 0000000..021a88f Binary files /dev/null and b/src/assets/logo-1.png differ diff --git a/src/assets/logo-2.png b/src/assets/logo-2.png new file mode 100644 index 0000000..b75a85b Binary files /dev/null and b/src/assets/logo-2.png differ diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 0000000..30d14b6 Binary files /dev/null and b/src/assets/logo.png differ diff --git a/src/lib/auth/LoginOverlay.svelte b/src/lib/auth/LoginOverlay.svelte index 18ea4d0..047f74a 100644 --- a/src/lib/auth/LoginOverlay.svelte +++ b/src/lib/auth/LoginOverlay.svelte @@ -1,12 +1,13 @@
- -

Map Journal

-

Sign in to start your journey

+ +

Journi

+

Collect Colors Along the Way

+
+ +
+ {#each [1,2,3] as s} +
s}>
+ {/each} +
+ +
+ {#if step < 3} + + {:else} + + {/if} +
+ + +
+
+ + {#if step === 1} + +

Trip details

+ +
+
+ + + {#if errors.country}{errors.country}{/if} +
+
+ + + {#if errors.cities}{errors.cities}{/if} + {#if cities.length > 0} +
+ {#each cities as c} + {c} + {/each} +
+ {/if} +
+
+ +
+
+ + + {#if errors.date}{errors.date}{/if} +
+
+ + + {#if errors.days}{errors.days}{/if} +
+
+ +
+ +
+ {#each ['solo','friends','family'] as t} + + {/each} +
+ {#if errors.tripType}{errors.tripType}{/if} +
+ +
+ +
+ {#each transportOptions as opt} + + {/each} +
+ {#if errors.transport}{errors.transport}{/if} +
+ + {:else if step === 2} + +

Photos

+

Optional — add photos from your trip

+ (photos = p)} /> + + {:else} + +

Your memories

+ + {#each questions as q, i} +
+

{q}

+ +
+ {/each} + {/if} + +
+
+
+ + diff --git a/src/lib/timeline 2/NewEntryForm 3.svelte b/src/lib/timeline 2/NewEntryForm 3.svelte new file mode 100644 index 0000000..829ba49 --- /dev/null +++ b/src/lib/timeline 2/NewEntryForm 3.svelte @@ -0,0 +1,465 @@ + + +
+
+
+ +
+ +
+ {#each [1,2,3] as s} +
s}>
+ {/each} +
+ +
+ {#if step < 3} + + {:else} + + {/if} +
+
+ +
+
+ + {#if step === 1} + +

Trip details

+ +
+
+ + + {#if errors.country}{errors.country}{/if} +
+
+ + + {#if errors.cities}{errors.cities}{/if} + {#if cities.length > 0} +
+ {#each cities as c} + {c} + {/each} +
+ {/if} +
+
+ +
+
+ + + {#if errors.date}{errors.date}{/if} +
+
+ + + {#if errors.days}{errors.days}{/if} +
+
+ +
+ +
+ {#each ['solo','friends','family'] as t} + + {/each} +
+ {#if errors.tripType}{errors.tripType}{/if} +
+ +
+ +
+ {#each transportOptions as opt} + + {/each} +
+ {#if errors.transport}{errors.transport}{/if} +
+ + {:else if step === 2} + +

Photos

+

Optional — add photos from your trip

+ (photos = p)} /> + + {:else} + +

Your memories

+ + {#each questions as q, i} +
+

{q}

+ +
+ {/each} + {/if} + +
+
+
+ + diff --git a/src/lib/timeline/NewEntryForm.svelte b/src/lib/timeline/NewEntryForm.svelte index 829ba49..07c661c 100644 --- a/src/lib/timeline/NewEntryForm.svelte +++ b/src/lib/timeline/NewEntryForm.svelte @@ -11,7 +11,7 @@ import shipImg from '../../assets/ship.png'; import walkImg from '../../assets/walk.png'; - let { initialCountry = '', onBack } = $props(); + let { initialCountry = '', onBack, onSaved = onBack } = $props(); // ── Fields ───────────────────────────────────────────────────────── let cities = $state([]); @@ -124,7 +124,7 @@ photos, location: { cities, country }, }); - onBack(); + onSaved(); } catch { saving = false; } @@ -172,7 +172,7 @@
- + {#if errors.cities}{errors.cities}{/if} {#if cities.length > 0}
@@ -369,6 +369,25 @@ .ferr { font-size: 11px; color: #dc2626; } + .combo-select, .city-text { + 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; + display: block; + } + .combo-select:focus, .city-text:focus { border-color: var(--accent-border); } + .combo-select { margin-bottom: 6px; cursor: pointer; } + .city-text { margin-top: 0; } + .input { font-family: var(--sans); font-size: 14px; diff --git a/src/lib/timeline/TimelineView.svelte b/src/lib/timeline/TimelineView.svelte index de7d0d3..c1b6dcf 100644 --- a/src/lib/timeline/TimelineView.svelte +++ b/src/lib/timeline/TimelineView.svelte @@ -9,7 +9,7 @@ import ShareCard from './ShareCard.svelte'; import SharePreview from './SharePreview.svelte'; - let { onDetailChange = () => {}, pendingCountry = '', onNewEntryClear = () => {} } = $props(); + let { onDetailChange = () => {}, pendingCountry = '', onNewEntryClear = () => {}, onGoToMap = () => {} } = $props(); let selectedId = $state(/** @type {string|null} */(null)); let view = $state(/** @type {'list'|'detail'|'edit'|'new'} */('list')); let showShare = $state(false); @@ -55,7 +55,7 @@ {#if view === 'new'}
- { view = 'list'; newEntryCountry = ''; onDetailChange(false); }} /> + { view = 'list'; newEntryCountry = ''; onDetailChange(false); }} onSaved={() => { onGoToMap(); }} />
{:else if view === 'edit' && selected}
@@ -275,38 +275,4 @@ } .new-btn:hover { background: var(--accent-dark); border-color: var(--accent-dark); } - .share-nudge { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - padding: 10px 14px; - margin-bottom: 12px; - border-radius: 10px; - border: 1px dashed var(--border-bright); - background: var(--bg-subtle); - cursor: pointer; - transition: border-color 0.15s, background 0.15s; - font-family: var(--sans); - text-align: left; - } - .share-nudge:hover { - border-color: var(--accent-light); - background: var(--accent-bg); - } - .nudge-left { - display: inline-flex; - align-items: center; - gap: 7px; - font-size: 13px; - font-weight: 400; - color: var(--text-h); - } - .nudge-right { - font-size: 11px; - font-weight: 300; - color: var(--text-sub); - letter-spacing: 0.02em; - } - .share-nudge:hover .nudge-right { color: var(--accent); } diff --git a/src/lib/world-map/JourneyView 2.svelte b/src/lib/world-map/JourneyView 2.svelte new file mode 100644 index 0000000..ce8c7b7 --- /dev/null +++ b/src/lib/world-map/JourneyView 2.svelte @@ -0,0 +1,391 @@ + + +
+ + {#if isFinished} +
Journey complete!
+ {/if} +
+ + diff --git a/src/lib/world-map/JourneyView.svelte b/src/lib/world-map/JourneyView.svelte index ce8c7b7..cb5d735 100644 --- a/src/lib/world-map/JourneyView.svelte +++ b/src/lib/world-map/JourneyView.svelte @@ -3,19 +3,27 @@ import * as d3 from 'd3'; import { feature } from 'topojson-client'; import worldData from 'world-atlas/countries-50m.json'; + import { get } from 'svelte/store'; + import { journals } from '../stores/journalStore.js'; + import airplaneImg from '../../assets/airplane.png'; + import trainImg from '../../assets/train.png'; + import busImg from '../../assets/bus.png'; + import carImg from '../../assets/car.png'; + import shipImg from '../../assets/ship.png'; + import walkImg from '../../assets/walk.png'; let { onclose, onprogress } = $props(); const HOME_CODE = '203'; - const MOCK_TRIPS = [ - { countryName: 'Japan', countryCode: '392', date: '2024-03-15', city: 'Tokyo' }, - { countryName: 'France', countryCode: '191', date: '2024-06-20', city: 'Paris' }, - { countryName: 'Spain', countryCode: '724', date: '2024-09-10', city: 'Barcelona' }, - { countryName: 'United States', countryCode: '840', date: '2025-01-05', city: 'New York' }, - { countryName: 'Thailand', countryCode: '764', date: '2025-04-18', city: 'Bangkok' }, - { countryName: 'Australia', countryCode: '036', date: '2025-08-22', city: 'Sydney' }, - ]; + const TRANSPORT_IMG = { + flight: airplaneImg, + train: trainImg, + bus: busImg, + car: carImg, + ship: shipImg, + walk: walkImg, + }; const HOME_COLOR = '#8b5cf6'; const VISITED_COLOR = '#22c55e'; @@ -128,7 +136,7 @@ }); } - async function animateTrip(destCode, destFeature) { + async function animateTrip(destCode, destFeature, transport = 'flight') { if (!homeFeature || !destFeature) return; const homeCentroid = d3.geoCentroid(homeFeature); @@ -142,6 +150,10 @@ if (!pathData) return; + const iconSrc = TRANSPORT_IMG[transport] ?? airplaneImg; + const iconSize = 28; + const iconHalf = iconSize / 2; + function createArc(pathData) { const el = g.append('path') .attr('d', pathData) @@ -150,9 +162,12 @@ .attr('stroke-width', 2.5) .attr('stroke-opacity', 0.8) .attr('stroke-linecap', 'round'); - const tip = g.append('path') - .attr('d', PLANE_PATH) - .attr('fill', PLANE_COLOR) + const tip = g.append('image') + .attr('href', iconSrc) + .attr('width', iconSize) + .attr('height', iconSize) + .attr('x', -iconHalf) + .attr('y', -iconHalf) .attr('opacity', 0); return { el, tip }; } @@ -216,7 +231,30 @@ isFinished = false; isCancelled = false; - const trips = MOCK_TRIPS; + // Build name → numeric ID map from loaded features + const nameToId = {}; + for (const [id, feat] of Object.entries(featuresById)) { + if (feat.properties?.name) nameToId[feat.properties.name] = id; + } + + // Use real journal entries, sorted by date + const entries = get(journals).slice().sort((a, b) => a.date.localeCompare(b.date)); + const trips = entries.length > 0 + ? entries.map(e => ({ + countryName: e.location.country, + countryCode: nameToId[e.location.country] ?? null, + city: e.location.cities[0] ?? e.location.country, + transport: e.transport ?? 'flight', + })).filter(t => t.countryCode) + : [ + { countryName: 'Japan', countryCode: '392', city: 'Tokyo', transport: 'flight' }, + { countryName: 'France', countryCode: '250', city: 'Paris', transport: 'flight' }, + { countryName: 'Spain', countryCode: '724', city: 'Barcelona', transport: 'flight' }, + { countryName: 'United States of America', countryCode: '840', city: 'New York', transport: 'flight' }, + { countryName: 'Thailand', countryCode: '764', city: 'Bangkok', transport: 'flight' }, + { countryName: 'Australia', countryCode: '036', city: 'Sydney', transport: 'flight' }, + ]; + const total = trips.length; for (let i = 0; i < total; i++) { @@ -229,7 +267,7 @@ const label = `${trip.city}, ${trip.countryName}`; if (onprogress) onprogress({ index: i + 1, total, label }); - await animateTrip(trip.countryCode, destFeature); + await animateTrip(trip.countryCode, destFeature, trip.transport); } if (!isCancelled) { @@ -351,14 +389,14 @@ .close-btn { position: absolute; - top: 12px; - right: 12px; + bottom: 24px; + right: 24px; z-index: 10; - width: 36px; - height: 36px; + width: 44px; + height: 44px; border-radius: 50%; border: none; - background: rgba(0,0,0,0.55); + background: #8b5cf6; color: #fff; font-size: 18px; line-height: 1; @@ -366,11 +404,17 @@ display: flex; align-items: center; justify-content: center; - transition: background 0.15s ease; + box-shadow: 0 2px 12px rgba(139, 92, 246, 0.4); + transition: background 0.15s ease, transform 0.1s ease, box-shadow 0.15s ease; } .close-btn:hover { - background: rgba(0,0,0,0.75); + background: #7c3aed; + box-shadow: 0 4px 18px rgba(139, 92, 246, 0.55); + } + + .close-btn:active { + transform: scale(0.92); } .done-badge { diff --git a/src/lib/world-map/WorldMap.svelte b/src/lib/world-map/WorldMap.svelte index cc061a3..97db120 100644 --- a/src/lib/world-map/WorldMap.svelte +++ b/src/lib/world-map/WorldMap.svelte @@ -3,7 +3,7 @@ import * as d3 from 'd3'; import { feature } from 'topojson-client'; import worldData from 'world-atlas/countries-50m.json'; - import { getSelected, toggle, setTotalCount, getHomeCountryCode } from '../layout/selection.svelte.js'; + import { getSelected, setTotalCount } from '../layout/selection.svelte.js'; let { onCountryClick = (_name) => {} } = $props(); @@ -84,10 +84,9 @@ function updateAllFills() { const sel = getSelected(); - const hc = getHomeCountryCode(); if (!_paths || !_g) return; - _paths.attr('fill', d => countryColor(d, sel, hc)); - _g.selectAll('.micro-state').attr('fill', d => countryColor(d, sel, hc)); + _paths.attr('fill', d => countryColor(d, sel, null)); + _g.selectAll('.micro-state').attr('fill', d => countryColor(d, sel, null)); } $effect(updateAllFills); @@ -123,24 +122,14 @@ .attr('class', 'tooltip') .style('display', 'none'); - function updateFill(sel) { - const s = getSelected(); - const hc = getHomeCountryCode(); - sel.attr('fill', d => countryColor(d, s, hc)); - _g.selectAll('.micro-state').attr('fill', d => countryColor(d, s, hc)); - } - function attachEvents(sel) { sel .on('click', (event, d) => { - toggle(effId(d)); - updateFill(d3.select(event.currentTarget)); onCountryClick(d.properties.name); }) .on('mouseenter', (event, d) => { const s = getSelected(); - const hc = getHomeCountryCode(); - d3.select(event.currentTarget).attr('fill', countryHoverColor(d, s, hc)); + d3.select(event.currentTarget).attr('fill', countryHoverColor(d, s, null)); tooltip.style('display', 'block').text(d.properties.name); }) .on('mousemove', (event) => { @@ -149,8 +138,7 @@ }) .on('mouseleave', (event, d) => { const s = getSelected(); - const hc = getHomeCountryCode(); - d3.select(event.currentTarget).attr('fill', countryColor(d, s, hc)); + d3.select(event.currentTarget).attr('fill', countryColor(d, s, null)); tooltip.style('display', 'none'); }); } @@ -172,14 +160,13 @@ const { width, height } = this.getBBox(); if (width < threshold && height < threshold) { const [cx, cy] = path.centroid(d); - const hc = getHomeCountryCode(); const c = _g.append('circle') .attr('class', 'micro-state') .datum(d) .attr('cx', cx) .attr('cy', cy) .attr('r', 2) - .attr('fill', countryColor(d, getSelected(), hc)) + .attr('fill', countryColor(d, getSelected(), null)) .attr('stroke', '#94a3b8') .attr('stroke-width', 0.5); attachEvents(c);