├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── habitTracker.js ├── index.html └── styles.css /.gitignore: -------------------------------------------------------------------------------- 1 | # System Files 2 | .DS_Store 3 | Thumbs.db 4 | 5 | # IDE Files 6 | .idea/ 7 | .vscode/ 8 | *.sublime-project 9 | *.sublime-workspace 10 | 11 | # Logs 12 | *.log 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Habit Tracker Wheel 2 | 3 | We love your input! We want to make contributing to Habit Tracker Wheel as easy and transparent as possible, whether it's: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Submitting a fix 8 | - Proposing new features 9 | - Becoming a maintainer 10 | 11 | ## We Develop with Github 12 | We use Github to host code, to track issues and feature requests, as well as accept pull requests. 13 | 14 | ## Pull Requests 15 | 1. Fork the repo and create your branch from `main`. 16 | 2. If you've added code that should be tested, add tests. 17 | 3. If you've changed APIs, update the documentation. 18 | 4. Ensure the test suite passes. 19 | 5. Make sure your code lints. 20 | 6. Issue that pull request! 21 | 22 | ## Any contributions you make will be under the MIT Software License 23 | In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern. 24 | 25 | ## Report bugs using Github's [issue tracker](https://github.com/yourusername/habit-tracker-wheel/issues) 26 | We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/yourusername/habit-tracker-wheel/issues/new). 27 | 28 | ## Write bug reports with detail, background, and sample code 29 | 30 | **Great Bug Reports** tend to have: 31 | 32 | - A quick summary and/or background 33 | - Steps to reproduce 34 | - Be specific! 35 | - Give sample code if you can. 36 | - What you expected would happen 37 | - What actually happens 38 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 39 | 40 | ## License 41 | By contributing, you agree that your contributions will be licensed under its MIT License. 42 | 43 | ## References 44 | This document was adapted from the open-source contribution guidelines for [Facebook's Draft](https://github.com/facebook/draft-js/blob/a9316a723f9e918afde44dea68b5f9f39b7d9b00/CONTRIBUTING.md). 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Anshul Mittal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Habit Tracker Wheel 🎯 2 | 3 | A visually appealing habit tracking application featuring a unique wheel-based interface to help you build and maintain daily, weekly, and monthly habits. 4 | 5 | ![Demo Screenshot - Add one later](https://github.com/user-attachments/assets/72b68130-0fb8-417a-8782-0541de1e6d61) 6 | 7 | ## Features ✨ 8 | 9 | - **Visual Habit Wheel**: Track daily habits with an intuitive circular interface 10 | - **Multiple Habit Types**: Support for daily, weekly, and monthly habits 11 | - **Progress Tracking**: Visual progress bars and completion percentages 12 | - **Data Management**: 13 | - Local storage for persistent data 14 | - Import/Export functionality via CSV 15 | - **Responsive Design**: Works on both desktop and mobile devices 16 | - **Monthly Navigation**: Easy switching between different months 17 | - **Color-coded Habits**: Visual distinction between different habits 18 | 19 | ## Getting Started 🌟 20 | 21 | 1. Clone the repository: 22 | ``` 23 | git clone https://github.com/anshulmittal712/habit-tracker.git 24 | ``` 25 | 26 | 27 | 2. Open `index.html` in your browser 28 | 29 | That's it! No build process or dependencies required. 30 | 31 | ## Usage 📝 32 | 33 | 1. **Select a Month**: Use the month picker at the top to select your tracking period 34 | 2. **Add Habits**: 35 | - Enter habit names in the respective sections (Daily/Weekly/Monthly) 36 | - Click the '+' button to add them 37 | 3. **Track Progress**: 38 | - Daily habits: Click segments in the wheel 39 | - Weekly habits: Click week boxes (W1-W5) 40 | - Monthly habits: Use the checkbox 41 | 4. **View Progress**: Check the summary section for completion rates 42 | 5. **Data Management**: 43 | - Export: Click 'Export CSV' to save your data 44 | - Import: Click 'Import CSV' to restore previous data 45 | 46 | ## Contributing 🤝 47 | 48 | Contributions are welcome! Please feel free to submit a Pull Request. 49 | 50 | 1. Fork the repository 51 | 2. Create your feature branch (`git checkout -b feature/AmazingFeature`) 52 | 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) 53 | 4. Push to the branch (`git push origin feature/AmazingFeature`) 54 | 5. Open a Pull Request 55 | 56 | ## License 📄 57 | 58 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 59 | 60 | ## Acknowledgments 🙏 61 | 62 | - Built with vanilla JavaScript, HTML, and CSS 63 | - Uses SVG for the wheel visualization 64 | -------------------------------------------------------------------------------- /habitTracker.js: -------------------------------------------------------------------------------- 1 | class HabitTracker { 2 | constructor() { 3 | this.state = { 4 | month: '', 5 | dailyHabits: [], 6 | weeklyHabits: [], 7 | monthlyHabits: [], 8 | dailyProgress: {}, // Format: {habitIndex: [day1, day2, ...]} 9 | weeklyProgress: {}, // Format: {habitIndex: [week1, week2, ...]} 10 | monthlyProgress: {} // Format: {habitIndex: boolean} 11 | }; 12 | 13 | this.daysInMonth = 31; // Default value 14 | this.habitColors = [ 15 | '#007bff', // blue 16 | '#28a745', // green 17 | '#dc3545', // red 18 | '#ffc107', // yellow 19 | '#17a2b8', // cyan 20 | '#6f42c1', // purple 21 | '#fd7e14', // orange 22 | ]; 23 | 24 | this.init(); 25 | this.loadFromLocalStorage(); 26 | } 27 | 28 | init() { 29 | // Initialize month input 30 | const monthInput = document.getElementById('monthInput'); 31 | monthInput.addEventListener('change', (e) => { 32 | this.state.month = e.target.value; 33 | // Update days in month when month changes 34 | const [year, month] = e.target.value.split('-'); 35 | this.daysInMonth = new Date(year, month, 0).getDate(); 36 | this.drawWheel(); 37 | this.updateSummary(); 38 | this.saveToLocalStorage(); 39 | }); 40 | 41 | // Initialize habit input handlers 42 | this.initializeHabitInputs(); 43 | 44 | // Initialize wheel 45 | this.drawWheel(); 46 | 47 | // Initialize import/export 48 | this.initializeImportExport(); 49 | 50 | // Initialize summary 51 | this.updateSummary(); 52 | } 53 | 54 | initializeHabitInputs() { 55 | // Add habit button handlers 56 | document.querySelectorAll('.add-habit').forEach(button => { 57 | button.addEventListener('click', (e) => { 58 | const container = e.target.closest('.habit-input').parentElement; 59 | const input = container.querySelector('input'); 60 | // Determine habit type based on parent container 61 | const habitType = container.closest('.daily-habits') ? 'daily' : 62 | container.closest('.weekly-habits') ? 'weekly' : 'monthly'; 63 | 64 | if (input.value.trim()) { 65 | this.addHabit(habitType, input.value.trim()); 66 | input.value = ''; 67 | } 68 | }); 69 | }); 70 | } 71 | 72 | addHabit(type, name) { 73 | const habitList = `${type}Habits`; 74 | this.state[habitList].push(name); 75 | this.renderHabits(); 76 | this.saveToLocalStorage(); 77 | } 78 | 79 | drawWheel() { 80 | const svg = document.getElementById('habitWheel'); 81 | svg.innerHTML = ''; 82 | 83 | // Calculate dimensions 84 | const centerX = 300; 85 | const centerY = 250; 86 | const radius = 160; 87 | const startAngleOffset = 0; // Start from top (-90 degrees) 88 | const endAngleOffset = 270; // End at bottom-left (180 degrees) 89 | 90 | // Create a group for each habit 91 | this.state.dailyHabits.forEach((habit, habitIndex) => { 92 | const habitGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g'); 93 | habitGroup.setAttribute('class', 'habit-ring'); 94 | habitGroup.setAttribute('data-habit', habitIndex); 95 | 96 | // Calculate radius for this habit ring 97 | const habitRadius = radius - (habitIndex * 30); 98 | 99 | // Get habit color 100 | const habitColor = this.habitColors[habitIndex % this.habitColors.length]; 101 | 102 | // Draw segments for each day 103 | for (let day = 1; day <= this.daysInMonth; day++) { 104 | const angleRange = endAngleOffset - startAngleOffset; 105 | const startAngle = startAngleOffset + ((day - 1) * (angleRange / this.daysInMonth)); 106 | const endAngle = startAngleOffset + (day * (angleRange / this.daysInMonth)); 107 | 108 | // Calculate path coordinates 109 | const path = this.describeArc(centerX, centerY, habitRadius, startAngle, endAngle); 110 | 111 | // Create segment 112 | const segment = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 113 | segment.setAttribute('d', path); 114 | segment.setAttribute('class', 'wheel-segment'); 115 | segment.setAttribute('data-day', day); 116 | 117 | segment.style.fill = this.state.dailyProgress[habitIndex]?.includes(day) 118 | ? habitColor 119 | : '#f0f0f0'; 120 | segment.style.stroke = habitColor; 121 | 122 | if (this.state.dailyProgress[habitIndex]?.includes(day)) { 123 | segment.classList.add('completed'); 124 | } 125 | 126 | segment.addEventListener('click', () => this.toggleDailyHabit(habitIndex, day)); 127 | habitGroup.appendChild(segment); 128 | } 129 | 130 | // Add habit label in the second quadrant (top-left) 131 | const labelX = 100; 132 | const labelY = 100 + (habitIndex * 30); 133 | 134 | // Add colored line connecting label to habit ring 135 | const connectingLine = document.createElementNS('http://www.w3.org/2000/svg', 'line'); 136 | connectingLine.setAttribute('x1', labelX); 137 | connectingLine.setAttribute('y1', labelY+5); 138 | connectingLine.setAttribute('x2', centerX); 139 | connectingLine.setAttribute('y2', labelY+5); 140 | connectingLine.setAttribute('stroke', habitColor); 141 | connectingLine.setAttribute('stroke-width', '1.5'); 142 | connectingLine.setAttribute('class', 'connecting-line'); 143 | habitGroup.appendChild(connectingLine); 144 | 145 | const habitLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); 146 | habitLabel.setAttribute('x', labelX); 147 | habitLabel.setAttribute('y', labelY); 148 | habitLabel.setAttribute('class', 'habit-wheel-label'); 149 | habitLabel.setAttribute('text-anchor', 'start'); 150 | habitLabel.style.fill = habitColor; 151 | habitLabel.textContent = habit; 152 | 153 | habitGroup.appendChild(habitLabel); 154 | 155 | svg.appendChild(habitGroup); 156 | }); 157 | 158 | // Add date numbers around the wheel 159 | for (let day = 1; day <= this.daysInMonth; day++) { 160 | const angleRange = endAngleOffset - startAngleOffset; 161 | const angle = startAngleOffset + ((day - 1) * (angleRange / this.daysInMonth)); 162 | const dateRadius = radius + 15; 163 | const position = this.polarToCartesian(centerX, centerY, dateRadius, angle); 164 | 165 | const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); 166 | text.setAttribute('x', position.x); 167 | text.setAttribute('y', position.y); 168 | text.setAttribute('text-anchor', 'middle'); 169 | text.setAttribute('alignment-baseline', 'middle'); 170 | text.setAttribute('class', 'date-label'); 171 | text.textContent = day; 172 | 173 | svg.appendChild(text); 174 | } 175 | } 176 | 177 | describeArc(x, y, radius, startAngle, endAngle) { 178 | const start = this.polarToCartesian(x, y, radius, endAngle); 179 | const end = this.polarToCartesian(x, y, radius, startAngle); 180 | const largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1"; 181 | 182 | return [ 183 | "M", start.x, start.y, 184 | "A", radius, radius, 0, largeArcFlag, 0, end.x, end.y, 185 | "L", x, y, 186 | "Z" 187 | ].join(" "); 188 | } 189 | 190 | polarToCartesian(centerX, centerY, radius, angleInDegrees) { 191 | const angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0; 192 | return { 193 | x: centerX + (radius * Math.cos(angleInRadians)), 194 | y: centerY + (radius * Math.sin(angleInRadians)) 195 | }; 196 | } 197 | 198 | saveToLocalStorage() { 199 | localStorage.setItem('habitTrackerState', JSON.stringify(this.state)); 200 | } 201 | 202 | loadFromLocalStorage() { 203 | const saved = localStorage.getItem('habitTrackerState'); 204 | if (saved) { 205 | this.state = JSON.parse(saved); 206 | this.renderHabits(); 207 | if (this.state.month) { 208 | document.getElementById('monthInput').value = this.state.month; 209 | } 210 | } 211 | } 212 | 213 | exportToCsv() { 214 | // Implementation for CSV export 215 | const csv = this.convertStateToCSV(); 216 | const blob = new Blob([csv], { type: 'text/csv' }); 217 | const url = window.URL.createObjectURL(blob); 218 | const a = document.createElement('a'); 219 | a.setAttribute('hidden', ''); 220 | a.setAttribute('href', url); 221 | a.setAttribute('download', `habit-tracker-${this.state.month || 'export'}.csv`); 222 | document.body.appendChild(a); 223 | a.click(); 224 | document.body.removeChild(a); 225 | } 226 | 227 | convertStateToCSV() { 228 | // Implementation for state to CSV conversion 229 | // This is a basic implementation - you might want to enhance it 230 | return JSON.stringify(this.state); 231 | } 232 | 233 | importFromCsv(file) { 234 | const reader = new FileReader(); 235 | reader.onload = (e) => { 236 | try { 237 | this.state = JSON.parse(e.target.result); 238 | this.saveToLocalStorage(); 239 | this.renderHabits(); 240 | if (this.state.month) { 241 | document.getElementById('monthInput').value = this.state.month; 242 | } 243 | } catch (error) { 244 | console.error('Error importing file:', error); 245 | alert('Invalid file format'); 246 | } 247 | }; 248 | reader.readAsText(file); 249 | } 250 | 251 | initializeImportExport() { 252 | document.getElementById('exportBtn').addEventListener('click', () => this.exportToCsv()); 253 | 254 | const importBtn = document.getElementById('importBtn'); 255 | const importInput = document.getElementById('importInput'); 256 | 257 | importBtn.addEventListener('click', () => importInput.click()); 258 | importInput.addEventListener('change', (e) => { 259 | if (e.target.files.length > 0) { 260 | this.importFromCsv(e.target.files[0]); 261 | } 262 | }); 263 | } 264 | 265 | renderHabits() { 266 | // Render daily habits 267 | this.renderHabitList('daily'); 268 | 269 | // Render weekly habits 270 | this.renderHabitList('weekly'); 271 | 272 | // Render monthly habits 273 | this.renderHabitList('monthly'); 274 | 275 | // Update summary 276 | this.updateSummary(); 277 | } 278 | 279 | renderHabitList(type) { 280 | // Skip rendering for daily habits as they're handled in the summary 281 | if (type === 'daily') { 282 | this.drawWheel(); 283 | return; 284 | } 285 | 286 | const container = document.getElementById(`${type}HabitsContainer`); 287 | const habitList = this.state[`${type}Habits`]; 288 | const progress = this.state[`${type}Progress`]; 289 | 290 | // Clear existing habits (except the input) 291 | const inputDiv = container.querySelector('.habit-input'); 292 | container.innerHTML = ''; 293 | container.appendChild(inputDiv); 294 | 295 | // Add each habit 296 | habitList.forEach((habit, index) => { 297 | const habitDiv = document.createElement('div'); 298 | habitDiv.className = 'habit-item'; 299 | 300 | // Add delete button 301 | const deleteBtn = document.createElement('button'); 302 | deleteBtn.className = 'delete-habit'; 303 | deleteBtn.innerHTML = '×'; 304 | deleteBtn.addEventListener('click', () => this.deleteHabit(type, index)); 305 | 306 | if (type === 'weekly') { 307 | // Create a span for the habit name and a container for week boxes 308 | habitDiv.innerHTML = ` 309 | 312 |
313 | ${Array(5).fill(0).map((_, i) => ` 314 |
316 | W${i + 1} 317 |
318 | `).join('')} 319 |
`; 320 | habitDiv.appendChild(deleteBtn); 321 | 322 | // Add click handlers for week boxes 323 | habitDiv.querySelectorAll('.week-box').forEach(box => { 324 | box.addEventListener('click', () => this.toggleWeeklyHabit(box)); 325 | }); 326 | } else { 327 | // For monthly habits, create a single checkbox 328 | habitDiv.innerHTML = ` 329 | `; 334 | habitDiv.appendChild(deleteBtn); 335 | } 336 | 337 | container.appendChild(habitDiv); 338 | }); 339 | 340 | // Add event listeners for monthly checkboxes 341 | if (type === 'monthly') { 342 | container.querySelectorAll('input[type="checkbox"]').forEach(checkbox => { 343 | checkbox.addEventListener('change', () => this.toggleMonthlyHabit(checkbox)); 344 | }); 345 | } 346 | } 347 | 348 | addHabitToWheel(habit, habitIndex) { 349 | // Redraw the entire wheel when a new habit is added 350 | this.drawWheel(); 351 | 352 | // Add the habit name to the list 353 | // const habitDiv = document.createElement('div'); 354 | // habitDiv.className = 'habit-item'; 355 | // habitDiv.innerHTML = `${habit}`; 356 | 357 | // const container = document.getElementById('dailyHabitsContainer'); 358 | // container.insertBefore(habitDiv, container.querySelector('.habit-input')); 359 | } 360 | 361 | toggleWeeklyHabit(box) { 362 | const habitIndex = parseInt(box.dataset.habit); 363 | const weekIndex = parseInt(box.dataset.week); 364 | 365 | if (!this.state.weeklyProgress[habitIndex]) { 366 | this.state.weeklyProgress[habitIndex] = []; 367 | } 368 | 369 | const progress = this.state.weeklyProgress[habitIndex]; 370 | const weekPos = progress.indexOf(weekIndex); 371 | 372 | if (weekPos === -1) { 373 | progress.push(weekIndex); 374 | } else { 375 | progress.splice(weekPos, 1); 376 | } 377 | 378 | box.classList.toggle('checked'); 379 | this.updateSummary(); 380 | this.saveToLocalStorage(); 381 | } 382 | 383 | toggleMonthlyHabit(checkbox) { 384 | const habitIndex = parseInt(checkbox.dataset.habit); 385 | this.state.monthlyProgress[habitIndex] = checkbox.checked; 386 | this.updateSummary(); 387 | this.saveToLocalStorage(); 388 | } 389 | 390 | toggleDailyHabit(habitIndex, day) { 391 | if (!this.state.dailyProgress[habitIndex]) { 392 | this.state.dailyProgress[habitIndex] = []; 393 | } 394 | 395 | const progress = this.state.dailyProgress[habitIndex]; 396 | const dayIndex = progress.indexOf(day); 397 | 398 | if (dayIndex === -1) { 399 | progress.push(day); 400 | } else { 401 | progress.splice(dayIndex, 1); 402 | } 403 | 404 | // Update the wheel 405 | this.drawWheel(); 406 | this.updateSummary(); 407 | this.saveToLocalStorage(); 408 | } 409 | 410 | deleteHabit(type, index) { 411 | // Remove the habit from the habits array 412 | this.state[`${type}Habits`].splice(index, 1); 413 | 414 | // Remove the corresponding progress data 415 | if (this.state[`${type}Progress`][index]) { 416 | delete this.state[`${type}Progress`][index]; 417 | } 418 | 419 | // Reindex the remaining progress data 420 | const newProgress = {}; 421 | Object.keys(this.state[`${type}Progress`]) 422 | .filter(i => i > index) 423 | .forEach(i => { 424 | newProgress[i - 1] = this.state[`${type}Progress`][i]; 425 | }); 426 | this.state[`${type}Progress`] = newProgress; 427 | 428 | // Save and re-render 429 | this.saveToLocalStorage(); 430 | this.renderHabits(); 431 | } 432 | 433 | updateSummary() { 434 | const summaryContainer = document.getElementById('summaryContainer'); 435 | summaryContainer.innerHTML = ''; 436 | 437 | // Daily habits summary only 438 | this.state.dailyHabits.forEach((habit, index) => { 439 | const progress = this.state.dailyProgress[index] || []; 440 | const percentage = (progress.length / this.daysInMonth) * 100; 441 | this.addSummaryItem( 442 | habit, 443 | percentage, 444 | this.habitColors[index % this.habitColors.length], 445 | `${progress.length}/${this.daysInMonth}`, 446 | 'daily', 447 | index 448 | ); 449 | }); 450 | } 451 | 452 | addSummaryItem(habit, percentage, color, count, type, index) { 453 | const summaryContainer = document.getElementById('summaryContainer'); 454 | const roundedPercentage = Math.round(percentage); 455 | 456 | const summaryItem = document.createElement('div'); 457 | summaryItem.className = 'summary-item'; 458 | 459 | // Add delete button for daily habits 460 | const deleteButton = type === 'daily' ? 461 | `` : ''; 462 | 463 | summaryItem.innerHTML = ` 464 | ${habit} 465 |
466 |
467 |
469 |
470 |
471 | ${count} (${roundedPercentage}%) 472 | ${deleteButton} 473 |
474 | `; 475 | 476 | // Add click handler for delete button if it's a daily habit 477 | if (type === 'daily') { 478 | summaryItem.querySelector('.delete-habit').addEventListener('click', () => { 479 | this.deleteHabit('daily', index); 480 | }); 481 | } 482 | 483 | summaryContainer.appendChild(summaryItem); 484 | } 485 | } 486 | 487 | // Initialize the application 488 | const habitTracker = new HabitTracker(); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Habit Tracker Wheel 7 | 8 | 9 | 10 |
11 |
12 | 13 |
14 | 15 | 16 | 17 |
18 |
19 | 20 |
21 |
22 |
23 |

Daily Habits

24 |
25 | 26 | 27 |
28 |
29 |
30 | 31 |
32 |
33 |
34 | 35 |
36 |

Weekly Habits

37 |
38 |
39 | 40 | 41 |
42 |
43 |
44 | 45 |
46 |

Monthly Habits

47 |
48 |
49 | 50 | 51 |
52 |
53 |
54 |
55 | 56 |
57 | 58 | 59 | 60 |
61 |
62 |
63 | 64 | 65 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | .container { 8 | max-width: 1200px; 9 | margin: 0 auto; 10 | padding: 20px; 11 | } 12 | 13 | header { 14 | display: flex; 15 | justify-content: space-between; 16 | align-items: center; 17 | margin-bottom: 40px; 18 | padding-bottom: 20px; 19 | border-bottom: 1px solid #e9ecef; 20 | } 21 | 22 | #monthInput { 23 | font-size: 1.5em; 24 | padding: 5px 10px; 25 | border: 2px solid #ccc; 26 | border-radius: 5px; 27 | } 28 | 29 | .file-controls { 30 | display: flex; 31 | gap: 10px; 32 | } 33 | 34 | button { 35 | padding: 8px 16px; 36 | border: none; 37 | border-radius: 5px; 38 | background-color: #007bff; 39 | color: white; 40 | cursor: pointer; 41 | } 42 | 43 | button:hover { 44 | background-color: #0056b3; 45 | } 46 | 47 | main { 48 | display: flex; 49 | gap: 40px; 50 | flex-direction: row-reverse; 51 | } 52 | 53 | .habits-panel { 54 | flex: 1; 55 | display: flex; 56 | flex-direction: column; 57 | gap: 30px; 58 | } 59 | 60 | .habit-wheel-container { 61 | flex: 1.2; 62 | position: sticky; 63 | top: 20px; 64 | height: fit-content; 65 | } 66 | 67 | #habitWheel { 68 | width: 100%; 69 | height: auto; 70 | } 71 | 72 | .habit-input { 73 | display: flex; 74 | gap: 10px; 75 | margin-bottom: 15px; 76 | } 77 | 78 | .habit-input input { 79 | flex: 1; 80 | padding: 8px 12px; 81 | border: 1px solid #ced4da; 82 | border-radius: 4px; 83 | font-size: 1rem; 84 | } 85 | 86 | .habit-input input:focus { 87 | outline: none; 88 | border-color: #007bff; 89 | box-shadow: 0 0 0 2px rgba(0,123,255,0.25); 90 | } 91 | 92 | .habit-checkbox { 93 | display: flex; 94 | align-items: center; 95 | gap: 10px; 96 | margin-bottom: 5px; 97 | } 98 | 99 | .week-boxes { 100 | display: flex; 101 | gap: 5px; 102 | } 103 | 104 | .week-box { 105 | width: 35px; 106 | height: 35px; 107 | border: 1px solid #ccc; 108 | cursor: pointer; 109 | position: relative; 110 | display: flex; 111 | align-items: center; 112 | justify-content: center; 113 | border-radius: 4px; 114 | transition: all 0.2s ease; 115 | background-color: white; 116 | } 117 | 118 | .week-box:hover { 119 | background-color: #e9ecef; 120 | } 121 | 122 | .week-box.checked { 123 | background-color: #007bff; 124 | color: white; 125 | } 126 | 127 | .week-box.checked .week-number { 128 | color: white; 129 | } 130 | 131 | .wheel-segment { 132 | cursor: pointer; 133 | fill: #f0f0f0; 134 | stroke: #ccc; 135 | stroke-width: 1; 136 | transition: fill 0.2s ease; 137 | opacity: 1; 138 | } 139 | 140 | .wheel-segment:hover { 141 | opacity: 1; 142 | fill: #f8f8f8; 143 | } 144 | 145 | .wheel-segment.completed { 146 | opacity: 1; 147 | fill-opacity: 1; 148 | } 149 | 150 | .habit-label { 151 | margin: 5px 0; 152 | padding: 5px; 153 | background-color: #f8f9fa; 154 | border-radius: 3px; 155 | } 156 | 157 | .habit-item { 158 | padding: 12px; 159 | background-color: white; 160 | border-radius: 5px; 161 | margin-bottom: 8px; 162 | display: flex; 163 | justify-content: space-between; 164 | align-items: center; 165 | gap: 10px; 166 | border: 1px solid #e9ecef; 167 | transition: box-shadow 0.2s ease; 168 | } 169 | 170 | .habit-item:hover { 171 | box-shadow: 0 2px 4px rgba(0,0,0,0.05); 172 | } 173 | 174 | .habit-checkbox { 175 | flex: 1; 176 | } 177 | 178 | .date-label { 179 | font-size: 12px; 180 | fill: #666; 181 | pointer-events: none; 182 | font-family: Arial, sans-serif; 183 | } 184 | 185 | .week-number { 186 | font-size: 14px; 187 | color: #495057; 188 | pointer-events: none; 189 | } 190 | 191 | .delete-habit { 192 | background-color: transparent; 193 | color: #dc3545; 194 | padding: 0 8px; 195 | font-size: 20px; 196 | line-height: 1; 197 | border: none; 198 | cursor: pointer; 199 | transition: color 0.2s ease; 200 | } 201 | 202 | .delete-habit:hover { 203 | background-color: transparent; 204 | color: #bd2130; 205 | } 206 | 207 | .habit-wheel-label { 208 | font-size: 14px; 209 | font-weight: 500; 210 | font-family: Arial, sans-serif; 211 | } 212 | 213 | .habit-legend-item { 214 | display: flex; 215 | align-items: center; 216 | gap: 8px; 217 | margin: 5px 0; 218 | } 219 | 220 | .legend-color { 221 | width: 12px; 222 | height: 12px; 223 | border-radius: 50%; 224 | } 225 | 226 | .legend-text { 227 | font-size: 14px; 228 | color: #333; 229 | } 230 | 231 | .connecting-line { 232 | opacity: 0.7; 233 | } 234 | 235 | .habit-ring { 236 | isolation: isolate; 237 | } 238 | 239 | .monthly-summary { 240 | padding: 15px; 241 | background-color: #f8f9fa; 242 | border-radius: 8px; 243 | box-shadow: 0 2px 4px rgba(0,0,0,0.05); 244 | } 245 | 246 | .monthly-summary h3 { 247 | margin-bottom: 10px; 248 | color: #333; 249 | } 250 | 251 | .summary-item { 252 | display: flex; 253 | align-items: center; 254 | margin-bottom: 8px; 255 | padding: 8px; 256 | background-color: white; 257 | border-radius: 4px; 258 | border: 1px solid #e9ecef; 259 | } 260 | 261 | .summary-label { 262 | flex: 1; 263 | font-weight: 500; 264 | } 265 | 266 | .summary-progress { 267 | display: flex; 268 | align-items: center; 269 | gap: 10px; 270 | margin-right: 10px; 271 | } 272 | 273 | .progress-bar { 274 | width: 120px; 275 | height: 8px; 276 | background-color: #e9ecef; 277 | border-radius: 4px; 278 | overflow: hidden; 279 | } 280 | 281 | .progress-fill { 282 | height: 100%; 283 | transition: width 0.3s ease; 284 | } 285 | 286 | .progress-text { 287 | min-width: 80px; 288 | text-align: right; 289 | font-size: 14px; 290 | color: #666; 291 | } 292 | 293 | .summary-item .delete-habit { 294 | padding: 0 8px; 295 | font-size: 18px; 296 | color: #dc3545; 297 | background: transparent; 298 | border: none; 299 | cursor: pointer; 300 | transition: color 0.2s ease; 301 | } 302 | 303 | .summary-item .delete-habit:hover { 304 | color: #bd2130; 305 | } 306 | 307 | .add-habit { 308 | padding: 8px 20px; 309 | font-size: 1.2rem; 310 | line-height: 1; 311 | } 312 | 313 | h2 { 314 | margin-bottom: 15px; 315 | color: #2c3e50; 316 | font-size: 1.5rem; 317 | } --------------------------------------------------------------------------------