add saving countries
This commit is contained in:
@@ -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;
|
||||
|
||||
45
src/lib/stores/entriesStore.svelte.js
Normal file
45
src/lib/stores/entriesStore.svelte.js
Normal 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));
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user