Savings Goal Tracker — Crea un rastreador de metas de ahorro con HTML, JS y Bootstrap 5

Compartir

Aprende a crear un Savings Goal Tracker con Bootstrap 5 y JavaScript. Registra aportes, muestra porcentaje alcanzado, historial y estimación del tiempo restante según el promedio de ahorro

En este tutorial aprenderás a crear tu propio Savings Goal Tracker, una aplicación web sencilla que te permitirá:

  • Establecer una meta de ahorro.
  • Registrar aportes periódicos.
  • Visualizar el porcentaje alcanzado.
  • Consultar un historial de aportes.
  • Recibir una estimación del tiempo que te falta para llegar a la meta según tu promedio de ahorro.

Tecnologías utilizadas

  • HTML5 → estructura de la app.
  • Bootstrap 5 → estilos rápidos y diseño responsivo.
  • CSS → personalización adicional.
  • JavaScript → lógica del ahorro, cálculos y manejo de LocalStorage.

Paso 1: Estructura de archivos

Necesitamos tres archivos principales:

index.html
style.css
script.js

Paso 2: Código HTML (index.html)

Crea un archivo llamado index.html y coloca el siguiente contenido:

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Savings Goal Tracker</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
	<link href="style.css" rel="stylesheet">

</head> <body> <div class="container py-4"> <div class="main-container p-4"> <div class="text-center mb-4"> <h1 class="display-4 fw-bold text-primary mb-2"> <i class="fas fa-piggy-bank me-3"></i>Savings Goal Tracker </h1> <p class="lead text-muted">Establece tus metas y alcanza tus sueños financieros</p> </div>

<!-- Goal Setup Form --> <div id="goalForm" class="mb-4"> <div class="card border-0 shadow-sm"> <div class="card-body p-4"> <h5 class="card-title mb-3"> <i class="fas fa-target me-2 text-primary"></i>Establecer Meta de Ahorro </h5> <form id="savingsGoalForm"> <div class="row"> <div class="col-md-6 mb-3"> <label class="form-label fw-semibold">Nombre de la Meta</label> <input type="text" class="form-control" id="goalName" placeholder="Ej: Vacaciones en Europa" required> </div> <div class="col-md-6 mb-3"> <label class="form-label fw-semibold">Meta ($)</label> <input type="number" class="form-control" id="goalAmount" placeholder="5000" min="1" required> </div> </div> <button type="submit" class="btn btn-primary"> <i class="fas fa-plus me-2"></i>Crear Meta </button> </form> </div> </div> </div>

<!-- Current Goal Display --> <div id="currentGoal" style="display: none;"> <div class="goal-card"> <div class="row align-items-center"> <div class="col-md-8"> <h3 id="goalTitle" class="mb-3"></h3> <div class="row"> <div class="col-6"> <h5>Ahorrado</h5> <h2 id="currentAmount">$0</h2> </div> <div class="col-6"> <h5>Meta</h5> <h2 id="targetAmount">$0</h2> </div> </div> <div class="mt-3"> <small id="timeEstimate" class="opacity-75"></small> </div> </div> <div class="col-md-4 text-center"> <div class="progress-circle mx-auto" id="progressCircle"> <div class="progress-inner"> <span id="progressPercent">0%</span> </div> </div> </div> </div> </div>

<!-- Add Contribution Form --> <div class="card border-0 shadow-sm mb-4"> <div class="card-body p-4"> <h5 class="card-title mb-3"> <i class="fas fa-plus-circle me-2 text-success"></i>Agregar Aporte </h5> <form id="contributionForm"> <div class="row"> <div class="col-md-8 mb-3"> <label class="form-label fw-semibold">Cantidad ($)</label> <input type="number" class="form-control" id="contributionAmount" placeholder="100" min="0.01" step="0.01" required> </div> <div class="col-md-4 mb-3"> <label class="form-label fw-semibold">&nbsp;</label> <button type="submit" class="btn btn-success w-100"> <i class="fas fa-plus me-2"></i>Agregar </button> </div> </div> </form> </div> </div>

<!-- Statistics --> <div class="row mb-4"> <div class="col-md-4"> <div class="stats-card"> <i class="fas fa-chart-line icon-lg text-primary"></i> <h6 class="fw-semibold">Promedio Mensual</h6> <h4 id="monthlyAverage">$0</h4> </div> </div> <div class="col-md-4"> <div class="stats-card"> <i class="fas fa-calendar-alt icon-lg text-success"></i> <h6 class="fw-semibold">Total Aportes</h6> <h4 id="totalContributions">0</h4> </div> </div> <div class="col-md-4"> <div class="stats-card"> <i class="fas fa-percentage icon-lg text-info"></i> <h6 class="fw-semibold">Progreso</h6> <h4 id="progressStats">0%</h4> </div> </div> </div>

<!-- Contribution History --> <div class="card border-0 shadow-sm"> <div class="card-body p-4"> <div class="d-flex justify-content-between align-items-center mb-3"> <h5 class="card-title mb-0"> <i class="fas fa-history me-2 text-primary"></i>Historial de Aportes </h5> <div class="btn-group"> <button class="btn btn-outline-warning btn-sm" onclick="editGoal()"> <i class="fas fa-edit me-1"></i>Editar Meta </button> <button class="btn btn-outline-secondary btn-sm" onclick="clearAllContributions()"> <i class="fas fa-broom me-1"></i>Limpiar Aportes </button> <button class="btn btn-outline-danger btn-sm" onclick="resetGoal()"> <i class="fas fa-redo me-1"></i>Nueva Meta </button> </div> </div> <div id="contributionHistory"> <div class="empty-state"> <i class="fas fa-coins"></i> <h6>No hay aportes aún</h6> <p>Agrega tu primer aporte para comenzar a ver tu progreso</p> </div> </div> </div> </div> </div> </div> </div>

<!-- Edit Goal Modal --> <div class="modal fade" id="editGoalModal" tabindex="-1"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title"> <i class="fas fa-edit me-2 text-warning"></i>Editar Meta de Ahorro </h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <form id="editGoalForm"> <div class="mb-3"> <label class="form-label fw-semibold">Nombre de la Meta</label> <input type="text" class="form-control" id="editGoalName" required> </div> <div class="mb-3"> <label class="form-label fw-semibold">Meta ($)</label> <input type="number" class="form-control" id="editGoalAmount" min="1" required> </div> </form> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button> <button type="button" class="btn btn-warning" id="saveGoalChanges"> <i class="fas fa-save me-2"></i>Guardar Cambios </button> </div> </div> </div> </div>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="script.js"></script> </body> </html>

Paso 3: Estilos personalizados (style.css)

Crea un archivo style.css con los siguientes estilos básicos:

        body {
            box-sizing: border-box;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }

.main-container { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); border-radius: 20px; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); margin: 20px auto; max-width: 800px; }

.goal-card { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); border-radius: 15px; color: white; padding: 2rem; margin-bottom: 2rem; box-shadow: 0 10px 30px rgba(79, 172, 254, 0.3); }

.progress-circle { width: 120px; height: 120px; border-radius: 50%; background: conic-gradient(#00f2fe 0deg, #00f2fe var(--progress), rgba(255,255,255,0.3) var(--progress), rgba(255,255,255,0.3) 360deg); display: flex; align-items: center; justify-content: center; position: relative; }

.progress-inner { width: 90px; height: 90px; background: rgba(255, 255, 255, 0.2); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 1.2rem; }

.contribution-card { background: white; border-radius: 15px; padding: 1.5rem; margin-bottom: 1rem; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08); border-left: 4px solid #4facfe; transition: transform 0.2s ease; }

.contribution-card:hover { transform: translateY(-2px); }

.btn-primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; border-radius: 10px; padding: 12px 30px; font-weight: 600; transition: all 0.3s ease; }

.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3); }

.btn-success { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); border: none; border-radius: 10px; padding: 12px 30px; font-weight: 600; }

.form-control { border-radius: 10px; border: 2px solid #e9ecef; padding: 12px 15px; transition: all 0.3s ease; }

.form-control:focus { border-color: #4facfe; box-shadow: 0 0 0 0.2rem rgba(79, 172, 254, 0.25); }

.stats-card { background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); border-radius: 15px; padding: 1.5rem; text-align: center; margin-bottom: 1rem; }

.icon-lg { font-size: 2rem; margin-bottom: 0.5rem; }

.fade-in { animation: fadeIn 0.5s ease-in; }

@keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }

.empty-state { text-align: center; padding: 3rem; color: #6c757d; }

.empty-state i { font-size: 4rem; margin-bottom: 1rem; opacity: 0.5; }

Paso 4: Lógica en JavaScript (script.js)

Ahora la parte más importante. Crea un archivo script.js y agrega este código:

   // variables globales 
        let goal = null;
        let contributions = [];

// Initializar la app function initApp() { loadData(); initEventListeners(); }

function initEventListeners() { document.getElementById('savingsGoalForm').addEventListener('submit', (e) => { e.preventDefault(); createGoal(); });

document.getElementById('contributionForm').addEventListener('submit', (e) => { e.preventDefault(); addContribution(); });

document.getElementById('saveGoalChanges').addEventListener('click', (e) => { e.preventDefault(); saveGoalEdit(); }); }

function createGoal() { const name = document.getElementById('goalName').value; const amount = parseFloat(document.getElementById('goalAmount').value);

goal = { name: name, target: amount, current: 0, createdAt: new Date() };

contributions = []; saveData(); updateDisplay(); showGoal();

// Reset formulario document.getElementById('savingsGoalForm').reset(); }

function addContribution() { const amount = parseFloat(document.getElementById('contributionAmount').value);

const contribution = { amount: amount, date: new Date(), id: Date.now() };

contributions.unshift(contribution); goal.current += amount;

saveData(); updateDisplay(); renderContributionHistory();

// Reset formulario document.getElementById('contributionForm').reset();

showSuccessMessage(amount); }

function showSuccessMessage(amount) { const button = document.querySelector('#contributionForm button'); const originalText = button.innerHTML; button.innerHTML = '<i class="fas fa-check me-2"></i>¡Agregado!'; button.classList.add('btn-outline-success'); button.classList.remove('btn-success');

setTimeout(() => { button.innerHTML = originalText; button.classList.remove('btn-outline-success'); button.classList.add('btn-success'); }, 1500); }

function updateDisplay() { if (!goal) return;

const percentage = Math.min((goal.current / goal.target) * 100, 100); const progressDegrees = (percentage / 100) * 360;

// Actualiza info de la meta document.getElementById('goalTitle').textContent = goal.name; document.getElementById('currentAmount').textContent = `$${goal.current.toLocaleString()}`; document.getElementById('targetAmount').textContent = `$${goal.target.toLocaleString()}`;

// actualiza el circulo de progreso const progressCircle = document.getElementById('progressCircle'); progressCircle.style.setProperty('--progress', `${progressDegrees}deg`); document.getElementById('progressPercent').textContent = `${Math.round(percentage)}%`;

// actualiza estadisticas updateStatistics(); updateTimeEstimate(); }

function updateStatistics() { const totalContributions = contributions.length; const percentage = Math.min((goal.current / goal.target) * 100, 100);

// Calcula el promedio mensual let monthlyAverage = 0; if (contributions.length > 0) { const firstContribution = new Date(contributions[contributions.length - 1].date); const now = new Date(); const monthsDiff = Math.max(1, (now - firstContribution) / (1000 * 60 * 60 * 24 * 30)); monthlyAverage = goal.current / monthsDiff; }

document.getElementById('monthlyAverage').textContent = `$${Math.round(monthlyAverage).toLocaleString()}`; document.getElementById('totalContributions').textContent = totalContributions; document.getElementById('progressStats').textContent = `${Math.round(percentage)}%`; }

function updateTimeEstimate() { if (contributions.length < 2) { document.getElementById('timeEstimate').textContent = 'Agrega más aportes para ver estimación de tiempo'; return; }

const remaining = goal.target - goal.current; if (remaining <= 0) { document.getElementById('timeEstimate').textContent = ' ¡Meta alcanzada! ¡Felicitaciones!'; return; }

// Calcular promedio de contribuciones 3 anteriores const recentContributions = contributions.slice(0, Math.min(3, contributions.length)); const avgContribution = recentContributions.reduce((sum, c) => sum + c.amount, 0) / recentContributions.length;

if (avgContribution > 0) { const contributionsNeeded = Math.ceil(remaining / avgContribution); const timeEstimate = contributionsNeeded === 1 ? 'Con el próximo aporte alcanzarás tu meta' : `Aproximadamente ${contributionsNeeded} aportes más para alcanzar tu meta`;

document.getElementById('timeEstimate').textContent = timeEstimate; } else { document.getElementById('timeEstimate').textContent = 'Continúa ahorrando para ver estimación'; } }

function renderContributionHistory() { const historyContainer = document.getElementById('contributionHistory');

if (contributions.length === 0) { historyContainer.innerHTML = ` <div class="empty-state"> <i class="fas fa-coins"></i> <h6>No hay aportes aún</h6> <p>Agrega tu primer aporte para comenzar a ver tu progreso</p> </div> `; return; }

historyContainer.innerHTML = contributions.map(contribution => ` <div class="contribution-card fade-in"> <div class="d-flex justify-content-between align-items-center"> <div> <h6 class="mb-1 text-success fw-semibold">+$${contribution.amount.toLocaleString()}</h6> <small class="text-muted"> <i class="fas fa-calendar me-1"></i> ${new Date(contribution.date).toLocaleDateString('es-ES', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' })} </small> </div> <div class="text-end d-flex align-items-center gap-2"> <button class="btn btn-outline-danger btn-sm" onclick="deleteContribution(${contribution.id})" title="Eliminar aporte"> <i class="fas fa-trash"></i> </button> <i class="fas fa-arrow-up text-success fs-4"></i> </div> </div> </div> `).join(''); }

function showGoal() { document.getElementById('goalForm').style.display = 'none'; document.getElementById('currentGoal').style.display = 'block'; renderContributionHistory(); }

function deleteContribution(contributionId) { const contribution = contributions.find(c => c.id === contributionId); if (!contribution) return;

const confirmMessage = `¿Estás seguro de que quieres eliminar el aporte de $${contribution.amount.toLocaleString()}?`;

if (confirm(confirmMessage)) {

contributions = contributions.filter(c => c.id !== contributionId);

goal.current -= contribution.amount;

if (goal.current < 0) { goal.current = 0; }

saveData(); updateDisplay(); renderContributionHistory();

showDeleteMessage(); } }

function showDeleteMessage() {

const alertDiv = document.createElement('div'); alertDiv.className = 'alert alert-success alert-dismissible fade show position-fixed'; alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 1050; min-width: 300px;'; alertDiv.innerHTML = ` <i class="fas fa-check-circle me-2"></i> Aporte eliminado correctamente <button type="button" class="btn-close" data-bs-dismiss="alert"></button> `;

document.body.appendChild(alertDiv);

// Auto cierre despues de 3 segundos setTimeout(() => { if (alertDiv.parentNode) { alertDiv.remove(); } }, 3000); }

function editGoal() { if (!goal) return;

document.getElementById('editGoalName').value = goal.name; document.getElementById('editGoalAmount').value = goal.target;

const modal = new bootstrap.Modal(document.getElementById('editGoalModal')); modal.show(); }

function saveGoalEdit() { const newName = document.getElementById('editGoalName').value; const newAmount = parseFloat(document.getElementById('editGoalAmount').value);

if (!newName || !newAmount || newAmount <= 0) { alert('Por favor, completa todos los campos correctamente.'); return; }

goal.name = newName; goal.target = newAmount;

saveData(); updateDisplay();

// Cerrar modal const modal = bootstrap.Modal.getInstance(document.getElementById('editGoalModal')); modal.hide();

// Mostra mensaje showEditMessage(); }

function showEditMessage() { const alertDiv = document.createElement('div'); alertDiv.className = 'alert alert-info alert-dismissible fade show position-fixed'; alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 1050; min-width: 300px;'; alertDiv.innerHTML = ` <i class="fas fa-edit me-2"></i> Meta actualizada correctamente <button type="button" class="btn-close" data-bs-dismiss="alert"></button> `;

document.body.appendChild(alertDiv);

setTimeout(() => { if (alertDiv.parentNode) { alertDiv.remove(); } }, 3000); }

function clearAllContributions() { if (contributions.length === 0) { alert('No hay aportes para eliminar.'); return; }

const confirmMessage = `¿Estás seguro de que quieres eliminar todos los ${contributions.length} aportes? Esta acción no se puede deshacer.`;

if (confirm(confirmMessage)) { contributions = []; goal.current = 0;

saveData(); updateDisplay(); renderContributionHistory();

showClearMessage(); } }

function showClearMessage() { const alertDiv = document.createElement('div'); alertDiv.className = 'alert alert-warning alert-dismissible fade show position-fixed'; alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 1050; min-width: 300px;'; alertDiv.innerHTML = ` <i class="fas fa-broom me-2"></i> Todos los aportes han sido eliminados <button type="button" class="btn-close" data-bs-dismiss="alert"></button> `;

document.body.appendChild(alertDiv);

setTimeout(() => { if (alertDiv.parentNode) { alertDiv.remove(); } }, 3000); }

function resetGoal() { if (confirm('¿Estás seguro de que quieres crear una nueva meta? Se perderá el progreso actual.')) { goal = null; contributions = []; saveData();

document.getElementById('goalForm').style.display = 'block'; document.getElementById('currentGoal').style.display = 'none'; } }

function saveData() { localStorage.setItem('savingsGoal', JSON.stringify(goal)); localStorage.setItem('savingsContributions', JSON.stringify(contributions)); }

function loadData() { const savedGoal = localStorage.getItem('savingsGoal'); const savedContributions = localStorage.getItem('savingsContributions');

if (savedGoal) { goal = JSON.parse(savedGoal); }

if (savedContributions) { contributions = JSON.parse(savedContributions); }

if (goal) { showGoal(); updateDisplay(); } }

// Initializar la app cuando el DOM cargue document.addEventListener('DOMContentLoaded', () => { initApp(); });

Paso 5: Prueba tu aplicación

Ahora abre index.html en tu navegador y prueba el funcionamiento:

  • Establece una meta (ejemplo: $1000).
  • Agrega aportes periódicos (ejemplo: $100 cada semana).
  • Observa cómo la barra de progreso aumenta.
  • Consulta el historial y la estimación de tiempo restante.

Conclusión

Con este Savings Goal Tracker ya tienes una aplicación funcional que te ayudará a gestionar mejor tus ahorros. La clave está en el uso de LocalStorage, que permite mantener tus datos guardados entre sesiones.

Puedes mejorar esta base con características adicionales como:

  • Exportar historial a CSV/Excel.
  • Permitir múltiples metas de ahorro.
  • Añadir gráficos con Chart.js para visualizar aportes.

Eres libre de modificarlo y crear una versión más completa usando bases de datos como MySQL y un lenguaje de servidor como PHP.

VER DEMOSTRACIÓN DESCARGAR ARCHIVOS

Obed Alvarado

Sobre el autor

Artículo redactado por Obed Alvarado. Comparto experiencias prácticas sobre PHP, MySQL, aplicaciones web, instalación en hosting y herramientas para negocios.

Comentarios