add saving countries

This commit is contained in:
2026-06-12 18:20:00 +09:00
parent 08d3e3ae56
commit 6701398da7
3 changed files with 98 additions and 16 deletions

View File

@@ -1,5 +1,4 @@
<script>
/** @type {{ entry: import('./stores/journalStore.js').JournalEntry, onBack: () => void }} */
let { entry, onBack } = $props();
let photoIdx = $state(0);
@@ -16,6 +15,25 @@
function next() {
photoIdx = (photoIdx + 1) % entry.photos.length;
}
function tripType(/** @type {string[] | undefined} */ companions) {
if (!companions || companions.length === 0 || (companions.length === 1 && companions[0] === 'solo')) return 'solo';
return 'friends';
}
function companionText(/** @type {string[] | undefined} */ companions) {
if (!companions || companions.length === 0 || (companions.length === 1 && companions[0] === 'solo')) return 'Solo';
return companions.join(', ');
}
const TRANSPORT_LABELS = {
plane: '✈️ Plane',
car: '🚗 Car',
train: '🚆 Train',
boat: '⛵ Boat',
bus: '🚌 Bus',
other: 'Other',
};
</script>
<article class="detail-page">
@@ -28,7 +46,7 @@
Back
</button>
{#if entry.photos.length > 0}
{#if entry.photos?.length > 0}
<div class="hero-gallery">
<img
class="hero-img"
@@ -58,10 +76,13 @@
<div class="detail-content">
<div class="meta-row">
<span class="badge loc-badge">📍 {entry.location.city}, {entry.location.country}</span>
<span class="badge trip-badge trip-badge--{entry.tripType}">
{entry.tripType === 'solo' ? '🧍 Solo' : '👥 With Friends'}
<span class="badge loc-badge">📍 {entry.city}, {entry.countryName}</span>
<span class="badge" class:trip-badge--solo={tripType(entry.companions) === 'solo'} class:trip-badge--friends={tripType(entry.companions) === 'friends'}>
{tripType(entry.companions) === 'solo' ? '🧍 Solo' : '👥 ' + companionText(entry.companions)}
</span>
{#if entry.transportation}
<span class="badge transport-badge">{TRANSPORT_LABELS[entry.transportation] || entry.transportation}</span>
{/if}
</div>
<h1 class="detail-title">{entry.title}</h1>
@@ -93,8 +114,8 @@
</div>
<div class="song-text">
<span class="song-label">Soundtrack</span>
<span class="song-name">{entry.song.title}</span>
<span class="song-artist">{entry.song.artist}</span>
<span class="song-name">{entry.song?.title || ''}</span>
<span class="song-artist">{entry.song?.artist || ''}</span>
</div>
</div>
</div>
@@ -223,6 +244,11 @@
.trip-badge--solo { background: rgba(245,158,11,0.12); color: #b45309; }
.trip-badge--friends { background: rgba(59,130,246,0.12); color: #1d4ed8; }
.transport-badge {
background: rgba(16,185,129,0.12);
color: #059669;
}
.detail-title {
font-size: 28px;
font-weight: 700;

View File

@@ -0,0 +1,45 @@
import { db } from '../firebase.js';
import { collection, doc, onSnapshot, query, orderBy, addDoc, updateDoc, deleteDoc, serverTimestamp } from 'firebase/firestore';
let entries = $state([]);
let _uid = null;
let _unsubscribe = null;
export function getEntries() {
return entries;
}
export function initEntriesListener(uid) {
if (_unsubscribe) _unsubscribe();
_uid = uid;
const q = query(
collection(db, 'users', uid, 'entries'),
orderBy('createdAt', 'desc')
);
_unsubscribe = onSnapshot(q, (snap) => {
entries = snap.docs.map((d) => ({ id: d.id, ...d.data() }));
});
}
export async function addEntry(data) {
if (!_uid) return null;
const ref = await addDoc(collection(db, 'users', _uid, 'entries'), {
...data,
createdAt: serverTimestamp(),
updatedAt: serverTimestamp(),
});
return ref.id;
}
export async function updateEntry(id, data) {
if (!_uid) return;
await updateDoc(doc(db, 'users', _uid, 'entries', id), {
...data,
updatedAt: serverTimestamp(),
});
}
export async function removeEntry(id) {
if (!_uid) return;
await deleteDoc(doc(db, 'users', _uid, 'entries', id));
}

View File

@@ -50,6 +50,8 @@
}
let frameEl;
let _paths = null;
let _g = null;
function fitProjection(proj, w, h) {
proj.fitSize([w, h], { type: 'Sphere' });
@@ -57,6 +59,15 @@
proj.scale(s).translate([w / 2, h * 0.70]);
}
function updateAllFills() {
if (!_paths || !_g) return;
const sel = getSelected();
_paths.attr('fill', d => sel.has(effId(d)) ? '#22c55e' : '#ffffff');
_g.selectAll('.micro-state').attr('fill', d => sel.has(effId(d)) ? '#22c55e' : '#ffffff');
}
$effect(updateAllFills);
onMount(() => {
const width = frameEl.clientWidth;
const height = frameEl.clientHeight;
@@ -81,7 +92,7 @@
.attr('width', width)
.attr('height', height);
const g = svg.append('g');
_g = svg.append('g');
const tooltip = d3.select(frameEl)
.append('div')
@@ -90,7 +101,7 @@
function updateFill(sel) {
sel.attr('fill', d => getSelected().has(effId(d)) ? '#22c55e' : '#ffffff');
g.selectAll('.micro-state').attr('fill', d => getSelected().has(effId(d)) ? '#22c55e' : '#ffffff');
_g.selectAll('.micro-state').attr('fill', d => getSelected().has(effId(d)) ? '#22c55e' : '#ffffff');
}
function attachEvents(sel) {
@@ -113,24 +124,24 @@
});
}
const paths = g.selectAll('path')
_paths = _g.selectAll('path')
.data(countries)
.join('path')
.attr('d', path)
.attr('fill', '#ffffff')
.attr('stroke', '#d4d4d4')
.attr('stroke-width', 0.5);
attachEvents(paths);
attachEvents(_paths);
function renderMicrostates() {
g.selectAll('.micro-state').remove();
_g.selectAll('.micro-state').remove();
const threshold = Math.max(4, 16 / d3.zoomTransform(svg.node()).k);
paths.each(function (d) {
_paths.each(function (d) {
if (effId(d) !== d.id) return;
const { width, height } = this.getBBox();
if (width < threshold && height < threshold) {
const [cx, cy] = path.centroid(d);
const c = g.append('circle')
const c = _g.append('circle')
.attr('class', 'micro-state')
.datum(d)
.attr('cx', cx)
@@ -149,7 +160,7 @@
const zoom = d3.zoom()
.scaleExtent([1, 32])
.on('zoom', (event) => {
g.attr('transform', event.transform);
_g.attr('transform', event.transform);
renderMicrostates();
});
@@ -166,7 +177,7 @@
const { width, height } = entry.contentRect;
svg.attr('width', width).attr('height', height);
fitProjection(projection, width, height);
const countryPaths = g.selectAll('path');
const countryPaths = _g.selectAll('path');
countryPaths.attr('d', path);
updateFill(countryPaths);
renderMicrostates();