Skip to content

Commit e9235ac

Browse files
committed
feat: add playground
1 parent 9c8a391 commit e9235ac

File tree

6 files changed

+262
-1
lines changed

6 files changed

+262
-1
lines changed

docs/app/playground/page.tsx

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
"use client"
2+
3+
import { useCallback, useEffect, useRef, useState } from "react"
4+
import type { VM } from "@stackblitz/sdk"
5+
import { StackBlitzPlayground } from "../../components/playground/StackBlitzPlayground"
6+
7+
const encodeCode = (code: string) => btoa(encodeURIComponent(code))
8+
const decodeCode = (encoded: string) => {
9+
try {
10+
return decodeURIComponent(atob(encoded))
11+
} catch {
12+
return null
13+
}
14+
}
15+
16+
const defaultCode = `import { Core } from "@evolution-sdk/evolution"
17+
18+
const bech32 = "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp"
19+
20+
// Parse from Bech32
21+
const address = Core.Address.fromBech32(bech32)
22+
23+
console.log("Address:", address)
24+
console.log("Network ID:", address.networkId)
25+
console.log("Payment credential:", address.paymentCredential)
26+
console.log("Has staking:", address.stakingCredential !== undefined)
27+
28+
// Check if it's an enterprise address
29+
console.log("Is enterprise:", Core.Address.isEnterprise(address))`
30+
31+
export default function PlaygroundPage() {
32+
const [code, setCode] = useState<string | undefined>()
33+
const [copied, setCopied] = useState(false)
34+
const [vmReady, setVmReady] = useState(false)
35+
const vmRef = useRef<VM | null>(null)
36+
37+
// Load code from URL on mount
38+
useEffect(() => {
39+
if (typeof window === 'undefined') return
40+
const params = new URLSearchParams(window.location.search)
41+
const encodedCode = params.get('code')
42+
if (encodedCode) {
43+
const decoded = decodeCode(encodedCode)
44+
if (decoded) {
45+
setCode(decoded)
46+
}
47+
}
48+
}, [])
49+
50+
const shareCode = async () => {
51+
if (typeof window === 'undefined') return
52+
53+
try {
54+
let currentCode = code || defaultCode
55+
56+
if (vmRef.current) {
57+
try {
58+
const files = await vmRef.current.getFsSnapshot()
59+
if (files?.['index.ts']) {
60+
currentCode = files['index.ts']
61+
}
62+
} catch (vmError) {
63+
console.warn('Could not read from VM:', vmError)
64+
}
65+
}
66+
67+
const encoded = encodeCode(currentCode)
68+
const url = new URL(window.location.href)
69+
url.searchParams.set('code', encoded)
70+
71+
window.history.pushState({}, '', url.toString())
72+
await navigator.clipboard.writeText(url.toString())
73+
74+
setCopied(true)
75+
setTimeout(() => setCopied(false), 2000)
76+
} catch (error) {
77+
console.error('Failed to share code:', error)
78+
}
79+
}
80+
81+
const handleVmReady = useCallback((vm: VM) => {
82+
vmRef.current = vm
83+
setVmReady(true)
84+
}, [])
85+
86+
return (
87+
<div className="h-screen flex flex-col">
88+
<header className="flex-shrink-0 p-4 border-b border-fd-border bg-fd-background">
89+
<div className="flex items-center justify-between">
90+
<div>
91+
<h1 className="text-2xl font-bold mb-1">Evolution SDK Playground</h1>
92+
<p className="text-sm text-muted-foreground">
93+
Full Node.js environment in your browser powered by StackBlitz WebContainers
94+
</p>
95+
<p className="text-xs text-blue-600 dark:text-blue-400 mt-1">
96+
💡 Save changes: <code className="bg-fd-muted px-1 rounded font-mono">Cmd+S</code> / <code className="bg-fd-muted px-1 rounded font-mono">Ctrl+S</code>, then run: <code className="bg-fd-muted px-1 rounded font-mono">npm start</code>
97+
</p>
98+
</div>
99+
<button
100+
onClick={shareCode}
101+
className="inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium h-9 px-6 py-2 bg-zinc-700 text-white hover:bg-zinc-600 active:bg-zinc-500 transition-all cursor-pointer shadow-sm hover:shadow"
102+
title={vmReady ? "Share your current code" : "Loading playground..."}
103+
>
104+
{copied ? (
105+
<>
106+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
107+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
108+
</svg>
109+
Copied!
110+
</>
111+
) : (
112+
<>
113+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
114+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" />
115+
</svg>
116+
Share Code
117+
</>
118+
)}
119+
</button>
120+
</div>
121+
</header>
122+
<div className="flex-1 overflow-hidden">
123+
<StackBlitzPlayground
124+
key={code || 'default'}
125+
initialCode={code}
126+
onVmReady={handleVmReady}
127+
/>
128+
</div>
129+
</div>
130+
)
131+
}
132+
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"use client"
2+
3+
import { useEffect, useRef, useState } from "react"
4+
import sdk, { type VM } from "@stackblitz/sdk"
5+
6+
export interface StackBlitzPlaygroundProps {
7+
initialCode?: string
8+
onVmReady?: (vm: VM) => void
9+
}
10+
11+
const defaultCode = `import { Core } from "@evolution-sdk/evolution"
12+
13+
const bech32 = "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp"
14+
15+
// Parse from Bech32
16+
const address = Core.Address.fromBech32(bech32)
17+
18+
console.log("Address:", address)
19+
console.log("Network ID:", address.networkId)
20+
console.log("Payment credential:", address.paymentCredential)
21+
console.log("Has staking:", address.stakingCredential !== undefined)
22+
23+
// Check if it's an enterprise address
24+
console.log("Is enterprise:", Core.Address.isEnterprise(address))
25+
`
26+
27+
export function StackBlitzPlayground({ initialCode = defaultCode, onVmReady }: StackBlitzPlaygroundProps) {
28+
const containerRef = useRef<HTMLDivElement>(null)
29+
const vmRef = useRef<VM | null>(null)
30+
const [isLoading, setIsLoading] = useState(true)
31+
const hasEmbeddedRef = useRef(false)
32+
33+
useEffect(() => {
34+
if (!containerRef.current || hasEmbeddedRef.current) return
35+
36+
hasEmbeddedRef.current = true
37+
setIsLoading(true)
38+
39+
sdk
40+
.embedProject(
41+
containerRef.current,
42+
{
43+
title: "Evolution SDK Playground",
44+
description: "Interactive TypeScript playground for Evolution SDK",
45+
template: "node",
46+
files: {
47+
"index.ts": initialCode,
48+
"package.json": JSON.stringify(
49+
{
50+
name: "evolution-sdk-playground",
51+
version: "1.0.0",
52+
description: "Evolution SDK Playground",
53+
type: "module",
54+
main: "index.ts",
55+
scripts: {
56+
start: "tsx index.ts"
57+
},
58+
dependencies: {
59+
"@evolution-sdk/evolution": "latest",
60+
effect: "latest"
61+
},
62+
devDependencies: {
63+
"@types/node": "latest",
64+
tsx: "latest",
65+
typescript: "latest"
66+
}
67+
},
68+
null,
69+
2
70+
),
71+
"tsconfig.json": JSON.stringify(
72+
{
73+
compilerOptions: {
74+
target: "ES2022",
75+
module: "ESNext",
76+
moduleResolution: "bundler",
77+
lib: ["ES2022"],
78+
strict: true,
79+
esModuleInterop: true,
80+
skipLibCheck: true,
81+
forceConsistentCasingInFileNames: true,
82+
resolveJsonModule: true,
83+
isolatedModules: true
84+
}
85+
},
86+
null,
87+
2
88+
)
89+
}
90+
},
91+
{
92+
openFile: "index.ts",
93+
view: "editor",
94+
theme: "dark",
95+
hideExplorer: true
96+
}
97+
)
98+
.then((vm) => {
99+
vmRef.current = vm
100+
onVmReady?.(vm)
101+
setIsLoading(false)
102+
})
103+
.catch((error) => {
104+
console.error("Failed to load StackBlitz:", error)
105+
setIsLoading(false)
106+
})
107+
}, [])
108+
109+
return (
110+
<div className="relative w-full h-full">
111+
{isLoading && (
112+
<div className="absolute inset-0 flex items-center justify-center bg-fd-background">
113+
<div className="text-fd-muted-foreground">Loading playground...</div>
114+
</div>
115+
)}
116+
<div ref={containerRef} className="w-full h-full" />
117+
</div>
118+
)
119+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { StackBlitzPlayground } from "./StackBlitzPlayground"

docs/next-env.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// <reference types="next" />
22
/// <reference types="next/image-types/global" />
3-
import "./.next/types/routes.d.ts";
3+
import "./out/dev/types/routes.d.ts";
44

55
// NOTE: This file should not be edited
66
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

docs/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"@evolution-sdk/devnet": "workspace:*",
1616
"@evolution-sdk/evolution": "workspace:*",
1717
"@orama/orama": "^3.1.11",
18+
"@stackblitz/sdk": "^1.11.0",
1819
"fumadocs-core": "16.0.8",
1920
"fumadocs-mdx": "13.0.5",
2021
"fumadocs-twoslash": "^3.1.10",

pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)