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 >
Filter States
Filter Types
Active filter:
Emerald border and background
Brighter text color
aria-pressed="true"
Inactive filter:
Slate border and background
Muted text color
Hover effects
ID Translation Key Purpose todosprojects.allShow all projects sitioWebprojects.webSitesWeb projects only variosprojects.othersMiscellaneous projects
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
Firestore query construction
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.
Error State
Loading State
Empty State
Success State
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. if ( isLoading ) {
return (
< section className = "grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3" >
{ Array . from ({ length: 3 }). map (( _ , index ) => (
< div key = { `skeleton- ${ index } ` } className = "w-full animate-pulse rounded-2xl border border-slate-800 bg-slate-900/40 p-6" >
{ /* Skeleton structure */ }
</ div >
)) }
</ section >
)
}
Shows 3 animated skeleton cards while fetching data. if ( projects . length === 0 ) {
return (
< p className = "rounded-2xl border border-slate-800 bg-slate-900/60 px-4 py-4 text-center text-sm text-slate-200" >
{ t ( "projects.empty" ) }
</ p >
)
}
Displays “No projects to show yet” message. return (
< section className = "grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3" >
{ projects . map (( project ) => {
if ( project . visible === false ) return null
return < Project key = { project . id } project = { project } />
}) }
</ section >
)
Renders project cards in responsive grid.
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:
Language-specific field (descriptionSpanish or descriptionEnglish)
Generic description field
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 >
Action Links
< 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
Section Labels
Filters
States & Labels
Key English Spanish projects.subtitleShowcase Showcase projects.titleProjects Proyectos projects.descriptionSelected projects that blend… Algunos proyectos destacados…
Key English Spanish projects.allAll Todos projects.webSitesWeb Web projects.othersVarious Varios
Key English Spanish projects.inProgressIn development En desarrollo projects.noImageImage coming soon Imagen en preparación projects.errorWe couldn’t load the projects… No se pudieron cargar… projects.emptyNo projects to show yet… Todavía no hay proyectos… projects.links.demoView demo Ver demo projects.links.repositoryRepository Repositorio projects.typeLabels.websiteWebsite Sitio web projects.typeLabels.miscOther Otros
Responsive Grid
className = "grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3"
Mobile (< 768px) 1 column Full-width cards stacked vertically
Tablet (≥ 768px) 2 columns Side-by-side card layout
Desktop (≥ 1280px) 3 columns Maximum density grid
Styling Details
Card Hover Effects
.project img {
transition : transform 500 ms ;
}
.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.
Link Button Styles
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"
Emerald theme for primary action (view demo)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"
Slate theme for secondary action (view code)
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)