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.

Portfolio Moderno is assembled from six React components, each responsible for one discrete region of the page. Five of them (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

Section: Fixed navigation bar pinned to the top of the viewport (position: fixed). Not a scroll-section; it sits above all content.Props:
PropTypeDescription
theme'light' | 'dark'Current color theme — used to render the correct toggle icon (Sun or Moon)
toggleTheme() => voidCallback to flip the theme; wired to the ThemeToggle button
Internal state:
StateInitialDescription
isMenuOpenfalseControls the mobile hamburger menu visibility
isScrolledfalseBecomes true when window.scrollY > 10
Animation: When 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.
Section: id="home" — the full-height landing section rendered first in <main>.Props: None. Hero is entirely self-contained.Internal state:
StateInitialDescription
isVisiblefalseSet to true by IntersectionObserver on mount (threshold 0.1)
typedText''The currently-visible string from useTypingEffect
Typing words array:
[
  'Desarrollador full stack',
  'Creador de experiencias digitales',
  'Soy un entusiasta de las monedas digitales y el hacking ético',
]
Called with 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.
Section: id="about" — bio paragraph and skills grid.Props: None.Internal state:
StateInitialDescription
isVisiblefalseTriggered by IntersectionObserver (threshold 0.1)
Skills array: The component defines an array of 18 Skill objects inline:
NameIcon sourceColor class
ReactCustom ReactIcontext-sky-500
TypeScriptCustom TypeScriptIcontext-blue-600
JavaScriptCustom JavaScriptIcontext-yellow-400
Next.jsCustom NextJsIcontext-sky
Node.jsCustom NodeJsIcontext-green-500
Tailwind CSSCustom TailwindIcontext-teal-500
HTML5Custom HTMLIcontext-orange-600
CSS3Custom CSSIcontext-blue-500
FigmaCustom FigmaIcontext-pink-500
GitCustom GitIcontext-red-600
PythonFaPython (react-icons)text-sky-600
AngularFaAngular (react-icons)text-red-600
DockerFaDocker (react-icons)text-sky-500
MongodbSiMongodb (react-icons)text-green-500
vscodeBiLogoVisualStudio (react-icons)text-blue-500
PostgresqlSiPostgresql (react-icons)text-blue-700
NotionPiNotionLogoBold (react-icons)text-black dark:text-white
Vue.jsFaVuejs (react-icons)text-green-500
Staggered animation: Each skill card receives an inline 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.
Section: id="projects" — a 2-column responsive card grid.Props: None.Internal state:
StateInitialDescription
isVisiblefalseTriggered by IntersectionObserver (threshold 0.1)
Projects data: Four hardcoded Project objects are defined inline:
#TitleTags
1DSS Comparador de Países BackendSpring Boot, Java, PostgreSQL
2DSS Comparador de Países FrontendTypeScript, Tailwind CSS, JavaScript
3Sitio Web de PortafolioReact, Tailwind CSS, Vite
4Sistema para Empresa de ArepasLaravel, 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.
Section: id="contact" — centered call-to-action with social links.Props: None.Internal state:
StateInitialDescription
isVisiblefalseTriggered by IntersectionObserver (threshold 0.1)
Links:
PlatformURL
GitHubhttps://github.com/nicolasgrajaleshoyos
LinkedInhttps://www.linkedin.com/in/nicolas-grajales-hoyos-12182a353/
Email (compose)https://mail.google.com/mail/?view=cm&fs=1&to=nicolasgrajaleshoyos@gmail.com
Animation: The entire <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.

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.
import { useState, useEffect, useRef } from 'react';

const sectionRef = useRef<HTMLElement>(null);
const [isVisible, setIsVisible] = useState(false);

useEffect(() => {
  const observer = new IntersectionObserver(
    ([entry]) => {
      if (entry.isIntersecting) {
        setIsVisible(true);          // flip once — never resets
        observer.unobserve(entry.target); // disconnect after first trigger
      }
    },
    { threshold: 0.1 }              // fire when 10% of the section is visible
  );

  const currentRef = sectionRef.current;
  if (currentRef) observer.observe(currentRef);

  return () => {
    if (currentRef) observer.unobserve(currentRef); // cleanup on unmount
  };
}, []);
The isVisible flag is then consumed directly in Tailwind conditional classes:
<section
  ref={sectionRef}
  className={`transition-all duration-700 ease-out ${
    isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'
  }`}
>

Build docs developers (and LLMs) love