2026-03-27 02:46:26 +00:00

147 lines
3.6 KiB
TypeScript

import { useEffect, useState, type ComponentType } from "react";
import { modules as discoveredModules } from "./.generated/mockup-components";
type ModuleMap = Record<string, () => Promise<Record<string, unknown>>>;
function _resolveComponent(
mod: Record<string, unknown>,
name: string,
): ComponentType | undefined {
const fns = Object.values(mod).filter(
(v) => typeof v === "function",
) as ComponentType[];
return (
(mod.default as ComponentType) ||
(mod.Preview as ComponentType) ||
(mod[name] as ComponentType) ||
fns[fns.length - 1]
);
}
function PreviewRenderer({
componentPath,
modules,
}: {
componentPath: string;
modules: ModuleMap;
}) {
const [Component, setComponent] = useState<ComponentType | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
setComponent(null);
setError(null);
async function loadComponent(): Promise<void> {
const key = `./components/mockups/${componentPath}.tsx`;
const loader = modules[key];
if (!loader) {
setError(`No component found at ${componentPath}.tsx`);
return;
}
try {
const mod = await loader();
if (cancelled) {
return;
}
const name = componentPath.split("/").pop()!;
const comp = _resolveComponent(mod, name);
if (!comp) {
setError(
`No exported React component found in ${componentPath}.tsx\n\nMake sure the file has at least one exported function component.`,
);
return;
}
setComponent(() => comp);
} catch (e) {
if (cancelled) {
return;
}
const message = e instanceof Error ? e.message : String(e);
setError(`Failed to load preview.\n${message}`);
}
}
void loadComponent();
return () => {
cancelled = true;
};
}, [componentPath, modules]);
if (error) {
return (
<pre style={{ color: "red", padding: "2rem", fontFamily: "system-ui" }}>
{error}
</pre>
);
}
if (!Component) return null;
return <Component />;
}
function getBasePath(): string {
return import.meta.env.BASE_URL.replace(/\/$/, "");
}
function getPreviewExamplePath(): string {
const basePath = getBasePath();
return `${basePath}/preview/ComponentName`;
}
function Gallery() {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-8">
<div className="text-center max-w-md">
<h1 className="text-2xl font-semibold text-gray-900 mb-3">
Component Preview Server
</h1>
<p className="text-gray-500 mb-4">
This server renders individual components for the workspace canvas.
</p>
<p className="text-sm text-gray-400">
Access component previews at{" "}
<code className="bg-gray-100 px-1.5 py-0.5 rounded text-gray-600">
{getPreviewExamplePath()}
</code>
</p>
</div>
</div>
);
}
function getPreviewPath(): string | null {
const basePath = getBasePath();
const { pathname } = window.location;
const local =
basePath && pathname.startsWith(basePath)
? pathname.slice(basePath.length) || "/"
: pathname;
const match = local.match(/^\/preview\/(.+)$/);
return match ? match[1] : null;
}
function App() {
const previewPath = getPreviewPath();
if (previewPath) {
return (
<PreviewRenderer
componentPath={previewPath}
modules={discoveredModules}
/>
);
}
return <Gallery />;
}
export default App;