la_track / index.html
Nra's picture
Update index.html
30c9766 verified
Raw
History Blame Contribute Delete
19.4 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Growth Progress Tracker</title>
<!-- Load Tailwind CSS for styling -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Load React, ReactDOM, and Babel for processing JSX in the browser -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body class="bg-gray-50 font-sans text-gray-800">
<!-- This is where our React app will load -->
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
function App() {
const [logs, setLogs] = useState({});
const [currentDate, setCurrentDate] = useState(new Date());
// Modal State
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedDate, setSelectedDate] = useState(null);
const [formData, setFormData] = useState({
completed: '',
reason: '',
timeSpent: '',
sleep: '',
notes: ''
});
// --- Local Storage Setup ---
// Load data when the app starts
useEffect(() => {
const savedData = localStorage.getItem('growthTrackerData');
if (savedData) {
try {
setLogs(JSON.parse(savedData));
} catch (e) {
console.error("Failed to load local data", e);
}
}
}, []);
// Save data automatically whenever it changes
useEffect(() => {
if (Object.keys(logs).length > 0) {
localStorage.setItem('growthTrackerData', JSON.stringify(logs));
}
}, [logs]);
// --- Calendar Helpers ---
const getDaysInMonth = (year, month) => new Date(year, month + 1, 0).getDate();
const getFirstDayOfMonth = (year, month) => new Date(year, month, 1).getDay();
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const daysInMonth = getDaysInMonth(year, month);
const firstDay = getFirstDayOfMonth(year, month);
const monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
const prevMonth = () => setCurrentDate(new Date(year, month - 1, 1));
const nextMonth = () => setCurrentDate(new Date(year, month + 1, 1));
const formatDateString = (y, m, d) => {
return `${y}-${String(m + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
};
// --- Handlers ---
const openModal = (day) => {
const dateStr = formatDateString(year, month, day);
setSelectedDate(dateStr);
if (logs[dateStr]) {
setFormData(logs[dateStr]);
} else {
setFormData({ completed: '', reason: '', timeSpent: '', sleep: '', notes: '' });
}
setIsModalOpen(true);
};
const handleSave = () => {
if (!selectedDate) return;
setLogs(prevLogs => ({
...prevLogs,
[selectedDate]: formData
}));
setIsModalOpen(false);
};
// --- Export / Import ---
const handleExport = () => {
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(logs, null, 2));
const downloadAnchorNode = document.createElement('a');
downloadAnchorNode.setAttribute("href", dataStr);
downloadAnchorNode.setAttribute("download", `growth_tracker_backup.json`);
document.body.appendChild(downloadAnchorNode);
downloadAnchorNode.click();
downloadAnchorNode.remove();
};
const handleImport = (event) => {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const importedData = JSON.parse(e.target.result);
setLogs(prevLogs => {
const merged = { ...prevLogs, ...importedData };
localStorage.setItem('growthTrackerData', JSON.stringify(merged));
return merged;
});
alert("Data imported successfully!");
} catch (error) {
alert("Error loading file. Please make sure it's a valid JSON backup.");
console.error("Error parsing imported file:", error);
}
};
reader.readAsText(file);
event.target.value = null; // reset input
};
// --- Render Calendar Grid ---
const renderCalendarDays = () => {
const cells = [];
// Empty cells for days before the 1st
for (let i = 0; i < firstDay; i++) {
cells.push(<div key={`empty-${i}`} className="bg-gray-50 border border-gray-100 rounded-md"></div>);
}
// Actual days
for (let day = 1; day <= daysInMonth; day++) {
const dateStr = formatDateString(year, month, day);
const logData = logs[dateStr];
const today = new Date();
const isToday = dateStr === formatDateString(today.getFullYear(), today.getMonth(), today.getDate());
cells.push(
<div
key={day}
onClick={() => openModal(day)}
className={`relative h-24 sm:h-28 border rounded-md p-2 cursor-pointer transition-all hover:shadow-md
${isToday ? 'border-blue-400 border-2 bg-blue-50/30' : 'border-gray-200 hover:border-blue-300 bg-white'}`}
>
<span className={`text-sm font-semibold ${isToday ? 'text-blue-600' : 'text-gray-700'}`}>{day}</span>
{logData && logData.completed && (
<div className="absolute inset-0 flex flex-col items-center justify-center mt-4">
{logData.completed === 'yes' ? (
<div className="text-green-500 flex flex-col items-center">
<svg className="w-8 h-8 sm:w-10 sm:h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path></svg>
<span className="text-xs hidden sm:block font-medium">Done</span>
</div>
) : (
<div className="text-red-500 flex flex-col items-center">
<svg className="w-8 h-8 sm:w-10 sm:h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path></svg>
<span className="text-xs hidden sm:block font-medium truncate w-full text-center px-1" title={logData.reason}>{logData.reason || 'Missed'}</span>
</div>
)}
</div>
)}
</div>
);
}
return cells;
};
return (
<div className="min-h-screen p-4 sm:p-8">
<div className="max-w-5xl mx-auto">
{/* Header */}
<header className="mb-8 text-center">
<h1 className="text-3xl sm:text-4xl font-bold text-slate-800 mb-2">Growth Tracker</h1>
<p className="text-slate-500">Log your daily progress and hit your goals</p>
</header>
{/* Calendar Controls */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6 mb-8">
<div className="flex items-center justify-between mb-6">
<button onClick={prevMonth} className="p-2 rounded-full hover:bg-gray-100 transition-colors focus:outline-none">
<svg className="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 19l-7-7 7-7"></path></svg>
</button>
<h2 className="text-2xl font-bold text-slate-700">
{monthNames[month]} {year}
</h2>
<button onClick={nextMonth} className="p-2 rounded-full hover:bg-gray-100 transition-colors focus:outline-none">
<svg className="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7"></path></svg>
</button>
</div>
{/* Days of Week */}
<div className="grid grid-cols-7 gap-2 mb-2 text-center text-xs sm:text-sm font-semibold text-slate-500 uppercase tracking-wider">
<div>Sun</div><div>Mon</div><div>Tue</div><div>Wed</div><div>Thu</div><div>Fri</div><div>Sat</div>
</div>
{/* Calendar Grid */}
<div className="grid grid-cols-7 gap-2">
{renderCalendarDays()}
</div>
</div>
{/* Export / Import Section */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 flex flex-col sm:flex-row items-center justify-between">
<div>
<h3 className="font-semibold text-slate-700">Manage Your Data</h3>
<p className="text-sm text-slate-500">Download your data as a file to keep it safe.</p>
</div>
<div className="mt-4 sm:mt-0 flex gap-4">
<button onClick={handleExport} className="px-4 py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 text-sm font-medium rounded-lg transition-colors">
Export JSON File
</button>
<label className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors cursor-pointer text-center">
Import File
<input type="file" accept=".json" onChange={handleImport} className="hidden" />
</label>
</div>
</div>
</div>
{/* Entry Modal */}
{isModalOpen && (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-2xl max-w-md w-full shadow-2xl overflow-hidden">
<div className="bg-slate-50 px-6 py-4 border-b border-gray-100 flex justify-between items-center">
<h3 className="text-lg font-bold text-slate-800">Log Progress: {selectedDate}</h3>
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-gray-600 focus:outline-none">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path></svg>
</button>
</div>
<div className="p-6 space-y-5">
{/* Completed Toggle */}
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">Did you complete your routine?</label>
<div className="flex gap-4">
<button
onClick={() => setFormData({ ...formData, completed: 'yes', reason: '' })}
className={`flex-1 py-3 px-4 rounded-xl font-medium transition-all border-2 ${
formData.completed === 'yes' ? 'bg-green-50 border-green-500 text-green-700' : 'bg-white border-gray-200 text-gray-500'
}`}
>Yes</button>
<button
onClick={() => setFormData({ ...formData, completed: 'no' })}
className={`flex-1 py-3 px-4 rounded-xl font-medium transition-all border-2 ${
formData.completed === 'no' ? 'bg-red-50 border-red-500 text-red-700' : 'bg-white border-gray-200 text-gray-500'
}`}
>No</button>
</div>
</div>
{/* Reason Dropdown */}
{formData.completed === 'no' && (
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">What happened?</label>
<select
value={formData.reason}
onChange={(e) => setFormData({ ...formData, reason: e.target.value })}
className="w-full border border-gray-300 rounded-lg p-3 bg-white focus:ring-2 focus:ring-red-200 outline-none"
>
<option value="" disabled>Select a reason...</option>
<option value="Tired">Too Tired</option>
<option value="Laziness">Laziness / Procrastination</option>
<option value="Sick">Felt Sick</option>
<option value="Too Busy">Too Busy / No Time</option>
<option value="Forgot">Forgot</option>
<option value="Other">Other</option>
</select>
</div>
)}
{/* Stats Row */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">Time Spent (Mins)</label>
<input type="number" placeholder="15" value={formData.timeSpent} onChange={(e) => setFormData({ ...formData, timeSpent: e.target.value })} className="w-full border border-gray-300 rounded-lg p-3 outline-none" />
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">Sleep (Hours)</label>
<input type="number" step="0.5" placeholder="8.5" value={formData.sleep} onChange={(e) => setFormData({ ...formData, sleep: e.target.value })} className="w-full border border-gray-300 rounded-lg p-3 outline-none" />
</div>
</div>
{/* Notes */}
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">Notes</label>
<textarea rows="2" placeholder="How did you feel today?" value={formData.notes} onChange={(e) => setFormData({ ...formData, notes: e.target.value })} className="w-full border border-gray-300 rounded-lg p-3 outline-none resize-none"></textarea>
</div>
</div>
<div className="bg-gray-50 px-6 py-4 border-t border-gray-100 flex justify-end gap-3">
<button onClick={() => setIsModalOpen(false)} className="px-5 py-2.5 text-gray-600 font-medium hover:bg-gray-200 rounded-lg">Cancel</button>
<button
onClick={handleSave}
disabled={!formData.completed || (formData.completed === 'no' && !formData.reason)}
className="px-5 py-2.5 bg-blue-600 text-white font-medium hover:bg-blue-700 rounded-lg disabled:opacity-50"
>Save Progress</button>
</div>
</div>
</div>
)}
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</body>
</html>