@@ -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'}
@@ -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);