diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..f3c9817 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "Map-Jurnal", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev"], + "port": 5173, + "autoPort": true + } + ] +} diff --git a/package-lock.json b/package-lock.json index bd6b99b..691083e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "d3": "^7.9.0", "firebase": "^12.14.0", + "flag-icons": "^7.5.0", "topojson-client": "^3.1.0", "world-atlas": "^2.0.2" }, @@ -746,14 +747,14 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.1" + "@tybys/wasm-util": "^0.10.2" }, "funding": { "type": "github", @@ -930,6 +931,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -947,6 +951,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -964,6 +971,9 @@ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -981,6 +991,9 @@ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -998,6 +1011,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1015,6 +1031,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1816,6 +1835,12 @@ "@firebase/util": "1.15.1" } }, + "node_modules/flag-icons": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-7.5.0.tgz", + "integrity": "sha512-kd+MNXviFIg5hijH766tt+3x76ele1AXlo4zDdCxIvqWZhKt4T83bOtxUOOMlTx/EcFdUMH5yvQgYlFh1EqqFg==", + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2035,6 +2060,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2056,6 +2084,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2077,6 +2108,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2098,6 +2132,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/package.json b/package.json index e1f8c88..7b3829b 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "dependencies": { "d3": "^7.9.0", "firebase": "^12.14.0", + "flag-icons": "^7.5.0", "topojson-client": "^3.1.0", "world-atlas": "^2.0.2" } diff --git a/src/App.svelte b/src/App.svelte index f769256..b59be82 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -5,7 +5,7 @@ import Layout from './lib/layout/Layout.svelte'; import WorldMap from './lib/world-map/WorldMap.svelte'; import StatsPanel from './lib/world-map/StatsPanel.svelte'; - import TimelineView from './lib/TimelineView.svelte'; + import TimelineView from './lib/timeline/TimelineView.svelte'; let screen = $state('worldmap'); @@ -61,9 +61,10 @@ } .worldmap-page { + flex: 1; display: flex; flex-direction: row; - width: 100%; + min-width: 0; height: 100%; } diff --git a/src/app.css b/src/app.css index 4a5d045..3af86af 100644 --- a/src/app.css +++ b/src/app.css @@ -1,11 +1,95 @@ -* { - margin: 0; - padding: 0; - box-sizing: border-box; +@import url('https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200;12..96,300;12..96,400&display=swap'); + +/* ── Color tokens ─────────────────────────────────────────── */ +:root { + --accent: #7c3aed; /* indigo-600 */ + --accent-dark: #5b21b6; + --accent-light: #a78bfa; + --accent-bg: rgba(124, 58, 237, 0.07); + --accent-border: rgba(124, 58, 237, 0.2); + + --lavender: #a78bfa; + --lavender-bg: rgba(167, 139, 250, 0.1); + + /* Light-first neutrals */ + --text: #52525b; /* zinc-600 */ + --text-h: #18181b; /* zinc-900 */ + --text-sub: #a1a1aa; /* zinc-400 */ + --bg: #ffffff; /* white */ + --bg-raised: #fafafa; /* off-white */ + --bg-subtle: #f4f4f5; /* zinc-100 */ + --border: #e4e4e7; /* zinc-200 */ + --border-bright: #d4d4d8; /* zinc-300 */ + --shadow: 0 4px 24px rgba(0,0,0,0.08); + + /* Typography */ + --sans: 'Bricolage Grotesque', system-ui, sans-serif; + --heading: 'Bricolage Grotesque', system-ui, sans-serif; + --mono: ui-monospace, Consolas, monospace; + + /* Type scale */ + --text-xs: 11px; + --text-sm: 13px; + --text-base: 14px; + --text-md: 16px; + --text-lg: 20px; + --text-xl: 28px; + --text-2xl: 40px; + + font-family: var(--sans); + font-size: var(--text-base); + line-height: 1.6; + font-weight: 300; + letter-spacing: 0.01em; + color: var(--text); + background: var(--bg); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } -html, body { +/* ── Reset ────────────────────────────────────────────────── */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, body, #app { width: 100%; height: 100%; overflow: hidden; } + +/* ── Text hierarchy ───────────────────────────────────────── */ +h1 { + font-size: var(--text-2xl); + font-weight: 400; + line-height: 1.1; + letter-spacing: -1px; + color: var(--text-h); +} + +h2 { + font-size: var(--text-xl); + font-weight: 400; + line-height: 1.15; + letter-spacing: -0.5px; + color: var(--text-h); +} + +h3 { + font-size: var(--text-lg); + font-weight: 300; + line-height: 1.3; + color: var(--text-h); +} + +h4, h5, h6 { + font-size: var(--text-md); + font-weight: 300; + color: var(--text-h); +} + +p { margin: 0; color: var(--text); } diff --git a/src/lib/JournalDetail.svelte b/src/lib/JournalDetail.svelte deleted file mode 100644 index 1cf348c..0000000 --- a/src/lib/JournalDetail.svelte +++ /dev/null @@ -1,339 +0,0 @@ - - -
- - - - {#if entry.photos?.length > 0} - - {/if} - -
-
- 📍 {entry.city}, {entry.countryName} - - {tripType(entry.companions) === 'solo' ? '🧍 Solo' : '👥 ' + companionText(entry.companions)} - - {#if entry.transportation} - {TRANSPORT_LABELS[entry.transportation] || entry.transportation} - {/if} -
- -

{entry.title}

- -
-
- Date - -
-
-
- Duration - {entry.days} {entry.days === 1 ? 'day' : 'days'} -
-
- -
-

{entry.memo}

-
- -
- -
- Soundtrack - {entry.song?.title || ''} - {entry.song?.artist || ''} -
-
-
-
- - diff --git a/src/lib/TimelineView.svelte b/src/lib/TimelineView.svelte deleted file mode 100644 index c1a0e4c..0000000 --- a/src/lib/TimelineView.svelte +++ /dev/null @@ -1,365 +0,0 @@ - - -{#if selected} - (selected = null)} /> -{:else} -
- -
-
-

Travel Journal

-

My Journey

-
-
- - -
-
- - {#if sortedEntries.length === 0} -

No journal entries yet.

- {:else} -
    - {#each sortedEntries as entry (entry.id)} - {@const idx = photoIdx[entry.id] ?? 0} -
  1. - -
    -
    - - · - {entry.city}, {entry.countryName} - · - {entry.days} {entry.days === 1 ? 'day' : 'days'} -
    -
    (selected = entry)} - onkeydown={(e) => e.key === 'Enter' && (selected = entry)}> - - {#if entry.photos?.length > 0} - - {/if} - -
    -

    {entry.title}

    - {#if entry.memo} -

    {entry.memo}

    - {/if} -
    - {#if entry.transportation && TRANSPORT_LABELS[entry.transportation]} - {TRANSPORT_LABELS[entry.transportation]} - {/if} - - - - - - {entry.song?.title || ''} - · - {entry.song?.artist || ''} - - {companionText(entry.companions)} - -
    -
    -
    -
    -
  2. - {/each} -
- {/if} - -
- {sortedEntries.length} {sortedEntries.length === 1 ? 'entry' : 'entries'} -
-
-{/if} - - diff --git a/src/lib/layout/Footer.svelte b/src/lib/layout/Footer.svelte index bee400e..d2517b1 100644 --- a/src/lib/layout/Footer.svelte +++ b/src/lib/layout/Footer.svelte @@ -1,19 +1,25 @@ + + diff --git a/src/lib/layout/Layout.svelte b/src/lib/layout/Layout.svelte index aafed57..ecff737 100644 --- a/src/lib/layout/Layout.svelte +++ b/src/lib/layout/Layout.svelte @@ -25,5 +25,9 @@ .main { overflow: hidden; position: relative; + display: flex; + flex-direction: row; + width: 100%; + height: 100%; } diff --git a/src/lib/layout/TopBar.svelte b/src/lib/layout/TopBar.svelte index 132ee86..40d884f 100644 --- a/src/lib/layout/TopBar.svelte +++ b/src/lib/layout/TopBar.svelte @@ -20,7 +20,6 @@
- Map Journal
@@ -66,15 +65,16 @@ diff --git a/src/lib/timeline/JournalDetail.svelte b/src/lib/timeline/JournalDetail.svelte new file mode 100644 index 0000000..fe43a0f --- /dev/null +++ b/src/lib/timeline/JournalDetail.svelte @@ -0,0 +1,401 @@ + + + +{#if lightboxSrc} + +{/if} + +
+ + +
+ +
+ {flagEmoji(entry.location.country)} +
+

{entry.location.city}

+

{entry.location.country}

+
+
+ +
+ {#if entry.photos.length === 0} +
No photos
+ {:else} +
+ {#each entry.photos as photo, i} +
1}> + lightboxSrc = photo} + onerror={(e) => e.currentTarget.parentElement.classList.add('cell-broken')} /> +
+ {/each} +
+ {/if} +
+
+ + +
+
+ + +
+ +
+

When did you go?

+

{formatDate(entry.date)}

+
+ +
+

How long did you stay?

+

{entry.days} {entry.days === 1 ? 'day' : 'days'}

+
+ +
+

Who did you go with?

+

+ {#if entry.tripType === 'solo'} + Just me — solo trip + {:else} + With friends + {/if} +

+
+ +
+

How was it?

+

{entry.memo}

+
+ +
+

Trip soundtrack

+
+
+ + + + + +
+
+

{entry.song.title}

+

{entry.song.artist}

+
+
+
+ +
+
+
+ + +
+ + +
+ +
+ + diff --git a/src/lib/timeline/JournalSummary.svelte b/src/lib/timeline/JournalSummary.svelte new file mode 100644 index 0000000..de0de9d --- /dev/null +++ b/src/lib/timeline/JournalSummary.svelte @@ -0,0 +1,215 @@ + + +{#if stats} +
+ +

My Journey

+

{stats.yearRange}

+ +
+ + +
+
+ {stats.countries.length} + Countries +
+
+ {stats.cities.length} + Cities +
+
+ {stats.totalDays} + Days abroad +
+
+ {stats.tripCount} + Trips +
+
+ +
+ + +
+ Most visited + {stats.topCountry[0]} + {stats.topCountry[1]} {stats.topCountry[1] === 1 ? 'trip' : 'trips'} +
+ + +
+ Latest trip + {stats.latest.location.city} + {stats.latest.location.country} +
+ +
+ + +
+ Trip style +
+
+
+
+ {stats.soloPct}% Solo + {100 - stats.soloPct}% Friends +
+
+ +
+{/if} + + diff --git a/src/lib/timeline/TimelineCard.svelte b/src/lib/timeline/TimelineCard.svelte new file mode 100644 index 0000000..ad4f8da --- /dev/null +++ b/src/lib/timeline/TimelineCard.svelte @@ -0,0 +1,320 @@ + + +
  • + + +
    + +
    + {flagEmoji(entry.location.country)} + {entry.location.country} +
    + + +
    e.key === 'Enter' && onClick()}> + + + + {entry.tripType === 'solo' ? 'Solo' : 'Friends'} + + + +
    0}> +
    + {#if mainPhoto} + { + e.currentTarget.style.display = 'none'; + e.currentTarget.nextElementSibling.style.display = 'flex'; + }} /> + + {:else} +
    + + + + + +
    + {/if} +
    + + {#if thumbPhotos.length > 0} +
    + {#each thumbPhotos as photo, i} +
    + { + e.currentTarget.style.display = 'none'; + e.currentTarget.nextElementSibling.style.display = 'flex'; + }} /> + + {#if i === 2 && extraCount > 0} +
    +{extraCount}
    + {/if} +
    + {/each} +
    + {/if} +
    + + +
    + {entry.location.city} +
    + {formatDate(entry.date)} + · + {entry.days} {entry.days === 1 ? 'day' : 'days'} +
    +
    + +
    +
    +
  • + + diff --git a/src/lib/timeline/TimelineToolbar.svelte b/src/lib/timeline/TimelineToolbar.svelte new file mode 100644 index 0000000..507b12f --- /dev/null +++ b/src/lib/timeline/TimelineToolbar.svelte @@ -0,0 +1,62 @@ + + +
    +

    My Journey

    +
    + +
    +
    + + diff --git a/src/lib/timeline/TimelineView.svelte b/src/lib/timeline/TimelineView.svelte new file mode 100644 index 0000000..a75b903 --- /dev/null +++ b/src/lib/timeline/TimelineView.svelte @@ -0,0 +1,168 @@ + + +
    + + {#if selected} +
    + (selected = null)} /> +
    + {:else} + + +
    +
    + (sortKey = k)} /> + + {#if sortedEntries.length === 0} +

    No journal entries yet.

    + {:else} +
      + {#each sortedEntries as entry, i (entry.id)} + {#if i === 0 || getYear(entry.date) !== getYear(sortedEntries[i - 1].date)} + + {/if} + (selected = entry)} /> + {/each} +
    + {/if} + +
    + {sortedEntries.length} {sortedEntries.length === 1 ? 'entry' : 'entries'} +
    +
    +
    + {/if} + +
    + + diff --git a/src/lib/world-map/StatsPanel.svelte b/src/lib/world-map/StatsPanel.svelte index 9e0e154..bb14ffa 100644 --- a/src/lib/world-map/StatsPanel.svelte +++ b/src/lib/world-map/StatsPanel.svelte @@ -137,12 +137,12 @@ {seg.cont} {/each} - + {:else} - - + + {/if}
    @@ -157,11 +157,11 @@ diff --git a/src/lib/world-map/continents.js b/src/lib/world-map/continents.js index 2eabc73..7a5ad24 100644 --- a/src/lib/world-map/continents.js +++ b/src/lib/world-map/continents.js @@ -210,4 +210,3 @@ for (const id of Object.keys(map)) { export function getContinent(id) { return map[id] ?? null; } -