a98096a042
- Created .gitignore to exclude build artifacts and dependencies. - Added index.html as the main entry point for the application. - Included LICENSE file with Apache 2.0 terms. - Initialized package.json and package-lock.json for project dependencies. - Added pnpm-lock.yaml for package management. - Created QUICKSTART.md for setup instructions. - Added README.md and README.zh-CN.md for project documentation in English and Chinese.
66 lines
2.2 KiB
TypeScript
66 lines
2.2 KiB
TypeScript
// Tiny URL router. We avoid pulling in react-router for two reasons:
|
|
// the surface area we need is small (three routes, plain pushState), and
|
|
// we want a single source of truth for "what file is open" — encoding
|
|
// that in the URL is the simplest way to make it deep-linkable.
|
|
|
|
import { useEffect, useState } from 'react';
|
|
|
|
export type Route =
|
|
| { kind: 'home' }
|
|
| { kind: 'project'; projectId: string; fileName: string | null };
|
|
|
|
export function parseRoute(pathname: string): Route {
|
|
const parts = pathname.replace(/\/+$/, '').split('/').filter(Boolean);
|
|
if (parts.length === 0) return { kind: 'home' };
|
|
if (parts[0] === 'projects' && parts[1]) {
|
|
const projectId = decodeURIComponent(parts[1]);
|
|
if (parts[2] === 'files' && parts[3]) {
|
|
return {
|
|
kind: 'project',
|
|
projectId,
|
|
fileName: decodeURIComponent(parts.slice(3).join('/')),
|
|
};
|
|
}
|
|
return { kind: 'project', projectId, fileName: null };
|
|
}
|
|
return { kind: 'home' };
|
|
}
|
|
|
|
export function buildPath(route: Route): string {
|
|
if (route.kind === 'home') return '/';
|
|
const id = encodeURIComponent(route.projectId);
|
|
if (route.fileName) {
|
|
const file = route.fileName
|
|
.split('/')
|
|
.map((s) => encodeURIComponent(s))
|
|
.join('/');
|
|
return `/projects/${id}/files/${file}`;
|
|
}
|
|
return `/projects/${id}`;
|
|
}
|
|
|
|
// Centralized navigation. Components call this instead of mutating
|
|
// `window.location` directly so we can fan the change out to any
|
|
// `useRoute()` subscriber via a custom event.
|
|
export function navigate(route: Route, opts: { replace?: boolean } = {}): void {
|
|
const target = buildPath(route);
|
|
const current = window.location.pathname;
|
|
if (target === current) return;
|
|
if (opts.replace) {
|
|
window.history.replaceState(null, '', target);
|
|
} else {
|
|
window.history.pushState(null, '', target);
|
|
}
|
|
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
}
|
|
|
|
export function useRoute(): Route {
|
|
const [route, setRoute] = useState<Route>(() => parseRoute(window.location.pathname));
|
|
useEffect(() => {
|
|
const onPop = () => setRoute(parseRoute(window.location.pathname));
|
|
window.addEventListener('popstate', onPop);
|
|
return () => window.removeEventListener('popstate', onPop);
|
|
}, []);
|
|
return route;
|
|
}
|