Tangy is building your next step
Shaping the lesson around what you want to make.
Tangy is building your next step
Shaping the lesson around what you want to make.
Computer Science
Turn your existing HTML and CSS foundation into a small working full-stack to-do list. You will add a focused Express server, connect a browser interface with vanilla JavaScript, and finish with a project you can run locally, use, and explain.
Shape the Interface Before the Server
3 lessonsUse the learner's existing HTML structure and CSS selector skills to create a complete static to-do interface first. The result works as a visible, dependency-free front end before any server or API is introduced.
Add the Express Server and API
4 lessonsIntroduce one new layer at a time: first a Node.js process serving the finished front end, then a tiny in-memory API for reading and creating tasks. The project becomes a real client-server application without adding a database or framework-heavy architecture.
Connect the Browser to the API
3 lessonsReplace sample data with real requests and make the core workflow usable. The learner will keep the interface simple while learning the exact browser-to-server path needed for a credible small full-stack project.
Finish the To-Do Workflow
3 lessonsComplete the project with task completion and deletion, then run a focused polish and verification pass. The final result is small enough to understand but complete enough to demonstrate a real full-stack workflow.
Public lesson
Your HTML work is already solid, so this lesson is about making the React screen’s structure match the interface: a page heading, task form, task list, empty state, and controls.
You’ll keep the work in one component for now. That gives you a clear screen to inspect before extracting smaller React components later.
Tasks
Open the React app and locate the component currently rendered for the to-do page. It may be named something like App, TodoPage, or TaskScreen.
Before changing it, open the app in the browser and note:
Do not create a new route or component if the project already has a screen for this. Extend the existing screen so the current app wiring keeps working.
Tasks
Replace the screen’s current placeholder content with an outline using the existing styling conventions from the project.
Use one main landmark, a page header, and separate section elements for the form and task area. Give each section a heading so its purpose is clear to both users and assistive technology.
function TodoPage() {
// Use the task data already present in this project.
const isEmpty = /* derive this from the task collection */;
return (
<main className="todo-page">
<header className="todo-header">
<p className="eyebrow">/* short supporting text */</p>
<h1>/* page title */</h1>
<p>/* one-sentence description */</p>
</header>
<section aria-labelledby="new-task-heading">
<h2 id="new-task-heading">/* form section heading */</h2>
{/* Add the task form here. */}
</section>
<section aria-labelledby="task-list-heading">
<h2 id="task-list-heading">/* list section heading */</h2>
{/* Render either the empty state or the task list here. */}
</section>
</main>
);
}
function TodoPage() {
// Use the task data already present in this project.
const isEmpty = /* derive this from the task collection */;
return (
<main className="todo-page">
<header className="todo-header">
<p className="eyebrow">/* short supporting text */</p>
<h1>/* page title */</h1>
<p>/* one-sentence description */</p>
</header>
<section aria-labelledby="new-task-heading">
<h2 id="new-task-heading">/* form section heading */</h2>
{/* Add the task form here. */}
</section>
<section aria-labelledby="task-list-heading">
<h2 id="task-list-heading">/* list section heading */</h2>
{/* Render either the empty state or the task list here. */}
</section>
</main>
);
}
Tasks
Inside the first section, create a form with:
labelhtmlFor and idnamerequiredtype="submit"Connect onSubmit to the project’s existing submit handler if one exists. If this lesson’s project does not have task behavior yet, use the handler placeholder already expected by the project rather than inventing a separate data flow.
<form onSubmit={/* use the existing submit handler */}>
<label htmlFor="task-title">/* label text */</label>
<input
id="task-title"
name="title"
type="text"
placeholder="/* useful example */"
required
/>
<button type="submit">/* action text */</button>
</form>
<form onSubmit={/* use the existing submit handler */}>
<label htmlFor="task-title">/* label text */</label>
<input
id="task-title"
name="title"
type="text"
placeholder="/* useful example */"
required
/>
<button type="submit">/* action text */</button>
</form>
Adapt the class names and text to your project rather than copying the example wording blindly.
Tasks
Use the task collection already used by the app. If the collection does not exist yet, create a small temporary collection in the component so you can inspect the layout. Give each temporary task a stable id, title, and completion value.
The list should use:
ul for the collectionli for each taskFill in the missing expressions and handlers for your project:
{!isEmpty && (
<ul className="task-list">
{tasks.map((task) => (
<li key={/* stable task id */} className="task-item">
<label>
<input
type="checkbox"
checked={/* task completion value */}
onChange={/* existing completion handler */}
/>
<span>{/* task title */}</span>
</label>
<div className="task-controls">
<button
type="button"
onClick={/* existing remove handler */}
aria-label={/* name the task this button removes */}
>
/* remove text */
</button>
</div>
</li>
))}
</ul>
)}
{!isEmpty && (
<ul className="task-list">
{tasks.map((task) => (
<li key={/* stable task id */} className="task-item">
<label>
<input
type="checkbox"
checked={/* task completion value */}
onChange={/* existing completion handler */}
/>
<span>{/* task title */}</span>
</label>
<div className="task-controls">
<button
type="button"
onClick={/* existing remove handler */}
aria-label={/* name the task this button removes */}
>
/* remove text */
</button>
</div>
</li>
))}
</ul>
)}
The button is explicitly type="button" so it cannot accidentally submit the separate task form.
Tasks
The empty state belongs in the same task section, but it should appear instead of the list when there are no tasks.
Add it to the isEmpty branch:
{isEmpty ? (
<div className="empty-state" role="status">
<h3>/* empty-state heading */</h3>
<p>/* tell the user what to do next */</p>
</div>
) : (
/* your task list */
)}
{isEmpty ? (
<div className="empty-state" role="status">
<h3>/* empty-state heading */</h3>
<p>/* tell the user what to do next */</p>
</div>
) : (
/* your task list */
)}
Use a heading level that follows the section’s h2. The empty state should explain the next action rather than only saying “No tasks.”
Tasks
With the page open, use the browser’s Elements panel to verify this outline:
main
├── header
│ └── h1
├── section
│ ├── h2
│ └── form
│ ├── label
│ ├── input
│ └── button
└── section
├── h2
└── ul
└── li
├── label + checkbox
└── button
main
├── header
│ └── h1
├── section
│ ├── h2
│ └── form
│ ├── label
│ ├── input
│ └── button
└── section
├── h2
└── ul
└── li
├── label + checkbox
└── button
Then test the empty branch once more by clearing the task collection.
4 modules · 13 lessons
Shape the Interface Before the Server
Add the Express Server and API
Connect the Browser to the API
Finish the To-Do Workflow
Learn by building your own version.
Remix this public project to open the workspace, follow the guided build, and let the AI mentor teach you through the work instead of doing it for you.