<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tutoring Earnings Tracker</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* Custom Animations */
@keyframes slideUpFade {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes pop {
0% { transform: scale(1); }
50% { transform: scale(1.15); color: #a5b4fc; } /* text-indigo-300 */
100% { transform: scale(1); }
}
.animate-entry {
animation: slideUpFade 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
.animate-pop {
animation: pop 0.4s ease-out;
}
/* Smooth input transitions */
.input-field {
transition: all 0.3s ease;
}
.input-field:focus {
transform: translateY(-2px);
box-shadow: 0 10px 25px -5px rgba(79, 70, 229, 0.15);
}
/* Hide number input arrows */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
-moz-appearance: textfield;
}
</style>
</head>
<body class="bg-gradient-to-br from-slate-100 to-indigo-50 min-h-screen p-4 md:p-8 flex items-center justify-center font-sans text-slate-800">
<!-- Main Card container with entrance animation -->
<div class="animate-entry max-w-md w-full mx-auto bg-white/90 backdrop-blur-xl rounded-3xl shadow-2xl shadow-indigo-200/50 border border-white p-6 sm:p-8">
<!-- Header -->
<div class="flex items-center justify-between mb-8">
<h1 class="text-2xl sm:text-3xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-indigo-600 to-purple-600 tracking-tight">
Tutor Tracker
</h1>
<div class="bg-indigo-100 text-indigo-600 p-2 sm:p-3 rounded-2xl shadow-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
<!-- Settings -->
<div class="grid grid-cols-2 gap-4 mb-8">
<div class="group">
<label class="block text-[10px] sm:text-xs font-bold text-slate-400 uppercase tracking-wider mb-2 group-focus-within:text-indigo-500 transition-colors">Threshold (min)</label>
<input type="number" id="threshold" value="45" class="input-field w-full p-3 sm:p-4 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none bg-slate-50 focus:bg-white text-lg font-bold text-slate-700">
</div>
<div class="group">
<label class="block text-[10px] sm:text-xs font-bold text-slate-400 uppercase tracking-wider mb-2 group-focus-within:text-indigo-500 transition-colors">Rate ($/hr)</label>
<input type="number" id="rate" value="20" class="input-field w-full p-3 sm:p-4 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none bg-slate-50 focus:bg-white text-lg font-bold text-slate-700">
</div>
</div>
<!-- Logging Session -->
<div class="mb-8">
<label class="block text-[10px] sm:text-xs font-bold text-slate-400 uppercase tracking-wider mb-2 focus-within:text-indigo-500 transition-colors">Log New Session</label>
<div class="flex gap-2 sm:gap-3 relative">
<input type="number" id="minutes" placeholder="Minutes..." class="input-field flex-1 p-3 sm:p-4 border border-slate-200 rounded-2xl outline-none bg-slate-50 focus:bg-white focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-lg font-medium placeholder-slate-300">
<button id="addBtn" onclick="addSession()" class="bg-gradient-to-tr from-indigo-600 to-indigo-500 hover:from-indigo-500 hover:to-indigo-400 active:scale-95 text-white font-semibold px-4 sm:px-6 py-3 sm:py-4 rounded-2xl shadow-lg shadow-indigo-200 hover:shadow-indigo-300 transition-all duration-200 flex items-center justify-center min-w-[100px]">
<span id="btnText">Add</span>
</button>
</div>
</div>
<!-- Dashboard Display -->
<div class="relative overflow-hidden bg-slate-900 rounded-3xl p-6 sm:p-8 text-center shadow-xl">
<!-- Background decorative blur elements -->
<div class="absolute -top-12 -right-12 w-40 h-40 bg-indigo-500 rounded-full mix-blend-screen filter blur-3xl opacity-40"></div>
<div class="absolute -bottom-12 -left-12 w-40 h-40 bg-purple-500 rounded-full mix-blend-screen filter blur-3xl opacity-40"></div>
<div class="relative z-10">
<p class="text-xs sm:text-sm text-indigo-300 font-medium tracking-widest uppercase mb-1">Total Earned</p>
<!-- This element animates when earnings update -->
<div id="totalEarnings" class="text-5xl sm:text-6xl font-black text-white my-2 tracking-tight inline-block transition-transform duration-300">$0.00</div>
<div class="mt-6 pt-5 border-t border-slate-700/50 flex justify-between items-center text-sm">
<span class="text-slate-400 font-medium">Minutes Logged</span>
<span id="totalMins" class="font-bold text-indigo-200 bg-white/10 px-3 py-1 rounded-xl shadow-inner">0</span>
</div>
</div>
</div>
<!-- Reset Data -->
<button onclick="resetData()" class="mt-8 w-full flex items-center justify-center gap-2 text-slate-400 hover:text-red-500 text-sm font-medium transition-colors p-3 rounded-xl hover:bg-red-50 active:scale-95 group">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 group-hover:animate-pulse" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Reset All History
</button>
</div>
<script>
let data = JSON.parse(localStorage.getItem('tutorData')) || { totalMinutes: 0 };
let lastEarningsStr = "$0.00";
function updateUI(triggerAnimation = false) {
const threshold = parseInt(document.getElementById('threshold').value) || 45;
const rate = parseFloat(document.getElementById('rate').value) || 0;
const payouts = Math.floor(data.totalMinutes / threshold);
const totalEarnings = (payouts * (threshold / 60) * rate).toFixed(2);
const newEarningsStr = `$${totalEarnings}`;
const earningsEl = document.getElementById('totalEarnings');
// Only play pop animation if the earnings amount actually changed
if (triggerAnimation && newEarningsStr !== lastEarningsStr) {
// Reset animation by temporarily removing the class
earningsEl.classList.remove('animate-pop');
void earningsEl.offsetWidth; // Trigger a DOM reflow
earningsEl.classList.add('animate-pop');
}
earningsEl.innerText = newEarningsStr;
lastEarningsStr = newEarningsStr;
document.getElementById('totalMins').innerText = data.totalMinutes;
localStorage.setItem('tutorData', JSON.stringify(data));
}
function addSession() {
const minsInput = document.getElementById('minutes');
const mins = parseInt(minsInput.value);
if (!isNaN(mins) && mins > 0) {
data.totalMinutes += mins;
minsInput.value = '';
// Visual Button Feedback
const btn = document.getElementById('addBtn');
const btnText = document.getElementById('btnText');
// Swap classes to show success state
btn.classList.remove('from-indigo-600', 'to-indigo-500');
btn.classList.add('from-emerald-500', 'to-teal-500');
btnText.innerText = 'Added! ✓';
setTimeout(() => {
btn.classList.remove('from-emerald-500', 'to-teal-500');
btn.classList.add('from-indigo-600', 'to-indigo-500');
btnText.innerText = 'Add';
}, 1200);
// Update UI and trigger earnings pop
updateUI(true);
}
}
function resetData() {
if (confirm('Are you sure you want to clear all logged minutes?')) {
data.totalMinutes = 0;
updateUI(false);
}
}
// Listen for user changing settings so it updates immediately
document.getElementById('threshold').addEventListener('input', () => updateUI(true));
document.getElementById('rate').addEventListener('input', () => updateUI(true));
// Allow pressing 'Enter' inside the minutes input to log time
document.getElementById('minutes').addEventListener('keypress', function (e) {
if (e.key === 'Enter') addSession();
});
// Initialize display on load
updateUI(false);
</script>
</body>
</html>