diff --git a/modules/rcal/components/folk-calendar-view.ts b/modules/rcal/components/folk-calendar-view.ts index 3a2725d..eb55e52 100644 --- a/modules/rcal/components/folk-calendar-view.ts +++ b/modules/rcal/components/folk-calendar-view.ts @@ -106,6 +106,7 @@ import { calendarSchema, calendarDocId, type CalendarDoc } from "../schemas"; import type { DocumentId } from "../../../shared/local-first/document"; import { TourEngine } from "../../../shared/tour-engine"; import { startPresenceHeartbeat } from '../../../shared/collab-presence'; +import { generateBreadcrumb, CONTINENT_CENTROIDS } from "../geo-hierarchy"; // ── Component ── @@ -260,6 +261,7 @@ class FolkCalendarView extends HTMLElement { all_day: e.allDay, source_id: e.sourceId, source_name: e.sourceName, source_color: e.sourceColor, location_name: e.locationName, + location_breadcrumb: e.locationBreadcrumb ?? null, location_lat: e.locationLat, location_lng: e.locationLng, latitude: e.locationLat ?? null, longitude: e.locationLng ?? null, @@ -270,6 +272,7 @@ class FolkCalendarView extends HTMLElement { } } + this.fillMissingBreadcrumbs(); this.render(); } @@ -435,6 +438,25 @@ class FolkCalendarView extends HTMLElement { return [sumLat / located.length, sumLng / located.length]; } + /** Fill missing location_breadcrumb fields from lat/lng using static geo lookup. */ + private fillMissingBreadcrumbs() { + for (const ev of this.events) { + if (!ev.location_breadcrumb && ev.latitude != null && ev.longitude != null) { + ev.location_breadcrumb = generateBreadcrumb(ev.latitude, ev.longitude, ev.location_name); + } + } + } + + /** Get events for a specific month (used by year/season/multi-year spatial labels). */ + private getMonthEvents(year: number, month: number): any[] { + const start = new Date(year, month, 1).getTime(); + const end = new Date(year, month + 1, 1).getTime(); + return this.events.filter(e => { + const t = new Date(e.start_time).getTime(); + return t >= start && t < end && !this.filteredSources.has(e.source_name); + }); + } + // ── Demo Data ── private loadDemoData() { @@ -650,6 +672,7 @@ class FolkCalendarView extends HTMLElement { }); this.sources = sources; + this.fillMissingBreadcrumbs(); // Compute lunar phases for visible range (month-6 through month+9) const lunar: Record = {}; @@ -706,6 +729,7 @@ class FolkCalendarView extends HTMLElement { if (lunarRes.ok) { this.lunarData = await lunarRes.json(); } if (remindersRes?.ok) { const data = await remindersRes.json(); this.reminders = data.reminders || []; } } catch { /* offline fallback */ } + this.fillMissingBreadcrumbs(); this.render(); } @@ -1133,7 +1157,20 @@ class FolkCalendarView extends HTMLElement { private renderMonth(): string { const year = this.currentDate.getFullYear(); const month = this.currentDate.getMonth(); + const spatialLevel = this.getEffectiveSpatialIndex(); + + // Spatial summary bar for zoomed-out views (country level or broader) + let spatialSummary = ""; + if (spatialLevel <= 3) { + const monthEvents = this.getMonthEvents(year, month); + const countries = this.getUniqueSpatialLabels(monthEvents, 3, 5); + if (countries.length > 0) { + spatialSummary = `
${countries.map(c => `${this.esc(c)}`).join("")}
`; + } + } + return ` + ${spatialSummary}
${["S", "M", "T", "W", "T", "F", "S"].map(d => `
${d}
`).join("")}
@@ -1328,18 +1365,16 @@ class FolkCalendarView extends HTMLElement { const months = [quarter * 3, quarter * 3 + 1, quarter * 3 + 2]; const seasonName = ["Winter", "Spring", "Summer", "Autumn"][quarter]; + const spatialLevel = this.getEffectiveSpatialIndex(); + const labelLevel = spatialLevel <= 3 ? 3 : 5; // country or city based on zoom + const monthsHtml = months.map(m => { - const dim = new Date(year, m + 1, 0).getDate(); - const monthEvents: any[] = []; - for (let d = 1; d <= dim; d++) { - const ds = `${year}-${String(m + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`; - monthEvents.push(...this.getEventsForDate(ds)); - } - const cities = this.getUniqueSpatialLabels(monthEvents, 5, 4); - const citiesHtml = cities.length > 0 - ? `
${cities.map(c => `${this.esc(c)}`).join("")}
` + const monthEvents = this.getMonthEvents(year, m); + const labels = this.getUniqueSpatialLabels(monthEvents, labelLevel, 4); + const labelsHtml = labels.length > 0 + ? `
${labels.map(c => `${this.esc(c)}`).join("")}
` : ""; - return `
${this.renderMiniMonth(year, m)}${citiesHtml}
`; + return `
${this.renderMiniMonth(year, m)}${labelsHtml}
`; }).join(""); return ` @@ -1354,9 +1389,21 @@ class FolkCalendarView extends HTMLElement { private renderYear(): string { const year = this.currentDate.getFullYear(); + const spatialLevel = this.getEffectiveSpatialIndex(); + const labelLevel = spatialLevel <= 3 ? 3 : 5; // country or city based on zoom + + const monthsHtml = Array.from({length: 12}, (_, i) => { + const monthEvents = this.getMonthEvents(year, i); + const labels = this.getUniqueSpatialLabels(monthEvents, labelLevel, 3); + const chipsHtml = labels.length > 0 + ? `
${labels.map(c => `${this.esc(c)}`).join("")}
` + : ""; + return `
${this.renderMiniMonth(year, i)}${chipsHtml}
`; + }).join(""); + return `
- ${Array.from({length: 12}, (_, i) => this.renderMiniMonth(year, i)).join("")} + ${monthsHtml}
`; } @@ -1537,8 +1584,10 @@ class FolkCalendarView extends HTMLElement { `; } - const countries = this.getUniqueSpatialLabels(monthEvents, 3, 2); - const countryHtml = countries.length > 0 ? `${countries.join(", ")}` : ""; + const spatialLevel = this.getEffectiveSpatialIndex(); + const labelLevel = spatialLevel <= 1 ? 1 : spatialLevel <= 3 ? 3 : 5; + const spatialLabels = this.getUniqueSpatialLabels(monthEvents, labelLevel, 2); + const countryHtml = spatialLabels.length > 0 ? `${spatialLabels.join(", ")}` : ""; html += `
${monthName}${countryHtml}
@@ -1561,13 +1610,22 @@ class FolkCalendarView extends HTMLElement { const isCurrent = y === new Date().getFullYear(); let monthsHtml = ""; + const yearEvents: any[] = []; for (let m = 0; m < 12; m++) { monthsHtml += this.renderMicroMonth(y, m); + yearEvents.push(...this.getMonthEvents(y, m)); } + // Continent-level labels for the year + const continents = this.getUniqueSpatialLabels(yearEvents, 1, 3); + const continentHtml = continents.length > 0 + ? `
${continents.map(c => `${this.esc(c)}`).join("")}
` + : ""; + html += `
${y}
${monthsHtml}
+ ${continentHtml}
`; } html += `
`; @@ -2714,8 +2772,92 @@ class FolkCalendarView extends HTMLElement { this.mapMarkerLayer.clearLayers(); const located = this.getVisibleLocatedEvents(); + const spatialLevel = this.getEffectiveSpatialIndex(); - for (const ev of located) { + if (spatialLevel <= 1) { + // Planet / Continent — group by continent + this.renderAggregateMarkers(located, 1); + } else if (spatialLevel <= 3) { + // Bioregion / Country — group by country + this.renderAggregateMarkers(located, 3); + } else if (spatialLevel <= 5) { + // Region / City — group by city + this.renderAggregateMarkers(located, 5); + } else { + // Neighborhood / Address / Coordinates — individual markers + this.renderIndividualMarkers(located); + } + + // Auto-fit map to visible event bounds + if (this.zoomCoupled && located.length > 0) { + if (located.length === 1) { + this.leafletMap?.flyTo([located[0].latitude, located[0].longitude], this.getEffectiveLeafletZoom(), { duration: 0.8 }); + } else { + const bounds = L.latLngBounds(located.map((e: any) => [e.latitude, e.longitude])); + this.leafletMap?.flyToBounds(bounds, { padding: [40, 40], maxZoom: 16, duration: 0.8 }); + } + } + } + + /** Render aggregate cluster markers grouped by spatial label at the given level. */ + private renderAggregateMarkers(events: any[], level: number) { + const L = (window as any).L; + if (!L) return; + + // Group events by spatial label + const groups = new Map(); + for (const ev of events) { + const label = this.getSpatialLabel(ev.location_breadcrumb, level) || "Other"; + if (!groups.has(label)) groups.set(label, []); + groups.get(label)!.push(ev); + } + + for (const [label, evs] of groups) { + // Compute centroid — use stable continent centroids at level ≤ 1 + let lat: number, lng: number; + if (level <= 1 && CONTINENT_CENTROIDS[label]) { + [lat, lng] = CONTINENT_CENTROIDS[label]; + } else { + let sumLat = 0, sumLng = 0; + for (const e of evs) { sumLat += e.latitude; sumLng += e.longitude; } + lat = sumLat / evs.length; + lng = sumLng / evs.length; + } + + // Determine dominant source color for border + const colorCounts: Record = {}; + for (const e of evs) { + const c = e.source_color || "#6366f1"; + colorCounts[c] = (colorCounts[c] || 0) + 1; + } + const dominantColor = Object.entries(colorCounts).sort((a, b) => b[1] - a[1])[0]?.[0] || "#6366f1"; + + const icon = L.divIcon({ + className: "", + html: `
${this.esc(label)}${evs.length}
`, + iconSize: [0, 0], + iconAnchor: [0, 0], + }); + + const marker = L.marker([lat, lng], { icon }); + + // Popup listing events in this cluster (first 10) + const popupList = evs.slice(0, 10).map((e: any) => + `
${this.esc(e.title)}
${new Date(e.start_time).toLocaleDateString("default", { month: "short", day: "numeric" })}
` + ).join(""); + const overflow = evs.length > 10 ? `
+${evs.length - 10} more
` : ""; + marker.bindPopup(`
${this.esc(label)} (${evs.length})
${popupList}${overflow}
`); + + this.mapMarkerLayer.addLayer(marker); + } + } + + /** Render individual event markers (original behavior). */ + private renderIndividualMarkers(events: any[]) { + const L = (window as any).L; + if (!L) return; + + for (const ev of events) { const es = this.getEventStyles(ev); const marker = L.circleMarker([ev.latitude, ev.longitude], { radius: 6, color: ev.source_color || "#6366f1", @@ -2731,16 +2873,6 @@ class FolkCalendarView extends HTMLElement { `); this.mapMarkerLayer.addLayer(marker); } - - // Auto-fit map to visible event bounds - if (this.zoomCoupled && located.length > 0) { - if (located.length === 1) { - this.leafletMap?.flyTo([located[0].latitude, located[0].longitude], this.getEffectiveLeafletZoom(), { duration: 0.8 }); - } else { - const bounds = L.latLngBounds(located.map((e: any) => [e.latitude, e.longitude])); - this.leafletMap?.flyToBounds(bounds, { padding: [40, 40], maxZoom: 16, duration: 0.8 }); - } - } } private updateTransitLines() { @@ -2748,6 +2880,9 @@ class FolkCalendarView extends HTMLElement { if (!L || !this.transitLineLayer) return; this.transitLineLayer.clearLayers(); + // Skip transit lines at aggregate zoom levels (spatial < 6) + if (this.getEffectiveSpatialIndex() < 6) return; + const sorted = this.getVisibleLocatedEvents() .sort((a, b) => new Date(a.start_time).getTime() - new Date(b.start_time).getTime()); @@ -2873,6 +3008,10 @@ class FolkCalendarView extends HTMLElement { .map-fab { position: absolute; bottom: 16px; right: 16px; width: 44px; height: 44px; border-radius: 50%; border: 1px solid var(--rs-border-strong); background: var(--rs-bg-surface); color: var(--rs-text-primary); cursor: pointer; font-size: 20px; display: flex; align-items: center; justify-content: center; z-index: 100; box-shadow: var(--rs-shadow-md); transition: all 0.15s; } .map-fab:hover { border-color: var(--rs-primary-hover); background: var(--rs-bg-surface-raised); transform: scale(1.1); } + /* ── Map Cluster Labels (semantic zoom) ── */ + .map-cluster-label { background: var(--rs-bg-surface, #1e1e2e); border: 2px solid var(--rs-primary, #6366f1); border-radius: 16px; padding: 4px 10px; font-size: 12px; font-weight: 600; white-space: nowrap; box-shadow: 0 2px 8px rgba(0,0,0,0.2); color: var(--rs-text-primary, #e0e0e0); display: inline-flex; align-items: center; gap: 4px; transform: translate(-50%, -50%); pointer-events: auto; cursor: pointer; } + .map-cluster-count { font-size: 10px; opacity: 0.7; } + /* ── Synodic (reused in overlay) ── */ .synodic-section { margin: 0 0 8px; } .synodic-labels { display: flex; justify-content: space-between; font-size: 11px; color: var(--rs-text-muted); margin-bottom: 6px; } diff --git a/modules/rcal/geo-hierarchy.ts b/modules/rcal/geo-hierarchy.ts new file mode 100644 index 0000000..32b7be2 --- /dev/null +++ b/modules/rcal/geo-hierarchy.ts @@ -0,0 +1,149 @@ +/** + * Static geographic hierarchy lookup — no API calls, works offline. + * + * Provides continent/country bounding-box lookups and breadcrumb generation + * for semantic zoom on the map and calendar views. + */ + +// ── Continent definitions ── + +interface BoundingBox { + name: string; + latMin: number; + latMax: number; + lngMin: number; + lngMax: number; +} + +interface CountryDef extends BoundingBox { + continent: string; + area: number; // approx sq degrees for specificity sorting +} + +export const CONTINENTS: BoundingBox[] = [ + { name: "Europe", latMin: 35, latMax: 72, lngMin: -25, lngMax: 45 }, + { name: "Asia", latMin: -10, latMax: 75, lngMin: 45, lngMax: 180 }, + { name: "Africa", latMin: -35, latMax: 37, lngMin: -20, lngMax: 55 }, + { name: "North America", latMin: 15, latMax: 84, lngMin: -170, lngMax: -50 }, + { name: "South America", latMin: -56, latMax: 15, lngMin: -82, lngMax: -34 }, + { name: "Oceania", latMin: -48, latMax: 0, lngMin: 110, lngMax: 180 }, + { name: "Antarctica", latMin: -90, latMax: -60, lngMin: -180, lngMax: 180 }, +]; + +export const CONTINENT_CENTROIDS: Record = { + "Europe": [50.0, 10.0], + "Asia": [35.0, 90.0], + "Africa": [5.0, 25.0], + "North America": [45.0, -100.0], + "South America": [-15.0, -60.0], + "Oceania": [-25.0, 140.0], + "Antarctica": [-82.0, 0.0], +}; + +// ~50 major countries sorted by area (ascending) for specificity-first matching +const COUNTRIES: CountryDef[] = [ + // Europe + { name: "Luxembourg", continent: "Europe", latMin: 49.4, latMax: 50.2, lngMin: 5.7, lngMax: 6.5, area: 1 }, + { name: "Slovenia", continent: "Europe", latMin: 45.4, latMax: 46.9, lngMin: 13.4, lngMax: 16.6, area: 5 }, + { name: "Belgium", continent: "Europe", latMin: 49.5, latMax: 51.5, lngMin: 2.5, lngMax: 6.4, area: 8 }, + { name: "Netherlands", continent: "Europe", latMin: 50.7, latMax: 53.6, lngMin: 3.4, lngMax: 7.2, area: 11 }, + { name: "Switzerland", continent: "Europe", latMin: 45.8, latMax: 47.8, lngMin: 5.9, lngMax: 10.5, area: 9 }, + { name: "Denmark", continent: "Europe", latMin: 54.5, latMax: 57.8, lngMin: 8.0, lngMax: 15.2, area: 24 }, + { name: "Austria", continent: "Europe", latMin: 46.4, latMax: 49.0, lngMin: 9.5, lngMax: 17.2, area: 20 }, + { name: "Czech Republic",continent: "Europe", latMin: 48.5, latMax: 51.1, lngMin: 12.1, lngMax: 18.9, area: 18 }, + { name: "Ireland", continent: "Europe", latMin: 51.4, latMax: 55.4, lngMin: -10.5, lngMax: -5.5, area: 20 }, + { name: "Portugal", continent: "Europe", latMin: 36.9, latMax: 42.2, lngMin: -9.5, lngMax: -6.2, area: 18 }, + { name: "Hungary", continent: "Europe", latMin: 45.7, latMax: 48.6, lngMin: 16.1, lngMax: 22.9, area: 20 }, + { name: "Croatia", continent: "Europe", latMin: 42.4, latMax: 46.6, lngMin: 13.5, lngMax: 19.4, area: 25 }, + { name: "Greece", continent: "Europe", latMin: 34.8, latMax: 41.8, lngMin: 19.3, lngMax: 29.6, area: 72 }, + { name: "Italy", continent: "Europe", latMin: 36.6, latMax: 47.1, lngMin: 6.6, lngMax: 18.5, area: 125 }, + { name: "United Kingdom",continent: "Europe", latMin: 49.9, latMax: 58.7, lngMin: -8.2, lngMax: 1.8, area: 88 }, + { name: "Romania", continent: "Europe", latMin: 43.6, latMax: 48.3, lngMin: 20.3, lngMax: 29.7, area: 44 }, + { name: "Poland", continent: "Europe", latMin: 49.0, latMax: 54.8, lngMin: 14.1, lngMax: 24.2, area: 59 }, + { name: "Germany", continent: "Europe", latMin: 47.3, latMax: 55.1, lngMin: 5.9, lngMax: 15.0, area: 71 }, + { name: "Finland", continent: "Europe", latMin: 59.8, latMax: 70.1, lngMin: 20.6, lngMax: 31.6, area: 113 }, + { name: "Norway", continent: "Europe", latMin: 57.9, latMax: 71.2, lngMin: 4.6, lngMax: 31.1, area: 353 }, + { name: "Sweden", continent: "Europe", latMin: 55.3, latMax: 69.1, lngMin: 11.1, lngMax: 24.2, area: 181 }, + { name: "France", continent: "Europe", latMin: 41.3, latMax: 51.1, lngMin: -5.1, lngMax: 9.6, area: 144 }, + { name: "Spain", continent: "Europe", latMin: 36.0, latMax: 43.8, lngMin: -9.3, lngMax: 4.3, area: 106 }, + { name: "Ukraine", continent: "Europe", latMin: 44.4, latMax: 52.4, lngMin: 22.1, lngMax: 40.2, area: 145 }, + { name: "Turkey", continent: "Europe", latMin: 36.0, latMax: 42.1, lngMin: 26.0, lngMax: 44.8, area: 115 }, + // Asia + { name: "Israel", continent: "Asia", latMin: 29.5, latMax: 33.3, lngMin: 34.2, lngMax: 35.9, area: 6 }, + { name: "South Korea", continent: "Asia", latMin: 33.1, latMax: 38.6, lngMin: 124.6, lngMax: 131.9, area: 40 }, + { name: "Japan", continent: "Asia", latMin: 24.0, latMax: 45.5, lngMin: 123.0, lngMax: 146.0, area: 495 }, + { name: "Thailand", continent: "Asia", latMin: 5.6, latMax: 20.5, lngMin: 97.3, lngMax: 105.6, area: 124 }, + { name: "Vietnam", continent: "Asia", latMin: 8.4, latMax: 23.4, lngMin: 102.1, lngMax: 109.5, area: 111 }, + { name: "Indonesia", continent: "Asia", latMin: -11.0, latMax: 6.0, lngMin: 95.0, lngMax: 141.0, area: 782 }, + { name: "India", continent: "Asia", latMin: 6.7, latMax: 35.5, lngMin: 68.2, lngMax: 97.4, area: 841 }, + { name: "China", continent: "Asia", latMin: 18.2, latMax: 53.6, lngMin: 73.5, lngMax: 134.8, area: 2172 }, + // Africa + { name: "Tunisia", continent: "Africa", latMin: 30.2, latMax: 37.3, lngMin: 7.5, lngMax: 11.6, area: 29 }, + { name: "Morocco", continent: "Africa", latMin: 27.7, latMax: 35.9, lngMin: -13.2, lngMax: -1.0, area: 100 }, + { name: "Kenya", continent: "Africa", latMin: -4.7, latMax: 5.0, lngMin: 33.9, lngMax: 41.9, area: 78 }, + { name: "South Africa", continent: "Africa", latMin: -34.8, latMax: -22.1, lngMin: 16.5, lngMax: 32.9, area: 209 }, + { name: "Nigeria", continent: "Africa", latMin: 4.3, latMax: 13.9, lngMin: 2.7, lngMax: 14.7, area: 115 }, + { name: "Egypt", continent: "Africa", latMin: 22.0, latMax: 31.7, lngMin: 24.7, lngMax: 36.9, area: 118 }, + { name: "Ethiopia", continent: "Africa", latMin: 3.4, latMax: 14.9, lngMin: 33.0, lngMax: 48.0, area: 173 }, + // North America + { name: "Costa Rica", continent: "North America", latMin: 8.0, latMax: 11.2, lngMin: -85.9, lngMax: -82.6, area: 11 }, + { name: "Mexico", continent: "North America", latMin: 14.5, latMax: 32.7, lngMin: -118.4, lngMax: -86.7, area: 577 }, + { name: "United States", continent: "North America", latMin: 24.5, latMax: 49.4, lngMin: -125.0, lngMax: -66.9, area: 1449 }, + { name: "Canada", continent: "North America", latMin: 41.7, latMax: 83.1, lngMin: -141.0, lngMax: -52.6, area: 3652 }, + // South America + { name: "Colombia", continent: "South America", latMin: -4.2, latMax: 12.5, lngMin: -79.0, lngMax: -66.9, area: 202 }, + { name: "Argentina", continent: "South America", latMin: -55.1, latMax: -21.8, lngMin: -73.6, lngMax: -53.6, area: 666 }, + { name: "Brazil", continent: "South America", latMin: -33.8, latMax: 5.3, lngMin: -73.9, lngMax: -34.8, area: 1528 }, + // Oceania + { name: "New Zealand", continent: "Oceania", latMin: -47.3, latMax: -34.4, lngMin: 166.4, lngMax: 178.6, area: 157 }, + { name: "Australia", continent: "Oceania", latMin: -43.6, latMax: -10.7, lngMin: 113.2, lngMax: 153.6, area: 1331 }, +].sort((a, b) => a.area - b.area); // smallest first for specificity + +// ── Lookup functions ── + +function inBox(lat: number, lng: number, box: BoundingBox): boolean { + return lat >= box.latMin && lat <= box.latMax && lng >= box.lngMin && lng <= box.lngMax; +} + +export function lookupContinent(lat: number, lng: number): string | null { + for (const c of CONTINENTS) { + if (inBox(lat, lng, c)) return c.name; + } + return null; +} + +export function lookupCountry(lat: number, lng: number): string | null { + // Countries sorted by area ascending → smallest match first (most specific) + for (const c of COUNTRIES) { + if (inBox(lat, lng, c)) return c.name; + } + return null; +} + +/** + * Generate a geographic breadcrumb string from coordinates. + * Returns e.g. "Earth > Europe > Germany > Berlin" + * + * @param lat Latitude + * @param lng Longitude + * @param locationName Optional city/venue name to append as most specific level + */ +export function generateBreadcrumb(lat: number, lng: number, locationName?: string | null): string { + const parts = ["Earth"]; + const continent = lookupContinent(lat, lng); + if (continent) parts.push(continent); + const country = lookupCountry(lat, lng); + if (country) parts.push(country); + if (locationName) { + // Strip common venue prefixes to get just the city/place + // e.g. "Factory Berlin" → "Berlin", "KICC, Nairobi" → "Nairobi" + const city = locationName.includes(",") + ? locationName.split(",").pop()!.trim() + : locationName; + // Avoid duplicating country in breadcrumb + if (city && city !== country) { + parts.push(city); + } + } + return parts.join(" > "); +} diff --git a/modules/rcal/local-first-client.ts b/modules/rcal/local-first-client.ts index d45f74b..943449e 100644 --- a/modules/rcal/local-first-client.ts +++ b/modules/rcal/local-first-client.ts @@ -70,7 +70,7 @@ export class CalLocalFirstClient { const docId = calendarDocId(this.#space) as DocumentId; this.#sync.change(docId, `Update event ${eventId}`, (d) => { if (!d.events[eventId]) { - d.events[eventId] = { id: eventId, title: '', description: '', startTime: 0, endTime: 0, allDay: false, timezone: null, rrule: null, status: null, visibility: null, sourceId: null, sourceName: null, sourceType: null, sourceColor: null, locationId: null, locationName: null, coordinates: null, locationGranularity: null, locationLat: null, locationLng: null, isVirtual: false, virtualUrl: null, virtualPlatform: null, rToolSource: null, rToolEntityId: null, attendees: [], attendeeCount: 0, metadata: null, createdAt: Date.now(), updatedAt: Date.now(), ...changes } as CalendarEvent; + d.events[eventId] = { id: eventId, title: '', description: '', startTime: 0, endTime: 0, allDay: false, timezone: null, rrule: null, status: null, visibility: null, sourceId: null, sourceName: null, sourceType: null, sourceColor: null, locationId: null, locationName: null, coordinates: null, locationGranularity: null, locationLat: null, locationLng: null, locationBreadcrumb: null, isVirtual: false, virtualUrl: null, virtualPlatform: null, rToolSource: null, rToolEntityId: null, attendees: [], attendeeCount: 0, metadata: null, createdAt: Date.now(), updatedAt: Date.now(), ...changes } as CalendarEvent; } else { Object.assign(d.events[eventId], changes); d.events[eventId].updatedAt = Date.now(); diff --git a/modules/rcal/mod.ts b/modules/rcal/mod.ts index fb627b0..4f069ee 100644 --- a/modules/rcal/mod.ts +++ b/modules/rcal/mod.ts @@ -87,6 +87,7 @@ function eventToRow(ev: CalendarEvent, sources: Record) location_lat: ev.locationLat, location_lng: ev.locationLng, location_granularity: ev.locationGranularity, + location_breadcrumb: ev.locationBreadcrumb, is_virtual: ev.isVirtual, virtual_url: ev.virtualUrl, virtual_platform: ev.virtualPlatform, @@ -254,6 +255,7 @@ function seedDemoIfEmpty(space: string) { locationGranularity: e.locationGranularity || null, locationLat: e.locationLat ?? null, locationLng: e.locationLng ?? null, + locationBreadcrumb: null, isVirtual: e.isVirtual || false, virtualUrl: e.virtualUrl || null, virtualPlatform: e.virtualPlatform || null, @@ -410,6 +412,7 @@ routes.post("/api/events", async (c) => { locationGranularity: null, locationLat: null, locationLng: null, + locationBreadcrumb: null, isVirtual: is_virtual || false, virtualUrl: virtual_url || null, virtualPlatform: virtual_platform || null, @@ -540,6 +543,7 @@ routes.post("/api/import-ics", async (c) => { locationGranularity: null, locationLat: null, locationLng: null, + locationBreadcrumb: null, isVirtual: false, virtualUrl: null, virtualPlatform: null, diff --git a/modules/rcal/schemas.ts b/modules/rcal/schemas.ts index a2dfd7a..b685da7 100644 --- a/modules/rcal/schemas.ts +++ b/modules/rcal/schemas.ts @@ -50,6 +50,7 @@ export interface CalendarEvent { virtualPlatform: string | null; rToolSource: string | null; rToolEntityId: string | null; + locationBreadcrumb: string | null; // "Earth > Europe > Germany > Berlin" attendees: unknown[]; attendeeCount: number; tags: string[] | null; diff --git a/modules/rschedule/mod.ts b/modules/rschedule/mod.ts index 45ceb48..443e387 100644 --- a/modules/rschedule/mod.ts +++ b/modules/rschedule/mod.ts @@ -259,6 +259,7 @@ async function executeCalendarEvent( locationGranularity: null, locationLat: null, locationLng: null, + locationBreadcrumb: null, isVirtual: false, virtualUrl: null, virtualPlatform: null, @@ -1085,6 +1086,7 @@ function syncReminderToCalendar(reminder: Reminder, space: string): string | nul locationGranularity: null, locationLat: null, locationLng: null, + locationBreadcrumb: null, isVirtual: false, virtualUrl: null, virtualPlatform: null, @@ -1695,6 +1697,7 @@ async function executeWorkflowNode( locationGranularity: null, locationLat: null, locationLng: null, + locationBreadcrumb: null, isVirtual: false, virtualUrl: null, virtualPlatform: null,