Skip to content

Commit 5864a9c

Browse files
committed
Setup sample projects for the web component test page
It's handle to be able to quickly try out different projects, and later when we introduce scratch it would be useful to be able to get a scratch project from the test page without a dependnecy on another app. I've set up json files for the projects and used AI to generate interesting multi-file html and python projects
1 parent d374d62 commit 5864a9c

6 files changed

Lines changed: 207 additions & 37 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"identifier": "blank-html-starter",
3+
"project_type": "html",
4+
"locale": "en",
5+
"name": "Blank HTML & CSS Starter",
6+
"user_id": null,
7+
"instructions": null,
8+
"components": [
9+
{
10+
"id": "42b4cdc9-d935-4a3d-b39a-32eb44c5ebfd",
11+
"name": "index",
12+
"extension": "html",
13+
"content": ""
14+
},
15+
{
16+
"id": "3b6e4869-5873-4782-8764-7bf844578a22",
17+
"name": "styles",
18+
"extension": "css",
19+
"content": ""
20+
}
21+
],
22+
"image_list": [],
23+
"videos": [],
24+
"audio": []
25+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"identifier": "blank-python-starter",
3+
"project_type": "python",
4+
"locale": "en",
5+
"name": "Blank Python Starter",
6+
"user_id": null,
7+
"instructions": null,
8+
"components": [
9+
{
10+
"id": "42b4cdc9-d935-4a3d-b39a-32eb44c5ebfe",
11+
"name": "main",
12+
"extension": "py",
13+
"content": ""
14+
}
15+
],
16+
"image_list": [],
17+
"videos": [],
18+
"audio": []
19+
}

src/projects/cool-html.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"identifier": "cool-html",
3+
"project_type": "html",
4+
"locale": "en",
5+
"name": "Cool HTML: Spotlight Gradient",
6+
"user_id": null,
7+
"instructions": {
8+
"content": "## Cool HTML: Spotlight Gradient\n\nA tiny project with **one** cool trick: an animated gradient background with a mouse/touch **spotlight** that follows you.\n\n### Controls\n- Move cursor / drag: move the spotlight\n- Click: cycle color palettes\n\nFiles: `index.html`, `styles.css`, `script.js`\n",
9+
"permitOverride": false
10+
},
11+
"components": [
12+
{
13+
"id": "1c7ab1a8-5f09-4b3e-9d0d-6c8c2f2d2a8a",
14+
"name": "index",
15+
"extension": "html",
16+
"content": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>Spotlight Gradient</title>\n <link rel=\"stylesheet\" href=\"styles.css\" />\n </head>\n <body>\n <main class=\"wrap\">\n <h1>Spotlight Gradient</h1>\n <p>Move your mouse. Click to change colors.</p>\n <button id=\"next\" type=\"button\">Next palette</button>\n </main>\n\n <script src=\"script.js\"></script>\n </body>\n</html>\n"
17+
},
18+
{
19+
"id": "d9d292e1-20c8-4e55-bad8-2b2c7a4b5f9b",
20+
"name": "styles",
21+
"extension": "css",
22+
"content": ":root {\n --a: #7c3aed;\n --b: #22d3ee;\n --c: #a3ff12;\n --x: 50%;\n --y: 50%;\n}\n\n* {\n box-sizing: border-box;\n}\n\nhtml,\nbody {\n height: 100%;\n}\n\nbody {\n margin: 0;\n color: rgba(255, 255, 255, 0.92);\n font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;\n\n /* Animated gradient layer */\n background: radial-gradient(900px 600px at 20% 20%, rgba(255, 255, 255, 0.07), transparent 60%),\n conic-gradient(from 180deg, var(--a), var(--b), var(--c), var(--a));\n background-size: 100% 100%, 140% 140%;\n animation: drift 10s ease-in-out infinite;\n\n /* Spotlight (follows pointer via CSS vars) */\n position: relative;\n overflow: hidden;\n}\n\nbody::before {\n content: \"\";\n position: absolute;\n inset: -30%;\n pointer-events: none;\n background: radial-gradient(300px 300px at var(--x) var(--y), rgba(255, 255, 255, 0.22), transparent 60%);\n mix-blend-mode: screen;\n filter: blur(6px);\n}\n\nbody::after {\n content: \"\";\n position: absolute;\n inset: 0;\n pointer-events: none;\n background: radial-gradient(800px 500px at var(--x) var(--y), rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.72));\n}\n\n@keyframes drift {\n 0% {\n background-position: 0% 0%, 30% 50%;\n }\n 50% {\n background-position: 0% 0%, 70% 40%;\n }\n 100% {\n background-position: 0% 0%, 30% 50%;\n }\n}\n\n.wrap {\n position: relative;\n z-index: 1;\n height: 100%;\n display: grid;\n place-content: center;\n gap: 10px;\n padding: 24px;\n text-align: center;\n}\n\nh1 {\n margin: 0;\n font-size: clamp(30px, 5vw, 54px);\n letter-spacing: -0.03em;\n}\n\np {\n margin: 0;\n color: rgba(255, 255, 255, 0.70);\n}\n\nbutton {\n justify-self: center;\n padding: 10px 14px;\n border-radius: 12px;\n border: 1px solid rgba(255, 255, 255, 0.22);\n background: rgba(0, 0, 0, 0.28);\n color: rgba(255, 255, 255, 0.92);\n cursor: pointer;\n}\n\n@media (prefers-reduced-motion: reduce) {\n body {\n animation: none;\n }\n}\n"
23+
},
24+
{
25+
"id": "a4a5c67c-4bdb-4c9d-94de-0d4b15b093f2",
26+
"name": "script",
27+
"extension": "js",
28+
"content": "(() => {\n const root = document.documentElement;\n const palettes = [\n ['#7c3aed', '#22d3ee', '#a3ff12'],\n ['#fb7185', '#38bdf8', '#fbbf24'],\n ['#34d399', '#60a5fa', '#a78bfa'],\n ];\n let idx = 0;\n\n function setPalette(i) {\n const [a, b, c] = palettes[i % palettes.length];\n root.style.setProperty('--a', a);\n root.style.setProperty('--b', b);\n root.style.setProperty('--c', c);\n }\n\n function setSpotlight(clientX, clientY) {\n root.style.setProperty('--x', `${(clientX / innerWidth) * 100}%`);\n root.style.setProperty('--y', `${(clientY / innerHeight) * 100}%`);\n }\n\n addEventListener('mousemove', (e) => setSpotlight(e.clientX, e.clientY));\n addEventListener(\n 'touchmove',\n (e) => {\n const t = e.touches && e.touches[0];\n if (t) setSpotlight(t.clientX, t.clientY);\n },\n { passive: true },\n );\n\n function next() {\n idx = (idx + 1) % palettes.length;\n setPalette(idx);\n }\n\n document.getElementById('next')?.addEventListener('click', next);\n addEventListener('click', (e) => {\n if (e.target?.id !== 'next') next();\n });\n\n setPalette(idx);\n setSpotlight(innerWidth * 0.5, innerHeight * 0.5);\n})();\n"
29+
}
30+
],
31+
"image_list": [],
32+
"videos": [],
33+
"audio": []
34+
}

src/projects/cool-python.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"identifier": "cool-python",
3+
"project_type": "python",
4+
"locale": "en",
5+
"name": "Cool Python: ASCII Orb",
6+
"user_id": null,
7+
"instructions": {
8+
"content": "## Cool Python: ASCII Orb\n\nA minimal project that prints a single shaded **ASCII orb** (a circle with simple lighting).\n\n### Try this\n- Change `WIDTH` / `HEIGHT` in `main.py`\n- Change the `light_dx` / `light_dy` values to move the highlight\n- Swap the `SHADES` string in `ascii_orb.py` to change the look\n\nFiles:\n- `main.py` (entry point)\n- `ascii_orb.py` (helper that renders the orb)\n",
9+
"permitOverride": false
10+
},
11+
"components": [
12+
{
13+
"id": "9b5a24a8-7d15-4a34-8a59-7b0a1f1b19b2",
14+
"name": "main",
15+
"extension": "py",
16+
"content": "from ascii_orb import render_orb\n\n\ndef main():\n # Keep this reasonably small so it fits nicely in the output panel\n WIDTH = 48\n HEIGHT = 24\n\n art = render_orb(width=WIDTH, height=HEIGHT, light_dx=-0.35, light_dy=-0.25)\n print(art)\n\n\nif __name__ == \"__main__\":\n main()\n"
17+
},
18+
{
19+
"id": "5e0a8f3d-3c54-44f9-9fd9-75d41c1bb447",
20+
"name": "ascii_orb",
21+
"extension": "py",
22+
"content": "import math\n\n\n# Dark -> light. Try: \" .:-=+*#%@\"\nSHADES = \" .:-=+*#%@\"\n\n\ndef render_orb(width=48, height=24, light_dx=-0.35, light_dy=-0.25):\n \"\"\"Render a shaded ASCII circle (an \"orb\") as a multiline string.\n\n light_dx/light_dy shift the highlight direction (-1..1 range feels good).\n \"\"\"\n\n w = int(width)\n h = int(height)\n cx = (w - 1) / 2.0\n cy = (h - 1) / 2.0\n\n # Keep the circle round-ish in terminal characters\n aspect = 2.0\n r = min(cx, cy) * 0.95\n\n lines = []\n for y in range(h):\n row = []\n for x in range(w):\n nx = (x - cx) / r\n ny = ((y - cy) / r) * aspect\n d2 = nx * nx + ny * ny\n\n if d2 > 1.0:\n row.append(' ')\n continue\n\n # Simple fake lighting: brighter near the light direction\n # and darker toward the edges (a bit of \"rim\" shading).\n edge = math.sqrt(max(0.0, 1.0 - d2))\n light = 0.6 + 0.4 * (-(nx * light_dx + ny * light_dy))\n value = (0.65 * edge + 0.35 * light)\n\n idx = int(value * (len(SHADES) - 1))\n idx = max(0, min(len(SHADES) - 1, idx))\n row.append(SHADES[idx])\n\n lines.append(''.join(row).rstrip())\n\n return '\\n'.join(lines)\n"
23+
}
24+
],
25+
"image_list": [],
26+
"videos": [],
27+
"audio": []
28+
}

src/web-component.html

Lines changed: 88 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,28 @@
1515
block-size: 100dvh;
1616
margin: 0;
1717
}
18+
#editor-component {
19+
flex: 1 1 auto;
20+
display: flex;
21+
min-block-size: 0;
22+
}
23+
.sample-projects-bar {
24+
flex: 0 0 auto;
25+
display: flex;
26+
align-items: center;
27+
gap: 12px;
28+
padding: 10px 12px;
29+
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
30+
background: #fff;
31+
color: #111;
32+
font-size: 14px;
33+
font-family: sans-serif;
34+
line-height: 1.4;
35+
}
36+
.sample-projects-bar a {
37+
color: #005fb8;
38+
text-decoration: underline;
39+
}
1840
.editor-wc {
1941
flex: 1 1 auto;
2042
display: flex;
@@ -35,38 +57,24 @@
3557
/>
3658
</head>
3759
<body>
60+
<div class="sample-projects-bar" id="sample-projects-bar">
61+
<span>Load Sample Projects:</span>
62+
<a href="#" data-project="blank-html-starter">blank-html-starter</a>
63+
<a href="#" data-project="cool-html">cool-html</a>
64+
<a href="#" data-project="blank-python-starter">blank-python-starter</a>
65+
<a href="#" data-project="cool-python">cool-python</a>
66+
</div>
67+
<div id="editor-component"></div>
3868
<p id="results"></p>
3969
<p id="project-identifier"></p>
70+
<button id="run-button">Run code</button>
4071
</body>
4172

73+
4274
<script>
4375
document.addEventListener("DOMContentLoaded", (e) => {
44-
const webComp = document.createElement("editor-wc");
4576
const queryParams = new URLSearchParams(window.location.search);
46-
47-
// sidebar
48-
webComp.setAttribute("with_projectbar", "true");
49-
webComp.setAttribute("with_sidebar", "true");
50-
webComp.setAttribute(
51-
"sidebar_options",
52-
JSON.stringify([
53-
"instructions",
54-
"file",
55-
"images",
56-
"download",
57-
"settings",
58-
"info",
59-
])
60-
);
61-
62-
// Pre-set the code attribute with an empty string.
63-
webComp.setAttribute("code", "");
64-
webComp.setAttribute("class", "editor-wc");
65-
webComp.setAttribute("host_styles", JSON.stringify([]));
66-
// Set any attribute you like in the query string, including class, style, hidden, script, etc.
67-
queryParams.forEach((value, key) => {
68-
webComp.setAttribute(key, value);
69-
});
77+
let webComp;
7078

7179
// subscribe to the 'codeChanged' custom event which is pushed by the project react component
7280
document.addEventListener("editor-codeChanged", function (e) {
@@ -76,7 +84,6 @@
7684
});
7785

7886
document.addEventListener("editor-runCompleted", (e) => {
79-
// const error = webComp.isErrorFree
8087
console.log(e.detail);
8188
document.getElementById("results").innerText = JSON.stringify(e.detail);
8289
});
@@ -85,16 +92,64 @@
8592
document.getElementById("project-identifier").innerText = e.detail;
8693
});
8794

88-
const body = document.getElementsByTagName("body")[0];
89-
body.prepend(webComp);
95+
const sampleBar = document.getElementById("sample-projects-bar");
96+
const editorContainer = document.getElementById("editor-component");
97+
98+
const createWebComponent = (initialProject = null) => {
99+
const newWebComp = document.createElement("editor-wc");
100+
101+
newWebComp.setAttribute("with_projectbar", "true");
102+
newWebComp.setAttribute("with_sidebar", "true");
103+
newWebComp.setAttribute(
104+
"sidebar_options",
105+
JSON.stringify([
106+
"instructions",
107+
"file",
108+
"images",
109+
"download",
110+
"settings",
111+
"info",
112+
])
113+
);
114+
115+
newWebComp.setAttribute("code", "");
116+
newWebComp.setAttribute("class", "editor-wc");
117+
newWebComp.setAttribute("host_styles", JSON.stringify([]));
118+
119+
queryParams.forEach((value, key) => {
120+
newWebComp.setAttribute(key, value);
121+
});
122+
123+
if (initialProject) {
124+
newWebComp.setAttribute("initial_project", initialProject);
125+
}
126+
127+
return newWebComp;
128+
};
129+
130+
webComp = createWebComponent();
131+
editorContainer.prepend(webComp);
132+
133+
const loadSampleProjectByName = async (projectName) => {
134+
const url = new URL(`projects/${projectName}.json`, window.location.href);
135+
const res = await fetch(url, { cache: "no-store" });
136+
const project = await res.text();
137+
138+
webComp.parentNode.removeChild(webComp);
139+
webComp = createWebComponent(project);
140+
editorContainer.prepend(webComp);
141+
};
142+
143+
sampleBar.addEventListener("click", (event) => {
144+
const link = event.target.closest("a[data-project]");
145+
if (!link || !sampleBar.contains(link)) return;
146+
event.preventDefault();
147+
loadSampleProjectByName(link.dataset.project);
148+
});
90149

91-
const runButton = document.createElement("button");
92-
const runButtonText = document.createTextNode("Run code");
93-
runButton.appendChild(runButtonText);
94-
runButton.onclick = (event) => {
150+
document.getElementById('run-button').onclick = (event) => {
95151
webComp.rerunCode();
96152
};
97-
body.append(runButton);
98153
});
99154
</script>
100155
</html>

webpack.config.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,15 @@ module.exports = {
102102
port: 3011,
103103
liveReload: true,
104104
hot: false,
105-
static: {
106-
directory: path.join(__dirname, "public"),
107-
},
105+
static: [
106+
{
107+
directory: path.join(__dirname, "public"),
108+
},
109+
{
110+
directory: path.join(__dirname, "src", "projects"),
111+
publicPath: `${publicUrl}projects`,
112+
},
113+
],
108114
headers: {
109115
"Access-Control-Allow-Origin": "*",
110116
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
@@ -143,7 +149,10 @@ module.exports = {
143149
filename: "web-component.html",
144150
}),
145151
new CopyWebpackPlugin({
146-
patterns: [{ from: "public", to: "" }],
152+
patterns: [
153+
{ from: "public", to: "" },
154+
{ from: "src/projects", to: "projects" },
155+
],
147156
}),
148157
],
149158
stats: "minimal",

0 commit comments

Comments
 (0)