File size: 3,654 Bytes
5ef6e9d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | 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;
|