mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-22 06:41:46 +00:00
feat(notices): add system notice infrastructure
Server-side notice registry with per-user condition evaluation (firstLogin, existingUserBeforeVersion, addonEnabled, dateWindow, role, custom). Notices are sorted by priority then severity, filtered against dismissals stored in a new user_notice_dismissals table, and served via GET /api/system-notices/active + POST /api/system-notices/:id/dismiss. Client renders notices through a host component that partitions by display type (modal / banner / toast). The modal renderer supports multi-page pagination with directional slide transitions, keyboard navigation, and correct dismiss-all semantics on CTA / X / ESC. Dismissals are optimistic with a single background retry. Includes 3.0.0 upgrade notices (v3-photos, v3-journey, v3-features), onboarding welcome modal, and full i18n coverage across 15 languages. The /journey route is addon-gated on both client and server. Also includes: unit + integration test suites, registry integrity test that validates action CTA IDs against client source, and technical documentation in docs/system-notices.md.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { create } from 'zustand';
|
||||
import axios from '../api/client.js';
|
||||
|
||||
// Type mirrors SystemNoticeDTO from the server (copy here to avoid cross-package import)
|
||||
export interface SystemNoticeDTO {
|
||||
id: string;
|
||||
display: 'modal' | 'banner' | 'toast';
|
||||
severity: 'info' | 'warn' | 'critical';
|
||||
titleKey: string;
|
||||
bodyKey: string;
|
||||
bodyParams?: Record<string, string>;
|
||||
icon?: string;
|
||||
media?: {
|
||||
src: string;
|
||||
srcDark?: string;
|
||||
altKey: string;
|
||||
placement?: 'hero' | 'inline';
|
||||
aspectRatio?: string;
|
||||
};
|
||||
highlights?: Array<{ labelKey: string; iconName?: string }>;
|
||||
cta?: (
|
||||
| { kind: 'nav'; labelKey: string; href: string }
|
||||
| { kind: 'action'; labelKey: string; actionId: string; dismissOnAction?: boolean }
|
||||
);
|
||||
dismissible: boolean;
|
||||
}
|
||||
|
||||
interface SystemNoticeState {
|
||||
notices: SystemNoticeDTO[];
|
||||
loaded: boolean;
|
||||
fetching: boolean;
|
||||
fetch: () => Promise<void>;
|
||||
dismiss: (id: string) => void;
|
||||
}
|
||||
|
||||
export const useSystemNoticeStore = create<SystemNoticeState>()((set, get) => ({
|
||||
notices: [],
|
||||
loaded: false,
|
||||
fetching: false,
|
||||
|
||||
async fetch() {
|
||||
if (get().fetching || get().loaded) return;
|
||||
set({ fetching: true });
|
||||
try {
|
||||
const res = await axios.get<SystemNoticeDTO[]>('/system-notices/active');
|
||||
set({ notices: res.data, loaded: true, fetching: false });
|
||||
} catch (err) {
|
||||
// Notices are non-critical. Fail silently; set loaded so UI doesn't hang.
|
||||
console.warn('[systemNotices] failed to fetch:', err);
|
||||
set({ loaded: true, fetching: false });
|
||||
}
|
||||
},
|
||||
|
||||
dismiss(id: string) {
|
||||
// Optimistic: remove immediately
|
||||
const prev = get().notices;
|
||||
set({ notices: prev.filter(n => n.id !== id) });
|
||||
|
||||
// POST in background; retry once on error
|
||||
const post = () => axios.post(`/system-notices/${id}/dismiss`);
|
||||
post().catch(() => {
|
||||
setTimeout(() => {
|
||||
post().catch(e => console.warn('[systemNotices] dismiss failed:', e));
|
||||
}, 2000);
|
||||
});
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user