Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/nicolasgrajaleshoyos/portafolio/llms.txt

Use this file to discover all available pages before exploring further.

The Projects component fills the third section of the portfolio (id #projects) with a responsive two-column card grid showcasing Nicolas’s work. Each card is handled by the internal ProjectCard sub-component, which manages its own image-loading skeleton, staggered entrance animation, and hover overlay that reveals GitHub and live-demo icon links. The section background alternates to bg-slate-50 dark:bg-dark to visually separate it from the About section.

Props

Projects accepts no external props. The list of projects is a static array defined at module level inside Projects.tsx.

Project Interface

The shape of each project entry is defined in src/types.ts:
export interface Project {
  id: number;
  title: string;
  description: string;
  imageUrl: string;
  tags: string[];
  liveUrl?: string;   // optional — live demo URL
  codeUrl?: string;   // optional — source code URL
}
Both liveUrl and codeUrl are optional; the ProjectCard renders an icon button only when the corresponding field is present.

ProjectCard Sub-component

ProjectCard receives three props:
project
Project
required
The project data object to render, including title, description, image, tags, and optional links.
isVisible
boolean
required
Passed down from the parent Projects component. When true, the card transitions from translate-y-8 opacity-0 to translate-y-0 opacity-100.
index
number
required
The zero-based position of the card in the list. Used to calculate a staggered transitionDelay so cards enter the viewport one after another.

Staggered entrance

Each card’s delay is proportional to its index:
style={{ transitionDelay: `${index * 150}ms` }}
Card 0 appears immediately, card 1 after 150 ms, card 2 after 300 ms, and card 3 after 450 ms.

Image skeleton / loading state

ProjectCard owns a loaded boolean state. While the image is downloading, an absolutely-positioned animate-pulse grey rectangle covers the image area. Once the <img> fires onLoad, loaded becomes true, which hides the skeleton and fades the image in:
const [loaded, setLoaded] = useState(false);
// ...
<img src={project.imageUrl} onLoad={() => setLoaded(true)} className={loaded ? 'opacity-100' : 'opacity-0'} />

Hover overlay

An absolute <div> with the gradient:
bg-gradient-to-t from-black/80 via-black/40 to-transparent
covers the card image at all times. The title and tag pills sit at the bottom of this overlay (absolute bottom-0 left-0 p-6). The GitHub and live-demo icon buttons appear at the top-right only on hover:
opacity-0 -translate-y-2 group-hover:opacity-100 group-hover:translate-y-0 transition-all duration-300

Projects Data

The current four projects are:
const projects: Project[] = [
  {
    id: 1,
    title: 'DSS Comparador de Países Backend',
    description: 'Backend del sistema de soporte a decisiones (DSS) para comparar países, proporcionando una API RESTful para gestionar y servir datos.',
    imageUrl: '/icons/backend.webp',
    tags: ['Spring Boot', 'Java', 'PostgreSQL'],
    codeUrl: 'https://github.com/nicolasgrajaleshoyos/DSS-Comparador-de-Pa-ses-Backend',
  },
  {
    id: 2,
    title: 'DSS Comparador de Países Frontend',
    description: 'Un sistema de soporte a decisiones (DSS) que permite comparar países utilizando análisis de datos y visualizaciones interactivas.',
    imageUrl: '/icons/frontend.webp',
    tags: ['TypeScript', 'Tailwind CSS', 'JavaScript'],
    codeUrl: 'https://github.com/nicolasgrajaleshoyos/DSS-Comparador-de-Pa-ses-Frontend',
  },
  {
    id: 3,
    title: 'Sitio Web de Portafolio',
    description: 'Un portafolio personal y dinámico para mostrar mis proyectos y habilidades (¡esta misma página!).',
    imageUrl: '/icons/portafolio.png',
    tags: ['React', 'Tailwind CSS', 'Vite'],
    codeUrl: 'https://github.com/nicolasgrajaleshoyos/portafolio',
  },
  {
    id: 4,
    title: 'Sistema para Empresa de Arepas',
    description: 'Proyecto desarrollado para una empresa familiar productora de arepas en Popayán, diseñado para optimizar sus procesos.',
    imageUrl: '/icons/arepas.png',
    tags: ['Laravel', 'Laravel Native', 'App de Escritorio'],
    codeUrl: 'https://github.com/nicolasgrajaleshoyos/arpas_el_buen_sabor',
  },
];

Behavior

Visibility via IntersectionObserver

The parent Projects component attaches an IntersectionObserver (threshold 0.1) to the section ref. When the section enters the viewport, isVisible is set to true and the observer disconnects. This boolean is forwarded to every ProjectCard as the isVisible prop, triggering the staggered entrance simultaneously for all visible cards.

Usage Example

Projects is mounted inside <main> in App.tsx, after <About />:
import Projects from '@/components/Projects';

const App: React.FC = () => (
  <main className="flex-grow">
    <Hero />
    <About />
    <Projects />
    <Contact />
  </main>
);

Customization

Append a new object to the projects array in Projects.tsx. Supply a unique id, a title, a description, an image path (place the image in public/icons/), and an array of technology tags. liveUrl and codeUrl are both optional:
{
  id: 5,
  title: 'Mi Nuevo Proyecto',
  description: 'Descripción breve del proyecto.',
  imageUrl: '/icons/nuevo-proyecto.png',
  tags: ['React', 'Node.js', 'MongoDB'],
  codeUrl: 'https://github.com/nicolasgrajaleshoyos/nuevo-proyecto',
  liveUrl: 'https://nuevo-proyecto.vercel.app',
},
The grid is defined on the wrapping <div> in Projects. The default is two columns on md and above. Adjust as needed:
grid md:grid-cols-2           // default (2 columns)
grid sm:grid-cols-2 lg:grid-cols-3  // 3 columns on large screens
In ProjectCard, change the multiplier for transitionDelay:
style={{ transitionDelay: `${index * 100}ms` }} // faster
style={{ transitionDelay: `${index * 250}ms` }} // slower

Build docs developers (and LLMs) love