dragonflight/services/editor/apps/web/src/hooks/use-router.ts
Zac b68f0c6aba feat(editor): integrate openreel-video as services/editor with MAM hooks
Vendored Augani/openreel-video (MIT) into services/editor and wired it to the MAM. Editor runs as its own container on port 47435. Library assets pull in via ?asset=<uuid>; render exports route back via POST /api/v1/upload/simple. Sidebar Editor link on every page; Edit button on every preview modal. See services/editor/INTEGRATION.md for the patch map.
2026-05-17 21:44:37 -04:00

185 lines
4.2 KiB
TypeScript

import { useState, useEffect, useCallback, useMemo } from "react";
export type AppRoute =
| "welcome"
| "editor"
| "new"
| "templates"
| "recent"
| "share";
export interface RouteParams {
dimensions?: string;
preset?: string;
width?: string;
height?: string;
fps?: string;
tab?: string;
shareId?: string;
}
export interface RouterState {
route: AppRoute;
params: RouteParams;
}
function parseHash(hash: string): RouterState {
const cleanHash = hash.replace(/^#\/?/, "");
const [path, queryString] = cleanHash.split("?");
const params: RouteParams = {};
if (queryString) {
const searchParams = new URLSearchParams(queryString);
searchParams.forEach((value, key) => {
params[key as keyof RouteParams] = value;
});
}
const pathParts = path.split("/");
let route: AppRoute = (pathParts[0] || "welcome") as AppRoute;
const validRoutes: AppRoute[] = [
"welcome",
"editor",
"new",
"templates",
"recent",
"share",
];
if (route === "share" && pathParts[1]) {
params.shareId = pathParts[1];
}
return {
route: validRoutes.includes(route) ? route : "welcome",
params,
};
}
function buildHash(route: AppRoute, params?: RouteParams): string {
let hash = `#/${route}`;
if (params && Object.keys(params).length > 0) {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
searchParams.set(key, String(value));
}
});
const queryString = searchParams.toString();
if (queryString) {
hash += `?${queryString}`;
}
}
return hash;
}
export function useRouter() {
const [state, setState] = useState<RouterState>(() => {
if (typeof window !== "undefined") {
return parseHash(window.location.hash);
}
return { route: "welcome", params: {} };
});
useEffect(() => {
const handleHashChange = () => {
setState(parseHash(window.location.hash));
};
window.addEventListener("hashchange", handleHashChange);
return () => window.removeEventListener("hashchange", handleHashChange);
}, []);
const navigate = useCallback((route: AppRoute, params?: RouteParams) => {
const hash = buildHash(route, params);
window.location.hash = hash;
}, []);
const updateParams = useCallback(
(newParams: Partial<RouteParams>) => {
const hash = buildHash(state.route, { ...state.params, ...newParams });
window.location.hash = hash;
},
[state.route, state.params],
);
const clearParams = useCallback(() => {
const hash = buildHash(state.route);
window.location.hash = hash;
}, [state.route]);
const parsedDimensions = useMemo(() => {
const { dimensions, width, height } = state.params;
if (dimensions) {
const match = dimensions.match(/^(\d+)x(\d+)$/i);
if (match) {
return {
width: parseInt(match[1], 10),
height: parseInt(match[2], 10),
};
}
}
if (width && height) {
return { width: parseInt(width, 10), height: parseInt(height, 10) };
}
return null;
}, [state.params]);
const fps = useMemo(() => {
const { fps } = state.params;
if (fps) {
const parsed = parseInt(fps, 10);
if (!isNaN(parsed) && parsed > 0 && parsed <= 120) {
return parsed;
}
}
return 30;
}, [state.params]);
return {
route: state.route,
params: state.params,
navigate,
updateParams,
clearParams,
parsedDimensions,
fps,
};
}
export function generateShareableLink(
route: AppRoute,
params?: RouteParams,
): string {
const baseUrl =
typeof window !== "undefined"
? `${window.location.origin}${window.location.pathname}`
: "";
return `${baseUrl}${buildHash(route, params)}`;
}
export function generateNewProjectLink(options: {
width?: number;
height?: number;
preset?: string;
fps?: number;
}): string {
const params: RouteParams = {};
if (options.preset) {
params.preset = options.preset;
} else if (options.width && options.height) {
params.dimensions = `${options.width}x${options.height}`;
}
if (options.fps && options.fps !== 30) {
params.fps = String(options.fps);
}
return generateShareableLink("new", params);
}