From 3672a2ae44915a44913e04b817aa18825924437a Mon Sep 17 00:00:00 2001 From: Divya Mahadevan Date: Mon, 14 Sep 2026 16:43:59 +0100 Subject: [PATCH 1/8] Cap product page width at 1100px Screens read badly stretched across a large monitor: line lengths go past what anyone scans, and a two-column layout drifts apart. In the workbench the whole framed surface sits on the measure, centred -- the frame already reads as a screenshot rather than a browser window, so narrowing it is consistent with how it is presented. Full screen moves the cap inside onto the page content instead, because there the chrome deliberately goes edge to edge; the bars keep their full-width backgrounds and only their contents line up with the column, or a breadcrumb would sit alone at the far left of a page starting halfway across. One token, --surface-measure, so a prototype that needs a different measure can override it on its own Surface rather than editing this. Applies to every prototype in the repo, not just one. Co-Authored-By: Claude Opus 5 --- src/styles/global.css | 30 ++++++++++++++++++++++++++++++ src/surfaces/tokens.css | 9 +++++++++ 2 files changed, 39 insertions(+) diff --git a/src/styles/global.css b/src/styles/global.css index a1d315f..e133181 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -406,6 +406,12 @@ hr.divider { display: flex; flex-direction: column; width: 100%; + /* Capped so a screen does not stretch across a large monitor. In the + workbench the whole framed product sits on this measure, centred — the + frame already reads as a screenshot rather than a browser window, so + narrowing it is consistent with how it is presented. Full screen moves the + cap inside, onto the page content; see the full-screen block below. */ + max-width: var(--surface-measure); min-height: 420px; overflow: hidden; border-radius: var(--radius-sm); @@ -1146,6 +1152,30 @@ body.full-screen-prototype .surface { border: none; border-radius: 0; min-height: 100vh; + /* Chrome goes edge to edge, as a real product's header and nav do. */ + max-width: none; +} +/* So the cap moves to the page content instead. The bars keep their own + full-width backgrounds and borders; only the content column is measured, + which is what the real sites do. */ +body.full-screen-prototype .surface-body { + width: 100%; + max-width: var(--surface-measure); + margin-inline: auto; +} +/* The bars stay full width but their contents line up with that column, + otherwise a breadcrumb sits alone at the far left of a wide monitor while + the page it describes starts halfway across. Padding rather than a wrapper, + so the background and the bottom border still run edge to edge, and `max()` + keeps the ordinary padding once the window is narrower than the measure. */ +body.full-screen-prototype .surface-topbar, +body.full-screen-prototype .surface-breadcrumbs, +body.full-screen-prototype .surface-nav, +body.full-screen-prototype .surface-footer { + padding-inline: max( + var(--space-2), + calc((100% - var(--surface-measure)) / 2 + var(--space-3)) + ); } .prototype-full-toggle { diff --git a/src/surfaces/tokens.css b/src/surfaces/tokens.css index 1ca09a8..e305cd9 100644 --- a/src/surfaces/tokens.css +++ b/src/surfaces/tokens.css @@ -18,6 +18,15 @@ --surface-accent: var(--rpf-text); --surface-nav: var(--rpf-white); --surface-nav-border: var(--rpf-grey-150); + /* How wide a product page is allowed to get. Screens read badly stretched + across a large monitor — line lengths go past what anyone scans and a + two-column layout drifts apart. Applied in global.css to the framed + surface in the workbench, and to the page content in full screen, where + the chrome goes edge to edge as it does in the real products. + + A prototype that genuinely needs a wider or narrower page can override + this on its own wrapper rather than changing it here. */ + --surface-measure: 1100px; } /* codeclub.org — raspberry accent on the pale green page the dashboard From b0a926be0ef33fa90e23a26287ed520a238137d0 Mon Sep 17 00:00:00 2001 From: Divya Mahadevan Date: Mon, 14 Sep 2026 16:44:09 +0100 Subject: [PATCH 2/8] Make breadcrumbs navigable instead of linking to the gallery Every non-final crumb was a hardcoded link to #/screens, so clicking one dropped whoever was being tested out of the flow and into the screens gallery. Code Classroom has no other navigation, which makes it the worst place in the app to have a link that lies. Surface now takes an optional onCrumb(index). Pass it and the earlier crumbs become working controls; leave it out and they are plain text rather than a link to nowhere. No default destination on purpose -- where a crumb goes is the prototype's decision, the same as every other callback on these screens. Three placeholder #/screens hrefs remain elsewhere, in RoleChooser, MentorSignIn and MentorDashboard. Same bug class, left alone for now. Co-Authored-By: Claude Opus 5 --- src/surfaces/Surface.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/surfaces/Surface.tsx b/src/surfaces/Surface.tsx index 5155fb1..72c189c 100644 --- a/src/surfaces/Surface.tsx +++ b/src/surfaces/Surface.tsx @@ -26,6 +26,17 @@ interface SurfaceProps { * are. The last item is the current page and is not a link. */ breadcrumbs?: string[] + /** + * What happens when a crumb is clicked, by its index in `breadcrumbs`. + * + * Pass this and the earlier crumbs become working controls; leave it out and + * they are plain text. Deliberately NOT optional-with-a-default-destination: + * this is the only navigation Code Classroom has, so where a crumb goes is + * the prototype's decision, the same as every other callback here. It used + * to be a hardcoded link to the screens gallery, which took anyone who + * clicked it out of the flow they were being tested on. + */ + onCrumb?: (index: number) => void /** Right-hand top bar item: "Your Account" for an educator, "Log Out" for a young person. */ account?: string } @@ -46,6 +57,7 @@ export function Surface({ badge, layout, breadcrumbs, + onCrumb, account, }: SurfaceProps) { const surface = SURFACES[id] @@ -74,8 +86,12 @@ export function Surface({ {index > 0 && /} {index === breadcrumbs.length - 1 ? ( {crumb} + ) : onCrumb ? ( + ) : ( - {crumb} + {crumb} )} ))} From 439cd41a725aec411fe3f0b1c9d467850c3b8fa2 Mon Sep 17 00:00:00 2001 From: Divya Mahadevan Date: Mon, 14 Sep 2026 16:44:27 +0100 Subject: [PATCH 3/8] Rebuild the class page to match the live product The reconstruction had drifted far enough from the real screen to be hard to look at: both actions were small secondary buttons, the settings gear was missing entirely, and projects were bare lines of blue link text with a language tag the real page does not have. Now, from the live screen: Class members is the primary action, because getting young people into the class is the job a mentor comes here to do; Copy link is secondary; the gear is there. Projects are a bordered group of pale rows with the name as bold body text and an overflow menu as the action affordance. Icons are the design system's own -- Material Symbols is already imported by its stylesheet and Button takes an icon name. The useful find: the crossed-out eye on each row is the live product's own marker for a project students cannot see yet, so visibility is per project and surfaced right here. That is new information for the open question about where "show to students" lives, and it gives us a much cheaper way to test it -- put a mentor on this page and see whether they read the icon as "not finished" or miss it. Exposed as an optional hiddenProjectIds, so existing callers are unaffected. onSettings and onProjectMenu are optional and default to no-ops. The gear and the kebabs exist on the real page so they render regardless, but they do nothing until a prototype wires them -- worth knowing before a session. Co-Authored-By: Claude Opus 5 --- src/screens/README.md | 7 ++ src/screens/classroom/EducatorClassPage.tsx | 94 +++++++++++++++++---- src/styles/global.css | 37 ++++++++ 3 files changed, 123 insertions(+), 15 deletions(-) diff --git a/src/screens/README.md b/src/screens/README.md index 9e62226..758c023 100644 --- a/src/screens/README.md +++ b/src/screens/README.md @@ -69,6 +69,13 @@ Two structural facts worth designing around: - **A mentor only sees work that has been SAVED.** "Only students who have saved their project will appear here." Walking round a room, a mentor cannot tell who has started and got stuck from who has not started at all. +- **Visibility is per project, and the class page shows it.** Each row on + `EducatorClassPage` can carry a crossed-out eye, which is how a mentor tells + a project nobody can see yet from one that is live. Pass `hiddenProjectIds` + if your flow has a notion of visibility. Any flow that puts a project into a + class inherits some visibility state whether it decides one or not — and + `EducatorProjectPage` only offers "Hide from students", so where "show" lives + is an open question rather than a settled one. ## Where mentor onboarding actually starts diff --git a/src/screens/classroom/EducatorClassPage.tsx b/src/screens/classroom/EducatorClassPage.tsx index 7a11369..0290b74 100644 --- a/src/screens/classroom/EducatorClassPage.tsx +++ b/src/screens/classroom/EducatorClassPage.tsx @@ -1,4 +1,4 @@ -import { Button, Card, Tag } from '../../kit' +import { Button, Card } from '../../kit' import type { ClassGroup, Project } from '../../fixtures' import type { ScreenMeta } from '../types' @@ -6,7 +6,7 @@ export const meta: ScreenMeta = { surface: 'classroom', existsToday: true, verified: true, - note: 'What a mentor sees inside a class. Projects are shared with the whole class; each carries starter code the mentor set up.', + note: 'What a mentor sees inside a class. Projects are shared with the whole class; each carries starter code the mentor set up, and each is hidden or shown to students individually.', } interface Props { @@ -17,12 +17,27 @@ interface Props { onOpenProject: (projectId: string) => void onCopyLink: () => void onClassMembers: () => void + /** + * Projects students cannot see yet. The live page marks each row with a + * crossed-out eye, so a mentor can tell at a glance what is not live — pass + * this if your flow has a notion of visibility, and leave it out if not. + */ + hiddenProjectIds?: string[] + /** The per-row overflow menu. Renders whether or not you wire it. */ + onProjectMenu?: (projectId: string) => void + /** The gear beside "Copy link". Renders whether or not you wire it. */ + onSettings?: () => void } /** * The class page an educator lands on. * - * Two things to notice for Code Club: + * Built from the live page, so the furniture is the real furniture: "Class + * members" is the primary action, projects sit in a bordered list of pale rows + * with an overflow menu each, and a project name is bold body text rather than + * a link. + * + * Three things to notice for Code Club: * * - "Projects are shared with students and contain starter code created by a * teacher" — projects here are set up BY the adult, not chosen by the young @@ -30,6 +45,10 @@ interface Props { * person browses and picks. * - "Copy link" is how young people get in, which is the shareable route that * skips the school code screen. + * - **Visibility is per project and shown right here.** The crossed-out eye is + * how a mentor tells a project nobody can see yet from one that is live. Any + * flow that adds a project to a class inherits that state whether it means + * to or not. */ export function EducatorClassPage({ classGroup, @@ -39,14 +58,31 @@ export function EducatorClassPage({ onOpenProject, onCopyLink, onClassMembers, + hiddenProjectIds = [], + onProjectMenu, + onSettings, }: Props) { return (

{classGroup.name}

-
@@ -57,21 +93,49 @@ export function EducatorClassPage({ Projects are shared with students and contain starter code created by a teacher.

- - - - ))} +
    + {projects.map((project) => { + const hidden = hiddenProjectIds.includes(project.id) + return ( +
  • + + {hidden && ( + // Labelled rather than aria-hidden: the icon is the only + // thing on the row carrying this, so it has to be read. + + visibility_off + + )} + + +
  • + ) + })}
)} diff --git a/src/styles/global.css b/src/styles/global.css index e133181..fb7bb8d 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -687,6 +687,43 @@ hr.divider { border-bottom: none; } +/* The class page draws its projects as a bordered, rounded group of pale rows + rather than as bare lines of text. Opt-in, because the same .cc-row is used + for plain lists elsewhere. */ +.cc-list-boxed { + border: 1px solid var(--rpf-grey-150); + border-radius: var(--radius-sm); + overflow: hidden; +} +.cc-list-boxed .cc-row { + padding: var(--space-2); + background: var(--rpf-off-white); +} +/* Icon and name travel together on the left; the overflow menu stays right. */ +.cc-row-main { + display: flex; + align-items: center; + gap: var(--space-1); + min-width: 0; +} +/* A project name on the live page is bold body text, not a blue link — the + row's overflow menu is the action affordance. Underlined on hover and focus + anyway, so it does not rely on being guessable. */ +.cc-row-title { + background: none; + border: none; + padding: 0; + font: inherit; + font-weight: var(--fw-bold); + color: var(--rpf-text); + text-align: left; + cursor: pointer; +} +.cc-row-title:hover, +.cc-row-title:focus-visible { + text-decoration: underline; +} + /* The educator project page puts guidance beside the student work list. */ .cc-columns { display: grid; From 3bb0fd081eee3ab302482403a80cadd79358e48a Mon Sep 17 00:00:00 2001 From: Divya Mahadevan Date: Mon, 14 Sep 2026 16:44:42 +0100 Subject: [PATCH 4/8] Make the project editor fill the page It rendered as a small panel in the middle of a large page with dead space under it: .editor had a 420px floor, and the canvas held a fixed 220px grey box that did not grow with its pane. .editor is now sized from the viewport, keeping 420px as a floor for short windows. flex: 1 would not have done it -- in the workbench the surface is only as tall as its content, so there is no spare height to grow into. The canvas placeholder stretches to the whole pane, which needed Placeholder's height widened to number | string so it can take '100%'. Its height is applied inline, so CSS cannot override it from outside. Both editors, Code Classroom and Code Club Projects, since they share the layout. Co-Authored-By: Claude Opus 5 --- src/kit/Placeholder.tsx | 8 ++++++-- src/screens/ccp/ProjectEditor.tsx | 2 +- src/screens/classroom/ClassroomProjectEditor.tsx | 2 +- src/styles/global.css | 11 ++++++++++- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/kit/Placeholder.tsx b/src/kit/Placeholder.tsx index 96af7f5..437e266 100644 --- a/src/kit/Placeholder.tsx +++ b/src/kit/Placeholder.tsx @@ -1,8 +1,12 @@ interface PlaceholderProps { /** What the real thing would be, e.g. "Project thumbnail". */ label?: string - /** Height in pixels. Defaults to a shallow block. */ - height?: number + /** + * Height. A number is pixels; a string is any CSS length, so '100%' + * stands in for something that fills whatever it is given — a code editor, + * say. Defaults to a shallow block. + */ + height?: number | string /** Stretch to fill the available width. Defaults to true. */ fullWidth?: boolean } diff --git a/src/screens/ccp/ProjectEditor.tsx b/src/screens/ccp/ProjectEditor.tsx index bdcbc2a..f5eedf0 100644 --- a/src/screens/ccp/ProjectEditor.tsx +++ b/src/screens/ccp/ProjectEditor.tsx @@ -152,7 +152,7 @@ export function ProjectEditor({
diff --git a/src/screens/classroom/ClassroomProjectEditor.tsx b/src/screens/classroom/ClassroomProjectEditor.tsx index 76535a4..c531ed2 100644 --- a/src/screens/classroom/ClassroomProjectEditor.tsx +++ b/src/screens/classroom/ClassroomProjectEditor.tsx @@ -113,7 +113,7 @@ export function ClassroomProjectEditor({
diff --git a/src/styles/global.css b/src/styles/global.css index fb7bb8d..de7d917 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -750,7 +750,12 @@ hr.divider { there is room, which is how the real editor lays out. */ grid-template-columns: 1fr; gap: var(--space-2); - min-height: 420px; + /* The editor IS the page it is on, not a panel sitting in the middle of one. + Sized from the viewport so it fills a large monitor, with 420px kept as a + floor for short windows. `flex: 1` alone would not do it: in the workbench + the surface is only as tall as its content, so there is no spare height to + grow into. */ + min-height: max(420px, 68vh); } @media (min-width: 860px) { .editor { @@ -867,6 +872,10 @@ hr.divider { .editor-canvas { flex: 1 1 auto; padding: var(--space-2); + /* So the thing standing in for the editor can stretch to the whole pane + rather than sitting as a short box with dead space beneath it. */ + display: flex; + min-height: 0; } /* Project instructions markdown */ From 20f8c803486babde8c2b96b70c738b329461042b Mon Sep 17 00:00:00 2001 From: Divya Mahadevan Date: Mon, 14 Sep 2026 16:45:02 +0100 Subject: [PATCH 5/8] Add import-mentor prototype: out to Projects and back A mentor starts in Code Classroom, goes out to Code Club Projects to browse, and comes back with a project attached to a class. Sarah Tucker's bet: Code Classroom is the hub for a club, so that is where a mentor goes looking for a project -- not Code Club Projects, where they have always gone for club content. Built to Sarah's "Option 1" flow chart, with four departures from it, all argued in notes.md: - The whole add-to-class interaction stays on Code Club Projects and crosses into Code Classroom only at "View your class". The chart put that boundary earlier; Experience CS, the other product feeding Code Classroom, does it this way, and Divya called it. The chart wants updating to match. - The find/create fork is one dialog with two steps, not a page and not two stacked dialogs. Step 2 is the live "Create a new project" dialog, so the branch this prototype tests against is the real screen. - The class-picking cards follow Experience CS's equivalent screens. - The catalogue follows the live Code Club Projects category pages, under a proposed "Code Classroom compatible" category. Five findings that fell out of making the chart run, not design opinions: "view project code" arrives before there are any instructions to view and would crash the shared editor; "show to students" inverts the live product, which only offers "hide"; the category now excludes nothing, so the "can we connect non-editor projects?" question is no longer visible in the flow and needs a fixture to test; success succeeds at less than it claims on the new-class branch; and creating a class from Projects is a write into another product, which is a much bigger ask than the rest of the flow. Nothing here ships and nothing is a commitment to build. Co-Authored-By: Claude Opus 5 --- .../AddProjectChoice.tsx | 249 +++++++ .../out-to-projects-and-back/AddToClass.tsx | 183 +++++ .../CreateInClassroom.tsx | 73 ++ .../MentorSchoolHome.tsx | 60 ++ .../out-to-projects-and-back/NewClass.tsx | 107 +++ .../ProjectsCatalogue.tsx | 230 +++++++ .../ShowToStudents.tsx | 89 +++ .../out-to-projects-and-back/meta.ts | 18 + .../out-to-projects-and-back/notes.md | 272 ++++++++ .../out-to-projects-and-back/projectTypes.ts | 115 ++++ .../out-to-projects-and-back/prototype.tsx | 632 ++++++++++++++++++ 11 files changed, 2028 insertions(+) create mode 100644 src/prototypes/import-mentor/out-to-projects-and-back/AddProjectChoice.tsx create mode 100644 src/prototypes/import-mentor/out-to-projects-and-back/AddToClass.tsx create mode 100644 src/prototypes/import-mentor/out-to-projects-and-back/CreateInClassroom.tsx create mode 100644 src/prototypes/import-mentor/out-to-projects-and-back/MentorSchoolHome.tsx create mode 100644 src/prototypes/import-mentor/out-to-projects-and-back/NewClass.tsx create mode 100644 src/prototypes/import-mentor/out-to-projects-and-back/ProjectsCatalogue.tsx create mode 100644 src/prototypes/import-mentor/out-to-projects-and-back/ShowToStudents.tsx create mode 100644 src/prototypes/import-mentor/out-to-projects-and-back/meta.ts create mode 100644 src/prototypes/import-mentor/out-to-projects-and-back/notes.md create mode 100644 src/prototypes/import-mentor/out-to-projects-and-back/projectTypes.ts create mode 100644 src/prototypes/import-mentor/out-to-projects-and-back/prototype.tsx diff --git a/src/prototypes/import-mentor/out-to-projects-and-back/AddProjectChoice.tsx b/src/prototypes/import-mentor/out-to-projects-and-back/AddProjectChoice.tsx new file mode 100644 index 0000000..cfab9e2 --- /dev/null +++ b/src/prototypes/import-mentor/out-to-projects-and-back/AddProjectChoice.tsx @@ -0,0 +1,249 @@ +import { useState, type CSSProperties } from 'react' +import { Modal, TextInput } from '../../../kit' +import { PROJECT_TYPES, type ProjectTypeId } from './projectTypes' + +// PROPOSED, and the fork is what this prototype exists to test. +// +// ONE dialog with TWO steps, not two dialogs. Step 1 asks find-or-create; if +// the answer is "create your own", the same dialog becomes the live "Create a +// new project" dialog rather than opening a second box on top of the first. +// Stacked dialogs were the first thing anyone objected to, and they were a +// symptom rather than a styling problem: the fork was wrapping a dialog the +// product already has, so the fix is to make it the first step OF that dialog. +// +// What that buys, beyond not stacking: step 2 is now the real screen, down to +// the type descriptions and the tile colours, so the branch this prototype is +// testing AGAINST is the genuine article rather than a page invented to stand +// in for it. "Back" also means something — it returns to the question instead +// of dismissing one dialog to reveal another. +// +// Step 1 is the only proposed part. It preselects nothing, because +// preselecting would answer the question being asked, and because the design +// system's modal takes no `disabled` for its footer buttons, "Continue" with +// nothing chosen asks for a choice rather than doing nothing. +// +// Tiles are flat colour with no glyph, as in ManageClub. The live dialog has +// small icons in them; there is no icon component in the kit for arbitrary +// tiles, and grey-boxing keeps a session about the flow rather than the art. +// +// Deliberately NOT badged "proposed" on screen; see screens/types.ts. + +type Choice = 'find' | 'create' + +const ROUTES: Array<{ id: Choice; tone: string; title: string; blurb: string }> = [ + { + id: 'find', + tone: 'green', + title: 'Find a Code Club project', + blurb: 'Instructions and starter code already written.', + }, + { + id: 'create', + tone: 'blue', + title: 'Create your own', + blurb: 'An empty project you write yourself.', + }, +] + +/** A selectable row: coloured tile, title, one line of description. */ +function OptionRow({ + tone, + title, + blurb, + selected, + onSelect, +}: { + tone: string + title: string + blurb: string + selected: boolean + onSelect: () => void +}) { + return ( + + ) +} + +interface Props { + isOpen: boolean + setIsOpen: (open: boolean) => void + className: string + onFind: () => void + onCreate: (name: string, type: ProjectTypeId) => void +} + +export function AddProjectChoice({ isOpen, setIsOpen, className, onFind, onCreate }: Props) { + const [onCreateStep, setOnCreateStep] = useState(false) + const [choice, setChoice] = useState() + const [nudge, setNudge] = useState(false) + + const [type, setType] = useState('Blocks') + const [typed, setTyped] = useState() + const [nameError, setNameError] = useState() + + // The live dialog arrives with "Blocks project" already in the field and + // Blocks selected, so the name follows the type until the mentor writes + // their own — after which it is theirs and stops moving. + const name = typed ?? `${type} project` + + function close() { + setOnCreateStep(false) + setChoice(undefined) + setNudge(false) + setType('Blocks') + setTyped(undefined) + setNameError(undefined) + setIsOpen(false) + } + + function forward() { + if (!choice) { + setNudge(true) + return + } + if (choice === 'find') { + onFind() + return + } + setOnCreateStep(true) + } + + function create() { + if (!name.trim()) { + setNameError('Give the project a name first.') + return + } + onCreate(name.trim(), type) + } + + return ( + // The custom property inherits down to the dialog element, which is how a + // narrower modal is possible without touching the shared stylesheet. +
+ (open ? setIsOpen(true) : close())} + heading={onCreateStep ? 'Create a new project' : 'Add a project'} + showCloseButton + primaryButtonText={onCreateStep ? 'Create project' : 'Continue'} + onClickPrimaryButton={onCreateStep ? create : forward} + // "Back" rather than "Cancel" on step 2: it came from a question, so + // returning to it is more use than dismissing. The header's X still + // cancels, which is how the live dialog's two buttons stay two. + secondaryButtonText={onCreateStep ? 'Back' : 'Cancel'} + onClickSecondaryButton={onCreateStep ? () => setOnCreateStep(false) : close} + > + {/* The modal's content box centres its children, so this has to claim + the full width or every row sits in the middle. */} +
+ {onCreateStep ? ( + <> + { + setTyped(event.target.value) + setNameError(undefined) + }} + /> + +

+ What kind of project do you want to make for your students? +

+ +
+ {PROJECT_TYPES.map((option) => ( + setType(option.id)} + /> + ))} +
+ + ) : ( + <> +

+ What kind of project do you want to add to {className}? +

+ + {nudge && ( +

+ Pick one of these to carry on. +

+ )} + +
+ {ROUTES.map((option) => ( + { + setChoice(option.id) + setNudge(false) + }} + /> + ))} +
+ + )} +
+
+
+ ) +} diff --git a/src/prototypes/import-mentor/out-to-projects-and-back/AddToClass.tsx b/src/prototypes/import-mentor/out-to-projects-and-back/AddToClass.tsx new file mode 100644 index 0000000..fcfc213 --- /dev/null +++ b/src/prototypes/import-mentor/out-to-projects-and-back/AddToClass.tsx @@ -0,0 +1,183 @@ +import { useState } from 'react' +import { Button, Card, SelectInput } from '../../../kit' +import type { ClassGroup } from '../../../fixtures' + +// PROPOSED. "Choose new or existing class" from the flow chart, reworked to +// follow the equivalent screens in Experience CS — the other product that +// integrates with Code Classroom, and so the closest thing to a precedent we +// have for how a content product hands a piece of work to a class. +// +// Both cards render on the Code Club Projects surface. The mentor picks a Code +// Classroom class without leaving the site they were browsing on, and the flow +// crosses products only when they press "View your class". That follows +// Experience CS rather than the chart, which had put this step in the Code +// Classroom lane; see prototype.tsx and notes.md. +// +// Taken from those screens: +// +// - one small centred card on a tinted page, not a full-width page +// - a heading naming the thing being added: "Add Weather watchers to class" +// - a sentence explaining what a class is FOR, before asking which one +// - the class as a single select labelled "School class", not a list of +// buttons — so picking a class is one decision, not one decision per class +// - one primary action, bottom right of the card +// - a success card of the same shape: same heading, a tick, one sentence +// naming what went where, then where to go next +// +// Changed for Code Club Projects rather than copied: +// +// - "unit" is Experience CS's word for a group of lessons. Code Club Projects +// has projects, so this says project. +// - Experience CS offers no way to create a class here; the chart's diamond +// does, so "Create a new class" sits beside the primary action. Without it +// this screen is a dead end for a mentor whose club has no class yet. +// - Experience CS's mint page is its own brand colour. These sit on Code Club +// Projects' own page colour rather than importing another product's, so the +// card is told apart by its border rather than by a tint. +// +// Deliberately NOT badged "proposed" on screen; see screens/types.ts. + +/** "Add to class" — the select screen. */ +export function AddToClass({ + projectTitle, + classes, + onAdd, + onNew, + onBack, +}: { + projectTitle: string + classes: ClassGroup[] + onAdd: (classId: string) => void + onNew: () => void + onBack: () => void +}) { + const [classId, setClassId] = useState(classes[0]?.id ?? '') + + return ( + +

Add {projectTitle} to class

+ +

+ Classes let you group students and assign projects to them. After adding this project to a + class, students will be able to work on it. +

+ + {classes.length === 0 ? ( + // Reachable: a club whose Code Classroom school has no class yet. The + // select has nothing to offer, so the only way on is to make one. +

+ You have no classes yet. Create one and this project will be added to it. +

+ ) : ( +
+ ({ key: group.id, value: group.name }))} + value={classId} + onChange={(event) => setClassId(event.target.value)} + /> + {/* Sits under the field it is about rather than in the button row. + Three buttons do not fit a 420px card, and more importantly the + Experience CS card drives at ONE action — a second button of + equal weight would undo that. */} + +
+ )} + +
+
+
+ ) +} + +/** The success card. Experience CS keeps the same heading and swaps the body. */ +export function AddedToClass({ + projectTitle, + className, + isNewClass, + onViewClass, + onFindAnother, +}: { + projectTitle: string + className: string + isNewClass: boolean + onViewClass: () => void + onFindAnother: () => void +}) { + return ( + +

Add {projectTitle} to class

+ + {/* Experience CS uses a circled tick here. There is no icon component in + the kit, so this is drawn from a border and a character, and hidden + from screen readers — the sentence below it carries the meaning. */} + + +

+ {projectTitle} has been added to {className}. +

+ +

+ You can manage this project anytime from the class project list on your Code Classroom + dashboard. +

+ + {isNewClass && ( + // Not in the Experience CS screens, and it matters here: the chart's + // new-class branch lands the project in a class with nobody in it. +

+ Nobody is in {className} yet, so nobody can see it. Add young people to the class from + your Code Classroom dashboard. +

+ )} + +
+ {/* Experience CS's secondary here is "View unit" — the thing you just + added. This keeps the chart's loop instead, which is the action a + mentor setting up a term of sessions actually wants. */} +
+
+ ) +} diff --git a/src/prototypes/import-mentor/out-to-projects-and-back/CreateInClassroom.tsx b/src/prototypes/import-mentor/out-to-projects-and-back/CreateInClassroom.tsx new file mode 100644 index 0000000..6cfdd3e --- /dev/null +++ b/src/prototypes/import-mentor/out-to-projects-and-back/CreateInClassroom.tsx @@ -0,0 +1,73 @@ +import { useState } from 'react' +import { Button, Card, TextareaInput } from '../../../kit' + +// The tail of the chart's "create your own" branch. Naming the project and +// picking its type now happen in step 2 of the add-project dialog, which is +// the live "Create a new project" screen — see AddProjectChoice.tsx. +// +// PROPOSED. Adding instructions exists in Code Classroom, but nobody has built +// this screen in `screens/` and nobody has checked it against the real thing, +// so treat the details as wrong. +// +// This branch is in the prototype because it is the alternative the mentor is +// choosing against. If it turns out to be the one they reach for, the +// hypothesis is wrong in an interesting way. + +/** "Add instructions" — the last box before the two branches rejoin. */ +export function AddInstructions({ + projectTitle, + onSave, + onBack, +}: { + projectTitle: string + onSave: (instructions: string) => void + onBack: () => void +}) { + const [text, setText] = useState('') + const [error, setError] = useState() + + return ( +
+
+
+ +
+

Add instructions

+

+ What young people read in the left-hand panel while they work on {projectTitle}. +

+
+ + + { + setText(event.target.value) + setError(undefined) + }} + /> + + +
+
+
+ ) +} diff --git a/src/prototypes/import-mentor/out-to-projects-and-back/MentorSchoolHome.tsx b/src/prototypes/import-mentor/out-to-projects-and-back/MentorSchoolHome.tsx new file mode 100644 index 0000000..c851e99 --- /dev/null +++ b/src/prototypes/import-mentor/out-to-projects-and-back/MentorSchoolHome.tsx @@ -0,0 +1,60 @@ +import { Card, Tag } from '../../../kit' +import type { ClassGroup, School } from '../../../fixtures' + +// The chart's first box: "Code Classroom". A mentor who is already signed in +// and already has a school lands on the school, and the classes under it are +// the only way onwards — there is no navigation to go anywhere else. +// +// This screen EXISTS in the real product and has never been built in +// `screens/`, and NOBODY HAS CHECKED IT against the real educator view. Built +// to mirror YoungPersonSchoolHome, so treat the details as wrong. It is here +// only so the flow can start where the chart starts. + +interface Props { + school: School + classes: ClassGroup[] + memberCount: (classId: string) => number + onOpenClass: (classId: string) => void +} + +export function MentorSchoolHome({ school, classes, memberCount, onOpenClass }: Props) { + return ( +
+
+

{school.name}

+ {/* Code Classroom calls this a school even when it is a library. The + mismatch is kept, not fixed — see screens/README.md. */} +

+ School code: {school.schoolCode} · {school.country} +

+
+ + +
+
+

Classes

+

+ Young people sign in with the school code and join the class you put them in. +

+
+
+ + {classes.length === 0 ? ( +

No classes yet

+ ) : ( +
    + {classes.map((group) => ( +
  • + + {memberCount(group.id)} members + +
  • + ))} +
+ )} +
+
+ ) +} diff --git a/src/prototypes/import-mentor/out-to-projects-and-back/NewClass.tsx b/src/prototypes/import-mentor/out-to-projects-and-back/NewClass.tsx new file mode 100644 index 0000000..e7a1b88 --- /dev/null +++ b/src/prototypes/import-mentor/out-to-projects-and-back/NewClass.tsx @@ -0,0 +1,107 @@ +import { useState } from 'react' +import { Button, Card, TextInput, TextareaInput } from '../../../kit' + +// PROPOSED. The chart's "create a new class" branch, reached while adding a +// project — so a mentor is setting up a class as a side effect of importing. +// +// The diamond before this one ("How do you want to create your class") has +// only ONE branch drawn on the board, to "Name + description". Kept as its own +// step so the flow matches the chart, with the missing branch called out in +// the workbench commentary rather than invented here. See notes.md. + +/** The diamond: "How do you want to create your class". */ +export function HowToCreateClass({ + onNameIt, + onBack, +}: { + onNameIt: () => void + onBack: () => void +}) { + return ( +
+
+
+ +

How do you want to create your class?

+ + +

Name it yourself

+

+ Give the class a name and a description, and add young people afterwards. +

+
+ ) +} + +/** "Name + description". */ +export function NameAndDescription({ + projectTitle, + onCreate, + onBack, +}: { + projectTitle: string + onCreate: (name: string, description: string) => void + onBack: () => void +}) { + const [name, setName] = useState('') + const [description, setDescription] = useState('') + const [error, setError] = useState() + + return ( +
+
+
+ +
+

Name your class

+

{projectTitle} will be added to it.

+
+ + + { + setName(event.target.value) + setError(undefined) + }} + /> + + + + setDescription(event.target.value)} + /> + + +
+
+
+ ) +} diff --git a/src/prototypes/import-mentor/out-to-projects-and-back/ProjectsCatalogue.tsx b/src/prototypes/import-mentor/out-to-projects-and-back/ProjectsCatalogue.tsx new file mode 100644 index 0000000..3829858 --- /dev/null +++ b/src/prototypes/import-mentor/out-to-projects-and-back/ProjectsCatalogue.tsx @@ -0,0 +1,230 @@ +import { useState } from 'react' +import { Button, Card, CheckboxInput, Placeholder, Tag } from '../../../kit' +import type { Project } from '../../../fixtures' +import { isEmbeddedEditorProject } from './projectTypes' + +// "CCP page filtered by embedded editor projects" — the real Code Club +// Projects catalogue, on the real Code Club Projects surface, reached by +// leaving Code Classroom. +// +// Built to match the live category pages (projects.raspberrypi.org/en/ +// technology/scratch): green hero panel with a Back pill, a big category +// title and a line of description; a results count; a filter rail; then a grid +// of cards, each an image band over a "language - Level n" eyebrow, a linked +// title and a description. +// +// The category is the proposal. "Scratch" and "Python" are real categories a +// young person browses by; "Code Classroom compatible" is a category defined +// by what a mentor can DO with a project rather than by what it teaches, and +// nothing like it exists on the live site. +// +// It now covers Scratch, Python and HTML — every language the fixtures have — +// so it excludes nothing here and a mentor sees the whole catalogue. Which +// raises its own question: a category that excludes nothing is a category +// doing no work. See notes.md, finding 3. +// +// Not built in `screens/` because no catalogue screen exists there, and this +// one is a proposal rather than a reconstruction. +// +// Deliberately left out of the copy: the live page's "Start a path" strip of +// three coloured path cards. Paths are a young person's route through a +// subject over several weeks, which is not what a mentor is doing here, and +// the fixtures have no paths to show. + +// Approximations of the live category page's palette, sampled from the page. +// The shared token set has no greens (src/styles/tokens.css), so they are +// declared once here and scoped to this prototype rather than repeated inline. +// Delete these if Code Club Projects' palette ever lands in tokens.css. +const HERO_GREEN = 'hsl(123, 45%, 87%)' +const HERO_ART = 'hsl(123, 38%, 80%)' + +interface Props { + projects: Project[] + /** Projects already in one of this mentor's classes. */ + alreadyAddedIds: string[] + onView: (projectId: string) => void + onBack: () => void +} + +export function ProjectsCatalogue({ projects, alreadyAddedIds, onView, onBack }: Props) { + const inCategory = projects.filter(isEmbeddedEditorProject) + const hidden = projects.length - inCategory.length + + // The live page filters by difficulty level, and `level` is the one filter + // the fixtures can actually honour. Interest and hardware are on the real + // page and are not modelled, so they are not faked here either. + const levels = [...new Set(inCategory.map((p) => p.level))].sort() + const [chosenLevels, setChosenLevels] = useState([]) + + const visible = + chosenLevels.length === 0 + ? inCategory + : inCategory.filter((p) => chosenLevels.includes(p.level)) + + function toggleLevel(level: number, on: boolean) { + setChosenLevels((current) => + on ? [...current, level] : current.filter((value) => value !== level), + ) + } + + return ( +
+ {/* Hero. The live one carries a category illustration on the right. */} +
+
+
+ {/* Says where it goes, not just "Back". A mentor is one click from + a different product and the button is the only thing that says + so — the live page, which never leaves, can afford "Back". */} +
+

+ Code Classroom compatible +

+

+ Projects you can add straight to one of your classes in Code Classroom, with their + instructions and starter code already written. +

+
+ +
+
+ +
+
+
+ + {/* Filter rail beside the results, as on the live page. `cc-columns` is + the shared rail-plus-content layout EducatorProjectPage uses — a + fixed rail from 720px up, stacked below it. Reusing it rather than + hand-rolling a grid means this page stacks on a narrow window + without needing a media query an inline style cannot express. */} +
+ +

Filter

+

+ The project list automatically updates when you apply a filter. +

+
+ + Difficulty level + + {levels.map((level) => ( + toggleLevel(level, event.target.checked)} + /> + ))} +
+
+ +
+
+

+ {visible.length} {visible.length === 1 ? 'result' : 'results'} +

+ {/* UNREACHABLE with today's fixtures, and kept on purpose. The + category now covers all three languages the fixtures have, so + nothing is excluded and this never renders. It becomes correct + the moment a project that cannot be embedded exists — which is + what the "can we connect projects that aren't editor projects?" + sticky is about. Do not read its absence as an answer. */} + {hidden > 0 && ( +

+ {hidden} more {hidden === 1 ? 'project is' : 'projects are'} not here, because + they do not run in an editor Code Classroom can embed. +

+ )} +
+ + {visible.length === 0 ? ( + +

No projects match that filter

+
+ ) : ( +
+ {visible.map((project) => { + const added = alreadyAddedIds.includes(project.id) + return ( + +
+ {/* The live cards lead with an illustration that runs to + the card's edges, so it has to be pulled back out + through the card's own padding and its top corners + rounded to match. Grey-boxed, as everywhere else + here, so a session is about the flow and not the + artwork. */} +
+ +
+ + + {project.language} - Level {project.level} + + + {/* On the live page the title is the link, not the card + and not a button. */} + + +

+ {project.intro.split('\n')[0].replace(/\*\*/g, '')} +

+ + {added && ( +
+ +
+ )} +
+
+ ) + })} +
+ )} +
+
+
+ ) +} diff --git a/src/prototypes/import-mentor/out-to-projects-and-back/ShowToStudents.tsx b/src/prototypes/import-mentor/out-to-projects-and-back/ShowToStudents.tsx new file mode 100644 index 0000000..f9c4de4 --- /dev/null +++ b/src/prototypes/import-mentor/out-to-projects-and-back/ShowToStudents.tsx @@ -0,0 +1,89 @@ +import { Alert, Button, Card } from '../../../kit' + +// PROPOSED, and it contradicts the live product — which is why it is worth +// building rather than arguing about. +// +// The chart ends both branches at "[Show to students]", in square brackets, +// with a sticky asking whether mentors understand the project is hidden by +// default. But the real Code Classroom project page (screens/EducatorProjectPage) +// offers "Hide from students", which means a project is VISIBLE the moment it +// exists. So this screen inverts today's behaviour: nothing is shown until the +// mentor says so. +// +// Both readings are defensible. Hidden-by-default lets a mentor set a session +// up in advance without young people wandering into a half-built project; +// visible-by-default means one less step and nothing to forget. The cost of +// hidden-by-default is the whole reason for the sticky: a mentor who does not +// realise has a room of young people who cannot see the thing they came for. +// +// The "Tool tip" sticky is realised as the hint below the toggle. Whether a +// tooltip is enough to carry a default nobody expects is the thing to watch. + +interface Props { + projectTitle: string + className: string + memberCount: number + shown: boolean + onShow: () => void + onDone: () => void + onBack: () => void +} + +export function ShowToStudents({ + projectTitle, + className, + memberCount, + shown, + onShow, + onDone, + onBack, +}: Props) { + return ( +
+
+
+ +
+

{projectTitle}

+

In {className}

+
+ + {shown ? ( + +

+ {memberCount === 0 + ? `Nobody is in ${className} yet, so there is still nobody to see it.` + : `All ${memberCount} young people in ${className} can open it now.`} +

+
+ ) : ( + +

+ You are the only person who can see this project. It stays hidden until you show it. +

+
+ )} + + +

Show to students

+ {/* The "Tool tip" sticky, as copy. A default nobody expects is being + carried by one line of hint text — that is the thing to test. */} +

+ {shown + ? `${projectTitle} is showing to everyone in ${className}. You can hide it again from the project page.` + : `New projects are hidden so you can set them up before a session. Young people will not see ${projectTitle} in ${className} until you show it to them.`} +

+
+
+
+
+ ) +} diff --git a/src/prototypes/import-mentor/out-to-projects-and-back/meta.ts b/src/prototypes/import-mentor/out-to-projects-and-back/meta.ts new file mode 100644 index 0000000..2b0ae56 --- /dev/null +++ b/src/prototypes/import-mentor/out-to-projects-and-back/meta.ts @@ -0,0 +1,18 @@ +import type { PrototypeMeta } from '../../types' + +export const meta: PrototypeMeta = { + title: 'Out to Projects and back', + + owner: 'Sarah Tucker', + + hypothesis: + 'Mentors treat Code Classroom as the hub for their club, so they will start there — and expect to go out to Code Club Projects, pick a project, and have it land back in one of their classes without leaving the flow.', + + status: 'sketch', + + // Thabo runs one club (Westlands Library Code Club) which already has two + // classes — Thursday Scratch and Thursday Python. One club keeps "which club + // am I in?" out of the way, and two existing classes make the "new class or + // existing class?" decision a real choice rather than a formality. + cast: { piAccount: 'pi-mentor-thabo' }, +} diff --git a/src/prototypes/import-mentor/out-to-projects-and-back/notes.md b/src/prototypes/import-mentor/out-to-projects-and-back/notes.md new file mode 100644 index 0000000..11538da --- /dev/null +++ b/src/prototypes/import-mentor/out-to-projects-and-back/notes.md @@ -0,0 +1,272 @@ +A mentor starts in Code Classroom, goes out to Code Club Projects to browse, +and comes back with a project attached to a class. + +Built to Sarah's flow chart, "Option 1: Code Classroom → Code Club Projects" +(Code Club account management discovery, FigJam). + +## What question is this answering? + +`import-mentor` — how does a mentor get a Code Club project into Code +Classroom? + +Specifically: what if they never start on Code Club Projects at all? The bet is +that Code Classroom is where a mentor already is — the hub for their club — so +the journey is Classroom → out to Projects to browse → back into Classroom with +the project in a class. + +Cast is **Thabo Mokoena** at Westlands Library Code Club. One club, so "which +club am I in?" stays out of the way, and two existing classes so "new class or +existing class?" is a real choice. The chart's own starting assumption — the +mentor is signed in and has a school — is carried by the cast rather than by a +sign-in step, which is why `ownsSignIn` is not set. + +## Why this approach + +The bet is about the **starting point**, not the screens. The obvious +alternative is that a mentor starts on Code Club Projects, where they have +always gone for club content, and pushes from there. That is worth its own +prototype; this one deliberately tests the opposite, because if Classroom +really is the hub then a mentor who starts on Projects has already gone the +wrong way round. + +**The one thing this flow relies on that the young-person lane cannot.** A +mentor holds a single Raspberry Pi account that signs them in to *both* Code +Club Projects and Code Classroom, so leaving and coming back costs them +nothing. A club member holding only a mentor-made classroom account cannot +follow that route at all. So "out to Projects and back" is available to mentors +precisely because the identity problem is a young person's problem. That +asymmetry is the strongest argument for this flow and it is worth saying out +loud in any writeup. + +## What is proposed, and what is real + +Real screens, unchanged: `EducatorClassPage`, `EducatorProjectPage`, +`ProjectPage`, `ClassroomProjectEditor`. + +Proposed — nothing like these exists: + +- `AddProjectChoice.tsx` — **one dialog with two steps**, over the class page. + Step 1 asks find-or-create. Answer "create your own" and the *same* dialog + becomes the live **"Create a new project"** dialog — project name, "what kind + of project do you want to make for your students?", and the Blocks / Python / + Web rows with the real descriptions and tile colours. + + It started as two dialogs, the fork opening the create dialog on top of it. + That was worth fixing and not just cosmetically: the stacking was a symptom + of the fork **wrapping a dialog the product already has**, so the fix was to + make it the first step *of* that dialog rather than a box in front of it. + + Two things that buys. Step 2 is now the genuine screen rather than a page + invented to stand in for it, so the branch this prototype tests *against* is + real. And "Back" means something — it returns to the question instead of + dismissing one dialog to reveal another. + + Keeping the class visible behind it also means adding a project reads as + something happening *to* this class rather than a place you go, and "back" + out of the find branch returns to the class with the dialog open again. + + **Two deliberate departures from the live dialog.** It preselects "Blocks"; + step 1 preselects nothing, because preselecting would answer the question the + prototype is asking. And because the design system's modal takes no + `disabled` for its footer buttons, "Continue" with nothing chosen asks for a + choice rather than doing nothing. + + Kept from the live dialog because it is a nice touch: the project name + arrives as "Blocks project" and follows the selected type until the mentor + types their own, after which it is theirs and stops moving. + + Worth knowing that select-then-confirm is not just fidelity — it is the only + arrangement that does not bias the result. Two footer actions would make one + option the primary button and the other the secondary, and which option a + mentor picks is the finding. + + Tiles are flat colour with no glyph, as in `ManageClub`. The live dialog has + small icons in them; there is no icon component in the kit for arbitrary + tiles, and grey-boxing keeps a session about the flow rather than the artwork. + + **The alternative worth building as its own prototype:** no fork dialog at + all. Two buttons in the Projects card header — "Add project" opening the real + create dialog untouched, and "Find a Code Club project" beside it. That tests + a different question. Here the import route is hidden behind "Add project", so + a session shows whether mentors *pick* it once they are looking at it; with + two buttons it shows whether they *notice* it, which is closer to the + hypothesis. Rejected for this prototype rather than for good. +- `ProjectsCatalogue.tsx` — the filtered catalogue, built to match the live + Code Club Projects category pages (`/en/technology/scratch`): green hero + panel with a Back pill, big category title and a line of description, then a + results count, a filter rail and a grid of cards — image band, "language - + Level n" eyebrow, linked title, description. + + **The category itself is the proposal, and it is a different kind of category + from the ones on the live site.** "Scratch" and "Python" describe what a + project teaches. "Code Classroom compatible" describes what a mentor can + *do* with it. Nothing on Code Club Projects is organised that way today, and + it is worth asking whether it should be: a category that exists because of a + technical constraint in another product is a strange thing to put in front of + people who are browsing for something to teach. + + The live page's "Start a path" strip is deliberately left out — paths are a + young person's route through a subject over weeks, not what a mentor is doing + here, and the fixtures have no paths. The filter rail keeps only difficulty + level, the one filter the fixtures can honour; interest and hardware are on + the real page and are not faked here. +- `AddToClass.tsx` — picking the class, and the success card. Built to follow + the equivalent screens in **Experience CS**, the other product that + integrates with Code Classroom and so the nearest thing to a precedent for + how a content product hands work to a class: one small centred card, a + sentence on what a class is for before asking which one, the class as a + single select labelled "School class" rather than a button per class, one + primary action, and a success card of the same shape with a tick and one + sentence naming what went where. + + Changed rather than copied: "unit" is Experience CS's word for a group of + lessons, so this says project; Experience CS offers no way to create a class + here and the chart's diamond does, so "Create a new class instead" sits under + the select as a link rather than competing with the primary button; and + Experience CS's mint page is its own brand colour, so these sit on Code Club + Projects' own page colour instead of importing another product's — the card + is told apart by its border rather than by a tint. +- all of `NewClass.tsx`. +- `ShowToStudents.tsx` — and it contradicts the live product. See finding 2. +- `ProjectPage`'s "Add to a class" button — the shared screen takes an + `onImport` prop for exactly this, and passing it is a proposal. + +`MentorSchoolHome` exists in the real product but has never been built in +`screens/` and **nobody has checked it against the real educator view.** Treat +the details as wrong. + +Every screen above lives in this folder. **One change does not**: page width. +`--surface-measure` in `src/surfaces/tokens.css` and the rules using it in +`src/styles/global.css` cap a product page at 1100px, because screens read +badly stretched across a large monitor. Divya asked for it and it applies to +every prototype in the repo, not just this one — so it wants reviewing and +merging separately from this prototype rather than riding along with it. + +## Five things the build exposed + +Not design opinions — things that fell out of making the chart actually run. + +**1. "View project code" comes before there is anything to view.** The chart +orders the create branch: name it → project page → view project code → add +instructions. So at "view project code" the project has no instructions, and +the Code Classroom editor renders `project.steps[stepIndex]` with no empty +state — a project with no steps crashes it. The prototype puts a "No +instructions yet" step in rather than a fake first step, so the gap is visible +on screen. Either the chart's two boxes want swapping, or creating a project +needs to ask for instructions up front — and now that naming happens in the +live "Create a new project" dialog, that dialog is where such a question would +have to go. Which makes it a change to a shipped screen, not a new one. + +**2. "Show to students" inverts the live product.** The chart ends both +branches at `[Show to students]`, with the sticky asking whether mentors +understand a project is hidden by default. But the real project page offers +**"Hide from students"** — which means a project is visible the moment it +exists. One of the two has to give. Both defaults are defensible: hidden-by- +default lets a mentor set up a session in advance; visible-by-default is one +less step to forget. Worth deciding before this is engineered, because it is a +behaviour change to a shipped product, not a new screen. + +The prototype implements the chart's version (hidden by default) so the sticky's +question can actually be tested. Note the trap it creates: after importing, the +project page still only offers "Hide from students" — a mentor looking for +"show to students" will not find it where they landed. + +**3. Nothing in the fixtures is now excluded, so the sticky's question is no +longer visible.** "CCP page filtered by embedded editor projects" covers +Scratch, Python and HTML — settled by Divya, September 2026. An earlier version +of this prototype guessed Python and HTML only, which dropped the Scratch +projects and so dropped most of what a club actually runs. That was wrong, and +the category now shows all four fixtures. + +The consequence is worth stating plainly: `Project['language']` has exactly +those three values, so **the filter can no longer exclude anything**, and the +pink sticky's question — "can we connect projects that aren't editor +projects?" — cannot be seen in this flow any more. It is still a live question. +The real catalogue has physical computing projects, third-party editor +projects and PDF-only projects that would not import. Testing what a mentor +does when the thing they want is not importable needs a non-editor project in +`src/fixtures`, which is shared and not this prototype's to change. + +The code for the "some projects are not here" message is kept and marked +unreachable, so it starts working the moment such a fixture exists. **Do not +read its absence as evidence that everything imports.** + +Which also leaves a question about the category itself: if almost everything +qualifies, what is a "Code Classroom compatible" category actually for? A +filter that excludes nothing is a filter nobody needs. + +**4. The product boundary moved, and the chart is now wrong about where it +sits.** The chart puts "choose new or existing class" and everything after it +in the Code Classroom lane, so clicking "Add to a class" on Code Club Projects +carried the mentor across a product boundary mid-task, before they had +finished. Experience CS — the other product that feeds Code Classroom — does +the whole interaction on **its own side** and crosses only at "View your +class". + +**Divya's call, September 2026: follow Experience CS.** So the whole +add-to-class interaction now happens on Code Club Projects — choosing the +class, creating one, and the success card — and the single crossing into Code +Classroom is the "View your class" button, which the mentor has to press. The +flow therefore has exactly one product change in it, and it is one they asked +for. **Sarah's chart still shows the old boundary and wants updating to +match.** + +Two things this buys, and one it costs. It means a mentor never loses the +catalogue they were browsing, so "Find another project" is a cheap loop rather +than a return trip. It also means the chrome never changes underneath them — +which is why these steps do NOT use the Surface's own `layout="centred"`: that +mode suppresses the product's navigation, and Code Club Projects' nav +disappearing halfway through would make it look like they had left after all. + +What it costs: creating a class from here is now a **write into another +product** from inside Code Club Projects. Picking an existing class is only a +read. That is a much bigger ask of engineering than the rest of this flow, and +it is the one part of the new boundary worth pushing on before anyone commits. + +**5. "Success" succeeds at less than it says.** On the new-class branch the +project lands in a class with nobody in it. The mentor still has to go and add +young people, and the flow never mentions it. The screen says so rather than +claiming victory. + +Two smaller ones: + +- The chart says the button is **"add to my club"**; the shared screen says + **"Add to a class"**. Code Club's word and Code Classroom's, on either side of + one click. Worth picking one. +- The board has two overlapping **"View project code"** boxes (the second has + no connectors attached, so it reads as a leftover), and the **"How do you want + to create your class"** diamond has only one branch drawn. The prototype keeps + the diamond as a single-option step rather than inventing a second answer — + five minutes with Sarah would settle it. + +## What I'd want to watch in testing + +- **Where a mentor goes first, before anything is on screen.** Ask them to find + a project for next week and watch which product they open. That is the + hypothesis, and it is answered before they touch a prototype screen. +- **Whether "Find a project" wins because mentors want it or because it is + first.** Neither option is preselected and neither is the primary button, so + reading order is the only bias left. If it wins every time, swap the two rows + round in a later session before believing the result. +- **Whether "Add to a class" survives "Start project".** The Code Club Projects + landing page drives at "Start project", repeated top and bottom. Watch whether + a mentor who came to import ends up starting the project instead. +- **The one crossing, and whether they take it.** Everything up to and + including "added to Thursday Scratch group" now happens on Code Club + Projects; "View your class" is the only button that leaves. Watch whether + anyone presses it at all, or whether "Find another project" wins and they + never go and look at what they built. If nobody ever crosses, the class page + is not where a mentor checks their work. +- **What they think the category means.** Everything in the fixtures qualifies, + so nothing on screen explains why this category exists. Ask at the end: "what + would you expect to find in a category called Code Classroom compatible, and + what would you expect to be missing?" If a mentor cannot answer, the category + is doing no work for them. +- **Whether they leave "Show to students" without pressing it** — then ask what + a young person can see right now. Do not ask whether they understood. +- **Whether the loop gets used.** "Find another project" is the only cheap way + to set up a term of sessions in one sitting. + +## What we learned + +Not tested yet. diff --git a/src/prototypes/import-mentor/out-to-projects-and-back/projectTypes.ts b/src/prototypes/import-mentor/out-to-projects-and-back/projectTypes.ts new file mode 100644 index 0000000..29186cd --- /dev/null +++ b/src/prototypes/import-mentor/out-to-projects-and-back/projectTypes.ts @@ -0,0 +1,115 @@ +import type { Project, ProjectStep } from '../../../fixtures' + +// Two decisions the flow chart implies but does not spell out, kept in one +// file so the flow cannot quietly disagree with itself and so changing either +// is a one-line change. + +/** + * "CCP page filtered by embedded editor projects" — the chart's own words for + * the catalogue the mentor browses. + * + * All three: Scratch as well as Python and HTML. Settled by Divya, September + * 2026, rather than guessed — an earlier version of this file left Scratch out + * on the assumption that "embedded editor" meant the Code Editor's own + * languages, which would have dropped most of what a Code Club runs. + * + * Worth knowing what this costs the prototype. `Project['language']` has + * exactly these three values, so the filter can no longer exclude anything in + * the fixtures — which means the question on the "can we connect projects that + * aren't editor projects?" sticky is no longer visible in this flow. It is + * still a real question: the live catalogue has physical computing and + * third-party-editor projects that would not import. Seeing that happen needs a + * non-editor project in `src/fixtures`, which is shared and not this + * prototype's to change. See notes.md. + */ +export const EMBEDDED_EDITOR_LANGUAGES: Array = [ + 'Scratch', + 'Python', + 'HTML', +] + +export function isEmbeddedEditorProject(project: Project): boolean { + return EMBEDDED_EDITOR_LANGUAGES.includes(project.language) +} + +/** + * "Name and project type e.g blocks" — the chart names one option, Blocks, so + * the other two are taken from what the Code Editor supports. + * + * Code Classroom says Blocks, Python and Web; Code Club Projects labels the + * same things Scratch, Python and HTML. The two products disagree, and a + * mentor who picks Blocks and then browses a list of things called Scratch has + * to work out they are the same. Mapped here so the mismatch is visible. + * + * Descriptions and tile colours are the live dialog's own, not invented. + */ +export const PROJECT_TYPES = [ + { + id: 'Blocks', + language: 'Scratch' as const, + tone: 'orange', + blurb: 'Based on the open-source Scratch editor', + }, + { + id: 'Python', + language: 'Python' as const, + tone: 'green', + blurb: 'Wide range of built-in libraries', + }, + { + id: 'Web', + language: 'HTML' as const, + tone: 'purple', + blurb: 'HTML, CSS, and JavaScript', + }, +] + +export type ProjectTypeId = (typeof PROJECT_TYPES)[number]['id'] + +export function projectType(id: ProjectTypeId) { + return PROJECT_TYPES.find((type) => type.id === id)! +} + +/** + * A project the mentor has named but not yet written instructions for. + * + * The step is a stand-in, and it is here because of a real gap. The chart + * sends the mentor to "View project code" BEFORE "Add instructions", so at + * that moment the project has no instructions at all — and the Code Classroom + * editor renders `project.steps[stepIndex]` with no empty state, so a project + * with no steps crashes it. Rather than hide that behind a fake first step, + * the stand-in says on screen what is missing. See notes.md, finding 1. + */ +const NO_INSTRUCTIONS_YET: ProjectStep = { + title: 'No instructions yet', + body: [ + 'You have not added instructions to this project.', + '', + 'Young people opening it will see this panel.', + ].join('\n'), +} + +export function createdProject(name: string, type: ProjectTypeId): Project { + return { + id: `created-${name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`, + title: name, + language: projectType(type).language, + level: 1, + ages: '', + intro: '', + steps: [NO_INSTRUCTIONS_YET], + starterCode: '', + } +} + +/** What "Add instructions" leaves behind, so the editor has something real. */ +export function withInstructions(project: Project, instructions: string): Project { + return { + ...project, + steps: [{ title: 'Step 1', body: instructions }], + } +} + +export function hasInstructions(project: Project): boolean { + return project.steps[0]?.title !== NO_INSTRUCTIONS_YET.title +} diff --git a/src/prototypes/import-mentor/out-to-projects-and-back/prototype.tsx b/src/prototypes/import-mentor/out-to-projects-and-back/prototype.tsx new file mode 100644 index 0000000..a1c8286 --- /dev/null +++ b/src/prototypes/import-mentor/out-to-projects-and-back/prototype.tsx @@ -0,0 +1,632 @@ +import { useState, type ReactNode } from 'react' +import { useSearchParams } from 'react-router-dom' +import { Alert, Button, Card } from '../../../kit' +import { + PROJECTS, + classesInSchool, + project, + school, + schoolsForMentor, + type ClassGroup, + type Project, +} from '../../../fixtures' +import { + ClassroomProjectEditor, + EducatorClassPage, + EducatorProjectPage, + ProjectPage, +} from '../../../screens' +import { Surface } from '../../../surfaces' +import { AddProjectChoice } from './AddProjectChoice' +import { AddInstructions } from './CreateInClassroom' +import { AddToClass, AddedToClass } from './AddToClass' +import { MentorSchoolHome } from './MentorSchoolHome' +import { HowToCreateClass, NameAndDescription } from './NewClass' +import { ProjectsCatalogue } from './ProjectsCatalogue' +import { ShowToStudents } from './ShowToStudents' +import { createdProject, hasInstructions, withInstructions } from './projectTypes' +import { meta } from './meta' + +// Everything comes from the one mentor named in meta.ts. Change the cast and +// the club, the classes and the school code all follow. Nothing below +// hardcodes Westlands. +// +// The pink sticky on the flow chart — "this assumes the mentor is logged in +// and has a school" — is satisfied by the cast rather than by a sign-in step, +// which is why `ownsSignIn` is not set: the workbench signs Thabo in before +// the flow renders, and the flow opens where the chart opens. +const MENTOR = meta.cast?.piAccount ?? '' +const CLUB = school(schoolsForMentor(MENTOR)[0].id)! +const CLASSES = classesInSchool(CLUB.id) + +/** + * What the mentor set up before today, so "Add project" sits in a class that + * is already running rather than an empty one. Not from the chart — the chart + * starts at a class page without saying what is in it. + */ +const ALREADY_THERE: Record = { + [CLASSES[0].id]: ['space-talk'], +} + +/** + * The narrow centred column the add-to-class steps sit in, like the Experience + * CS cards they follow. + * + * Done here rather than with the Surface's own `layout="centred"`, which looks + * right and is wrong for this: it suppresses the product's navigation, because + * the pages it was built for — Pi Accounts, the Code Classroom sign-in — do not + * have any. Code Club Projects does, and the whole point of keeping these steps + * on Projects is that the mentor does not appear to leave. Chrome vanishing + * halfway through would undo exactly what the change is for. + * + * 520px rather than that layout's 420px, measured rather than picked: the + * widest button row here is "Find another project" plus "View your class" at + * 241 + 8 + 190, and the card's own padding adds 48, so 488px is where they + * stop stacking. The rest is slack. The row still wraps on a narrow window, + * which is what should happen there. + */ +const CENTRED_COLUMN = { width: '100%', maxWidth: '520px', margin: '0 auto' } as const + +/** A project sitting in one of this mentor's classes. */ +interface Placed { + project: Project + classId: string + /** Hidden until the mentor says otherwise — see ShowToStudents.tsx. */ + shownToStudents: boolean +} + +// Neither the fork ("find a project or create your own") nor naming a new +// project is a step here. Both are steps INSIDE one dialog over the class page, +// so the class stays visible behind the question and nothing ever stacks — see +// AddProjectChoice.tsx. "Back" out of the find branch returns to the class with +// that dialog open again, rather than to a page of its own. +type Step = + // Code Classroom + | 'school' + | 'class' + // Create your own + | 'create-project-page' + | 'create-code' + | 'create-instructions' + // Find a project — Code Club Projects, then back + | 'browse' + | 'project-landing' + | 'choose-class' + | 'new-class-how' + | 'new-class-name' + | 'success' + // Both branches end here + | 'project-page' + | 'show-to-students' + +export default function OutToProjectsAndBack() { + const [params] = useSearchParams() + // Author commentary belongs to the team, not to the person being tested. + const showWorkbench = params.get('full') !== '1' + + const [step, setStep] = useState('school') + /** The "add a project" fork, open over the class page. */ + const [addOpen, setAddOpen] = useState(false) + + /** Back out of either branch: the class, with the fork open again. */ + function backToFork() { + setStep('class') + setAddOpen(true) + } + + // Classes can grow: the find branch can create one on the way through. + const [classes, setClasses] = useState(CLASSES) + const [placed, setPlaced] = useState(() => + Object.entries(ALREADY_THERE).flatMap(([classId, projectIds]) => + projectIds.map((id) => ({ project: project(id)!, classId, shownToStudents: true })), + ), + ) + + const [activeClassId, setActiveClassId] = useState(CLASSES[0].id) + const [activeProjectId, setActiveProjectId] = useState() + /** The project being looked at on Code Club Projects, before it is added. */ + const [viewingId, setViewingId] = useState() + const [newClassName, setNewClassName] = useState() + const [stepIndex, setStepIndex] = useState(0) + + const activeClass = classes.find((group) => group.id === activeClassId) ?? classes[0] + const activePlaced = placed.find( + (item) => item.project.id === activeProjectId && item.classId === activeClassId, + ) + const activeProject = activePlaced?.project + const viewing = viewingId ? project(viewingId) : undefined + + const inClass = (classId: string) => + placed.filter((item) => item.classId === classId).map((item) => item.project) + + /** Projects in a class that students cannot see yet — the crossed-out eye. */ + const hiddenIn = (classId: string) => + placed + .filter((item) => item.classId === classId && !item.shownToStudents) + .map((item) => item.project.id) + + function memberCount(classId: string) { + return classes.find((group) => group.id === classId)?.studentIds.length ?? 0 + } + + /** Put a project in a class and make it the one being looked at. */ + function place(item: Project, classId: string) { + setPlaced((current) => + current.some((p) => p.project.id === item.id && p.classId === classId) + ? current + : [...current, { project: item, classId, shownToStudents: false }], + ) + setActiveClassId(classId) + setActiveProjectId(item.id) + } + + /** Replace a placed project, for "Add instructions". */ + function update(item: Project) { + setPlaced((current) => + current.map((p) => + p.project.id === item.id && p.classId === activeClassId ? { ...p, project: item } : p, + ), + ) + } + + function show() { + setPlaced((current) => + current.map((p) => + p.project.id === activeProjectId && p.classId === activeClassId + ? { ...p, shownToStudents: true } + : p, + ), + ) + } + + /** Author commentary. Rendered only in the workbench. */ + function Note({ children }: { children: ReactNode }) { + if (!showWorkbench) return null + return ( + +

{children}

+
+ ) + } + + const crumbs = (...rest: string[]) => ['Your school', ...rest] + + /** + * Where a breadcrumb goes. Code Classroom has no other navigation, so this + * is the only way back up — and the trail here is always + * ['Your school', class, project, ...], so an index maps to a step. + */ + function goToCrumb(index: number) { + if (index === 0) setStep('school') + else if (index === 1) setStep('class') + else if (index === 2) setStep('project-page') + } + + // --- Code Classroom: where the chart starts -------------------------------- + + if (step === 'school') { + return ( + +
+ { + setActiveClassId(classId) + setStep('class') + }} + /> + + The chart's starting assumption, and the whole bet: {CLUB.name} already exists in Code + Classroom and the mentor is already in it. If that is true, Code Classroom is the hub + and this is where a mentor goes looking for a project. If a mentor would really open + Code Club Projects first, this prototype is answering the wrong question — and that is + the thing to watch for, before anyone touches a screen. + +
+
+ ) + } + + if (step === 'class') { + return ( + +
+ setAddOpen(true)} + onOpenProject={(projectId) => { + setActiveProjectId(projectId) + setStep('project-page') + }} + onCopyLink={() => {}} + onClassMembers={() => {}} + /> + + {/* The fork, over the class rather than instead of it. */} + { + setAddOpen(false) + setStep('browse') + }} + onCreate={(name, type) => { + setAddOpen(false) + place(createdProject(name, type), activeClass.id) + setStep('create-project-page') + }} + /> + + + The real class page, and everything on it except the dialog is real. Two things it + already says that this flow has to live with: projects "contain starter code created + by a teacher", so a project is the adult's to set up, and there is no sign anywhere + that Code Club Projects exists. +
+
+ The crossed-out eye is the live product's own marker for a project students cannot see + yet, and it is worth watching against finding 2 in notes.md: a project imported by + this flow arrives hidden, so it shows up here marked, in a list beside projects that + are live. Whether a mentor reads that icon as "not finished" or misses it entirely is + the cheapest test of the hidden-by-default question. +
+
+ "Add project" is the only way on, and what it opens is the proposal. Today it goes + straight to creating a project from nothing, with no question asked and no mention of + Code Club Projects. Watch which option a mentor reads first, whether "find a project" + reads as Code Club's own projects or as something vaguer, and whether anyone closes + the dialog to go and look at the class again before choosing. +
+
+
+ ) + } + + // --- Create your own ------------------------------------------------------ + + if (step === 'create-project-page' && activeProject) { + return ( + +
+ { + setStepIndex(0) + setStep('create-code') + }} + onHideFromStudents={() => setStep('show-to-students')} + onCopyLink={() => {}} + onOpenWork={() => {}} + /> + + The real project page, and it disagrees with the chart. It offers "Hide from students", + which means a project is visible as soon as it exists — the chart ends at "Show to + students", which means the opposite. One of the two has to give; see notes.md. + +
+
+ ) + } + + if (step === 'create-code' && activeProject) { + return ( + +
+ {}} + onSave={() => {}} + onUpload={() => {}} + onDownload={() => {}} + onBack={() => setStep('create-project-page')} + /> +
+
+ + {hasInstructions(activeProject) + ? 'The instructions the mentor wrote, in the panel a young person reads. Compare it with what an imported Code Club project arrives with.' + : 'The chart sends the mentor here BEFORE "Add instructions", so the instructions panel has nothing in it — the mentor is writing starter code into a shell. The panel says so out loud rather than showing an empty step, because the screen has no empty state to show. See notes.md, finding 1.'} + +
+
+ ) + } + + if (step === 'create-instructions' && activeProject) { + return ( + +
+ { + update(withInstructions(activeProject, instructions)) + setStep('show-to-students') + }} + onBack={() => setStep('create-code')} + /> + + Where the two branches rejoin. The chart runs a long connector from here straight to + "Show to students", skipping the class page — so a mentor who creates their own project + never passes back through the class to check it landed. + +
+
+ ) + } + + // --- Find a project: out to Code Club Projects ----------------------------- + + if (step === 'browse') { + return ( + +
+ item.project.id)} + onView={(projectId) => { + setViewingId(projectId) + setStep('project-landing') + }} + onBack={backToFork} + /> + + A different product, and the chrome says so. Worth noticing what this crossing costs a + mentor and does not cost a young person: {CLUB.name}'s mentor signs in here with the + same Raspberry Pi account they use for Code Classroom, so leaving is free. A club + member holding only a classroom account cannot follow them — which is why this route + exists for mentors and not for the young people they are choosing on behalf of. + +
+
+ ) + } + + if (step === 'project-landing' && viewing) { + return ( + +
+ setStep('project-landing')} + onImport={() => setStep('choose-class')} + /> + + The real Code Club Projects landing page. "Add to my club" is PROPOSED — nothing like + it exists on the live site or in the designs — and it has to compete with "Start + project", which the page repeats top and bottom. Watch whether a mentor who came here + to import ends up starting the project instead. + +
+
+ ) + } + + // --- Adding to a class, all of it still on Code Club Projects ------------- + // + // A DELIBERATE DEPARTURE FROM THE CHART, decided by Divya. The chart puts + // "choose new or existing class" and everything after it in the Code + // Classroom lane, so clicking "Add to a class" on Projects crossed a product + // boundary mid-task, before the mentor had finished. Experience CS — the + // other product that feeds Code Classroom — does the whole interaction on + // its own side and crosses only at "View your class", and that is what the + // flow does now. + // + // So every step below stays on the `ccp` surface, and no step below carries + // breadcrumbs: breadcrumbs are Code Classroom's entire navigation model and + // Code Club Projects does not have them. + + if (step === 'choose-class' && viewing) { + return ( + +
+ { + place(viewing, classId) + setNewClassName(undefined) + setStep('success') + }} + onNew={() => setStep('new-class-how')} + onBack={() => setStep('project-landing')} + /> + + Still on Code Club Projects, which is the change: the mentor picks a Code Classroom + class without leaving the site they are browsing on. Following Experience CS, which + does the same job for the same Code Classroom. Watch whether anyone is surprised to + be choosing a class here — and whether "School class" means anything to a volunteer + whose club is not a school. + +
+
+ ) + } + + if (step === 'new-class-how') { + return ( + +
+ setStep('new-class-name')} + onBack={() => setStep('choose-class')} + /> + + A decision point with one option. The chart draws this diamond with a single branch + leaving it, so either there is a second way to create a class that the board does not + show, or the diamond should not be a diamond. Left as drawn rather than invented — + worth five minutes with Sarah before this goes in front of anyone. +
+
+ Worth noticing what it now claims, though: creating a Code Classroom class from + inside Code Club Projects. Picking an existing class from here is a read; making a new + one is a write into another product, and a bigger thing to ask for. +
+
+
+ ) + } + + if (step === 'new-class-name' && viewing) { + return ( + +
+ { + const group: ClassGroup = { + id: `class-new-${classes.length + 1}`, + schoolId: CLUB.id, + name, + kind: 'code-club', + studentIds: [], + } + setClasses((current) => [...current, group]) + setNewClassName(name) + place(viewing, group.id) + setStep('success') + }} + onBack={() => setStep('new-class-how')} + /> + + A class with nobody in it. The chart's next box is "Success", and it is worth asking + what succeeded: the project is in a class no young person can reach until the mentor + goes and adds them, which is a job this flow never mentions — and which now sits in a + different product from the one they are standing in. + +
+
+ ) + } + + if (step === 'success' && activeProject) { + return ( + +
+ setStep('class')} + onFindAnother={() => setStep('browse')} + /> + + The same card as the step before it, which is how Experience CS does it — same + heading, a tick, and one sentence saying what went where. +
+
+ This is the only screen in the flow that crosses products, and it makes the mentor ask + for it: "View your class" is the one button that leaves Code Club Projects. "Find + another project" keeps them here and loops back to the catalogue, which is the chart's + own loop and the thing a mentor setting up a term of sessions actually wants. Watch + which of the two they reach for — and whether they notice the product changed when + they do leave. +
+
+
+ ) + } + + // --- Both branches end here ----------------------------------------------- + + if (step === 'project-page' && activeProject) { + return ( + +
+ { + setStepIndex(0) + setStep('create-code') + }} + onHideFromStudents={() => setStep('show-to-students')} + onCopyLink={() => {}} + onOpenWork={() => {}} + /> + + {activePlaced?.shownToStudents + ? 'The imported project, in the class, visible. "Student work" is empty and stays empty until someone saves — a mentor cannot tell who has started and got stuck from who has not started.' + : 'The imported project is here and hidden. The only thing on this page about visibility is "Hide from students" — the opposite of what this flow just did to it. A mentor looking for "show to students" will not find it here.'} + +
+
+ ) + } + + if (step === 'show-to-students' && activeProject) { + return ( + +
+ setStep('class')} + onBack={() => setStep('project-page')} + /> + + The end of both branches, and the most uncertain screen here — it is in square brackets + on the chart for a reason. It inverts the live product: today a project is visible the + moment it exists. The sticky's question is the one to test, and it cannot be tested by + asking. Watch whether a mentor leaves this screen without pressing the button, then ask + them what a young person can see. + +
+
+ ) + } + + // Any step whose data went missing — only reachable by editing state by hand. + return ( + +
+ +

This step needs a project and there is not one.

+
+
+
+ ) +} From c76aa82a7f574b585206bdfd8da102d9e678792f Mon Sep 17 00:00:00 2001 From: Divya Mahadevan Date: Mon, 14 Sep 2026 17:39:52 +0100 Subject: [PATCH 6/8] Stop the catalogue's filter rail overflowing its own card .rpf-input-checkbox carries a hardcoded min-width of 240px. The shared .cc-columns layout fixes its rail at 260px, which leaves 212px inside a card's padding -- so every checkbox in the rail spilled past the card's edge, by 6px here. Swapped for a flex rail with a 19rem basis, which also still stacks on a narrow window without a media query an inline style cannot express. Noted in notes.md rather than fixed in the shared CSS: a wider rail variant, or a fix upstream in the design system, is somebody's call and not a prototype's. Co-Authored-By: Claude Opus 5 --- .../ProjectsCatalogue.tsx | 89 ++++++++++++------- .../out-to-projects-and-back/notes.md | 10 +++ 2 files changed, 66 insertions(+), 33 deletions(-) diff --git a/src/prototypes/import-mentor/out-to-projects-and-back/ProjectsCatalogue.tsx b/src/prototypes/import-mentor/out-to-projects-and-back/ProjectsCatalogue.tsx index 3829858..3aad71f 100644 --- a/src/prototypes/import-mentor/out-to-projects-and-back/ProjectsCatalogue.tsx +++ b/src/prototypes/import-mentor/out-to-projects-and-back/ProjectsCatalogue.tsx @@ -93,7 +93,13 @@ export function ProjectsCatalogue({ projects, alreadyAddedIds, onView, onBack }: onClick={onBack} /> -

+

Code Classroom compatible

@@ -109,35 +115,46 @@ export function ProjectsCatalogue({ projects, alreadyAddedIds, onView, onBack }: - {/* Filter rail beside the results, as on the live page. `cc-columns` is - the shared rail-plus-content layout EducatorProjectPage uses — a - fixed rail from 720px up, stacked below it. Reusing it rather than - hand-rolling a grid means this page stacks on a narrow window - without needing a media query an inline style cannot express. */} -

- -

Filter

-

- The project list automatically updates when you apply a filter. -

-
- - Difficulty level - - {levels.map((level) => ( - toggleLevel(level, event.target.checked)} - /> - ))} -
-
- -
+ {/* Filter rail beside the results, as on the live page. NOT the shared + `cc-columns` layout, which fixes the rail at 260px: the design + system's checkbox label carries a hardcoded min-width of 240px, and a + 260px card only has 212px inside its padding, so every checkbox in + the rail overflowed it. Flex with wrap rather than a grid, so the two + columns still stack on a narrow window without needing a media query + an inline style cannot express. */} +
+
+ +

Filter

+

+ The project list automatically updates when you apply a filter. +

+
+ + Difficulty level + + {levels.map((level) => ( + toggleLevel(level, event.target.checked)} + /> + ))} +
+
+
+ +

{visible.length} {visible.length === 1 ? 'result' : 'results'} @@ -150,8 +167,8 @@ export function ProjectsCatalogue({ projects, alreadyAddedIds, onView, onBack }: sticky is about. Do not read its absence as an answer. */} {hidden > 0 && (

- {hidden} more {hidden === 1 ? 'project is' : 'projects are'} not here, because - they do not run in an editor Code Classroom can embed. + {hidden} more {hidden === 1 ? 'project is' : 'projects are'} not here, because they + do not run in an editor Code Classroom can embed.

)}
@@ -173,7 +190,13 @@ export function ProjectsCatalogue({ projects, alreadyAddedIds, onView, onBack }: const added = alreadyAddedIds.includes(project.id) return ( -
+
{/* The live cards lead with an illustration that runs to the card's edges, so it has to be pulled back out through the card's own padding and its top corners diff --git a/src/prototypes/import-mentor/out-to-projects-and-back/notes.md b/src/prototypes/import-mentor/out-to-projects-and-back/notes.md index 11538da..9d603b9 100644 --- a/src/prototypes/import-mentor/out-to-projects-and-back/notes.md +++ b/src/prototypes/import-mentor/out-to-projects-and-back/notes.md @@ -142,6 +142,16 @@ badly stretched across a large monitor. Divya asked for it and it applies to every prototype in the repo, not just this one — so it wants reviewing and merging separately from this prototype rather than riding along with it. +## A design system constraint worth knowing + +`.rpf-input-checkbox` carries a hardcoded `min-width: 240px`. The shared +`.cc-columns` layout fixes its rail at 260px, which leaves 212px inside a +card's padding — so **any filter rail built with `cc-columns` and checkboxes +overflows its own card.** The catalogue's rail here used to, by 6px. It now +uses a flex rail with a 19rem basis instead, which also still stacks on a +narrow window. The sibling prototype hit the same thing. Worth raising +upstream, or worth a wider rail variant in the shared CSS. + ## Five things the build exposed Not design opinions — things that fell out of making the chart actually run. From cdb33e595fd0f6182ca4db11c02bbb5ea936ed86 Mon Sep 17 00:00:00 2001 From: Divya Mahadevan Date: Mon, 14 Sep 2026 17:40:09 +0100 Subject: [PATCH 7/8] Add import-mentor prototype: start on Projects, not Classroom The mirror of out-to-projects-and-back, and the pair is the point. Both answer "how does a mentor get a Code Club project into Code Classroom?" and they disagree about exactly one thing: where the mentor starts. Sarah Tucker's bet here is that Code Club Projects is the site a mentor already knows, so the route has to begin in the catalogue rather than in Code Classroom. Same mentor, same club, same classes, same add-to-class cards, same product boundary -- so a session reacts to the starting point and not the furniture. Built to Sarah's "Option 2" flow chart, with three departures, all argued in notes.md: - The first screen is the live project selector, not a category page. That collapses three of the chart's boxes into one screen and is the chart read correctly: on the real site "Scratch" is a checkbox in the Technology group of the selector's rail, right next to where the new filter goes. It also makes Sarah's own assumption -- "mentors will find a project based on technology over interest" -- observable, because the rail puts Interest and Technology side by side. A category page assumed that answer. - The flow starts SIGNED OUT, which produced the sharpest findings in either prototype. A mentor browsing Projects has usually not logged in. So the site cannot call out a club it does not know about, which means the call-out Sarah's sticky asks for is only possible after the step most likely to lose them; "Add to a class" is offered anyway with the log-in coming after the intent; and signing in adds Pi Accounts as a third product to the crossing. - Compatibility is a filter, not a category. A category defined by a constraint in another product sits oddly beside categories defined by what they teach, and one that excludes almost nothing does no work. Two tags were built and removed: "Works with Code Classroom", which would sit on nearly every card in a catalogue shared with young people, and "In a class", which is a fact about one mentor and belongs on the project page where they are deciding. The inverted idea -- telling a mentor "this will not work in a class" at the moment they try to add it -- is not built and is probably the right one. Known gap before this is testable: facets.ts invents which projects are Code Classroom compatible, because src/fixtures has no such field and fixtures are shared. Nothing about the real Rock Band says it would not import. The filter's central mechanic needs a usableInClassroom field and a project or two that genuinely is not. Nothing here ships and nothing is a commitment to build. Co-Authored-By: Claude Opus 5 --- .../start-on-projects/AddToClass.tsx | 183 ++++++ .../start-on-projects/NewClass.tsx | 107 ++++ .../start-on-projects/ProjectSelector.tsx | 318 ++++++++++ .../start-on-projects/ShowToStudents.tsx | 89 +++ .../import-mentor/start-on-projects/facets.ts | 74 +++ .../import-mentor/start-on-projects/meta.ts | 24 + .../import-mentor/start-on-projects/notes.md | 228 +++++++ .../start-on-projects/prototype.tsx | 565 ++++++++++++++++++ 8 files changed, 1588 insertions(+) create mode 100644 src/prototypes/import-mentor/start-on-projects/AddToClass.tsx create mode 100644 src/prototypes/import-mentor/start-on-projects/NewClass.tsx create mode 100644 src/prototypes/import-mentor/start-on-projects/ProjectSelector.tsx create mode 100644 src/prototypes/import-mentor/start-on-projects/ShowToStudents.tsx create mode 100644 src/prototypes/import-mentor/start-on-projects/facets.ts create mode 100644 src/prototypes/import-mentor/start-on-projects/meta.ts create mode 100644 src/prototypes/import-mentor/start-on-projects/notes.md create mode 100644 src/prototypes/import-mentor/start-on-projects/prototype.tsx diff --git a/src/prototypes/import-mentor/start-on-projects/AddToClass.tsx b/src/prototypes/import-mentor/start-on-projects/AddToClass.tsx new file mode 100644 index 0000000..fcfc213 --- /dev/null +++ b/src/prototypes/import-mentor/start-on-projects/AddToClass.tsx @@ -0,0 +1,183 @@ +import { useState } from 'react' +import { Button, Card, SelectInput } from '../../../kit' +import type { ClassGroup } from '../../../fixtures' + +// PROPOSED. "Choose new or existing class" from the flow chart, reworked to +// follow the equivalent screens in Experience CS — the other product that +// integrates with Code Classroom, and so the closest thing to a precedent we +// have for how a content product hands a piece of work to a class. +// +// Both cards render on the Code Club Projects surface. The mentor picks a Code +// Classroom class without leaving the site they were browsing on, and the flow +// crosses products only when they press "View your class". That follows +// Experience CS rather than the chart, which had put this step in the Code +// Classroom lane; see prototype.tsx and notes.md. +// +// Taken from those screens: +// +// - one small centred card on a tinted page, not a full-width page +// - a heading naming the thing being added: "Add Weather watchers to class" +// - a sentence explaining what a class is FOR, before asking which one +// - the class as a single select labelled "School class", not a list of +// buttons — so picking a class is one decision, not one decision per class +// - one primary action, bottom right of the card +// - a success card of the same shape: same heading, a tick, one sentence +// naming what went where, then where to go next +// +// Changed for Code Club Projects rather than copied: +// +// - "unit" is Experience CS's word for a group of lessons. Code Club Projects +// has projects, so this says project. +// - Experience CS offers no way to create a class here; the chart's diamond +// does, so "Create a new class" sits beside the primary action. Without it +// this screen is a dead end for a mentor whose club has no class yet. +// - Experience CS's mint page is its own brand colour. These sit on Code Club +// Projects' own page colour rather than importing another product's, so the +// card is told apart by its border rather than by a tint. +// +// Deliberately NOT badged "proposed" on screen; see screens/types.ts. + +/** "Add to class" — the select screen. */ +export function AddToClass({ + projectTitle, + classes, + onAdd, + onNew, + onBack, +}: { + projectTitle: string + classes: ClassGroup[] + onAdd: (classId: string) => void + onNew: () => void + onBack: () => void +}) { + const [classId, setClassId] = useState(classes[0]?.id ?? '') + + return ( + +

Add {projectTitle} to class

+ +

+ Classes let you group students and assign projects to them. After adding this project to a + class, students will be able to work on it. +

+ + {classes.length === 0 ? ( + // Reachable: a club whose Code Classroom school has no class yet. The + // select has nothing to offer, so the only way on is to make one. +

+ You have no classes yet. Create one and this project will be added to it. +

+ ) : ( +
+ ({ key: group.id, value: group.name }))} + value={classId} + onChange={(event) => setClassId(event.target.value)} + /> + {/* Sits under the field it is about rather than in the button row. + Three buttons do not fit a 420px card, and more importantly the + Experience CS card drives at ONE action — a second button of + equal weight would undo that. */} + +
+ )} + +
+
+
+ ) +} + +/** The success card. Experience CS keeps the same heading and swaps the body. */ +export function AddedToClass({ + projectTitle, + className, + isNewClass, + onViewClass, + onFindAnother, +}: { + projectTitle: string + className: string + isNewClass: boolean + onViewClass: () => void + onFindAnother: () => void +}) { + return ( + +

Add {projectTitle} to class

+ + {/* Experience CS uses a circled tick here. There is no icon component in + the kit, so this is drawn from a border and a character, and hidden + from screen readers — the sentence below it carries the meaning. */} + + +

+ {projectTitle} has been added to {className}. +

+ +

+ You can manage this project anytime from the class project list on your Code Classroom + dashboard. +

+ + {isNewClass && ( + // Not in the Experience CS screens, and it matters here: the chart's + // new-class branch lands the project in a class with nobody in it. +

+ Nobody is in {className} yet, so nobody can see it. Add young people to the class from + your Code Classroom dashboard. +

+ )} + +
+ {/* Experience CS's secondary here is "View unit" — the thing you just + added. This keeps the chart's loop instead, which is the action a + mentor setting up a term of sessions actually wants. */} +
+
+ ) +} diff --git a/src/prototypes/import-mentor/start-on-projects/NewClass.tsx b/src/prototypes/import-mentor/start-on-projects/NewClass.tsx new file mode 100644 index 0000000..e7a1b88 --- /dev/null +++ b/src/prototypes/import-mentor/start-on-projects/NewClass.tsx @@ -0,0 +1,107 @@ +import { useState } from 'react' +import { Button, Card, TextInput, TextareaInput } from '../../../kit' + +// PROPOSED. The chart's "create a new class" branch, reached while adding a +// project — so a mentor is setting up a class as a side effect of importing. +// +// The diamond before this one ("How do you want to create your class") has +// only ONE branch drawn on the board, to "Name + description". Kept as its own +// step so the flow matches the chart, with the missing branch called out in +// the workbench commentary rather than invented here. See notes.md. + +/** The diamond: "How do you want to create your class". */ +export function HowToCreateClass({ + onNameIt, + onBack, +}: { + onNameIt: () => void + onBack: () => void +}) { + return ( +
+
+
+ +

How do you want to create your class?

+ + +

Name it yourself

+

+ Give the class a name and a description, and add young people afterwards. +

+
+ ) +} + +/** "Name + description". */ +export function NameAndDescription({ + projectTitle, + onCreate, + onBack, +}: { + projectTitle: string + onCreate: (name: string, description: string) => void + onBack: () => void +}) { + const [name, setName] = useState('') + const [description, setDescription] = useState('') + const [error, setError] = useState() + + return ( +
+
+
+ +
+

Name your class

+

{projectTitle} will be added to it.

+
+ + + { + setName(event.target.value) + setError(undefined) + }} + /> + + + + setDescription(event.target.value)} + /> + + +
+
+
+ ) +} diff --git a/src/prototypes/import-mentor/start-on-projects/ProjectSelector.tsx b/src/prototypes/import-mentor/start-on-projects/ProjectSelector.tsx new file mode 100644 index 0000000..01b69f6 --- /dev/null +++ b/src/prototypes/import-mentor/start-on-projects/ProjectSelector.tsx @@ -0,0 +1,318 @@ +import { useState } from 'react' +import { Card, CheckboxInput, Placeholder, SearchInput, Tag } from '../../../kit' +import type { Project } from '../../../fixtures' +import { FILTER_LABEL, interestOf, usableInClassroom } from './facets' + +// The first page, and on the live site it is the project selector: +// projects.raspberrypi.org/en/projects — a green "Find a project" band with a +// search box, one filter rail, "Showing N projects", then a grid of cards. +// +// It collapses two of the chart's boxes into one screen, and that is the +// chart read correctly rather than a shortcut. "Code Club Projects" then +// "Scratch" then "Filter by 'can be used in my code classroom'" are not three +// pages: on the real site Scratch is a checkbox in the Technology group of +// this rail, right next to where the new filter would go. A mentor arrives, +// ticks Scratch, ticks the Code Classroom filter, and is looking at their +// shortlist — all here. +// +// That is also better for the question Sarah wants answered. Her assumption on +// the chart is *"mentors will find a project based on technology over +// interest"*, and the live rail puts Interest and Technology side by side. On +// a category page you cannot see that choice being made; here you can watch +// which group they reach for first. +// +// THE PROPOSAL IS ONE CHECKBOX. The page around it is the live page. +// +// Sarah's sticky floated a second idea — *"we could also have a tag design or +// a way to differentiate which cards are compatible with Code Classroom"* — +// and it was built and then removed. Three reasons, in order of weight: +// +// - It would sit on nearly every card. A badge on the majority carries no +// information; if anything is worth marking it is the exceptions. +// - THIS PAGE IS SHARED WITH YOUNG PEOPLE. The selector has no idea who is +// looking, and "Works with Code Classroom" means nothing to a nine-year-old +// browsing for something to make. +// - At the grid's narrowest column the tag was wider than the card's content +// box, so it dragged the card's text out past its own edges. +// +// The mentor who never opens the filter rail is still covered: the project +// page carries a banner naming their club. See notes.md for the inverted +// version of this idea, which is probably the right one. +// +// Only filter groups the fixtures can actually honour are here. The live rail +// also has PDF only and Hardware; faking them would put controls in a testing +// session that do nothing. Interest works, but only because facets.ts invents +// it — see the warning there. + +/** The live band colour, sampled from the page. No green in the token set. */ +const HERO_GREEN = 'rgb(65, 180, 82)' + +export interface Filters { + query: string + classroomOnly: boolean + levels: number[] + interests: string[] + technologies: string[] +} + +interface Props { + projects: Project[] + filters: Filters + onFiltersChange: (next: Filters) => void + onView: (projectId: string) => void +} + +function toggle(list: T[], value: T, on: boolean) { + return on ? [...list, value] : list.filter((item) => item !== value) +} + +export function ProjectSelector({ + projects, + filters, + onFiltersChange, + onView, +}: Props) { + const set = (patch: Partial) => onFiltersChange({ ...filters, ...patch }) + + // The design system's SearchInput is uncontrolled, so its value has to come + // in as `defaultValue` — and wiring that straight to live state is a trap: + // React re-assigns the DOM value when `defaultValue` changes, which can + // fight whatever the person is typing. Captured once here instead, so it + // never changes while mounted. Returning from a successful import remounts + // the screen, which is when the stored query gets picked up again. + const [initialQuery] = useState(filters.query) + + const levels = [...new Set(projects.map((p) => p.level))].sort() + const technologies = [...new Set(projects.map((p) => p.language))].sort() + const interests = [...new Set(projects.map(interestOf).filter(Boolean) as string[])].sort() + + const query = filters.query.trim().toLowerCase() + const visible = projects.filter((p) => { + if (filters.classroomOnly && !usableInClassroom(p)) return false + if (filters.levels.length > 0 && !filters.levels.includes(p.level)) return false + if (filters.technologies.length > 0 && !filters.technologies.includes(p.language)) return false + const interest = interestOf(p) + if (filters.interests.length > 0 && (!interest || !filters.interests.includes(interest))) + return false + if (query && !`${p.title} ${p.intro}`.toLowerCase().includes(query)) return false + return true + }) + + return ( +
+ {/* The green band. 24px radius and 40px of padding are the live values; + the token set has neither, and neither is a colour or a font size. */} +
+

Find a project

+
+ {/* Requires a visible label, so this carries one where the live + page has none — an accessibility gain rather than a fidelity + loss. See `initialQuery` above for why the value is not wired + straight to state. */} + set({ query: event.target.value })} + onClick={() => {}} + /> +
+
+ + {/* Not the shared `cc-columns` rail-and-content layout, which fixes the + rail at 260px. The design system's checkbox label carries a hardcoded + min-width of 240px, and a 260px card only has 212px inside its + padding — so every checkbox in the rail overflowed it. Flex with wrap + rather than a grid, so the two columns still stack on a narrow window + without needing a media query an inline style cannot express. */} +
+
+ +

Filter

+

+ The project list automatically updates when you apply a filter. +

+ + {/* The proposal. Ungrouped, above the first group heading, which is + exactly where the live rail puts its standalone "PDF only" + checkbox — so it needs no legend of its own, and a legend reading + "Code Classroom" above a label saying the same thing was only + ever repetition. + + Where it sits is still worth arguing about: first makes it + findable, and also makes it the first thing a mentor has to + understand about a page they came to for something else. */} + set({ classroomOnly: event.target.checked })} + /> + +
+ + Difficulty level + + {levels.map((level) => ( + + set({ + levels: toggle(filters.levels, level, event.target.checked), + }) + } + /> + ))} +
+ +
+ + Interest + + {interests.map((interest) => ( + + set({ + interests: toggle(filters.interests, interest, event.target.checked), + }) + } + /> + ))} +
+ + {/* Where the chart's "Scratch" box actually lives. */} +
+ + Technology + + {technologies.map((tech) => ( + + set({ + technologies: toggle(filters.technologies, tech, event.target.checked), + }) + } + /> + ))} +
+
+
+ +
+

+ Showing {visible.length} {visible.length === 1 ? 'project' : 'projects'} +

+ + {visible.length === 0 ? ( + +

No projects match those filters

+
+ ) : ( +
+ {visible.map((project) => { + const interest = interestOf(project) + return ( + +
+ {/* The live cards lead with an illustration running to + the card's edges, so it is pulled back out through + the card's own padding. */} +
+ +
+ + + {project.language} - Level {project.level} + + + {/* On the live page the title is the link, not the card + and not a button. */} + + +

+ {project.intro.split('\n')[0].replace(/\*\*/g, '')} +

+ + {/* Only the topic tag, as on the live cards. Whether a + project is already in one of the mentor's classes is + deliberately NOT here: a card in a catalogue shared + with young people is the wrong place for a fact about + one mentor's classes, and "already added" only + matters once you are looking at the project. It is on + the project page instead. */} + {interest && ( +
+ +
+ )} +
+
+ ) + })} +
+ )} +
+
+
+ ) +} diff --git a/src/prototypes/import-mentor/start-on-projects/ShowToStudents.tsx b/src/prototypes/import-mentor/start-on-projects/ShowToStudents.tsx new file mode 100644 index 0000000..f9c4de4 --- /dev/null +++ b/src/prototypes/import-mentor/start-on-projects/ShowToStudents.tsx @@ -0,0 +1,89 @@ +import { Alert, Button, Card } from '../../../kit' + +// PROPOSED, and it contradicts the live product — which is why it is worth +// building rather than arguing about. +// +// The chart ends both branches at "[Show to students]", in square brackets, +// with a sticky asking whether mentors understand the project is hidden by +// default. But the real Code Classroom project page (screens/EducatorProjectPage) +// offers "Hide from students", which means a project is VISIBLE the moment it +// exists. So this screen inverts today's behaviour: nothing is shown until the +// mentor says so. +// +// Both readings are defensible. Hidden-by-default lets a mentor set a session +// up in advance without young people wandering into a half-built project; +// visible-by-default means one less step and nothing to forget. The cost of +// hidden-by-default is the whole reason for the sticky: a mentor who does not +// realise has a room of young people who cannot see the thing they came for. +// +// The "Tool tip" sticky is realised as the hint below the toggle. Whether a +// tooltip is enough to carry a default nobody expects is the thing to watch. + +interface Props { + projectTitle: string + className: string + memberCount: number + shown: boolean + onShow: () => void + onDone: () => void + onBack: () => void +} + +export function ShowToStudents({ + projectTitle, + className, + memberCount, + shown, + onShow, + onDone, + onBack, +}: Props) { + return ( +
+
+
+ +
+

{projectTitle}

+

In {className}

+
+ + {shown ? ( + +

+ {memberCount === 0 + ? `Nobody is in ${className} yet, so there is still nobody to see it.` + : `All ${memberCount} young people in ${className} can open it now.`} +

+
+ ) : ( + +

+ You are the only person who can see this project. It stays hidden until you show it. +

+
+ )} + + +

Show to students

+ {/* The "Tool tip" sticky, as copy. A default nobody expects is being + carried by one line of hint text — that is the thing to test. */} +

+ {shown + ? `${projectTitle} is showing to everyone in ${className}. You can hide it again from the project page.` + : `New projects are hidden so you can set them up before a session. Young people will not see ${projectTitle} in ${className} until you show it to them.`} +

+
+
+
+
+ ) +} diff --git a/src/prototypes/import-mentor/start-on-projects/facets.ts b/src/prototypes/import-mentor/start-on-projects/facets.ts new file mode 100644 index 0000000..a708532 --- /dev/null +++ b/src/prototypes/import-mentor/start-on-projects/facets.ts @@ -0,0 +1,74 @@ +import type { Project } from '../../../fixtures' + +// What the project selector filters on. Two of these are properties the +// fixtures do not have, so they are stood in for here — read the warnings. + +/** + * Which projects can be used inside Code Classroom. **PROPOSED, and invented.** + * + * Sarah's flow chart turns on a filter called "can be used in my code + * classroom", and her sticky says why it should exist: *"Should we only allow + * them to add embedded editor projects? Think yes — we know that having + * separate tabs is pain for younger users."* + * + * So compatibility is a property of a PROJECT, not of a language: on the live + * site a Scratch project that drives a Raspberry Pi, or one that only ships as + * a PDF, will not run inside a class however Scratch it is. `src/fixtures` has + * no such field, and fixtures are shared and not this prototype's to change. + * + * **The assignment is invented.** One Scratch project is marked incompatible + * purely so the filter and the compatibility tag have visible work to do; + * without it every fixture qualifies, the filter removes nothing, and the + * central mechanic of this flow cannot be tested. Nothing about the real Rock + * Band project says it would not import. The honest fix is a + * `usableInClassroom` field in the fixtures. + */ +const NOT_COMPATIBLE = new Set(['rock-band']) + +export function usableInClassroom(project: Project): boolean { + return !NOT_COMPATIBLE.has(project.id) +} + +/** + * What the filter is called. Sarah's sticky says the wording is TBD, so this + * is a proposal rather than a decision. + * + * Built to match the live rail's own idiom: it already carries a standalone + * "PDF only" checkbox above the first group heading, so an "X only" filter is + * a shape a mentor has seen on this page before. Short enough for one line in + * the rail, which "Can be used in my Code Classroom" was not. + * + * Dropping "my" is deliberate and not only about length. Compatibility is a + * property of the PROJECT — whether it runs in an embedded editor — and has + * nothing to do with which classroom happens to be yours. "My" implied a + * personalised filter that was never personalised. + * + * Alternatives, if this one tests badly: "Works in Code Classroom" is clearer + * and still fits; "Can be added to a class" describes what the mentor gets to + * do rather than what the project is. + */ +export const FILTER_LABEL = 'Code Classroom only' + +/** + * What a project is *about*. **Also invented**, and for a specific reason. + * + * Sarah's testing assumption on the chart is that *"mentors will find a + * project based on technology over interest"*. The live selector puts Interest + * and Technology side by side in one rail, which makes that assumption + * watchable — you can see which group a mentor reaches for first. + * + * The fixtures carry no interest, so it could not be tested at all without + * this. Values are taken from the live page's own Interest list; which project + * got which is a guess, and a harmless one — nothing in the flow branches on + * it. Unlike the compatibility map above, being wrong here costs nothing. + */ +const INTERESTS: Record = { + 'space-talk': 'Space', + 'rock-band': 'Music', + chatbot: 'Communication', + 'find-the-bug': 'Games', +} + +export function interestOf(project: Project): string | undefined { + return INTERESTS[project.id] +} diff --git a/src/prototypes/import-mentor/start-on-projects/meta.ts b/src/prototypes/import-mentor/start-on-projects/meta.ts new file mode 100644 index 0000000..46a8a0f --- /dev/null +++ b/src/prototypes/import-mentor/start-on-projects/meta.ts @@ -0,0 +1,24 @@ +import type { PrototypeMeta } from '../../types' + +export const meta: PrototypeMeta = { + title: 'Start on Projects, not Classroom', + + owner: 'Sarah Tucker', + + hypothesis: + 'Mentors are more likely to start on Code Club Projects, because it is the site they already know — so the route into Code Classroom should begin in the catalogue, with a filter for what will actually work in a class, rather than beginning in Code Classroom and sending them out.', + + status: 'sketch', + + // The SAME mentor as out-to-projects-and-back, on purpose. These two + // prototypes exist to be compared, and they should differ in exactly one + // thing: where the mentor starts. Same person, same club, same two classes, + // so anything a session turns up is about the route and not about the cast. + cast: { piAccount: 'pi-mentor-thabo' }, + + // The flow starts SIGNED OUT and signs Thabo in itself, because a mentor + // browsing Code Club Projects usually has not logged in — there has never + // been a reason to. That is the condition the import route actually has to + // work from, and it changes what the site is allowed to know about them. + ownsSignIn: true, +} diff --git a/src/prototypes/import-mentor/start-on-projects/notes.md b/src/prototypes/import-mentor/start-on-projects/notes.md new file mode 100644 index 0000000..caa9d36 --- /dev/null +++ b/src/prototypes/import-mentor/start-on-projects/notes.md @@ -0,0 +1,228 @@ +A mentor starts on Code Club Projects' own project selector, filters to what +will work in a class, and imports. The mirror of its sibling, +`out-to-projects-and-back`, which starts in Code Classroom. + +Built to Sarah's flow chart, "Option 2: Code Club Projects → Code Classroom" +(Code Club account management discovery, FigJam). + +## What question is this answering? + +`import-mentor` — how does a mentor get a Code Club project into Code +Classroom? + +Both prototypes in this lane answer that. They disagree about one thing, and +only one thing: **where a mentor starts.** Option 1 bets that Code Classroom is +the hub for a club, so the route begins there and goes out. This one bets that +Code Club Projects is the site a mentor already knows, so the route begins in +the catalogue and ends in a class. + +That is the point of having two. Everything else is held identical on purpose +— same mentor (**Thabo Mokoena** at Westlands Library Code Club), same club, +same two classes, same add-to-class cards, same boundary between the products. +If a session reacts to something, it should be the starting point and not the +furniture. + +**One thing is deliberately NOT the same, and it is a consequence of the +starting point.** This flow begins **signed out**. A mentor who opens Code Club +Projects to browse has usually not logged in — there has never been a reason +to, because browsing needs no account. Its sibling starts inside Code +Classroom, which you cannot reach without logging in, so it can assume an +identity from the first screen. This one cannot, and that changes what the site +is allowed to know. + +## Why this approach + +**The first screen is the live project selector** +(`projects.raspberrypi.org/en/projects`), and using the real page settled two +things a mocked-up home page had got wrong. + +**It collapses three of the chart's boxes into one screen, and that is the +chart read correctly rather than a shortcut.** "Code Club Projects", then +"Scratch", then "Filter by 'can be used in my code classroom'" are not three +pages. On the real site Scratch is a checkbox in the **Technology** group of +the selector's filter rail — right next to where the new filter would go. A +mentor arrives, ticks Scratch, ticks the Code Classroom filter, and is looking +at their shortlist, all in one place. + +**It makes Sarah's own testing assumption answerable.** Her sticky says +*"mentors will find a project based on technology over interest"*. The live +rail puts Interest and Technology side by side, so you can watch which group a +mentor reaches for first. An earlier version of this prototype sent them to a +Scratch category page, which quietly assumed the answer — you cannot observe a +choice that has already been made for you. + +**A filter beats a category, and building both made that obvious.** The +sibling prototype gives compatibility a category of its own, "Code Classroom +compatible". A category defined by a constraint in another product sits oddly +beside categories defined by what they teach, and if nearly everything +qualifies it is a category doing no work. As a filter it is exactly what +filters are for: narrowing a list you already chose to browse. + +**Both of Sarah's "explore in design" stickies are built, not just noted.** + +- *"Wording TBD. We could also have a tag design or a way to differentiate + which cards are compatible with Code Classroom"* — **built, then removed.** + See below; the idea is right but the version on the card is not. +- *"If a user finds themselves on a project page and has a club connected to + their account, can we/should we call out the ability to get started with Code + Classroom"* — the project landing page carries a banner naming their club. It + is the only thing in the flow connecting a mentor's club to the project in + front of them, and it is worth asking whether it earns its place on a page + whose whole design drives at "Start project". + +## What is proposed, and what is real + +Real screens, unchanged: `ProjectPage`, `EducatorClassPage`, +`EducatorProjectPage`. + +Proposed — nothing like these exists: + +- **One checkbox** in `ProjectSelector.tsx`. Everything else on that screen is + the live page: the green "Find a project" band with its search box, the + filter rail, "Showing N projects", the card grid. Cards carry an interest tag + like the live ones, and "In a class" once a project has been added. +- The **club banner** on the project landing page, and the **"already in a + class"** header that replaces it once the project has been added. Both only + appear when signed in. +- Offering **"Add to a class" while signed out**, with the log-in coming after + the intent rather than before it. +- `ProjectPage`'s "Add to a class" button — the shared screen takes an + `onImport` prop for exactly this, and passing it is a proposal. +- `AddToClass.tsx` and `NewClass.tsx` — copied from the sibling prototype + rather than shared, which is what this repo asks for. They follow Experience + CS's equivalent screens. +- `ShowToStudents.tsx` — and it contradicts the live product. Same open + question as the sibling: the real project page only offers "Hide from + students", so a project is visible the moment it exists. + +Only filter groups the fixtures can honour are in the rail. The live page also +has **PDF only** and **Hardware**; faking them would put controls in a testing +session that do nothing. + +Nothing outside this folder was touched. + +## Starting signed out is the most interesting thing here + +It was a one-line change to `meta.ts` and it produced the sharpest findings in +either prototype. + +**Signed out, Code Club Projects cannot call out a club it does not know +about.** Sarah's green sticky asks whether we should surface Code Classroom "if +a user has a club connected to their account" — and the answer is that there is +no account to connect to until they log in. So the project page says nothing +about their club on first visit. The call-out she is asking for is only +possible *after* the step most likely to lose them. + +**"Add to a class" is offered anyway, and the log-in comes after the intent.** +Asking someone to log in before they have said what they want is how you lose +them, so the button is visible signed out and the sign-in is triggered by +pressing it. Whether an offer to add to a class means anything to someone the +site has not identified is a real question for a session. + +**It adds a third product to the crossing.** Signing in is a Pi Accounts page, +so a mentor who started on Code Club Projects passes through Pi Accounts on the +way to Code Classroom. Three products, two of which they did not ask for. The +sign-in screen is `screens/MentorSignIn`, which is **not verified** — it is +behind a login, so it was built from the pattern and its details are probably +wrong. + +**The log-in wall is now the most expensive step in the flow**, and the thing +most worth watching. It also makes the return path matter: signing in returns +them to choosing a class for the project they were looking at, not to the +catalogue. Any other behaviour here would be infuriating, and it is the kind of +detail that gets lost between a flow chart and a build. + +## Why there is no "Works with Code Classroom" tag + +Sarah's sticky floated it, it was built, and Divya asked whether it was +necessary. It is not, and the reasons are worth keeping: + +- **It would sit on nearly every card.** A badge on the majority carries no + information. If anything is worth marking it is the exceptions. +- **This page is shared with young people.** The selector has no idea who is + looking, and "Works with Code Classroom" means nothing to a nine-year-old + browsing for something to make. A mentor-only concern does not belong in a + catalogue everyone uses. +- **It broke the cards.** The tag rendered 245px wide; at the grid's narrowest + column a card has 192px inside its padding, so the tag dragged the card's own + text out past its edges. + +The mentor who never opens the filter rail is still covered — the project page +carries a banner naming their club, once they are signed in. + +**"In a class" is not on the cards either**, for the same reason and one more. +Whether a project is already in one of *your* classes is a fact about one +mentor, and the catalogue is shared with young people; it also only matters +once you are looking at the project rather than scanning a grid. It lives on +the project page's header instead, which is where a mentor is deciding. + +**The inverted version is probably the right idea**, and it is not built: say +nothing on the cards, and tell a mentor "this will not work in a class" at the +moment they try to add it. Mark the exception, at the point of action, to the +person it concerns. Worth designing if the filter survives testing. + +## A design system constraint worth knowing + +`.rpf-input-checkbox` carries a hardcoded `min-width: 240px`. The shared +`.cc-columns` layout fixes its rail at 260px, which leaves 212px inside a +card's padding — so **any filter rail built with `cc-columns` and checkboxes +overflows its own card.** Both prototypes in this lane hit it. Both now use a +flex rail with a 19rem basis instead, which also still stacks on a narrow +window. Worth raising upstream, or worth a wider rail variant in the shared +CSS. + +## The thing to fix before this is tested properly + +**`facets.ts` invents two project properties, and one of them matters.** + +**Compatibility, which matters.** Sarah's sticky is clear about why the filter +should exist — *"should we only allow them to add embedded editor projects? +Think yes, we know that having separate tabs is pain for younger users"* — and +that is a per-**project** property, not a per-language one. A Scratch project +that drives a Raspberry Pi, or one that only ships as a PDF, will not run in a +class however Scratch it is. + +`src/fixtures` has no such field, and fixtures are shared. So this prototype +marks one Scratch project (Rock Band) as incompatible purely so the filter and +the tag have visible work to do. **Nothing about the real Rock Band says it +would not import.** Without that invention every fixture qualifies, the filter +removes nothing, and the central mechanic of this flow cannot be tested. + +The honest fix is a `usableInClassroom` field in the fixtures plus a project or +two that genuinely is not — a shared change, and a small one. Worth doing +before this goes in front of a mentor, because a filter that visibly lies is +worse than no filter. + +**Interest, which does not matter much.** The fixtures carry no interest +either, so the Interest group is populated from the live page's own list with +one guess per project. Nothing in the flow branches on it; it is there so the +technology-versus-interest question can be watched at all. Being wrong here +costs nothing. + +## What I'd want to watch in testing + +- **Which product they open, before any screen is on offer.** Ask a mentor to + find something for next week and watch what they reach for. That is the + hypothesis, and it is answered before they touch either prototype. +- **Which filter group they reach for first.** Interest and Technology are side + by side. This is Sarah's assumption, and this screen is where it gets tested. +- **Whether they open the rail at all.** Many people search instead, or just + scroll. If the tag on the cards is doing all the work, the filter is a + designer's answer to a problem the tag already solved. +- **Whether anything on that page says Code Classroom exists.** One checkbox, + in a rail, on a page whose job is finding a project — in front of a mentor + who may not know the two products connect at all. +- **Whether the club banner earns its place** on the project page, or reads as + being told what you run when you came here to find something. +- **Whether "Add to a class" survives "Start project"**, which the page repeats + top and bottom. +- **Whether the log-in wall loses them.** It arrives mid-browse, after they + have found something they want. Watch for hesitation, and ask afterwards + whether they would have bothered. +- **Whether they ever press "View your class".** It is the only button that + leaves Code Club Projects. If nobody does, the class page is not where a + mentor checks their work — and the crossed-out eye nobody sees is a problem. + +## What we learned + +Not tested yet. diff --git a/src/prototypes/import-mentor/start-on-projects/prototype.tsx b/src/prototypes/import-mentor/start-on-projects/prototype.tsx new file mode 100644 index 0000000..a7b1782 --- /dev/null +++ b/src/prototypes/import-mentor/start-on-projects/prototype.tsx @@ -0,0 +1,565 @@ +import { useState, type ReactNode } from 'react' +import { useSearchParams } from 'react-router-dom' +import { Alert, Button, Card } from '../../../kit' +import { + PROJECTS, + classesInSchool, + piAccount, + project, + school, + schoolsForMentor, + type ClassGroup, + type Project, +} from '../../../fixtures' +import { + EducatorClassPage, + EducatorProjectPage, + MentorSignIn, + ProjectPage, +} from '../../../screens' +import { Surface } from '../../../surfaces' +import { useSession } from '../../../session' +import { AddToClass, AddedToClass } from './AddToClass' +import { ProjectSelector, type Filters } from './ProjectSelector' +import { HowToCreateClass, NameAndDescription } from './NewClass' +import { ShowToStudents } from './ShowToStudents' +import { meta } from './meta' + +// Everything comes from the one mentor named in meta.ts — the same mentor as +// the sibling prototype, so the two can be compared. Change the cast and the +// club and its classes follow. Nothing below hardcodes Westlands. +// +// The pink sticky on the flow chart — "this assumes the mentor is logged in +// and has a school" — is carried by the cast rather than by a sign-in step, +// which is why `ownsSignIn` is not set. +const MENTOR = meta.cast?.piAccount ?? '' +const CLUB = school(schoolsForMentor(MENTOR)[0].id)! +const CLASSES = classesInSchool(CLUB.id) +const MENTOR_EMAIL = piAccount(MENTOR)?.email ?? '' + +/** What the mentor set up before today, so the class is already running. */ +const ALREADY_THERE: Record = { + [CLASSES[0].id]: ['space-talk'], +} + +/** The narrow centred column the add-to-class cards sit in. */ +const CENTRED_COLUMN = { width: '100%', maxWidth: '520px', margin: '0 auto' } as const + +/** A project sitting in one of this mentor's classes. */ +interface Placed { + project: Project + classId: string + /** Hidden until the mentor says otherwise — see ShowToStudents.tsx. */ + shownToStudents: boolean +} + +// There is no "create your own" branch in this chart. Option 2 is entirely +// about importing, which is why it is shorter than its sibling. +type Step = + // Code Club Projects + | 'selector' + | 'project-landing' + | 'sign-in' + | 'choose-class' + | 'new-class-how' + | 'new-class-name' + | 'success' + // Code Classroom, reached only by asking for it + | 'class' + | 'project-page' + | 'show-to-students' + +export default function StartOnProjects() { + const [params] = useSearchParams() + // Author commentary belongs to the team, not to the person being tested. + const showWorkbench = params.get('full') !== '1' + + // Signed in, or not, is the session's business rather than this flow's — + // `ownsSignIn` in meta.ts holds the cast back so the fiction starts logged + // out, and this reads the result. + const { piAccount: account, signInPiAccount } = useSession() + const signedIn = account?.id === MENTOR + + // `?autofill=0` empties the sign-in fields, which is what you want when the + // thing you are there to watch is a mentor actually logging in. + const autofill = params.get('autofill') !== '0' + const [email, setEmail] = useState(autofill ? MENTOR_EMAIL : '') + // Visibly not a real password. Nobody should ever type one into this. + const [password, setPassword] = useState(autofill ? 'not-a-real-password' : '') + const [signInError, setSignInError] = useState() + + const [step, setStep] = useState('selector') + /** + * The selector's filters live here, not in the screen, because the chart + * loops "find another project" back to the FILTERED list — so they have to + * survive leaving the page. All start empty: filters that arrive switched on + * would narrow the catalogue before the mentor knows they exist, and "do + * they ever touch the Code Classroom one?" is a thing to watch. + */ + const [filters, setFilters] = useState({ + query: '', + classroomOnly: false, + levels: [], + interests: [], + technologies: [], + }) + + const [classes, setClasses] = useState(CLASSES) + const [placed, setPlaced] = useState(() => + Object.entries(ALREADY_THERE).flatMap(([classId, projectIds]) => + projectIds.map((id) => ({ project: project(id)!, classId, shownToStudents: true })), + ), + ) + + const [activeClassId, setActiveClassId] = useState(CLASSES[0].id) + const [activeProjectId, setActiveProjectId] = useState() + /** The project being looked at on Code Club Projects, before it is added. */ + const [viewingId, setViewingId] = useState() + const [newClassName, setNewClassName] = useState() + + const activeClass = classes.find((group) => group.id === activeClassId) ?? classes[0] + const activePlaced = placed.find( + (item) => item.project.id === activeProjectId && item.classId === activeClassId, + ) + const activeProject = activePlaced?.project + const viewing = viewingId ? project(viewingId) : undefined + + const inClass = (classId: string) => + placed.filter((item) => item.classId === classId).map((item) => item.project) + + /** The mentor's classes that already hold the project being looked at. */ + const viewingIn = viewing + ? placed + .filter((item) => item.project.id === viewing.id) + .map((item) => classes.find((group) => group.id === item.classId)) + .filter((group): group is ClassGroup => Boolean(group)) + : [] + + /** Projects in a class that students cannot see yet — the crossed-out eye. */ + const hiddenIn = (classId: string) => + placed + .filter((item) => item.classId === classId && !item.shownToStudents) + .map((item) => item.project.id) + + function memberCount(classId: string) { + return classes.find((group) => group.id === classId)?.studentIds.length ?? 0 + } + + function place(item: Project, classId: string) { + setPlaced((current) => + current.some((p) => p.project.id === item.id && p.classId === classId) + ? current + : [...current, { project: item, classId, shownToStudents: false }], + ) + setActiveClassId(classId) + setActiveProjectId(item.id) + } + + function show() { + setPlaced((current) => + current.map((p) => + p.project.id === activeProjectId && p.classId === activeClassId + ? { ...p, shownToStudents: true } + : p, + ), + ) + } + + /** Author commentary. Rendered only in the workbench. */ + function Note({ children }: { children: ReactNode }) { + if (!showWorkbench) return null + return ( + +

{children}

+
+ ) + } + + const crumbs = (...rest: string[]) => ['Your school', ...rest] + + /** Code Classroom's only navigation. The trail is always school, class... */ + function goToCrumb(index: number) { + if (index === 0) setStep('class') + else if (index === 1) setStep('class') + } + + // --- Code Club Projects: where this chart starts -------------------------- + + if (step === 'selector') { + return ( + +
+ { + setViewingId(projectId) + setStep('project-landing') + }} + /> + + The whole bet, and the opposite of the sibling prototype's. This one says a mentor + opens the site they already know and finds a project the way they always have — so + the route into Code Classroom has to start here. If they would really open Code + Classroom first, this flow begins in the wrong place and "Out to Projects and back" is + the right shape. Run the two with different mentors and the answer is the finding. +
+
+ This is the live project selector, and the proposal is one checkbox in the rail plus a + tag on the cards. Three things to watch, in order of how much they would change: +
+
+ Which filter group they reach for. Sarah's assumption on the chart is + that "mentors will find a project based on technology over interest". Interest + and Technology sit side by side here, so this screen can actually answer that — and + the chart's "Scratch" box is a tick in Technology, not a page of its own. +
+
+ Whether they open the rail at all. Most people do not, which is why + compatible cards carry a tag regardless. If the tag is doing all the work, the filter + is a designer's answer to a problem the tag already solved. +
+
+ Whether anything here says Code Classroom exists. One checkbox, in a + rail, on a page whose job is finding a project — against a mentor who may not know + the two products connect at all. +
+
+
+ ) + } + + if (step === 'project-landing' && viewing) { + return ( + +
+ {/* The project's header, and the only place that says anything + about this mentor's classes. Deliberately not on the catalogue + cards: "already in a class" is a fact about one mentor, and the + catalogue is shared with young people. + + Signed out there is nothing here at all, which is the honest + state — Code Club Projects has no idea who is looking, so it + cannot call out a club it does not know about. Sarah's green + sticky asks whether we should make that call-out "if a user has a + club connected to their account", and the answer only exists once + they are logged in. */} + {signedIn && + (viewingIn.length > 0 ? ( + group.name).join(' and ')}`} + > +

+ Young people in {viewingIn.length === 1 ? 'that class' : 'those classes'} can + already find {viewing.title}. You can add it to another class as well. +

+
+ ) : ( + +

+ You can add this project to one of your classes in Code Classroom, with its + instructions and starter code. +

+
+ ))} + + setStep('project-landing')} + // Visible whether or not they are logged in, and the sign-in + // happens after the intent rather than before it. Asking someone + // to log in before they have said what they want is how you lose + // them. + onImport={() => setStep(signedIn ? 'choose-class' : 'sign-in')} + /> + + + {signedIn + ? 'Signed in, so the page can say something about their club — the banner is the green sticky\'s question made concrete. Worth asking whether it earns its place: it is the loudest thing on a page whose whole design drives at "Start project", and a mentor who came here to find something for Thursday may not want to be told what they run.' + : 'Signed out, which is how a mentor usually arrives, and the page has nothing to say about their club because it does not know they have one. "Add to a class" is still offered — the intent comes first and the log-in comes after, because asking someone to log in before they have said what they want is how you lose them. Watch whether an offer to add to a class means anything to someone the site has not identified.'} +
+
+ Either way "Add to a class" has to compete with "Start project", which the live page + repeats top and bottom. +
+
+
+ ) + } + + if (step === 'sign-in' && viewing) { + return ( + +
+ { + setEmail(value) + setSignInError(undefined) + }} + onPasswordChange={(value) => { + setPassword(value) + setSignInError(undefined) + }} + onLogIn={() => { + // Matched on email only; the password is never checked, because + // nothing here is real. + if (email.trim().toLowerCase() !== MENTOR_EMAIL.toLowerCase()) { + setSignInError( + `We do not recognise that email address. This mentor's account is ${MENTOR_EMAIL}.`, + ) + return + } + signInPiAccount(MENTOR) + setSignInError(undefined) + setStep('choose-class') + }} + error={signInError} + /> + + A third product, mid-task. Adding a project to a class turns out to need a Raspberry Pi + account, and signing in to one is a Pi Accounts page — so a mentor who started on Code + Club Projects has now been sent somewhere with different chrome before they have + finished the thing they came to do. +
+
+ This screen is NOT VERIFIED: it is behind a login, so `screens/MentorSignIn` was built + from the pattern and its details are probably wrong. +
+
+ Two things worth watching. Whether anyone abandons here — a log-in wall in the middle + of a browsing session is the most expensive step in the flow. And whether they come + back to the right place: after signing in this returns them to choosing a class for + the project they were looking at, rather than to the catalogue, which is the only + version of this that is not infuriating. +
+
+
+ ) + } + + // --- Still on Projects: adding to a class --------------------------------- + // + // The chart puts these in the Code Classroom lane. They stay on Code Club + // Projects here for two reasons: Divya settled that question on the sibling + // prototype, following Experience CS, and holding it identical is what lets + // the two prototypes be compared on the one thing they are meant to differ + // about — where the mentor starts. + + if (step === 'choose-class' && viewing) { + return ( + +
+ { + place(viewing, classId) + setNewClassName(undefined) + setStep('success') + }} + onNew={() => setStep('new-class-how')} + onBack={() => setStep('project-landing')} + /> + + Identical to the sibling prototype, on purpose. If these two flows differed here as + well as at the start, a session could not tell which difference it was reacting to. + +
+
+ ) + } + + if (step === 'new-class-how') { + return ( + +
+ setStep('new-class-name')} + onBack={() => setStep('choose-class')} + /> + + A decision point with one option: the chart draws this diamond with a single branch + leaving it, in both options. Left as drawn rather than invented. +
+
+ And it still claims the big thing — creating a Code Classroom class from inside Code + Club Projects. Picking an existing class is a read; making one is a write into another + product. +
+
+
+ ) + } + + if (step === 'new-class-name' && viewing) { + return ( + +
+ { + const group: ClassGroup = { + id: `class-new-${classes.length + 1}`, + schoolId: CLUB.id, + name, + kind: 'code-club', + studentIds: [], + } + setClasses((current) => [...current, group]) + setNewClassName(name) + place(viewing, group.id) + setStep('success') + }} + onBack={() => setStep('new-class-how')} + /> + + A class with nobody in it, and the mentor is two products away from the screen where + they would add anyone. + +
+
+ ) + } + + if (step === 'success' && activeProject) { + return ( + +
+ setStep('class')} + // The chart loops back to the FILTERED LIST, not to a fresh + // catalogue — so a mentor keeps every filter they set. + onFindAnother={() => setStep('selector')} + /> + + "Find another project" returns to the selector with every filter and the search + text as they left them, which is how the chart draws it. That matters more in this + flow than in its sibling: a mentor who started here is already mid-browse, and setting + up a term of sessions is the obvious next thing. +
+
+ "View your class" is the only button in the whole flow that leaves Code Club Projects. +
+
+
+ ) + } + + // --- Code Classroom, reached only by asking ------------------------------- + + if (step === 'class') { + return ( + +
+ setStep('selector')} + onOpenProject={(projectId) => { + setActiveProjectId(projectId) + setStep('project-page') + }} + onCopyLink={() => {}} + onClassMembers={() => {}} + /> + + The first Code Classroom screen in the flow, and the mentor asked to come here. The + project they just imported is in the list and marked with a crossed-out eye, because + it arrives hidden from students. +
+
+ "Add project" here opens Code Classroom's own create dialog in the real product. In + this prototype it goes back to Code Club Projects, which is a shortcut rather than a + proposal — the sibling prototype is where that fork is designed. +
+
+
+ ) + } + + if (step === 'project-page' && activeProject) { + return ( + +
+ {}} + onHideFromStudents={() => setStep('show-to-students')} + onCopyLink={() => {}} + onOpenWork={() => {}} + /> + + {activePlaced?.shownToStudents + ? 'Visible now. "Student work" stays empty until someone saves, so a mentor cannot tell who has started and got stuck from who has not started.' + : 'The imported project, hidden. The only thing on this page about visibility is "Hide from students" — the opposite of what the flow just did to it.'} + +
+
+ ) + } + + if (step === 'show-to-students' && activeProject) { + return ( + +
+ setStep('class')} + onBack={() => setStep('project-page')} + /> + + In square brackets on the chart, and it inverts the live product, which only offers + "hide". Same open question as the sibling prototype — see notes.md there and here. +
+
+ Sarah's other sticky lands here too: "when a YP starts the project, should they + only be able to do it once, or are the options more like continue or start a new + one". Nothing in this flow answers that, and it is a young person's question + rather than a mentor's — it belongs in the import-yp lane. +
+
+
+ ) + } + + // Any step whose data went missing — only reachable by editing state by hand. + return ( + +
+ +

This step needs a project and there is not one.

+
+
+
+ ) +} From 13e489c960db469fd3e3fa9f95d534039fbc0430 Mon Sep 17 00:00:00 2001 From: Divya Mahadevan Date: Mon, 14 Sep 2026 17:45:53 +0100 Subject: [PATCH 8/8] Add usableInClassroom to projects, with two that are not The import prototypes both turn on a filter for "projects that can be used in my Code Classroom", and neither could honestly test it. Compatibility is a property of a PROJECT -- whether it runs in an editor Code Classroom can embed -- and the fixtures had no way to say so. One prototype guessed from the language and got it wrong twice, first dropping Scratch and then excluding nothing at all; the other invented that Rock Band would not import, which nothing about Rock Band supports. A filter that visibly lies is worse than no filter. So: an optional `usableInClassroom` on Project, absent meaning usable, plus two projects that genuinely are not -- Rain or Shine (Scratch, needs a Raspberry Pi and a rain sensor) and Door Watcher (Python, needs a Pi and a motion sensor). Physical computing is a real and sizeable part of the live catalogue, so the exclusion is the honest kind. Optional rather than required on purpose: prototypes build their own Project objects for work a young person invents, and making the field mandatory would have meant editing another prototype's folder. Both import-mentor prototypes now read the field. The "2 more projects are not here" message in out-to-projects-and-back was marked unreachable and is now the point -- a session can finally watch what a mentor does when the project they wanted is not in the list. Worth knowing: this changes what every prototype listing projects shows. import-yp now has six to browse instead of four, including two that need hardware, and it does not know about the field -- so it will let a young person add one to a class. Not this branch's folder to fix, but someone's. Co-Authored-By: Claude Opus 5 --- src/fixtures/projects.ts | 85 +++++++++++++++++++ src/fixtures/types.ts | 17 ++++ .../ProjectsCatalogue.tsx | 11 ++- .../out-to-projects-and-back/notes.md | 39 ++++----- .../out-to-projects-and-back/projectTypes.ts | 29 +++---- .../import-mentor/start-on-projects/facets.ts | 36 ++++---- .../import-mentor/start-on-projects/notes.md | 50 +++++------ 7 files changed, 171 insertions(+), 96 deletions(-) diff --git a/src/fixtures/projects.ts b/src/fixtures/projects.ts index 8113547..b885bfa 100644 --- a/src/fixtures/projects.ts +++ b/src/fixtures/projects.ts @@ -173,6 +173,91 @@ export const PROJECTS: Project[] = [ '', ].join('\n'), }, + // The two below exist so an import flow can be tested against a project + // that CANNOT be imported. Everything above runs in an editor Code Classroom + // can embed; these need a Raspberry Pi and something plugged into it, so a + // young person cannot open them inside a class however well they suit a + // club session. One Scratch and one Python, so a filter has visible work to + // do in either technology. + // + // Physical computing is a real and sizeable part of the live catalogue, and + // it is exactly what the "should we only allow embedded editor projects?" + // question on the Code Classroom FigJam is about. + { + id: 'rain-or-shine', + title: 'Rain or Shine', + language: 'Scratch', + ages: '9–13', + level: 2, + usableInClassroom: false, + intro: [ + 'Build a weather station that records rainfall and shows it on screen.', + '', + 'You will need a **Raspberry Pi** and a rain sensor connected to its pins.', + '', + 'You will:', + '', + '- Wire a sensor to the `GPIO` pins', + '- Read the sensor from Scratch', + '- Draw a chart of what you collect', + ].join('\n'), + steps: [ + { + title: 'What you will make', + body: [ + 'A **weather station** that notices when it is raining and keeps a record.', + '', + 'This project needs hardware: a Raspberry Pi and a rain sensor.', + ].join('\n'), + }, + { + title: 'Wire up the sensor', + body: 'Connect the sensor to the `GPIO` pins, following the diagram.', + }, + ], + starterCode: '// Scratch starter project — GPIO sensor blocks\n', + }, + { + id: 'door-watcher', + title: 'Door Watcher', + language: 'Python', + ages: '10–14', + level: 3, + usableInClassroom: false, + intro: [ + 'Make an alarm that tells you when someone opens a door.', + '', + 'You will need a **Raspberry Pi** and a motion sensor.', + '', + 'You will:', + '', + '- Read a sensor with `gpiozero`', + '- Play a sound when it triggers', + ].join('\n'), + steps: [ + { + title: 'What you will make', + body: [ + 'A **door alarm**, built from a Raspberry Pi and a motion sensor.', + '', + 'The code runs on the Pi itself, not in a browser.', + ].join('\n'), + }, + { + title: 'Read the sensor', + body: 'Use `gpiozero` to tell when the sensor has been triggered.', + }, + ], + starterCode: [ + '# Door Watcher', + '# Runs on a Raspberry Pi with a motion sensor attached.', + '', + 'from gpiozero import MotionSensor', + '', + 'sensor = MotionSensor(4)', + '', + ].join('\n'), + }, ] export function project(id: string): Project | undefined { diff --git a/src/fixtures/types.ts b/src/fixtures/types.ts index 7118f5d..145aff9 100644 --- a/src/fixtures/types.ts +++ b/src/fixtures/types.ts @@ -132,4 +132,21 @@ export interface Project { landingTask?: { title: string; body: string } steps: ProjectStep[] starterCode: string + /** + * Whether this project can run inside Code Classroom — whether it uses an + * editor Code Classroom can embed. + * + * Not everything can. A Scratch project that drives a Raspberry Pi needs + * hardware attached, and a download-only resource has no editor at all, so + * neither works in a class however well it suits a club. That is the whole + * question behind "should we only allow embedded editor projects?" on the + * Code Classroom FigJam, and an import flow could not put it in front of + * anyone until a project existed that genuinely does not import. + * + * OPTIONAL, and absent means usable — read it as `usableInClassroom !== + * false`. Most projects are usable, so stating it on every fixture would be + * noise; and prototypes build their own Project objects for work a young + * person invents, which is always an editor project. + */ + usableInClassroom?: boolean } diff --git a/src/prototypes/import-mentor/out-to-projects-and-back/ProjectsCatalogue.tsx b/src/prototypes/import-mentor/out-to-projects-and-back/ProjectsCatalogue.tsx index 3aad71f..8e0e619 100644 --- a/src/prototypes/import-mentor/out-to-projects-and-back/ProjectsCatalogue.tsx +++ b/src/prototypes/import-mentor/out-to-projects-and-back/ProjectsCatalogue.tsx @@ -159,12 +159,11 @@ export function ProjectsCatalogue({ projects, alreadyAddedIds, onView, onBack }:

{visible.length} {visible.length === 1 ? 'result' : 'results'}

- {/* UNREACHABLE with today's fixtures, and kept on purpose. The - category now covers all three languages the fixtures have, so - nothing is excluded and this never renders. It becomes correct - the moment a project that cannot be embedded exists — which is - what the "can we connect projects that aren't editor projects?" - sticky is about. Do not read its absence as an answer. */} + {/* Reachable, and the point. The fixtures carry two physical + computing projects that need a Raspberry Pi, so the category + really is shorter than the catalogue — which is what makes the + "can we connect projects that aren't editor projects?" sticky + something a session can watch. */} {hidden > 0 && (

{hidden} more {hidden === 1 ? 'project is' : 'projects are'} not here, because they diff --git a/src/prototypes/import-mentor/out-to-projects-and-back/notes.md b/src/prototypes/import-mentor/out-to-projects-and-back/notes.md index 9d603b9..2221be2 100644 --- a/src/prototypes/import-mentor/out-to-projects-and-back/notes.md +++ b/src/prototypes/import-mentor/out-to-projects-and-back/notes.md @@ -181,29 +181,22 @@ question can actually be tested. Note the trap it creates: after importing, the project page still only offers "Hide from students" — a mentor looking for "show to students" will not find it where they landed. -**3. Nothing in the fixtures is now excluded, so the sticky's question is no -longer visible.** "CCP page filtered by embedded editor projects" covers -Scratch, Python and HTML — settled by Divya, September 2026. An earlier version -of this prototype guessed Python and HTML only, which dropped the Scratch -projects and so dropped most of what a club actually runs. That was wrong, and -the category now shows all four fixtures. - -The consequence is worth stating plainly: `Project['language']` has exactly -those three values, so **the filter can no longer exclude anything**, and the -pink sticky's question — "can we connect projects that aren't editor -projects?" — cannot be seen in this flow any more. It is still a live question. -The real catalogue has physical computing projects, third-party editor -projects and PDF-only projects that would not import. Testing what a mentor -does when the thing they want is not importable needs a non-editor project in -`src/fixtures`, which is shared and not this prototype's to change. - -The code for the "some projects are not here" message is kept and marked -unreachable, so it starts working the moment such a fixture exists. **Do not -read its absence as evidence that everything imports.** - -Which also leaves a question about the category itself: if almost everything -qualifies, what is a "Code Classroom compatible" category actually for? A -filter that excludes nothing is a filter nobody needs. +**3. The category really is shorter than the catalogue, and the sticky's +question is testable.** "CCP page filtered by embedded editor projects" is now +read from `usableInClassroom` on each project rather than guessed from its +language — two earlier versions of this got it wrong in opposite directions, +first dropping Scratch and then excluding nothing at all. + +The field and two physical computing projects (`rain-or-shine`, +`door-watcher`) were added to `src/fixtures` for exactly this. They need a +Raspberry Pi and something plugged into it, so a young person cannot open them +inside a class however well they suit a club session — which is what Sarah's +pink sticky, *"can we connect projects that aren't editor projects?"*, is +about. The catalogue says on screen that two are missing and why. + +So a session can now watch the thing that matters: **what a mentor does when +the project they wanted is not there.** That was impossible while the filter +excluded nothing. **4. The product boundary moved, and the chart is now wrong about where it sits.** The chart puts "choose new or existing class" and everything after it diff --git a/src/prototypes/import-mentor/out-to-projects-and-back/projectTypes.ts b/src/prototypes/import-mentor/out-to-projects-and-back/projectTypes.ts index 29186cd..40748b7 100644 --- a/src/prototypes/import-mentor/out-to-projects-and-back/projectTypes.ts +++ b/src/prototypes/import-mentor/out-to-projects-and-back/projectTypes.ts @@ -8,28 +8,19 @@ import type { Project, ProjectStep } from '../../../fixtures' * "CCP page filtered by embedded editor projects" — the chart's own words for * the catalogue the mentor browses. * - * All three: Scratch as well as Python and HTML. Settled by Divya, September - * 2026, rather than guessed — an earlier version of this file left Scratch out - * on the assumption that "embedded editor" meant the Code Editor's own - * languages, which would have dropped most of what a Code Club runs. + * Reads `usableInClassroom` from the fixtures. Two earlier versions of this + * guessed: first that "embedded editor" meant Python and HTML only, which + * wrongly dropped Scratch; then that every language qualified, which made the + * filter exclude nothing and quietly took the question off the table. The + * answer belongs per project, not per language — a Scratch project driving a + * Raspberry Pi does not import however Scratch it is. * - * Worth knowing what this costs the prototype. `Project['language']` has - * exactly these three values, so the filter can no longer exclude anything in - * the fixtures — which means the question on the "can we connect projects that - * aren't editor projects?" sticky is no longer visible in this flow. It is - * still a real question: the live catalogue has physical computing and - * third-party-editor projects that would not import. Seeing that happen needs a - * non-editor project in `src/fixtures`, which is shared and not this - * prototype's to change. See notes.md. + * So the category is genuinely shorter than the whole catalogue again, and the + * "can we connect projects that aren't editor projects?" sticky is something a + * session can watch rather than something the notes apologise for. */ -export const EMBEDDED_EDITOR_LANGUAGES: Array = [ - 'Scratch', - 'Python', - 'HTML', -] - export function isEmbeddedEditorProject(project: Project): boolean { - return EMBEDDED_EDITOR_LANGUAGES.includes(project.language) + return project.usableInClassroom !== false } /** diff --git a/src/prototypes/import-mentor/start-on-projects/facets.ts b/src/prototypes/import-mentor/start-on-projects/facets.ts index a708532..86eab12 100644 --- a/src/prototypes/import-mentor/start-on-projects/facets.ts +++ b/src/prototypes/import-mentor/start-on-projects/facets.ts @@ -1,32 +1,26 @@ import type { Project } from '../../../fixtures' -// What the project selector filters on. Two of these are properties the -// fixtures do not have, so they are stood in for here — read the warnings. +// What the project selector filters on. Compatibility is a real field on the +// fixtures now; interest is still stood in for here — read the warning. /** - * Which projects can be used inside Code Classroom. **PROPOSED, and invented.** + * Which projects can be used inside Code Classroom. * - * Sarah's flow chart turns on a filter called "can be used in my code - * classroom", and her sticky says why it should exist: *"Should we only allow - * them to add embedded editor projects? Think yes — we know that having - * separate tabs is pain for younger users."* + * Reads `usableInClassroom` from the fixtures, which is the real thing rather + * than a guess. An earlier version of this file invented the answer, because + * the fixtures had no such field and a filter that excluded nothing could not + * be tested — the field and two physical computing projects were added for + * exactly this. Absent means usable; see the field's comment in + * `src/fixtures/types.ts`. * - * So compatibility is a property of a PROJECT, not of a language: on the live - * site a Scratch project that drives a Raspberry Pi, or one that only ships as - * a PDF, will not run inside a class however Scratch it is. `src/fixtures` has - * no such field, and fixtures are shared and not this prototype's to change. - * - * **The assignment is invented.** One Scratch project is marked incompatible - * purely so the filter and the compatibility tag have visible work to do; - * without it every fixture qualifies, the filter removes nothing, and the - * central mechanic of this flow cannot be tested. Nothing about the real Rock - * Band project says it would not import. The honest fix is a - * `usableInClassroom` field in the fixtures. + * Sarah's sticky is the reason any of it exists: *"Should we only allow them + * to add embedded editor projects? Think yes — we know that having separate + * tabs is pain for younger users."* Note whose problem that is. The filter + * looks like a convenience for the mentor and is really about the young + * person's session. */ -const NOT_COMPATIBLE = new Set(['rock-band']) - export function usableInClassroom(project: Project): boolean { - return !NOT_COMPATIBLE.has(project.id) + return project.usableInClassroom !== false } /** diff --git a/src/prototypes/import-mentor/start-on-projects/notes.md b/src/prototypes/import-mentor/start-on-projects/notes.md index caa9d36..8dd510c 100644 --- a/src/prototypes/import-mentor/start-on-projects/notes.md +++ b/src/prototypes/import-mentor/start-on-projects/notes.md @@ -171,33 +171,29 @@ flex rail with a 19rem basis instead, which also still stacks on a narrow window. Worth raising upstream, or worth a wider rail variant in the shared CSS. -## The thing to fix before this is tested properly - -**`facets.ts` invents two project properties, and one of them matters.** - -**Compatibility, which matters.** Sarah's sticky is clear about why the filter -should exist — *"should we only allow them to add embedded editor projects? -Think yes, we know that having separate tabs is pain for younger users"* — and -that is a per-**project** property, not a per-language one. A Scratch project -that drives a Raspberry Pi, or one that only ships as a PDF, will not run in a -class however Scratch it is. - -`src/fixtures` has no such field, and fixtures are shared. So this prototype -marks one Scratch project (Rock Band) as incompatible purely so the filter and -the tag have visible work to do. **Nothing about the real Rock Band says it -would not import.** Without that invention every fixture qualifies, the filter -removes nothing, and the central mechanic of this flow cannot be tested. - -The honest fix is a `usableInClassroom` field in the fixtures plus a project or -two that genuinely is not — a shared change, and a small one. Worth doing -before this goes in front of a mentor, because a filter that visibly lies is -worse than no filter. - -**Interest, which does not matter much.** The fixtures carry no interest -either, so the Interest group is populated from the live page's own list with -one guess per project. Nothing in the flow branches on it; it is there so the -technology-versus-interest question can be watched at all. Being wrong here -costs nothing. +## What the filter actually excludes + +`usableInClassroom` is a real field on the fixtures, and two projects carry it +as `false`: **Rain or Shine** (Scratch, needs a Raspberry Pi and a rain sensor) +and **Door Watcher** (Python, needs a Pi and a motion sensor). Ticking the +filter takes the catalogue from six projects to four. + +That matters more than it sounds. An earlier version of this prototype invented +which project was incompatible, because the fixtures had no such field — so the +filter either excluded nothing, or excluded something for no reason anyone could +defend. Neither could be put in front of a mentor: a filter that visibly lies is +worse than no filter. The field and the two hardware projects were added to +`src/fixtures` to fix that, which is the one change in this work that reaches +outside a prototype folder. + +Physical computing is a real and sizeable part of the live catalogue, so the +exclusion is the honest kind. **The question a session can now answer is what a +mentor does when the project they wanted is not in the list** — and whether +"Code Classroom only" tells them enough to work out why. + +Interest is still invented (see `facets.ts`). Nothing branches on it; it exists +so the technology-versus-interest question can be watched at all, and being +wrong about which project is about Space costs nothing. ## What I'd want to watch in testing