Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Streakly - Beautiful Habit Tracker</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 900px; | |
| margin: 0 auto; | |
| background: rgba(255, 255, 255, 0.95); | |
| backdrop-filter: blur(20px); | |
| border-radius: 24px; | |
| padding: 32px; | |
| box-shadow: 0 32px 64px rgba(0, 0, 0, 0.1); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| } | |
| .header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: flex-start; | |
| margin-bottom: 32px; | |
| } | |
| .greeting h1 { | |
| font-size: 28px; | |
| font-weight: 700; | |
| color: #1a202c; | |
| margin-bottom: 4px; | |
| } | |
| .greeting p { | |
| color: #718096; | |
| font-size: 16px; | |
| } | |
| .date-section { | |
| text-align: right; | |
| } | |
| .current-date { | |
| font-size: 18px; | |
| font-weight: 600; | |
| color: #2d3748; | |
| margin-bottom: 4px; | |
| } | |
| .progress-text { | |
| color: #718096; | |
| font-size: 14px; | |
| } | |
| .progress-card { | |
| background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%); | |
| border-radius: 16px; | |
| padding: 24px; | |
| margin-bottom: 32px; | |
| text-align: center; | |
| border: 1px solid rgba(255, 255, 255, 0.5); | |
| } | |
| .progress-title { | |
| font-size: 20px; | |
| font-weight: 600; | |
| color: #2d3748; | |
| margin-bottom: 12px; | |
| } | |
| .progress-bar-container { | |
| background: rgba(255, 255, 255, 0.8); | |
| height: 12px; | |
| border-radius: 6px; | |
| overflow: hidden; | |
| margin-bottom: 8px; | |
| } | |
| .progress-bar { | |
| height: 100%; | |
| background: linear-gradient(90deg, #48bb78, #38a169); | |
| border-radius: 6px; | |
| transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1); | |
| box-shadow: 0 2px 4px rgba(72, 187, 120, 0.3); | |
| } | |
| .add-habit-section { | |
| display: flex; | |
| gap: 12px; | |
| margin-bottom: 32px; | |
| } | |
| .habit-input { | |
| flex: 1; | |
| padding: 16px 20px; | |
| border: 2px solid #e2e8f0; | |
| border-radius: 12px; | |
| font-size: 16px; | |
| font-weight: 500; | |
| background: rgba(255, 255, 255, 0.9); | |
| transition: all 0.3s ease; | |
| outline: none; | |
| } | |
| .habit-input:focus { | |
| border-color: #4299e1; | |
| box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.1); | |
| transform: translateY(-1px); | |
| } | |
| .add-btn { | |
| padding: 16px 24px; | |
| background: linear-gradient(135deg, #4299e1, #3182ce); | |
| color: white; | |
| border: none; | |
| border-radius: 12px; | |
| font-size: 16px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 4px 12px rgba(66, 153, 225, 0.3); | |
| } | |
| .add-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 24px rgba(66, 153, 225, 0.4); | |
| } | |
| .habits-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); | |
| gap: 20px; | |
| margin-bottom: 40px; | |
| } | |
| .habit-card { | |
| background: rgba(255, 255, 255, 0.9); | |
| border: 2px solid #e2e8f0; | |
| border-radius: 16px; | |
| padding: 24px; | |
| cursor: pointer; | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .habit-card::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 4px; | |
| background: linear-gradient(90deg, #e2e8f0, #cbd5e0); | |
| transition: background 0.3s ease; | |
| } | |
| .habit-card:hover { | |
| transform: translateY(-4px); | |
| box-shadow: 0 12px 32px rgba(0, 0, 0, 0.1); | |
| border-color: #4299e1; | |
| } | |
| .habit-card.completed { | |
| background: linear-gradient(135deg, #48bb78, #38a169); | |
| color: white; | |
| border-color: #48bb78; | |
| } | |
| .habit-card.completed::before { | |
| background: linear-gradient(90deg, rgba(255,255,255,0.3), rgba(255,255,255,0.1)); | |
| } | |
| .habit-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 12px; | |
| } | |
| .habit-name { | |
| font-size: 20px; | |
| font-weight: 600; | |
| } | |
| .habit-icon { | |
| font-size: 24px; | |
| opacity: 0.8; | |
| } | |
| .habit-status { | |
| font-size: 14px; | |
| opacity: 0.9; | |
| font-weight: 500; | |
| } | |
| .delete-btn { | |
| position: absolute; | |
| top: 12px; | |
| right: 12px; | |
| background: rgba(255, 255, 255, 0.2); | |
| border: none; | |
| border-radius: 8px; | |
| width: 32px; | |
| height: 32px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| opacity: 0; | |
| transition: all 0.3s ease; | |
| color: #e53e3e; | |
| font-size: 16px; | |
| } | |
| .habit-card:hover .delete-btn { | |
| opacity: 1; | |
| } | |
| .delete-btn:hover { | |
| background: rgba(229, 62, 62, 0.1); | |
| transform: scale(1.1); | |
| } | |
| .week-section { | |
| background: rgba(247, 250, 252, 0.8); | |
| border-radius: 20px; | |
| padding: 28px; | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255, 255, 255, 0.3); | |
| } | |
| .week-header { | |
| font-size: 22px; | |
| font-weight: 700; | |
| color: #2d3748; | |
| margin-bottom: 24px; | |
| text-align: center; | |
| } | |
| .week-grid { | |
| display: grid; | |
| grid-template-columns: 120px repeat(7, 1fr); | |
| gap: 12px; | |
| align-items: center; | |
| } | |
| .day-header { | |
| text-align: center; | |
| font-size: 14px; | |
| font-weight: 600; | |
| color: #718096; | |
| padding: 12px 4px; | |
| } | |
| .habit-label { | |
| font-size: 16px; | |
| font-weight: 500; | |
| color: #4a5568; | |
| padding: 12px 0; | |
| border-right: 2px solid #e2e8f0; | |
| text-align: right; | |
| padding-right: 16px; | |
| } | |
| .day-cell { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 12px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 14px; | |
| font-weight: 600; | |
| margin: 0 auto; | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| cursor: pointer; | |
| } | |
| .day-cell:hover { | |
| transform: scale(1.1); | |
| } | |
| .day-empty { | |
| background: rgba(226, 232, 240, 0.6); | |
| color: #a0aec0; | |
| border: 2px solid transparent; | |
| } | |
| .day-completed { | |
| background: linear-gradient(135deg, #48bb78, #38a169); | |
| color: white; | |
| box-shadow: 0 4px 12px rgba(72, 187, 120, 0.3); | |
| border: 2px solid transparent; | |
| } | |
| .day-today { | |
| border: 2px solid #4299e1; | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { box-shadow: 0 0 0 0 rgba(66, 153, 225, 0.4); } | |
| 70% { box-shadow: 0 0 0 8px rgba(66, 153, 225, 0); } | |
| 100% { box-shadow: 0 0 0 0 rgba(66, 153, 225, 0); } | |
| } | |
| .streak-info { | |
| text-align: center; | |
| margin-top: 24px; | |
| padding: 20px; | |
| background: rgba(255, 255, 255, 0.5); | |
| border-radius: 12px; | |
| border: 1px solid rgba(255, 255, 255, 0.3); | |
| } | |
| .streak-number { | |
| font-size: 32px; | |
| font-weight: 700; | |
| color: #48bb78; | |
| margin-bottom: 4px; | |
| } | |
| .streak-label { | |
| color: #718096; | |
| font-size: 14px; | |
| font-weight: 500; | |
| } | |
| .empty-state { | |
| text-align: center; | |
| padding: 60px 20px; | |
| color: #718096; | |
| } | |
| .empty-state h3 { | |
| font-size: 24px; | |
| font-weight: 600; | |
| margin-bottom: 12px; | |
| color: #4a5568; | |
| } | |
| .floating-add { | |
| position: fixed; | |
| bottom: 32px; | |
| right: 32px; | |
| width: 64px; | |
| height: 64px; | |
| background: linear-gradient(135deg, #4299e1, #3182ce); | |
| border: none; | |
| border-radius: 50%; | |
| color: white; | |
| font-size: 24px; | |
| cursor: pointer; | |
| box-shadow: 0 8px 24px rgba(66, 153, 225, 0.4); | |
| transition: all 0.3s ease; | |
| display: none; | |
| } | |
| .floating-add:hover { | |
| transform: scale(1.1); | |
| box-shadow: 0 12px 32px rgba(66, 153, 225, 0.5); | |
| } | |
| @media (max-width: 768px) { | |
| .container { | |
| margin: 10px; | |
| padding: 24px; | |
| } | |
| .header { | |
| flex-direction: column; | |
| gap: 16px; | |
| text-align: center; | |
| } | |
| .habits-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| .week-grid { | |
| grid-template-columns: 80px repeat(7, 1fr); | |
| gap: 8px; | |
| } | |
| .day-cell { | |
| width: 32px; | |
| height: 32px; | |
| font-size: 12px; | |
| } | |
| .floating-add { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .add-habit-section { | |
| display: none; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <div class="greeting"> | |
| <h1>Hey there! 👋</h1> | |
| <p>Keep building those amazing habits</p> | |
| </div> | |
| <div class="date-section"> | |
| <div class="current-date" id="currentDate"></div> | |
| <div class="progress-text" id="progressText">0% of daily goals achieved</div> | |
| </div> | |
| </div> | |
| <div class="progress-card"> | |
| <div class="progress-title" id="progressTitle">Today's Progress: 0/0 habits completed</div> | |
| <div class="progress-bar-container"> | |
| <div class="progress-bar" id="progressBar" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <div class="add-habit-section"> | |
| <input type="text" class="habit-input" id="habitInput" placeholder="Add a new habit..." maxlength="50"> | |
| <button class="add-btn" onclick="addHabit()">+ Add Habit</button> | |
| </div> | |
| <div class="habits-grid" id="habitsGrid"></div> | |
| <div class="week-section"> | |
| <div class="week-header" id="weekHeader">This Week</div> | |
| <div class="week-grid" id="weekGrid"></div> | |
| <div class="streak-info"> | |
| <div class="streak-number" id="streakNumber">0</div> | |
| <div class="streak-label">day current streak</div> | |
| </div> | |
| </div> | |
| </div> | |
| <button class="floating-add" onclick="showAddHabitPrompt()">+</button> | |
| <script> | |
| // Sample data - in a real app, this would be stored in localStorage or a database | |
| let habits = [ | |
| { id: 1, name: 'Exercise', icon: '💪' }, | |
| { id: 2, name: 'Read', icon: '📚' }, | |
| { id: 3, name: 'Meditate', icon: '🧘' }, | |
| { id: 4, name: 'Drink Water', icon: '💧' } | |
| ]; | |
| let completedHabits = {}; // Format: { 'YYYY-MM-DD': [habitId1, habitId2, ...] } | |
| // Initialize with some sample data | |
| const today = new Date(); | |
| const yesterday = new Date(today); | |
| yesterday.setDate(yesterday.getDate() - 1); | |
| completedHabits[formatDate(today)] = [1, 3]; // Exercise and Meditate completed today | |
| completedHabits[formatDate(yesterday)] = [1, 2, 4]; // Exercise, Read, Water completed yesterday | |
| function formatDate(date) { | |
| return date.toISOString().split('T')[0]; | |
| } | |
| function formatDateForDisplay(date) { | |
| return date.toLocaleDateString('en-US', { | |
| weekday: 'short', | |
| month: 'short', | |
| day: 'numeric' | |
| }); | |
| } | |
| function updateHeader() { | |
| const now = new Date(); | |
| document.getElementById('currentDate').textContent = formatDateForDisplay(now); | |
| const todayCompleted = getTodayCompletedCount(); | |
| const totalHabits = habits.length; | |
| const percentage = totalHabits > 0 ? Math.round((todayCompleted / totalHabits) * 100) : 0; | |
| document.getElementById('progressText').textContent = `${percentage}% of daily goals achieved`; | |
| document.getElementById('progressTitle').textContent = `Today's Progress: ${todayCompleted}/${totalHabits} habits completed`; | |
| document.getElementById('progressBar').style.width = `${percentage}%`; | |
| } | |
| function getTodayCompletedCount() { | |
| const today = formatDate(new Date()); | |
| return completedHabits[today] ? completedHabits[today].length : 0; | |
| } | |
| function isHabitCompletedToday(habitId) { | |
| const today = formatDate(new Date()); | |
| return completedHabits[today] && completedHabits[today].includes(habitId); | |
| } | |
| function toggleHabit(habitId) { | |
| const today = formatDate(new Date()); | |
| if (!completedHabits[today]) { | |
| completedHabits[today] = []; | |
| } | |
| const index = completedHabits[today].indexOf(habitId); | |
| if (index > -1) { | |
| completedHabits[today].splice(index, 1); | |
| if (completedHabits[today].length === 0) { | |
| delete completedHabits[today]; | |
| } | |
| } else { | |
| completedHabits[today].push(habitId); | |
| } | |
| renderHabits(); | |
| renderWeekView(); | |
| updateHeader(); | |
| updateStreak(); | |
| } | |
| function addHabit() { | |
| const input = document.getElementById('habitInput'); | |
| const habitName = input.value.trim(); | |
| if (habitName && habitName.length > 0) { | |
| const newId = Math.max(...habits.map(h => h.id), 0) + 1; | |
| const icons = ['🎯', '⭐', '🔥', '✨', '🌟', '💎', '🎊', '🏆', '⚡', '🌈']; | |
| const randomIcon = icons[Math.floor(Math.random() * icons.length)]; | |
| habits.push({ | |
| id: newId, | |
| name: habitName, | |
| icon: randomIcon | |
| }); | |
| input.value = ''; | |
| renderHabits(); | |
| updateHeader(); | |
| renderWeekView(); | |
| } | |
| } | |
| function deleteHabit(habitId) { | |
| habits = habits.filter(h => h.id !== habitId); | |
| // Remove from completed habits | |
| Object.keys(completedHabits).forEach(date => { | |
| completedHabits[date] = completedHabits[date].filter(id => id !== habitId); | |
| if (completedHabits[date].length === 0) { | |
| delete completedHabits[date]; | |
| } | |
| }); | |
| renderHabits(); | |
| updateHeader(); | |
| renderWeekView(); | |
| } | |
| function renderHabits() { | |
| const grid = document.getElementById('habitsGrid'); | |
| if (habits.length === 0) { | |
| grid.innerHTML = ` | |
| <div class="empty-state" style="grid-column: 1 / -1;"> | |
| <h3>No habits yet! 🌱</h3> | |
| <p>Add your first habit to start building amazing streaks</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| grid.innerHTML = habits.map(habit => { | |
| const isCompleted = isHabitCompletedToday(habit.id); | |
| return ` | |
| <div class="habit-card ${isCompleted ? 'completed' : ''}" onclick="toggleHabit(${habit.id})"> | |
| <button class="delete-btn" onclick="event.stopPropagation(); deleteHabit(${habit.id})" title="Delete habit">×</button> | |
| <div class="habit-header"> | |
| <div class="habit-name">${habit.name}</div> | |
| <div class="habit-icon">${habit.icon}</div> | |
| </div> | |
| <div class="habit-status">${isCompleted ? '✓ Completed today!' : 'Click to mark complete'}</div> | |
| </div> | |
| `; | |
| }).join(''); | |
| } | |
| function renderWeekView() { | |
| const weekGrid = document.getElementById('weekGrid'); | |
| const today = new Date(); | |
| const startOfWeek = new Date(today); | |
| startOfWeek.setDate(today.getDate() - today.getDay() + 1); // Monday | |
| const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; | |
| const weekDates = []; | |
| for (let i = 0; i < 7; i++) { | |
| const date = new Date(startOfWeek); | |
| date.setDate(startOfWeek.getDate() + i); | |
| weekDates.push(date); | |
| } | |
| // Update week header | |
| document.getElementById('weekHeader').textContent = | |
| `This Week - ${startOfWeek.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} to ${weekDates[6].toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}`; | |
| let gridHTML = '<div></div>'; // Empty corner cell | |
| // Day headers | |
| days.forEach((day, index) => { | |
| gridHTML += `<div class="day-header">${day}<br><span style="font-size: 12px; opacity: 0.7;">${weekDates[index].getDate()}</span></div>`; | |
| }); | |
| // Habit rows | |
| habits.forEach(habit => { | |
| gridHTML += `<div class="habit-label">${habit.name}</div>`; | |
| weekDates.forEach(date => { | |
| const dateStr = formatDate(date); | |
| const isCompleted = completedHabits[dateStr] && completedHabits[dateStr].includes(habit.id); | |
| const isToday = formatDate(date) === formatDate(today); | |
| let classes = 'day-cell '; | |
| classes += isCompleted ? 'day-completed' : 'day-empty'; | |
| classes += isToday ? ' day-today' : ''; | |
| gridHTML += `<div class="${classes}" onclick="toggleHabitForDate(${habit.id}, '${dateStr}')">${isCompleted ? '●' : '○'}</div>`; | |
| }); | |
| }); | |
| weekGrid.innerHTML = gridHTML; | |
| } | |
| function toggleHabitForDate(habitId, dateStr) { | |
| if (!completedHabits[dateStr]) { | |
| completedHabits[dateStr] = []; | |
| } | |
| const index = completedHabits[dateStr].indexOf(habitId); | |
| if (index > -1) { | |
| completedHabits[dateStr].splice(index, 1); | |
| if (completedHabits[dateStr].length === 0) { | |
| delete completedHabits[dateStr]; | |
| } | |
| } else { | |
| completedHabits[dateStr].push(habitId); | |
| } | |
| renderWeekView(); | |
| if (dateStr === formatDate(new Date())) { | |
| renderHabits(); | |
| updateHeader(); | |
| } | |
| updateStreak(); | |
| } | |
| function updateStreak() { | |
| // Calculate current streak (simplified - just counts consecutive days with any habit completed) | |
| let streak = 0; | |
| const today = new Date(); | |
| for (let i = 0; i < 30; i++) { | |
| const date = new Date(today); | |
| date.setDate(today.getDate() - i); | |
| const dateStr = formatDate(date); | |
| if (completedHabits[dateStr] && completedHabits[dateStr].length > 0) { | |
| streak++; | |
| } else { | |
| break; | |
| } | |
| } | |
| document.getElementById('streakNumber').textContent = streak; | |
| } | |
| function showAddHabitPrompt() { | |
| const habitName = prompt('Enter new habit name:'); | |
| if (habitName && habitName.trim()) { | |
| document.getElementById('habitInput').value = habitName.trim(); | |
| addHabit(); | |
| } | |
| } | |
| // Handle Enter key in input | |
| document.getElementById('habitInput').addEventListener('keypress', function(e) { | |
| if (e.key === 'Enter') { | |
| addHabit(); | |
| } | |
| }); | |
| // Initialize app | |
| updateHeader(); | |
| renderHabits(); | |
| renderWeekView(); | |
| updateStreak(); | |
| </script> | |
| </body> | |
| </html> |