Skip to main content

Overview

The Projects component is a multi-layer system that fetches, filters, and displays project data from Firebase Firestore. It consists of three main components working together: ProjectsView (UI and filtering), ProjectListContainer (data fetching), and Project (individual card rendering).

Component Architecture

ProjectsView

Location: src/components/Main/ProjectsView.jsxUI container with filter controls

ProjectListContainer

Location: src/components/Projects/ProjectListContainer.jsxFirebase data fetching and state management

ProjectList

Location: src/components/Projects/ProjectList.jsxList rendering with loading and error states

Project

Location: src/components/Projects/Project.jsxIndividual project card component

ProjectsView Component

Overview

Manages the UI layout and project type filtering.
function ProjectsView() {
  const { t } = useTranslation()
  const [type, setType] = useState("todos")

  const filters = [
    { id: "todos", label: t("projects.all") },
    { id: "sitioWeb", label: t("projects.webSites") },
    { id: "varios", label: t("projects.others") },
  ]

  return (
    <section id="projects" className="scroll-mt-32 px-6">
      {/* Content */}
    </section>
  )
}

Filter System

<div className="flex flex-wrap gap-3">
  {filters.map((filter) => {
    const isActive = type === filter.id
    return (
      <button
        key={filter.id}
        type="button"
        onClick={() => setType(filter.id)}
        aria-pressed={isActive}
        className={`inline-flex items-center rounded-full border px-4 py-2 text-sm font-medium uppercase tracking-[0.2em] transition ${
          isActive
            ? "border-emerald-400 bg-emerald-500/10 text-emerald-200"
            : "border-slate-700 bg-slate-900/70 text-slate-300 hover:border-slate-500 hover:text-white"
        }`}
      >
        {filter.label}
      </button>
    )
  })}
</div>
Active filter:
  • Emerald border and background
  • Brighter text color
  • aria-pressed="true"
Inactive filter:
  • Slate border and background
  • Muted text color
  • Hover effects

Data Flow

<ProjectListContainer type={type} />
Passes the selected filter type to the container component.

ProjectListContainer Component

Firebase Integration

import { collection, getDocs, query, where } from "firebase/firestore"
import { db } from "../db/data"

function ProjectListContainer({ type }) {
  const [projectsList, setProjectsList] = useState([])
  const [isLoading, setIsLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    const projectCollection = collection(db, "projects")
    let queryFilter

    if (type === "todos") {
      queryFilter = projectCollection
    } else {
      queryFilter = query(projectCollection, where("type", "==", type))
    }

    setIsLoading(true)
    setError(null)

    getDocs(queryFilter)
      .then((res) => {
        const projectsMapped = res.docs.map((project) => ({
          id: project.id,
          ...project.data(),
        }))
        setProjectsList(projectsMapped)
        setIsLoading(false)
      })
      .catch(() => {
        setError("projects.error")
        setIsLoading(false)
      })
  }, [type])

  return (
    <ProjectList
      isLoading={isLoading}
      error={error}
      projects={projectsList}
    />
  )
}

Query Logic

Show all projects:
// When type === "todos"
queryFilter = collection(db, "projects")
// Fetches entire collection
Filter by type:
// When type === "sitioWeb" or "varios"
queryFilter = query(
  collection(db, "projects"),
  where("type", "==", type)
)
// Filters documents where type field matches

Props Interface

ProjectListContainer.propTypes = {
  type: PropTypes.string.isRequired,
}

ProjectList Component

State Handling

Manages three states: loading, error, and success.
if (error) {
  return (
    <p className="rounded-2xl border border-red-500/40 bg-red-500/10 px-4 py-3 text-center text-sm text-red-200">
      {t(error)}
    </p>
  )
}
Displays error message in red-themed alert box.

Skeleton Loader

<div className="w-full animate-pulse rounded-2xl border border-slate-800 bg-slate-900/40 p-6">
  <div className="mb-4 h-44 w-full rounded-xl bg-slate-800" /> {/* Image */}
  <div className="mb-2 h-4 w-2/3 rounded bg-slate-800" />    {/* Title */}
  <div className="mb-2 h-3 w-1/2 rounded bg-slate-800" />    {/* Subtitle */}
  <div className="mb-3 flex flex-wrap gap-2">
    <span className="h-6 w-16 rounded-full bg-slate-800" /> {/* Badges */}
    <span className="h-6 w-14 rounded-full bg-slate-800" />
    <span className="h-6 w-20 rounded-full bg-slate-800" />
  </div>
  <div className="h-3 w-full rounded bg-slate-800" />       {/* Description */}
</div>
The animate-pulse Tailwind utility provides the pulsing animation effect.

Project Component

Project Card Structure

function Project({ project }) {
  const { t, i18n } = useTranslation()

  const isInProgress = project?.isFinished === false
  const description =
    i18n.language === "es"
      ? project?.descriptionSpanish ?? project?.description ?? ""
      : project?.descriptionEnglish ?? project?.description ?? ""

  const imageSrc = project?.imageUrl || project?.img || (project?.title ? `/${project.title}.webp` : "")
  const technologies = Array.isArray(project?.techs) ? project.techs : []

  const typeLabels = {
    sitioWeb: t("projects.typeLabels.website"),
    varios: t("projects.typeLabels.misc"),
  }

  const secondaryInfo = project?.role || typeLabels[project?.type] || project?.type

  return (
    <article className="project relative flex h-full flex-col gap-4 overflow-hidden rounded-2xl border border-slate-800 bg-slate-900/60 p-5 shadow-xl shadow-slate-950/40 backdrop-blur">
      {/* Card content */}
    </article>
  )
}

Description Localization

Handles multilingual descriptions with fallbacks:
const description =
  i18n.language === "es"
    ? project?.descriptionSpanish ?? project?.description ?? ""
    : project?.descriptionEnglish ?? project?.description ?? ""
Fallback chain:
  1. Language-specific field (descriptionSpanish or descriptionEnglish)
  2. Generic description field
  3. Empty string

In-Progress Badge

{isInProgress && (
  <span className="absolute right-4 top-4 inline-flex items-center gap-2 rounded-full bg-red-500/90 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-white">
    <span className="h-2 w-2 rounded-full bg-white" />
    {t("projects.inProgress")}
  </span>
)}
Displays when: project.isFinished === false
  • Position: Absolute, top-right corner
  • Color: Red background with white text
  • Indicator: White dot for “live” feeling
  • Text: “IN DEVELOPMENT” (translated)

Project Image

<figure className="overflow-hidden rounded-xl border border-slate-800/80 bg-slate-900">
  {imageSrc ? (
    <img
      src={imageSrc}
      alt={project?.title}
      loading="lazy"
      className="h-48 w-full object-cover transition duration-500 hover:scale-105"
    />
  ) : (
    <div className="flex h-48 w-full items-center justify-center text-slate-500">
      {t("projects.noImage")}
    </div>
  )}
</figure>
Features:
  • Lazy loading with loading="lazy"
  • Hover zoom effect with hover:scale-105
  • Fallback placeholder if no image
  • Fixed height of 12rem (h-48)

Content Section

<div className="flex flex-col gap-2">
  <div>
    <h3 className="text-xl font-semibold text-white">{project?.title}</h3>
    {secondaryInfo && <p className="text-sm text-slate-400">{secondaryInfo}</p>}
  </div>

  {technologies.length > 0 && (
    <ul className="flex flex-wrap gap-2">
      {technologies.map((tech) => (
        <li
          key={tech}
          className="rounded-full border border-sky-500/60 bg-sky-500/10 px-3 py-1 text-xs font-medium uppercase tracking-wide text-sky-200"
        >
          {tech}
        </li>
      ))}
    </ul>
  )}

  {description && <p className="text-sm leading-relaxed text-slate-200">{description}</p>}
</div>
<div className="mt-auto flex flex-wrap gap-3">
  {project?.webLink && (
    <a
      href={project.webLink}
      target="_blank"
      rel="noopener noreferrer"
      className="inline-flex items-center gap-2 rounded-full border border-emerald-500/60 px-4 py-2 text-sm font-medium text-emerald-200 transition hover:bg-emerald-500/10"
    >
      <i className="bi bi-laptop text-lg" aria-hidden="true" />
      <span>{t("projects.links.demo")}</span>
    </a>
  )}
  {project?.githubLink && (
    <a
      href={project.githubLink}
      target="_blank"
      rel="noopener noreferrer"
      className="inline-flex items-center gap-2 rounded-full border border-slate-500/80 px-4 py-2 text-sm font-medium text-slate-200 transition hover:bg-slate-500/10"
    >
      <i className="bi bi-github text-lg" aria-hidden="true" />
      <span>{t("projects.links.repository")}</span>
    </a>
  )}
</div>
The mt-auto class pushes links to the bottom of the card, ensuring consistent alignment across cards of different heights.

Project Data Structure

Firebase Document Schema

interface Project {
  id: string                    // Document ID
  title: string                 // Project name
  description?: string          // Generic description
  descriptionEnglish?: string   // English description
  descriptionSpanish?: string   // Spanish description
  imageUrl?: string             // Image URL (Firebase Storage)
  img?: string                  // Alternative image field
  techs?: string[]              // Array of technologies
  isFinished?: boolean          // Completion status
  visible?: boolean             // Visibility flag
  role?: string                 // Developer role
  type?: string                 // "sitioWeb" | "varios"
  webLink?: string              // Live demo URL
  githubLink?: string           // Repository URL
}

PropTypes Definition

Project.propTypes = {
  project: PropTypes.shape({
    id: PropTypes.string,
    title: PropTypes.string,
    description: PropTypes.string,
    descriptionEnglish: PropTypes.string,
    descriptionSpanish: PropTypes.string,
    imageUrl: PropTypes.string,
    img: PropTypes.string,
    techs: PropTypes.arrayOf(PropTypes.string),
    isFinished: PropTypes.bool,
    role: PropTypes.string,
    type: PropTypes.string,
    webLink: PropTypes.string,
    githubLink: PropTypes.string,
  }).isRequired,
}

Translation Keys

KeyEnglishSpanish
projects.subtitleShowcaseShowcase
projects.titleProjectsProyectos
projects.descriptionSelected projects that blend…Algunos proyectos destacados…

Responsive Grid

className="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3"

Mobile (< 768px)

1 columnFull-width cards stacked vertically

Tablet (≥ 768px)

2 columnsSide-by-side card layout

Desktop (≥ 1280px)

3 columnsMaximum density grid

Styling Details

Card Hover Effects

.project img {
  transition: transform 500ms;
}

.project img:hover {
  transform: scale(1.05);
}

Technology Badge Colors

// Sky theme for project technologies
className="rounded-full border border-sky-500/60 bg-sky-500/10 px-3 py-1 text-xs font-medium uppercase tracking-wide text-sky-200"
Consistent with skills section styling.

Accessibility

  • Semantic HTML: <article> for each project card
  • Heading hierarchy: <h2> for section, <h3> for project titles
  • Alternative text: Descriptive alt text using project titles
  • Link security: rel="noopener noreferrer" on external links
  • ARIA labels: aria-pressed on filter buttons
  • Icon labels: aria-hidden="true" on decorative icons
  • Lazy loading: Images load as needed for performance
  • Keyboard navigation: All interactive elements focusable

Error Handling

.catch(() => {
  setError("projects.error")
  setIsLoading(false)
})
Generic error handling displays user-friendly message without exposing technical details.

Performance Considerations

Lazy Loading

Images use loading="lazy" attribute to defer offscreen image loading

Conditional Rendering

visible === false projects are filtered out before rendering

Optimized Queries

Firestore queries filter by type at database level, not in-memory

Skeleton States

Skeleton loaders provide instant feedback while data loads

Integration Example

import ProjectsView from "./components/Main/ProjectsView"

function MainContent() {
  return (
    <main>
      <Hero />
      <About />
      <Skills />
      <Works />
      <ProjectsView />
      {/* Other sections */}
    </main>
  )
}

Firebase Setup

Ensure Firebase is properly initialized:
// src/components/db/data.js
import { initializeApp } from "firebase/app"
import { getFirestore } from "firebase/firestore"

const firebaseConfig = {
  // Your config
}

const app = initializeApp(firebaseConfig)
export const db = getFirestore(app)
Ensure Firestore security rules allow read access to the “projects” collection for your application’s users.

Dependencies

  • React: useState, useEffect hooks
  • react-i18next: Translation and language detection
  • Firebase: Firestore for data storage and retrieval
  • PropTypes: Runtime type checking
  • Tailwind CSS: Utility styling
  • Bootstrap Icons: Icon library (bi-laptop, bi-github)

Build docs developers (and LLMs) love