Skip to content

Commit 562e648

Browse files
Merge pull request #81 from IntersectMBO/feat/evolution-playground
feat: add playground
2 parents 9c8a391 + 7a494d5 commit 562e648

File tree

6 files changed

+277
-1
lines changed

6 files changed

+277
-1
lines changed

docs/app/playground/page.tsx

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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+
// Encode/decode code for URL sharing - handles Unicode properly with base64url
8+
const encodeCode = (code: string): string => {
9+
const bytes = new TextEncoder().encode(code)
10+
const binString = Array.from(bytes, (byte) => String.fromCodePoint(byte)).join("")
11+
return btoa(binString)
12+
.replace(/\+/g, "-")
13+
.replace(/\//g, "_")
14+
.replace(/=+$/, "")
15+
}
16+
17+
const decodeCode = (encoded: string): string | null => {
18+
try {
19+
const base64 = encoded
20+
.replace(/-/g, "+")
21+
.replace(/_/g, "/")
22+
const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, "=")
23+
const binString = atob(padded)
24+
const bytes = Uint8Array.from(binString, (char) => char.codePointAt(0)!)
25+
return new TextDecoder().decode(bytes)
26+
} catch {
27+
return null
28+
}
29+
}
30+
31+
const defaultCode = `import { Core } from "@evolution-sdk/evolution"
32+
33+
const bech32 = "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp"
34+
35+
// Parse from Bech32
36+
const address = Core.Address.fromBech32(bech32)
37+
38+
console.log("Address:", address)
39+
console.log("Network ID:", address.networkId)
40+
console.log("Payment credential:", address.paymentCredential)
41+
console.log("Has staking:", address.stakingCredential !== undefined)
42+
43+
// Check if it's an enterprise address
44+
console.log("Is enterprise:", Core.Address.isEnterprise(address))`
45+
46+
export default function PlaygroundPage() {
47+
const [code, setCode] = useState<string | undefined>()
48+
const [copied, setCopied] = useState(false)
49+
const [vmReady, setVmReady] = useState(false)
50+
const vmRef = useRef<VM | null>(null)
51+
52+
// Load code from URL on mount
53+
useEffect(() => {
54+
if (typeof window === 'undefined') return
55+
const params = new URLSearchParams(window.location.search)
56+
const encodedCode = params.get('code')
57+
if (encodedCode) {
58+
const decoded = decodeCode(encodedCode)
59+
if (decoded) {
60+
setCode(decoded)
61+
}
62+
}
63+
}, [])
64+
65+
const shareCode = async () => {
66+
if (typeof window === 'undefined') return
67+
68+
try {
69+
let currentCode = code || defaultCode
70+
71+
if (vmRef.current) {
72+
try {
73+
const files = await vmRef.current.getFsSnapshot()
74+
if (files?.['index.ts']) {
75+
currentCode = files['index.ts']
76+
}
77+
} catch (vmError) {
78+
console.warn('Could not read from VM:', vmError)
79+
}
80+
}
81+
82+
const encoded = encodeCode(currentCode)
83+
const url = new URL(window.location.href)
84+
url.searchParams.set('code', encoded)
85+
86+
window.history.pushState({}, '', url.toString())
87+
await navigator.clipboard.writeText(url.toString())
88+
89+
setCopied(true)
90+
setTimeout(() => setCopied(false), 2000)
91+
} catch (error) {
92+
console.error('Failed to share code:', error)
93+
}
94+
}
95+
96+
const handleVmReady = useCallback((vm: VM) => {
97+
vmRef.current = vm
98+
setVmReady(true)
99+
}, [])
100+
101+
return (
102+
<div className="h-screen flex flex-col">
103+
<header className="flex-shrink-0 p-4 border-b border-fd-border bg-fd-background">
104+
<div className="flex items-center justify-between">
105+
<div>
106+
<h1 className="text-2xl font-bold mb-1">Evolution SDK Playground</h1>
107+
<p className="text-sm text-muted-foreground">
108+
Full Node.js environment in your browser powered by StackBlitz WebContainers
109+
</p>
110+
<p className="text-xs text-blue-600 dark:text-blue-400 mt-1">
111+
💡 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>
112+
</p>
113+
</div>
114+
<button
115+
onClick={shareCode}
116+
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"
117+
title={vmReady ? "Share your current code" : "Loading playground..."}
118+
>
119+
{copied ? (
120+
<>
121+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
122+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
123+
</svg>
124+
Copied!
125+
</>
126+
) : (
127+
<>
128+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
129+
<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" />
130+
</svg>
131+
Share Code
132+
</>
133+
)}
134+
</button>
135+
</div>
136+
</header>
137+
<div className="flex-1 overflow-hidden">
138+
<StackBlitzPlayground
139+
key={code || 'default'}
140+
initialCode={code}
141+
onVmReady={handleVmReady}
142+
/>
143+
</div>
144+
</div>
145+
)
146+
}
147+
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)