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 a single-page application (SPA) built by Nicolas Grajales Hoyos. The browser loads a single index.html entry point, which bootstraps a Vite-powered development server and build pipeline. At runtime, React mounts the entire application into the #root div via src/main.tsx, hydrating the component tree from a single root <App /> component. Because all navigation stays on one page, section transitions are handled through smooth-scroll anchor links rather than a client-side router.

Application Entry Point

App.tsx is the root of the React component tree. It owns the only piece of shared UI state — the active color theme — and passes it down to Header. Every other component is self-contained and renders its own section of the page.
App
├── Header   (fixed top bar — receives theme + toggleTheme)
└── main
    ├── Hero       (#home)
    ├── About      (#about)
    ├── Projects   (#projects)
    └── Contact    (#contact)
└── Footer

State Management

Portfolio Moderno uses no external state library (no Redux, Zustand, or Context API). The only shared state is the theme value ('light' | 'dark'), managed entirely by the custom useTheme hook that lives inside App.tsx. The hook returns a [theme, toggleTheme] tuple. theme is forwarded as a prop to Header so it can render the correct icon in the theme-toggle button; all other components are stateless with respect to global state and manage only their own local animation flags.

Custom Hooks

useTheme()

Defined in App.tsx. Manages dark/light mode persistence and synchronisation with the DOM.
const useTheme = (): [Theme, () => void] => {
  const [theme, setTheme] = useState<Theme>('light');

  // Initialization: read persisted value or fall back to OS preference
  useEffect(() => {
    const storedTheme = localStorage.getItem('theme') as Theme | null;
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
    const initialTheme = storedTheme || (prefersDark ? 'dark' : 'light');
    setTheme(initialTheme);
  }, []);

  // Sync: update <html> class and persist to localStorage on every change
  useEffect(() => {
    if (theme === 'dark') {
      document.documentElement.classList.add('dark');
      localStorage.setItem('theme', 'dark');
    } else {
      document.documentElement.classList.remove('dark');
      localStorage.setItem('theme', 'light');
    }
  }, [theme]);

  const toggleTheme = () => {
    setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  return [theme, toggleTheme];
};
Initialization order: stored localStorage value → OS prefers-color-scheme → default 'light'.

useTypingEffect(words, typeSpeed?, deleteSpeed?, delay?)

Defined in Hero.tsx. Produces an animated typing illusion that cycles through an array of strings. Returns the currently-visible substring so the component can render it alongside a blinking cursor.
ParameterTypeSignature defaultCall-site value (Hero.tsx)Description
wordsstring[](required)Array of phrases to cycle through
typeSpeednumber150100Milliseconds per character when typing
deleteSpeednumber10050Milliseconds per character when deleting
delaynumber10002000Pause in ms after a word is fully typed before deletion begins
const useTypingEffect = (
  words: string[],
  typeSpeed = 150,
  deleteSpeed = 100,
  delay = 1000
) => {
  const [text, setText] = useState('');
  const [isDeleting, setIsDeleting] = useState(false);
  const [wordIndex, setWordIndex] = useState(0);

  useEffect(() => {
    const handleTyping = () => {
      const currentWord = words[wordIndex];
      const updatedText = isDeleting
        ? currentWord.substring(0, text.length - 1)
        : currentWord.substring(0, text.length + 1);

      setText(updatedText);

      if (!isDeleting && updatedText === currentWord) {
        setTimeout(() => setIsDeleting(true), delay);
      } else if (isDeleting && updatedText === '') {
        setIsDeleting(false);
        setWordIndex((prev) => (prev + 1) % words.length);
      }
    };

    const speed = isDeleting ? deleteSpeed : typeSpeed;
    const timeout = setTimeout(handleTyping, speed);
    return () => clearTimeout(timeout);
  }, [text, isDeleting, wordIndex, words, typeSpeed, deleteSpeed, delay]);

  return text;
};

TypeScript Interfaces

Both shared data-shape contracts are defined in src/types.ts and imported wherever needed.

Project

Used by the Projects component and its ProjectCard sub-component to type each portfolio entry.
export interface Project {
  id: number;
  title: string;
  description: string;
  imageUrl: string;
  tags: string[];
  liveUrl?: string;   // optional — shown only if a live demo URL is present
  codeUrl?: string;   // optional — shown only if a repository URL is present
}

Skill

Used by the About component to type each entry in the skills grid. The icon field accepts any React functional component that takes an optional className prop, allowing both custom SVG icons and third-party icon-library components to be used interchangeably.
export interface Skill {
  name: string;
  icon: React.FC<{ className?: string }>;
  className?: string; // Tailwind color class applied to the icon
}

Build Pipeline

Vite is configured in vite.config.ts with the following settings:
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import postcss from './postcss.config.js';

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, '.', '');
  return {
    css: {
      postcss, // PostCSS with Tailwind CSS
    },
    server: {
      port: 3000,   // Dev server on http://localhost:3000
      host: '0.0.0.0',
    },
    plugins: [react()], // @vitejs/plugin-react — enables Fast Refresh
    define: {
      'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
      'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY),
    },
    resolve: {
      alias: {
        '@': path.resolve(__dirname, 'src'), // @/ maps to src/
      },
    },
  };
});
SettingValueEffect
Dev server port3000Local dev available at http://localhost:3000
Path alias @/src/All imports use @/components/… instead of relative paths
React plugin@vitejs/plugin-reactBabel-powered HMR with Fast Refresh
CSS processorPostCSS + TailwindUtility class generation and purging

Build docs developers (and LLMs) love