Build a Community Showcase Wall
Build a React app where learners post projects, give kudos, and filter by tech.
Introduction
30 Second Summary
Think about the last time you finished a project and wanted to show it off. A place where learners can post their work, see what others built, and cheer each other on turns solo learning into something that feels like a community.
In this project, you will build a Project Showcase Wall with React. It is a web app where learners post cards about completed projects, browse by technology, and give kudos to each other.
What You'll Build
You submit your latest project, watch your card appear on a wall of other learners' showcases, filter down to a technology you care about, and click kudos on the ones that inspire you.
By the end of this project, you'll have:
- A responsive wall of project showcase cards where you click a kudos button to show appreciation, and the counter ticks up instantly.
- A submission form where you type a project title, your name, a description, and tech tags, then see your new card appear on the wall.
- Persistent data that survives page refreshes, plus a filter bar that narrows the wall to projects using a specific technology.
- Secret Mission: Add a search bar and animated card transitions to make the wall feel polished and professional.
Are there any prerequisites?
This project assumes you are comfortable with HTML, CSS, and JavaScript. You should also have basic React knowledge, including components and JSX.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
Set Up Your React Project
To build the Project Showcase Wall, you need a place to write and run React code on your machine.
In this step, you'll confirm Node.js is installed, scaffold a React project using Vite, and launch the development server to see your starter page.
In this step, get ready to:
- Verify that Node.js is installed and up to date.
- Scaffold a React project using Vite.
- Start the development server and see the starter page in your browser.
Check your Node.js version
Vite requires Node.js version 20.19+ or 22.12+ to run. Let's confirm you have a compatible version installed.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Cursor and press Enter to open it.
- Press Cmd+J (macOS) or Ctrl+J (Windows) to open Cursor's integrated terminal.
- Check your Node.js version by running this command:
node -v
Look at the output in your terminal, then choose the tab below that matches what you see.
✔️ I see v20.19 or higher
Your Node.js version meets the requirement. You're ready to scaffold your project.
ⓧ I see a lower version number
Your current Node.js version is too old for Vite. You need to upgrade to version 20.19+ or 22.12+.
- Go to https://nodejs.org in your browser.
- Download the LTS version (this will be 22.x or higher).
- Run the installer and follow the prompts to complete the upgrade.
- Close and reopen your terminal in Cursor, then run node -v again to confirm the new version.
Why not just keep the old version?
Vite relies on newer Node.js features for fast development builds. An older version will cause install errors or silent failures when you try to run the project.
ⓧ Command not found
Node.js isn't installed on your machine yet. Let's fix that.
- Go to https://nodejs.org in your browser.
- Download the LTS version (this will be 22.x or higher).
- Run the installer and follow the prompts to complete the installation.
- Close and reopen your terminal in Cursor, then run node -v to confirm it installed correctly.
You should now see a version number starting with v (for example, v22.12.0).
Scaffold a new React project with Vite
Vite is a build tool that creates a ready-to-go React project in seconds. It also gives you a lightning-fast development server that updates the browser every time you save a file.
- Move to your Desktop by running this command:
cd ~/Desktop
- Scaffold your React project by running this command:
npm create vite@latest showcase-wall -- --template react
What does this command do?
This tells npm to run the create-vite tool, which generates a brand new project folder called showcase-wall using the React template.
The --template react flag tells Vite to set up React with JSX out of the box. Without it, Vite would prompt you to choose a framework interactively.
Seeing a prompt to install a package?
If this is your first time using this command, npm may ask you to install the create-vite package. Enter y and press Enter to continue.
You should see output confirming the project was scaffolded, ending with instructions to cd into the folder and install dependencies.
- Move into the new project folder and install dependencies by running these commands:
cd showcase-wall
npm install
You should see npm download and install the project's packages. When it finishes, you'll see a summary of the packages added.
Seeing errors during install?
Make sure you ran cd showcase-wall before npm install. Running install in the wrong folder means there is no package.json to read from.
If you see permission errors, try closing Cursor and reopening it, then running the commands again.
Help me debug my npm install errors.
Start the development server
Your project is scaffolded and dependencies are installed. Time to see it in the browser.
- Start the Vite development server by running this command:
npm run dev
What does this command do?
This starts a local development server that serves your React app. Vite watches your files for changes and instantly updates the browser whenever you save, so you never need to manually refresh.
Your terminal should display a local URL. Before you open it, what do you think you'll see in the browser?
- Hold Cmd (macOS) or Ctrl (Windows) and click the URL in your terminal to open it in your browser.
You should see the default Vite + React starter page with a counter button. The URL is typically http://localhost:5173.
Keep this server running
Leave the dev server running in your terminal for the rest of the project. Vite will automatically refresh the browser as you make changes. If you need to run other terminal commands, open a second terminal panel in Cursor by clicking the + icon in the terminal toolbar.
Don't see the starter page?
Make sure the terminal still shows the dev server running. If you accidentally closed it, run npm run dev again from inside the showcase-wall folder.
If the URL doesn't load, check that nothing else is already using port 5173. Vite will use a different port if 5173 is taken, so look at the actual URL printed in your terminal.
Help me get the Vite dev server running.
Your React development environment is ready to go. Next up, you'll build the first showcase card component and see it appear on screen.
Build Your First Showcase Card
Your dev server is running and the default Vite + React starter page is live in the browser. Now it's time to build the core building block of the showcase wall: a single project card.
Every showcase on the wall is made from the same card component. In this step, you will create a reusable ShowcaseCard component, render it with test data, style it with a dark theme, and wire up an interactive kudos button.
In this step, get ready to:
- Create a ShowcaseCard component that displays project information through props.
- Style the card with a dark theme and render it in the browser.
- Add an interactive kudos button that tracks clicks.
Create the ShowcaseCard component
React apps are built from components. Each component is a function that receives data through props and returns JSX describing what should appear on screen.
- In the Explorer sidebar in Cursor, right-click on the src folder.
- Select New File.
- Name the file ShowcaseCard.jsx.
- Add this component code to the new file:
// Component that displays a single project showcase card
function ShowcaseCard({ title, author, description, tags }) {
return (
<div className="showcase-card">
<h3 className="card-title">{title}</h3>
<p className="card-author">by {author}</p>
<p className="card-description">{description}</p>
{/* Loop through the tags array to render each technology */}
<div className="card-tags">
{tags.map((tag) => (
<span key={tag} className="tag">
{tag}
</span>
))}
</div>
</div>
)
}
export default ShowcaseCard
What does this component do?
ShowcaseCard receives four props: title, author, description, and tags. It renders each one inside a container using CSS class names.
The tags.map() call loops through the tags array and creates a <span> element for each technology. The key prop helps React track which items change when the list updates.
- Save ShowcaseCard.jsx.
Render the card in your app
The component exists, but nothing uses it yet. Let's import it into App.jsx and pass it some test data to see it on screen.
- Open src/App.jsx in Cursor.
- Replace its entire contents with this code:
import ShowcaseCard from './ShowcaseCard.jsx'
import './App.css'
function App() {
return (
<div className="app">
{/* Render a single card with hardcoded test data */}
<ShowcaseCard
title="My First Project"
author="You"
description="A sample project to test the card component."
tags={['React', 'Vite']}
/>
</div>
)
}
export default App
What does this code do?
The import statement pulls in your ShowcaseCard component so you can use it as a JSX tag. Each attribute on the <ShowcaseCard> tag passes a prop to the component.
The tags prop receives a JavaScript array of strings. It's wrapped in curly braces because anything inside JSX curly braces is evaluated as JavaScript.
- Save App.jsx and check your browser.
The browser hot-reloads automatically. You should see the card's text content on screen, but it looks unstyled because the template's default CSS is still in place.
Don't see any card text?
- Make sure the dev server is still running in your terminal. If it stopped, run npm run dev again.
- Check that you saved both ShowcaseCard.jsx and App.jsx.
- Verify the filename is exactly ShowcaseCard.jsx with a capital S and capital C. The import path is case-sensitive.
- My ShowcaseCard isn't showing up in the browser after saving. What could be wrong?
The Vite template ships with default CSS that clashes with your card design. Let's clear it and start fresh with a dark theme.
- Open src/index.css in Cursor and replace its entire contents with this single rule:
:root {
color-scheme: dark;
}
- Save index.css.
- Now open src/App.css and replace its entire contents with these base styles:
/* Reset default browser spacing */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Dark theme for the full page */
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background-color: #0f0f1a;
color: #e0e0e0;
min-height: 100vh;
}
/* Center content with a max width */
.app {
max-width: 1100px;
margin: 0 auto;
padding: 2rem 1rem;
}
/* Card container with rounded corners */
.showcase-card {
background-color: #1a1a2e;
border: 1px solid #2a2a4a;
border-radius: 12px;
padding: 1.25rem;
}
What do these styles do?
- The * selector resets margins and padding on every element so you start from a clean slate.
- The body styles set the dark background #0f0f1a and light text color for the entire page.
- The .app class centers the content with a max width of 1100px.
- The .showcase-card class creates the card container with a dark background, subtle border, and rounded corners.
- Save App.css and check your browser.
The page now has a dark background. Your card sits in a dark container with a subtle border and rounded corners. The text inside is light gray against the dark card.
- Back in src/App.css, add these styles below the .showcase-card block:
/* Card title in white */
.card-title {
font-size: 1.15rem;
color: #ffffff;
margin-bottom: 0.3rem;
}
/* Author name in purple accent */
.card-author {
font-size: 0.85rem;
color: #6366f1;
margin-bottom: 0.6rem;
}
/* Description text in muted gray */
.card-description {
font-size: 0.9rem;
color: #9ca3af;
line-height: 1.5;
margin-bottom: 0.8rem;
}
What do these styles do?
- .card-title makes the project name white and slightly larger than body text.
- .card-author colors the author name with a purple accent #6366f1.
- .card-description renders the project description in muted gray with comfortable line spacing.
- Save App.css and check your browser.
Your card now shows the title in white, the author in purple, and the description in gray. The technology tags at the bottom still appear as plain text.
- Add these tag styles below the .card-description block in src/App.css:
/* Horizontal layout for technology tags */
.card-tags {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
margin-bottom: 0.8rem;
}
/* Individual tag pill */
.tag {
padding: 0.2rem 0.6rem;
background-color: #2a2a4a;
border-radius: 12px;
font-size: 0.75rem;
color: #a5b4fc;
}
What do these styles do?
.card-tags uses flexbox to arrange tags in a horizontal row that wraps to the next line if needed. Each .tag gets a dark pill shape with light purple text, making technologies easy to scan at a glance.
- Save App.css and check your browser.
The React and Vite tags now display as small rounded pills beneath the description. Your card layout is complete.
Add the kudos button
The card displays project info, but there's no way to interact with it. A kudos button lets other learners show appreciation for a project by clicking to increment a counter.
What is useState?
useState is a React hook that lets a component remember a value between renders. You give it an initial value (like 0), and it returns the current value plus a setter function. When you call the setter, React re-renders the component with the updated value.
- Open src/ShowcaseCard.jsx in Cursor.
- Replace the entire file with this updated version that adds kudos support:
// Component that displays a project card with an interactive kudos button
function ShowcaseCard({ title, author, description, tags, kudos, onKudos }) {
return (
<div className="showcase-card">
<h3 className="card-title">{title}</h3>
<p className="card-author">by {author}</p>
<p className="card-description">{description}</p>
{/* Loop through the tags array to render each technology */}
<div className="card-tags">
{tags.map((tag) => (
<span key={tag} className="tag">
{tag}
</span>
))}
</div>
{/* Kudos button calls onKudos when clicked */}
<button className="kudos-button" onClick={onKudos}>
Kudos {kudos}
</button>
</div>
)
}
export default ShowcaseCard
What changed?
The component now accepts two additional props: kudos (the current count) and onKudos (a function to call when the button is clicked).
The component does not manage the count itself. The parent component controls the data and passes it down. This pattern makes the card reusable because different parents can provide different kudos values.
- Save ShowcaseCard.jsx.
- Now open src/App.jsx and replace its contents with this updated version:
import { useState } from 'react'
import ShowcaseCard from './ShowcaseCard.jsx'
import './App.css'
function App() {
// Track kudos count as state so clicks trigger re-renders
const [kudos, setKudos] = useState(0)
return (
<div className="app">
{/* Pass kudos count and increment function as props */}
<ShowcaseCard
title="My First Project"
author="You"
description="A sample project to test the card component."
tags={['React', 'Vite']}
kudos={kudos}
onKudos={() => setKudos(kudos + 1)}
/>
</div>
)
}
export default App
What changed?
- The useState import gives App access to React's state management.
- const [kudos, setKudos] = useState(0) creates a state variable starting at zero.
- The onKudos prop passes an arrow function that calls setKudos(kudos + 1) each time the button is clicked.
Before you save, think about what you expect to see. The card already has styled text and tags. Will the new kudos button appear styled or unstyled?
- Save App.jsx and check your browser.
The kudos button appears at the bottom of the card. Clicking it increments the number next to "Kudos". The button works, but it has no custom styling yet.
- Add these final styles at the bottom of src/App.css:
/* Kudos button - transparent by default, fills purple on hover */
.kudos-button {
padding: 0.4rem 1rem;
background-color: transparent;
border: 1px solid #2a2a4a;
border-radius: 8px;
color: #e0e0e0;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
}
.kudos-button:hover {
background-color: #6366f1;
border-color: #6366f1;
color: #ffffff;
}
What do these styles do?
The .kudos-button starts transparent with a subtle border, blending into the card. On hover, it fills with the purple accent color for a satisfying visual response. The transition: all 0.2s animates the color change smoothly.
- Save App.css and check your browser.
Your card is fully styled. The kudos button blends into the card and lights up purple when you hover over it. Click it a few times to watch the counter go up.
Kudos button not incrementing?
- Verify that onKudos={() => setKudos(kudos + 1)} appears on the ShowcaseCard tag in App.jsx. A missing onKudos prop means clicks do nothing.
- Make sure the onClick handler in ShowcaseCard.jsx calls onKudos, not a local function.
- My kudos button renders but the count doesn't change when I click it. What could be wrong?
✔️ Awesome, I've got everything!
Great. Make sure you have saved all three files before moving on.
ⓧ I'd like to double check the full code
Here is the full code for each file at this point in the project.
src/ShowcaseCard.jsx
// Component that displays a project card with an interactive kudos button
function ShowcaseCard({ title, author, description, tags, kudos, onKudos }) {
return (
<div className="showcase-card">
<h3 className="card-title">{title}</h3>
<p className="card-author">by {author}</p>
<p className="card-description">{description}</p>
{/* Loop through the tags array to render each technology */}
<div className="card-tags">
{tags.map((tag) => (
<span key={tag} className="tag">
{tag}
</span>
))}
</div>
{/* Kudos button calls onKudos when clicked */}
<button className="kudos-button" onClick={onKudos}>
Kudos {kudos}
</button>
</div>
)
}
export default ShowcaseCard
src/App.jsx
import { useState } from 'react'
import ShowcaseCard from './ShowcaseCard.jsx'
import './App.css'
function App() {
// Track kudos count as state so clicks trigger re-renders
const [kudos, setKudos] = useState(0)
return (
<div className="app">
{/* Pass kudos count and increment function as props */}
<ShowcaseCard
title="My First Project"
author="You"
description="A sample project to test the card component."
tags={['React', 'Vite']}
kudos={kudos}
onKudos={() => setKudos(kudos + 1)}
/>
</div>
)
}
export default App
src/App.css
Your App.css should contain each of these selector blocks in order. Scroll up and confirm each section is present:
- * (reset margins, padding, box-sizing).
- body (dark background, light text, font family).
- .app (max width, centered, padding).
- .showcase-card (dark card background, border, rounded corners).
- .card-title (white text, larger font).
- .card-author (purple accent text).
- .card-description (muted gray text with line spacing).
- .card-tags (flex row layout for tags).
- .tag (dark pill shape with purple text).
- .kudos-button (transparent, with border and transition).
- .kudos-button:hover (fills purple on hover).
You have a fully styled, interactive showcase card. Next up, you will turn this single card into a whole wall of project showcases by rendering multiple cards from an array of data.
Display a Wall of Cards
In the last step, you built a single ShowcaseCard component and proved it renders correctly with props and a working kudos button.
A single card is a good start, but a showcase wall needs many cards. In this step, you will create an array of sample projects, render a card for each one, and lay them out in a responsive grid.
In this step, get ready to:
- Create sample project data and render multiple cards from an array.
- Wire up per-card kudos so clicking one card only affects that card's counter.
- Style the wall as a responsive CSS grid.
Replace App.jsx with sample project data
Right now, App.jsx renders one hardcoded card with a single kudos counter. To build a real wall, you need an array of project objects that each carry their own data.
- Select all the code in src/App.jsx and delete it.
- Paste the following imports and sample data array:
import { useState } from 'react'
import ShowcaseCard from './ShowcaseCard.jsx'
import './App.css'
// Sample project data to populate the showcase wall
const SAMPLE_PROJECTS = [
{
id: 1,
title: 'CI/CD Pipeline with CodePipeline',
author: 'Alex',
description: 'Built an automated deployment pipeline using AWS CodePipeline, CodeBuild, and CodeDeploy.',
tags: ['AWS', 'DevOps', 'CI/CD'],
kudos: 5,
},
{
id: 2,
title: 'AI Chatbot with Amazon Lex',
author: 'Priya',
description: 'Created a conversational chatbot that answers customer questions using Amazon Lex and Lambda.',
tags: ['AWS', 'AI', 'Chatbot'],
kudos: 12,
},
{
id: 3,
title: 'Kubernetes Cluster on EKS',
author: 'Jordan',
description: 'Deployed a multi-container app to Amazon EKS with auto-scaling and load balancing.',
tags: ['AWS', 'Kubernetes', 'Docker'],
kudos: 8,
},
]
What does this code do?
SAMPLE_PROJECTS is an array of three project objects. Each object has an id for unique identification, plus the same fields your ShowcaseCard already expects: title, author, description, tags, and kudos.
Defining this array outside the component means it stays constant and serves as the initial dataset for the wall.
- Now paste the App function directly below the SAMPLE_PROJECTS array:
function App() {
// Store projects in state so the UI re-renders when data changes
const [projects, setProjects] = useState(SAMPLE_PROJECTS)
// Update only the matching project's kudos count
const handleKudos = (id) => {
setProjects(projects.map((project) =>
project.id === id
? { ...project, kudos: project.kudos + 1 }
: project
))
}
return (
<div className="app">
<div className="showcase-grid">
{projects.map((project) => (
<ShowcaseCard
key={project.id}
title={project.title}
author={project.author}
description={project.description}
tags={project.tags}
kudos={project.kudos}
onKudos={() => handleKudos(project.id)}
/>
))}
</div>
</div>
)
}
export default App
What does this code do?
- useState(SAMPLE_PROJECTS) stores the projects array in React state so the UI updates whenever a project changes.
- handleKudos finds the project with the matching id and creates a copy with kudos incremented by one. Every other project stays unchanged.
- projects.map(...) loops through the array and renders a ShowcaseCard for each project, passing its data as props.
- key={project.id} helps React track which card is which during re-renders, so only the changed card updates.
Why move kudos into the projects array?
In the previous step, you had a single kudos counter for one card. That approach does not scale to multiple cards because every card would share the same number.
By storing each project's kudos inside the projects array, every card owns its own count. The handleKudos function uses the project's id to update only the right entry.
- Save src/App.jsx.
- Switch to your browser. You should see three project cards stacked vertically.
The cards are stacked in a single column because there is no grid layout yet. That is expected.
Not seeing three cards?
- Check that you deleted all the old code in App.jsx before pasting. Leftover fragments cause syntax errors.
- Make sure the SAMPLE_PROJECTS array sits above function App(), not inside it.
- Confirm you saved the file. Unsaved changes will not trigger a browser refresh.
Why aren't my three showcase cards showing up?
✔️ Awesome, I've got everything!
Great. Make sure you have saved src/App.jsx before moving on.
ⓧ I'd like to double check the full code
Scroll up and confirm your src/App.jsx contains both chunks in order:
- The imports and SAMPLE_PROJECTS constant array (three project objects).
- The App function with useState, handleKudos, and the projects.map() return block, followed by export default App.
Style the wall as a responsive grid
The cards are stacked in a single column right now. A CSS Grid layout will arrange them into columns that automatically reflow when the window resizes.
- In src/App.css, find the .showcase-card rule.
- Add the following new rules directly above .showcase-card:
/* Responsive grid that flows cards into columns */
.showcase-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.25rem;
}
What does this code do?
repeat(auto-fill, minmax(300px, 1fr)) tells the browser to create as many columns as will fit, with each column at least 300 pixels wide. On a narrow screen, the cards stack into a single column automatically.
- Now update the existing .showcase-card rule. Find this line:
.showcase-card { background-color: #1a1a2e; border: 1px solid #2a2a4a; border-radius: 12px; padding: 1.25rem; }
- Replace it with this updated version that adds a hover transition:
/* Card container with smooth hover effect */
.showcase-card {
background-color: #1a1a2e;
border: 1px solid #2a2a4a;
border-radius: 12px;
padding: 1.25rem;
transition: border-color 0.2s;
}
.showcase-card:hover {
border-color: #6366f1;
}
What does this code do?
transition: border-color 0.2s animates the border colour over 0.2 seconds instead of snapping instantly. The :hover rule changes the border to the indigo accent colour when the learner hovers over a card.
- Save src/App.css and switch to your browser.
- You should see the three cards arranged side by side in a grid. Hover over a card to see its border glow indigo.
Cards still stacked in a column?
- Make sure you added the .showcase-grid rule in App.css. The class name must match showcase-grid exactly (no typos, no capital letters).
- Confirm your App.jsx has <div className="showcase-grid"> wrapping the projects.map() call.
- If your browser window is narrow, the grid may show only one column. Widen the window past about 700 pixels to see multiple columns.
Help me fix my CSS grid layout for the showcase wall.
✔️ Awesome, I've got everything!
Great. Make sure you have saved src/App.css before moving on.
ⓧ I'd like to double check the full code
Scroll up and confirm your src/App.css contains the following in order:
- The global reset rules (*, body, .app).
- The .showcase-grid rule you just added.
- The updated .showcase-card rule with transition: border-color 0.2s and the .showcase-card:hover rule.
- All the existing card-element rules (.card-title, .card-author, .card-tags, .tag, .kudos-button).
Verify your showcase wall
Before you move on, take a moment to predict what will happen when you click the kudos button on the middle card. Will all three counters go up, or only the one you clicked?
- Click the kudos button on the AI Chatbot with Amazon Lex card a few times.
- Check the other two cards. Their counters should stay unchanged.
- Resize your browser window narrower and wider. The cards should reflow from one column to two or three columns depending on the available width.
Only the clicked card's counter increases. That confirms handleKudos is targeting the correct project by id.
Your showcase wall is alive. Three cards sit in a responsive grid, each with its own independent kudos counter.
Try adding a fourth project in your head. How would you do it? Right now, you would have to edit the SAMPLE_PROJECTS array in the source code. That is not something a real user can do. In the next step, you will build a submission form so anyone can post a project directly from the browser.
Add a Submission Form
Your showcase wall has a responsive grid of project cards with working kudos buttons. However, the wall is completely static. There is no way for a learner to add their own project.
Without a submission form, this is just a display board. In this step, you will build a form component that lets learners post their own projects to the wall.
In this step, get ready to:
- Create a SubmitForm component with controlled form fields.
- Wire the form into App.jsx so new submissions appear on the wall.
- Style the form and test that validation works.
Create the SubmitForm component
React forms use controlled components. Each input's value comes from state, and typing triggers an onChange handler that updates that state.
- Create a new file called SubmitForm.jsx inside your src/ folder.
- Start building the component by pasting this code into the new file:
import { useState } from 'react'
function SubmitForm({ onSubmit }) {
// Track each form field in state
const [title, setTitle] = useState('')
const [author, setAuthor] = useState('')
const [description, setDescription] = useState('')
const [tagsInput, setTagsInput] = useState('')
const handleSubmit = (e) => {
e.preventDefault()
// Split comma-separated tags into an array
const tags = tagsInput
.split(',')
.map((tag) => tag.trim())
.filter((tag) => tag !== '')
onSubmit({ title, author, description, tags })
// Clear all fields after submission
setTitle('')
setAuthor('')
setDescription('')
setTagsInput('')
}
// Disable submit when required fields are empty
const isDisabled = title.trim() === '' || author.trim() === ''
What does this code do?
- Four useState hooks each track one form field: title, author, description, and tags.
- handleSubmit prevents the default page reload, splits the comma-separated tag text into a clean array, passes the data up to the parent via the onSubmit prop, then resets every field to empty.
- isDisabled is true whenever the title or author field is blank, so the submit button stays grayed out until both are filled.
- Add the form's JSX template directly below the isDisabled line you just pasted:
return (
<form className="submit-form" onSubmit={handleSubmit}>
<h2>Share Your Project</h2>
<div className="form-row">
<input type="text" placeholder="Project title" value={title} onChange={(e) => setTitle(e.target.value)} />
<input type="text" placeholder="Your name" value={author} onChange={(e) => setAuthor(e.target.value)} />
</div>
<textarea placeholder="Describe what you built..." value={description} onChange={(e) => setDescription(e.target.value)} />
<input type="text" placeholder="Tech tags (comma-separated, e.g. AWS, React, Python)" value={tagsInput} onChange={(e) => setTagsInput(e.target.value)} />
<button type="submit" disabled={isDisabled}>Post to Wall</button>
</form>
)
}
export default SubmitForm
What does this JSX do?
- Each input is a controlled component. Its value comes from state, and its onChange updates that state on every keystroke.
- The disabled={isDisabled} attribute on the button prevents submission when the title or author is empty.
- The title and author inputs sit side by side in a form-row div, which you will style as a flex row later.
- Save SubmitForm.jsx.
- Confirm the new file appears in the src/ folder in the Explorer sidebar.
Don't see SubmitForm.jsx in the sidebar?
- Make sure you created the file inside the src/ folder, not the project root.
- Check the filename is exactly SubmitForm.jsx with a capital S and capital F.
Help me find where my SubmitForm.jsx file went.
✔️ Awesome, I've got everything!
Great. Make sure you have saved SubmitForm.jsx before moving on.
ⓧ I'd like to double check the full code
Scroll up and confirm you have both code blocks in order: the state hooks and handleSubmit logic first, then the JSX return and export statement second. The file should start with import { useState } from 'react' and end with export default SubmitForm.
Connect the form to the showcase wall
The SubmitForm component exists, but App.jsx does not know about it yet. You need to import the component and give it a callback that adds new projects to the shared state.
- In src/App.jsx, find the import lines at the top of the file. Find this line:
import ShowcaseCard from './ShowcaseCard.jsx'
import './App.css'
- Add the SubmitForm import between those two lines, so it looks like this:
import ShowcaseCard from './ShowcaseCard.jsx'
import SubmitForm from './SubmitForm.jsx'
import './App.css'
- Now find the closing brace } of the handleKudos function. Replace everything from after that closing brace down to and including the existing return block with this code:
// Add a new project to the top of the wall
const handleSubmit = (newProject) => {
setProjects([{ ...newProject, id: Date.now(), kudos: 0 }, ...projects])
}
return (
<div className="app">
<header className="app-header">
<h1>Project Showcase Wall</h1>
<p>Show off what you've built. Give kudos to fellow learners.</p>
</header>
<SubmitForm onSubmit={handleSubmit} />
<div className="showcase-grid">
{projects.map((project) => (
<ShowcaseCard
key={project.id}
title={project.title}
author={project.author}
description={project.description}
tags={project.tags}
kudos={project.kudos}
onKudos={() => handleKudos(project.id)}
/>
))}
</div>
</div>
)
What changed in App.jsx?
- handleSubmit takes the new project data from the form, assigns it a unique id using Date.now() and sets kudos to 0, then prepends it to the projects array so it appears first.
- A <header> element adds a title and subtitle above the form.
- <SubmitForm onSubmit={handleSubmit} /> renders the form and passes the callback so it can add projects to the shared state.
- Save App.jsx and check your browser.
You should see a heading that says "Project Showcase Wall" and a form with input fields above the card grid. The form will look unstyled for now.
Seeing a blank page or an error?
- Check the import path in App.jsx matches the filename exactly: ./SubmitForm.jsx.
- Make sure both SubmitForm.jsx and App.jsx are saved. An unsaved file will not be picked up by the dev server.
- Look at the terminal running npm run dev for error messages. A syntax error in either file will show the line number.
Help me fix the error I see when importing SubmitForm.
✔️ Awesome, I've got everything!
Great. Make sure you have saved App.jsx before continuing.
ⓧ I'd like to double check the full code
Your App.jsx should now have these sections, in this order:
- Imports for useState, ShowcaseCard, SubmitForm, and App.css.
- The SAMPLE_PROJECTS array (unchanged from before).
- The App function with useState(SAMPLE_PROJECTS), handleKudos, and handleSubmit.
- A return block rendering a <header>, <SubmitForm />, and the showcase-grid div.
Style the submission form
The form is functional but it blends into the background. Time to give it the same dark-themed styling as the rest of the app.
- In src/App.css, find the body rule. Find this value:
font-family: system-ui, sans-serif;
- Replace it with this expanded font stack so the text renders consistently across operating systems:
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
- Now add the header and form container styles. Paste the following code after the .app rule and before the .showcase-grid rule:
/* Page header */
.app-header {
text-align: center;
margin-bottom: 2rem;
}
.app-header h1 {
font-size: 2.2rem;
color: #ffffff;
margin-bottom: 0.5rem;
}
.app-header p {
color: #9ca3af;
font-size: 1.1rem;
}
/* Form container */
.submit-form {
background-color: #1a1a2e;
border: 1px solid #2a2a4a;
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.submit-form h2 {
font-size: 1.3rem;
margin-bottom: 1rem;
color: #ffffff;
}
What do these styles do?
The .app-header rules center the page title. The .submit-form rules give the form a dark container with rounded corners that matches the card styling.
- Save App.css and check your browser.
The header should be centered and the form should have a dark rounded container. The input fields are still unstyled.
- Add the form field and layout styles directly below the code you just pasted:
.form-row {
display: flex;
gap: 1rem;
margin-bottom: 0.75rem;
}
.submit-form input,
.submit-form textarea {
width: 100%;
padding: 0.7rem 1rem;
border: 1px solid #2a2a4a;
border-radius: 8px;
background-color: #0f0f1a;
color: #e0e0e0;
font-size: 0.95rem;
font-family: inherit;
}
.submit-form textarea {
min-height: 80px;
resize: vertical;
margin-bottom: 0.75rem;
}
.submit-form input:focus,
.submit-form textarea:focus {
outline: none;
border-color: #6366f1;
}
What do these styles do?
The .form-row class uses flexbox to place the title and name inputs side by side. The input and textarea rules apply the dark theme, and the :focus rules highlight the active field with a purple border.
- Save App.css and check your browser.
The inputs and text area should be dark-themed with rounded corners. Clicking into a field should show a purple border.
- Add the button styles directly below the code you just pasted:
.submit-form button {
margin-top: 0.75rem;
padding: 0.7rem 1.5rem;
background-color: #6366f1;
color: #ffffff;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;
}
.submit-form button:hover:not(:disabled) {
background-color: #4f46e5;
}
.submit-form button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
What do these button styles do?
The button gets a purple background that darkens on hover. The :disabled rule drops the opacity to 40% and switches the cursor to not-allowed, giving the learner a clear visual signal that required fields are missing.
- Save App.css.
- In the browser, look at the Post to Wall button. It should be faded and unclickable because the title and author fields are empty.
- Type a project title into the first field. The button should stay disabled because the author field is still empty.
- Type your name into the author field. The button should now turn solid purple and become clickable.
Button not changing when you type?
- Make sure the isDisabled line in SubmitForm.jsx checks both title.trim() and author.trim() with || (logical OR), not &&.
- Check that the button element has disabled={isDisabled} and not disabled="isDisabled" (curly braces, not quotes).
Help me debug why my submit button is always disabled.
✔️ Awesome, I've got everything!
Great. Make sure you have saved App.css before moving on.
ⓧ I'd like to double check the full code
Your App.css should now have these rule groups in order. Scroll up and confirm each one is present:
- Reset rules (*, body, .app) with the updated font-family.
- Header styles (.app-header, .app-header h1, .app-header p).
- Form styles (.submit-form, .form-row, input, textarea, focus, button, hover, disabled).
- Grid and card styles (.showcase-grid, .showcase-card, tags, kudos button) from the previous step.
Now test the full submission flow.
- Type a project title into the Project title field.
- Type your name into the Your name field.
- Type a short description in the description area.
- Type React, JavaScript into the tech tags field.
- Click Post to Wall.
A new card should appear at the top of the grid with your project details, a "React" tag, a "JavaScript" tag, and a kudos count starting at 0. The form fields should clear automatically.
Before you do anything else, think about this: what do you expect to happen if you refresh the page right now?
- Refresh your browser.
Your new project card is gone. The wall is back to the three sample projects. Every project you submitted was stored in React state, which lives only in memory and resets on every page load.
Why did my data disappear?
React state is ephemeral. It exists for as long as the page session lasts. The moment you refresh, the browser tears down the entire JavaScript runtime and starts fresh from the initial SAMPLE_PROJECTS array.
For a community tool, that is a dealbreaker. You will fix this in the next step by saving projects to localStorage so they survive page refreshes.
Your form works, but the data does not survive a page refresh. In the next step, you will add localStorage persistence to fix that gap and build a filter bar so learners can browse projects by technology.
Persist Data and Filter by Technology
Your submission form works. New project cards appear the moment you click Post to Wall. But if you refreshed the browser after adding projects in the last step, you noticed something frustrating.
Everything vanished. For a community tool, losing data on every page load is not an option. In this step, you'll give your wall a memory with localStorage. Then you'll add a filter bar so learners can browse showcases by the technologies they care about.
In this step, get ready to:
- Save and load project data from localStorage so showcases survive page refreshes.
- Create a FilterBar component with clickable technology tags.
- Wire the filter into App so learners can browse showcases by technology.
Add localStorage persistence
The browser has a built-in storage API called localStorage that saves key-value pairs across page refreshes. You'll read from it when the app loads and write to it whenever the projects array changes.
- In App.jsx, find this import line at the top of the file:
import { useState } from 'react'
- Add useEffect to the import so the line looks like this:
import { useState, useEffect } from 'react'
- Now scroll down inside the App function and find this line:
const [projects, setProjects] = useState(SAMPLE_PROJECTS)
- Replace that entire line with the following code, which reads saved projects from localStorage on load and writes them back whenever they change:
const [projects, setProjects] = useState(() => {
// Load saved projects from localStorage, or use samples if none exist
const saved = localStorage.getItem('showcase-projects')
return saved ? JSON.parse(saved) : SAMPLE_PROJECTS
})
// Save projects to localStorage whenever the array changes
useEffect(() => {
localStorage.setItem('showcase-projects', JSON.stringify(projects))
}, [projects])
What does this code do?
The useState initializer is now a function (called a lazy initializer). It runs once when the component first mounts. It checks localStorage for a key called showcase-projects. If data exists, it parses the JSON string back into an array. If not, it falls back to SAMPLE_PROJECTS.
The useEffect hook watches the projects array. Every time it changes (a new project is added, or kudos are given), the effect converts the array to a JSON string and saves it to localStorage.
Before you test, what do you think will happen when you refresh the page after adding a project?
- Save App.jsx.
- Switch to your browser and add a new project using the form.
- Refresh the page.
Your submitted project is still on the wall. The sample projects and any new ones you added all survived the refresh.
Projects still disappearing after refresh?
- Make sure you replaced the entire original useState(SAMPLE_PROJECTS) line with the new lazy initializer. If both exist, the old one takes priority.
- Check that the useEffect is inside the App function, not outside it. Hooks must be called inside the component.
- Confirm the localStorage key matches in both places: 'showcase-projects' in both getItem and setItem.
Help me debug why my localStorage persistence isn't working in my React app.
Create the filter bar component
Each project card has tech tags like AWS or Docker. A filter bar collects every unique tag and renders them as clickable buttons so learners can narrow the wall to one technology.
- Create a new file called FilterBar.jsx inside your src/ folder and paste the following component:
function FilterBar({ tags, activeFilter, onFilterChange }) {
return (
<div className="filter-bar">
<button
className={activeFilter === 'All' ? 'filter-button active' : 'filter-button'}
onClick={() => onFilterChange('All')}
>
All
</button>
{tags.map((tag) => (
<button
key={tag}
className={activeFilter === tag ? 'filter-button active' : 'filter-button'}
onClick={() => onFilterChange(tag)}
>
{tag}
</button>
))}
</div>
)
}
export default FilterBar
What does this component do?
- It receives three props: tags (an array of unique tag strings), activeFilter (the currently selected tag), and onFilterChange (a callback to update the filter).
- The first button is always All, which clears the filter and shows every project.
- Each tag gets its own button. The className toggles between filter-button and filter-button active based on whether it matches the current filter.
- Save FilterBar.jsx.
- Confirm FilterBar.jsx appears in the src/ folder in Cursor's sidebar.
✔️ Awesome, I've got everything!
Great. Make sure you've saved the file before moving on.
ⓧ I'd like to double check the full code
function FilterBar({ tags, activeFilter, onFilterChange }) {
return (
<div className="filter-bar">
<button
className={activeFilter === 'All' ? 'filter-button active' : 'filter-button'}
onClick={() => onFilterChange('All')}
>
All
</button>
{tags.map((tag) => (
<button
key={tag}
className={activeFilter === tag ? 'filter-button active' : 'filter-button'}
onClick={() => onFilterChange(tag)}
>
{tag}
</button>
))}
</div>
)
}
export default FilterBar
Wire the filter into your app
The FilterBar component is ready but not yet connected to your app. You need to import it, track which tag is selected, compute the filtered list, and render it all in the JSX.
- In App.jsx, find the import section at the top of the file. Add this import line after the SubmitForm import:
import FilterBar from './FilterBar.jsx'
- Inside the App function, find the closing }) of your useState initializer (the one that reads from localStorage). Add this new state variable right below it, before the useEffect:
const [activeFilter, setActiveFilter] = useState('All')
- Now scroll down past the handleSubmit function. Add these two computed values below it, which extract every unique tag and filter the projects list:
// Collect all unique tags across every project
const allTags = [...new Set(projects.flatMap((project) => project.tags))]
// Filter projects based on the selected tag
const filteredProjects =
activeFilter === 'All'
? projects
: projects.filter((project) => project.tags.includes(activeFilter))
What does this code do?
- allTags uses flatMap to gather every tag from every project into one flat array. Wrapping it in a Set removes duplicates, and the spread operator converts it back to a regular array.
- filteredProjects checks the selected tag. If it's 'All', every project passes through. Otherwise, only projects whose tags array includes the selected tag are kept.
- In the return statement, find the <SubmitForm onSubmit={handleSubmit} /> line. Add the FilterBar component right below it, before the <div className="showcase-grid">:
<FilterBar tags={allTags} activeFilter={activeFilter} onFilterChange={setActiveFilter} />
- Finally, find this line inside the showcase-grid div:
{projects.map((project) => (
- Change projects.map to filteredProjects.map so it reads:
{filteredProjects.map((project) => (
- Save App.jsx and check your browser.
You should see a row of unstyled buttons above the card grid. Each button shows a technology tag from your projects. Clicking one should already filter the cards.
Seeing a blank page or error?
- Check the browser console for errors. A common one is a typo in the import path. Make sure it says './FilterBar.jsx' with the correct casing.
- Verify that you changed projects.map to filteredProjects.map in the JSX. If filteredProjects is not defined, the page will crash.
Help me fix errors after wiring FilterBar into my React app.
- Now style the filter bar. In App.css, add these styles at the end of the file (after the .submit-form button:disabled block and before the .showcase-grid block):
.filter-bar {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.filter-button {
padding: 0.4rem 1rem;
border: 1px solid #2a2a4a;
border-radius: 20px;
background-color: transparent;
color: #9ca3af;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
}
.filter-button:hover {
border-color: #6366f1;
color: #e0e0e0;
}
.filter-button.active {
background-color: #6366f1;
border-color: #6366f1;
color: #ffffff;
}
What do these styles do?
The filter bar uses flexbox with wrapping so the buttons flow across the page. Each button has a pill shape (border-radius: 20px) and starts with a subtle border. On hover, the border highlights in purple. The .active class fills the button with the same purple to show which filter is selected.
- Save App.css and check your browser.
You should see a row of styled pill buttons between the form and the card grid. The All button is highlighted in purple.
Before you click a filter, predict what will happen if you click the DevOps button. How many cards should remain on the wall?
- Click the DevOps filter button.
Only the one card tagged with DevOps remains visible. The DevOps button is now highlighted, and the All button is no longer active.
- Click All to see every card again.
- Refresh the page to confirm your projects are still persisted.
All cards reappear when you click All. After the refresh, every project is still on the wall.
Filter buttons not changing the displayed cards?
- Make sure you changed projects.map to filteredProjects.map in the JSX. If the grid still maps over projects, the filter has no effect.
- Check that activeFilter state is declared inside the App function and passed as the onFilterChange prop to FilterBar.
Help me debug why my tag filter isn't working in React.
✔️ Awesome, I've got everything!
Great. Double check you've saved both files before moving on.
ⓧ I'd like to double check the full code
Scroll through your App.jsx and confirm it has these sections in order:
- Imports for useState, useEffect, ShowcaseCard, SubmitForm, FilterBar, and App.css.
- SAMPLE_PROJECTS array with 3 project objects.
- useState with a lazy initializer that reads from localStorage.
- activeFilter state initialized to 'All'.
- useEffect that saves projects to localStorage.
- handleKudos and handleSubmit functions.
- allTags derived with Set and flatMap.
- filteredProjects computed based on activeFilter.
- JSX return with header, SubmitForm, FilterBar, and the showcase grid mapping filteredProjects.
For App.css, confirm you have .filter-bar, .filter-button, .filter-button:hover, and .filter-button.active style blocks between the submit form styles and the showcase grid styles.
Your showcase wall now has a memory and a way to browse. Projects survive page refreshes, and learners can filter the wall down to the technology they care about. Next up, you'll take on the Secret Mission to add search and animations.
Secret mission
Add Search and Card Animations
Your showcase wall works, but can learners actually find what they are looking for? A real community tool needs search. Add real-time search that works alongside tag filtering, plus smooth card animations that make the wall feel polished and professional.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine with no paid cloud services. There are no ongoing costs to worry about. Your project data lives in your browser's localStorage.
Resources you used:
- The showcase-wall project folder on your Desktop.
- Browser localStorage data stored under the showcase-projects key.
- The Vite dev server process running in your terminal.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with the Showcase Wall.
- Your dev server stays running at the URL shown in your terminal.
- Any projects you submitted are saved in your browser's localStorage and persist across refreshes.
- If you close the terminal and want to relaunch later, navigate to your showcase-wall folder and run npm run dev.
Pause - I'll come back to this later
Shut down the dev server to free up your terminal and port. Your project files and localStorage data stay intact.
- Switch to the terminal where the dev server is running.
- Press Ctrl+C to stop the server.
- Your showcase-wall folder and all project files remain on your Desktop.
- When you are ready to resume, navigate to the showcase-wall folder in your terminal and run npm run dev.
Delete - I don't want to use this again
Remove all project files and stored data to start completely fresh.
- If your dev server is still running, switch to that terminal and press Ctrl+C to stop it.
- Delete the project folder by running this command on macOS or Linux:
rm -rf ~/Desktop/showcase-wall
- On Windows, run this instead:
rmdir /s /q "$env:USERPROFILE\Desktop\showcase-wall"
- To clear your localStorage data, open your browser to any page, open DevTools with Cmd+Option+J (macOS) or Ctrl+Shift+J (Windows), and run this in the console:
localStorage.removeItem('showcase-projects')
- Confirm the key is removed by running this in the same console:
localStorage.getItem('showcase-projects')
You should see null returned, confirming the data is gone.
Nice Work!
Nice Work!
You just built a fully interactive Community Project Showcase Wall from scratch using React and Vite. Learners can post projects, browse by technology, give kudos, and come back to find everything still there.
You've learned how to:
- Built reusable React components (ShowcaseCard, SubmitForm, FilterBar) that communicate through props and callbacks, and managed shared state by lifting it up into App.
- Persisted project data with localStorage so showcases survive page refreshes, turning a throwaway demo into a tool learners can actually rely on.
- Implemented tag-based filtering to let learners browse projects by the technology they care about.
- Secret Mission: Added real-time search that combines with tag filtering, plus CSS animations for a polished user experience.
Ready to quiz yourself?