| <!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> |
| |
| <script src="https://cdn.tailwindcss.com"></script> |
| |
| <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"> |
| |
| |
| <div id="root"></div> |
|
|
| <script type="text/babel"> |
| const { useState, useEffect } = React; |
| |
| function App() { |
| const [logs, setLogs] = useState({}); |
| const [currentDate, setCurrentDate] = useState(new Date()); |
| |
| |
| const [isModalOpen, setIsModalOpen] = useState(false); |
| const [selectedDate, setSelectedDate] = useState(null); |
| const [formData, setFormData] = useState({ |
| completed: '', |
| reason: '', |
| timeSpent: '', |
| sleep: '', |
| notes: '' |
| }); |
| |
| |
| |
| useEffect(() => { |
| const savedData = localStorage.getItem('growthTrackerData'); |
| if (savedData) { |
| try { |
| setLogs(JSON.parse(savedData)); |
| } catch (e) { |
| console.error("Failed to load local data", e); |
| } |
| } |
| }, []); |
| |
| |
| useEffect(() => { |
| if (Object.keys(logs).length > 0) { |
| localStorage.setItem('growthTrackerData', JSON.stringify(logs)); |
| } |
| }, [logs]); |
| |
| |
| 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')}`; |
| }; |
| |
| |
| 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); |
| }; |
| |
| |
| 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; |
| }; |
| |
| |
| const renderCalendarDays = () => { |
| const cells = []; |
| |
| |
| for (let i = 0; i < firstDay; i++) { |
| cells.push(<div key={`empty-${i}`} className="bg-gray-50 border border-gray-100 rounded-md"></div>); |
| } |
| |
| |
| 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> |