Files
TREK/client/src/store/tripJournalLinkStore.ts
T
Maurice fe05afec19 feat(trips): trip settings with a linked journal tab
The Share button becomes the trip's Settings, a round button beside the back
pill. The dialog is rebuilt around three areas (People, Sharing, Journal) with
a side navigation, a fixed height and 960 px width on wide screens.

Journal links one of your journals from the Journey addon to the trip. Every
trip member is ticked as a reader by default, each can be raised to editor or
left out, and the people a save would let in for the first time are named
before it saves. The link can be changed or removed. With a link the trip gets
a Journal tab on desktop and phone that shows the Journey addon's own view,
live through its sync and trip:journal:changed. Nothing shows with the addon
off.

members.shareTrip and nav.share are gone with the button that used them.
2026-10-18 17:41:25 +02:00

50 lines
2.3 KiB
TypeScript

import type { TripJournalLinkMemberRequest, TripJournalLinkResponse } from '@trek/shared';
import { create } from 'zustand';
import { tripJournalLinkRepo } from '../repo/tripJournalLinkRepo';
/**
* The journal each trip links, as this person may see it, remembered for the
* session so the trip's Journal tab and its Settings dialog render the last
* answer at once. Both read it from here, so the tab appears and goes the moment
* the dialog links or unlinks, without a second request.
*/
interface TripJournalLinkState {
/** Per trip id, the server's answer. Missing until asked. */
byTrip: Record<string, TripJournalLinkResponse>;
refresh: (tripId: number) => Promise<void>;
link: (tripId: number, journeyId: number, members: TripJournalLinkMemberRequest[]) => Promise<void>;
setMembers: (tripId: number, members: TripJournalLinkMemberRequest[]) => Promise<void>;
unlink: (tripId: number) => Promise<void>;
}
/** The answer for a trip someone cannot reach, or with the Journey addon off: no journal. */
const NONE: TripJournalLinkResponse = { link: null, can_link: false, can_unlink: false, can_edit_members: false };
export const useTripJournalLinkStore = create<TripJournalLinkState>((set) => {
const keep = (tripId: number, answer: TripJournalLinkResponse) =>
set((s) => ({ byTrip: { ...s.byTrip, [String(tripId)]: answer } }));
return {
byTrip: {},
refresh: async (tripId) => {
try {
keep(tripId, await tripJournalLinkRepo.get(tripId));
} catch (err) {
// 404 is the Journey addon switched off or a trip this person no longer
// reaches: both a real "no journal". Anything else, offline included, is
// no answer at all, and the last one stands.
const status = (err as { response?: { status?: number } })?.response?.status;
if (status === 404 || status === 403) keep(tripId, NONE);
}
},
// The writes answer with the link as it now stands, so the tab and the
// dialog move together; a failure is the caller's to report.
link: async (tripId, journeyId, members) =>
keep(tripId, await tripJournalLinkRepo.link(tripId, journeyId, members)),
setMembers: async (tripId, members) => keep(tripId, await tripJournalLinkRepo.setMembers(tripId, members)),
unlink: async (tripId) => keep(tripId, await tripJournalLinkRepo.unlink(tripId)),
};
});