add saving countries
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
<script>
|
<script>
|
||||||
/** @type {{ entry: import('./stores/journalStore.js').JournalEntry, onBack: () => void }} */
|
|
||||||
let { entry, onBack } = $props();
|
let { entry, onBack } = $props();
|
||||||
|
|
||||||
let photoIdx = $state(0);
|
let photoIdx = $state(0);
|
||||||
@@ -16,6 +15,25 @@
|
|||||||
function next() {
|
function next() {
|
||||||
photoIdx = (photoIdx + 1) % entry.photos.length;
|
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>
|
</script>
|
||||||
|
|
||||||
<article class="detail-page">
|
<article class="detail-page">
|
||||||
@@ -28,7 +46,7 @@
|
|||||||
Back
|
Back
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if entry.photos.length > 0}
|
{#if entry.photos?.length > 0}
|
||||||
<div class="hero-gallery">
|
<div class="hero-gallery">
|
||||||
<img
|
<img
|
||||||
class="hero-img"
|
class="hero-img"
|
||||||
@@ -58,10 +76,13 @@
|
|||||||
|
|
||||||
<div class="detail-content">
|
<div class="detail-content">
|
||||||
<div class="meta-row">
|
<div class="meta-row">
|
||||||
<span class="badge loc-badge">📍 {entry.location.city}, {entry.location.country}</span>
|
<span class="badge loc-badge">📍 {entry.city}, {entry.countryName}</span>
|
||||||
<span class="badge trip-badge trip-badge--{entry.tripType}">
|
<span class="badge" class:trip-badge--solo={tripType(entry.companions) === 'solo'} class:trip-badge--friends={tripType(entry.companions) === 'friends'}>
|
||||||
{entry.tripType === 'solo' ? '🧍 Solo' : '👥 With Friends'}
|
{tripType(entry.companions) === 'solo' ? '🧍 Solo' : '👥 ' + companionText(entry.companions)}
|
||||||
</span>
|
</span>
|
||||||
|
{#if entry.transportation}
|
||||||
|
<span class="badge transport-badge">{TRANSPORT_LABELS[entry.transportation] || entry.transportation}</span>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 class="detail-title">{entry.title}</h1>
|
<h1 class="detail-title">{entry.title}</h1>
|
||||||
@@ -93,8 +114,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="song-text">
|
<div class="song-text">
|
||||||
<span class="song-label">Soundtrack</span>
|
<span class="song-label">Soundtrack</span>
|
||||||
<span class="song-name">{entry.song.title}</span>
|
<span class="song-name">{entry.song?.title || ''}</span>
|
||||||
<span class="song-artist">{entry.song.artist}</span>
|
<span class="song-artist">{entry.song?.artist || ''}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -223,6 +244,11 @@
|
|||||||
.trip-badge--solo { background: rgba(245,158,11,0.12); color: #b45309; }
|
.trip-badge--solo { background: rgba(245,158,11,0.12); color: #b45309; }
|
||||||
.trip-badge--friends { background: rgba(59,130,246,0.12); color: #1d4ed8; }
|
.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 {
|
.detail-title {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: 700;
|
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 frameEl;
|
||||||
|
let _paths = null;
|
||||||
|
let _g = null;
|
||||||
|
|
||||||
function fitProjection(proj, w, h) {
|
function fitProjection(proj, w, h) {
|
||||||
proj.fitSize([w, h], { type: 'Sphere' });
|
proj.fitSize([w, h], { type: 'Sphere' });
|
||||||
@@ -57,6 +59,15 @@
|
|||||||
proj.scale(s).translate([w / 2, h * 0.70]);
|
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(() => {
|
onMount(() => {
|
||||||
const width = frameEl.clientWidth;
|
const width = frameEl.clientWidth;
|
||||||
const height = frameEl.clientHeight;
|
const height = frameEl.clientHeight;
|
||||||
@@ -81,7 +92,7 @@
|
|||||||
.attr('width', width)
|
.attr('width', width)
|
||||||
.attr('height', height);
|
.attr('height', height);
|
||||||
|
|
||||||
const g = svg.append('g');
|
_g = svg.append('g');
|
||||||
|
|
||||||
const tooltip = d3.select(frameEl)
|
const tooltip = d3.select(frameEl)
|
||||||
.append('div')
|
.append('div')
|
||||||
@@ -90,7 +101,7 @@
|
|||||||
|
|
||||||
function updateFill(sel) {
|
function updateFill(sel) {
|
||||||
sel.attr('fill', d => getSelected().has(effId(d)) ? '#22c55e' : '#ffffff');
|
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) {
|
function attachEvents(sel) {
|
||||||
@@ -113,24 +124,24 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const paths = g.selectAll('path')
|
_paths = _g.selectAll('path')
|
||||||
.data(countries)
|
.data(countries)
|
||||||
.join('path')
|
.join('path')
|
||||||
.attr('d', path)
|
.attr('d', path)
|
||||||
.attr('fill', '#ffffff')
|
.attr('fill', '#ffffff')
|
||||||
.attr('stroke', '#d4d4d4')
|
.attr('stroke', '#d4d4d4')
|
||||||
.attr('stroke-width', 0.5);
|
.attr('stroke-width', 0.5);
|
||||||
attachEvents(paths);
|
attachEvents(_paths);
|
||||||
|
|
||||||
function renderMicrostates() {
|
function renderMicrostates() {
|
||||||
g.selectAll('.micro-state').remove();
|
_g.selectAll('.micro-state').remove();
|
||||||
const threshold = Math.max(4, 16 / d3.zoomTransform(svg.node()).k);
|
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;
|
if (effId(d) !== d.id) return;
|
||||||
const { width, height } = this.getBBox();
|
const { width, height } = this.getBBox();
|
||||||
if (width < threshold && height < threshold) {
|
if (width < threshold && height < threshold) {
|
||||||
const [cx, cy] = path.centroid(d);
|
const [cx, cy] = path.centroid(d);
|
||||||
const c = g.append('circle')
|
const c = _g.append('circle')
|
||||||
.attr('class', 'micro-state')
|
.attr('class', 'micro-state')
|
||||||
.datum(d)
|
.datum(d)
|
||||||
.attr('cx', cx)
|
.attr('cx', cx)
|
||||||
@@ -149,7 +160,7 @@
|
|||||||
const zoom = d3.zoom()
|
const zoom = d3.zoom()
|
||||||
.scaleExtent([1, 32])
|
.scaleExtent([1, 32])
|
||||||
.on('zoom', (event) => {
|
.on('zoom', (event) => {
|
||||||
g.attr('transform', event.transform);
|
_g.attr('transform', event.transform);
|
||||||
renderMicrostates();
|
renderMicrostates();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -166,7 +177,7 @@
|
|||||||
const { width, height } = entry.contentRect;
|
const { width, height } = entry.contentRect;
|
||||||
svg.attr('width', width).attr('height', height);
|
svg.attr('width', width).attr('height', height);
|
||||||
fitProjection(projection, width, height);
|
fitProjection(projection, width, height);
|
||||||
const countryPaths = g.selectAll('path');
|
const countryPaths = _g.selectAll('path');
|
||||||
countryPaths.attr('d', path);
|
countryPaths.attr('d', path);
|
||||||
updateFill(countryPaths);
|
updateFill(countryPaths);
|
||||||
renderMicrostates();
|
renderMicrostates();
|
||||||
|
|||||||
Reference in New Issue
Block a user