57 lines
3.8 KiB
TypeScript
57 lines
3.8 KiB
TypeScript
// Routes: Portfolio -> one App (tabs) plus the workspace screens. Pure, so tests can check the deep links.
|
|
// An app is keyed by its project id, or (store-only apps, no folder) by its store target ("store:...").
|
|
import { createContext, useContext } from 'react';
|
|
import type { Key } from './i18n';
|
|
import type { IconName } from './ui/icons';
|
|
|
|
export const APP_TABS = ['releases', 'catalog', 'prices', 'listings', 'art', 'health', 'reviews', 'testflight', 'growth', 'appinfo', 'checks', 'tools'] as const;
|
|
export type AppTab = (typeof APP_TABS)[number];
|
|
/** Tabs of a store-only app: store reads only (catalog, prices, app info and checks need the app folder; listings and
|
|
* store art are the live ones; Growth shows the Product Page Optimization tests only). */
|
|
export const STORE_TABS: readonly AppTab[] = ['releases', 'listings', 'art', 'health', 'reviews', 'testflight', 'growth'];
|
|
export const tabsFor = (hasFolder: boolean): readonly AppTab[] => (hasFolder ? APP_TABS : STORE_TABS);
|
|
export const TAB_KEY: Record<AppTab, Key> = {
|
|
releases: 'tab.releases', catalog: 'tab.catalog', prices: 'tab.prices', listings: 'tab.listings', art: 'tab.art', health: 'tab.health', reviews: 'tab.reviews',
|
|
testflight: 'tab.testflight', growth: 'tab.growth', appinfo: 'tab.appinfo', checks: 'tab.checks', tools: 'tab.tools',
|
|
};
|
|
export const TAB_ICON: Record<AppTab, IconName> = {
|
|
releases: 'rocket', catalog: 'tag', prices: 'coins', listings: 'text', art: 'grid', health: 'activity', reviews: 'message', testflight: 'plane', growth: 'chart',
|
|
appinfo: 'clipboard', checks: 'shield', tools: 'sliders',
|
|
};
|
|
export const WORKSPACE = ['apps', 'accounts', 'ignored', 'dependencies', 'settings'] as const;
|
|
export type WorkspaceScreen = (typeof WORKSPACE)[number];
|
|
|
|
export type Route = { screen: 'portfolio' } | { screen: 'app'; appId: string; tab: AppTab } | { screen: WorkspaceScreen; section?: string };
|
|
|
|
/** storebound://<screen> deep links and the dev-only start switch ("app:health" = first app, Health tab;
|
|
* "app:health:2" = second app; "settings:refresh" = a Settings section). `apps` are the app keys in list order. */
|
|
export function parseStart(v: string, apps: string[]): Route | null {
|
|
const [screen, tab, n] = v.split(':');
|
|
const app = apps[Math.max(0, (Number(n) || 1) - 1)];
|
|
if (screen === 'app' && app) return { screen: 'app', appId: app, tab: APP_TABS.includes(tab as AppTab) ? tab as AppTab : 'releases' };
|
|
if (screen === 'portfolio') return { screen: 'portfolio' };
|
|
if (!(WORKSPACE as readonly string[]).includes(screen ?? '')) return null;
|
|
return tab ? { screen: screen as WorkspaceScreen, section: tab } : { screen: screen as WorkspaceScreen };
|
|
}
|
|
|
|
/** A route sent as JSON by the host (tray menu, notification target); anything that is not a known route is null. */
|
|
export function parseRouteJson(v: string): Route | null {
|
|
let o: unknown;
|
|
try { o = JSON.parse(v); } catch { return null; }
|
|
if (!o || typeof o !== 'object') return null;
|
|
const r = o as Record<string, unknown>;
|
|
if (r.screen === 'portfolio') return { screen: 'portfolio' };
|
|
if (r.screen === 'app') return typeof r.appId === 'string' && r.appId ? { screen: 'app', appId: r.appId, tab: APP_TABS.includes(r.tab as AppTab) ? r.tab as AppTab : 'releases' } : null;
|
|
if (typeof r.screen === 'string' && (WORKSPACE as readonly string[]).includes(r.screen)) return typeof r.section === 'string' ? { screen: r.screen as WorkspaceScreen, section: r.section } : { screen: r.screen as WorkspaceScreen };
|
|
return null;
|
|
}
|
|
|
|
export const sameRoute = (a: Route, b: Route): boolean => JSON.stringify(a) === JSON.stringify(b);
|
|
|
|
export interface Nav { route: Route; go: (r: Route) => void; openPalette: () => void; openWelcome: () => void }
|
|
export const NavCtx = createContext<Nav | null>(null);
|
|
export const useNav = (): Nav => {
|
|
const n = useContext(NavCtx);
|
|
if (!n) throw new Error('useNav outside NavCtx');
|
|
return n;
|
|
};
|