Build a Motorcycle Shop - Build a Motorcycle Shop

Tell us what’s happening:

I need help, I can’t pass test 17 to 24 I don’t know why

Your code so far

<!-- file: index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>MotoShop - Find Your Perfect Ride</title>
    <link rel="stylesheet" href="./styles.css">
</head>
<body>
    <div id="app">
        <header class="bg-gradient-to-r from-gray-900 via-gray-800 to-gray-900 text-white">
            <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
                <div class="flex flex-col md:flex-row items-start md:items-center gap-4 md:gap-6">
                    <div class="flex items-center gap-3">
                        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bike-icon lucide-bike w-8 h-8 text-orange-500"><circle cx="18.5" cy="17.5" r="3.5"/><circle cx="5.5" cy="17.5" r="3.5"/><circle cx="15" cy="5" r="1"/><path d="M12 17.5V14l-3-3 4-3 2 3h2"/></svg>
                        <h1 class="text-3xl font-bold">MotoShop</h1>
                    </div>
                    <div class="flex-1 w-full md:w-auto">
                        <div class="relative">
                            <input
                                type="text"
                                id="name-filter-input"
                                placeholder="Search motorcycles by name..."
                                class="w-full md:w-96 px-4 py-2 pl-10 rounded-lg bg-gray-700 text-white placeholder-gray-400 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent"
                            />
                            <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400">
                                <circle cx="11" cy="11" r="8"/>
                                <path d="m21 21-4.35-4.35"/>
                            </svg>
                        </div>
                    </div>
                </div>
            </div>
        </header>

        <section class="bg-gradient-to-r from-orange-600 via-orange-500 to-red-500 text-white py-20">
            <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
                <h2 class="text-5xl font-bold mb-4">Find Your Perfect Ride</h2>
                <p class="text-xl opacity-90 max-w-2xl mx-auto">
                    Explore our curated collection of premium motorcycles from the world's leading manufacturers
                </p>
            </div>
        </section>

        <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">

            <div id="results-count" class="results-count">
                <p class="results-count-text">
                    Showing <span id="results-number" class="results-count-number">0</span> motorcycles
                </p>
            </div>

            <div id="loading-container" class="loading-container" style="display: none;">
                <div class="loading-spinner">
                   <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-loader-circle-icon lucide-loader-circle w-24 h-24 text-orange-500 animate-spin"><path d="M21 12a9 9 0 1 1-6.219-8.56"></path></svg>
                </div>
            </div>

            <div id="no-results" class="no-results" style="display: none;">
                <p class="no-results-text">
                    No motorcycles found matching your filters.
                </p>
            </div>

            <div id="motorcycle-grid" class="motorcycle-grid">
            </div>
        </main>

        <footer class="bg-gray-900 text-white py-8 mt-20">
            <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
                <p class="text-gray-400">
                    © 2024 MotoGallery. Ride with passion, choose with confidence.
                </p>
            </div>
        </footer>
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/lucide/0.344.0/lucide.min.js"></script>
    <script src="./index.ts"></script>
</body>
</html>
/* file: styles.css */
/* Reset and base styles */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Layout utilities */
.max-w-7xl {
  max-width: 80rem;
}

.mx-auto {
  margin-left: auto;
  margin-right: auto;
}

.px-4 {
  padding-left: 1rem;
  padding-right: 1rem;
}

.py-6 {
  padding-top: 1.5rem;
  padding-bottom: 1.5rem;
}

.py-8 {
  padding-top: 2rem;
  padding-bottom: 2rem;
}

.py-20 {
  padding-top: 5rem;
  padding-bottom: 5rem;
}

.mt-20 {
  margin-top: 5rem;
}

.mb-4 {
  margin-bottom: 1rem;
}

/* Responsive padding */
@media (min-width: 640px) {
  .sm\:px-6 {
    padding-left: 1.5rem;
    padding-right: 1.5rem;
  }
}

@media (min-width: 1024px) {
  .lg\:px-8 {
    padding-left: 2rem;
    padding-right: 2rem;
  }
}

/* Background gradients */
.bg-gradient-to-r {
  background: linear-gradient(to right, var(--tw-gradient-stops));
}

.from-gray-900 {
  --tw-gradient-from: #111827;
  --tw-gradient-to: rgb(17 24 39 / 0);
  --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}

.via-gray-800 {
  --tw-gradient-to: rgb(31 41 55 / 0);
  --tw-gradient-stops: var(--tw-gradient-from), #1f2937, var(--tw-gradient-to);
}

.to-gray-900 {
  --tw-gradient-to: #111827;
}

.from-orange-600 {
  --tw-gradient-from: #ea580c;
  --tw-gradient-to: rgb(234 88 12 / 0);
  --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}

.via-orange-500 {
  --tw-gradient-to: rgb(249 115 22 / 0);
  --tw-gradient-stops: var(--tw-gradient-from), #f97316, var(--tw-gradient-to);
}

.to-red-500 {
  --tw-gradient-to: #ef4444;
}

/* Colors */
.bg-gray-900 {
  background-color: #111827;
}

.text-white {
  color: #ffffff;
}

.text-gray-400 {
  color: #9ca3af;
}

.text-orange-500 {
  color: #f97316;
  stroke: #f97316;
}

/* Flexbox utilities */
.flex {
  display: flex;
}

.items-center {
  align-items: center;
}

.gap-3 {
  gap: 0.75rem;
}

/* Grid utilities */
.grid {
  display: grid;
}

.gap-6 {
  gap: 1.5rem;
}

/* Typography */
.text-xl {
  font-size: 1.25rem;
  line-height: 1.75rem;
}

.text-3xl {
  font-size: 1.875rem;
  line-height: 2.25rem;
}

.text-5xl {
  font-size: 3rem;
  line-height: 1;
}

.font-bold {
  font-weight: 700;
}

.text-center {
  text-align: center;
}

.opacity-90 {
  opacity: 0.9;
}

.max-w-2xl {
  max-width: 42rem;
}

/* Spacing utilities */
.w-8 {
  width: 2rem;
}

.h-8 {
  height: 2rem;
}

.h-2 {
  height: 0.5rem;
}

.w-full {
  width: 100%;
}

/* Border utilities */
.border {
  border-width: 1px;
}

.border-t {
  border-top-width: 1px;
}

.rounded-lg {
  border-radius: 0.5rem;
}

/* Position utilities */
.relative {
  position: relative;
}

.absolute {
  position: absolute;
}

/* Padding utilities */
.px-4 {
  padding-left: 1rem;
  padding-right: 1rem;
}

.px-6 {
  padding-left: 1.5rem;
  padding-right: 1.5rem;
}

.py-1 {
  padding-top: 0.25rem;
  padding-bottom: 0.25rem;
}

.py-2 {
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
}

/* Margin utilities */
.mb-4 {
  margin-bottom: 1rem;
}

/* Focus utilities */
.focus\:ring-2:focus {
  --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
  --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
  box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}

.focus\:ring-orange-500:focus {
  --tw-ring-color: #f97316;
}

.focus\:border-transparent:focus {
  border-color: transparent;
}

/* Hover utilities */
.hover\:-translate-y-1:hover {
  transform: translateY(-0.25rem);
}

/* Animation utilities */
.animate-spin {
  animation: spin 1s linear infinite;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

/* Custom component styles */
.loading-container {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 400px;
}

.loading-spinner {
  display: flex;
  align-items: center;
  justify-content: center;
}

.motorcycle-grid {
  display: grid;
  grid-template-columns: repeat(1, minmax(0, 1fr));
  gap: 2rem;
}

@media (min-width: 768px) {
  .motorcycle-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 1024px) {
  .motorcycle-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

.motorcycle-card {
  background-color: #ffffff;
  border-radius: 0.75rem;
  overflow: hidden;
  box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
  transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1);
  transform: translateY(0);
}

.motorcycle-card:hover {
  box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
  transform: translateY(-0.25rem);
}

.motorcycle-card-image-container {
  position: relative;
  height: 16rem;
  overflow: hidden;
  background-color: #111827;
}

.motorcycle-card-image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 500ms cubic-bezier(0.4, 0, 0.2, 1);
}

.motorcycle-card:hover .motorcycle-card-image {
  transform: scale(1.1);
}

.motorcycle-card-year-badge {
  position: absolute;
  top: 1rem;
  right: 1rem;
  background-color: #f97316;
  color: #ffffff;
  padding: 0.25rem 0.75rem;
  border-radius: 9999px;
  font-size: 0.875rem;
  font-weight: 700;
}

.motorcycle-card-content {
  padding: 1.5rem;
}

.motorcycle-card-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  margin-bottom: 0.75rem;
}

.motorcycle-card-title {
  font-size: 1.25rem;
  font-weight: 700;
  color: #111827;
  margin-bottom: 0.25rem;
}

.motorcycle-card-manufacturer {
  color: #4b5563;
  font-weight: 500;
}

.motorcycle-card-category {
  padding: 0.25rem 0.75rem;
  background-color: #f3f4f6;
  color: #374151;
  border-radius: 9999px;
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.025em;
}

.motorcycle-card-description {
  color: #4b5563;
  font-size: 0.875rem;
  margin-bottom: 1rem;
  overflow: hidden;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
}

.motorcycle-card-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-top: 1rem;
  border-top: 1px solid #e5e7eb;
}

.motorcycle-card-price {
  font-size: 1.5rem;
  font-weight: 700;
  color: #ea580c;
}

.motorcycle-card-engine {
  font-size: 0.75rem;
  color: #6b7280;
}

.motorcycle-card-button {
  background-color: #f97316;
  color: #ffffff;
  padding: 0.5rem 1.5rem;
  border-radius: 0.5rem;
  font-weight: 600;
  transition: background-color 200ms cubic-bezier(0.4, 0, 0.2, 1);
  border: none;
  cursor: pointer;
}

.motorcycle-card-button:hover {
  background-color: #ea580c;
}

.no-results {
  text-align: center;
  padding: 3rem 0;
}

.no-results-text {
  font-size: 1.25rem;
  color: #4b5563;
}

.results-count {
  margin-bottom: 1.5rem;
}

.results-count-text {
  color: #4b5563;
}

.results-count-number {
  font-weight: 700;
  color: #111827;
}

/* Additional utility classes for search input */
.bg-gray-700 {
  background-color: #374151;
}

.border-gray-600 {
  border-color: #4b5563;
}

.placeholder-gray-400::placeholder {
  color: #9ca3af;
}

.focus\:outline-none:focus {
  outline: none;
}

.focus\:ring-2:focus {
  box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.5);
}

.focus\:ring-orange-500:focus {
  box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.5);
}

.focus\:border-transparent:focus {
  border-color: transparent;
}

.pl-10 {
  padding-left: 2.5rem;
}

.w-96 {
  width: 24rem;
}

.left-3 {
  left: 0.75rem;
}

.top-1\/2 {
  top: 50%;
}

.transform {
  transform: translateY(-50%);
}

.flex-1 {
  flex: 1 1 0%;
}

.gap-4 {
  gap: 1rem;
}

.gap-6 {
  gap: 1.5rem;
}

.flex-col {
  flex-direction: column;
}

.items-start {
  align-items: flex-start;
}

.items-center {
  align-items: center;
}

@media (min-width: 768px) {
  .md\:flex-row {
    flex-direction: row;
  }
  
  .md\:items-center {
    align-items: center;
  }
  
  .md\:w-auto {
    width: auto;
  }
}
/* file: index.ts */
let nameFilterInput: HTMLInputElement;
let resultsNumber: HTMLElement;
let motorcycleGrid: HTMLElement;

type Category = 'Sport' | 'Cruiser' | 'Touring' | 'Dirt' | 'Adventure' | 'Naked' | 'Electric';

interface Motorcycle {
  id: string;
  name: string;
  manufacturer: string;
  category: Category;
  price: number;
  image_url: string;
  created_at: Date;
  description: string;
  year: number;
}

const getElement = (id: string): HTMLElement => {
  const el = document.getElementById(id);
  if (!el) throw new Error(`Element not found: ${id}`);
  return el;
};

const handleSearchInput = (e: Event) => {
  e.preventDefault();

  const val = e.currentTarget as HTMLInputElement;
  if (!val) {
    return "target value not found";
  }

  storeGallery.renderMotorcycles(val.value.trim());
};

const fetchMotorcycles = async (): Promise<Motorcycle[]> => {
  try {
    const response = await fetch("https://cdn.freecodecamp.org/curriculum/labs/data/motorcycles.json");
    const data = await response.json();

    /* if (!data || data.length === 0) {
      return;
    } */

    return data;
  } catch(err) {
    console.log(err);
    return [];
  }
}

const renderMotorcycleCard = (motorcycle: Motorcycle): string => {
  return `
    <div class="motorcycle-card">
      <div class="motorcycle-card-image-container">
        <img src="${motorcycle.image_url}" />

        <div class="motorcycle-card-year-badge">${motorcycle.year}</div>
      </div>

      <div class="motorcycle-card-content">
        <div class="motorcycle-card-header">
          <div>
            <h3 class="motorcycle-card-title">${motorcycle.name}</h3>

            <p class="motorcycle-card-manufacturer">${motorcycle.manufacturer}</p>
          </div>

          <span class="motorcycle-card-category">${motorcycle.category}</span>
        </div>

        <p class="motorcycle-card-description">${motorcycle.description}</p>

        <div class="motorcycle-card-footer">
          <div>
            <p class="motorcycle-card-price">$${motorcycle.price}</p>
            <p class="motorcycle-card-engine">undefinedcc</p>
          </div>
        </div>
      </div>
    </div>
  `;
}

class MotorcycleGalleryApp {
  private allMotorcycles: Motorcycle[] = [];

  constructor() {
    fetchMotorcycles()
    .then((data) => {
      this.allMotorcycles = data;
      this.renderMotorcycles();
    });
  }

  renderMotorcycles(searchbarValue: string = ""): void {
    const filterList = this.allMotorcycles.filter((motocycle) => motocycle.name.toLowerCase().includes(searchbarValue.toLowerCase()));

    resultsNumber.textContent = filterList.length.toString();
    motorcycleGrid.innerHTML = filterList.map((motocycle) => renderMotorcycleCard(motocycle)).join("");
  }
}

const storeGallery = new MotorcycleGalleryApp();

const initializeApp = () => {
  nameFilterInput = getElement("name-filter-input") as HTMLInputElement;
  motorcycleGrid = getElement("motorcycle-grid") as HTMLElement;
  resultsNumber = getElement("results-number") as HTMLElement;

  nameFilterInput.oninput = handleSearchInput;
}

document.addEventListener("DOMContentLoaded", initializeApp);

Your browser information:

User Agent is: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36

Challenge Information:

Build a Motorcycle Shop - Build a Motorcycle Shop

GitHub Link: freeCodeCamp/curriculum/challenges/english/blocks/lab-motorcycle-shop/694175528a794a090ea0ba74.md at main · freeCodeCamp/freeCodeCamp · GitHub

This is the test code, quoted from the lab, not your code:

const gallery = new MotorcycleGalleryApp();
gallery.renderMotorcycles();
const titles = document.getElementsByClassName("motorcycle-card-title");
assert.lengthOf(titles, 25);

All eight of the tests you are failing look like that. The test builds a brand new gallery and calls renderMotorcycles on the very next line, with nothing in between that waits for the data to arrive. So on that instance allMotorcycles is still an empty array, and the 25 elements the test is counting are the ones your own page rendered when it loaded.

Now look at the last line of your renderMotorcycles:

motorcycleGrid.innerHTML = filterList.map(...).join("");

With an empty list that assigns an empty string, which wipes out the cards that were already on the page, and the test then counts zero.

So renderMotorcycles should not clear the grid when it has nothing to show. Return early in that case. The page needs that behavior anyway, since the markup already includes a no-results element for the empty case, and right now an empty search leaves the user staring at a blank page with no message.

About the undefinedcc in your card: that one is not on you. The user stories ask for the engine, but the data at that endpoint has no engine field at all, so there is nothing to display. The test only counts the elements with the motorcycle-card-engine class, so it passes either way. That gap in the lab is already tracked at "Build a motorcycle shop" lab audit · Issue #69141 · freeCodeCamp/freeCodeCamp · GitHub

I put an if inside the renderMotorcycles method and it’s works

renderMotorcycles(searchbarValue: string = ""): void {

    if (this.allMotorcycles.length) {

      const filterList = this.allMotorcycles.filter((motocycle) => motocycle.name.toLowerCase().includes(searchbarValue.toLowerCase()));




      resultsNumber.textContent = filterList.length.toString();

      motorcycleGrid.innerHTML = filterList.map((motocycle) => renderMotorcycleCard(motocycle)).join("");

    }

  }