18 Commits

Author SHA1 Message Date
522e41cb79 Update README.md 2026-06-17 02:24:15 +00:00
caaae887f9 fix: animation bug fix 2026-06-17 11:21:20 +09:00
3ae1b25b0c Merge branch 'main' of https://git.prototyping.id/20256426/Map-Jurnal 2026-06-17 09:00:54 +09:00
e6bc251386 final cleanup 2026-06-17 09:00:20 +09:00
4e1981e1f6 Update README.md 2026-06-16 17:15:17 +00:00
59b89fb276 Aktualizovat README.md 2026-06-16 16:50:57 +00:00
5231b0ead7 update README 2026-06-16 16:50:25 +00:00
6fc0172ec3 updated README 2026-06-17 01:45:32 +09:00
c27200d9cd Aktualizovat README.md 2026-06-16 16:26:42 +00:00
8fbe40f178 fix; timeline sorting 2026-06-17 01:20:32 +09:00
8b52437655 Merge branch 'main' of https://git.prototyping.id/20256426/Map-Jurnal 2026-06-17 01:15:52 +09:00
5e36f6e140 bug fix 2026-06-17 01:15:43 +09:00
2f49c35128 Aktualizovat README.md 2026-06-16 16:13:51 +00:00
3e43d09723 published the website 2026-06-17 01:04:45 +09:00
0f1e43d82d Aktualizovat README.md 2026-06-16 15:54:33 +00:00
944b73d215 Aktualizovat README.md 2026-06-16 15:54:00 +00:00
f22b0f1b28 Aktualizovat README.md 2026-06-16 15:49:34 +00:00
5e943df6ef Aktualizovat README.md 2026-06-16 15:46:31 +00:00
14 changed files with 104 additions and 253 deletions

View File

@@ -0,0 +1,17 @@
logo.png,1781605272907,e1d8cae38fa1d4a97f6847ccdc69dae37d8b481e6862625b6d8a6565137a622b
index.html,1781628774437,ba46c34c4641f9cba330944cbd3f9b21ee4cb43addcf58dc1876428f5aef2c0c
assets/index-DvsKFHEX.css,1781628774437,5c4f357ff59c59917ce7c70ee28b29bc28c9d7eec3f6afd12e4c2cc8120537e1
assets/walk-2q6BKbeA.png,1781628774437,19a9f5d599221a0077761a7777285ca17dbc2775a9c4096304098602f2067b87
assets/airplane-DW4h3HkK.png,1781628774437,8d70ae12e46f5abdb4e1ccdf59af4bcab1805229d02f1260321f1f61d6375af1
assets/ship-xaUNk1p4.png,1781628774438,58b126e20c22710d5af92a45b6ec8af6e0b3dea30a743f9e8b91e849ed66cd91
assets/car-C6BGShf6.png,1781628774437,ac10c46e0a8245a9e52b8c3cebe110112f57764c9bda1fe93d4fbc077e79a1b2
assets/bus-Dy8WA7au.png,1781628774437,8285587abd857cce3b7083e799a8d2a95b019cfc40546e914cc2b8348c448c10
assets/default-3-BcrUpMyO.jpeg,1781628774448,f10d24e80f9f94ddf40d1103ea18f96214e89c18102c75be6b816c8ee0a2b4b9
assets/logo-signin-BAMDzBb4.png,1781628774437,7e5d35cac2cea449b27ef72e386e66e1b9c8cbb9261a26b85fda4cb2d89ac226
assets/train-DAjH23UQ.png,1781628774437,9b3a2b3b0376f74a47daf69b2f202d757fd8425edede28a8263d2ef5994ed5f2
assets/default-2-CoZoomA5.jpeg,1781628774448,fbdb512018aba7f68167796632defe75019af5432ed0bfc48d0e24851c47261f
assets/profile-lvFMnGZN.png,1781628774437,0892c6fcffa6be93a86ddedc09b6c26ad725fd1dd66479b4382dbfddcb6d392f
assets/default-1-CZLThRLv.jpeg,1781628774448,72f1614c5862c5fa1b79db6130985b681cd971e4c415758990dc1fd3c9eee1a4
assets/home-D9_bbHBx.png,1781628774423,201c0fa3a0d38a8ce25c5494d0bd1f2d5d7e0f03fb07cd9c34b3e7593858899f
DB-diagram.PNG,1781628177434,978dcc3d9b7e059510c59d5c8a836018dfdb4b6cf59a9e362a89a5a6f9cb6f26
assets/index-gfLawFjf.js,1781628774466,b5e58d7bfb36663198ac3fbbece0600acef7c8b8822ab9c1132cfc0a68ce214a

135
README.md
View File

@@ -1,23 +1,29 @@
# Map Journal # Map Journal
**Author:** _[Your Name]_ **Authors:** Tomas Horsky, Haeri Kim
**Student ID:** _[Your ID]_
**Email:** _[Your Email]_ **Student IDs:** 20256426, 20254236
**Emails:** tomashorsky@kaist.ac.kr , kimhaeri@kaist.ac.kr
**Live website:** <https://map-jurnal.web.app/>
**Repository:** <https://git.prototyping.id/20256426/Map-Jurnal.git> **Repository:** <https://git.prototyping.id/20256426/Map-Jurnal.git>
**Video Demo:** _[YouTube URL]_
**Video Demo:** https://youtu.be/j8-Ue4wanRU
--- ---
## Overview ## Overview
Map Journal is a single-page web application for documenting and visualizing travel experiences. Users log trips by country, pinning cities, dates, transport modes, and photos, and then explore their journey on an interactive world map or a timeline view. Journi is a web application for journaling and visualizing travel experiences. Users log trips by country, pinning cities, dates, transport modes, and photos. Then user can explore their journey on an interactive world map, timeline view or can play short animations overviewing his trips.
### How It Works ### How It Works
1. **Sign in** with a Google account. 1. **Sign in** with a Google account.
2. **Select your home country** — it is automatically marked as visited on the map. 2. **Select your home country** — it is automatically marked as visited on the map.
3. **Add journal entries** via a multi-step form: 3. **Add journal entries**:
- **Step 0** — Click on country in map you visited or click add trip in timeline.
- **Step 1** — Choose country, cities, arrival date, days stayed, trip type (solo/friends/family), and transport (flight/train/bus/car/ship/walk). - **Step 1** — Choose country, cities, arrival date, days stayed, trip type (solo/friends/family), and transport (flight/train/bus/car/ship/walk).
- **Step 2** — Upload photos (stored in Firebase Storage). - **Step 2** — Upload photos (stored in Firebase Storage).
- **Step 3** — Answer three random reflective questions about the trip (e.g., *"What was the most unexpected thing that happened?"*). - **Step 3** — Answer three random reflective questions about the trip (e.g., *"What was the most unexpected thing that happened?"*).
@@ -28,6 +34,13 @@ Map Journal is a single-page web application for documenting and visualizing tra
--- ---
## Backend — Firebase
![Firestore diagram](public/DB-diagram.PNG)
Journi uses Firebase as its backend. Users sign in with Google through Firebase Authentication, and their data is linked to their user ID. Firestore stores user profiles in `users/{uid}` and journal entries in `users/{uid}/entries/{id}`. Firebase Storage stores uploaded trip photos. Firebase Security Rules protect the data, so authenticated users can only read and write their own profiles, entries, and photos.
---
## Code Organization ## Code Organization
``` ```
@@ -81,92 +94,7 @@ src/
└── SharePreview.svelte Share card preview modal └── SharePreview.svelte Share card preview modal
``` ```
### Architecture
**Component tree** (simplified):
```
App.svelte
└── Layout.svelte
├── LoginOverlay.svelte
├── TopBar.svelte
├── CountryPicker.svelte
├── WorldMap.svelte
│ └── JourneyView.svelte
│ └── StatsPanel.svelte
└── TimelineView.svelte
├── TimelineCard.svelte
├── JournalDetail.svelte
│ ├── DeleteConfirm.svelte
│ └── EditForm.svelte
│ ├── StepNavbar.svelte
│ ├── TripBasicInfo.svelte
│ └── PhotoEditor.svelte
├── NewEntryForm.svelte
│ ├── StepNavbar.svelte
│ ├── PhotoEditor.svelte
│ └── ...
├── ShareCard.svelte
└── SharePreview.svelte
```
### Data Flow
```
Firebase Auth ──→ userStore.svelte.js ──→ Layout (auth guard)
Firebase Firestore ──→ entriesStore.svelte.js ──→ Timeline, Map (via $state/$derived)
Firebase Storage ──→ PhotoEditor.svelte (upload)
selection.svelte.js ──→ visited set (derived from entries + home country)
```
### Key Patterns
- **Svelte 5 runes** — `$state`, `$derived`, `$effect`, `$bindable`, `$props` replace the old Svelte store/reactivity model.
- **$bindable props** — `TripBasicInfo` uses `$bindable()` for two-way binding with its parent form, keeping form state in the parent while delegating UI rendering.
- **Firebase listeners** — `entriesStore` uses `onSnapshot` for real-time Firestore sync; `userStore` uses `onAuthStateChanged`.
- **D3.js** — `WorldMap` renders a GeoJSON world map with centered zoom via `d3-geo`; `JourneyView` animates SVG flight paths.
- **No framework router** — the app uses a simple `mode` state variable (`'map' | 'journal'`) in `App.svelte` to switch between views.
---
## Features
| Feature | Details |
|---------|---------|
| Google sign-in | Firebase Auth with `GoogleAuthProvider` |
| Interactive world map | D3projected map with country highlighting, zoom, and pan |
| Animated journeys | Flightpath arcs between entries, played sequentially |
| Multistep forms | 3step wizard for both new and edit modes |
| Photo upload | Firebase Storage with CORS support, error display |
| Random trip questions | 10 curated prompts, 3 randomly chosen per entry |
| Share card | Autogenerated trip summary image via `html-to-image` |
| Home country marker | 16×16 icon placed at country centroid on map |
| Tooltips | Country name shown on hover, offset 22px from cursor |
---
## Known Bugs & Limitations
- **Photo Editor** — The `.add-btn` CSS class is unused (replaced by `.add-cell`); leftover from refactoring.
- **ShareCard** — Several CSS classes (`.stat-grid`, `.stat-box`, `.cont-section`, etc.) are defined but unused in the template; they were intended for a statistics section that was not implemented.
- **Photo upload error** — If Firebase Storage upload fails, the error is displayed but the photo grid does not automatically roll back the failed entry.
- **State_referenced_locally warnings** — `EditForm.svelte` initializes `$state` variables from `$props()` at declaration; this is intentional (onetime init on edit mode) but Svelte 5's analyzer emits warnings.
- **A11y warnings** — Some form labels lack `for`/`id` associations (transport pills, triptype toggles); these are visualonly controls where the label wraps the input, which is functional but not strictly valid per WAIARIA.
---
## Dependencies
| Package | Purpose |
|---------|---------|
| `svelte` ^5.55 | UI framework (runes, snippets, `$props`) |
| `firebase` ^12 | Auth, Firestore, Storage |
| `d3` ^7 | World map projection and SVG rendering |
| `topojson-client` | Convert TopoJSON → GeoJSON for map data |
| `world-atlas` | Country boundary data |
| `html-to-image` | Generate share card PNG |
---
## Setup & Run ## Setup & Run
@@ -182,25 +110,24 @@ npm install
# VITE_FIREBASE_MESSAGING_SENDER_ID=... # VITE_FIREBASE_MESSAGING_SENDER_ID=...
# VITE_FIREBASE_APP_ID=... # VITE_FIREBASE_APP_ID=...
# Dev server # Run server
npm run dev npm run dev
# Production build
npm run build
``` ```
--- ---
## Known Issues
- **Globe animation** — The D3 globe rotation animation during journey replay can be a little bit laggy.
- **Switching between animations** - after switching from map to globe animation, the old animation might be still visible for a moment. Restatarting the animation fixes the issue.
---
## Acknowledgments ## Acknowledgments
- **D3.js** — [Mike Bostock](https://d3js.org/) for the visualization library. - **Opencode & Claude Code (Sonnet 4.6) ** - researching, writing code, debuging... (https://opencode.ai/)
- **world-atlas** — [Topojson world data](https://github.com/topojson/world-atlas) by Mike Bostock. - **Gemini Nanobanana ** - generating logo and illustrations
- **Firebase** — Google for the backend suite (Auth, Firestore, Storage). - **D3.js docs** — help with using D3.js library (https://d3js.org/)
- **Svelte** — [Svelte team](https://svelte.dev/) for the frontend framework. - **D3.js world tour example** — inspiration for adding animation (https://observablehq.com/@d3/world-tour)
- **html-to-image** — [tsayen](https://github.com/bubkoo/html-to-image) for DOM-to-image capture.
- **Flag emoji** — Country-to-flag mapping based on regional indicator symbols.
- **Cursor icon** — Derived from the SVG airplane asset used in the app.
---
_This project was developed as part of a software prototyping course._

View File

@@ -1,5 +1,12 @@
{ {
"storage": { "storage": {
"rules": "storage.rules" "rules": "storage.rules"
},
"hosting": {
"public": "dist",
"ignore": ["firebase.json", ".firebaserc"],
"rewrites": [
{ "source": "**", "destination": "/index.html" }
]
} }
} }

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "map-journal", "name": "map-journal",
"version": "0.0.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "map-journal", "name": "map-journal",
"version": "0.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"d3": "^7.9.0", "d3": "^7.9.0",
"firebase": "^12.14.0", "firebase": "^12.14.0",

BIN
public/DB-diagram.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -11,15 +11,24 @@
*/ */
let { entry = null, initialCountry = '', onBack } = $props(); let { entry = null, initialCountry = '', onBack } = $props();
// svelte-ignore state_referenced_locally
let isNew = !entry; let isNew = !entry;
// svelte-ignore state_referenced_locally
let cities = $state([...(entry?.location.cities ?? [])]); let cities = $state([...(entry?.location.cities ?? [])]);
// svelte-ignore state_referenced_locally
let country = $state(entry?.location.country ?? initialCountry); let country = $state(entry?.location.country ?? initialCountry);
// svelte-ignore state_referenced_locally
let date = $state(entry?.date ?? new Date().toISOString().slice(0, 10)); let date = $state(entry?.date ?? new Date().toISOString().slice(0, 10));
// svelte-ignore state_referenced_locally
let days = $state(String(entry?.days ?? '')); let days = $state(String(entry?.days ?? ''));
// svelte-ignore state_referenced_locally
let tripType = $state(entry?.tripType ?? ''); let tripType = $state(entry?.tripType ?? '');
// svelte-ignore state_referenced_locally
let photos = $state([...(entry?.photos ?? [])]); let photos = $state([...(entry?.photos ?? [])]);
// svelte-ignore state_referenced_locally
let memo = $state(entry?.memo ?? ''); let memo = $state(entry?.memo ?? '');
// svelte-ignore state_referenced_locally
let transport = $state(entry?.transport ?? ''); let transport = $state(entry?.transport ?? '');
let step = $state(1); let step = $state(1);

View File

@@ -84,9 +84,10 @@
<div class="photo-grid"> <div class="photo-grid">
{#each entry.photos as photo, i} {#each entry.photos as photo, i}
<div class="photo-cell" class:cell-wide={i === 0 && entry.photos.length > 1}> <div class="photo-cell" class:cell-wide={i === 0 && entry.photos.length > 1}>
<button type="button" class="photo-btn" onclick={() => lightboxSrc = photo}>
<img src={photo} alt="" <img src={photo} alt=""
onclick={() => lightboxSrc = photo} onerror={(e) => { e.currentTarget.closest('.photo-cell')?.classList.add('cell-broken'); }} />
onerror={(e) => e.currentTarget.parentElement.classList.add('cell-broken')} /> </button>
</div> </div>
{/each} {/each}
</div> </div>
@@ -254,21 +255,24 @@
grid-column: 1 / -1; grid-column: 1 / -1;
grid-row: span 2; grid-row: span 2;
} }
.photo-cell img { .photo-btn {
display: block;
width: 100%;
height: 100%;
padding: 0;
border: none;
background: none;
cursor: zoom-in;
}
.photo-btn img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
display: block; display: block;
transition: transform 0.2s ease; transition: transform 0.2s ease;
} }
.photo-cell:hover img { transform: scale(1.03); } .photo-cell:hover .photo-btn img { transform: scale(1.03); }
.photo-cell.cell-broken {
display: flex;
align-items: center;
justify-content: center;
color: var(--text-sub);
font-size: 12px;
}
.no-photos { .no-photos {
display: flex; display: flex;

View File

@@ -204,7 +204,7 @@
</div> </div>
<div class="field"> <div class="field">
<label class="label"><span class="kw">Who</span> did you go <span class="kw">with</span>? <span class="req">*</span></label> <span class="label"><span class="kw">Who</span> did you go <span class="kw">with</span>? <span class="req">*</span></span>
<div class="toggle-row"> <div class="toggle-row">
{#each ['solo','friends','family'] as t} {#each ['solo','friends','family'] as t}
<label class="toggle-opt" class:active={tripType === t}> <label class="toggle-opt" class:active={tripType === t}>
@@ -217,7 +217,7 @@
</div> </div>
<div class="field"> <div class="field">
<label class="label">How did you <span class="kw">get</span> there? <span class="req">*</span></label> <span class="label">How did you <span class="kw">get</span> there? <span class="req">*</span></span>
<div class="transport-grid"> <div class="transport-grid">
{#each transportOptions as opt} {#each transportOptions as opt}
<label class="toggle-opt" class:active={transport === opt.value}> <label class="toggle-opt" class:active={transport === opt.value}>

View File

@@ -66,7 +66,7 @@
</div> </div>
{/each} {/each}
<button type="button" class="add-cell" onclick={() => fileInput.click()} disabled={uploading}> <button type="button" class="add-cell" onclick={() => fileInput.click()} disabled={uploading} aria-label="Add photo">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"> <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
<path d="M12 5v14M5 12h14"/> <path d="M12 5v14M5 12h14"/>
</svg> </svg>
@@ -101,23 +101,6 @@
flex: 1; flex: 1;
} }
.add-btn {
display: inline-flex;
align-items: center;
gap: 5px;
font-family: var(--sans);
font-size: 12px;
font-weight: 300;
color: var(--accent);
background: var(--accent-bg);
border: 1px solid var(--accent-border);
border-radius: 6px;
padding: 4px 10px;
cursor: pointer;
transition: background 0.15s;
}
.add-btn:hover { background: rgba(124,58,237,0.12); }
.empty-zone { .empty-zone {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -90,7 +90,7 @@
</div> </div>
<div class="field"> <div class="field">
<label class="label"><span class="kw">Who</span> did you go <span class="kw">with</span>? <span class="req">*</span></label> <span class="label"><span class="kw">Who</span> did you go <span class="kw">with</span>? <span class="req">*</span></span>
<div class="toggle-row"> <div class="toggle-row">
{#each ['solo','friends','family'] as t} {#each ['solo','friends','family'] as t}
<label class="toggle-opt" class:active={tripType === t}> <label class="toggle-opt" class:active={tripType === t}>
@@ -103,7 +103,7 @@
</div> </div>
<div class="field"> <div class="field">
<label class="label">How did you <span class="kw">get</span> there? <span class="req">*</span></label> <span class="label">How did you <span class="kw">get</span> there? <span class="req">*</span></span>
<div class="transport-grid"> <div class="transport-grid">
{#each transportOptions as opt} {#each transportOptions as opt}
<label class="toggle-opt transport-opt" class:active={transport === opt.value}> <label class="toggle-opt transport-opt" class:active={transport === opt.value}>

View File

@@ -216,12 +216,6 @@
<span class="fact-text">Longest stay: <strong>{stats.longest.days} days</strong> in {stats.longest.location.cities.join(', ')}</span> <span class="fact-text">Longest stay: <strong>{stats.longest.days} days</strong> in {stats.longest.location.cities.join(', ')}</span>
</div> </div>
{/if} {/if}
{#if stats.flightHrs > 0}
<div class="fact">
<span class="fact-icon">✈️</span>
<span class="fact-text">~<strong>{stats.flightHrs} hrs</strong> crossing skies</span>
</div>
{/if}
{#if stats.favCountry && stats.favCountry[1] > 1} {#if stats.favCountry && stats.favCountry[1] > 1}
<div class="fact"> <div class="fact">
<span class="fact-icon">❤️</span> <span class="fact-icon">❤️</span>
@@ -407,42 +401,6 @@
margin: 8px 0; margin: 8px 0;
} }
/* Stat grid */
.stat-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
margin-bottom: 24px;
position: relative;
z-index: 1;
}
.stat-box {
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 10px;
padding: 10px 8px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.stat-num {
font-size: 22px;
font-weight: 400;
color: #fff;
letter-spacing: -0.5px;
line-height: 1;
margin-bottom: 4px;
}
.stat-desc {
font-size: 9px;
font-weight: 400;
color: rgba(255,255,255,0.4);
text-transform: uppercase;
letter-spacing: 0.1em;
}
/* Facts */ /* Facts */
.facts { .facts {
display: flex; display: flex;
@@ -470,42 +428,6 @@
} }
.fact-text strong { color: #fff; font-weight: 400; } .fact-text strong { color: #fff; font-weight: 400; }
/* Continent bar */
.cont-section {
position: relative;
z-index: 1;
margin-bottom: 20px;
}
.cont-bar {
display: flex;
height: 6px;
border-radius: 3px;
overflow: hidden;
gap: 2px;
margin-bottom: 8px;
}
.cont-seg { border-radius: 3px; min-width: 4px; }
.cont-legend {
display: flex;
flex-wrap: wrap;
gap: 6px 12px;
}
.cont-item {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 9px;
font-weight: 300;
color: rgba(255,255,255,0.5);
letter-spacing: 0.04em;
}
.cont-dot {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
}
/* Footer */ /* Footer */
.card-footer { .card-footer {
margin-top: auto; margin-top: auto;

View File

@@ -227,15 +227,4 @@
letter-spacing: 0.04em; letter-spacing: 0.04em;
} }
.pc-cta {
position: relative;
z-index: 1;
font-size: 11px;
font-weight: 400;
color: #a5b4fc;
letter-spacing: 0.04em;
padding-top: 4px;
border-top: 1px solid rgba(255,255,255,0.08);
text-align: right;
}
</style> </style>

View File

@@ -82,11 +82,11 @@
{:else} {:else}
<div class="sort-row"> <div class="sort-row">
<span class="sort-label">Sort by</span> <span class="sort-label">Sort by</span>
<select class="sort-select" onchange={(e) => (sortKey = e.currentTarget.value)}> <select class="sort-select" bind:value={sortKey}>
<option value="date-desc" selected={sortKey === 'date-desc'}>Newest first</option> <option value="date-desc">Newest first</option>
<option value="date-asc" selected={sortKey === 'date-asc'}>Oldest first</option> <option value="date-asc">Oldest first</option>
<option value="country-asc" selected={sortKey === 'country-asc'}>Country A Z</option> <option value="country-asc">Country A → Z</option>
<option value="country-desc" selected={sortKey === 'country-desc'}>Country Z A</option> <option value="country-desc">Country Z → A</option>
</select> </select>
</div> </div>
<ol class="v-list"> <ol class="v-list">

View File

@@ -5,11 +5,13 @@
import worldData from 'world-atlas/countries-50m.json'; import worldData from 'world-atlas/countries-50m.json';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { journals } from '../stores/entriesStore.svelte.js'; import { journals } from '../stores/entriesStore.svelte.js';
import { getUserProfile } from '../auth/userStore.svelte.js';
import { nameToId } from '../shared/countries.js';
import airplaneImg from '../../assets/airplane-animation.png'; import airplaneImg from '../../assets/airplane-animation.png';
let { onclose, onprogress, mode = 'map', onmodechange } = $props(); let { onclose, onprogress, mode = 'map', onmodechange } = $props();
const HOME_CODE = '203'; const HOME_CODE = $derived(nameToId[getUserProfile()?.homeCountry] ?? null);
const PLANE_SIZE = 26; const PLANE_SIZE = 26;
@@ -291,23 +293,14 @@
renderMap(); renderMap();
const nameToId = Object.fromEntries(Object.entries(featuresById).filter(([,f]) => f.properties?.name).map(([id, f]) => [f.properties.name, id])); const nameToId = Object.fromEntries(Object.entries(featuresById).filter(([,f]) => f.properties?.name).map(([id, f]) => [f.properties.name, id]));
const entries = get(journals).slice().sort((a, b) => a.date.localeCompare(b.date)); const entries = get(journals).slice().sort((a, b) => b.date.localeCompare(a.date));
const trips = entries.length > 0 const trips = entries.map(e => ({
? entries.map(e => ({
countryName: e.location.country, countryName: e.location.country,
countryCode: nameToId[e.location.country] ?? null, countryCode: nameToId[e.location.country] ?? null,
city: e.location.cities?.[0] ?? e.location.country, city: e.location.cities?.[0] ?? e.location.country,
transport: e.transport ?? 'flight', transport: e.transport ?? 'flight',
date: e.date, date: e.date,
})).filter(t => t.countryCode) })).filter(t => t.countryCode);
: [
{ countryName: 'Japan', countryCode: '392', city: 'Tokyo', transport: 'flight', date: '2024-03-15' },
{ countryName: 'France', countryCode: '250', city: 'Paris', transport: 'flight', date: '2024-06-20' },
{ countryName: 'Spain', countryCode: '724', city: 'Barcelona', transport: 'flight', date: '2024-09-10' },
{ countryName: 'United States of America', countryCode: '840', city: 'New York', transport: 'flight', date: '2025-01-05' },
{ countryName: 'Thailand', countryCode: '764', city: 'Bangkok', transport: 'flight', date: '2025-04-18' },
{ countryName: 'Australia', countryCode: '036', city: 'Sydney', transport: 'flight', date: '2025-08-22' },
];
for (let i = 0; i < trips.length; i++) { for (let i = 0; i < trips.length; i++) {
if (isCancelled || myId !== animId) break; if (isCancelled || myId !== animId) break;