mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
a9fb0fc475
A Photo credits link in the dialog's footer opens a second view with every photo: its title linking to its page on Wikimedia Commons, its author and its license linking to the deed. The list is its own chunk, loaded when a visitor opens it. The countdown test reads the clock the interval moved.
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. */
|
|
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')}`;
|
|
}
|