Build GradeFlow Rubric Grader
Build a local rubric grader with scoring, feedback, publishing, and a queue.
Introduction
30 Second Summary
Grading the same question across a class can turn into repeated judgment calls. A shared set of criteria keeps each decision consistent from one submission to the next.
In this project, you will build GradeFlow as a local browser-based rubric grader inspired by Gradescope. The app turns three fictional submissions into a complete grading queue with scoring, feedback, navigation, and publishing.
What You'll Build
You'll open GradeFlow in Safari to see a polished grading workspace that carries one fictional class from rubric choices to published student results.
By the end of this project, you'll have:
- A two-pane grading experience where you can read a fictional answer beside the rubric used to assess it.
- A bounded scoring system that updates immediately as you apply positive or negative rubric items.
- A grading queue that preserves each submission's feedback in memory while moving you to the next ungraded answer.
- Secret Mission: Add a focus-aware Z keyboard shortcut that speeds up grading without interrupting feedback entry.
Are there any prerequisites?
You'll need basic familiarity with HTML, CSS, and JavaScript. Visual Studio Code and Safari are enough for the entire project, so you do not need Node.js or an account.
Before We Start
A clear goal keeps GradeFlow focused on consistent rubric grading instead of the complexity of a full learning platform. This checkpoint is your chance to commit to what you are building before the hands-on work begins.
Set Up GradeFlow
A local three-file workspace gets GradeFlow to its first visible result quickly. This approach needs no Node.js or backend setup.
You'll use Visual Studio Code to create the project files. Safari will confirm that your starter page opens from your Mac.
In this step, get ready to:
- Open a local gradeflow folder as your Visual Studio Code workspace.
- Create index.html, styles.css, and script.js.
- Confirm the starter page opens in Safari.
Open the GradeFlow workspace
A workspace gives Visual Studio Code one folder to treat as the current project. Every GradeFlow file will live inside the gradeflow folder.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
You'll see the Visual Studio Code start screen.
- Click File in the top menu bar.
- Select Open Folder....
You'll see the macOS folder picker.
- Select Desktop in the folder picker sidebar.
- Click the folder creation control in the folder picker.
You'll see a field for the new folder name.
- Enter gradeflow as the folder name.
- Press Enter.
You'll see the new gradeflow folder on your Desktop.
- Select the gradeflow folder.
- Click Open.
The Explorer sidebar now shows gradeflow as your workspace. Your project has a clear home for every file you add next.
Create the project files
Each file has one browser responsibility. HTML provides the page structure.
CSS controls the appearance. JavaScript adds behavior.
- Click New File... in the Explorer view.
You'll see a filename field inside the gradeflow workspace.
- Enter index.html in the filename field.
- Press Enter.
You'll see index.html in the Explorer view.
- Click New File... in the Explorer view.
You'll see another filename field inside the workspace.
- Enter styles.css in the filename field.
- Press Enter.
You'll see styles.css beside index.html in the Explorer view.
- Click New File... in the Explorer view.
You'll see the filename field one final time.
- Enter script.js in the filename field.
- Press Enter.
Good progress. The Explorer view now lists all three GradeFlow files.
Display the starter page in Safari
The starter document connects all three files while keeping your first browser check simple. The defer attribute lets the document finish loading before script.js runs.
- Select index.html in the Explorer view.
- Paste this starter shell into the editor:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>GradeFlow Rubric Grader</title>
<link rel="stylesheet" href="styles.css">
<script src="script.js" defer></script>
</head>
<body>
<h1>GradeFlow is ready</h1>
</body>
</html>
What does this code do?
- The document declaration tells Safari to use modern HTML.
- The metadata gives the page a standard text encoding.
- The viewport metadata lets the page follow the width of the browser.
- The stylesheet link connects styles.css to the page.
- The deferred script loads script.js after the document has been parsed.
- The heading gives you a visible browser check.
- Save index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).
The unsaved indicator disappears from the index.html tab.
- Control-click index.html in the Explorer view.
- Select Reveal in Finder.
You'll see Finder open to the gradeflow folder. The index.html file is selected.
Before you open the file, what heading do you expect Safari to display?
- Control-click index.html in Finder.
- Choose Open With.
- Select Safari.
Safari opens the local file. You'll see GradeFlow is ready as the page heading.
That first browser result is in. GradeFlow now has a local page that Safari can load directly.
Don't see the starter heading?
- Confirm Safari opened index.html from the gradeflow folder.
- Check that <h1>GradeFlow is ready</h1> appears between the opening and closing body tags.
- Save index.html before refreshing Safari.
Still stuck? Help me find why my local GradeFlow heading is missing.
✔️ Awesome, I've got everything!
Your three project files are in place. Safari confirms the starter page works.
ⓧ I'd like to double check the full code
Your index.html file should match this code exactly.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>GradeFlow Rubric Grader</title>
<link rel="stylesheet" href="styles.css">
<script src="script.js" defer></script>
</head>
<body>
<h1>GradeFlow is ready</h1>
</body>
</html>
The styles.css file should be empty at this checkpoint. The script.js file should also be empty.
Your local project foundation is ready. Next, you'll turn the starter heading into the complete grading workspace.
Build the Grading Workspace
Your three-file GradeFlow workspace in Visual Studio Code already opens in Safari. The starter heading proves the local page is wired correctly.
A lone heading cannot support the grading workflow. An instructor needs the submission and rubric visible together.
You will replace the starter with a two-pane grading workspace. A bottom action area will keep the score and queue controls visible.
In this step, get ready to:
- Replace the starter heading with the GradeFlow grading interface.
- Style the workspace as a responsive two-column grader.
- Test the static rubric to expose the missing grading behavior.
Replace the starter page with the grader
Semantic HTML gives each part of the page a clear role. The header identifies the assignment while the main area holds the grading workflow.
- Switch back to the index.html tab from earlier.
- Find the line containing <h1>GradeFlow is ready</h1>.
- Replace that line with the page shell below:
<header class="topbar">
<div>
<p class="eyebrow">CS 101 | Homework 3</p>
<h1>GradeFlow</h1>
</div>
<span id="publishStatus" class="status-badge">Not published</span>
</header>
<main class="app-shell">
<section class="grader-layout" aria-label="Grading workspace">
</section>
</main>
<footer class="action-bar">
<div>
<span class="eyebrow">Progress</span>
<strong id="progress">1 of 3</strong>
</div>
<div class="score-display">Score: <strong id="score">0</strong>/10</div>
<p id="actionMessage" class="action-message" aria-live="polite"></p>
<div class="actions">
<button id="publishButton" class="secondary-button" type="button">Publish Grade</button>
<button id="nextButton" class="primary-button" type="button">Next Ungraded</button>
</div>
</footer>
What does this structure do?
- The header identifies GradeFlow and holds the publication status.
- The main element reserves the central grading workspace.
- The footer groups the progress indicator and grading controls.
- Save index.html.
- Return to the Safari page from earlier.
- Refresh the page.
You should see a GradeFlow heading above progress and score controls. The center of the page is still empty.
Still seeing the starter heading?
- Confirm you replaced the heading inside the existing body element.
- Confirm the Safari tab still points to the index.html file inside gradeflow.
Still stuck? Help me check why Safari still shows the GradeFlow starter heading.
The submission pane keeps the question and fictional answer in one reading area. Its status badge gives the instructor an immediate grading state.
- In index.html find the closing </section> tag inside main.
- Insert the submission card immediately above that closing tag:
<article class="submission-card">
<div class="panel-heading">
<div>
<p class="eyebrow">Student submission</p>
<h2 id="studentName">Student 01</h2>
</div>
<span id="gradingStatus" class="status-badge">Ungraded</span>
</div>
<div class="question-block">
<p class="question-label">Question 1 | 10 points</p>
<h3>Explain why a binary search is faster than a linear search on sorted data.</h3>
</div>
<div class="answer-sheet">
<p id="studentAnswer"></p>
</div>
</article>
How is the submission organized?
- The submission-card contains one fictional student's work.
- The studentName and studentAnswer identifiers provide update points for later JavaScript.
- The empty answer paragraph reserves space for the current submission.
- Save index.html.
- Refresh the GradeFlow page in Safari.
You should see Student 01 and the binary search question in the center of the page. An empty answer area sits underneath.
Submission card missing?
- Check that the new article sits between the opening and closing grader-layout section tags.
- Check that every opening div has a matching closing tag.
Need help? Help me find the HTML nesting problem in my GradeFlow submission card.
A reusable rubric lets an instructor apply the same criteria across submissions. Each button pairs a criterion with its score effect.
- Find the closing </article> tag for the submission card.
- Insert the rubric panel immediately below that closing tag:
<aside class="rubric-card">
<div class="panel-heading">
<div>
<p class="eyebrow">Rubric</p>
<h2>Question 1</h2>
</div>
<span>0 to 10</span>
</div>
<div class="rubric-list">
<button id="rubric0" class="rubric-item" type="button">
<span><b>Correct method</b><small>Explains repeatedly halving the search space.</small></span>
<strong>+6</strong>
</button>
<button id="rubric1" class="rubric-item" type="button">
<span><b>Correct final answer</b><small>States the logarithmic comparison clearly.</small></span>
<strong>+4</strong>
</button>
<button id="rubric2" class="rubric-item" type="button">
<span><b>Missing explanation</b><small>Gives a conclusion without supporting reasoning.</small></span>
<strong>-2</strong>
</button>
</div>
<label for="feedback">Submission feedback</label>
<textarea id="feedback" rows="5" placeholder="Add feedback for this student"></textarea>
</aside>
What makes this a rubric?
- Each rubric-item button represents one reusable grading criterion.
- The positive and negative values show how each criterion affects the score.
- The feedback textarea provides a separate place for written comments.
- Save index.html.
- Refresh the GradeFlow page in Safari.
You should see three rubric buttons and a submission feedback field below the question details.
Rubric sitting outside the workspace?
- Confirm the aside appears before the closing </section> tag.
- Confirm all three rubric buttons remain inside rubric-list.
Need a second pair of eyes? Help me place the rubric panel beside the submission card.
Published results need a dedicated student-facing region. The hidden attribute keeps that region private until the instructor releases the grade.
- Find the closing </main> tag in index.html.
- Insert the student preview immediately above that closing tag:
<section id="studentPreview" class="student-preview" hidden>
<p class="eyebrow">Student preview</p>
<div class="preview-row">
<h2>Published result</h2>
<strong id="previewScore">0 / 10</strong>
</div>
<p id="previewFeedback">No written feedback.</p>
</section>
Why start with a hidden preview?
The preview contains the released score and written feedback. Starting it hidden keeps the instructor workspace separate from the student-facing result.
- Save index.html.
- Refresh the GradeFlow page in Safari.
You should not see a Published result panel. That confirms the preview begins in its hidden state.
Published result already visible?
- Confirm the opening studentPreview section includes the hidden attribute.
- Confirm the preview sits inside main.
Still visible? Help me check why the hidden GradeFlow student preview appears in Safari.
Your page structure is complete. Use these tabs to confirm the full file before styling it.
✔️ Awesome, I've got everything!
Great. Save index.html before moving to the stylesheet.
ⓧ I'd like to double check the full code
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>GradeFlow Rubric Grader</title>
<link rel="stylesheet" href="styles.css">
<script src="script.js" defer></script>
</head>
<body>
<header class="topbar">
<div>
<p class="eyebrow">CS 101 | Homework 3</p>
<h1>GradeFlow</h1>
</div>
<span id="publishStatus" class="status-badge">Not published</span>
</header>
<main class="app-shell">
<section class="grader-layout" aria-label="Grading workspace">
<article class="submission-card">
<div class="panel-heading">
<div>
<p class="eyebrow">Student submission</p>
<h2 id="studentName">Student 01</h2>
</div>
<span id="gradingStatus" class="status-badge">Ungraded</span>
</div>
<div class="question-block">
<p class="question-label">Question 1 | 10 points</p>
<h3>Explain why a binary search is faster than a linear search on sorted data.</h3>
</div>
<div class="answer-sheet">
<p id="studentAnswer"></p>
</div>
</article>
<aside class="rubric-card">
<div class="panel-heading">
<div>
<p class="eyebrow">Rubric</p>
<h2>Question 1</h2>
</div>
<span>0 to 10</span>
</div>
<div class="rubric-list">
<button id="rubric0" class="rubric-item" type="button">
<span><b>Correct method</b><small>Explains repeatedly halving the search space.</small></span>
<strong>+6</strong>
</button>
<button id="rubric1" class="rubric-item" type="button">
<span><b>Correct final answer</b><small>States the logarithmic comparison clearly.</small></span>
<strong>+4</strong>
</button>
<button id="rubric2" class="rubric-item" type="button">
<span><b>Missing explanation</b><small>Gives a conclusion without supporting reasoning.</small></span>
<strong>-2</strong>
</button>
</div>
<label for="feedback">Submission feedback</label>
<textarea id="feedback" rows="5" placeholder="Add feedback for this student"></textarea>
</aside>
</section>
<section id="studentPreview" class="student-preview" hidden>
<p class="eyebrow">Student preview</p>
<div class="preview-row">
<h2>Published result</h2>
<strong id="previewScore">0 / 10</strong>
</div>
<p id="previewFeedback">No written feedback.</p>
</section>
</main>
<footer class="action-bar">
<div>
<span class="eyebrow">Progress</span>
<strong id="progress">1 of 3</strong>
</div>
<div class="score-display">Score: <strong id="score">0</strong>/10</div>
<p id="actionMessage" class="action-message" aria-live="polite"></p>
<div class="actions">
<button id="publishButton" class="secondary-button" type="button">Publish Grade</button>
<button id="nextButton" class="primary-button" type="button">Next Ungraded</button>
</div>
</footer>
</body>
</html>
Style the responsive workspace
Responsive CSS lets the same grader use two columns on a wide screen and one column on a narrow screen. Shared colors and spacing make the submission and rubric feel like parts of one workflow.
- Switch to the empty styles.css tab from earlier.
- Add the color palette and page foundation below:
:root {
color-scheme: light;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #172033;
background: #eef2f7;
--navy: #1f3a5f;
--blue: #2f6fed;
--green: #16794a;
--border: #d8e0eb;
--muted: #657189;
--surface: #ffffff;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background: #eef2f7;
}
What does this foundation control?
- The custom properties keep GradeFlow's main colors in one place.
- The universal sizing rule includes borders and padding inside declared dimensions.
- The body rule removes the browser margin and fills the viewport.
- Save styles.css.
- Refresh the GradeFlow page in Safari.
You should see a pale gray page background and updated system typography.
Background still white?
- Confirm styles.css is saved inside the gradeflow folder.
- Confirm the stylesheet link in index.html still points to styles.css.
Need help? Help me find why my GradeFlow stylesheet is not loading.
- Add the control typography and top bar styles below the body rule:
button,
textarea {
font: inherit;
}
button {
cursor: pointer;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 28px;
color: white;
background: var(--navy);
}
h1,
h2,
h3,
p {
margin-top: 0;
}
h1 {
margin-bottom: 0;
font-size: 1.55rem;
}
How does the header take shape?
- Buttons and the textarea inherit the page font.
- The top bar places its two sides on one horizontal line.
- The heading rules remove inconsistent browser spacing.
- Save styles.css.
- Refresh the page in Safari.
You should see a navy header with GradeFlow on the left and Not published on the right.
Header still stacked vertically?
- Check that the selector starts with .topbar.
- Check that the declaration contains display: flex.
Still stuck? Help me troubleshoot the GradeFlow top bar layout.
- Add the labels and two-column page layout below the heading rules:
.eyebrow {
margin-bottom: 5px;
color: var(--muted);
font-size: 0.74rem;
font-weight: 800;
letter-spacing: 0.09em;
text-transform: uppercase;
}
.topbar .eyebrow {
color: #bfd2ec;
}
.app-shell {
width: min(1180px, calc(100% - 32px));
margin: 26px auto 120px;
}
.grader-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 380px;
gap: 20px;
}
Why use a two-column grid?
- The flexible first column gives the student answer the available space.
- The fixed second column keeps rubric items easy to scan.
- The outer shell limits line length on large displays.
- Save styles.css.
- Refresh the page in Safari.
You should see the submission area on the left and the rubric on the right.
Rubric still below the submission?
- Confirm the grid selector is .grader-layout.
- Confirm the browser window is wide enough to show both columns.
Need help? Help me diagnose why the GradeFlow grid is not using two columns.
- Add the card surfaces and shared heading layout below the grid rules:
.submission-card,
.rubric-card,
.student-preview {
border: 1px solid var(--border);
border-radius: 14px;
background: var(--surface);
box-shadow: 0 10px 28px rgba(31, 58, 95, 0.08);
}
.submission-card,
.rubric-card {
padding: 22px;
}
.panel-heading,
.preview-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
What creates the card hierarchy?
- The shared border and shadow separate each panel from the gray page.
- The shared padding gives the submission and rubric matching inner space.
- The heading layout keeps titles beside their status details.
- Save styles.css.
- Refresh the page in Safari.
You should see two white cards with rounded corners and soft shadows.
Cards still look like plain text?
- Confirm the HTML uses submission-card and rubric-card as class names.
- Confirm the CSS selectors begin with periods.
Still plain? Help me match my GradeFlow card classes to the CSS selectors.
- Add the status badge and question divider styles below the card rules:
.status-badge {
display: inline-flex;
align-items: center;
border-radius: 999px;
padding: 7px 11px;
color: #49566e;
background: #e8edf4;
font-size: 0.8rem;
font-weight: 800;
}
.status-badge.graded,
.status-badge.published {
color: #0d633b;
background: #dff5e9;
}
.question-block {
margin: 20px 0 16px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border);
}
.question-label {
color: var(--blue);
font-weight: 800;
}
How do status and question details stand out?
- The rounded badge treatment makes grading states easy to spot.
- The green variants reserve a consistent success treatment.
- The question divider separates the prompt from the answer sheet.
- Save styles.css.
- Refresh the page in Safari.
You should see pill-shaped status badges and a blue question label above a divider.
Badges missing their pill shape?
- Confirm both status elements use the status-badge class.
- Check that the border-radius value remains inside the badge rule.
Need help? Help me fix the GradeFlow status badge styling.
- Add the answer sheet and rubric spacing styles below the question rules:
.answer-sheet {
min-height: 330px;
padding: 28px;
border: 1px solid #cad3df;
border-radius: 8px;
background: #fffefb;
font-family: Georgia, serif;
font-size: 1.12rem;
line-height: 1.8;
}
.rubric-list {
display: grid;
gap: 10px;
margin: 20px 0;
}
Why separate the answer visually?
- The warm answer surface resembles a document inside the application.
- The serif typeface distinguishes student work from interface labels.
- The rubric grid creates consistent space between criteria.
- Save styles.css.
- Refresh the page in Safari.
You should see a large paper-like answer area and evenly spaced rubric rows.
Answer area still collapsed?
- Confirm the answer container uses the answer-sheet class.
- Confirm min-height: 330px remains inside that rule.
Still collapsed? Help me troubleshoot the GradeFlow answer sheet height.
- Add the rubric button layout and description styles below the rubric list:
.rubric-item {
display: grid;
grid-template-columns: 1fr auto;
gap: 12px;
width: 100%;
padding: 14px;
border: 2px solid var(--border);
border-radius: 10px;
color: #172033;
background: white;
text-align: left;
}
.rubric-item span {
display: grid;
gap: 5px;
}
.rubric-item small {
color: var(--muted);
line-height: 1.4;
}
How do rubric buttons stay readable?
- Each button separates the criterion from its point value.
- The inner grid stacks the criterion name above its explanation.
- The muted description keeps the criterion name visually dominant.
- Save styles.css.
- Refresh the page in Safari.
You should see three full-width outlined rubric buttons with point values aligned on the right.
Point values not aligned right?
- Confirm .rubric-item uses a grid display.
- Confirm its column rule contains 1fr auto.
Need help? Help me fix the GradeFlow rubric button columns.
- Add the rubric state and feedback field styles below the button description rule:
.rubric-item strong {
color: var(--blue);
}
.rubric-item.selected {
border-color: var(--blue);
background: #edf3ff;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 800;
}
textarea {
width: 100%;
resize: vertical;
padding: 12px;
border: 1px solid #b9c5d4;
border-radius: 9px;
}
What do these states communicate?
- Blue point values make score effects easy to scan.
- The selected class defines a highlighted rubric state.
- The full-width textarea gives written feedback its own editing area.
- Save styles.css.
- Refresh the page in Safari.
You should see blue rubric values and a rounded feedback field spanning the rubric card.
Feedback field too narrow?
- Confirm the textarea rule uses a width of 100%.
- Confirm the universal sizing rule remains near the top of the stylesheet.
Still too narrow? Help me fix the GradeFlow feedback field width.
- Add the student preview and fixed action bar styles below the textarea rule:
.student-preview {
margin-top: 20px;
padding: 22px;
border-left: 5px solid var(--green);
}
.student-preview[hidden] {
display: none;
}
.action-bar {
position: fixed;
right: 0;
bottom: 0;
left: 0;
display: grid;
grid-template-columns: auto auto 1fr auto;
align-items: center;
gap: 24px;
padding: 15px 28px;
border-top: 1px solid var(--border);
background: white;
box-shadow: 0 -8px 24px rgba(31, 58, 95, 0.1);
}
Why keep the action bar fixed?
- The fixed position keeps grading controls available at the bottom of the viewport.
- The four-column grid separates progress and score from the action buttons.
- The hidden selector keeps the unpublished student preview out of view.
- Save styles.css.
- Refresh the page in Safari.
You should see the progress and grading controls anchored across the bottom of the Safari window.
Action bar scrolling with the page?
- Confirm the selector is .action-bar.
- Confirm its position is set to fixed.
Need help? Help me keep the GradeFlow action bar fixed to the viewport.
- Add the score and action button styles below the action bar rule:
.score-display {
font-size: 1.2rem;
}
.action-message {
margin: 0;
color: #9b3d2f;
font-weight: 700;
}
.actions {
display: flex;
gap: 10px;
}
.primary-button,
.secondary-button {
border-radius: 9px;
padding: 11px 16px;
font-weight: 800;
}
.primary-button {
border: 1px solid var(--blue);
color: white;
background: var(--blue);
}
How do the main actions stand out?
- The larger score text keeps the current result prominent.
- The actions container places both controls on one row.
- The primary button uses blue to emphasize queue navigation.
- Save styles.css.
- Refresh the page in Safari.
You should see a larger score and a blue Next Ungraded button at the bottom right.
Next Ungraded button not blue?
- Confirm the button uses the primary-button class.
- Confirm the blue custom property remains inside :root.
Still unstyled? Help me connect the GradeFlow primary button class to its CSS.
The final styles distinguish the secondary action and protect the workspace on narrow screens. The layout collapses when the viewport reaches its responsive breakpoint.
- Add the secondary button and responsive layout rules below the primary button rule:
.secondary-button {
border: 1px solid #aeb9c8;
color: #24344f;
background: white;
}
@media (max-width: 820px) {
.grader-layout {
grid-template-columns: 1fr;
}
.action-bar {
position: static;
grid-template-columns: 1fr 1fr;
}
.app-shell {
margin-bottom: 26px;
}
.actions {
justify-content: flex-end;
}
}
What changes on narrow screens?
- The grader switches to one column when the viewport is at most 820px wide.
- The action bar returns to the normal document flow.
- The controls use two columns to preserve usable spacing.
- Save styles.css.
- Refresh the page in Safari.
- Narrow the Safari window until the workspace changes layout.
You should see the rubric move below the submission. The action bar should move below the workspace.
Layout not responding to window width?
- Confirm the media rule starts with @media (max-width: 820px).
- Confirm the final closing brace remains at the bottom of styles.css.
Need help? Help me troubleshoot the responsive GradeFlow layout.
Your workspace now has its complete visual system. Use these tabs to compare the finished stylesheet.
✔️ Awesome, I've got everything!
Excellent. Save styles.css before testing the finished static workspace.
ⓧ I'd like to double check the full code
:root {
color-scheme: light;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #172033;
background: #eef2f7;
--navy: #1f3a5f;
--blue: #2f6fed;
--green: #16794a;
--border: #d8e0eb;
--muted: #657189;
--surface: #ffffff;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background: #eef2f7;
}
button,
textarea {
font: inherit;
}
button {
cursor: pointer;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 28px;
color: white;
background: var(--navy);
}
h1,
h2,
h3,
p {
margin-top: 0;
}
h1 {
margin-bottom: 0;
font-size: 1.55rem;
}
.eyebrow {
margin-bottom: 5px;
color: var(--muted);
font-size: 0.74rem;
font-weight: 800;
letter-spacing: 0.09em;
text-transform: uppercase;
}
.topbar .eyebrow {
color: #bfd2ec;
}
.app-shell {
width: min(1180px, calc(100% - 32px));
margin: 26px auto 120px;
}
.grader-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 380px;
gap: 20px;
}
.submission-card,
.rubric-card,
.student-preview {
border: 1px solid var(--border);
border-radius: 14px;
background: var(--surface);
box-shadow: 0 10px 28px rgba(31, 58, 95, 0.08);
}
.submission-card,
.rubric-card {
padding: 22px;
}
.panel-heading,
.preview-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.status-badge {
display: inline-flex;
align-items: center;
border-radius: 999px;
padding: 7px 11px;
color: #49566e;
background: #e8edf4;
font-size: 0.8rem;
font-weight: 800;
}
.status-badge.graded,
.status-badge.published {
color: #0d633b;
background: #dff5e9;
}
.question-block {
margin: 20px 0 16px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border);
}
.question-label {
color: var(--blue);
font-weight: 800;
}
.answer-sheet {
min-height: 330px;
padding: 28px;
border: 1px solid #cad3df;
border-radius: 8px;
background: #fffefb;
font-family: Georgia, serif;
font-size: 1.12rem;
line-height: 1.8;
}
.rubric-list {
display: grid;
gap: 10px;
margin: 20px 0;
}
.rubric-item {
display: grid;
grid-template-columns: 1fr auto;
gap: 12px;
width: 100%;
padding: 14px;
border: 2px solid var(--border);
border-radius: 10px;
color: #172033;
background: white;
text-align: left;
}
.rubric-item span {
display: grid;
gap: 5px;
}
.rubric-item small {
color: var(--muted);
line-height: 1.4;
}
.rubric-item strong {
color: var(--blue);
}
.rubric-item.selected {
border-color: var(--blue);
background: #edf3ff;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 800;
}
textarea {
width: 100%;
resize: vertical;
padding: 12px;
border: 1px solid #b9c5d4;
border-radius: 9px;
}
.student-preview {
margin-top: 20px;
padding: 22px;
border-left: 5px solid var(--green);
}
.student-preview[hidden] {
display: none;
}
.action-bar {
position: fixed;
right: 0;
bottom: 0;
left: 0;
display: grid;
grid-template-columns: auto auto 1fr auto;
align-items: center;
gap: 24px;
padding: 15px 28px;
border-top: 1px solid var(--border);
background: white;
box-shadow: 0 -8px 24px rgba(31, 58, 95, 0.1);
}
.score-display {
font-size: 1.2rem;
}
.action-message {
margin: 0;
color: #9b3d2f;
font-weight: 700;
}
.actions {
display: flex;
gap: 10px;
}
.primary-button,
.secondary-button {
border-radius: 9px;
padding: 11px 16px;
font-weight: 800;
}
.primary-button {
border: 1px solid var(--blue);
color: white;
background: var(--blue);
}
.secondary-button {
border: 1px solid #aeb9c8;
color: #24344f;
background: white;
}
@media (max-width: 820px) {
.grader-layout {
grid-template-columns: 1fr;
}
.action-bar {
position: static;
grid-template-columns: 1fr 1fr;
}
.app-shell {
margin-bottom: 26px;
}
.actions {
justify-content: flex-end;
}
}
Test the static grading experience
The workspace now presents the full grading flow. Testing a visible control reveals whether the interface can carry out that flow.
Before you click a rubric item, do you think the score will respond to your selection?
- Return to the GradeFlow page in Safari.
- Refresh the page.
- Read the binary search question in the submission pane.
- Click Correct method in the rubric pane.
The score remains at 0/10. The Correct method button also remains visually unchanged.
This shortfall is intentional
The workspace contains buttons and score displays. It has no scoring behavior yet.
This test proves the visual workflow is ready for interactive state. The next step connects rubric choices to the score and graded status.
Your grading workspace now shows every part of the instructor workflow. Next up, you will make each rubric choice update the submission's score and grading state.
Make the Rubric Score Submissions
Your styled workspace now shows the full grading flow in Safari. The static rubric buttons still leave the score frozen.
In this step, JavaScript stores each fictional submission in memory. Event handlers then turn rubric clicks into bounded scores and visible grading states.
In this step, get ready to:
- Model three fictional submissions and connect their page elements.
- Calculate bounded scores and render the current submission.
- Connect each rubric button to the scoring behavior.
Model the submissions and page elements
Each submission needs its own score and rubric selections. Keeping that state in one array lets GradeFlow update one student without changing the others.
- Select script.js in the Explorer sidebar in Visual Studio Code.
- Add the three fictional submissions by pasting this code into the empty file:
const submissions = [
{
name: "Student 01",
answer: "Binary search checks the middle value and removes half of the remaining values after each comparison. Linear search may inspect every value, so binary search needs fewer comparisons on sorted data.",
selectedRubrics: [false, false, false],
score: 0,
feedback: "",
graded: false,
published: false
},
{
name: "Student 02",
answer: "Binary search is faster because it starts in the middle instead of at the beginning.",
selectedRubrics: [false, false, false],
score: 0,
feedback: "",
graded: false,
published: false
},
{
name: "Student 03",
answer: "A linear search checks items one by one. Binary search repeatedly halves the sorted list, which gives logarithmic rather than linear growth in comparisons.",
selectedRubrics: [false, false, false],
score: 0,
feedback: "",
graded: false,
published: false
}
];
What does this data represent?
- Each object holds one fictional student's answer and grading state.
- The three values in selectedRubrics match the three rubric buttons in index.html.
- The graded and published values describe the current workflow state.
- Save script.js.
- Return to Safari.
- Refresh the GradeFlow page.
You should see the existing grading workspace reload with its header and rubric intact. This confirms that the submission data contains valid JavaScript syntax.
Did the workspace disappear?
- Check that every submission object closes with a curly brace.
- Check that the submissions array closes with a square bracket followed by a semicolon.
Still seeing a blank page? Help me find the syntax error in my submissions array.
GradeFlow also needs references to the elements it updates. Each reference connects one JavaScript variable to an existing element ID in the page.
- Add the rubric values and page references at the bottom of script.js by pasting this code:
const rubricPoints = [6, 4, -2];
let currentIndex = 0;
const studentName = document.getElementById("studentName");
const studentAnswer = document.getElementById("studentAnswer");
const gradingStatus = document.getElementById("gradingStatus");
const feedback = document.getElementById("feedback");
const progress = document.getElementById("progress");
const score = document.getElementById("score");
const rubricButtons = [
document.getElementById("rubric0"),
document.getElementById("rubric1"),
document.getElementById("rubric2")
];
How do these references work?
- The rubricPoints array maps the three rubric items to their point values.
- The currentIndex value identifies the submission currently shown.
- Each getElementById() lookup connects JavaScript to a matching element in index.html.
- The rubricButtons array keeps the three button references in scoring order.
- Save script.js.
- Refresh the GradeFlow page in Safari.
You should still see the full workspace with Student 01 and all three rubric buttons. The matching element IDs are now available to the scoring code.
Seeing a missing page element?
- Compare each ID in script.js with the matching ID in index.html.
- Check the capitalization of studentName and studentAnswer.
Need help matching the elements? Help me check my GradeFlow DOM references.
Calculate and render each score
A rubric can include both positive and negative values. GradeFlow totals the selected values while keeping every result between 0 and 10.
- Add the score calculation function at the bottom of script.js by pasting this code:
function calculateScore(submission) {
let total = 0;
for (let index = 0; index < rubricPoints.length; index += 1) {
if (submission.selectedRubrics[index]) {
total += rubricPoints[index];
}
}
if (total < 0) {
total = 0;
}
if (total > 10) {
total = 10;
}
return total;
}
How are scores bounded?
- The loop adds a point value only when the matching rubric item is selected.
- The first boundary check raises negative totals to 0.
- The second boundary check lowers totals above 10 to 10.
- The returned value becomes the current submission's score.
- Save script.js.
- Refresh the GradeFlow page in Safari.
You should see the workspace reload with a score of 0. The page remains ready for the function that detects a rubric selection.
Did the page stop loading?
- Check that both boundary conditions use the total variable.
- Check that the function ends with return total;.
Still stuck? Help me debug my calculateScore function.
A submission becomes graded as soon as at least one rubric item is selected. A small helper checks that condition without changing the submission.
- Add the selection check below calculateScore() by pasting this code:
function hasSelection(submission) {
for (let index = 0; index < submission.selectedRubrics.length; index += 1) {
if (submission.selectedRubrics[index]) {
return true;
}
}
return false;
}
What does this check do?
- The loop examines each Boolean value in selectedRubrics.
- The function returns true when it finds a selected item.
- The final false means every rubric item is clear.
- Save script.js.
- Refresh the GradeFlow page in Safari.
You should see the workspace reload with the Ungraded badge visible. That state matches a submission with no selected rubric items.
Is the page blank after the helper?
- Check that return false; sits after the loop.
- Check that selectedRubrics uses the same spelling as the submission objects.
Need another pair of eyes? Help me inspect my hasSelection function.
The render() function turns the current submission state into visible text and CSS classes. Calling it once also fills the previously empty fictional answer.
- Add the rendering function below hasSelection() by pasting this code:
function render() {
const submission = submissions[currentIndex];
submission.score = calculateScore(submission);
studentName.textContent = submission.name;
studentAnswer.textContent = submission.answer;
feedback.value = submission.feedback;
progress.textContent = `${currentIndex + 1} of ${submissions.length}`;
score.textContent = submission.score;
gradingStatus.textContent = submission.graded ? "Graded" : "Ungraded";
gradingStatus.className = submission.graded ? "status-badge graded" : "status-badge";
for (let index = 0; index < rubricButtons.length; index += 1) {
rubricButtons[index].className = submission.selectedRubrics[index]
? "rubric-item selected"
: "rubric-item";
}
}
What does render update?
- The function reads the submission at currentIndex.
- It recalculates the score before updating the visible student details.
- It switches the grading badge between the ungraded and graded styles.
- It applies the selected class to every chosen rubric button.
- Start the first render by adding this line at the bottom of script.js.
render();
Why call render here?
The function definition describes how to update the page. This call runs that process when Safari loads script.js.
- Save script.js.
- Refresh the GradeFlow page in Safari.
You should now see Student 01's full binary-search answer inside the submission pane. The progress display should read 1 of 3.
That is your first live state render. GradeFlow now fills the workspace from the submission data.
Is the student answer still empty?
- Check that render(); is the final line in script.js.
- Check that the function assigns submission.answer to studentAnswer.textContent.
Still seeing an empty answer? Help me debug my GradeFlow render function.
Connect the rubric buttons
The page can now render state, but clicks still need to change that state. The toggle function flips one rubric choice before asking the page to render again.
- Insert the toggle function directly above the final render(); line in script.js by pasting this code:
function toggleRubric(index) {
const submission = submissions[currentIndex];
submission.selectedRubrics[index] = !submission.selectedRubrics[index];
submission.graded = hasSelection(submission);
if (!submission.graded) {
submission.published = false;
}
render();
}
How does toggling work?
- The supplied index identifies which rubric choice was clicked.
- The exclamation operator flips that choice between false and true.
- The grading state follows the result from hasSelection().
- The final render exposes the updated score and button style.
- Save script.js.
- Refresh the GradeFlow page in Safari.
You should see Student 01's answer and the ungraded score again. The toggle behavior is ready for the first button connection.
Did the initial render stop working?
- Check that toggleRubric() appears above the final render(); call.
- Check that every reference to submission uses the singular spelling.
Need help with the toggle? Help me check my toggleRubric function.
Each button needs its own click handler. The first handler passes index 0 to the shared toggle function.
This insertion point is a little fiddly. Keep every handler directly above the final render(); line.
- Connect the first rubric button by inserting this handler above render();.
rubricButtons[0].onclick = function () {
toggleRubric(0);
};
What does this handler do?
A click on rubric0 now calls toggleRubric(0). Index 0 maps to the positive 6-point rubric value.
- Save script.js.
- Refresh the GradeFlow page in Safari.
- Click Correct method.
You should see the button gain a blue selected style. The score should change to 6/10.
Does the first button stay static?
- Check that the first handler uses index 0 in both places.
- Check that the handler appears before the final render(); call.
Still seeing a score of 0? Help me debug the first rubric click handler.
The second button uses the same pattern with index 1. Its 4 points can bring a fully correct answer to the maximum score.
- Connect the second rubric button by inserting this handler above render();.
rubricButtons[1].onclick = function () {
toggleRubric(1);
};
How is the second item connected?
A click on rubric1 passes index 1 into the same toggle function. That index maps to the positive 4-point value.
- Save script.js.
- Refresh the GradeFlow page in Safari.
- Click Correct method.
- Click Correct final answer.
You should see both buttons highlighted. The score should read 10/10.
Does the second button use the wrong score?
- Check that the second handler uses index 1 in both places.
- Check that the second value in rubricPoints is 4.
Need help tracing the value? Help me debug the second rubric button.
The final button applies a negative rubric value. This completes the scoring loop and demonstrates that the total stays inside its allowed bounds.
- Connect the third rubric button by inserting this handler above render();.
rubricButtons[2].onclick = function () {
toggleRubric(2);
};
How is the deduction applied?
A click on rubric2 passes index 2 into the toggle function. That index maps to the negative 2-point rubric value.
- Save script.js.
Before you test all three choices, what score do you expect after both positive items and the negative item are selected?
- Refresh the GradeFlow page in Safari.
- Click Correct method.
- Click Correct final answer.
You should see a score of 10/10 with both positive rubric items highlighted.
- Click Missing explanation.
You should now see 8/10 with all three items highlighted. The status badge should read Graded.
That is the full scoring loop working. GradeFlow now converts rubric choices into a bounded score and visible grading state.
Is the final score different?
- Check that the third handler uses index 2 in both places.
- Check that rubricPoints contains -2 as its third value.
- Refresh Safari before repeating the three-click sequence.
Still getting the wrong score? Help me trace my GradeFlow rubric state and score calculation.
✔️ Awesome, I've got everything!
Your complete scoring script now matches the intended state model and rubric interaction.
ⓧ I'd like to double check the full code
const submissions = [
{
name: "Student 01",
answer: "Binary search checks the middle value and removes half of the remaining values after each comparison. Linear search may inspect every value, so binary search needs fewer comparisons on sorted data.",
selectedRubrics: [false, false, false],
score: 0,
feedback: "",
graded: false,
published: false
},
{
name: "Student 02",
answer: "Binary search is faster because it starts in the middle instead of at the beginning.",
selectedRubrics: [false, false, false],
score: 0,
feedback: "",
graded: false,
published: false
},
{
name: "Student 03",
answer: "A linear search checks items one by one. Binary search repeatedly halves the sorted list, which gives logarithmic rather than linear growth in comparisons.",
selectedRubrics: [false, false, false],
score: 0,
feedback: "",
graded: false,
published: false
}
];
const rubricPoints = [6, 4, -2];
let currentIndex = 0;
const studentName = document.getElementById("studentName");
const studentAnswer = document.getElementById("studentAnswer");
const gradingStatus = document.getElementById("gradingStatus");
const feedback = document.getElementById("feedback");
const progress = document.getElementById("progress");
const score = document.getElementById("score");
const rubricButtons = [
document.getElementById("rubric0"),
document.getElementById("rubric1"),
document.getElementById("rubric2")
];
function calculateScore(submission) {
let total = 0;
for (let index = 0; index < rubricPoints.length; index += 1) {
if (submission.selectedRubrics[index]) {
total += rubricPoints[index];
}
}
if (total < 0) {
total = 0;
}
if (total > 10) {
total = 10;
}
return total;
}
function hasSelection(submission) {
for (let index = 0; index < submission.selectedRubrics.length; index += 1) {
if (submission.selectedRubrics[index]) {
return true;
}
}
return false;
}
function render() {
const submission = submissions[currentIndex];
submission.score = calculateScore(submission);
studentName.textContent = submission.name;
studentAnswer.textContent = submission.answer;
feedback.value = submission.feedback;
progress.textContent = `${currentIndex + 1} of ${submissions.length}`;
score.textContent = submission.score;
gradingStatus.textContent = submission.graded ? "Graded" : "Ungraded";
gradingStatus.className = submission.graded ? "status-badge graded" : "status-badge";
for (let index = 0; index < rubricButtons.length; index += 1) {
rubricButtons[index].className = submission.selectedRubrics[index]
? "rubric-item selected"
: "rubric-item";
}
}
function toggleRubric(index) {
const submission = submissions[currentIndex];
submission.selectedRubrics[index] = !submission.selectedRubrics[index];
submission.graded = hasSelection(submission);
if (!submission.graded) {
submission.published = false;
}
render();
}
rubricButtons[0].onclick = function () {
toggleRubric(0);
};
rubricButtons[1].onclick = function () {
toggleRubric(1);
};
rubricButtons[2].onclick = function () {
toggleRubric(2);
};
render();
Your rubric now grades a submission with immediate visual feedback. Next up, you will preserve written feedback and move through the ungraded queue before publishing a result.
Move Through and Publish Grades
Your rubric can now score one fictional answer. A complete grading product also needs to carry each student's work through a queue.
In this step, you'll connect the existing controls to feedback, queue navigation, and publication. The finished workflow preserves each student's state while you move through the work.
In this step, get ready to:
- Store feedback separately for each fictional submission.
- Move through the queue of ungraded submissions.
- Publish completed grades in a student-facing preview.
Store feedback for each submission
The existing submissions array already gives each student separate feedback and published values. New references connect that DOM state to the controls that are already visible.
- In script.js, locate the line that defines studentName.
- Confirm that the current reference group ends with the line that defines score.
- Replace that reference group with this expanded version:
const studentName = document.getElementById("studentName");
const studentAnswer = document.getElementById("studentAnswer");
const gradingStatus = document.getElementById("gradingStatus");
const publishStatus = document.getElementById("publishStatus");
const feedback = document.getElementById("feedback");
const progress = document.getElementById("progress");
const score = document.getElementById("score");
const actionMessage = document.getElementById("actionMessage");
const publishButton = document.getElementById("publishButton");
const nextButton = document.getElementById("nextButton");
const studentPreview = document.getElementById("studentPreview");
const previewScore = document.getElementById("previewScore");
const previewFeedback = document.getElementById("previewFeedback");
What Do These References Connect?
- The publication references connect the current submission to its status badge.
- The preview references connect the current score to the student-facing result.
- The action references let the script respond to publication and queue controls.
- Save script.js.
- Switch back to Safari from earlier.
- Refresh the GradeFlow page.
- Select Correct method.
You should still see a score of 6/10. The new references have loaded without breaking the existing rubric behavior.
Did the Page Stop Responding?
- Check that each element ID matches the reference block exactly.
- Confirm that the existing rubricButtons array still appears immediately after the new references.
Need help locating the mismatch? Help me debug the new GradeFlow DOM references.
The render() function is where saved submission state becomes visible. Expanding it keeps the instructor controls and student preview synchronized.
- In script.js, locate the existing render() function.
- Replace the entire function with this version:
function render() {
const submission = submissions[currentIndex];
submission.score = calculateScore(submission);
studentName.textContent = submission.name;
studentAnswer.textContent = submission.answer;
feedback.value = submission.feedback;
progress.textContent = `${currentIndex + 1} of ${submissions.length}`;
score.textContent = submission.score;
actionMessage.textContent = "";
gradingStatus.textContent = submission.graded ? "Graded" : "Ungraded";
gradingStatus.className = submission.graded ? "status-badge graded" : "status-badge";
publishStatus.textContent = submission.published ? "Published" : "Not published";
publishStatus.className = submission.published ? "status-badge published" : "status-badge";
publishButton.textContent = submission.published ? "Unpublish Grade" : "Publish Grade";
for (let index = 0; index < rubricButtons.length; index += 1) {
rubricButtons[index].className = submission.selectedRubrics[index]
? "rubric-item selected"
: "rubric-item";
}
studentPreview.hidden = !submission.published;
previewScore.textContent = `${submission.score} / 10`;
previewFeedback.textContent = submission.feedback || "No written feedback.";
}
What Does This Code Do?
- The publication badge reflects the current submission's published value.
- The publication button changes its label to match the available action.
- The hidden property controls whether the student preview is visible.
- The preview reads its score and feedback from the current submission.
- Save script.js.
- Switch back to Safari.
- Refresh the GradeFlow page.
You should see Not published in the top bar. The student preview should remain hidden.
Did the Rubric Stop Updating?
- Check that the replacement starts with function render().
- Confirm that the rubricButtons loop remains inside render().
Need another pair of eyes? Help me debug the expanded GradeFlow render function.
An input event handler can copy every feedback change into the current submission. The next render then restores that submission's saved text.
- In script.js, locate the rubricButtons[2].onclick handler.
- Add this feedback handler directly below it:
feedback.oninput = function () {
const submission = submissions[currentIndex];
submission.feedback = feedback.value;
if (submission.published) {
previewFeedback.textContent = submission.feedback || "No written feedback.";
}
};
How Does Feedback Stay Attached?
- The handler reads the submission at currentIndex whenever the instructor types.
- The text area value is stored in that submission's feedback property.
- Published feedback updates immediately when the student preview is visible.
- Save script.js.
- Switch back to Safari.
- Refresh the GradeFlow page.
- Enter Clear explanation. in the feedback field.
- Select Correct method.
You should see the score update while Clear explanation. remains in the feedback field. The saved value survived the render triggered by your rubric selection.
Did the Feedback Disappear?
- Confirm that the handler assigns feedback.value to the current submission's feedback property.
- Check that the handler sits outside every rubric button handler.
Still losing the text? Help me debug the GradeFlow feedback handler.
Move through ungraded submissions
The queue should start searching after the current submission. It should wrap back to the beginning until it finds unfinished work.
- In script.js, locate the end of toggleRubric().
- Add this queue function below toggleRubric().
function moveToNextUngraded() {
let nextIndex = -1;
for (let offset = 1; offset <= submissions.length; offset += 1) {
const candidateIndex = (currentIndex + offset) % submissions.length;
if (!submissions[candidateIndex].graded) {
nextIndex = candidateIndex;
break;
}
}
if (nextIndex === -1) {
actionMessage.textContent = "Every submission is graded.";
return;
}
currentIndex = nextIndex;
render();
}
How Does the Queue Search Work?
- The offset starts at the next queue position.
- The remainder calculation wraps the search back to the first submission.
- The first ungraded candidate becomes the new currentIndex.
- A nextIndex value of -1 means every submission is graded.
- Scroll to the initial render(); call at the bottom of script.js.
- Add this button assignment directly above that call:
nextButton.onclick = moveToNextUngraded;
render();
What Does This Assignment Connect?
The Next Ungraded button now calls moveToNextUngraded. A successful search finishes by rendering the selected submission.
- Save script.js.
- Switch back to Safari.
- Refresh the GradeFlow page.
- Enter Feedback for Student 01. in the feedback field.
- Select Correct method.
- Click Next Ungraded.
You should see Student 02 with an empty feedback field. The progress display should read 2 of 3.
- Select Correct method for Student 02.
- Click Next Ungraded.
You should see Student 03 with an Ungraded status.
- Select Correct method for Student 03.
Before the final click, consider what the queue can show when every submission has a rubric selection.
- Click Next Ungraded.
You should see Every submission is graded. in the action bar. The current submission stays visible because no ungraded work remains.
Did the Queue Stay on Student 01?
- Confirm that you selected a rubric item before using Next Ungraded.
- Check that nextButton.onclick references moveToNextUngraded without parentheses.
- Confirm that the queue function updates currentIndex before calling render().
Need help with the search loop? Help me debug the GradeFlow queue navigation.
Publish grades for the student preview
Publication controls when the student-facing result becomes visible. GradeFlow blocks publication until the instructor has applied at least one rubric item.
- In script.js, locate the end of the feedback.oninput handler.
- Add this publication handler below it:
publishButton.onclick = function () {
const submission = submissions[currentIndex];
if (!submission.graded) {
actionMessage.textContent = "Apply at least one rubric item before publishing.";
return;
}
submission.published = !submission.published;
render();
actionMessage.textContent = submission.published
? "The student preview is now visible."
: "The grade is hidden from the student preview.";
};
How Does Publication Stay Controlled?
- The first condition stops publication when the current submission is ungraded.
- The published value toggles the student preview between visible and hidden states.
- The render updates the publication badge and button label.
- Each submission keeps its own publication state inside the submissions array.
- Save script.js.
Your completed file now contains the feedback, queue, and publication logic. Use the comparison tab if you want to check every line before testing the full workflow.
✔️ Awesome, I've got everything!
Your completed script.js is ready. Make sure the file is saved before returning to Safari.
ⓧ I'd like to double check the full code
const submissions = [
{
name: "Student 01",
answer: "Binary search checks the middle value and removes half of the remaining values after each comparison. Linear search may inspect every value, so binary search needs fewer comparisons on sorted data.",
selectedRubrics: [false, false, false],
score: 0,
feedback: "",
graded: false,
published: false
},
{
name: "Student 02",
answer: "Binary search is faster because it starts in the middle instead of at the beginning.",
selectedRubrics: [false, false, false],
score: 0,
feedback: "",
graded: false,
published: false
},
{
name: "Student 03",
answer: "A linear search checks items one by one. Binary search repeatedly halves the sorted list, which gives logarithmic rather than linear growth in comparisons.",
selectedRubrics: [false, false, false],
score: 0,
feedback: "",
graded: false,
published: false
}
];
const rubricPoints = [6, 4, -2];
let currentIndex = 0;
const studentName = document.getElementById("studentName");
const studentAnswer = document.getElementById("studentAnswer");
const gradingStatus = document.getElementById("gradingStatus");
const publishStatus = document.getElementById("publishStatus");
const feedback = document.getElementById("feedback");
const progress = document.getElementById("progress");
const score = document.getElementById("score");
const actionMessage = document.getElementById("actionMessage");
const publishButton = document.getElementById("publishButton");
const nextButton = document.getElementById("nextButton");
const studentPreview = document.getElementById("studentPreview");
const previewScore = document.getElementById("previewScore");
const previewFeedback = document.getElementById("previewFeedback");
const rubricButtons = [
document.getElementById("rubric0"),
document.getElementById("rubric1"),
document.getElementById("rubric2")
];
function calculateScore(submission) {
let total = 0;
for (let index = 0; index < rubricPoints.length; index += 1) {
if (submission.selectedRubrics[index]) {
total += rubricPoints[index];
}
}
if (total < 0) {
total = 0;
}
if (total > 10) {
total = 10;
}
return total;
}
function hasSelection(submission) {
for (let index = 0; index < submission.selectedRubrics.length; index += 1) {
if (submission.selectedRubrics[index]) {
return true;
}
}
return false;
}
function render() {
const submission = submissions[currentIndex];
submission.score = calculateScore(submission);
studentName.textContent = submission.name;
studentAnswer.textContent = submission.answer;
feedback.value = submission.feedback;
progress.textContent = `${currentIndex + 1} of ${submissions.length}`;
score.textContent = submission.score;
actionMessage.textContent = "";
gradingStatus.textContent = submission.graded ? "Graded" : "Ungraded";
gradingStatus.className = submission.graded ? "status-badge graded" : "status-badge";
publishStatus.textContent = submission.published ? "Published" : "Not published";
publishStatus.className = submission.published ? "status-badge published" : "status-badge";
publishButton.textContent = submission.published ? "Unpublish Grade" : "Publish Grade";
for (let index = 0; index < rubricButtons.length; index += 1) {
rubricButtons[index].className = submission.selectedRubrics[index]
? "rubric-item selected"
: "rubric-item";
}
studentPreview.hidden = !submission.published;
previewScore.textContent = `${submission.score} / 10`;
previewFeedback.textContent = submission.feedback || "No written feedback.";
}
function toggleRubric(index) {
const submission = submissions[currentIndex];
submission.selectedRubrics[index] = !submission.selectedRubrics[index];
submission.graded = hasSelection(submission);
if (!submission.graded) {
submission.published = false;
}
render();
}
function moveToNextUngraded() {
let nextIndex = -1;
for (let offset = 1; offset <= submissions.length; offset += 1) {
const candidateIndex = (currentIndex + offset) % submissions.length;
if (!submissions[candidateIndex].graded) {
nextIndex = candidateIndex;
break;
}
}
if (nextIndex === -1) {
actionMessage.textContent = "Every submission is graded.";
return;
}
currentIndex = nextIndex;
render();
}
rubricButtons[0].onclick = function () {
toggleRubric(0);
};
rubricButtons[1].onclick = function () {
toggleRubric(1);
};
rubricButtons[2].onclick = function () {
toggleRubric(2);
};
feedback.oninput = function () {
const submission = submissions[currentIndex];
submission.feedback = feedback.value;
if (submission.published) {
previewFeedback.textContent = submission.feedback || "No written feedback.";
}
};
publishButton.onclick = function () {
const submission = submissions[currentIndex];
if (!submission.graded) {
actionMessage.textContent = "Apply at least one rubric item before publishing.";
return;
}
submission.published = !submission.published;
render();
actionMessage.textContent = submission.published
? "The student preview is now visible."
: "The grade is hidden from the student preview.";
};
nextButton.onclick = moveToNextUngraded;
render();
Before you click, consider whether an ungraded submission should reveal a student preview.
- Switch back to Safari.
- Refresh the GradeFlow page.
- Click Publish Grade while Student 01 is ungraded.
You should see Apply at least one rubric item before publishing. in the action bar. The student preview should remain hidden.
- Select Correct method.
- Select Correct final answer.
- Enter Clear explanation. in the feedback field.
- Click Publish Grade.
You should see a score of 10/10 with the Published status. The student preview should show the same score and feedback.
- Click Unpublish Grade.
You should see the status return to Not published. The student preview should disappear.
- Click Publish Grade again.
You should see the student preview return with the saved score and feedback. GradeFlow now controls both release states from the current submission.
Before the queue advances, consider which submission should appear after the graded first result.
- Click Next Ungraded.
You should see Student 02 with a score of 0/10. Its status should read Ungraded.
Is Publishing or Navigation Failing?
- Check that the publication handler appears before the nextButton.onclick assignment.
- Confirm that render() sets the preview's hidden property from the current submission's publication state.
- Refresh Safari after each saved code change.
Still blocked? Help me troubleshoot the completed GradeFlow workflow.
That's the full mouse-driven workflow working. GradeFlow now preserves feedback, skips completed work, controls publication, and reveals the result a student can see.
Secret mission
Add the Z Grading Shortcut
Your grading queue already advances with a button. Add a focus-aware Z keyboard shortcut that moves to the next ungraded submission without interfering with feedback entry.
Clean Up Your Resources
Clean Up Your Resources
GradeFlow runs entirely from local files on your Mac with no ongoing costs. Decide whether to keep it, pause your work, or delete it entirely.
Resources you used:
- The local gradeflow folder, which contains index.html, styles.css, and script.js.
Keep everything running
No action is needed. Choose this if you want to keep grading fictional submissions or extending GradeFlow.
- Continue working in the gradeflow workspace already open in Visual Studio Code.
- Return to the GradeFlow tab already open in Safari whenever you want to test a change.
- Keep the gradeflow folder in its current location.
Pause - I'll come back to this later
GradeFlow has no server or background process to pause. Closing its Safari tab clears the session data stored in memory.
- Close the GradeFlow tab in Safari.
- Close the gradeflow workspace in Visual Studio Code.
- Leave the gradeflow folder in its current location so you can return later.
Delete - I don't want to use this again
Removing the project folder becomes permanent after you clear your Trash. Your other files and installed applications stay untouched.
- Control-click index.html in the Explorer view in Visual Studio Code.
- Select Reveal in Finder to locate the gradeflow folder.
- Move the gradeflow folder to your Mac's Trash.
- Clear your Mac's Trash to remove the folder permanently.
- Confirm that the gradeflow folder is gone from its previous Finder location.
This deletes index.html, styles.css, and script.js. Safari and Visual Studio Code remain installed on your Mac.
Nice Work!
Nice Work!
Project complete! You built GradeFlow as a focused browser workflow for grading fictional submissions.
You've learned how to:
- Built a two-pane grading workspace for reviewing a fictional answer beside its rubric. Used responsive CSS to keep the workflow clear on narrower screens.
- Modeled three fictional submissions with independent DOM state. Connected rubric buttons through event handlers that keep scores between 0 and 10.
- Added a next-ungraded queue that preserves each submission's feedback. Controlled grade publishing so the student preview reveals only published results.
- Completed the Secret Mission by adding a focus-aware Z shortcut. The shortcut advances the queue without disrupting typing in the feedback field.
Ready to quiz yourself?