Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 15 additions & 106 deletions src/app/api/components/all/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,130 +6,39 @@ import { componentExamples } from "@/lib/component-registry";
type ComponentData = {
name: string;
fileName: string;
files: Record<string, string>;
code: string;
exampleCode: string;
};

/**
* Extracts @/components/ui/ imports from a code string
*/
function extractUIImports(code: string): string[] {
const importRegex = /from\s+["']@\/components\/ui\/([^"']+)["']/g;
const matches = [...code.matchAll(importRegex)];
return matches.map((match) => match[1]);
}

/**
* Recursively loads component dependencies by parsing imports
*/
async function loadComponentWithDependencies(
componentFileName: string,
componentsDir: string,
loadedComponents = new Set<string>()
): Promise<Record<string, string>> {
// Prevent circular dependencies and duplicates
if (loadedComponents.has(componentFileName)) {
return {};
}

loadedComponents.add(componentFileName);

const files: Record<string, string> = {};
const filePath = join(componentsDir, componentFileName);

try {
const code = await readFile(filePath, "utf-8");

// Extract component name without extension for the key
const componentKey = componentFileName.replace(/\.tsx?$/, "");
files[componentKey] = code;

// Parse imports to find dependencies on @/components/ui/
const dependencies = extractUIImports(code);

// Load each dependency recursively
for (const dependencyName of dependencies) {
const dependencyFileName = `${dependencyName}.tsx`;

const dependencyFiles = await loadComponentWithDependencies(
dependencyFileName,
componentsDir,
loadedComponents
);

// Merge dependency files
Object.assign(files, dependencyFiles);
}

return files;
} catch (error) {
console.error(`Error loading component ${componentFileName}:`, error);
return files;
}
}

export async function GET() {
try {
const componentsDir = join(process.cwd(), "src", "components", "ui");
const files = await readdir(componentsDir);

// Get list of components
const componentsList = files
.filter((file) => file.endsWith(".tsx"))
.map((file) => ({
fileName: file,
name: file
.replace(".tsx", "")
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" "),
}))
.sort((a, b) => a.name.localeCompare(b.name));

// Load all components with their dependencies
const components: ComponentData[] = [];

for (const component of componentsList) {
const loadedComponents = new Set<string>();
const componentFiles = await loadComponentWithDependencies(
component.fileName,
componentsDir,
loadedComponents
);
for (const file of files) {
if (!file.endsWith(".tsx")) continue;

const componentKey = component.fileName.replace(".tsx", "");
const componentKey = file.replace(".tsx", "");
const filePath = join(componentsDir, file);
const code = await readFile(filePath, "utf-8");
const exampleCode = componentExamples[componentKey] || "";

// Extract dependencies from the example code
if (exampleCode) {
const exampleDependencies = extractUIImports(exampleCode);

// Load any dependencies from the example code that weren't already loaded
for (const dependencyName of exampleDependencies) {
const dependencyFileName = `${dependencyName}.tsx`;

// Only load if not already loaded
if (!loadedComponents.has(dependencyFileName)) {
const dependencyFiles = await loadComponentWithDependencies(
dependencyFileName,
componentsDir,
loadedComponents
);

// Merge dependency files
Object.assign(componentFiles, dependencyFiles);
}
}
}

components.push({
name: component.name,
fileName: component.fileName,
files: componentFiles,
name: file
.replace(".tsx", "")
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" "),
fileName: file,
code,
exampleCode,
});
}

components.sort((a, b) => a.name.localeCompare(b.name));

return NextResponse.json({ components });
} catch (error) {
console.error("Error loading all components:", error);
Expand Down
114 changes: 67 additions & 47 deletions src/components/component-viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
useSandpackNavigation,
} from "@codesandbox/sandpack-react";
import { useTheme } from "next-themes";
import { useEffect, useMemo } from "react";
import { useEffect, useLayoutEffect, useMemo, useState } from "react";
import { ALL_DEPENDENCIES } from "@/lib/component-registry";

function transformAbsoluteToRelativeImports(code: string): string {
Expand Down Expand Up @@ -290,8 +290,7 @@ const getPackageJSON = (dependencies: Record<string, string>) =>
type: "module",
scripts: {
dev: "vite",
build:
"npx tailwindcss -i ./index.css -o ./output.css && tsc && vite build",
build: "tsc && vite build",
preview: "vite preview",
},
dependencies: {
Expand Down Expand Up @@ -355,34 +354,29 @@ export function ComponentViewer() {
const { activeComponentName } = useActiveComponent();
const { data: allComponents = [] } = useComponents();
const { resolvedTheme } = useTheme();
const [initialFiles, setInitialFiles] = useState<
Record<string, { code: string; readOnly?: boolean }>
>({});

const activeComponent = useMemo(() => {
return allComponents.find(
(c) => c.fileName === activeComponentName
) as ComponentData;
}, [allComponents, activeComponentName]);

const files = useMemo(() => {
if (!activeComponent) return {};

const { fileName, exampleCode, files: componentFiles } = activeComponent;
const componentName = fileName.replace(".tsx", "");
const componentCode = componentFiles[componentName] || "";
// Setup all files once on mount
useEffect(() => {
if (allComponents.length === 0 || !activeComponent) return;

const transformedExampleCode =
transformAbsoluteToRelativeImports(exampleCode);
const transformedComponentCode =
transformAbsoluteToRelativeImports(componentCode);
const transformedExampleCode = transformAbsoluteToRelativeImports(
activeComponent.exampleCode
);

const setupFiles: Record<string, { code: string; readOnly?: boolean }> = {
"/App.tsx": {
code: transformedExampleCode,
readOnly: false,
},
[`/${componentName}.tsx`]: {
code: transformedComponentCode,
readOnly: false,
},
"/index.html": {
code: getIndexHTML(resolvedTheme === "dark"),
readOnly: false,
Expand All @@ -391,24 +385,29 @@ export function ComponentViewer() {
code: getPackageJSON(ALL_DEPENDENCIES),
readOnly: true,
},
"/vite.config.ts": {
code: getViteConfigTS(ALL_DEPENDENCIES),
readOnly: false,
},
...INITIAL_FILES,
};

for (const [fileName, code] of Object.entries(componentFiles)) {
if (fileName === componentName) continue;
for (const component of allComponents) {
const componentName = component.fileName.replace(".tsx", "");
const transformedCode = transformAbsoluteToRelativeImports(
component.code
);

const transformedCode = transformAbsoluteToRelativeImports(code);

setupFiles[`/${fileName}.tsx`] = {
setupFiles[`/${componentName}.tsx`] = {
code: transformedCode,
readOnly: false,
};
}

return setupFiles;
}, [activeComponent]);
setInitialFiles(setupFiles);
}, [allComponents]);

if (Object.keys(files).length === 0) {
if (Object.keys(initialFiles).length === 0) {
return (
<div className="flex h-full items-center justify-center">
<p className="text-muted-foreground">Loading component...</p>
Expand All @@ -419,7 +418,7 @@ export function ComponentViewer() {
return (
<SandboxProvider
template="vite-react-ts"
files={files}
files={initialFiles}
theme={resolvedTheme === "dark" ? "dark" : "light"}
options={{
initMode: "immediate",
Expand All @@ -433,7 +432,8 @@ export function ComponentViewer() {
</div>
<div className="flex-1">
<SandboxLayout>
<UpdateDarkMode files={files} />
<UpdateActiveComponent />
<UpdateDarkMode />
<SandboxCodeEditor
showTabs
showLineNumbers
Expand All @@ -450,13 +450,18 @@ export function ComponentViewer() {
<h3 className="text-sm font-medium text-foreground">Preview</h3>
</div>
<div className="flex-1">
<SandboxLayout>
<SandboxPreview
showOpenInCodeSandbox={false}
showRefreshButton
className="h-full!"
/>
</SandboxLayout>
<div className="flex flex-col h-full">
<SandboxLayout>
<SandboxPreview
showOpenInCodeSandbox={false}
showRefreshButton
className="h-full! p-8"
/>
</SandboxLayout>
<SandboxLayout>
<SandboxConsole className="h-64!" />
</SandboxLayout>
</div>
</div>
</div>
</div>
Expand All @@ -465,25 +470,40 @@ export function ComponentViewer() {
);
}

function UpdateDarkMode({
files,
}: {
files: Record<string, { code: string; readOnly?: boolean }>;
}) {
const { resolvedTheme } = useTheme();
function UpdateActiveComponent() {
const { activeComponentName } = useActiveComponent();
const { data: allComponents = [] } = useComponents();
const { sandpack } = useSandpack();
const { refresh } = useSandpackNavigation();

useEffect(() => {
const timeout = setTimeout(() => {
refresh();
}, 300);
console.log("running update active component hook");
const activeComponent = allComponents.find(
(c) => c.fileName === activeComponentName
);

return () => clearTimeout(timeout);
}, [files, refresh]);
if (!activeComponent) return;

const transformedExampleCode = transformAbsoluteToRelativeImports(
activeComponent.exampleCode
);

sandpack.updateFile("/App.tsx", transformedExampleCode, true);
}, [activeComponentName, allComponents]);

return null;
}

function UpdateDarkMode() {
const { resolvedTheme } = useTheme();
const { sandpack } = useSandpack();

useEffect(() => {
sandpack.updateFile("/index.html", getIndexHTML(resolvedTheme === "dark"));
console.log("running update dark mode hook");
sandpack.updateFile(
"/index.html",
getIndexHTML(resolvedTheme === "dark"),
true
);
}, [resolvedTheme]);

return null;
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/use-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useQuery } from "@tanstack/react-query";
export type ComponentData = {
name: string;
fileName: string;
files: Record<string, string>;
code: string;
exampleCode: string;
};

Expand Down