mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-14 00:01:03 +00:00
390135f3bc
The old popup was a narrow white card with its own copy of the texts in eight languages, hex colours and a minute counter on the browser clock. The new DemoWelcome is a wide dialog on DialogShell with theme tokens only: what TREK is in one line, the hourly reset with a live countdown in the server's time zone, the 5.0 highlights to try right away (trip planner with bookings and costs, Tours with 3D terrain, TREK Studio photo books, live collaboration, Journey, Atlas, Vacay, Collections, offline use), what the demo leaves out and what the full version adds, and the link to the source. Its strings are real i18n keys in a new demo domain file, translated into all 27 locales. Both dashboards open it instead of the old DemoBanner, which leaves the theme and format baselines with its file and the test non-null assertions it held.
112 lines
4.0 KiB
TypeScript
112 lines
4.0 KiB
TypeScript
import type { LucideIcon } from 'lucide-react';
|
|
import {
|
|
BookImage,
|
|
Bookmark,
|
|
Bot,
|
|
CalendarDays,
|
|
DatabaseBackup,
|
|
Globe,
|
|
KeyRound,
|
|
LayoutDashboard,
|
|
Map as MapIcon,
|
|
Mountain,
|
|
NotebookPen,
|
|
Puzzle,
|
|
ShieldCheck,
|
|
Sparkles,
|
|
Terminal,
|
|
Upload,
|
|
UserLock,
|
|
Users,
|
|
WifiOff,
|
|
} from 'lucide-react';
|
|
|
|
/** One thing a visitor can try in the demo right away: a tile of the welcome dialog. */
|
|
export interface DemoFeature {
|
|
id: string;
|
|
icon: LucideIcon;
|
|
titleKey: string;
|
|
bodyKey: string;
|
|
}
|
|
|
|
/** A line of the two lists under the tiles: what the demo leaves out, and what the full version adds. */
|
|
export interface DemoLine {
|
|
icon: LucideIcon;
|
|
labelKey: string;
|
|
}
|
|
|
|
export const DEMO_FEATURES: readonly DemoFeature[] = [
|
|
{ id: 'planner', icon: MapIcon, titleKey: 'demo.feature.planner.title', bodyKey: 'demo.feature.planner.body' },
|
|
{ id: 'tours', icon: Mountain, titleKey: 'demo.feature.tours.title', bodyKey: 'demo.feature.tours.body' },
|
|
{ id: 'studio', icon: BookImage, titleKey: 'demo.feature.studio.title', bodyKey: 'demo.feature.studio.body' },
|
|
{ id: 'collab', icon: Users, titleKey: 'demo.feature.collab.title', bodyKey: 'demo.feature.collab.body' },
|
|
{ id: 'journey', icon: NotebookPen, titleKey: 'demo.feature.journey.title', bodyKey: 'demo.feature.journey.body' },
|
|
{ id: 'atlas', icon: Globe, titleKey: 'demo.feature.atlas.title', bodyKey: 'demo.feature.atlas.body' },
|
|
{ id: 'vacay', icon: CalendarDays, titleKey: 'demo.feature.vacay.title', bodyKey: 'demo.feature.vacay.body' },
|
|
{
|
|
id: 'collections',
|
|
icon: Bookmark,
|
|
titleKey: 'demo.feature.collections.title',
|
|
bodyKey: 'demo.feature.collections.body',
|
|
},
|
|
{ id: 'offline', icon: WifiOff, titleKey: 'demo.feature.offline.title', bodyKey: 'demo.feature.offline.body' },
|
|
];
|
|
|
|
/** What the shared demo account cannot do; the server refuses each of them too. */
|
|
export const DEMO_LIMITS: readonly DemoLine[] = [
|
|
{ icon: Upload, labelKey: 'demo.limit.uploads' },
|
|
{ icon: UserLock, labelKey: 'demo.limit.account' },
|
|
{ icon: KeyRound, labelKey: 'demo.limit.apiKeys' },
|
|
{ icon: Terminal, labelKey: 'demo.limit.mcp' },
|
|
{ icon: Sparkles, labelKey: 'demo.limit.ai' },
|
|
];
|
|
|
|
export const DEMO_FULL_VERSION: readonly DemoLine[] = [
|
|
{ icon: ShieldCheck, labelKey: 'demo.full.sso' },
|
|
{ icon: DatabaseBackup, labelKey: 'demo.full.backups' },
|
|
{ icon: Puzzle, labelKey: 'demo.full.plugins' },
|
|
{ icon: Bot, labelKey: 'demo.full.assistant' },
|
|
{ icon: LayoutDashboard, labelKey: 'demo.full.admin' },
|
|
];
|
|
|
|
export const TREK_REPO_URL = 'https://github.com/liketrek/TREK';
|
|
|
|
/** The demo resets on the hour (the server's `0 * * * *` cron). */
|
|
const HOUR_SECONDS = 3600;
|
|
|
|
/**
|
|
* Minute and second of the hour at `now` in the server's time zone, where the
|
|
* reset cron runs. A zone whose offset is not whole hours (India, Nepal) moves
|
|
* the hour mark; an unknown zone falls back to the browser's clock.
|
|
*/
|
|
function minuteAndSecond(now: Date, timeZone: string | undefined): [number, number] {
|
|
if (timeZone) {
|
|
try {
|
|
const parts = new Intl.DateTimeFormat('en-US', { timeZone, minute: 'numeric', second: 'numeric' }).formatToParts(
|
|
now
|
|
);
|
|
const read = (type: 'minute' | 'second') => Number(parts.find((p) => p.type === type)?.value);
|
|
const minute = read('minute');
|
|
const second = read('second');
|
|
if (Number.isFinite(minute) && Number.isFinite(second)) return [minute, second];
|
|
} catch {
|
|
// A zone this browser does not know: the local clock below.
|
|
}
|
|
}
|
|
return [now.getMinutes(), now.getSeconds()];
|
|
}
|
|
|
|
/** Seconds until the next hourly reset, from 1 up to 3600. */
|
|
export function secondsUntilReset(now: Date, timeZone?: string): number {
|
|
const [minute, second] = minuteAndSecond(now, timeZone);
|
|
return HOUR_SECONDS - (minute * 60 + second);
|
|
}
|
|
|
|
/** The countdown as minutes and seconds, `42:07`. */
|
|
export function formatCountdown(seconds: number): string {
|
|
const safe = Math.max(0, Math.floor(seconds));
|
|
const minutes = Math.floor(safe / 60);
|
|
const rest = safe % 60;
|
|
return `${String(minutes).padStart(2, '0')}:${String(rest).padStart(2, '0')}`;
|
|
}
|