Volver al curso

JavaScript Desde Cero: Tu Primer Lenguaje de Programación

leccion
22 / 22
beginner
8 horas
Proyectos Prácticos

Proyecto 4: Portfolio Personal Interactivo

Lectura
50 min~9 min lectura

Proyecto 4: Portfolio Personal Interactivo

Objetivos de aprendizaje

Al finalizar este proyecto serás capaz de:

  • Construir un sitio web portfolio completo y profesional
  • Implementar scroll suave, animaciones de entrada, lazy loading y navegación dinámica
  • Integrar datos dinámicos desde la API de GitHub para mostrar proyectos reales
  • Crear un formulario de contacto funcional con validación completa
  • Aplicar TODOS los conceptos aprendidos en el curso en un proyecto unificado

Descripción del proyecto

Este es el proyecto final del curso. Vas a construir tu portfolio personal como desarrollador, aplicando todo lo aprendido: variables, funciones, DOM, eventos, fetch API, async/await y más.

Secciones del portfolio

  1. Hero: Presentación con efecto de typing
  2. Sobre mí: Descripción personal con skills animadas
  3. Proyectos: Cards dinámicas desde la API de GitHub
  4. Experiencia: Timeline interactivo
  5. Contacto: Formulario con validación en tiempo real
  6. Footer: Links a redes sociales

Funcionalidades JavaScript

  1. Efecto typing en el hero (texto que se "escribe" solo)
  2. Scroll animations (elementos que aparecen al hacer scroll)
  3. Navegación sticky con resaltado de sección activa
  4. Proyectos desde GitHub API (repos reales)
  5. Filtrado de proyectos por lenguaje
  6. Formulario con validación en tiempo real
  7. Modo oscuro/claro con toggle
  8. Botón scroll-to-top
  9. Lazy loading de imágenes
  10. Contador animado de estadísticas

Código JavaScript del portfolio
// ============================================
// PORTFOLIO PERSONAL
// Proyecto Final del Curso JavaScript Desde Cero
// ============================================

// ===== 1. EFECTO TYPING =====
class TypingEffect {
  constructor(elemento, textos, velocidad = 100, pausa = 2000) {
    this.elemento = elemento;
    this.textos = textos;
    this.velocidad = velocidad;
    this.pausa = pausa;
    this.textoIndex = 0;
    this.charIndex = 0;
    this.borrando = false;
    this.iniciar();
  }
  
  iniciar() {
    const textoActual = this.textos[this.textoIndex];
    
    if (!this.borrando) {
      this.elemento.textContent = textoActual.substring(0, this.charIndex + 1);
      this.charIndex++;
      
      if (this.charIndex === textoActual.length) {
        this.borrando = true;
        setTimeout(() => this.iniciar(), this.pausa);
        return;
      }
    } else {
      this.elemento.textContent = textoActual.substring(0, this.charIndex - 1);
      this.charIndex--;
      
      if (this.charIndex === 0) {
        this.borrando = false;
        this.textoIndex = (this.textoIndex + 1) % this.textos.length;
      }
    }
    
    setTimeout(() => this.iniciar(), this.borrando ? 50 : this.velocidad);
  }
}

// Inicializar typing
const typingElement = document.getElementById("typing-text");
if (typingElement) {
  new TypingEffect(typingElement, [
    "Desarrollador JavaScript",
    "Creador de Aplicaciones Web",
    "Estudiante de Cursalo",
    "Apasionado por la Tecnologia"
  ]);
}

// ===== 2. SCROLL ANIMATIONS =====
const observerOptions = {
  threshold: 0.15,
  rootMargin: "0px 0px -50px 0px"
};

const scrollObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.classList.add("visible");
      scrollObserver.unobserve(entry.target);
    }
  });
}, observerOptions);

// Observar elementos con clase .animate-on-scroll
document.querySelectorAll(".animate-on-scroll").forEach(el => {
  scrollObserver.observe(el);
});

// ===== 3. NAVEGACION STICKY =====
const nav = document.querySelector("nav");
const sections = document.querySelectorAll("section[id]");
const navLinks = document.querySelectorAll(".nav-link");

// Resaltar sección activa en el nav
const navObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      navLinks.forEach(link => {
        link.classList.toggle("active", 
          link.getAttribute("href") === `#${entry.target.id}`
        );
      });
    }
  });
}, { threshold: 0.3 });

sections.forEach(section => navObserver.observe(section));

// Scroll suave para links del nav
navLinks.forEach(link => {
  link.addEventListener("click", (e) => {
    e.preventDefault();
    const targetId = link.getAttribute("href");
    const target = document.querySelector(targetId);
    if (target) {
      target.scrollIntoView({ behavior: "smooth", block: "start" });
    }
  });
});

// ===== 4. CARGAR PROYECTOS DESDE GITHUB =====
async function cargarProyectosGitHub(username) {
  const container = document.getElementById("proyectos-grid");
  const filtrosContainer = document.getElementById("proyectos-filtros");
  
  try {
    container.innerHTML = '<div class="loading">Cargando proyectos...</div>';
    
    const resp = await fetch(
      `https://api.github.com/users/${username}/repos?sort=updated&per_page=12`
    );
    
    if (!resp.ok) throw new Error(`GitHub API: ${resp.status}`);
    const repos = await resp.json();
    
    // Filtrar repos con descripción y no forks
    const proyectos = repos
      .filter(r => !r.fork && r.description)
      .slice(0, 9);
    
    if (proyectos.length === 0) {
      container.innerHTML = '<p>No se encontraron proyectos</p>';
      return;
    }
    
    // Obtener lenguajes únicos para filtros
    const lenguajes = [...new Set(proyectos.map(p => p.language).filter(Boolean))];
    
    // Renderizar filtros
    filtrosContainer.innerHTML = `
      <button class="filtro-btn activo" data-lang="todos">Todos</button>
      ${lenguajes.map(lang => 
        `<button class="filtro-btn" data-lang="${lang}">${lang}</button>`
      ).join("")}
    `;
    
    // Renderizar proyectos
    function renderizarProyectos(filtro = "todos") {
      const filtrados = filtro === "todos" 
        ? proyectos 
        : proyectos.filter(p => p.language === filtro);
      
      container.innerHTML = filtrados.map(repo => `
        <article class="proyecto-card animate-on-scroll" data-lang="${repo.language}">
          <div class="proyecto-header">
            <h3>${repo.name.replace(/-/g, ' ')}</h3>
            ${repo.language ? `<span class="lang-badge">${repo.language}</span>` : ''}
          </div>
          <p class="proyecto-desc">${repo.description || 'Sin descripcion'}</p>
          <div class="proyecto-stats">
            <span title="Estrellas">${repo.stargazers_count} stars</span>
            <span title="Forks">${repo.forks_count} forks</span>
          </div>
          <div class="proyecto-links">
            <a href="${repo.html_url}" target="_blank" rel="noopener">Ver codigo</a>
            ${repo.homepage ? `<a href="${repo.homepage}" target="_blank" rel="noopener">Demo</a>` : ''}
          </div>
        </article>
      `).join("");
      
      // Re-observar nuevos elementos
      container.querySelectorAll(".animate-on-scroll").forEach(el => {
        scrollObserver.observe(el);
      });
    }
    
    renderizarProyectos();
    
    // Event delegation para filtros
    filtrosContainer.addEventListener("click", (e) => {
      const btn = e.target.closest(".filtro-btn");
      if (!btn) return;
      
      filtrosContainer.querySelectorAll(".filtro-btn").forEach(b => b.classList.remove("activo"));
      btn.classList.add("activo");
      renderizarProyectos(btn.dataset.lang);
    });
    
  } catch (error) {
    container.innerHTML = `
      <div class="error-state">
        <p>No se pudieron cargar los proyectos</p>
        <button onclick="cargarProyectosGitHub('${username}')">Reintentar</button>
      </div>
    `;
  }
}

// Cargar proyectos (reemplazá con tu username)
cargarProyectosGitHub("octocat");

// ===== 5. FORMULARIO DE CONTACTO =====
const formulario = document.getElementById("contacto-form");

if (formulario) {
  const campos = {
    nombre: { 
      validar: v => v.trim().length >= 2, 
      mensaje: "El nombre debe tener al menos 2 caracteres" 
    },
    email: { 
      validar: v => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), 
      mensaje: "Ingresa un email valido" 
    },
    asunto: { 
      validar: v => v.trim().length >= 5, 
      mensaje: "El asunto debe tener al menos 5 caracteres" 
    },
    mensaje: { 
      validar: v => v.trim().length >= 20, 
      mensaje: "El mensaje debe tener al menos 20 caracteres" 
    }
  };
  
  // Validación en tiempo real
  Object.keys(campos).forEach(campo => {
    const input = formulario.querySelector(`[name="${campo}"]`);
    if (!input) return;
    
    input.addEventListener("blur", () => validarCampo(input, campos[campo]));
    input.addEventListener("input", () => {
      if (input.classList.contains("error")) {
        validarCampo(input, campos[campo]);
      }
    });
  });
  
  function validarCampo(input, regla) {
    const esValido = regla.validar(input.value);
    const errorEl = input.parentElement.querySelector(".error-msg") || 
      (() => {
        const el = document.createElement("span");
        el.className = "error-msg";
        input.parentElement.appendChild(el);
        return el;
      })();
    
    input.classList.toggle("error", !esValido);
    input.classList.toggle("valid", esValido);
    errorEl.textContent = esValido ? "" : regla.mensaje;
    return esValido;
  }
  
  formulario.addEventListener("submit", async (e) => {
    e.preventDefault();
    
    // Validar todos los campos
    let todosValidos = true;
    Object.keys(campos).forEach(campo => {
      const input = formulario.querySelector(`[name="${campo}"]`);
      if (input && !validarCampo(input, campos[campo])) {
        todosValidos = false;
      }
    });
    
    if (!todosValidos) return;
    
    const btnSubmit = formulario.querySelector("button[type='submit']");
    const textoOriginal = btnSubmit.textContent;
    btnSubmit.disabled = true;
    btnSubmit.textContent = "Enviando...";
    
    // Simular envío
    await new Promise(resolve => setTimeout(resolve, 1500));
    
    const datos = Object.fromEntries(new FormData(formulario));
    console.log("Formulario enviado:", datos);
    
    // Mostrar éxito
    formulario.innerHTML = `
      <div class="success-message">
        <h3>Mensaje enviado</h3>
        <p>Gracias ${datos.nombre}, te respondere pronto.</p>
      </div>
    `;
  });
}

// ===== 6. MODO OSCURO/CLARO =====
const btnTema = document.getElementById("btn-tema");
const temaGuardado = localStorage.getItem("tema") || "oscuro";
document.documentElement.setAttribute("data-theme", temaGuardado);

if (btnTema) {
  btnTema.addEventListener("click", () => {
    const temaActual = document.documentElement.getAttribute("data-theme");
    const nuevoTema = temaActual === "oscuro" ? "claro" : "oscuro";
    document.documentElement.setAttribute("data-theme", nuevoTema);
    localStorage.setItem("tema", nuevoTema);
  });
}

// ===== 7. CONTADOR ANIMADO =====
function animarContador(elemento, objetivo, duracion = 2000) {
  let inicio = 0;
  const incremento = objetivo / (duracion / 16);
  
  function actualizar() {
    inicio += incremento;
    if (inicio >= objetivo) {
      elemento.textContent = objetivo;
      return;
    }
    elemento.textContent = Math.floor(inicio);
    requestAnimationFrame(actualizar);
  }
  actualizar();
}

// Observer para activar contadores al ser visibles
const contadoresObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const objetivo = parseInt(entry.target.dataset.target);
      animarContador(entry.target, objetivo);
      contadoresObserver.unobserve(entry.target);
    }
  });
}, { threshold: 0.5 });

document.querySelectorAll(".stat-number").forEach(el => {
  contadoresObserver.observe(el);
});

// ===== 8. SCROLL TO TOP =====
const btnScrollTop = document.getElementById("btn-scroll-top");
if (btnScrollTop) {
  window.addEventListener("scroll", () => {
    btnScrollTop.classList.toggle("visible", window.scrollY > 500);
  });
  
  btnScrollTop.addEventListener("click", () => {
    window.scrollTo({ top: 0, behavior: "smooth" });
  });
}

console.log("Portfolio cargado. Todos los sistemas operativos.");

Resumen de TODOS los conceptos del curso aplicados

Este proyecto final integra TODO lo que aprendiste:

Modulo Conceptos aplicados
Modulo 1: Fundamentos Variables (const/let), tipos de datos, operadores, template literals, console methods
Modulo 2: Control de flujo Condicionales (validacion), loops (forEach, for...of), funciones (declarations, arrows, closures), scope
Modulo 3: Arrays y DOM Arrays (map, filter, reduce, Set), objetos (destructuring, spread), DOM manipulation, eventos, event delegation
Modulo 4: Asincrono Fetch API, async/await, Promise, try/catch, manejo de errores HTTP
Modulo 5: Proyectos Arquitectura modular, patrones de diseno (Observer, Factory), localStorage, IntersectionObserver, clase ES6, debounce

Proximos pasos: tu camino como desarrollador

Terminaste el curso "JavaScript Desde Cero". Ahora tenes una base solida para seguir creciendo. Estos son los caminos recomendados:

Frontend

  1. React.js — La libreria de UI mas popular (ideal para tu siguiente curso en Cursalo)
  2. TypeScript — JavaScript con tipos estaticos (demandado en el mercado)
  3. Next.js — Framework de React para apps de produccion

Backend

  1. Node.js + Express — Backend con JavaScript
  2. PostgreSQL — Base de datos relacional
  3. APIs REST — Disenar y construir tus propias APIs

Herramientas

  1. Git + GitHub — Control de versiones (esencial)
  2. VS Code avanzado — Shortcuts, extensions, debugging
  3. Terminal — Comandos basicos de Linux/Mac

Recursos recomendados

  • MDN Web Docs (developer.mozilla.org) — La referencia definitiva de JavaScript
  • JavaScript.info — Tutorial moderno y completo
  • FreeCodeCamp — Proyectos practicos gratuitos
  • Cursalo.com — Segui con React, Node.js y mas cursos de desarrollo

Quiz final del curso

1. Si tuvieras que elegir UNA cosa que aprendiste en este curso que te parece mas valiosa, ¿cual seria?

No hay respuesta correcta. Reflexiona sobre tu aprendizaje.

2. ¿Que proyecto del modulo 5 te gusto mas y por que?

Esta reflexion te ayuda a descubrir que area del desarrollo te apasiona.

3. ¿Cual es tu proximo objetivo como desarrollador?

Escribilo. Los objetivos escritos tienen mas probabilidad de cumplirse.


💡 Concepto Clave

Revisemos los puntos más importantes de esta lección antes de continuar.

Felicitaciones

Si llegaste hasta aca, completaste un viaje desde cero absoluto hasta construir aplicaciones web completas con JavaScript. Eso no es poca cosa. Ahora tenes las herramientas para:

  • Crear sitios web interactivos
  • Consumir APIs y mostrar datos dinamicos
  • Construir aplicaciones CRUD funcionales
  • Escribir codigo limpio y mantenible
  • Resolver problemas con logica de programacion

El siguiente paso es seguir construyendo. Cada proyecto que hagas te va a ensenar algo nuevo. No tengas miedo de equivocarte — los errores son tus mejores maestros.

Nos vemos en el proximo curso en Cursalo.com.

🧠 Pon a prueba tu conocimiento
¿Cuál es el aspecto más importante que aprendiste en esta lección?
  • Comprendo el concepto principal y puedo explicarlo con mis palabras
  • Entiendo cómo aplicarlo en mi situación específica
  • Necesito repasar algunas partes antes de continuar
  • Quiero ver más ejemplos prácticos del tema
✅ ¡Excelente! Continúa con la siguiente lección para profundizar más.