From aadc80b7a80f80e3c3511f8663c38a1dfc3ef4cf Mon Sep 17 00:00:00 2001 From: Haeri Kim Date: Sun, 14 Jun 2026 10:24:45 +0900 Subject: [PATCH 1/4] added firebase & changed main tab color, uyear stand out --- .claude/launch.json | 1 + .env | 6 + src/App.svelte | 5 +- src/app.css | 2 +- src/lib/layout/Layout.svelte | 11 +- src/lib/layout/TopBar.svelte | 6 +- src/lib/timeline/JournalDetail.svelte | 359 ++++++++++++------------ src/lib/timeline/JournalSummary.svelte | 305 ++++++++++---------- src/lib/timeline/TimelineCard.svelte | 2 +- src/lib/timeline/TimelineToolbar.svelte | 26 +- src/lib/timeline/TimelineView.svelte | 78 ++++- 11 files changed, 409 insertions(+), 392 deletions(-) create mode 100644 .env diff --git a/.claude/launch.json b/.claude/launch.json index f3c9817..974415a 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -3,6 +3,7 @@ "configurations": [ { "name": "Map-Jurnal", + "cwd": "/Users/haerikim/Desktop/Map-Jurnal", "runtimeExecutable": "npm", "runtimeArgs": ["run", "dev"], "port": 5173, diff --git a/.env b/.env new file mode 100644 index 0000000..2deacb9 --- /dev/null +++ b/.env @@ -0,0 +1,6 @@ +VITE_FIREBASE_API_KEY=AIzaSyC_hZf9TpIIb4H7y7umUeYtFKD-guN_iR0 +VITE_FIREBASE_AUTH_DOMAIN=map-jurnal.firebaseapp.com +VITE_FIREBASE_PROJECT_ID=map-jurnal +VITE_FIREBASE_STORAGE_BUCKET=map-jurnal.firebasestorage.app +VITE_FIREBASE_MESSAGING_SENDER_ID=922587077950 +VITE_FIREBASE_APP_ID=1:922587077950:web:9f140f84468e306152606f diff --git a/src/App.svelte b/src/App.svelte index 35ecae6..df020c8 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -5,9 +5,10 @@ import TimelineView from './lib/timeline/TimelineView.svelte'; let screen = $state('worldmap'); + let inDetail = $state(false); - (screen = s)}> + (screen = s)} hideTopBar={inDetail}> {#if screen === 'worldmap'}
@@ -16,7 +17,7 @@
{:else} - + (inDetail = v)} /> {/if} diff --git a/src/app.css b/src/app.css index 3af86af..72f41fe 100644 --- a/src/app.css +++ b/src/app.css @@ -1,4 +1,4 @@ -@import url('https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200;12..96,300;12..96,400&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,300;12..96,400;12..96,500&display=swap'); /* ── Color tokens ─────────────────────────────────────────── */ :root { diff --git a/src/lib/layout/Layout.svelte b/src/lib/layout/Layout.svelte index ecff737..ea23cf4 100644 --- a/src/lib/layout/Layout.svelte +++ b/src/lib/layout/Layout.svelte @@ -2,11 +2,13 @@ import TopBar from './TopBar.svelte'; import Footer from './Footer.svelte'; - let { screen, onNavigate, children } = $props(); + let { screen, onNavigate, hideTopBar = false, children } = $props(); -
- +
+ {#if !hideTopBar} + + {/if}
{@render children()}
@@ -21,6 +23,9 @@ grid-template-rows: auto 1fr auto; overflow: hidden; } + .layout.no-topbar { + grid-template-rows: 1fr auto; + } .main { overflow: hidden; diff --git a/src/lib/layout/TopBar.svelte b/src/lib/layout/TopBar.svelte index 20dc095..fd0124b 100644 --- a/src/lib/layout/TopBar.svelte +++ b/src/lib/layout/TopBar.svelte @@ -58,8 +58,8 @@ } .nav-btn:hover { color: var(--text-h); } .nav-btn.active { - background: var(--bg); - color: var(--text-h); - box-shadow: 0 1px 4px rgba(0,0,0,0.08); + background: #7c3aed; + color: #fff; + box-shadow: 0 1px 4px rgba(124,58,237,0.25); } diff --git a/src/lib/timeline/JournalDetail.svelte b/src/lib/timeline/JournalDetail.svelte index fe43a0f..cf28b8e 100644 --- a/src/lib/timeline/JournalDetail.svelte +++ b/src/lib/timeline/JournalDetail.svelte @@ -35,149 +35,214 @@
- -
- -
- {flagEmoji(entry.location.country)} -
-

{entry.location.city}

-

{entry.location.country}

+ +
+
+ + +
+ + {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')} /> +
+ + +
+
+ + +
+ + +
+
+ {#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}
- {/each} + {/if}
- {/if}
-
- -
-
+ +
+
+
+
+

When did you go?

+

{formatDate(entry.date)}

+
-
+
+

How long did you stay?

+

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

+
-
-

When did you go?

-

{formatDate(entry.date)}

-
+
+

Who did you go with?

+

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

+
-
-

How long did you stay?

-

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

-
+
+

How was it?

+

{entry.memo}

+
-
-

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}

+
+

Trip soundtrack

+
+
+ + + + + +
+
+

{entry.song.title}

+

{entry.song.artist}

+
-
+
-
- -
- -
-
diff --git a/src/lib/timeline/JournalSummary.svelte b/src/lib/timeline/JournalSummary.svelte index de0de9d..959e638 100644 --- a/src/lib/timeline/JournalSummary.svelte +++ b/src/lib/timeline/JournalSummary.svelte @@ -9,207 +9,190 @@ const countries = [...new Set(entries.map(e => e.location.country))]; const cities = [...new Set(entries.map(e => e.location.city))]; - const countryCounts = {}; - for (const e of entries) countryCounts[e.location.country] = (countryCounts[e.location.country] ?? 0) + 1; - const topCountry = Object.entries(countryCounts).sort((a, b) => b[1] - a[1])[0]; - - const soloCount = entries.filter(e => e.tripType === 'solo').length; - const soloPct = Math.round(soloCount / entries.length * 100); - - const years = entries.map(e => new Date(e.date).getFullYear()); - const minYear = Math.min(...years); - const maxYear = Math.max(...years); + const years = entries.map(e => new Date(e.date).getFullYear()); + const minYear = Math.min(...years); + const maxYear = Math.max(...years); const yearRange = minYear === maxYear ? `${minYear}` : `${minYear} – ${maxYear}`; - const latest = [...entries].sort((a, b) => b.date.localeCompare(a.date))[0]; - - return { totalDays, countries, cities, topCountry, soloCount, soloPct, yearRange, latest, tripCount: entries.length }; + return { totalDays, countries, cities, yearRange, tripCount: entries.length }; }); {#if stats} -
+
+ -

My Journey

-

{stats.yearRange}

+
+ +
+
+ + + + + + + + TRAVEL JOURNAL +
+
+

PASSPORT

+

{stats.yearRange}

+
+
-
+
- -
-
- {stats.countries.length} - Countries -
-
- {stats.cities.length} - Cities -
-
- {stats.totalDays} - Days abroad -
-
- {stats.tripCount} - Trips + +
+
+ TRIPS + {stats.tripCount} +
+
+ COUNTRIES + {stats.countries.length} +
+
+ DAYS + {stats.totalDays} +
-
- - -
- Most visited - {stats.topCountry[0]} - {stats.topCountry[1]} {stats.topCountry[1] === 1 ? 'trip' : 'trips'} + +
+ P<JNL{String(stats.tripCount).padStart(2,'0')}<<<<<<<<<<<<<<<<<<<<<<<<<<< + {stats.yearRange.replace(' – ','').replace(/\s/g,'')}{'<'.repeat(12)}{String(stats.totalDays).padStart(4,'0')}
- - -
- 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 index ad4f8da..b649034 100644 --- a/src/lib/timeline/TimelineCard.svelte +++ b/src/lib/timeline/TimelineCard.svelte @@ -126,7 +126,7 @@ display: flex; gap: 14px; align-items: flex-start; - padding-bottom: 28px; + padding-bottom: 48px; position: relative; } .v-item:last-child { padding-bottom: 0; } diff --git a/src/lib/timeline/TimelineToolbar.svelte b/src/lib/timeline/TimelineToolbar.svelte index 507b12f..f930098 100644 --- a/src/lib/timeline/TimelineToolbar.svelte +++ b/src/lib/timeline/TimelineToolbar.svelte @@ -9,34 +9,12 @@ let { sortKey, onSort } = $props(); -
-

My Journey

-
- -
-
+
From cdf36436220d065ba3031838288143089c51ed64 Mon Sep 17 00:00:00 2001 From: Haeri Kim Date: Sun, 14 Jun 2026 10:47:21 +0900 Subject: [PATCH 2/4] added edit page and revised edit form --- src/lib/shared/SearchInput.svelte | 127 ++++++++++++ src/lib/shared/countries.js | 25 +++ src/lib/stores/journalStore.js | 5 + src/lib/timeline/DeleteConfirm.svelte | 88 +++++++++ src/lib/timeline/EditForm.svelte | 268 ++++++++++++++++++++++++++ src/lib/timeline/JournalDetail.svelte | 33 ++-- src/lib/timeline/PhotoEditor.svelte | 189 ++++++++++++++++++ src/lib/timeline/TimelineCard.svelte | 23 +-- src/lib/timeline/TimelineView.svelte | 20 +- 9 files changed, 736 insertions(+), 42 deletions(-) create mode 100644 src/lib/shared/SearchInput.svelte create mode 100644 src/lib/shared/countries.js create mode 100644 src/lib/timeline/DeleteConfirm.svelte create mode 100644 src/lib/timeline/EditForm.svelte create mode 100644 src/lib/timeline/PhotoEditor.svelte diff --git a/src/lib/shared/SearchInput.svelte b/src/lib/shared/SearchInput.svelte new file mode 100644 index 0000000..bb8e0c7 --- /dev/null +++ b/src/lib/shared/SearchInput.svelte @@ -0,0 +1,127 @@ + + +
+ open = true} + onblur={onBlur} + /> + {#if open && filtered.length > 0} + + {/if} +
+ + diff --git a/src/lib/shared/countries.js b/src/lib/shared/countries.js new file mode 100644 index 0000000..a6401c4 --- /dev/null +++ b/src/lib/shared/countries.js @@ -0,0 +1,25 @@ +export const countryCodeMap = { + 'Argentina': 'AR', 'Australia': 'AU', 'Austria': 'AT', + 'Belgium': 'BE', 'Brazil': 'BR', + 'Canada': 'CA', 'Chile': 'CL', 'China': 'CN', 'Croatia': 'HR', + 'Czech Republic': 'CZ', 'Denmark': 'DK', 'Egypt': 'EG', + 'Finland': 'FI', 'France': 'FR', 'Germany': 'DE', 'Greece': 'GR', + 'Hungary': 'HU', 'India': 'IN', 'Indonesia': 'ID', 'Italy': 'IT', + 'Japan': 'JP', 'Kenya': 'KE', + 'Malaysia': 'MY', 'Mexico': 'MX', 'Morocco': 'MA', + 'Netherlands': 'NL', 'New Zealand': 'NZ', 'Norway': 'NO', + 'Peru': 'PE', 'Poland': 'PL', 'Portugal': 'PT', + 'Singapore': 'SG', 'South Africa': 'ZA', 'South Korea': 'KR', + 'Spain': 'ES', 'Sweden': 'SE', 'Switzerland': 'CH', + 'Taiwan': 'TW', 'Thailand': 'TH', 'Turkey': 'TR', + 'UK': 'GB', 'USA': 'US', 'Vietnam': 'VN', +}; + +export const countryNames = Object.keys(countryCodeMap).sort(); + +/** @param {string} country */ +export function flagEmoji(country) { + const code = countryCodeMap[country]; + if (!code) return ''; + return [...code].map(c => String.fromCodePoint(0x1F1E6 - 65 + c.charCodeAt(0))).join(''); +} diff --git a/src/lib/stores/journalStore.js b/src/lib/stores/journalStore.js index 1ef37c9..fe2c673 100644 --- a/src/lib/stores/journalStore.js +++ b/src/lib/stores/journalStore.js @@ -116,3 +116,8 @@ export function addJournal(entry) { export function removeJournal(id) { journals.update((entries) => entries.filter((e) => e.id !== id)); } + +/** @param {JournalEntry} updated */ +export function updateJournal(updated) { + journals.update((entries) => entries.map((e) => e.id === updated.id ? updated : e)); +} diff --git a/src/lib/timeline/DeleteConfirm.svelte b/src/lib/timeline/DeleteConfirm.svelte new file mode 100644 index 0000000..b602869 --- /dev/null +++ b/src/lib/timeline/DeleteConfirm.svelte @@ -0,0 +1,88 @@ + + + + + diff --git a/src/lib/timeline/EditForm.svelte b/src/lib/timeline/EditForm.svelte new file mode 100644 index 0000000..cd1f52f --- /dev/null +++ b/src/lib/timeline/EditForm.svelte @@ -0,0 +1,268 @@ + + +
+ +
+
+ +
+ Edit +
+ +
+
+ +
+
{ e.preventDefault(); save(); }}> + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ +
+ + +
+
+ + (photos = p)} /> + +
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + diff --git a/src/lib/timeline/JournalDetail.svelte b/src/lib/timeline/JournalDetail.svelte index cf28b8e..6508741 100644 --- a/src/lib/timeline/JournalDetail.svelte +++ b/src/lib/timeline/JournalDetail.svelte @@ -1,19 +1,16 @@ +{#if showDeleteConfirm} + showDeleteConfirm = false} /> +{/if} + {#if lightboxSrc}
- - + +
+ + {#if photos.length === 0} + + {:else} +
+ {#each photos as src, i (src + i)} +
+ photo {i + 1} + +
+ {/each} + + +
+ {/if} +
+ + diff --git a/src/lib/timeline/TimelineCard.svelte b/src/lib/timeline/TimelineCard.svelte index b649034..166e5fc 100644 --- a/src/lib/timeline/TimelineCard.svelte +++ b/src/lib/timeline/TimelineCard.svelte @@ -1,28 +1,9 @@ (screen = s)} hideTopBar={inDetail}> {#if screen === 'worldmap'}
- +
{:else} - (inDetail = v)} /> + (inDetail = v)} + {pendingCountry} + onNewEntryClear={() => (pendingCountry = '')} + /> {/if}
diff --git a/src/lib/layout/TopBar.svelte b/src/lib/layout/TopBar.svelte index fd0124b..f2fbbd5 100644 --- a/src/lib/layout/TopBar.svelte +++ b/src/lib/layout/TopBar.svelte @@ -3,7 +3,9 @@
@@ -340,34 +323,6 @@ line-height: 1.75; } - .song-answer { - display: flex; - align-items: center; - gap: 10px; - } - .song-icon { - width: 36px; - height: 36px; - border-radius: 50%; - background: var(--accent-bg); - color: var(--accent); - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - } - .song-title { - font-size: 14px; - font-weight: 400; - color: var(--text-h); - } - .song-artist { - font-size: 12px; - font-weight: 300; - color: var(--text-sub); - margin-top: 2px; - } - /* ── Lightbox ── */ .lightbox { position: fixed; diff --git a/src/lib/timeline/ShareCard.svelte b/src/lib/timeline/ShareCard.svelte new file mode 100644 index 0000000..1fc1b16 --- /dev/null +++ b/src/lib/timeline/ShareCard.svelte @@ -0,0 +1,483 @@ + + + + + + diff --git a/src/lib/timeline/TimelineCard.svelte b/src/lib/timeline/TimelineCard.svelte index 166e5fc..7e8b113 100644 --- a/src/lib/timeline/TimelineCard.svelte +++ b/src/lib/timeline/TimelineCard.svelte @@ -13,6 +13,16 @@ let mainPhoto = $derived(entry.photos[0] ?? null); let thumbPhotos = $derived(entry.photos.slice(1, 4)); let extraCount = $derived(entry.photos.length > 4 ? entry.photos.length - 4 : 0); + + const transportIcons = { + flight: ``, + train: ``, + bus: ``, + car: ``, + ship: ``, + walk: ``, + }; + let transportLabel = $derived({ flight: 'Flight', train: 'Train', bus: 'Bus', car: 'Car', ship: 'Ship', walk: 'Walk' }[entry.transport] ?? '');
  • @@ -91,6 +101,13 @@
    {entry.location.city}
    + {#if entry.transport} + + {@html transportIcons[entry.transport] ?? ''} + {transportLabel} + + · + {/if} {formatDate(entry.date)} · {entry.days} {entry.days === 1 ? 'day' : 'days'} @@ -298,4 +315,23 @@ flex-shrink: 0; } .dot-sep { color: var(--border-bright); } + + .transport-chip { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 11px; + font-weight: 400; + padding: 2px 7px; + border-radius: 20px; + border: 1px solid var(--border); + background: var(--bg-subtle); + color: var(--text-sub); + } + .transport-chip--flight { color: #7c3aed; background: rgba(124,58,237,0.07); border-color: rgba(124,58,237,0.2); } + .transport-chip--train { color: #0369a1; background: rgba(3,105,161,0.07); border-color: rgba(3,105,161,0.2); } + .transport-chip--bus { color: #15803d; background: rgba(21,128,61,0.07); border-color: rgba(21,128,61,0.2); } + .transport-chip--car { color: #b45309; background: rgba(180,83,9,0.07); border-color: rgba(180,83,9,0.2); } + .transport-chip--ship { color: #0e7490; background: rgba(14,116,144,0.07); border-color: rgba(14,116,144,0.2); } + .transport-chip--walk { color: #65a30d; background: rgba(101,163,13,0.07); border-color: rgba(101,163,13,0.2); } diff --git a/src/lib/timeline/TimelineView.svelte b/src/lib/timeline/TimelineView.svelte index 5e79106..a6c8d7f 100644 --- a/src/lib/timeline/TimelineView.svelte +++ b/src/lib/timeline/TimelineView.svelte @@ -4,16 +4,19 @@ import TimelineToolbar from './TimelineToolbar.svelte'; import TimelineCard from './TimelineCard.svelte'; import JournalDetail from './JournalDetail.svelte'; - import JournalSummary from './JournalSummary.svelte'; import EditForm from './EditForm.svelte'; + import ShareCard from './ShareCard.svelte'; let { onDetailChange = () => {}, pendingCountry = '', onNewEntryClear = () => {} } = $props(); let selectedId = $state(/** @type {string|null} */(null)); let view = $state(/** @type {'list'|'detail'|'edit'|'new'} */('list')); + let showShare = $state(false); + let newEntryCountry = $state(''); - // When App passes a country from the map, open new-entry form automatically + // When App passes a country from the map, capture it locally before clearing $effect(() => { if (pendingCountry) { + newEntryCountry = pendingCountry; selectedId = null; view = 'new'; onNewEntryClear(); @@ -50,7 +53,7 @@ {#if view === 'new'}
    - { view = 'list'; onDetailChange(false); }} /> + { view = 'list'; newEntryCountry = ''; onDetailChange(false); }} />
    {:else if view === 'edit' && selected}
    @@ -77,10 +80,23 @@
    - - (sortKey = k)} /> + {#if sortedEntries.length > 0} + + {/if} + {#if sortedEntries.length === 0}

    No journal entries yet.

    {:else} @@ -114,6 +130,10 @@
    +{#if showShare} + (showShare = false)} /> +{/if} +