diff --git a/HabitLoopVisualizer/README.md b/HabitLoopVisualizer/README.md new file mode 100644 index 000000000..489053046 --- /dev/null +++ b/HabitLoopVisualizer/README.md @@ -0,0 +1,41 @@ +# Habit Loop Visualizer + +A simple JavaScript mini project that visualizes the **Habit Loop** cycle: +**Cue β†’ Routine β†’ Reward** + +This project demonstrates how habits are formed and maintained using an interactive visualization. + +--- + +## πŸš€ Features +- Interactive UI to show the habit loop stages +- Visual transitions between **Cue β†’ Routine β†’ Reward** +- Clean design using HTML, CSS, and JavaScript +- Easy to extend for additional customization + +--- + +## πŸ“‚ Project Structure +HabitLoopVisualizer/ +│── index.html # Main HTML file +│── style.css # Styling for the visualizer +│── script.js # JavaScript logic +│── README.md # Project documentation + + + +## πŸ› οΈ How to Run +1. Clone or download this repository. +2. Navigate to the `HabitLoopVisualizer` folder. +3. Open `index.html` in your browser. + +That’s it β€” no additional setup required! πŸŽ‰ + +--- + +## 🎯 Preview +Here’s how the Habit Loop Visualizer looks in action: +![preview](image.png) + +## 🀝 Contribution +Feel free to fork this project and enhance it β€” add animations, more loops, or a dashboard for tracking multiple habits. \ No newline at end of file diff --git a/HabitLoopVisualizer/image.png b/HabitLoopVisualizer/image.png new file mode 100644 index 000000000..fb7ca8d24 Binary files /dev/null and b/HabitLoopVisualizer/image.png differ diff --git a/HabitLoopVisualizer/index.html b/HabitLoopVisualizer/index.html new file mode 100644 index 000000000..987260360 --- /dev/null +++ b/HabitLoopVisualizer/index.html @@ -0,0 +1,119 @@ + + + + + + Cue β†’ Routine β†’ Reward App + + + +
+

✨ Cue β†’ Routine β†’ Reward ✨

+ + +
+ + + +
+ + +
+ +
+ + +
+ + +
+ + + + + +
+ + +
+ + +
+ + +
+

+

πŸ”₯ Streak: 0 days

+
+
+ + +
+

Consistency Calendar

+
+ +

January 2023

+ +
+
+
+
+ + Completed +
+
+ + Partial +
+
+ + Missed +
+
+
+ + +
+

Your Habit Statistics

+ +
+
+

Most Common Cues

+
+
+ +
+

Most Completed Routines

+
+
+ +
+

Average Routine Time

+
0 min
+
+ +
+

Completion Rate

+
0%
+
+
+ +
+

Recent Activities

+
+
+
+
+ + + + \ No newline at end of file diff --git a/HabitLoopVisualizer/script.js b/HabitLoopVisualizer/script.js new file mode 100644 index 000000000..1a7d01cb4 --- /dev/null +++ b/HabitLoopVisualizer/script.js @@ -0,0 +1,447 @@ +// Routine options for each cue +const routines = { + sad: ["Go for a gym workout", "Take a walk in the park", "Do a 10-min meditation"], + stressed: ["Try deep breathing", "Write down thoughts", "Listen to relaxing music"], + excited: ["Dance to your favorite song", "Plan something creative", "Share joy with a friend"], + default: ["Take a deep breath", "Smile and stretch", "Drink water"] +}; + +// Motivational lines +const motivationLines = [ + "🌟 You're stronger than you think!", + "πŸ’ͺ Keep going, amazing things await!", + "πŸ”₯ One small step today builds a better tomorrow.", + "🌈 You're creating a better version of yourself!" +]; + +// Rewards (with emojis) +const rewards = [ + "🍫 Enjoy a sweet treat!", + "🍽️ Have a delicious meal!", + "β˜• Relax with your favorite drink!", + "🎢 Play your favorite song!", + "πŸ›‹οΈ Take a cozy rest!" +]; + +// Quotes +const quotes = [ + "Every day is a second chance.", + "Push yourself, no one else will do it for you.", + "Small progress is still progress.", + "Believe you can and you're halfway there." +]; + +// Initialize variables +let streak = 0; +let timerInterval = null; +let timerSeconds = 0; +let currentRoutine = null; +let userData = { + activities: [], + completionStats: {}, + cueStats: {}, + routineTimes: [] +}; + +// Initialize the app +document.addEventListener('DOMContentLoaded', function() { + loadUserData(); + initializeTabs(); + initializeCalendar(); + updateStats(); + + // Handle cue input + document.getElementById("cueBtn").addEventListener("click", handleCueInput); + + // Timer controls + document.getElementById("startTimer").addEventListener("click", startTimer); + document.getElementById("pauseTimer").addEventListener("click", pauseTimer); + document.getElementById("resetTimer").addEventListener("click", resetTimer); + document.getElementById("completeRoutine").addEventListener("click", completeRoutine); + + // Calendar navigation + document.getElementById("prevMonth").addEventListener("click", showPreviousMonth); + document.getElementById("nextMonth").addEventListener("click", showNextMonth); +}); + +// Tab functionality +function initializeTabs() { + const tabBtns = document.querySelectorAll('.tab-btn'); + const tabContents = document.querySelectorAll('.tab-content'); + + tabBtns.forEach(btn => { + btn.addEventListener('click', () => { + // Remove active class from all buttons and contents + tabBtns.forEach(b => b.classList.remove('active')); + tabContents.forEach(c => c.classList.remove('active')); + + // Add active class to clicked button and corresponding content + btn.classList.add('active'); + const tabId = btn.getAttribute('data-tab'); + document.getElementById(`${tabId}-tab`).classList.add('active'); + + // Update stats and calendar when switching to those tabs + if (tabId === 'stats') updateStats(); + if (tabId === 'calendar') renderCalendar(currentMonth, currentYear); + }); + }); +} + +// Handle cue input +function handleCueInput() { + const cue = document.getElementById("cueInput").value.toLowerCase(); + const routineSection = document.getElementById("routineSection"); + routineSection.innerHTML = ""; + + const availableRoutines = routines[cue] || routines.default; + + availableRoutines.forEach(routine => { + const btn = document.createElement("button"); + btn.textContent = routine; + btn.classList.add("routine-option"); + btn.addEventListener("click", () => selectRoutine(routine, cue)); + routineSection.appendChild(btn); + }); + + document.getElementById("motivation").textContent = ""; + document.getElementById("reward").style.display = "none"; + document.getElementById("checklist").innerHTML = ""; + document.getElementById("timerSection").style.display = "none"; +} + +// When user selects a routine +function selectRoutine(routine, cue) { + currentRoutine = routine; + const routineSection = document.getElementById("routineSection"); + routineSection.innerHTML = `

${routine}

`; + + // Show timer section + document.getElementById("timerSection").style.display = "block"; + resetTimer(); + + // Motivation + const motivation = motivationLines[Math.floor(Math.random() * motivationLines.length)]; + document.getElementById("motivation").textContent = motivation; + + // Checklist + document.getElementById("checklist").innerHTML = ` +

βœ… To-Do Checklist

+
+
+
+ `; + + // Track cue usage + if (userData.cueStats[cue]) { + userData.cueStats[cue]++; + } else { + userData.cueStats[cue] = 1; + } + + saveUserData(); +} + +// Timer functions +function startTimer() { + if (timerInterval) return; + + document.getElementById("startTimer").disabled = true; + document.getElementById("pauseTimer").disabled = false; + document.getElementById("resetTimer").disabled = false; + + timerInterval = setInterval(() => { + timerSeconds++; + updateTimerDisplay(); + }, 1000); +} + +function pauseTimer() { + clearInterval(timerInterval); + timerInterval = null; + + document.getElementById("startTimer").disabled = false; + document.getElementById("pauseTimer").disabled = true; +} + +function resetTimer() { + pauseTimer(); + timerSeconds = 0; + updateTimerDisplay(); + + document.getElementById("startTimer").disabled = false; + document.getElementById("pauseTimer").disabled = true; + document.getElementById("resetTimer").disabled = true; +} + +function updateTimerDisplay() { + const minutes = Math.floor(timerSeconds / 60); + const seconds = timerSeconds % 60; + document.getElementById("timerDisplay").textContent = + `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; +} + +function completeRoutine() { + pauseTimer(); + + // Record activity + const activity = { + routine: currentRoutine, + date: new Date(), + duration: timerSeconds + }; + + userData.activities.push(activity); + + // Track routine completion + if (userData.completionStats[currentRoutine]) { + userData.completionStats[currentRoutine]++; + } else { + userData.completionStats[currentRoutine] = 1; + } + + // Track routine time + userData.routineTimes.push(timerSeconds); + + // Update streak + updateStreak(); + + // Reward with emojis + animation + const reward = rewards[Math.floor(Math.random() * rewards.length)]; + const rewardDiv = document.getElementById("reward"); + rewardDiv.textContent = reward; + rewardDiv.style.display = "block"; + + // Update quote + document.getElementById("quote").textContent = quotes[Math.floor(Math.random() * quotes.length)]; + + saveUserData(); +} + +function updateStreak() { + // Simple streak implementation - in a real app, you'd check consecutive days + streak++; + document.getElementById("streak").textContent = streak; +} + +// Calendar functionality +let currentDate = new Date(); +let currentMonth = currentDate.getMonth(); +let currentYear = currentDate.getFullYear(); + +function initializeCalendar() { + renderCalendar(currentMonth, currentYear); +} + +function renderCalendar(month, year) { + const calendarEl = document.getElementById("calendar"); + const monthNames = ["January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December"]; + + document.getElementById("currentMonth").textContent = `${monthNames[month]} ${year}`; + + // Clear previous calendar + calendarEl.innerHTML = ''; + + // Create day headers + const dayHeaders = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + dayHeaders.forEach(day => { + const dayEl = document.createElement("div"); + dayEl.textContent = day; + dayEl.style.fontWeight = "bold"; + dayEl.style.textAlign = "center"; + calendarEl.appendChild(dayEl); + }); + + // Get first day of month and number of days + const firstDay = new Date(year, month, 1).getDay(); + const daysInMonth = new Date(year, month + 1, 0).getDate(); + + // Add empty days before first day of month + for (let i = 0; i < firstDay; i++) { + const emptyDay = document.createElement("div"); + emptyDay.classList.add("calendar-day", "empty"); + calendarEl.appendChild(emptyDay); + } + + // Add days of the month + for (let day = 1; day <= daysInMonth; day++) { + const dayEl = document.createElement("div"); + dayEl.classList.add("calendar-day"); + dayEl.textContent = day; + + // Check if this day has any activities (simplified for demo) + const hasActivity = Math.random() > 0.7; // Random for demo + if (hasActivity) { + const completion = Math.random(); // Random for demo + if (completion > 0.7) { + dayEl.classList.add("completed"); + } else if (completion > 0.4) { + dayEl.classList.add("partial"); + } else { + dayEl.classList.add("missed"); + } + } + + calendarEl.appendChild(dayEl); + } +} + +function showPreviousMonth() { + currentMonth--; + if (currentMonth < 0) { + currentMonth = 11; + currentYear--; + } + renderCalendar(currentMonth, currentYear); +} + +function showNextMonth() { + currentMonth++; + if (currentMonth > 11) { + currentMonth = 0; + currentYear++; + } + renderCalendar(currentMonth, currentYear); +} + +// Statistics functionality +function updateStats() { + updateCuesChart(); + updateRoutinesChart(); + updateAverageTime(); + updateCompletionRate(); + updateRecentActivities(); +} + +function updateCuesChart() { + const chartContainer = document.getElementById("cuesChart"); + chartContainer.innerHTML = ''; + + // Get top cues + const topCues = Object.entries(userData.cueStats) + .sort((a, b) => b[1] - a[1]) + .slice(0, 5); + + if (topCues.length === 0) { + chartContainer.innerHTML = '

No data yet

'; + return; + } + + const maxValue = Math.max(...topCues.map(item => item[1])); + + topCues.forEach(([cue, count]) => { + const barHeight = (count / maxValue) * 100; + const bar = document.createElement("div"); + bar.classList.add("bar"); + bar.style.height = `${barHeight}%`; + bar.title = `${cue}: ${count} times`; + + const label = document.createElement("div"); + label.classList.add("bar-label"); + label.textContent = cue.substring(0, 3); + + chartContainer.appendChild(bar); + chartContainer.appendChild(label); + }); +} + +function updateRoutinesChart() { + const chartContainer = document.getElementById("routinesChart"); + chartContainer.innerHTML = ''; + + // Get top routines + const topRoutines = Object.entries(userData.completionStats) + .sort((a, b) => b[1] - a[1]) + .slice(0, 5); + + if (topRoutines.length === 0) { + chartContainer.innerHTML = '

No data yet

'; + return; + } + + const maxValue = Math.max(...topRoutines.map(item => item[1])); + + topRoutines.forEach(([routine, count]) => { + const barHeight = (count / maxValue) * 100; + const bar = document.createElement("div"); + bar.classList.add("bar"); + bar.style.height = `${barHeight}%`; + bar.title = `${routine}: ${count} times`; + + const label = document.createElement("div"); + label.classList.add("bar-label"); + label.textContent = routine.split(' ')[0].substring(0, 3); + + chartContainer.appendChild(bar); + chartContainer.appendChild(label); + }); +} + +function updateAverageTime() { + if (userData.routineTimes.length === 0) { + document.getElementById("avgTime").textContent = "0 min"; + return; + } + + const totalSeconds = userData.routineTimes.reduce((sum, time) => sum + time, 0); + const avgSeconds = totalSeconds / userData.routineTimes.length; + const avgMinutes = Math.round(avgSeconds / 60); + + document.getElementById("avgTime").textContent = `${avgMinutes} min`; +} + +function updateCompletionRate() { + // Simplified calculation for demo + const totalActivities = userData.activities.length; + const completed = userData.activities.filter(a => a.duration > 60).length; + const rate = totalActivities > 0 ? Math.round((completed / totalActivities) * 100) : 0; + + document.getElementById("completionRate").textContent = `${rate}%`; +} + +function updateRecentActivities() { + const activitiesList = document.getElementById("recentActivities"); + activitiesList.innerHTML = ''; + + const recentActivities = userData.activities.slice(-5).reverse(); + + if (recentActivities.length === 0) { + activitiesList.innerHTML = '

No recent activities

'; + return; + } + + recentActivities.forEach(activity => { + const activityItem = document.createElement("div"); + activityItem.classList.add("activity-item"); + + const minutes = Math.floor(activity.duration / 60); + const seconds = activity.duration % 60; + + activityItem.innerHTML = ` + ${activity.routine} + ${minutes}m ${seconds}s + `; + + activitiesList.appendChild(activityItem); + }); +} + +// Data persistence +function saveUserData() { + localStorage.setItem('habitLoopUserData', JSON.stringify(userData)); + localStorage.setItem('habitLoopStreak', streak.toString()); +} + +function loadUserData() { + const savedData = localStorage.getItem('habitLoopUserData'); + const savedStreak = localStorage.getItem('habitLoopStreak'); + + if (savedData) { + userData = JSON.parse(savedData); + } + + if (savedStreak) { + streak = parseInt(savedStreak); + document.getElementById("streak").textContent = streak; + } +} \ No newline at end of file diff --git a/HabitLoopVisualizer/style.css b/HabitLoopVisualizer/style.css new file mode 100644 index 000000000..d94446a6a --- /dev/null +++ b/HabitLoopVisualizer/style.css @@ -0,0 +1,421 @@ +:root { + --bg-primary: #121212; + --bg-secondary: #1e1e1e; + --accent: #bb86fc; + --accent-secondary: #03dac6; + --text-primary: #e0e0e0; + --text-secondary: #a0a0a0; + --card-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); + --completed: #4caf50; + --partial: #ff9800; + --missed: #f44336; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; + transition: all 0.3s ease; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: var(--bg-primary); + color: var(--text-primary); + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + padding: 20px; +} + +.app-container { + max-width: 600px; + width: 100%; + background: var(--bg-secondary); + border-radius: 15px; + padding: 25px; + box-shadow: var(--card-shadow); + border: 1px solid #333; +} + +h1 { + text-align: center; + margin-bottom: 25px; + color: var(--accent); + font-size: 1.8rem; + text-shadow: 0 0 10px rgba(187, 134, 252, 0.3); +} + +/* Tabs */ +.tabs { + display: flex; + margin-bottom: 20px; + border-bottom: 1px solid #333; +} + +.tab-btn { + padding: 10px 20px; + background: none; + border: none; + color: var(--text-secondary); + cursor: pointer; + font-weight: bold; +} + +.tab-btn.active { + color: var(--accent); + border-bottom: 2px solid var(--accent); +} + +.tab-content { + display: none; +} + +.tab-content.active { + display: block; +} + +/* Cue Section */ +.cue-section { + display: flex; + margin-bottom: 20px; +} + +#cueInput { + flex: 1; + padding: 12px 15px; + border-radius: 8px 0 0 8px; + border: 1px solid #333; + background: #2a2a2a; + color: var(--text-primary); + outline: none; +} + +#cueInput:focus { + border-color: var(--accent); + box-shadow: 0 0 0 2px rgba(187, 134, 252, 0.2); +} + +#cueBtn { + padding: 12px 20px; + border-radius: 0 8px 8px 0; + border: none; + background: var(--accent); + color: #000; + font-weight: bold; + cursor: pointer; + outline: none; +} + +#cueBtn:hover { + background: #c5a2ff; +} + +/* Routine Section */ +.routine-section { + margin-bottom: 20px; + min-height: 60px; +} + +.routine-option { + display: block; + width: 100%; + padding: 12px; + margin: 8px 0; + background: #2a2a2a; + color: var(--text-primary); + border: 1px solid #333; + border-radius: 8px; + cursor: pointer; + text-align: left; +} + +.routine-option:hover { + background: #333; + border-color: var(--accent-secondary); + transform: translateX(5px); +} + +/* Timer Section */ +.timer-section { + background: #2a2a2a; + padding: 15px; + border-radius: 8px; + margin-bottom: 20px; + border: 1px solid #333; + text-align: center; +} + +.timer-display { + font-size: 2.5rem; + font-weight: bold; + margin: 15px 0; + color: var(--accent); +} + +.timer-controls { + display: flex; + justify-content: center; + gap: 10px; +} + +.timer-controls button { + padding: 8px 15px; + border: none; + border-radius: 5px; + background: #333; + color: var(--text-primary); + cursor: pointer; +} + +.timer-controls button:hover:not(:disabled) { + background: var(--accent); + color: #000; +} + +.timer-controls button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Motivation */ +.motivation { + padding: 15px; + background: linear-gradient(45deg, #2a2a2a, #1e1e1e); + border-left: 4px solid var(--accent); + border-radius: 8px; + margin-bottom: 20px; + font-style: italic; + color: var(--accent-secondary); +} + +/* Reward */ +.reward { + font-size: 1.4rem; + text-align: center; + padding: 20px; + background: #2a2a2a; + border-radius: 8px; + margin-bottom: 20px; + display: none; + animation: pop 0.6s ease; + color: var(--accent); + border: 1px solid #333; +} + +@keyframes pop { + 0% { transform: scale(0.5); opacity: 0; } + 100% { transform: scale(1); opacity: 1; } +} + +/* Checklist */ +.checklist { + background: #2a2a2a; + padding: 15px; + border-radius: 8px; + margin-bottom: 20px; + border: 1px solid #333; +} + +.checklist h4 { + margin-bottom: 10px; + color: var(--accent-secondary); +} + +.checklist label { + display: block; + padding: 8px 0; + cursor: pointer; +} + +.checklist input[type="checkbox"] { + margin-right: 10px; + accent-color: var(--accent); +} + +/* Extras */ +.extras { + text-align: center; + padding: 15px; + background: #2a2a2a; + border-radius: 8px; + border: 1px solid #333; +} + +#quote { + font-style: italic; + margin-bottom: 15px; + color: var(--text-secondary); +} + +#streak { + color: var(--accent); + font-weight: bold; +} + +/* Calendar */ +.calendar-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; +} + +.calendar-header button { + background: var(--accent); + color: #000; + border: none; + border-radius: 5px; + padding: 5px 10px; + cursor: pointer; +} + +.calendar { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 5px; + margin-bottom: 20px; +} + +.calendar-day { + aspect-ratio: 1; + display: flex; + align-items: center; + justify-content: center; + border-radius: 5px; + background: #2a2a2a; + font-size: 0.9rem; +} + +.calendar-day.empty { + background: transparent; +} + +.calendar-day.completed { + background: var(--completed); + color: #000; +} + +.calendar-day.partial { + background: var(--partial); + color: #000; +} + +.calendar-day.missed { + background: var(--missed); + color: #000; +} + +.calendar-legend { + display: flex; + justify-content: center; + gap: 15px; +} + +.legend-item { + display: flex; + align-items: center; + gap: 5px; +} + +.legend-color { + width: 15px; + height: 15px; + border-radius: 3px; +} + +.legend-color.completed { + background: var(--completed); +} + +.legend-color.partial { + background: var(--partial); +} + +.legend-color.missed { + background: var(--missed); +} + +/* Statistics */ +.stats-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 15px; + margin-bottom: 20px; +} + +.stat-card { + background: #2a2a2a; + padding: 15px; + border-radius: 8px; + border: 1px solid #333; +} + +.stat-card h3 { + margin-bottom: 10px; + color: var(--accent-secondary); +} + +.chart-container { + height: 150px; + display: flex; + align-items: flex-end; + gap: 5px; + padding: 10px 0; +} + +.bar { + background: var(--accent); + border-radius: 3px 3px 0 0; + position: relative; + flex: 1; + min-width: 20px; +} + +.bar-label { + position: absolute; + bottom: -20px; + left: 0; + right: 0; + text-align: center; + font-size: 0.7rem; + color: var(--text-secondary); +} + +.big-number { + font-size: 2.5rem; + text-align: center; + color: var(--accent); + font-weight: bold; +} + +.activities-list { + background: #2a2a2a; + border-radius: 8px; + border: 1px solid #333; + padding: 10px; + max-height: 200px; + overflow-y: auto; +} + +.activity-item { + padding: 8px; + border-bottom: 1px solid #333; + display: flex; + justify-content: space-between; +} + +.activity-item:last-child { + border-bottom: none; +} + +.activity-routine { + font-weight: bold; + color: var(--accent); +} + +.activity-time { + color: var(--text-secondary); + font-size: 0.9rem; +} \ No newline at end of file