147 lines
3.6 KiB
TypeScript
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;
|