Skip to content
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

# [Unreleased]

### Changed

- Add sample projects to code editor test page (#1294)

## [0.34.3] - 2026-01-19

### Changed
Expand Down
1 change: 1 addition & 0 deletions cypress.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default defineConfig({
defaultCommandTimeout: 10000,
video: false,
defaultBrowser: "chrome",
testIsolation: true,
setupNodeEvents(on, config) {
on("task", {
log(message) {
Expand Down
4 changes: 2 additions & 2 deletions cypress/e2e/missionZero-wc.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,13 @@ it("resets criteria correctly", () => {
"text",
"from sense_hat import SenseHat\nsense = SenseHat()\nsense.get_pressure()\nsense.get_humidity()\nsense.get_temperature()",
);
cy.get("editor-wc").shadow().find(".btn--run").contains("Run").click();
cy.get("editor-wc").shadow().find(".btn--run").click();
cy.get("#results").should("contain", '"readPressure":true');
cy.get("editor-wc")
.shadow()
.find("div[class=cm-content]")
.invoke("text", "from sense_hat import SenseHat");
cy.get("editor-wc").shadow().find(".btn--run").contains("Run").click();
cy.get("editor-wc").shadow().find(".btn--run").click();
cy.get("#results").should(
"contain",
'"noInputEvents":true,"readColour":false,"readHumidity":false,"readPressure":false,"readTemperature":false,"usedLEDs":false',
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@
"build": "NODE_ENV=production BABEL_ENV=production webpack build -c ./webpack.config.js",
"build:dev": "yarn install --check-cache && yarn run build-storybook",
"build-storybook": "cd ./storybook && yarn install && yarn run build-storybook -- -o ../public/storybook --loglevel warn",
"lint": "eslint 'src/**/*.{js,jsx,json}' cypress/**/*.js",
"lint:fix": "eslint --fix 'src/**/*.{js,jsx,json}' cypress/**/*.js",
"lint": "eslint 'src/**/*.{js,jsx}' cypress/**/*.js",
"lint:fix": "eslint --fix 'src/**/*.{js,jsx}' cypress/**/*.js",
"stylelint": "stylelint src/**/*.scss",
"test": "node scripts/test.js --transformIgnorePatterns 'node_modules/(?!three)/'",
"storybook": "cd storybook && rm -rf ./node_modules/.cache/storybook && yarn run storybook",
Expand Down
2 changes: 2 additions & 0 deletions src/containers/WebComponentLoader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const WebComponentLoader = (props) => {
withProjectbar = false,
withSidebar = false,
loadCache = true, // Always use cache unless explicitly disabled
initialProject = null,
} = props;
const dispatch = useDispatch();
const { t } = useTranslation();
Expand Down Expand Up @@ -146,6 +147,7 @@ const WebComponentLoader = (props) => {
loadRemix: loadRemix && !loadRemixDisabled,
loadCache,
remixLoadFailed,
initialProject,
});

useProjectPersistence({
Expand Down
6 changes: 6 additions & 0 deletions src/containers/WebComponentLoader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ describe("When no user is in state", () => {
assetsIdentifier: undefined,
projectIdentifier: identifier,
code,
initialProject: null,
accessToken: undefined,
loadRemix: false,
loadCache: true,
Expand Down Expand Up @@ -319,6 +320,7 @@ describe("When no user is in state", () => {
expect(useProject).toHaveBeenCalledWith({
assetsIdentifier: assetsIdentifier,
code,
initialProject: null,
accessToken: undefined,
loadRemix: false,
loadCache: true,
Expand Down Expand Up @@ -354,6 +356,7 @@ describe("When no user is in state", () => {
assetsIdentifier: undefined,
projectIdentifier: identifier,
code,
initialProject: null,
accessToken: "my_token",
loadRemix: true,
loadCache: true,
Expand Down Expand Up @@ -482,6 +485,7 @@ describe("When user is in state", () => {
assetsIdentifier: undefined,
projectIdentifier: identifier,
code: undefined,
initialProject: null,
accessToken: "my_token",
loadRemix: true,
loadCache: true,
Expand Down Expand Up @@ -512,6 +516,7 @@ describe("When user is in state", () => {
assetsIdentifier: undefined,
projectIdentifier: identifier,
code: undefined,
initialProject: null,
accessToken: "my_token",
loadRemix: false,
loadCache: true,
Expand Down Expand Up @@ -592,6 +597,7 @@ describe("When user is in state", () => {
assetsIdentifier: undefined,
projectIdentifier: identifier,
code: undefined,
initialProject: null,
accessToken: "my_token",
loadRemix: false,
loadCache: true,
Expand Down
11 changes: 10 additions & 1 deletion src/hooks/useProject.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const useProject = ({
reactAppApiEndpoint = null,
assetsIdentifier = null,
projectIdentifier = null,
initialProject = null,
code = null,
accessToken = null,
loadRemix = false,
Expand Down Expand Up @@ -45,7 +46,8 @@ export const useProject = ({
projectIdentifier &&
cachedProject &&
cachedProject.identifier === projectIdentifier;
const is_cached_unsaved_project = !projectIdentifier && cachedProject;
const is_cached_unsaved_project =
!projectIdentifier && cachedProject && !initialProject;

if (loadCache && (is_cached_saved_project || is_cached_unsaved_project)) {
loadCachedProject();
Expand Down Expand Up @@ -77,6 +79,12 @@ export const useProject = ({
return;
}

if (initialProject) {
const project = JSON.parse(initialProject);
dispatch(setProject(project));
Comment thread
zetter-rpf marked this conversation as resolved.
return;
}

if (code) {
const project = {
name: "Blank project",
Expand All @@ -97,6 +105,7 @@ export const useProject = ({
i18n.language,
accessToken,
loadRemix,
initialProject,
]);

// Try to load the remix, if it fails set `remixLoadFailed` true, and load the project in the next useEffect
Expand Down
9 changes: 9 additions & 0 deletions src/hooks/useProject.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ describe("When not embedded", () => {
expect(setProject).toHaveBeenCalledWith(defaultPythonProject);
});

test("sets project to initialProject if provided", () => {
const initialProject = { proj: "my-project" };
renderHook(
() => useProject({ initialProject: JSON.stringify(initialProject) }),
{ wrapper },
);
expect(setProject).toHaveBeenCalledWith(initialProject);
});

test("If cached project matches identifer uses cached project", () => {
localStorage.setItem(
cachedProject.identifier,
Expand Down
25 changes: 25 additions & 0 deletions src/projects/blank-html-starter.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"identifier": "blank-html-starter",
"project_type": "html",
"locale": "en",
"name": "Blank HTML & CSS Starter",
"user_id": null,
"instructions": null,
"components": [
{
"id": "42b4cdc9-d935-4a3d-b39a-32eb44c5ebfd",
"name": "index",
"extension": "html",
"content": ""
},
{
"id": "3b6e4869-5873-4782-8764-7bf844578a22",
"name": "styles",
"extension": "css",
"content": ""
}
],
"image_list": [],
"videos": [],
"audio": []
}
19 changes: 19 additions & 0 deletions src/projects/blank-python-starter.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"identifier": "blank-python-starter",
"project_type": "python",
"locale": "en",
"name": "Blank Python Starter",
"user_id": null,
"instructions": null,
"components": [
{
"id": "42b4cdc9-d935-4a3d-b39a-32eb44c5ebfe",
"name": "main",
"extension": "py",
"content": ""
}
],
"image_list": [],
"videos": [],
"audio": []
}
34 changes: 34 additions & 0 deletions src/projects/cool-html.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"identifier": "cool-html",
"project_type": "html",
"locale": "en",
"name": "Cool HTML: Spotlight Gradient",
"user_id": null,
"instructions": {
"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",
"permitOverride": false
},
"components": [
{
"id": "1c7ab1a8-5f09-4b3e-9d0d-6c8c2f2d2a8a",
"name": "index",
"extension": "html",
"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"
},
{
"id": "d9d292e1-20c8-4e55-bad8-2b2c7a4b5f9b",
"name": "styles",
"extension": "css",
"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"
},
{
"id": "a4a5c67c-4bdb-4c9d-94de-0d4b15b093f2",
"name": "script",
"extension": "js",
"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"
}
],
"image_list": [],
"videos": [],
"audio": []
}
28 changes: 28 additions & 0 deletions src/projects/cool-python.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"identifier": "cool-python",
"project_type": "python",
"locale": "en",
"name": "Cool Python: ASCII Orb",
"user_id": null,
"instructions": {
"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",
"permitOverride": false
},
"components": [
{
"id": "9b5a24a8-7d15-4a34-8a59-7b0a1f1b19b2",
"name": "main",
"extension": "py",
"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"
},
{
"id": "5e0a8f3d-3c54-44f9-9fd9-75d41c1bb447",
"name": "ascii_orb",
"extension": "py",
"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"
}
],
"image_list": [],
"videos": [],
"audio": []
}
Loading
Loading