feat: US-013 - Frontend - Stats section and weekly summary
This commit is contained in:
@@ -675,6 +675,134 @@
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-muted);
|
||||
}
|
||||
|
||||
/* Stats section */
|
||||
.stats-section {
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: var(--space-3);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Weekly summary */
|
||||
.weekly-summary {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.weekly-summary-header {
|
||||
padding: var(--space-3) var(--space-4);
|
||||
background: var(--bg-muted);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: background var(--transition-base);
|
||||
}
|
||||
|
||||
.weekly-summary-header:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.weekly-summary-title {
|
||||
font-size: var(--text-base);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.weekly-summary-chevron {
|
||||
transition: transform var(--transition-base);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.weekly-summary-chevron.expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.weekly-summary-content {
|
||||
display: none;
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.weekly-summary-content.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.weekly-chart {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-2);
|
||||
height: 150px;
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.weekly-bar-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.weekly-bar {
|
||||
width: 100%;
|
||||
background: var(--accent);
|
||||
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
|
||||
transition: all var(--transition-base);
|
||||
min-height: 4px;
|
||||
}
|
||||
|
||||
.weekly-bar:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.weekly-day-label {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.weekly-stats {
|
||||
display: flex;
|
||||
gap: var(--space-4);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-row {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -754,6 +882,42 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Section -->
|
||||
<div id="statsSection" class="stats-section" style="display: none;">
|
||||
<div class="stats-row">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Total Habits</div>
|
||||
<div class="stat-value" id="statTotalHabits">0</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Avg Completion (30d)</div>
|
||||
<div class="stat-value" id="statAvgCompletion">0%</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Best Streak 🏆</div>
|
||||
<div class="stat-value" id="statBestStreak">0</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Total Lives</div>
|
||||
<div class="stat-value" id="statTotalLives">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="weekly-summary">
|
||||
<div class="weekly-summary-header" onclick="toggleWeeklySummary()">
|
||||
<div class="weekly-summary-title">Weekly Summary</div>
|
||||
<i data-lucide="chevron-down" class="weekly-summary-chevron" id="weeklySummaryChevron"></i>
|
||||
</div>
|
||||
<div class="weekly-summary-content" id="weeklySummaryContent">
|
||||
<div class="weekly-chart" id="weeklyChart"></div>
|
||||
<div class="weekly-stats" id="weeklyStats">
|
||||
<span id="weeklyCompletedText">0 completed this week</span>
|
||||
<span id="weeklySkippedText">0 skipped this week</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="habitsContainer">
|
||||
<div class="empty-state">
|
||||
<i data-lucide="loader"></i>
|
||||
@@ -1031,6 +1195,9 @@
|
||||
function renderHabits() {
|
||||
const container = document.getElementById('habitsContainer');
|
||||
|
||||
// Render stats section
|
||||
renderStats();
|
||||
|
||||
if (habits.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
@@ -1199,6 +1366,114 @@
|
||||
return 'low';
|
||||
}
|
||||
|
||||
// Stats calculation and rendering
|
||||
function renderStats() {
|
||||
const statsSection = document.getElementById('statsSection');
|
||||
|
||||
if (habits.length === 0) {
|
||||
statsSection.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
statsSection.style.display = 'block';
|
||||
|
||||
// Calculate stats
|
||||
const totalHabits = habits.length;
|
||||
|
||||
// Average completion rate (30d) across all habits
|
||||
const avgCompletion = habits.length > 0
|
||||
? Math.round(habits.reduce((sum, h) => sum + (h.completion_rate_30d || 0), 0) / habits.length)
|
||||
: 0;
|
||||
|
||||
// Best streak across all habits
|
||||
const bestStreak = Math.max(...habits.map(h => h.streak?.best || 0), 0);
|
||||
|
||||
// Total lives available
|
||||
const totalLives = habits.reduce((sum, h) => sum + (h.lives || 0), 0);
|
||||
|
||||
// Update DOM
|
||||
document.getElementById('statTotalHabits').textContent = totalHabits;
|
||||
document.getElementById('statAvgCompletion').textContent = `${avgCompletion}%`;
|
||||
document.getElementById('statBestStreak').textContent = bestStreak;
|
||||
document.getElementById('statTotalLives').textContent = totalLives;
|
||||
|
||||
// Render weekly summary
|
||||
renderWeeklySummary();
|
||||
}
|
||||
|
||||
function renderWeeklySummary() {
|
||||
const chartContainer = document.getElementById('weeklyChart');
|
||||
|
||||
// Get current week's data (Mon-Sun)
|
||||
const today = new Date();
|
||||
const currentDayOfWeek = today.getDay(); // 0 = Sunday, 1 = Monday, etc.
|
||||
const mondayOffset = currentDayOfWeek === 0 ? -6 : 1 - currentDayOfWeek;
|
||||
const monday = new Date(today);
|
||||
monday.setDate(today.getDate() + mondayOffset);
|
||||
monday.setHours(0, 0, 0, 0);
|
||||
|
||||
// Calculate completions per day (Mon-Sun)
|
||||
const daysOfWeek = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
const completionsPerDay = new Array(7).fill(0);
|
||||
let weeklyCompleted = 0;
|
||||
let weeklySkipped = 0;
|
||||
|
||||
habits.forEach(habit => {
|
||||
(habit.completions || []).forEach(completion => {
|
||||
const compDate = new Date(completion.date);
|
||||
compDate.setHours(0, 0, 0, 0);
|
||||
|
||||
// Check if completion is in current week
|
||||
const daysDiff = Math.floor((compDate - monday) / (1000 * 60 * 60 * 24));
|
||||
if (daysDiff >= 0 && daysDiff < 7) {
|
||||
if (completion.type === 'check') {
|
||||
completionsPerDay[daysDiff]++;
|
||||
weeklyCompleted++;
|
||||
} else if (completion.type === 'skip') {
|
||||
weeklySkipped++;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Find max for scaling bars
|
||||
const maxCompletions = Math.max(...completionsPerDay, 1);
|
||||
|
||||
// Render bars
|
||||
let barsHtml = '';
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const count = completionsPerDay[i];
|
||||
const height = (count / maxCompletions) * 100;
|
||||
barsHtml += `
|
||||
<div class="weekly-bar-wrapper">
|
||||
<div class="weekly-bar" style="height: ${height}%" title="${count} completed"></div>
|
||||
<div class="weekly-day-label">${daysOfWeek[i]}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
chartContainer.innerHTML = barsHtml;
|
||||
|
||||
// Update weekly stats text
|
||||
document.getElementById('weeklyCompletedText').textContent = `${weeklyCompleted} completed this week`;
|
||||
document.getElementById('weeklySkippedText').textContent = `${weeklySkipped} skipped this week`;
|
||||
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
function toggleWeeklySummary() {
|
||||
const content = document.getElementById('weeklySummaryContent');
|
||||
const chevron = document.getElementById('weeklySummaryChevron');
|
||||
|
||||
if (content.classList.contains('visible')) {
|
||||
content.classList.remove('visible');
|
||||
chevron.classList.remove('expanded');
|
||||
} else {
|
||||
content.classList.add('visible');
|
||||
chevron.classList.add('expanded');
|
||||
}
|
||||
}
|
||||
|
||||
// Modal state
|
||||
let selectedColor = '#3B82F6';
|
||||
let selectedIcon = 'dumbbell';
|
||||
|
||||
Reference in New Issue
Block a user