Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Loan Calculator</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<style> | |
/* Custom scrollbar for schedule */ | |
.schedule-container::-webkit-scrollbar { | |
width: 6px; | |
height: 6px; | |
} | |
.schedule-container::-webkit-scrollbar-track { | |
background: #f1f1f1; | |
} | |
.schedule-container::-webkit-scrollbar-thumb { | |
background: #888; | |
border-radius: 3px; | |
} | |
.schedule-container::-webkit-scrollbar-thumb:hover { | |
background: #555; | |
} | |
/* Animation for results */ | |
@keyframes fadeIn { | |
from { opacity: 0; transform: translateY(10px); } | |
to { opacity: 1; transform: translateY(0); } | |
} | |
.fade-in { | |
animation: fadeIn 0.5s ease-out forwards; | |
} | |
</style> | |
</head> | |
<body class="bg-gray-100 min-h-screen"> | |
<div class="flex items-center justify-center min-h-screen"> | |
<div class="w-full max-w-2xl mx-4"> | |
<div class="bg-white rounded-xl shadow-xl overflow-hidden"> | |
<div class="text-center p-6 bg-indigo-700 text-white"> | |
<h1 class="text-3xl font-bold mb-2">Loan Calculator</h1> | |
<p class="opacity-90">Calculate your EMI and payment schedule</p> | |
</div> | |
<div class="p-6"> | |
<div class="mb-6"> | |
<label for="loanAmount" class="block text-gray-700 font-medium mb-2">Loan Amount ($)</label> | |
<div class="relative"> | |
<span class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500">$</span> | |
<input type="number" id="loanAmount" class="w-full pl-8 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" placeholder="10,000" min="1"> | |
</div> | |
</div> | |
<div class="mb-6"> | |
<label for="interestRate" class="block text-gray-700 font-medium mb-2">Interest Rate (% p.a.)</label> | |
<div class="relative"> | |
<span class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500">%</span> | |
<input type="number" id="interestRate" class="w-full pl-8 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" placeholder="8.5" step="0.01" min="0"> | |
</div> | |
</div> | |
<div class="mb-6"> | |
<label for="loanTerm" class="block text-gray-700 font-medium mb-2">Loan Term</label> | |
<div class="flex gap-4"> | |
<div class="flex-1"> | |
<input type="number" id="loanTerm" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" placeholder="5" min="1"> | |
</div> | |
<select id="termType" class="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"> | |
<option value="years">Years</option> | |
<option value="months">Months</option> | |
</select> | |
</div> | |
</div> | |
<div class="mb-6"> | |
<label class="block text-gray-700 font-medium mb-2">Payment Type</label> | |
<div class="grid grid-cols-2 gap-3"> | |
<button id="emiBtn" class="payment-type-btn bg-indigo-600 text-white py-2 px-4 rounded-lg shadow-sm hover:bg-indigo-700 transition active:bg-indigo-800"> | |
<i class="fas fa-calendar-alt mr-2"></i> EMI | |
</button> | |
<button id="otherBtn" class="payment-type-btn bg-gray-200 text-gray-700 py-2 px-4 rounded-lg shadow-sm hover:bg-gray-300 transition" disabled> | |
<i class="fas fa-clock mr-2"></i> Other (Coming Soon) | |
</button> | |
</div> | |
</div> | |
<button id="calculateBtn" class="w-full bg-indigo-600 text-white py-3 px-4 rounded-lg font-medium hover:bg-indigo-700 transition active:bg-indigo-800 shadow-md"> | |
Calculate <i class="fas fa-calculator ml-2"></i> | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Results Modal --> | |
<div id="resultsModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 hidden z-50"> | |
<div class="bg-white rounded-xl shadow-xl w-full max-w-4xl max-h-[90vh] flex flex-col"> | |
<div class="p-6 bg-indigo-700 text-white flex justify-between items-center"> | |
<h2 class="text-2xl font-bold">Loan Calculation Results</h2> | |
<button id="closeModal" class="text-white hover:text-gray-200 text-2xl">×</button> | |
</div> | |
<div class="p-6 overflow-y-auto flex-1"> | |
<div class="grid grid-cols-2 gap-4 mb-6"> | |
<div class="bg-gray-50 p-4 rounded-lg"> | |
<p class="text-sm text-gray-500">Monthly Payment</p> | |
<p id="monthlyPayment" class="text-2xl font-bold text-indigo-600">$0</p> | |
</div> | |
<div class="bg-gray-50 p-4 rounded-lg"> | |
<p class="text-sm text-gray-500">Total Interest</p> | |
<p id="totalInterest" class="text-2xl font-bold text-indigo-600">$0</p> | |
</div> | |
<div class="bg-gray-50 p-4 rounded-lg"> | |
<p class="text-sm text-gray-500">Total Payment</p> | |
<p id="totalPayment" class="text-2xl font-bold text-indigo-600">$0</p> | |
</div> | |
<div class="bg-gray-50 p-4 rounded-lg"> | |
<p class="text-sm text-gray-500">Loan Term</p> | |
<p id="loanTermResult" class="text-2xl font-bold text-indigo-600">0 Months</p> | |
</div> | |
</div> | |
<div class="border-t pt-4"> | |
<div class="flex justify-between items-center mb-4"> | |
<h3 class="text-lg font-semibold text-gray-800">Payment Schedule</h3> | |
<button id="downloadBtn" class="text-indigo-600 hover:text-indigo-800 flex items-center"> | |
<i class="fas fa-download mr-1"></i> Export | |
</button> | |
</div> | |
<div class="schedule-container max-h-96 overflow-y-auto"> | |
<table class="w-full text-sm"> | |
<thead class="sticky top-0 bg-gray-100"> | |
<tr class="text-left text-gray-600 border-b"> | |
<th class="py-2 px-3">#</th> | |
<th class="py-2 px-3">Payment</th> | |
<th class="py-2 px-3">Principal</th> | |
<th class="py-2 px-3">Interest</th> | |
<th class="py-2 px-3">Balance</th> | |
</tr> | |
</thead> | |
<tbody id="scheduleBody" class="divide-y divide-gray-200"> | |
<!-- Schedule will be populated here --> | |
</tbody> | |
</table> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
const calculateBtn = document.getElementById('calculateBtn'); | |
const emiBtn = document.getElementById('emiBtn'); | |
const otherBtn = document.getElementById('otherBtn'); | |
const resultsSection = document.getElementById('resultsSection'); | |
const downloadBtn = document.getElementById('downloadBtn'); | |
// Format currency | |
const formatCurrency = (amount) => { | |
return new Intl.NumberFormat('en-US', { | |
style: 'currency', | |
currency: 'USD', | |
minimumFractionDigits: 2 | |
}).format(amount); | |
}; | |
// Calculate EMI | |
const calculateEMI = (principal, annualRate, termYears, termType) => { | |
let termMonths; | |
if (termType === 'years') { | |
termMonths = termYears * 12; | |
} else { | |
termMonths = termYears; | |
} | |
const monthlyRate = annualRate / 100 / 12; | |
const emi = principal * monthlyRate * Math.pow(1 + monthlyRate, termMonths) / (Math.pow(1 + monthlyRate, termMonths) - 1); | |
return { | |
emi: emi, | |
termMonths: termMonths, | |
totalPayment: emi * termMonths, | |
totalInterest: (emi * termMonths) - principal | |
}; | |
}; | |
// Generate payment schedule | |
const generateSchedule = (principal, annualRate, termMonths, emi) => { | |
const monthlyRate = annualRate / 100 / 12; | |
let balance = principal; | |
const schedule = []; | |
for (let i = 1; i <= termMonths; i++) { | |
const interest = balance * monthlyRate; | |
const principalPayment = emi - interest; | |
balance -= principalPayment; | |
// Ensure balance doesn't go negative due to rounding | |
if (i === termMonths && Math.abs(balance) < 0.01) { | |
balance = 0; | |
} | |
schedule.push({ | |
month: i, | |
payment: emi, | |
principal: principalPayment, | |
interest: interest, | |
balance: balance > 0 ? balance : 0 | |
}); | |
} | |
return schedule; | |
}; | |
// Modal elements | |
const resultsModal = document.getElementById('resultsModal'); | |
const closeModal = document.getElementById('closeModal'); | |
// Close modal handler | |
closeModal.addEventListener('click', function() { | |
resultsModal.classList.add('hidden'); | |
}); | |
// Close modal when clicking outside | |
resultsModal.addEventListener('click', function(e) { | |
if (e.target === resultsModal) { | |
resultsModal.classList.add('hidden'); | |
} | |
}); | |
// Calculate button click handler | |
calculateBtn.addEventListener('click', function() { | |
const loanAmount = parseFloat(document.getElementById('loanAmount').value); | |
const interestRate = parseFloat(document.getElementById('interestRate').value); | |
const loanTerm = parseFloat(document.getElementById('loanTerm').value); | |
const termType = document.getElementById('termType').value; | |
// Validate inputs | |
if (isNaN(loanAmount) || isNaN(interestRate) || isNaN(loanTerm) || loanAmount <= 0 || loanTerm <= 0) { | |
alert('Please enter valid values for all fields'); | |
return; | |
} | |
// Calculate EMI | |
const result = calculateEMI(loanAmount, interestRate, loanTerm, termType); | |
// Update summary | |
document.getElementById('monthlyPayment').textContent = formatCurrency(result.emi); | |
document.getElementById('totalInterest').textContent = formatCurrency(result.totalInterest); | |
document.getElementById('totalPayment').textContent = formatCurrency(result.totalPayment); | |
document.getElementById('loanTermResult').textContent = `${result.termMonths} Months`; | |
// Generate and display schedule | |
const schedule = generateSchedule(loanAmount, interestRate, result.termMonths, result.emi); | |
const scheduleBody = document.getElementById('scheduleBody'); | |
scheduleBody.innerHTML = ''; | |
schedule.forEach((payment, index) => { | |
const row = document.createElement('tr'); | |
row.className = index % 2 === 0 ? 'bg-white' : 'bg-gray-50'; | |
row.innerHTML = ` | |
<td class="py-2 px-3">${payment.month}</td> | |
<td class="py-2 px-3">${formatCurrency(payment.payment)}</td> | |
<td class="py-2 px-3">${formatCurrency(payment.principal)}</td> | |
<td class="py-2 px-3">${formatCurrency(payment.interest)}</td> | |
<td class="py-2 px-3">${formatCurrency(payment.balance)}</td> | |
`; | |
scheduleBody.appendChild(row); | |
}); | |
// Show modal with results | |
resultsModal.classList.remove('hidden'); | |
}); | |
// Payment type buttons | |
emiBtn.addEventListener('click', function() { | |
emiBtn.classList.remove('bg-gray-200', 'text-gray-700'); | |
emiBtn.classList.add('bg-indigo-600', 'text-white'); | |
otherBtn.classList.remove('bg-indigo-600', 'text-white'); | |
otherBtn.classList.add('bg-gray-200', 'text-gray-700'); | |
}); | |
otherBtn.addEventListener('click', function() { | |
alert('Other payment types coming soon! Currently only EMI is available.'); | |
}); | |
// Export schedule | |
downloadBtn.addEventListener('click', function() { | |
const loanAmount = document.getElementById('loanAmount').value; | |
const interestRate = document.getElementById('interestRate').value; | |
const loanTerm = document.getElementById('loanTerm').value; | |
const termType = document.getElementById('termType').value; | |
if (!resultsSection.classList.contains('hidden')) { | |
// Create CSV content | |
let csv = 'Payment #,Payment,Principal,Interest,Balance\n'; | |
const rows = document.querySelectorAll('#scheduleBody tr'); | |
rows.forEach(row => { | |
const cols = row.querySelectorAll('td'); | |
const rowData = []; | |
cols.forEach(col => { | |
// Remove currency symbols and commas for CSV | |
const text = col.textContent.replace(/[$,]/g, ''); | |
rowData.push(text); | |
}); | |
csv += rowData.join(',') + '\n'; | |
}); | |
// Create download link | |
const blob = new Blob([csv], { type: 'text/csv' }); | |
const url = URL.createObjectURL(blob); | |
const a = document.createElement('a'); | |
a.setAttribute('hidden', ''); | |
a.setAttribute('href', url); | |
a.setAttribute('download', `loan_schedule_${loanAmount}_${interestRate}_${loanTerm}${termType === 'years' ? 'y' : 'm'}.csv`); | |
document.body.appendChild(a); | |
a.click(); | |
document.body.removeChild(a); | |
} else { | |
alert('Please calculate a loan first'); | |
} | |
}); | |
// Add animation to results when they appear | |
const observer = new IntersectionObserver((entries) => { | |
entries.forEach(entry => { | |
if (entry.isIntersecting) { | |
entry.target.classList.add('fade-in'); | |
} | |
}); | |
}, { threshold: 0.1 }); | |
document.querySelectorAll('#resultsSection > div').forEach(el => { | |
observer.observe(el); | |
}); | |
}); | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=sombochea/loan-calculator" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |