@@ -28,7 +46,7 @@
Back
- {#if entry.photos.length > 0}
+ {#if entry.photos?.length > 0}
- 📍 {entry.location.city}, {entry.location.country}
-
- {entry.tripType === 'solo' ? '🧍 Solo' : '👥 With Friends'}
+ 📍 {entry.city}, {entry.countryName}
+
+ {tripType(entry.companions) === 'solo' ? '🧍 Solo' : '👥 ' + companionText(entry.companions)}
+ {#if entry.transportation}
+ {TRANSPORT_LABELS[entry.transportation] || entry.transportation}
+ {/if}
{entry.title}
@@ -93,8 +114,8 @@
Soundtrack
- {entry.song.title}
- {entry.song.artist}
+ {entry.song?.title || ''}
+ {entry.song?.artist || ''}
@@ -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;
diff --git a/src/lib/stores/entriesStore.svelte.js b/src/lib/stores/entriesStore.svelte.js
new file mode 100644
index 0000000..9337777
--- /dev/null
+++ b/src/lib/stores/entriesStore.svelte.js
@@ -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));
+}
diff --git a/src/lib/world-map/WorldMap.svelte b/src/lib/world-map/WorldMap.svelte
index cca1ed5..ee705f8 100644
--- a/src/lib/world-map/WorldMap.svelte
+++ b/src/lib/world-map/WorldMap.svelte
@@ -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();