How the Journey Journal Feature Aggregates and Displays Information Across Multiple Trips in TREK
The Journey Journal links multiple trips via a trip_ids array, aggregates entries from all linked trips into a unified chronological timeline using client-side grouping logic, and renders them through synchronized timeline, gallery, and map views.
TREK is an open-source travel platform that implements a photo-first Journey Journal capable of spanning one or more trips. Unlike isolated trip logs, this feature aggregates entries from multiple excursions into a single chronological stream, allowing travelers to document extended journeys that cross multiple destinations or time periods.
Data Model and Trip Linking
The aggregation architecture begins in the data layer, where journeys maintain explicit links to their constituent trips through a relational array structure.
Creating Multi-Trip Journeys
Journeys are created with an optional trip_ids parameter that links existing trips to the new journal. According to the TREK source code in client/src/store/journeyStore.ts, the createJourney method accepts this array and triggers the backend to import those trips' places as location anchors for future entries.
// From journeyStore.ts (lines 66-70)
const newJourney = await useJourneyStore.getState().createJourney({
title: 'European Summer',
subtitle: 'June‑August 2025',
trip_ids: [12, 34, 57], // IDs of existing trips to link
});
When the backend receives this request, it stores the relationships in a JourneyTrip[] structure and copies the linked trips' places into the journey's location suggestion pool. This allows users to tag entries with locations from any linked trip without affecting the display logic.
Loading Aggregated Data
The loadJourney function in the same store (lines 50-56) retrieves the complete journal payload, including the trips array and all associated entries. Because entries are stored at the journey level rather than nested within individual trips, the API returns a flat list of entries regardless of which trip they originated from.
Client-Side Aggregation Logic
Once the data reaches the client, the useJourneyPublic hook (for public views) and similar internal hooks transform the flat entry list into derived collections optimized for UI rendering.
Grouping Entries Chronologically
The hook first aggregates entries by date using the pure helper groupByDate, defined in client/src/pages/journeyPublic/journeyPublicModel.ts (lines 44-51). This creates a Map<string, Entry[]> where keys are ISO date strings and values are arrays of entries for that day.
// From journeyPublicModel.ts
const groupedEntries = useMemo(() => groupByDate(entries), [entries]);
const sortedDates = useMemo(() => [...groupedEntries.keys()].sort(), [groupedEntries]);
The sortedDates array (calculated in useJourneyPublic.ts lines 54-57) provides the chronological backbone for the entire UI, ensuring day headers appear in correct temporal order even when entries originate from different linked trips.
Synchronizing Map Markers with Timeline Days
To maintain visual consistency between the timeline and map views, the hook generates sidebarMapItems with color coding derived from the global sortedDates list. As implemented in useJourneyPublic.ts (lines 63-84), this logic assigns each entry a dayColor based on its position in the sorted dates array, ensuring markers match their corresponding timeline day headers.
// From useJourneyPublic.ts
const sidebarMapItems = useMemo(() => {
const counters = new Map<string, number>();
return entries
.filter(e => e.location_lat && e.location_lng)
.map(e => {
const dayIdx = sortedDates.indexOf(e.entry_date);
const dayLabel = (counters.get(e.entry_date) ?? 0) + 1;
counters.set(e.entry_date, dayLabel);
return {
id: String(e.id),
lat: e.location_lat!,
lng: e.location_lng!,
title: e.title ?? '',
mood: e.mood,
dayColor: DAY_COLORS[dayIdx % DAY_COLORS.length],
dayLabel,
};
});
}, [entries, sortedDates]);
Rendering Multi-Trip Views
The presentation layer in JourneyPublicPage.tsx consumes these aggregated data structures to render three interchangeable view modes that treat multi-trip content as a single coherent narrative.
Timeline View with Day Headers
The timeline implementation (lines 97-107) iterates over sortedDates, rendering a colored day header for each date followed by all entries belonging to that day via groupedEntries.get(date). This approach seamlessly interleaves entries from different linked trips when they occur on the same date.
{sortedDates.map((date, dayIdx) => {
const dayEntries = groupedEntries.get(date)!;
const { weekday, month, day } = formatDate(date, locale);
const dayColor = DAY_COLORS[dayIdx % DAY_COLORS.length];
return (
<div key={date}>
{/* Day header with chronological color coding */}
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-xl flex items-center justify-center text-[14px] font-bold text-white"
style={{ background: dayColor }}>
{dayIdx + 1}
</div>
<div>
<div className="text-[14px] font-semibold">{weekday}</div>
<div className="text-[11px]">{month} {day}</div>
</div>
</div>
{/* Entries aggregated from all linked trips */}
{dayEntries.map(entry => (
<EntryCard key={entry.id} entry={entry} token={token!} />
))}
</div>
);
})}
Map and Gallery Integration
The map view (lines 109-118) receives the pre-computed sidebarMapItems array, rendering markers with their day-specific colors already assigned. This guarantees that hovering a timeline entry highlights the corresponding map marker with matching color coding.
The gallery view (lines 94-99) operates on allPhotos, a flat collection of every photo belonging to the journey regardless of source trip. This allows users to browse the entire visual narrative without trip boundaries.
Summary
- Multi-trip linking: Journeys store references to multiple trips via the
trip_idsarray injourneyStore.ts, importing their places as location suggestions while keeping entries at the journey level. - Chronological aggregation: The
groupByDatehelper injourneyPublicModel.tsand thesortedDatesderivation inuseJourneyPublic.tscreate a unified timeline across all linked trips. - Synchronized visualization: Map markers receive
dayColorassignments based on the global sorted date list, ensuring consistency between timeline, map, and gallery views. - Entry storage model: Entries belong to the journey, not individual trips, enabling seamless aggregation even when trips overlap or span different time zones.
Frequently Asked Questions
How does the Journey Journal link multiple trips in TREK?
The journal links trips through the trip_ids array passed to createJourney in client/src/store/journeyStore.ts (lines 66-70). When a journey is created with this array, the backend establishes JourneyTrip relationships and copies the linked trips' places into the journey's location pool for tagging entries.
Where are journal entries stored in relation to trips?
Entries are stored at the journey level, not nested within individual trips. This design allows the UI to treat all entries as a single chronological stream regardless of which linked trip they document. The trip links serve primarily as location sources during entry creation.
How does the map view stay synchronized with the timeline day colors?
The useJourneyPublic hook generates map markers with dayColor values computed from the sortedDates array index (lines 63-84). Since both the timeline headers and map markers derive their colors from the same sorted date list, they remain visually synchronized even when days contain entries from multiple different trips.
Can entries be added to a journey without linking a trip initially?
Yes. While the creation flow described in wiki/Journey-Journal.md (lines 21-22) allows selecting trips to import location anchors, entries can be created independently and tagged with any location. The linked trips primarily provide convenience suggestions during the authoring process.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →