Proyecto 4: Portfolio Personal Interactivo
Objetivos de aprendizajeAl 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
- Hero: Presentación con efecto de typing
- Sobre mí: Descripción personal con skills animadas
- Proyectos: Cards dinámicas desde la API de GitHub
- Experiencia: Timeline interactivo
- Contacto: Formulario con validación en tiempo real
- Footer: Links a redes sociales
Funcionalidades JavaScript
- Efecto typing en el hero (texto que se "escribe" solo)
- Scroll animations (elementos que aparecen al hacer scroll)
- Navegación sticky con resaltado de sección activa
- Proyectos desde GitHub API (repos reales)
- Filtrado de proyectos por lenguaje
- Formulario con validación en tiempo real
- Modo oscuro/claro con toggle
- Botón scroll-to-top
- Lazy loading de imágenes
- 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
- React.js — La libreria de UI mas popular (ideal para tu siguiente curso en Cursalo)
- TypeScript — JavaScript con tipos estaticos (demandado en el mercado)
- Next.js — Framework de React para apps de produccion
Backend
- Node.js + Express — Backend con JavaScript
- PostgreSQL — Base de datos relacional
- APIs REST — Disenar y construir tus propias APIs
Herramientas
- Git + GitHub — Control de versiones (esencial)
- VS Code avanzado — Shortcuts, extensions, debugging
- 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.
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.
- 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