From 6cee6095ed2ac9341b9289fc5aae41c8360c5141 Mon Sep 17 00:00:00 2001 From: haerikimmm Date: Mon, 15 Jun 2026 10:20:55 +0900 Subject: [PATCH] added sharable stats --- package-lock.json | 7 + package.json | 1 + src/lib/stores/journalStore.js | 14 +- src/lib/timeline/EditForm.svelte | 66 +++- src/lib/timeline/JournalDetail.svelte | 45 --- src/lib/timeline/ShareCard.svelte | 483 ++++++++++++++++++++++++++ src/lib/timeline/TimelineCard.svelte | 36 ++ src/lib/timeline/TimelineView.svelte | 65 +++- 8 files changed, 646 insertions(+), 71 deletions(-) create mode 100644 src/lib/timeline/ShareCard.svelte diff --git a/package-lock.json b/package-lock.json index 4c7fce0..f5205b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "d3": "^7.9.0", "flag-icons": "^7.5.0", + "html-to-image": "^1.11.13", "topojson-client": "^3.1.0", "world-atlas": "^2.0.2" }, @@ -1004,6 +1005,12 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/html-to-image": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz", + "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==", + "license": "MIT" + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", diff --git a/package.json b/package.json index c363719..9e07327 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "dependencies": { "d3": "^7.9.0", "flag-icons": "^7.5.0", + "html-to-image": "^1.11.13", "topojson-client": "^3.1.0", "world-atlas": "^2.0.2" } diff --git a/src/lib/stores/journalStore.js b/src/lib/stores/journalStore.js index fe2c673..57b29e6 100644 --- a/src/lib/stores/journalStore.js +++ b/src/lib/stores/journalStore.js @@ -7,7 +7,7 @@ import { writable } from 'svelte/store'; * date: string, * location: { country: string, city: string }, * photos: string[], - * song: { title: string, artist: string }, + * transport: 'flight' | 'train' | 'bus' | 'car' | 'ship' | 'walk', * tripType: 'solo' | 'friends', * days: number, * memo: string @@ -26,7 +26,7 @@ const mockEntries = [ 'https://images.unsplash.com/photo-1513407030348-c983a97b98d8?w=600&q=80', 'https://images.unsplash.com/photo-1490806843957-31f4c9a91c65?w=600&q=80', ], - song: { title: 'Tokyo', artist: 'Imagine Dragons' }, + transport: 'flight', tripType: 'solo', days: 5, memo: 'Got completely lost in Shinjuku — stumbled into a tiny ramen shop with no English menu. The chashu just melted. Worth every wrong turn.', @@ -40,7 +40,7 @@ const mockEntries = [ 'https://images.unsplash.com/photo-1528360983277-13d401cdc186?w=600&q=80', 'https://images.unsplash.com/photo-1545569341-9eb8b30979d9?w=600&q=80', ], - song: { title: 'Spirited Away Suite', artist: 'Joe Hisaishi' }, + transport: 'train', tripType: 'friends', days: 3, memo: 'Arrived at 6am before the crowds. Just me and the wind moving through the bamboo. One of those moments you keep coming back to.', @@ -55,7 +55,7 @@ const mockEntries = [ 'https://images.unsplash.com/photo-1499856871958-5b9627545d1a?w=600&q=80', 'https://images.unsplash.com/photo-1511739001486-6bfe10ce785f?w=600&q=80', ], - song: { title: 'La Vie en Rose', artist: 'Édith Piaf' }, + transport: 'flight', tripType: 'solo', days: 7, memo: 'Watched the whole city turn orange from the steps of Sacré-Cœur. A street musician was playing La Vie en Rose. Cliché, perfect.', @@ -69,7 +69,7 @@ const mockEntries = [ 'https://images.unsplash.com/photo-1523531294919-4bcd7c65e216?w=600&q=80', 'https://images.unsplash.com/photo-1583422409516-2895a77efded?w=600&q=80', ], - song: { title: 'Spain', artist: 'Chick Corea' }, + transport: 'flight', tripType: 'friends', days: 4, memo: 'Nothing prepares you for the light inside. The stained glass turns the whole nave into a kaleidoscope. Gaudí was building a forest.', @@ -84,7 +84,7 @@ const mockEntries = [ 'https://images.unsplash.com/photo-1485871981521-5b1fd3805345?w=600&q=80', 'https://images.unsplash.com/photo-1522083165195-3424ed129620?w=600&q=80', ], - song: { title: 'New York, New York', artist: 'Frank Sinatra' }, + transport: 'car', tripType: 'friends', days: 6, memo: 'Peak foliage. Joggers, picnics, a guy playing saxophone near Bethesda Fountain. Hard to believe a city this big wraps around this much quiet.', @@ -98,7 +98,7 @@ const mockEntries = [ 'https://images.unsplash.com/photo-1563492065599-3520f775eeed?w=600&q=80', 'https://images.unsplash.com/photo-1552465011-b4e21bf6e79a?w=600&q=80', ], - song: { title: 'Elephant', artist: 'Tame Impala' }, + transport: 'ship', tripType: 'solo', days: 2, memo: 'Stood in front of the 45m golden Buddha for a long time. The mother-of-pearl inlay on the soles of the feet is impossibly detailed.', diff --git a/src/lib/timeline/EditForm.svelte b/src/lib/timeline/EditForm.svelte index 76e11ea..c4d942a 100644 --- a/src/lib/timeline/EditForm.svelte +++ b/src/lib/timeline/EditForm.svelte @@ -21,8 +21,16 @@ let tripType = $state(entry?.tripType ?? 'solo'); let photos = $state([...(entry?.photos ?? [])]); let memo = $state(entry?.memo ?? ''); - let songTitle = $state(entry?.song.title ?? ''); - let songArtist = $state(entry?.song.artist ?? ''); + let transport = $state(entry?.transport ?? 'flight'); + + const transportOptions = [ + { value: 'flight', label: '✈ Flight' }, + { value: 'train', label: '🚂 Train' }, + { value: 'bus', label: '🚌 Bus' }, + { value: 'car', label: '🚗 Car' }, + { value: 'ship', label: '🚢 Ship' }, + { value: 'walk', label: '🚶 Walk' }, + ]; const MEMO_MAX = 100; let wordCount = $derived(memo.trim() === '' ? 0 : memo.trim().split(/\s+/).length); @@ -53,8 +61,8 @@ tripType, memo, photos, + transport, location: { city, country }, - song: { title: songTitle, artist: songArtist }, }); } else { updateJournal({ @@ -62,10 +70,10 @@ date, days: Number(days), tripType, + transport, memo, photos, location: { city, country }, - song: { title: songTitle, artist: songArtist }, }); } onBack(); @@ -126,6 +134,18 @@ +
+ +
+ {#each transportOptions as opt} + + {/each} +
+
+ (photos = p)} />
@@ -136,16 +156,6 @@
-
-
- - -
-
- - -
-
@@ -317,4 +327,32 @@ color: var(--accent); } + .transport-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px; + } + .transport-opt { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + font-size: 13px; + font-weight: 300; + color: var(--text); + padding: 8px 10px; + border-radius: 8px; + border: 1px solid var(--border); + cursor: pointer; + transition: border-color 0.15s, background 0.15s, color 0.15s; + background: var(--bg-subtle); + white-space: nowrap; + } + .transport-opt input { display: none; } + .transport-opt.active { + border-color: var(--accent-border); + background: var(--accent-bg); + color: var(--accent); + } + diff --git a/src/lib/timeline/JournalDetail.svelte b/src/lib/timeline/JournalDetail.svelte index 6508741..e8dcc58 100644 --- a/src/lib/timeline/JournalDetail.svelte +++ b/src/lib/timeline/JournalDetail.svelte @@ -125,23 +125,6 @@

{entry.memo}

-
-

Trip soundtrack

-
-
- - - - - -
-
-

{entry.song.title}

-

{entry.song.artist}

-
-
-
- @@ -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} +