diff --git a/docs/examples/ProgressTrackerExample.mdx b/docs/examples/ProgressTrackerExample.mdx new file mode 100644 index 000000000..763a1bcc6 --- /dev/null +++ b/docs/examples/ProgressTrackerExample.mdx @@ -0,0 +1,208 @@ +--- +id: progress-tracker-example +title: Learning Progress Tracker +description: Track learning progress through tutorial series +--- + +import ProgressTracker from '@site/src/components/ProgressTracker'; + +# Learning Progress Tracker + +The Progress Tracker component helps learners monitor their progress through a series of tutorials or lessons. It provides visual feedback with progress percentages, estimated completion time, and persistent progress storage. + +## Features + +✨ **Visual Progress Display**: Circular progress indicator with percentage + +📊 **Statistics**: Completed lessons, remaining lessons, and estimated time + +💾 **Persistent Storage**: Progress is saved to browser localStorage + +✅ **Lesson Management**: Toggle completion status for each lesson + +⏱️ **Time Estimation**: Automatically calculates remaining study time + +🎉 **Achievement Celebrations**: Special message when all lessons are complete + +📱 **Responsive Design**: Works on mobile and desktop + +## Basic Usage + +```jsx +import ProgressTracker from '@site/src/components/ProgressTracker'; + +const lessons = [ + { id: 'lesson-1', title: 'Getting Started' }, + { id: 'lesson-2', title: 'Core Concepts' }, + { id: 'lesson-3', title: 'Advanced Topics' }, +]; + +export default function Course() { + return ; +} +``` + +## With Durations and Descriptions + +```jsx +const lessons = [ + { + id: 'intro', + title: 'Course Introduction', + duration: 5, + description: 'Learn what you\'ll accomplish in this course' + }, + { + id: 'setup', + title: 'Environment Setup', + duration: 15, + description: 'Install and configure necessary tools' + }, + { + id: 'basics', + title: 'Fundamentals', + duration: 30, + description: 'Understand core concepts and principles' + }, +]; + +export default function JavaScriptCourse() { + return ( + + ); +} +``` + +## Example Progress Tracker + + + +## Props + +| Prop | Type | Description | +|------|------|-------------| +| `seriesId` | string | Unique ID for the course (used for localStorage key) | +| `lessons` | array | Array of lesson objects | +| `onProgress` | function | Callback when progress changes | + +## Lesson Object Structure + +```javascript +{ + id: string, // Unique lesson identifier (required) + title: string, // Lesson title (required) + duration: number, // Duration in minutes (optional) + description: string // Lesson description (optional) +} +``` + +## Features + +### Progress Visualization +- Circular progress indicator showing completion percentage +- Progress bar showing visual completion status +- Statistics: completed/total lessons and estimated remaining time + +### Lesson Management +- Click checkboxes to mark lessons complete/incomplete +- "Complete All" button to quickly finish remaining lessons +- "Reset" button to clear all progress (with confirmation) +- Collapsible lesson list for clean interface + +### Persistent Storage +- Progress automatically saved to localStorage +- Each course has separate storage using `seriesId` +- Progress persists across browser sessions + +### User Feedback +- Completion count and percentage display +- Time estimate based on lesson duration +- Celebration message when course is complete +- Encouragement messages based on progress level + +## Time Estimation + +Time estimates are calculated based on lesson duration values: +- Each lesson's individual duration (in minutes) is considered +- If duration is not provided, defaults to 0.5 hours per lesson +- Total estimated time = sum of uncompleted lesson durations + +## Callbacks + +```jsx +function handleProgress(completedLessons) { + console.log('Progress updated:', completedLessons); + // Optionally sync progress to backend +} + + +``` + +## Customization + +The component uses CSS variables for theming. Override these in your custom CSS: + +```css +:root { + --ifm-color-primary: #2e93e2; + --ifm-color-success: #22c55e; + --ifm-color-info: #3b82f6; +} +``` + +## Accessibility + +- ARIA labels on all interactive elements +- Semantic HTML structure +- Keyboard navigation support +- Clear visual indicators for completed/incomplete lessons +- Collapsible sections with proper `aria-expanded` attributes + +## Tips + +1. Use consistent lesson IDs - they're used for localStorage keys +2. Include duration estimates for accurate time calculations +3. Add descriptions to help learners understand lesson content +4. Use the `seriesId` to track different courses separately +5. Reset progress only when explicitly requested by the user + +## Storage Management + +Progress is stored in localStorage with key: `tutorial_progress_{seriesId}` + +To manually clear progress: +```javascript +localStorage.removeItem('tutorial_progress_my-course'); +``` + +To export progress: +```javascript +const progress = localStorage.getItem('tutorial_progress_my-course'); +console.log(JSON.parse(progress)); +``` + +## Browser Compatibility + +Works on all modern browsers supporting: +- localStorage API +- CSS Grid and Flexbox +- ES6 JavaScript + +Supported: Chrome 60+, Firefox 55+, Safari 11+, Edge 79+ diff --git a/src/components/ProgressTracker.jsx b/src/components/ProgressTracker.jsx new file mode 100644 index 000000000..8bb67ac20 --- /dev/null +++ b/src/components/ProgressTracker.jsx @@ -0,0 +1,206 @@ +import React, { useState, useEffect } from 'react'; +import styles from './ProgressTracker.module.css'; + +export default function ProgressTracker({ seriesId = 'default', lessons = [], onProgress = null }) { + const [completed, setCompleted] = useState({}); + const [showDetails, setShowDetails] = useState(false); + const [lastUpdated, setLastUpdated] = useState(null); + + const storageKey = `tutorial_progress_${seriesId}`; + + useEffect(() => { + const saved = localStorage.getItem(storageKey); + if (saved) { + try { + setCompleted(JSON.parse(saved)); + } catch (e) { + console.error('Failed to load progress:', e); + } + } + }, [storageKey]); + + const toggleLesson = (lessonId) => { + const updated = { + ...completed, + [lessonId]: !completed[lessonId] + }; + setCompleted(updated); + localStorage.setItem(storageKey, JSON.stringify(updated)); + setLastUpdated(new Date().toLocaleTimeString()); + if (onProgress) onProgress(updated); + }; + + const markAllComplete = () => { + const updated = {}; + lessons.forEach(lesson => { + updated[lesson.id] = true; + }); + setCompleted(updated); + localStorage.setItem(storageKey, JSON.stringify(updated)); + setLastUpdated(new Date().toLocaleTimeString()); + }; + + const resetProgress = () => { + if (window.confirm('Are you sure you want to reset your progress?')) { + setCompleted({}); + localStorage.removeItem(storageKey); + setLastUpdated(new Date().toLocaleTimeString()); + } + }; + + const completedCount = Object.values(completed).filter(Boolean).length; + const totalCount = lessons.length; + const percentage = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0; + const remainingLessons = totalCount - completedCount; + const estimatedHours = (remainingLessons * 0.5).toFixed(1); + + return ( +
+
+
+

Learning Progress

+ {lastUpdated && ( + + Updated: {lastUpdated} + + )} +
+
+ +
+
+ + + + +
{percentage}%
+
+ +
+
+
Completed
+
{completedCount} of {totalCount}
+
+
+
Remaining
+
{remainingLessons}
+
+
+
Est. Time
+
{estimatedHours}h
+
+
+
+ + {percentage === 100 && ( +
+ 🎉 Congratulations! You've completed all lessons! +
+ )} + +
+
+
+ +
+ + + +
+ + {showDetails && ( +
+
+ + {completedCount} of {totalCount} lessons completed + +
+
+ {lessons.length === 0 ? ( +
+ No lessons added yet. Create lessons with unique IDs to track progress. +
+ ) : ( + lessons.map((lesson) => { + const isComplete = completed[lesson.id]; + return ( +
+ +
+
{lesson.title}
+ {lesson.duration && ( +
+ {lesson.duration} min +
+ )} + {lesson.description && ( +
+ {lesson.description} +
+ )} +
+ {isComplete && ( +
+ Completed +
+ )} +
+ ); + }) + )} +
+
+ )} + + {percentage > 0 && percentage < 100 && ( +
+ {percentage < 25 && '🚀 Great start! Keep going!'} + {percentage >= 25 && percentage < 50 && '⚡ You\'re making progress!'} + {percentage >= 50 && percentage < 75 && '💪 More than halfway there!'} + {percentage >= 75 && '🎯 Almost done! Don\'t stop now!'} +
+ )} +
+ ); +} diff --git a/src/components/ProgressTracker.module.css b/src/components/ProgressTracker.module.css new file mode 100644 index 000000000..40774dd90 --- /dev/null +++ b/src/components/ProgressTracker.module.css @@ -0,0 +1,368 @@ +.tracker { + background: var(--ifm-background-color); + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 8px; + padding: 24px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.header { + margin-bottom: 24px; +} + +.titleSection { + display: flex; + justify-content: space-between; + align-items: center; +} + +.title { + margin: 0; + font-size: 20px; + font-weight: 600; + color: var(--ifm-heading-color); +} + +.lastUpdated { + font-size: 12px; + color: var(--ifm-text-color-secondary); +} + +.statsContainer { + display: flex; + gap: 24px; + align-items: center; + margin-bottom: 24px; + padding-bottom: 24px; + border-bottom: 1px solid var(--ifm-color-emphasis-300); +} + +.progressCircle { + position: relative; + width: 120px; + height: 120px; + flex-shrink: 0; +} + +.svg { + width: 100%; + height: 100%; + transform: rotate(-90deg); +} + +.bgCircle { + fill: none; + stroke: var(--ifm-color-emphasis-200); + stroke-width: 4; +} + +.progressCircle { + fill: none; + stroke: var(--ifm-color-primary); + stroke-width: 4; + stroke-linecap: round; +} + +.percentageText { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 28px; + font-weight: 700; + color: var(--ifm-heading-color); +} + +.stats { + flex: 1; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); + gap: 16px; +} + +.statItem { + text-align: center; +} + +.statLabel { + font-size: 12px; + color: var(--ifm-text-color-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 4px; +} + +.statValue { + font-size: 20px; + font-weight: 600; + color: var(--ifm-heading-color); +} + +.celebration { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 16px; + border-radius: 6px; + text-align: center; + font-size: 16px; + font-weight: 600; + margin-bottom: 20px; + animation: slideIn 0.4s ease; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.progressBar { + width: 100%; + height: 8px; + background: var(--ifm-color-emphasis-200); + border-radius: 4px; + overflow: hidden; + margin-bottom: 20px; +} + +.progressFill { + height: 100%; + background: linear-gradient(90deg, var(--ifm-color-primary), #29d5b0); + border-radius: 4px; + transition: width 0.3s ease; +} + +.actions { + display: flex; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 20px; +} + +.toggleBtn { + padding: 10px 16px; + border: 1px solid var(--ifm-color-primary); + background: transparent; + color: var(--ifm-color-primary); + border-radius: 6px; + cursor: pointer; + font-weight: 500; + font-size: 14px; + font-family: inherit; + transition: all 0.2s ease; +} + +.toggleBtn:hover { + background: var(--ifm-color-primary); + color: white; +} + +.actionBtn { + padding: 10px 16px; + border: none; + background: var(--ifm-color-primary); + color: white; + border-radius: 6px; + cursor: pointer; + font-weight: 500; + font-size: 14px; + font-family: inherit; + transition: all 0.2s ease; +} + +.actionBtn:hover:not(:disabled) { + opacity: 0.9; + transform: translateY(-2px); +} + +.actionBtn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.lessonsList { + margin-top: 20px; + border-top: 1px solid var(--ifm-color-emphasis-300); + padding-top: 20px; + animation: slideIn 0.3s ease; +} + +.lessonsHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + padding: 0 12px; +} + +.lessonCount { + font-size: 14px; + color: var(--ifm-text-color-secondary); + font-weight: 500; +} + +.lessons { + display: flex; + flex-direction: column; + gap: 12px; +} + +.emptyMessage { + text-align: center; + color: var(--ifm-text-color-secondary); + padding: 20px; + font-style: italic; +} + +.lessonItem { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 12px; + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 6px; + background: transparent; + transition: all 0.2s ease; +} + +.lessonItem:hover { + background: var(--ifm-color-emphasis-50); + border-color: var(--ifm-color-primary); +} + +.lessonItem.completed { + background: var(--ifm-color-success-light); + border-color: var(--ifm-color-success); +} + +.checkbox { + flex-shrink: 0; + width: 24px; + height: 24px; + border: 2px solid var(--ifm-color-emphasis-300); + border-radius: 4px; + background: transparent; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + transition: all 0.2s ease; + font-family: inherit; +} + +.lessonItem:hover .checkbox { + border-color: var(--ifm-color-primary); +} + +.lessonItem.completed .checkbox { + background: var(--ifm-color-success); + border-color: var(--ifm-color-success); +} + +.checkmark { + color: white; + font-weight: bold; +} + +.lessonContent { + flex: 1; + min-width: 0; +} + +.lessonTitle { + font-weight: 600; + color: var(--ifm-heading-color); + margin-bottom: 4px; + font-size: 15px; +} + +.lessonItem.completed .lessonTitle { + text-decoration: line-through; + opacity: 0.7; +} + +.lessonMeta { + font-size: 12px; + color: var(--ifm-text-color-secondary); + margin-bottom: 6px; +} + +.lessonDescription { + font-size: 13px; + color: var(--ifm-text-color-secondary); + line-height: 1.4; + margin-top: 6px; +} + +.completedBadge { + flex-shrink: 0; + background: var(--ifm-color-success); + color: white; + padding: 4px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.encouragement { + text-align: center; + padding: 12px; + background: var(--ifm-color-info-light); + color: var(--ifm-color-info-dark); + border-radius: 6px; + font-size: 14px; + font-weight: 500; + margin-top: 20px; +} + +@media (max-width: 768px) { + .tracker { + padding: 16px; + } + + .statsContainer { + flex-direction: column; + gap: 16px; + } + + .stats { + width: 100%; + } + + .actions { + flex-direction: column; + } + + .toggleBtn, + .actionBtn { + width: 100%; + } + + .lessonItem { + flex-direction: column; + } + + .completedBadge { + align-self: flex-start; + } +} + +@media (prefers-reduced-motion: no-preference) { + .lessonItem, + .progressFill, + .actionBtn { + transition: all 0.2s ease; + } + + .celebration { + animation: slideIn 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); + } +}