Portfolio Moderno is assembled from six React components, each responsible for one discrete region of the page. Five of them (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.
Header, Hero, About, Projects, Contact) are full-width sections, and one (Footer) is a simple signature bar. Every section component except Header follows the same visibility pattern: a useRef is attached to the outermost <section> element, and a native IntersectionObserver watches that element. The moment the section crosses a 0.1 threshold in the viewport, an isVisible flag flips to true and is never reset — producing a one-shot fade-in that fires as the user scrolls down the page.
Components
Header — fixed top bar
Header — fixed top bar
Section: Fixed navigation bar pinned to the top of the viewport (
Internal state:
Animation: When
position: fixed). Not a scroll-section; it sits above all content.Props:| Prop | Type | Description |
|---|---|---|
theme | 'light' | 'dark' | Current color theme — used to render the correct toggle icon (Sun or Moon) |
toggleTheme | () => void | Callback to flip the theme; wired to the ThemeToggle button |
| State | Initial | Description |
|---|---|---|
isMenuOpen | false | Controls the mobile hamburger menu visibility |
isScrolled | false | Becomes true when window.scrollY > 10 |
isScrolled is true, the header gains bg-white/80 backdrop-blur-md shadow-md dark:bg-dark/80 via a transition-all duration-300 class — creating a frosted-glass effect that appears as soon as the user begins scrolling.Navigation behavior: Each NavLink intercepts the default anchor behavior. On click, it calculates the target element’s position, subtracts a 64px header offset (h-16), and calls window.scrollTo({ behavior: 'smooth' }) — giving pixel-accurate smooth scroll without a router dependency.Hero — #home
Hero — #home
Section:
Typing words array:Called with
id="home" — the full-height landing section rendered first in <main>.Props: None. Hero is entirely self-contained.Internal state:| State | Initial | Description |
|---|---|---|
isVisible | false | Set to true by IntersectionObserver on mount (threshold 0.1) |
typedText | '' | The currently-visible string from useTypingEffect |
typeSpeed: 100, deleteSpeed: 50, delay: 2000.Animation: The outer <section> transitions from opacity-0 to opacity-100 over 1 000 ms when isVisible becomes true. The text column adds a translate-y-0 entrance and the profile image column uses scale-100 with a 200 ms CSS delay. The profile image also applies animate-float motion-reduce:animate-none for the continuous vertical drift, and animate-tilt on the glow ring behind it.About — #about
About — #about
Section:
Skills array: The component defines an array of 18
Staggered animation: Each skill card receives an inline
id="about" — bio paragraph and skills grid.Props: None.Internal state:| State | Initial | Description |
|---|---|---|
isVisible | false | Triggered by IntersectionObserver (threshold 0.1) |
Skill objects inline:| Name | Icon source | Color class |
|---|---|---|
| React | Custom ReactIcon | text-sky-500 |
| TypeScript | Custom TypeScriptIcon | text-blue-600 |
| JavaScript | Custom JavaScriptIcon | text-yellow-400 |
| Next.js | Custom NextJsIcon | text-sky |
| Node.js | Custom NodeJsIcon | text-green-500 |
| Tailwind CSS | Custom TailwindIcon | text-teal-500 |
| HTML5 | Custom HTMLIcon | text-orange-600 |
| CSS3 | Custom CSSIcon | text-blue-500 |
| Figma | Custom FigmaIcon | text-pink-500 |
| Git | Custom GitIcon | text-red-600 |
| Python | FaPython (react-icons) | text-sky-600 |
| Angular | FaAngular (react-icons) | text-red-600 |
| Docker | FaDocker (react-icons) | text-sky-500 |
| Mongodb | SiMongodb (react-icons) | text-green-500 |
| vscode | BiLogoVisualStudio (react-icons) | text-blue-500 |
| Postgresql | SiPostgresql (react-icons) | text-blue-700 |
| Notion | PiNotionLogoBold (react-icons) | text-black dark:text-white |
| Vue.js | FaVuejs (react-icons) | text-green-500 |
transitionDelay of index * 100ms and transitions its opacity and transform values driven by isVisible, creating a ripple-style reveal as the section enters the viewport. The profile image uses animate-float motion-reduce:animate-none.Projects — #projects
Projects — #projects
Section:
Projects data: Four hardcoded
id="projects" — a 2-column responsive card grid.Props: None.Internal state:| State | Initial | Description |
|---|---|---|
isVisible | false | Triggered by IntersectionObserver (threshold 0.1) |
Project objects are defined inline:| # | Title | Tags |
|---|---|---|
| 1 | DSS Comparador de Países Backend | Spring Boot, Java, PostgreSQL |
| 2 | DSS Comparador de Países Frontend | TypeScript, Tailwind CSS, JavaScript |
| 3 | Sitio Web de Portafolio | React, Tailwind CSS, Vite |
| 4 | Sistema para Empresa de Arepas | Laravel, Laravel Native, App de Escritorio |
ProjectCard sub-component: Each project is rendered by an internal ProjectCard functional component that accepts { project, isVisible, index }. It adds its own loaded boolean state that flips when the card image fires its onLoad event, swapping out an animate-pulse skeleton placeholder for the real image. Cards receive a transitionDelay of index * 150ms for a staggered entrance. On hover, action icons (GitHub and optional live-demo link) slide in from the top of the image overlay.Contact — #contact
Contact — #contact
Section:
Links:
Animation: The entire
id="contact" — centered call-to-action with social links.Props: None.Internal state:| State | Initial | Description |
|---|---|---|
isVisible | false | Triggered by IntersectionObserver (threshold 0.1) |
| Platform | URL |
|---|---|
| GitHub | https://github.com/nicolasgrajaleshoyos |
https://www.linkedin.com/in/nicolas-grajales-hoyos-12182a353/ | |
| Email (compose) | https://mail.google.com/mail/?view=cm&fs=1&to=nicolasgrajaleshoyos@gmail.com |
<section> transitions from opacity-0 translate-y-8 to opacity-100 translate-y-0 over 700 ms when isVisible is true. Two decorative blurred div elements (bg-primary/10 and bg-blue-500/10) are positioned absolutely in the corners as ambient color blobs, rendered behind the content via z-10.Footer — page bottom
Footer — page bottom
Scroll Animation Pattern
Hero, About, Projects, and Contact all share this identical useRef + IntersectionObserver pattern. Once the section enters the viewport, the observer disconnects itself (unobserve) so the animation fires only once per page load.
isVisible flag is then consumed directly in Tailwind conditional classes: