Skip to content

Commit

Permalink
Enhance documentation and add CodeSandbox integration
Browse files Browse the repository at this point in the history
- Added `codesandbox` dependency to package.json and package-lock.json.
- Introduced a new API route for creating CodeSandbox instances.
- Updated various components to utilize the new `ReactQueryProvider` for managing API calls.
- Refactored code to improve modularity, including the addition of new components like `LoadingSpinner`, `Editor`, and `Playground`.
- Updated `page.mdx` to include CodeSandbox buttons for examples.
- Removed unused `dedent` dependency from package.json and package-lock.json.
- Improved CORS headers in the Next.js configuration for API routes.
- Enhanced the layout and structure of documentation pages for better user experience.
  • Loading branch information
marklundin committed Dec 13, 2024
1 parent 38684bc commit 2671bb5
Show file tree
Hide file tree
Showing 22 changed files with 395 additions and 190 deletions.
17 changes: 3 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,8 @@
"globals": "^15.12.0",
"type-fest": "^4.26.1",
"typescript-eslint": "^8.12.2"
},
"dependencies": {
"codesandbox": "^2.2.3"
}
}
3 changes: 2 additions & 1 deletion packages/docs/mdx-components.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { Application, Entity } from '@playcanvas/react'
import { OrbitControls } from '@playcanvas/react/scripts'
import { Align, Light, Anim, Camera, Collision, EnvAtlas, GSplat, Script, Render, RigidBody } from '@playcanvas/react/components'

import ReactQueryProvider from '@/docs-components/ReactQueryProvider'

import EnvAtlasComponent from '@components/EnvAtlas'
import Grid from '@components/Grid'
import PostEffects, { StaticPostEffects} from '@components/PostEffects'
import ShadowCatcher from '@components/ShadowCatcher'
import AutoRotate from '@components/AutoRotate'
import ReactQueryProvider from '@/components/react-query-provider'
import { MotionEntity, MotionLight } from '@components/MotionEntity'
const docsComponents = getDocsMDXComponents()

Expand Down
16 changes: 15 additions & 1 deletion packages/docs/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,21 @@ const withNextra = nextra({

export default withNextra({
reactStrictMode: true,
transpilePackages: [/*'next-mdx-remote', */'next-mdx-remote-client', 'playcanvas'],
transpilePackages: ['next-mdx-remote-client', 'playcanvas'],
async headers() {
return [
{
// matching all API routes
source: "/",
headers: [
{ key: "Access-Control-Allow-Credentials", value: "true" },
{ key: "Access-Control-Allow-Origin", value: "*" }, // replace this your actual origin
{ key: "Access-Control-Allow-Methods", value: "GET,DELETE,PATCH,POST,PUT" },
{ key: "Access-Control-Allow-Headers", value: "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version" },
]
}
]
},
redirects: async () => [
{
source: '/playground',
Expand Down
1 change: 0 additions & 1 deletion packages/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
"@monaco-editor/react": "^4.6.0",
"@tanstack/react-query": "^5.61.5",
"codesandbox": "^2.2.3",
"dedent": "^1.5.3",
"leva": "^0.9.35",
"lucide-react": "^0.465.0",
"monaco-editor": "^0.52.0",
Expand Down
123 changes: 123 additions & 0 deletions packages/docs/src/app/api/codesandbox/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import fs from 'fs/promises';
import path from 'path';

const filePathRegex = /(["'])\/((?!\/)[^"']*\.(?:glb|png|jpg|jpeg|gif|svg|mp4|webm|ogg|mp3|wav))/g

// Function to recursively read files in a directory
async function readFiles(dir, relativeDir = '') {

const entries = await fs.readdir(dir, { withFileTypes: true });
const fileMap = {};

for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
const relativePath = path.join(relativeDir, entry.name);

if (entry.isDirectory()) {
// Recursively read directories
const nestedFiles = await readFiles(fullPath, path.join(relativeDir, entry.name));
Object.assign(fileMap, nestedFiles);
} else {
// Skip .DS_Store files
if (entry.name === '.DS_Store') {
continue;
}
// Read the file contents
if (entry.name.endsWith('.json')) {
// Read and parse JSON files
const jsonContent = await fs.readFile(fullPath, 'utf-8');
fileMap[relativePath] = { content: JSON.parse(jsonContent) };
} else {
// Read other files as text
const content = await fs.readFile(fullPath, 'utf-8');
fileMap[relativePath] = { content };
}
}
}
return fileMap;
}

export async function POST(request: Request) {
try {
// Get template and content from request body
const { template, content, entry } = await request.json();

let error = null;
if(!template) {
error = '`template` is required';
}

if(!entry && !content) {
error = '`entry` or `content` is required';
}

if(error) {
return new Response(JSON.stringify({ error }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}

// Base directory for the files
const baseDir = path.resolve(path.join(process.cwd(), 'src/templates', template));
const componentDir = path.resolve(path.join(process.cwd(), 'src/components/'));

// Get all files starting from the base directory
let files = await readFiles(baseDir);
const components = await readFiles(componentDir, 'src/components');

// Merge the files and components
files = { ...files, ...components };

files["/src/App.jsx"] = content
? { content } // If content is provided, use it
: { content : await fs.readFile(path.resolve(path.join(process.cwd(), entry)), 'utf-8') }; // If entry is provided, use it's content

// Replace the baseUrl in the content
const baseUrl = process.env.VERCEL_URL ?? 'https://playcanvas-react.vercel.app';
files["/src/App.jsx"].content = files["/src/App.jsx"].content.replace(
filePathRegex, `$1${baseUrl}/$2`)

files["/jsconfig.json"] = {
content: {
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@/components/*": ["src/components/*"],
}
}
}
};

// Call the CodeSandbox Define API
const response = await fetch("https://codesandbox.io/api/v1/sandboxes/define?json=1", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json"
},
body: JSON.stringify({ files })
});

if (!response.ok) {
throw new Error(`CodeSandbox API returned ${response.status}: ${await response.text()}`);
}

const data = await response.json();

if (!data.sandbox_id) {
throw new Error('Invalid response from CodeSandbox API');
}

return new Response(
`https://codesandbox.io/s/${data.sandbox_id}?file=%2Fsrc%2FApp.jsx`
);

} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
}
17 changes: 9 additions & 8 deletions packages/docs/src/app/docs/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,24 @@ title: Introduction
---

import { Cards } from 'nextra/components'
import { Icons } from '@/components/icons'
import { Terminal, CodesandboxIcon } from 'lucide-react'
import { HomePageExample } from '@/components/HomePageExample'
import { PCReactCodeSandBoxButton } from '@/utils/code-sandbox'
import { Icons } from '@/docs-components/Icons'
import { Terminal } from 'lucide-react'
import { OpenHomePageExampleInCodeSandbox } from '@/docs-components/utils/code-sandbox'

import Example from '@/templates/HomePageExample'

# `@playcanvas/react`

A react library for for creating 3D apps, with an extensible Entity-Component System supporting physics, interaction, and scripting.

import { Tabs, Code } from 'nextra/components'
import dedent from 'dedent';

<Tabs items={['Demo', 'Code']}>
<Tabs.Tab>
<div className='w-full aspect-video rounded-xl overflow-hidden'>
<ReactQueryProvider>
<Application className='hover:cursor-grab active:cursor-grabbing w-full' fillMode="NONE" resolutionMode="AUTO">
<HomePageExample />
<Example />
</Application>
</ReactQueryProvider>
</div>
Expand All @@ -30,16 +30,17 @@ import dedent from 'dedent';
</Tabs.Tab>
</Tabs>

<Cards>
<Cards num={2}>
<Cards.Card
icon={<Terminal />}
title="Open in Playground"
href="/playground/model-viewer"
arrow
/>
<OpenHomePageExampleInCodeSandbox />
</Cards>

<PCReactCodeSandBoxButton name="Basic Demo" examplePath="packages/docs/src/components/HomePageExample.tsx" />


## How does it work?

Expand Down
2 changes: 1 addition & 1 deletion packages/docs/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Banner, Head } from 'nextra/components'
import { getPageMap } from 'nextra/page-map'
import './globals.css'
import { CodeXml } from 'lucide-react'
import ReactQueryProvider from '@/components/react-query-provider'
import ReactQueryProvider from '@/docs-components/ReactQueryProvider'

export const { viewport } = Head

Expand Down
9 changes: 4 additions & 5 deletions packages/docs/src/app/playground/[[...mdxPath]]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { generateStaticParamsFor, importPage } from 'nextra/pages'
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import Playground from '@/components/playground'
// import { useLayoutEffect } from 'react'
import Playground from '@/docs-components/Playground'

export const generateStaticParams = generateStaticParamsFor('mdxPath')

Expand All @@ -13,11 +12,11 @@ export async function generateMetadata(props: PageProps) {
// Try to import the meta file
const { default: metadata } = await import(`@/content/${params.mdxPath}.meta.tsx`)
return metadata;
} catch (error) {
} catch {
// If the meta file is not found, use the default metadata
const { metadata } = await importPage(params.mdxPath)
return metadata
}
return metadata
}
}

interface PageProps {
Expand Down
25 changes: 25 additions & 0 deletions packages/docs/src/components/LoadingSpinner.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Entity } from '@playcanvas/react'
import { Script, Render, Camera, Light } from '@playcanvas/react/components'
import { Script as PcScript } from 'playcanvas'

class Spin extends PcScript {
update(dt) {
this.entity.rotate(0, this.speed * dt, 0)
}
}

/**
* A simple spinning cube used as a loading indicator.
*/
export const LoadingSpinner = () => (<>
<Entity name='light' >
<Light type='directional' color="orange" />
</Entity>
<Entity name="camera" position={[0, 0, 50]}>
<Camera />
</Entity>
<Entity name="loading">
<Render type="box"/>
<Script script={Spin} speed={10}/>
</Entity>
</>)
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import ActualMonacoEditor, { editor } from "monaco-editor";
import * as motion from 'motion/react';
// Required by MDX
import * as pc from 'playcanvas';
import { useModel, useSplat, useTexture } from "./hooks/use-asset";
import { useModel, useSplat, useTexture } from "../components/hooks/use-asset";
import { useApp, useMaterial, useParent } from "@playcanvas/react/hooks";

interface EditorContextType {
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { FC, useEffect, useRef, useState } from "react";

import { Editor, EditorProvider, Preview } from './editor';
import { Editor, EditorProvider, Preview } from './Editor';
import { Suspense } from 'react';
import { Leva } from "leva";
import { FILLMODE_NONE, RESOLUTION_AUTO } from "playcanvas";
Expand Down
Loading

0 comments on commit 2671bb5

Please sign in to comment.