Community Showcase Wall

Build a React app with project cards, kudos, a post form, and tech filters.

Introduction

30 Second Summary

Every great community has a place where people share what they've made and cheer each other on. That kind of wall turns a group of strangers into a real community.

In this project, you will build a Community Showcase Wall in React. You will render project cards from data, wire up interactive kudos, build a post form, and add tech-stack filtering.

What You'll Build

You run the finished app and see a wall of project cards you can cheer with kudos, grow by posting your own project through a form, and filter by clicking any tech tag.

By the end of this project, you'll have:

  • A grid of project cards that fills the wall from seed data the moment you open the app, with an interactive kudos button on each card that increments on click.
  • A controlled form where you type a title, your name, a description, and a comma-separated tech stack, then submit and watch your card appear at the top of the wall.
  • A filter bar that extracts every unique tech tag from the wall and lets you click one to narrow the grid to matching projects.
  • Secret Mission: Add a search bar and a "sort by kudos" toggle so you can find projects by keyword and surface the most popular ones.

Are there any prerequisites?

This project assumes you are comfortable with JavaScript and React basics like JSX, components, and props. If you have built a simple React app before, you are good to go.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Scaffold Your React Project

To build the Community Showcase Wall, you need a running React development environment. Every change you make should show up instantly in the browser so you can watch the wall come together.

In this step, you'll scaffold a new React project using Vite, a modern build tool. You'll install dependencies, open the project in Cursor, and start a dev server that refreshes automatically.

In this step, get ready to:
  • Scaffold a new React project with Vite.
  • Open the project in Cursor and explore the scaffold files.
  • Start the dev server and see the starter page in your browser.
Create the project scaffold
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Scaffold a new Vite + React project by running this command:
npm create vite@latest showcase-wall -- --template react

What does this command do?

  • npm create vite@latest downloads and runs the latest version of the Vite project scaffolder.
  • showcase-wall sets the name of your project folder.
  • -- --template react tells Vite to use the React JavaScript template, so you get .jsx files ready to go.

Why Vite instead of Create React App?

Create React App has been unmaintained since 2022. Vite is the current standard for scaffolding React projects and is recommended by the React team.

It starts faster, refreshes the browser almost instantly when you save a file, and produces smaller production builds.

npm may ask to install create-vite

If npm asks whether to install the create-vite package, type y and press Enter to continue.

Vite creates a showcase-wall folder on your Desktop with starter files including index.html, src/App.jsx, and src/main.jsx.

  • Move into the project folder and install its dependencies by running these commands:
cd showcase-wall
npm install

You should see a summary showing packages were added. npm install reads the package.json that Vite generated and downloads React and all other packages into a node_modules folder.

Seeing an error from npm?

  • Make sure Node.js is installed by running node --version in your terminal. You should see a version number.
  • If you see a permission error on macOS, try prefixing the command with sudo.
  • Help me fix my npm install error.
Open the project in Cursor
  • Open the project in Cursor by running this command from the showcase-wall folder:
cursor .

The . tells Cursor to open the current folder as a workspace.

  • In the Explorer sidebar on the left, confirm you see index.html, a src folder, and package.json.

What are these files?

  • index.html is the entry point. The browser loads this file first.
  • src/main.jsx mounts the React app into the HTML page.
  • src/App.jsx is the root component where you'll build the showcase wall.
  • package.json lists the project's dependencies and scripts like npm run dev.

Cursor didn't open?

If you see "command not found", open Cursor manually instead.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Cursor and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop and select the showcase-wall folder, then click Open.

Help me open my project in Cursor.

Start the dev server

Now that the project is open in Cursor, you can start the development server to see your app in the browser.

  • In Cursor, open the built-in terminal by clicking Terminal in the top menu bar, then New Terminal. You can also press Ctrl+` (backtick) on both macOS and Windows.
  • Start the development server by running this command:
npm run dev

What does the dev server do?

The dev server watches your files and automatically refreshes the browser whenever you save a change. This is called Hot Module Replacement (HMR). You'll see every edit reflected instantly without manually reloading the page.

This command keeps your terminal occupied while the server runs. Leave it running for the rest of the project.

Before you check the browser, what do you think you'll see at http://localhost:5173?

  • Open http://localhost:5173 in your browser.

You should see the default Vite + React starter page. It has the Vite and React logos, a spinning animation, and a counter button that increments when you click it.

Don't see the starter page?

  • Check that the terminal still shows the dev server running. You should see a line with Local: http://localhost:5173/ in the output.
  • If the port number is different, use the URL shown in your terminal instead.
  • Try a hard refresh with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows).
  • My Vite dev server isn't loading in the browser.

Your development environment is ready and the starter page is live. Next up, you'll replace that starter page with a grid of project cards that form the showcase wall.

Render the Project Wall

Your React project is scaffolded and the dev server is running. Right now the browser shows a generic Vite starter page with a spinning logo.

A showcase wall with no projects is just a blank page. Seed data gives the wall life from the very first render.

Building a card component to display that data teaches you how to pass data through props and map over arrays to produce a list of React components.

In this step, get ready to:
  • Create seed data to populate the wall with six starter projects.
  • Build a reusable ProjectCard component that displays project details through props.
  • Style the wall with a dark theme and responsive CSS Grid layout.
Create the seed data file

Every project on the wall needs a title, author, description, tech stack, and kudos count. A seed data file defines all of this in one place so the wall has content the moment it loads.

  • In the Explorer sidebar in Cursor, right-click on the src folder.
  • Click New File.
  • Type data/seedProjects.js and press Enter. Cursor creates both the data/ folder and the file.
  • Paste the first three projects into the new file:
const seedProjects = [
  {
    id: 1,
    title: "Weather Dashboard",
    author: "Alex",
    description:
      "A real-time weather app that pulls forecasts from an API and displays them with animated icons.",
    techStack: ["React", "CSS", "API"],
    kudos: 5,
  },
  {
    id: 2,
    title: "Markdown Previewer",
    author: "Sam",
    description:
      "Type markdown on the left, see the rendered HTML on the right. Supports code blocks and tables.",
    techStack: ["React", "JavaScript"],
    kudos: 3,
  },
  {
    id: 3,
    title: "Budget Tracker",
    author: "Jordan",
    description:
      "Track income and expenses with category breakdowns and a visual chart.",
    techStack: ["React", "CSS", "Chart.js"],
    kudos: 8,
  },

What does this data represent?

Each object in the array is one project card. The id uniquely identifies the project, techStack is an array of technology names, and kudos tracks how many cheers a project has received.

  • Add the remaining three projects and the export directly below the third object:
  {
    id: 4,
    title: "Recipe Finder",
    author: "Taylor",
    description:
      "Search for recipes by ingredient and fetch meal ideas from a public API.",
    techStack: ["React", "API", "CSS"],
    kudos: 2,
  },
  {
    id: 5,
    title: "Portfolio Site",
    author: "Morgan",
    description:
      "A clean personal portfolio with project cards, an about section, and a contact form.",
    techStack: ["React", "CSS"],
    kudos: 6,
  },
  {
    id: 6,
    title: "CLI Task Manager",
    author: "Casey",
    description:
      "A terminal-based to-do app built with Node.js. Supports priorities and due dates.",
    techStack: ["Node.js", "JavaScript"],
    kudos: 4,
  },
];

export default seedProjects;

What does the export do?

The export default at the bottom makes the array available to any other file that imports it. Without it, React components would have no way to access this data.

  • Save seedProjects.js by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Confirm the file appears in the Explorer sidebar under src/data/seedProjects.js.

Don't see the data folder?

Make sure you typed data/seedProjects.js (with the slash) when creating the file. The slash tells Cursor to create the data/ folder automatically.

help me create the seedProjects.js file inside a data folder under src

✔️ Awesome, I've got everything!

Great. Make sure you have saved seedProjects.js before moving on.

ⓧ I'd like to double check the full code

Scroll up and confirm your file has both chunks in order. The file should start with const seedProjects = [ and contain six objects (IDs 1 through 6), ending with ]; and export default seedProjects; on the last line.

Build the ProjectCard component

Each project in the seed data needs a visual representation. A ProjectCard component receives a single project as a prop and renders its title, author, description, tech tags, and a kudos button.

  • Right-click on the src folder in the Explorer sidebar.
  • Click New File, type components/ProjectCard.jsx, and press Enter.
  • Paste this component into the new file:
function ProjectCard({ project, onKudos }) {
  return (
    <div className="project-card">
      <h3>{project.title}</h3>
      <p className="author">by {project.author}</p>
      <p className="description">{project.description}</p>
      {/* Map each tech in the array to a small tag */}
      <div className="tech-stack">
        {project.techStack.map((tech) => (
          <span key={tech} className="tech-tag">
            {tech}
          </span>
        ))}
      </div>
      {/* Kudos button calls onKudos with this project's id */}
      <button className="kudos-button" onClick={() => onKudos(project.id)}>
        {project.kudos} Kudos
      </button>
    </div>
  );
}

export default ProjectCard;

What does this component do?

The function signature { project, onKudos } destructures two props from the parent. The project prop carries all the data for one card.

The .map() call turns the techStack array into a list of JSX spans. Each span needs a unique key so React can efficiently update the list.

  • Save ProjectCard.jsx.
  • Confirm the file appears in the Explorer under src/components/ProjectCard.jsx.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file.

ⓧ I'd like to double check the full code

function ProjectCard({ project, onKudos }) {
  return (
    <div className="project-card">
      <h3>{project.title}</h3>
      <p className="author">by {project.author}</p>
      <p className="description">{project.description}</p>
      <div className="tech-stack">
        {project.techStack.map((tech) => (
          <span key={tech} className="tech-tag">
            {tech}
          </span>
        ))}
      </div>
      <button className="kudos-button" onClick={() => onKudos(project.id)}>
        {project.kudos} Kudos
      </button>
    </div>
  );
}

export default ProjectCard;
Wire up the wall in App

The seed data and card component exist, but nothing connects them to what the browser displays. The root App component needs to import both, then map over the seed array to render one ProjectCard per project.

  • Open src/App.jsx in the Explorer sidebar.
  • Select all the existing code and replace it with this:
import seedProjects from "./data/seedProjects";
import ProjectCard from "./components/ProjectCard";
import "./App.css";

function App() {
  return (
    <div className="app">
      {/* Header with app title and tagline */}
      <header className="app-header">
        <h1>Community Showcase Wall</h1>
        <p>Post your projects, discover what others built, and give kudos.</p>
      </header>
      {/* Map each seed project to a ProjectCard */}
      <div className="project-grid">
        {seedProjects.map((project) => (
          <ProjectCard key={project.id} project={project} onKudos={() => {}} />
        ))}
      </div>
    </div>
  );
}

export default App;

What does this code do?

The seedProjects.map() call loops through all six projects and returns a ProjectCard for each one. React needs a unique key prop on every item in a list so it can track which items change.

Notice that onKudos={() => {}} passes an empty function to every card. The button will render, but clicking it does nothing yet.

  • Save App.jsx.
  • Switch to your browser at http://localhost:5173. Vite auto-refreshes on save.

You should see a list of project titles, author names, descriptions, tech tags, and kudos buttons. Everything is unstyled and plain right now. That is expected.

Seeing a blank page or an error?

  • Check that the import path in App.jsx matches exactly: ./data/seedProjects and ./components/ProjectCard. A wrong path causes a module-not-found error in the terminal.
  • Confirm seedProjects.js ends with export default seedProjects;. A missing export means the import returns undefined.
  • Make sure your dev server is still running. If you stopped it, run npm run dev again from the showcase-wall folder.

Help me debug why my project cards are not showing up in the browser

✔️ Awesome, I've got everything!

Great. Double check you have saved App.jsx before moving on.

ⓧ I'd like to double check the full code

import seedProjects from "./data/seedProjects";
import ProjectCard from "./components/ProjectCard";
import "./App.css";

function App() {
  return (
    <div className="app">
      <header className="app-header">
        <h1>Community Showcase Wall</h1>
        <p>Post your projects, discover what others built, and give kudos.</p>
      </header>
      <div className="project-grid">
        {seedProjects.map((project) => (
          <ProjectCard key={project.id} project={project} onKudos={() => {}} />
        ))}
      </div>
    </div>
  );
}

export default App;
Style the wall with a dark theme

The cards render correctly but they look like unstyled text on a white page. Two CSS files will transform the wall into a polished dark-themed grid.

  • Open src/index.css in the Explorer sidebar.
  • Select all the existing code and replace it with this global reset:
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: system-ui, -apple-system, sans-serif;
  background: #0f0f1a;
  color: #e0e0e0;
  min-height: 100vh;
}

What does this CSS do?

The * selector strips default margins and padding from every element so spacing is fully under your control. The body rule sets the dark background (#0f0f1a) and light text color for the entire page.

  • Save index.css and check your browser.

The background should now be a deep dark blue, with light-colored text. The cards still have no borders or layout.

  • Open src/App.css in the Explorer sidebar.
  • Select all the existing code and replace it with the app container and header styles:
.app {
  max-width: 1100px;
  margin: 0 auto;
  padding: 2rem 1rem;
}

.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: #9a9ab0;
  font-size: 1.1rem;
}

What does this CSS do?

The .app class centers the page content with a max width of 1100px. The header styles center the title and give the subtitle a muted color.

  • Add the grid and card styles directly below the header styles:
.project-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 1.25rem;
}

.project-card {
  background: #1a1a2e;
  border: 1px solid #2a2a40;
  border-radius: 12px;
  padding: 1.25rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.project-card h3 {
  font-size: 1.15rem;
  color: #ffffff;
}

.project-card .author {
  font-size: 0.85rem;
  color: #7a7a95;
}

.project-card .description {
  font-size: 0.9rem;
  color: #b0b0c5;
  line-height: 1.4;
}

What does this CSS do?

The grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)) rule creates a responsive grid. Cards are at least 300px wide and expand to fill available space. The grid automatically wraps to fewer columns on narrow screens.

Each .project-card uses flex-direction: column so its children stack vertically with consistent gap spacing.

  • Save App.css and check your browser.

You should see the cards arranged in a grid with dark rounded borders. The tech tags and kudos button are still unstyled.

  • Add the tech tag and kudos button styles below the description styles in App.css:
.tech-stack {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.25rem;
}

.tech-tag {
  padding: 0.2rem 0.55rem;
  background: #25253d;
  border-radius: 6px;
  font-size: 0.78rem;
  color: #a0a0d0;
}

.kudos-button {
  margin-top: auto;
  padding: 0.45rem 0.9rem;
  background: transparent;
  border: 1px solid #6c63ff;
  border-radius: 8px;
  color: #6c63ff;
  font-size: 0.9rem;
  cursor: pointer;
  align-self: flex-start;
}

.kudos-button:hover {
  background: #6c63ff;
  color: #ffffff;
}

What does this CSS do?

The .tech-stack uses flexbox with wrapping so tech tags flow naturally across the card. Each .tech-tag gets a subtle dark pill shape.

The margin-top: auto on .kudos-button pushes the button to the bottom of the card regardless of how long the description is. This keeps all kudos buttons visually aligned across the grid.

  • Save App.css and check your browser.

Every card now shows rounded tech tags and a purple-bordered kudos button. Hover over a kudos button and the button fills with purple.

Cards look unstyled or misaligned?

  • Make sure you saved both index.css and App.css. Unsaved changes will not appear.
  • Check that the class names in your CSS match the JSX exactly. For example, .project-card in CSS must match className="project-card" in ProjectCard.jsx. Class names are case-sensitive.
  • Confirm App.jsx imports ./App.css. Without this import, Vite does not load the stylesheet.

Help me fix styling issues on my project cards

✔️ Awesome, I've got everything!

Great. Make sure both CSS files are saved before moving on.

ⓧ I'd like to double check the full code

Scroll up and confirm your App.css has all three chunks in order. It should start with the .app container, then the .project-grid and .project-card rules, and end with .kudos-button:hover.

Your index.css should look like this:

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: system-ui, -apple-system, sans-serif;
  background: #0f0f1a;
  color: #e0e0e0;
  min-height: 100vh;
}

Before you try it, what do you think will happen when you click one of the kudos buttons?

  • Click any kudos button on the wall.

Nothing happens. The count stays exactly where it started. That is intentional.

Why doesn't the kudos button work?

The seed data is imported as a static JavaScript constant. Clicking the button calls the empty () => {} function you passed as onKudos, which does nothing.

For the UI to update on click, the data needs to live in React state, not in a static import. The next step fixes exactly this.

Your wall looks great, but those kudos buttons are just for show. Next up, you will convert the static data into React state and wire up a handler so every click actually updates the count on screen.

Make Kudos Interactive

In the last step, you built a wall of project cards and tried clicking a kudos button. Nothing happened.

The reason is that seedProjects is a static import. React has no way to know the data changed, so it never re-renders. Converting the data to useState gives React control over the data, so any update triggers a fresh render.

In this step, get ready to:
  • Import useState and convert seed data to React state.
  • Write a handleKudos function that updates the count immutably.
  • Verify the kudos button increments on click.
Lift seed data into state

Right now, App.jsx imports seedProjects as a plain JavaScript array and passes it straight into the JSX. React does not watch plain variables for changes.

  • In App.jsx, find the import section at the top of the file:
import seedProjects from "./data/seedProjects";
import ProjectCard from "./components/ProjectCard";
import "./App.css";
  • Replace the import section with the following, which adds a useState import from React:
import { useState } from "react";
import seedProjects from "./data/seedProjects";
import ProjectCard from "./components/ProjectCard";
import "./App.css";

What is useState?

useState is a React hook that stores data and tells React to re-render the component whenever that data changes. It returns two things: the current value and a setter function to update it.

  • Now find the first line inside the App function:
function App() {
  return (
  • Add a state initialization line between function App() { and return (, so the top of your function looks like this:
function App() {
  // Store the projects in React state so changes trigger a re-render
  const [projects, setProjects] = useState(seedProjects);

What does this line do?

const [projects, setProjects] = useState(seedProjects) creates a state variable called projects initialized with the seed data. setProjects is the function you call to update that data and trigger a re-render.

  • Inside the return block, find this line:
{seedProjects.map((project) => (
  • Replace seedProjects with projects so the line reads:
{projects.map((project) => (
  • Save App.jsx.
  • Check your browser. The same six project cards should still appear, looking identical to before.

Cards disappeared or page is blank?

  • Check that the useState import uses curly braces: import { useState } from "react". Missing braces causes an error because useState is a named export.
  • Make sure the map now reads projects.map, not seedProjects.map. If you changed one but not the other, the variable name won't match.
  • help me debug why my cards disappeared after adding useState
Create the kudos handler and pass it down

The data is in state now, but there is no function to update it yet. You need a handler that finds the clicked project and returns a new array with its kudos count incremented by one.

  • In App.jsx, add the handleKudos function below the useState line and above the return statement:
  function handleKudos(projectId) {
    setProjects(
      projects.map((p) =>
        p.id === projectId ? { ...p, kudos: p.kudos + 1 } : p
      )
    );
  }

What does this code do?

projects.map() loops through every project and returns a brand-new array. For the project whose id matches, it creates a copy with { ...p, kudos: p.kudos + 1 }. Every other project passes through unchanged.

This is called an immutable state update. React compares the old array to the new one and re-renders only what changed. Mutating the original object directly would skip that comparison.

  • Now find the onKudos prop inside the projects.map call:
<ProjectCard key={project.id} project={project} onKudos={() => {}} />
  • Replace onKudos={() => {}} with onKudos={handleKudos} so the lines look like this:
<ProjectCard
  key={project.id}
  project={project}
  onKudos={handleKudos}
/>
  • Save App.jsx.

Before you click a kudos button, take a guess. What do you think will happen when you click the kudos button on the Budget Tracker card, which currently shows 8 Kudos?

  • Click the kudos button on any project card.

The number increments by one each time you click. Click it a few more times to see the count climb.

Kudos count not changing?

  • Check that you replaced the empty function () => {} with handleKudos (no parentheses). Writing onKudos={handleKudos()} calls the function immediately instead of passing it.
  • Make sure handleKudos is defined inside the App function, not outside it. It needs access to projects and setProjects.
  • help me figure out why my kudos button is not updating the count
Confirm ProjectCard is already wired

You did not need to change ProjectCard.jsx at all. The component was already set up in Step 2 to accept onKudos as a prop and call it on click.

  • Open src/components/ProjectCard.jsx and confirm the button already has this wiring:
<button className="kudos-button" onClick={() => onKudos(project.id)}>
  {project.kudos} Kudos
</button>

How does the data flow?

App owns the projects state and defines handleKudos. It passes the function down to ProjectCard as the onKudos prop.

When the learner clicks the button, ProjectCard calls onKudos(project.id), which runs handleKudos in App. That creates a new array with the updated count, setProjects saves it, and React re-renders the card with the new number. This pattern is called lifting state up.

✔️ Awesome, I've got everything!

Great. Make sure you have saved App.jsx.

ⓧ I'd like to double check the full code

The only file you changed in this step is App.jsx. Scroll up and confirm these three changes are in place, in order:

  • Line 1 imports { useState } from "react".
  • Inside the App function, const [projects, setProjects] = useState(seedProjects) appears before the handleKudos function.
  • handleKudos uses projects.map() to return a new array with the matching project's kudos incremented.
  • The JSX renders projects.map() (not seedProjects.map()) and each ProjectCard receives onKudos={handleKudos}.

Your kudos button is alive. Every click proves that React state, immutable updates, and props-as-callbacks work together to keep the UI in sync. Next up, you will build a form that lets anyone post a brand-new project to the wall.

Build the Post Form

Your kudos button works, and the wall feels alive. But every project on it came from seedProjects.js. A real community showcase needs a way for people to post their own projects.

In this step, you'll build a form that lets users type a title, name, description, and tech stack, then submit it to add a new card to the wall. This introduces controlled forms, where every input's value is tied to React state so your component always knows what the user has typed.

In this step, get ready to:
  • Create a ProjectForm component with four controlled inputs.
  • Write a submission handler that validates fields and splits the tech stack.
  • Connect the form to App so new cards appear at the top of the wall.
Create the ProjectForm component

In a controlled form, each input's value comes from state, and each onChange updates that state. React is always the single source of truth for what the user has typed.

  • Create a new file at src/components/ProjectForm.jsx and paste the following code:
import { useState } from "react";

function ProjectForm({ onAddProject }) {
  // Each input gets its own state variable
  const [title, setTitle] = useState("");
  const [author, setAuthor] = useState("");
  const [description, setDescription] = useState("");
  const [techInput, setTechInput] = useState("");

  return (
    <form className="project-form">
      <h2>Share Your Project</h2>
      <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)}
      />
    </form>
  );
}

export default ProjectForm;

What does this code do?

Four pieces of state (title, author, description, techInput) track the value of each form field.

Each <input> is controlled: its value is tied to state, and its onChange calls the matching setter. React always knows what the user has typed.

The form has a title input and a name input so far. Let's make it visible in the browser before adding the rest.

  • In src/App.jsx, find the existing import lines at the top of the file:
import ProjectCard from "./components/ProjectCard";
  • Add this import directly below it:
import ProjectForm from "./components/ProjectForm";
  • Now find the closing </header> tag inside the return statement:
      </header>
  • Add this line directly below it, above the <div className="project-grid">:
      <ProjectForm onAddProject={handleAddProject} />
  • Save both ProjectForm.jsx and App.jsx.
  • Check your browser. You should see a heading that says Share Your Project with two text inputs above the project grid. The form is unstyled for now.

Don't see the form?

Check the terminal for errors. A common cause is a mismatched import path. Make sure the import says ./components/ProjectForm with a capital P and capital F.

If the browser shows a blank page, open the browser console for error details.

Help me debug why my ProjectForm component is not appearing in the browser.

The form is showing up, but it's missing the description and tech stack fields. Let's add those now.

  • In ProjectForm.jsx, find the closing /> of the second input (the one with placeholder "Your name").
  • Add the following code directly below it, above the </form> tag:
      <textarea
        placeholder="Describe your project..."
        value={description}
        onChange={(e) => setDescription(e.target.value)}
      />
      <input
        type="text"
        placeholder="Tech stack (comma-separated, e.g. React, CSS, API)"
        value={techInput}
        onChange={(e) => setTechInput(e.target.value)}
      />
      <button type="submit">Post Project</button>

What are these new fields?

The <textarea> gives users a multi-line area for their project description. It's controlled with the same value + onChange pattern as the text inputs.

The tech stack input accepts a comma-separated string like React, CSS, API. You'll split this into an array when the form submits.

  • Save ProjectForm.jsx.
  • Check your browser. The form now has all four fields and a Post Project button.

The form works, but it looks out of place against the dark theme. Let's add styles to match the rest of the wall.

  • In src/App.css, find the closing brace of the .app-header p rule.
  • Add the following styles directly below it, above the .project-grid rule:
.project-form {
  background: #1a1a2e;
  border: 1px solid #2a2a40;
  border-radius: 12px;
  padding: 1.5rem;
  margin-bottom: 2rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.project-form h2 {
  font-size: 1.3rem;
  color: #ffffff;
}

.project-form input,
.project-form textarea {
  padding: 0.6rem 0.8rem;
  border: 1px solid #2a2a40;
  border-radius: 8px;
  background: #12121f;
  color: #e0e0e0;
  font-size: 0.95rem;
  font-family: inherit;
}
  • Continue adding these styles directly below the block you just pasted:
.project-form input:focus,
.project-form textarea:focus {
  outline: none;
  border-color: #6c63ff;
}

.project-form textarea {
  resize: vertical;
  min-height: 70px;
}

.project-form button {
  padding: 0.6rem 1.2rem;
  background: #6c63ff;
  color: #ffffff;
  border: none;
  border-radius: 8px;
  font-size: 0.95rem;
  cursor: pointer;
  align-self: flex-start;
}

.project-form button:hover {
  background: #5a52d5;
}

What do these styles do?

The form container gets the same dark card treatment as the project cards. The inputs use a darker background so they feel recessed into the form.

The :focus styles add a purple border highlight when the user clicks into an input. The button uses the same purple accent color as the kudos button.

  • Save App.css.
  • Check your browser. The form now has a dark card background, styled inputs with purple focus highlights, and a purple Post Project button.

Form still looks unstyled?

Make sure the form element in ProjectForm.jsx uses className="project-form". CSS class names are case-sensitive.

Also verify you pasted the new styles between .app-header p and .project-grid in App.css. Misplacing them inside another rule's braces will break the styles.

Help me figure out why my form styles are not applying.

Handle form submission

The form renders, but clicking the button reloads the page (the browser's default form behavior). You need a submit handler that prevents the reload, checks the fields, and passes the data up to App.

  • In ProjectForm.jsx, find the line that says return (.
  • Add the following function directly above it:
  function handleSubmit(e) {
    // Stop the browser from reloading the page
    e.preventDefault();

    // Don't submit if required fields are empty
    if (!title.trim() || !author.trim() || !description.trim()) return;

    // Split comma-separated tech input into an array
    const techStack = techInput
      .split(",")
      .map((t) => t.trim())
      .filter((t) => t.length > 0);

    // Send the new project data up to the parent
    onAddProject({
      title: title.trim(),
      author: author.trim(),
      description: description.trim(),
      techStack,
    });

    // Clear all fields after submission
    setTitle("");
    setAuthor("");
    setDescription("");
    setTechInput("");
  }

What does handleSubmit do?

  • e.preventDefault() stops the browser from reloading the page on form submission.
  • The if guard checks that the title, author, and description are not blank. If any are empty, the function exits early.
  • The tech stack string gets split on commas, trimmed, and filtered to remove empties. Typing React, CSS, API becomes ["React", "CSS", "API"].
  • After sending the data to the parent via onAddProject, all four state values reset to empty strings so the form is cleared.

Now you need to wire this function to the form's submit event.

  • In ProjectForm.jsx, find this line in the return statement:
    <form className="project-form">
  • Replace it with this line to attach the submit handler:
    <form className="project-form" onSubmit={handleSubmit}>
  • Save ProjectForm.jsx.
  • In the browser, click Post Project without filling in any fields. The page should NOT reload. Nothing visible happens because the validation guard exits early.

Why test with empty fields?

This confirms two things at once. First, preventDefault is working because the page stays put. Second, the validation guard is doing its job by silently blocking the empty submission.

Page still reloads when you click submit?

Make sure the form tag has onSubmit={handleSubmit} and not onClick. The submit event fires on the <form> element, not the button.

Also confirm that e.preventDefault() is the first line inside handleSubmit. If it appears after other code that throws an error, the prevent call never runs.

Help me figure out why my form is still reloading the page on submit.

Connect the form to App

The form prevents reload and validates input, but onAddProject is still undefined. You need a function in App that creates a complete project object and prepends it to the state array.

  • In src/App.jsx, find the closing brace of the handleKudos function.
  • Add the following function directly below it:
  function handleAddProject(newProject) {
    // Build a full project object with a unique ID and zero kudos
    const project = {
      ...newProject,
      id: Date.now(),
      kudos: 0,
    };
    // Prepend so new projects appear at the top of the wall
    setProjects([project, ...projects]);
  }

What does handleAddProject do?

  • It receives the form data from ProjectForm (title, author, description, techStack) and adds an id using Date.now() and sets kudos to zero.
  • The spread syntax [project, ...projects] creates a brand new array with the new project at the front. This is an immutable update. React detects the new array reference and re-renders.

The <ProjectForm> in your JSX already has onAddProject={handleAddProject} from the line you added earlier. Everything is wired up.

  • Save App.jsx.

Before you test, what do you think will happen when you fill in all four fields and click Post Project? Where will the new card appear in the grid?

  • In the browser, type a title, your name, a short description, and a comma-separated tech stack (e.g. React, CSS).
  • Click Post Project.

Your new project card appears at the very top of the grid with 0 kudos. The form clears itself, ready for the next submission.

Card not appearing after you submit?

Open the browser console and check for errors. If you see a message about onAddProject is not a function, make sure the <ProjectForm> tag in App.jsx has onAddProject={handleAddProject}.

If the form clears but no card shows, confirm that handleAddProject calls setProjects([project, ...projects]) with square brackets. Missing the brackets means you are passing arguments, not creating an array.

Help me debug why my new project card is not appearing on the wall after I submit the form.

✔️ Awesome, I've got everything!

Great. Double check you've saved all your files.

ⓧ I'd like to double check the full code

Scroll up and confirm each of these pieces is present and in order:

  • ProjectForm.jsx: imports useState, declares four state variables (title, author, description, techInput), has the handleSubmit function, and returns a form with onSubmit={handleSubmit} containing four controlled inputs and a submit button.
  • App.jsx: imports ProjectForm, has handleAddProject below handleKudos, and renders <ProjectForm onAddProject={handleAddProject} /> between the header and the project grid.
  • App.css: has the .project-form styles (container, h2, inputs, focus, textarea, button, button hover) between .app-header p and .project-grid.

Users can now post their own projects and cheer each other on with kudos. As the wall grows, finding specific projects gets harder. Next up, you'll build a filter bar that lets you narrow the wall to just the tech stack you care about.

Filter Projects by Tech

Your showcase wall is alive. Users can post their projects and give each other kudos.

But with a growing wall, there is no way to find only the React projects or only the API projects. A filter bar that extracts unique tech tags and narrows the grid solves this. It also teaches you an important React pattern: deriving data from state without duplicating it.

In this step, get ready to:
  • Create a FilterBar component that renders clickable tech tag buttons.
  • Add filter state and derived values to App so the grid responds to filter clicks.
  • Handle the empty state when no projects match the active filter.
Create the FilterBar component

The filter bar needs to show an "All" button and one button for every unique tech tag on the wall. It does not decide which projects to show. It just tells App which tag the user picked.

  • Create a new file at src/components/FilterBar.jsx and add this code:
function FilterBar({ allTechs, activeFilter, onFilterChange }) {
  return (
    <div className="filter-bar">
      <span className="filter-label">Filter by tech:</span>
      {/* "All" resets the filter to an empty string */}
      <button
        className={activeFilter === "" ? "filter-tag active" : "filter-tag"}
        onClick={() => onFilterChange("")}
      >
        All
      </button>
      {/* One button per unique tech tag */}
      {allTechs.map((tech) => (
        <button
          key={tech}
          className={
            activeFilter === tech ? "filter-tag active" : "filter-tag"
          }
          onClick={() => onFilterChange(tech)}
        >
          {tech}
        </button>
      ))}
    </div>
  );
}

export default FilterBar;

What does this code do?

FilterBar receives three props from its parent. allTechs is an array of unique tech names. activeFilter is the currently selected tag (or an empty string for "All"). onFilterChange is a callback that fires when the user clicks a tag.

The component maps over allTechs to produce one button per tag. Whichever button matches activeFilter gets the active CSS class so it highlights visually.

  • Save FilterBar.jsx.

You will not see it on the page yet because nothing imports or renders it. The next substep wires it in.

✔️ Awesome, I've got everything!

Great. Make sure you saved FilterBar.jsx.

ⓧ I'd like to double check the full code

Here is the complete FilterBar.jsx file. Compare yours against it:

function FilterBar({ allTechs, activeFilter, onFilterChange }) {
  return (
    <div className="filter-bar">
      <span className="filter-label">Filter by tech:</span>
      <button
        className={activeFilter === "" ? "filter-tag active" : "filter-tag"}
        onClick={() => onFilterChange("")}
      >
        All
      </button>
      {allTechs.map((tech) => (
        <button
          key={tech}
          className={
            activeFilter === tech ? "filter-tag active" : "filter-tag"
          }
          onClick={() => onFilterChange(tech)}
        >
          {tech}
        </button>
      ))}
    </div>
  );
}

export default FilterBar;
Add filter state and logic to App

The FilterBar component is ready, but App does not know about it yet. You need to add a piece of state to track the active filter, then derive two new values from the existing projects array: the list of unique tech tags and the filtered project list.

What is derived state?

Derived state is a value you calculate from existing state instead of storing separately. You already have the projects array, which contains every tech tag. Instead of keeping a separate list of tags in state (which could go out of sync), you compute it fresh on every render.

  • In src/App.jsx, find this import line near the top:
import "./App.css";
  • Add this new import line directly above it:
import FilterBar from "./components/FilterBar";
  • Now find the existing useState line inside the App function:
const [projects, setProjects] = useState(seedProjects);
  • Add this new state variable directly below it:
const [activeFilter, setActiveFilter] = useState("");
  • Scroll down to the closing } of the handleAddProject function. Add these derived values below it:
  // Collect every unique tech tag and sort alphabetically
  const allTechs = [...new Set(projects.flatMap((p) => p.techStack))].sort();

  // Show only matching projects when a filter is active
  const filteredProjects = activeFilter
    ? projects.filter((p) => p.techStack.includes(activeFilter))
    : projects;

What does this code do?

allTechs uses flatMap to collect every tech tag from every project into one flat array. Wrapping it in a Set removes duplicates. The spread into a new array and .sort() gives you a clean alphabetical list.

filteredProjects checks whether activeFilter is set. If it is, only projects whose techStack includes that tag survive the filter. If it is empty, the full array passes through.

Now the grid needs to render from filteredProjects instead of the raw projects array.

  • Inside the return statement, find this line:
        {projects.map((project) => (
  • Replace projects with filteredProjects, so the line now reads:
        {filteredProjects.map((project) => (
  • Save App.jsx and refresh your browser.

The wall should look exactly the same as before. That is correct. The filter defaults to an empty string, so filteredProjects includes every project.

Seeing a blank page or errors?

  • Check that the FilterBar import path matches the file you created: ./components/FilterBar.
  • Make sure you typed filteredProjects (not filterProjects or filterdProjects) in both the variable declaration and the .map() call.
  • Check your browser console for a red error message. Why is my showcase wall showing a blank page after adding filter logic?
Render the FilterBar and handle empty states

The filtering logic is in place, but the FilterBar component is not rendered in the JSX yet. You also need to handle the case where no projects match the selected filter.

  • In App.jsx, find this line inside the return:
      <ProjectForm onAddProject={handleAddProject} />
  • Add the FilterBar component directly below it, passing the three props it needs:
      <FilterBar
        allTechs={allTechs}
        activeFilter={activeFilter}
        onFilterChange={setActiveFilter}
      />

Why pass setActiveFilter directly?

FilterBar calls onFilterChange with a single string argument (the tech name or an empty string). That is exactly the shape that setActiveFilter expects, so you can pass the setter directly without wrapping it in another function.

  • Now find the closing </div> of the project-grid div. Add the empty-state message directly below it:
      {filteredProjects.length === 0 && (
        <p className="no-results">No projects match that filter.</p>
      )}

What does this code do?

This uses short-circuit rendering. When filteredProjects.length === 0 is true, React renders the <p> element. When it is false, nothing renders. This way the message only appears when every project has been filtered out.

  • Save App.jsx and refresh your browser.

You should see a row of unstyled buttons between the form and the grid, starting with "Filter by tech:" and then one button per unique tech tag. They work, but they look bare. Time to style them.

  • In src/App.css, find the .project-form button:hover block. Add these filter bar styles directly below it:
.filter-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 1.5rem;
}

.filter-label {
  color: #9a9ab0;
  font-size: 0.9rem;
}

.filter-tag {
  padding: 0.35rem 0.75rem;
  border: 1px solid #2a2a40;
  border-radius: 20px;
  background: #1a1a2e;
  color: #c0c0d0;
  font-size: 0.85rem;
  cursor: pointer;
}

.filter-tag:hover {
  border-color: #6c63ff;
  color: #ffffff;
}

.filter-tag.active {
  background: #6c63ff;
  border-color: #6c63ff;
  color: #ffffff;
}

What does this code do?

filter-bar uses flexbox with flex-wrap so the tag buttons flow onto a new row if they overflow. Each filter-tag gets a pill shape from border-radius: 20px.

The .active class fills the selected tag with the purple accent color so the user always knows which filter is on.

  • Now scroll to the very bottom of App.css, after the .kudos-button:hover block. Add this style for the empty-state message:
.no-results {
  text-align: center;
  color: #7a7a95;
  padding: 2rem;
}
  • Save App.css.

Before you click anything, what do you think will happen when you click the "React" filter tag? Will you see all six projects, or fewer?

  • Click the React filter tag in the filter bar.

The grid narrows to only the projects that have "React" in their tech stack. The React tag turns purple to show it is active.

  • Click All and confirm every project comes back.
  • Use the form to post a new project with a tech tag that does not exist yet (for example, Python).
  • Check that the new tag automatically appears in the filter bar.

Why does the new tag appear automatically?

allTechs is derived from the projects state array. Every time projects changes (like when you add a new project), React re-renders the component and recalculates allTechs. No extra state to update, no extra logic to write.

Filter not working?

  • Make sure you are rendering filteredProjects.map in the grid, not projects.map. If the grid still shows all projects when you click a tag, this is the most likely cause.
  • Confirm that onFilterChange={setActiveFilter} is passed to the FilterBar component. Without it, clicking a tag does nothing.
  • Check that the filter-tag and filter-tag active class names in your CSS match the ones in FilterBar.jsx.
  • My filter bar clicks are not filtering the grid. What could be wrong?

✔️ Awesome, I've got everything!

Great. Make sure both App.jsx and App.css are saved.

ⓧ I'd like to double check the full code

Scroll up and confirm each chunk is present in your files. For App.jsx, check these in order:

  • The FilterBar import line sits above the App.css import.
  • const [activeFilter, setActiveFilter] = useState("") sits below the projects state.
  • allTechs and filteredProjects are declared below handleAddProject.
  • <FilterBar ... /> sits between <ProjectForm> and the project-grid div.
  • The grid maps over filteredProjects, not projects.
  • The no-results paragraph sits after the closing </div> of project-grid.

For App.css, confirm the .filter-bar, .filter-label, .filter-tag, .filter-tag:hover, and .filter-tag.active blocks sit between .project-form button:hover and .project-grid. Confirm .no-results is at the very bottom of the file.

Your showcase wall now lets users post projects, give kudos, and filter the whole grid by technology. Next up, the Secret Mission challenges you to add a search bar and a sort toggle that work alongside the filter.

Secret mission

Add Search and Sort

Your showcase wall has tech filters, but what if you want to find a specific project by name or surface the most popular ones? In this mission, you will add a real-time search bar and a kudos sort toggle that chain with your existing filter.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your project files, pause by stopping the dev server, or delete everything entirely.

Resources you used:

  • The showcase-wall project folder on your Desktop (source code and node_modules).
  • The Vite dev server running in your terminal.

Keep everything running

No action needed. Choose this if you are still actively building or want to keep experimenting with your showcase wall.

  • Your Vite dev server stays running in the terminal. You can keep refreshing your browser at http://localhost:5173 to see changes.
  • Your showcase-wall folder stays on your Desktop with all your code intact.

Pause - I'll come back to this later

Shut down the dev server to free up your terminal. Your project files stay on your Desktop so you can pick up where you left off.

  • In the terminal where the Vite dev server is running, press Ctrl+C to stop it.
  • When you come back, open the showcase-wall folder in Cursor and run npm run dev to start the dev server again.

Delete - I don't want to use this again

Remove all project files and free up disk space. This deletes everything you built in this project.

  • In the terminal where the Vite dev server is running, press Ctrl+C to stop it.
  • Delete the project folder by running this command (macOS/Linux):
rm -rf ~/Desktop/showcase-wall
  • On Windows, run this instead:
rmdir /s /q %USERPROFILE%\Desktop\showcase-wall
  • Confirm the folder is gone by running:
ls ~/Desktop

You should no longer see showcase-wall in the output.

Nice Work!

Nice Work!

Nice work. You just built a fully interactive Community Showcase Wall with React and Vite, complete with live kudos, a post form, and a tech filter.

You've learned how to:

  • Composed a component architecture (App, ProjectCard, ProjectForm, FilterBar) and rendered a grid of project cards from seed data using .map() and JSX.
  • Managed shared state with useState, passed handlers through props so child components can update parent state, and built a controlled form with validation and state reset on submit.
  • Derived unique tech tags and filtered projects from state without duplicating data, keeping a single source of truth in the parent component.
  • Secret Mission: Added a real-time search bar and a kudos sort toggle that chain with the tech filter to narrow and reorder the wall.

Ready to quiz yourself?