Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import type { ReactNode } from 'react'
import { Button, Card } from '../../../kit'
import type { ClassGroup, Project } from '../../../fixtures'

// A COPY of screens/classroom/EducatorClassPage, changed here rather than
// there.
//
// The shared screen renders its two bands — the white title block and the
// off-white content below it — as a fragment, with nothing between them and no
// prop to put anything there. Putting the "no members yet" alert where it
// belongs means opening that seam, and opening it in the shared screen would
// change the page for every other prototype using it. So it is copied in, per
// the repo rule.
//
// What is different from the shared screen:
//
// - A `banner` slot between the two bands.
//
// That is the whole change. Everything else is the shared screen as it stands,
// including the real page's furniture: "Class members" is the primary action,
// projects sit in a bordered list of pale rows, and a project name is bold body
// text rather than a link. Keep it that way — if the shared screen gains
// something, this copy should gain it too.

interface Props {
classGroup: ClassGroup
projects: Project[]
memberCount: number
/** The blurb under the class title. */
description?: string
/**
* Rendered between the title band and the content, which is where a page-
* level message belongs — after a mentor has read what page they are on, and
* before the thing they would otherwise start doing.
*/
banner?: ReactNode
onAddProject: () => void
onOpenProject: (projectId: string) => void
onCopyLink: () => void
onClassMembers: () => void
/** Projects students cannot see yet. */
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
}

export function ClassPageWithBanner({
classGroup,
projects,
memberCount,
description,
banner,
onAddProject,
onOpenProject,
onCopyLink,
onClassMembers,
hiddenProjectIds = [],
onProjectMenu,
onSettings,
}: Props) {
return (
<>
<div className="cc-page-header">
<div className="cc-page-title">
<h1 className="title-lg">{classGroup.name}</h1>
{description && <p className="cc-page-desc">{description}</p>}
</div>

<div className="cc-actions">
{/* Primary on the live page. Getting young people into the class is
the job a mentor comes here to do. */}
<Button
type="primary"
icon="group"
text={`Class members (${memberCount})`}
onClick={onClassMembers}
/>
<Button type="secondary" icon="content_copy" text="Copy link" onClick={onCopyLink} />
<Button
type="secondary"
icon="settings"
iconOnly
aria-label="Class settings"
onClick={onSettings ?? (() => {})}
/>
</div>
</div>

{/* Padded to the same gutter as both bands, so the alert lines up with
the title above it and the Projects card below it. */}
{banner && <div style={{ padding: '0 var(--space-3)' }}>{banner}</div>}

<div className="cc-page-main">
<Card>
<div className="cc-section-head">
<div>
<h2 className="title-sm">Projects</h2>
<p className="body muted">
Projects are shared with students and contain starter code created by a teacher.
</p>
</div>
<Button
type="primary"
icon="add"
iconPosition="right"
text="Add project"
onClick={onAddProject}
/>
</div>

{projects.length === 0 ? (
<p className="lane-empty">No projects yet</p>
) : (
<ul className="cc-list cc-list-boxed">
{projects.map((project) => {
const hidden = hiddenProjectIds.includes(project.id)
return (
<li className="cc-row" key={project.id}>
<span className="cc-row-main">
{hidden && (
// Labelled rather than aria-hidden: the icon is the only
// thing on the row carrying this, so it has to be read.
<span
className="material-symbols-sharp"
role="img"
aria-label="Hidden from students"
>
visibility_off
</span>
)}
<button className="cc-row-title" onClick={() => onOpenProject(project.id)}>
{project.title}
</button>
</span>
<Button
type="tertiary"
icon="more_vert"
iconOnly
aria-label={`More options for ${project.title}`}
onClick={() => onProjectMenu?.(project.id)}
/>
</li>
)
})}
</ul>
)}
</Card>
</div>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { Alert, Button } from '../../../kit'
import type { School } from '../../../fixtures'

// A COPY of screens/code-club/MentorDashboard, changed here rather than there.
//
// The shared screen has no prop for another club action, and the change this
// prototype is making is not a prop — it is a reordering of the card's
// actions, which is the whole hypothesis. So it is copied in, per the repo
// rule: never edit a shared screen, copy it and change your copy.
//
// What is different from the live dashboard:
//
// - "Code Classroom" is the primary button on each club card, first in the row.
// - "Create an event" is gone from the card entirely. That is a real cost, not
// a detail — events are how a club fills its sessions and how Code Club
// counts activity — and it is the trade this variant is making on purpose.
// - Nothing on the card is labelled as a proposal. The page reads as if it
// shipped, so a mentor in a session reacts to the product rather than to a
// prototype. What is and is not real is in notes.md.
//
// Everything else is left exactly as the shared screen has it.

interface Props {
clubsManaged: School[]
pendingRequests: string[]
onDismissRequest: (request: string) => void
onManageClub: (schoolId: string) => void
onViewPublicProfile: (schoolId: string) => void
onStartAClub: () => void
onFindAClub: () => void
clubsVolunteeredAt?: School[]
/** Which of these clubs already has a Code Classroom behind it. */
isSetUp: (schoolId: string) => boolean
onCodeClassroom: (schoolId: string) => void
}

export function DashboardWithClassroom({
clubsManaged,
pendingRequests,
onDismissRequest,
onManageClub,
onViewPublicProfile,
onStartAClub,
onFindAClub,
clubsVolunteeredAt = [],
isSetUp,
onCodeClassroom,
}: Props) {
return (
<div className="cc-dash">
<aside className="cc-dash-side">
<h2 className="title-sm">Club homepage</h2>
<nav className="cc-dash-nav" aria-label="Club homepage">
<span className="cc-dash-nav-item active">Your Clubs</span>
<span className="cc-dash-nav-item">Your information</span>
<span className="cc-dash-nav-item">Club support</span>
</nav>
<hr className="divider" />
<nav className="cc-dash-nav" aria-label="More">
<button className="link-button" onClick={onFindAClub}>
Find a Club
</button>
<button className="link-button" onClick={onStartAClub}>
Start a Club
</button>
</nav>
</aside>

<div className="cc-dash-main">
<h1 className="title-lg cc-dash-title">Your Clubs</h1>

{pendingRequests.map((request) => (
<Alert
key={request}
type="information"
text={request}
closable
onClose={() => onDismissRequest(request)}
/>
))}

<div className="cc-section-head">
<h2 className="title-sm">Clubs you manage</h2>
<button className="link-button" onClick={onStartAClub}>
Start a Club
</button>
</div>

{clubsManaged.map((club) => (
<div className="cc-club-card" key={club.id}>
<div className="cc-club-head">{club.name}</div>
<div className="cc-club-body">
<div>
<p className="body">
<strong>Nearest 4.5 miles</strong>
</p>
<p className="body muted">{club.venue ?? 'No venue set'}</p>
</div>
<div>
<p className="body muted">{club.schedule ?? 'No sessions scheduled'}</p>
<p className="body muted">
{club.online ? 'Online Sessions' : 'In-person Sessions'}
</p>
</div>
</div>
<div className="cc-club-actions">
{/* The proposal. Primary, and first — a mentor arriving with
"what are we doing on Thursday?" should not have to know
the words "Manage club". "Create an event" is not here at
all: this variant gives the card's top slot to Code
Classroom rather than sharing it. */}
<Button
type="primary"
text={isSetUp(club.id) ? 'Open Code Classroom' : 'Set up Code Classroom'}
onClick={() => onCodeClassroom(club.id)}
/>
<Button type="secondary" text="Manage club" onClick={() => onManageClub(club.id)} />
<Button
type="secondary"
text="View public profile"
onClick={() => onViewPublicProfile(club.id)}
/>
</div>
</div>
))}

<div className="cc-section-head">
<h2 className="title-sm">Clubs you volunteer at</h2>
<button className="link-button" onClick={onFindAClub}>
Find a club
</button>
</div>

{clubsVolunteeredAt.length === 0 ? (
<div className="cc-empty">
<h3 className="title-sm">You have not volunteered at any Clubs</h3>
<p className="body muted">
Why not explore our resources on{' '}
<a href="#/screens">recruiting volunteers for your Club</a>
</p>
</div>
) : (
<ul className="cc-list">
{clubsVolunteeredAt.map((club) => (
<li className="cc-row" key={club.id}>
{club.name}
</li>
))}
</ul>
)}
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { ReactNode } from 'react'

interface Props {
/**
* A Material Symbols ligature name, e.g. "checklist". The font renders the
* text content as the glyph, so this IS the icon.
*/
icon: string
title: string
children: ReactNode
}

/**
* A pale green panel with an icon, a bold title and a line underneath — what
* the three "what Code Classroom is" points are shown as, instead of bullets.
*
* Local to this prototype rather than added to kit/: the kit has no equivalent,
* and the repo rule is that a prototype adds to its own folder. If a second
* prototype wants one of these, copy it.
*
* Material Symbols Outlined is already loaded by the design system's
* stylesheet — the same family Surface uses for its breadcrumb chevron and
* safeguarding flag — so there is no font to add and no dependency to install.
*
* The icon is decorative and hidden from screen readers: the title next to it
* says the same thing, and a reader announcing the ligature name would just
* read "checklist" twice.
*/
export function FeatureBox({ icon, title, children }: Props) {
return (
<div
style={{
display: 'grid',
gridTemplateColumns: 'auto 1fr',
columnGap: 'var(--space-2)',
rowGap: 'var(--space-05)',
alignItems: 'center',
background: 'var(--rpf-green-100)',
border: '1px solid var(--rpf-green-400)',
borderRadius: 'var(--radius-card)',
padding: 'var(--space-3)',
}}
>
<span
aria-hidden="true"
className="material-symbols-outlined"
style={{ fontSize: 28, color: 'var(--rpf-green-900)' }}
>
{icon}
</span>
{/* Body size, bold — still an h3, so the heading structure survives
it reading at the same scale as the line underneath. */}
<h3 className="body" style={{ fontWeight: 'var(--fw-bold)' }}>
{title}
</h3>
<p className="body" style={{ gridColumn: 2 }}>
{children}
</p>
</div>
)
}
20 changes: 20 additions & 0 deletions src/prototypes/onboarding-mentor/card-action-restyled/meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { PrototypeMeta } from '../../types'

export const meta: PrototypeMeta = {
title: 'Club card action, restyled',

owner: 'Sarah Tucker',

// DRAFT — Sarah to correct. The flow is a straight copy of
// "Code Classroom as the club card action", so the route is not what is
// being tested here; only how the confirm screen is presented.
hypothesis:
'The confirm screen is where a mentor decides whether Code Classroom is for them, and two dense cards of prose get skimmed. Presented as one centred explainer — a short claim per panel, each with an icon — a mentor will be able to say in their own words what Code Classroom is before they press yes, rather than agreeing and finding out afterwards.',

status: 'sketch',

// The same mentor as the prototype this copies, on purpose: two visual
// treatments of one route only compare if the club, the code and the class
// name are identical.
cast: { piAccount: 'pi-mentor-jo' },
}
Loading
Loading