# Statistics Frontend Tasks **Project:** mnemo_cards_web_v2 **Feature:** Statistics System Upgrade - Frontend **Created:** 2025-11-08 --- ## Phase 1: Frontend Services ### Task F1.1: Update HttpRepositoryV2 **Estimated Time:** 2-3 hours **File:** `lib/domain/services/http_repository_v2.dart` **Add Methods:** ```dart class HttpRepositoryV2 { // Existing methods... /// Get detailed user statistics Future getDetailedStatistics() async { final response = await _get('/users/me/statistics/detailed'); return UserDataDto.fromJson(response); } /// Get packs statistics Future> getPacksStatistics({String? packId}) async { final queryParams = packId != null ? '?packId=$packId' : ''; final response = await _get('/users/me/statistics/packs$queryParams'); return (response as List) .map((e) => PackProgressDto.fromJson(e as Map)) .toList(); } /// Get words statistics with pagination Future> getWordsStatistics({ String? packId, int limit = 50, int offset = 0, String sortBy = 'difficulty', bool needsReview = false, }) async { final queryParams = { 'limit': limit.toString(), 'offset': offset.toString(), 'sortBy': sortBy, 'needsReview': needsReview.toString(), if (packId != null) 'packId': packId, }; final query = queryParams.entries .map((e) => '${e.key}=${e.value}') .join('&'); return await _get('/users/me/statistics/words?$query'); } /// Get timeline statistics Future> getTimelineStatistics({ required String period, DateTime? from, DateTime? to, }) async { final queryParams = { 'period': period, if (from != null) 'from': from.toIso8601String(), if (to != null) 'to': to.toIso8601String(), }; final query = queryParams.entries .map((e) => '${e.key}=${e.value}') .join('&'); return await _get('/users/me/statistics/timeline?$query'); } /// Record study session Future recordStudySession(StudySessionDto session) async { await _post('/users/me/sessions', body: session.toJson()); } /// Get achievements Future> getAchievements() async { final response = await _get('/users/me/achievements'); return (response as List) .map((e) => AchievementDto.fromJson(e as Map)) .toList(); } } ``` **Steps:** - [ ] Add getDetailedStatistics method - [ ] Add getPacksStatistics method with optional packId - [ ] Add getWordsStatistics method with all filters - [ ] Add getTimelineStatistics method - [ ] Add recordStudySession method - [ ] Add getAchievements method - [ ] Add error handling for all methods - [ ] Write unit tests with mocked responses --- ### Task F1.2: Create Enhanced StatisticsService **Estimated Time:** 4-5 hours **File:** `lib/domain/services/statistics_service.dart` (rewrite) **New Models File:** `lib/domain/models/statistics_models.dart` (new) **Models:** ```dart /// Detailed user statistics class DetailedUserStatistics { final int totalWords; final Duration totalStudyTime; final int testsCompleted; final int currentStreak; final int longestStreak; final double averageAccuracy; final List recentAchievements; final Map packStats; const DetailedUserStatistics({...}); } /// Pack statistics class PackStatistics { final String packId; final String packName; final int totalCards; final int learnedCards; final double progress; final Duration studyTime; final DateTime? lastStudyDate; final double accuracy; const PackStatistics({...}); factory PackStatistics.fromDto(PackProgressDto dto, String packName) {...} } /// Words statistics data with pagination class WordsStatisticsData { final List words; final int totalCount; final int page; final int pageSize; final bool hasMore; const WordsStatisticsData({...}); } /// Individual word statistics class WordStatistics { final String word; final String? translation; final double correctRate; final int totalAttempts; final DateTime? lastReviewed; final double difficultyScore; final bool needsReview; final String? packName; const WordStatistics({...}); factory WordStatistics.fromDto(DetailedWordStatisticsDto dto) {...} } /// Timeline data class TimelineData { final List dailyActivity; final Map hourlyActivity; // hour -> duration final Map weekdayActivity; // weekday -> duration const TimelineData({...}); } /// Daily activity class DailyActivity { final DateTime date; final int wordsLearned; final Duration studyTime; final int testsCompleted; final bool hasActivity; const DailyActivity({...}); } /// Achievement class Achievement { final String id; final String title; final String description; final String? iconUrl; final DateTime? unlockedAt; final bool isLocked; final double progress; // 0.0 to 1.0 final AchievementType type; const Achievement({...}); bool get isUnlocked => unlockedAt != null; factory Achievement.fromDto(AchievementDto dto) {...} } ``` **Service:** ```dart class StatisticsService { final HttpRepositoryV2 _repository; StatisticsService(this._repository); /// Get detailed statistics Future getDetailedStatistics() async { final userDataDto = await _repository.getDetailedStatistics(); return _convertToDetailedStatistics(userDataDto); } /// Get packs statistics Future> getPacksStatistics({String? packId}) async { final dtos = await _repository.getPacksStatistics(packId: packId); // Convert DTOs to PackStatistics (need to fetch pack names) return _convertToPackStatistics(dtos); } /// Get words statistics with pagination Future getWordsStatistics({ String? packId, int page = 0, int pageSize = 50, WordsSortOption sortBy = WordsSortOption.difficulty, bool needsReview = false, }) async { final response = await _repository.getWordsStatistics( packId: packId, limit: pageSize, offset: page * pageSize, sortBy: sortBy.value, needsReview: needsReview, ); return _convertToWordsStatisticsData(response, page, pageSize); } /// Get timeline statistics Future getTimelineStatistics({ required TimelinePeriod period, DateTime? from, DateTime? to, }) async { final response = await _repository.getTimelineStatistics( period: period.value, from: from, to: to, ); return _convertToTimelineData(response); } // Session management String? _currentSessionId; DateTime? _sessionStartTime; /// Start study session String startSession({String? packId, String? testId}) { _currentSessionId = _generateSessionId(); _sessionStartTime = DateTime.now(); // Will be sent to backend when ended return _currentSessionId!; } /// End study session Future endSession(String sessionId, { int wordsLearned = 0, int testsCompleted = 0, double accuracy = 0.0, }) async { if (_currentSessionId != sessionId) return; if (_sessionStartTime == null) return; final session = StudySessionDto( sessionId: sessionId, startTime: _sessionStartTime!, endTime: DateTime.now(), wordsLearned: wordsLearned, testsCompleted: testsCompleted, accuracy: accuracy, ); await _repository.recordStudySession(session); _currentSessionId = null; _sessionStartTime = null; } /// Get achievements Future> getAchievements() async { final dtos = await _repository.getAchievements(); return dtos.map((dto) => Achievement.fromDto(dto)).toList(); } /// Get new (recently unlocked) achievements Future> getNewAchievements() async { final achievements = await getAchievements(); final now = DateTime.now(); final threeDaysAgo = now.subtract(const Duration(days: 3)); return achievements .where((a) => a.isUnlocked && a.unlockedAt!.isAfter(threeDaysAgo)) .toList(); } // Private helper methods DetailedUserStatistics _convertToDetailedStatistics(UserDataDto dto) {...} List _convertToPackStatistics(List dtos) {...} WordsStatisticsData _convertToWordsStatisticsData(Map response, int page, int pageSize) {...} TimelineData _convertToTimelineData(Map response) {...} String _generateSessionId() => 'session_${DateTime.now().millisecondsSinceEpoch}'; } /// Sort options for words enum WordsSortOption { difficulty('difficulty'), accuracy('accuracy'), recent('recent'), alphabetical('alphabetical'); final String value; const WordsSortOption(this.value); } /// Timeline period enum TimelinePeriod { day('day'), week('week'), month('month'), year('year'); final String value; const TimelinePeriod(this.value); } ``` **Steps:** - [ ] Create statistics_models.dart with all model classes - [ ] Rewrite StatisticsService with real logic - [ ] Implement all conversion methods - [ ] Add session tracking logic - [ ] Add error handling - [ ] Write comprehensive unit tests --- ### Task F1.3: Create State Managers **Estimated Time:** 3-4 hours #### 1. StatisticsStateManager **File:** `lib/domain/state/statistics_state_manager.dart` (new) ```dart @freezed class StatisticsState with _$StatisticsState { const factory StatisticsState.loading() = _Loading; const factory StatisticsState.loaded(DetailedUserStatistics statistics) = _Loaded; const factory StatisticsState.error(String message) = _Error; } class StatisticsStateManager extends StateManager { final StatisticsService _service; StatisticsStateManager(this._service) : super(const StatisticsState.loading()); Future loadStatistics() => handle((emit) async { emit(const StatisticsState.loading()); try { final statistics = await _service.getDetailedStatistics(); emit(StatisticsState.loaded(statistics)); } catch (e) { emit(StatisticsState.error(e.toString())); } }); Future refreshStatistics() => loadStatistics(); } ``` #### 2. PacksStatisticsStateManager **File:** `lib/domain/state/packs_statistics_state_manager.dart` (new) ```dart @freezed class PacksStatisticsState with _$PacksStatisticsState { const factory PacksStatisticsState.loading() = _Loading; const factory PacksStatisticsState.loaded(List packs) = _Loaded; const factory PacksStatisticsState.error(String message) = _Error; } class PacksStatisticsStateManager extends StateManager { final StatisticsService _service; PacksStatisticsStateManager(this._service) : super(const PacksStatisticsState.loading()); Future loadStatistics({String? packId}) => handle((emit) async { emit(const PacksStatisticsState.loading()); try { final packs = await _service.getPacksStatistics(packId: packId); emit(PacksStatisticsState.loaded(packs)); } catch (e) { emit(PacksStatisticsState.error(e.toString())); } }); } ``` #### 3. WordsStatisticsStateManager **File:** `lib/domain/state/words_statistics_state_manager.dart` (new) ```dart @freezed class WordsStatisticsState with _$WordsStatisticsState { const factory WordsStatisticsState.loading() = _Loading; const factory WordsStatisticsState.loaded(WordsStatisticsData data) = _Loaded; const factory WordsStatisticsState.error(String message) = _Error; } class WordsStatisticsStateManager extends StateManager { final StatisticsService _service; WordsStatisticsStateManager(this._service) : super(const WordsStatisticsState.loading()); Future loadStatistics({ String? packId, int page = 0, WordsSortOption sortBy = WordsSortOption.difficulty, bool needsReview = false, }) => handle((emit) async { emit(const WordsStatisticsState.loading()); try { final data = await _service.getWordsStatistics( packId: packId, page: page, sortBy: sortBy, needsReview: needsReview, ); emit(WordsStatisticsState.loaded(data)); } catch (e) { emit(WordsStatisticsState.error(e.toString())); } }); Future loadMore() => handle((emit) async { final currentState = state; if (currentState is! _Loaded) return; final currentData = currentState.data; if (!currentData.hasMore) return; // Load next page and append // Implementation... }); } ``` #### 4. AchievementsStateManager **File:** `lib/domain/state/achievements_state_manager.dart` (new) ```dart @freezed class AchievementsState with _$AchievementsState { const factory AchievementsState.loading() = _Loading; const factory AchievementsState.loaded(List achievements) = _Loaded; const factory AchievementsState.error(String message) = _Error; } class AchievementsStateManager extends StateManager { final StatisticsService _service; AchievementsStateManager(this._service) : super(const AchievementsState.loading()); Future loadAchievements() => handle((emit) async { emit(const AchievementsState.loading()); try { final achievements = await _service.getAchievements(); emit(AchievementsState.loaded(achievements)); } catch (e) { emit(AchievementsState.error(e.toString())); } }); List get unlockedAchievements { final currentState = state; if (currentState is! _Loaded) return []; return currentState.achievements.where((a) => a.isUnlocked).toList(); } List get lockedAchievements { final currentState = state; if (currentState is! _Loaded) return []; return currentState.achievements.where((a) => a.isLocked).toList(); } } ``` **Steps:** - [ ] Create all state manager files - [ ] Generate freezed classes - [ ] Add to UserScope DI module - [ ] Write unit tests for each state manager --- ### Task F1.4: Add to DI Module **Estimated Time:** 1 hour **File:** `lib/di/user_scope/modules/statistics_module.dart` (new) ```dart @module abstract class StatisticsModule { @lazySingleton StatisticsService statisticsService(HttpRepositoryV2 repository) { return StatisticsService(repository); } @lazySingleton StatisticsStateManager statisticsStateManager(StatisticsService service) { return StatisticsStateManager(service); } @lazySingleton PacksStatisticsStateManager packsStatisticsStateManager( StatisticsService service, ) { return PacksStatisticsStateManager(service); } @lazySingleton WordsStatisticsStateManager wordsStatisticsStateManager( StatisticsService service, ) { return WordsStatisticsStateManager(service); } @lazySingleton AchievementsStateManager achievementsStateManager( StatisticsService service, ) { return AchievementsStateManager(service); } } ``` **Steps:** - [ ] Create statistics_module.dart - [ ] Add module to UserScope - [ ] Run DI code generation - [ ] Verify injection works --- ## Phase 2: UI Components - Statistics Widgets ### Task F2.1: Create Base Statistics Widgets **Estimated Time:** 6-8 hours #### 1. StatsCard **File:** `lib/presentation/widgets/stats/stats_card.dart` (new) ```dart class StatsCard extends StatelessWidget { final IconData icon; final String label; final String value; final Color? color; final VoidCallback? onTap; const StatsCard({ required this.icon, required this.label, required this.value, this.color, this.onTap, super.key, }); @override Widget build(BuildContext context) { // Beautiful card with gradient, icon, value, label // Shimmer loading animation // Counter animation for value } } ``` #### 2. CircularProgressWidget **File:** `lib/presentation/widgets/stats/circular_progress_widget.dart` ```dart class CircularProgressWidget extends StatelessWidget { final double progress; // 0.0 to 1.0 final double size; final Color? color; final String? centerText; const CircularProgressWidget({ required this.progress, this.size = 100, this.color, this.centerText, super.key, }); @override Widget build(BuildContext context) { // Custom circular progress with gradient // Percentage or custom text in center // Animation } } ``` #### 3. ActivityHeatmap **File:** `lib/presentation/widgets/stats/activity_heatmap.dart` ```dart class ActivityHeatmap extends StatelessWidget { final List activities; final int daysToShow; const ActivityHeatmap({ required this.activities, this.daysToShow = 30, super.key, }); @override Widget build(BuildContext context) { // GitHub-style heatmap // Tooltips on hover // Color intensity based on activity } } ``` #### 4. StreakCalendar **File:** `lib/presentation/widgets/stats/streak_calendar.dart` ```dart class StreakCalendar extends StatelessWidget { final int currentStreak; final int longestStreak; final List studyDates; const StreakCalendar({ required this.currentStreak, required this.longestStreak, required this.studyDates, super.key, }); @override Widget build(BuildContext context) { // Calendar view with streak visualization // Fire icon for current streak // Trophy icon for longest streak } } ``` #### 5. TimelineChart **File:** `lib/presentation/widgets/stats/timeline_chart.dart` ```dart class TimelineChart extends StatelessWidget { final TimelineData data; final TimelinePeriod period; const TimelineChart({ required this.data, required this.period, super.key, }); @override Widget build(BuildContext context) { // Line chart using fl_chart // Interactive tooltips // Smooth animations } } ``` **Steps:** - [ ] Create all widget files - [ ] Implement beautiful UI for each - [ ] Add animations - [ ] Make responsive - [ ] Add loading states - [ ] Write widget tests --- ## Phase 3: UI Pages - Profile Redesign ### Task F3.1: Redesign ProfilePage **Estimated Time:** 12-15 hours **File:** `lib/presentation/pages/profile/profile_page.dart` (major rewrite) **New Components to Create:** #### 1. ProfileUserHeader **File:** `lib/presentation/pages/profile/widgets/profile_user_header.dart` ```dart class ProfileUserHeader extends StatelessWidget { final UserDto user; final int currentStreak; const ProfileUserHeader({ required this.user, required this.currentStreak, super.key, }); @override Widget build(BuildContext context) { return Card( child: Padding( child: Row( children: [ // Large avatar with gradient border _buildAvatar(), // User info Expanded( child: Column( children: [ _buildNameAndEmail(), _buildBadges(), // streak, subscription, level ], ), ), ], ), ), ); } } ``` #### 2. QuickStatsGrid **File:** `lib/presentation/pages/profile/widgets/quick_stats_grid.dart` ```dart class QuickStatsGrid extends StatelessWidget { final DetailedUserStatistics statistics; const QuickStatsGrid({ required this.statistics, super.key, }); @override Widget build(BuildContext context) { return GridView.count( crossAxisCount: 2, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), children: [ StatsCard( icon: Icons.book, label: 'Words Learned', value: statistics.totalWords.toString(), ), StatsCard( icon: Icons.timer, label: 'Study Time', value: _formatDuration(statistics.totalStudyTime), ), StatsCard( icon: Icons.quiz, label: 'Tests Completed', value: statistics.testsCompleted.toString(), ), StatsCard( icon: Icons.trending_up, label: 'Accuracy', value: '${(statistics.averageAccuracy * 100).toStringAsFixed(1)}%', ), ], ); } } ``` #### 3. StreakCard **File:** `lib/presentation/pages/profile/widgets/streak_card.dart` ```dart class StreakCard extends StatelessWidget { final int currentStreak; final int longestStreak; final List studyDates; const StreakCard({ required this.currentStreak, required this.longestStreak, required this.studyDates, super.key, }); @override Widget build(BuildContext context) { return Card( child: Padding( child: Column( children: [ _buildHeader(), const SizedBox(height: 16), StreakCalendar( currentStreak: currentStreak, longestStreak: longestStreak, studyDates: studyDates, ), ], ), ), ); } } ``` #### 4. PackProgressCard **File:** `lib/presentation/pages/profile/widgets/pack_progress_card.dart` ```dart class PackProgressCard extends StatelessWidget { final PackStatistics packStats; final VoidCallback? onTap; const PackProgressCard({ required this.packStats, this.onTap, super.key, }); @override Widget build(BuildContext context) { return Card( child: InkWell( onTap: onTap, child: Padding( child: Row( children: [ // Pack image _buildPackImage(), const SizedBox(width: 16), // Pack info and progress Expanded( child: Column( children: [ _buildPackName(), _buildProgressBar(), _buildStats(), ], ), ), // Progress circle CircularProgressWidget( progress: packStats.progress, size: 60, ), ], ), ), ), ); } } ``` #### 5. AchievementBadge **File:** `lib/presentation/pages/profile/widgets/achievement_badge.dart` ```dart class AchievementBadge extends StatelessWidget { final Achievement achievement; final VoidCallback? onTap; const AchievementBadge({ required this.achievement, this.onTap, super.key, }); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Tooltip( message: achievement.description, child: Container( width: 80, height: 100, child: Column( children: [ // Badge icon/image _buildBadgeIcon(), const SizedBox(height: 8), // Badge title Text( achievement.title, maxLines: 2, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodySmall, ), ], ), ), ), ); } } ``` **Main ProfilePage Structure:** ```dart class ProfilePage extends StatefulWidget { const ProfilePage({super.key}); @override State createState() => _ProfilePageState(); } class _ProfilePageState extends State { @override void initState() { super.initState(); // Load statistics on page open _loadStatistics(); } void _loadStatistics() { final statisticsManager = context.read(); statisticsManager.loadStatistics(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Profile'), actions: [ IconButton( icon: const Icon(Icons.settings), onPressed: () => context.go('/settings'), ), ], ), body: RefreshIndicator( onRefresh: () async { await _loadStatistics(); }, child: ScopeBuilder( builder: (context, userScope) { if (userScope == null) { return const Center(child: CircularProgressIndicator()); } return StateBuilder( stateReadable: userScope.userStateManager, builder: (context, userState, _) { return userState.when( guest: () => _buildGuestView(context), authenticated: (user) => _buildAuthenticatedView( context, user, userScope, ), loading: () => const Center( child: CircularProgressIndicator(), ), ); }, ); }, ), ), ); } Widget _buildAuthenticatedView( BuildContext context, UserDto user, UserScope userScope, ) { return StateBuilder( stateReadable: userScope.statisticsStateManager, builder: (context, statisticsState, _) { return statisticsState.when( loading: () => _buildLoadingSkeleton(), loaded: (statistics) => _buildProfileContent( context, user, statistics, userScope, ), error: (message) => _buildErrorView(message), ); }, ); } Widget _buildProfileContent( BuildContext context, UserDto user, DetailedUserStatistics statistics, UserScope userScope, ) { return SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // User header with avatar and badges ProfileUserHeader( user: user, currentStreak: statistics.currentStreak, ), const SizedBox(height: 24), // Quick stats grid (4 cards) QuickStatsGrid(statistics: statistics), const SizedBox(height: 24), // Streak card with calendar StreakCard( currentStreak: statistics.currentStreak, longestStreak: statistics.longestStreak, studyDates: [], // from statistics ), const SizedBox(height: 24), // Activity chart _buildActivitySection(userScope), const SizedBox(height: 24), // Packs progress _buildPacksProgressSection( context, statistics.packStats.values.toList(), ), const SizedBox(height: 24), // Achievements _buildAchievementsSection( context, statistics.recentAchievements, ), const SizedBox(height: 24), // Account actions _buildAccountActionsCard(context), ], ), ); } Widget _buildActivitySection(UserScope userScope) { // TimelineChart with tabs for different periods } Widget _buildPacksProgressSection( BuildContext context, List packs, ) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Your Packs Progress', style: Theme.of(context).textTheme.titleLarge, ), TextButton( onPressed: () => context.go('/statistics/packs'), child: const Text('View All'), ), ], ), const SizedBox(height: 16), ...packs.take(3).map((pack) => Padding( padding: const EdgeInsets.only(bottom: 12), child: PackProgressCard( packStats: pack, onTap: () => context.go('/packs/${pack.packId}'), ), )), ], ); } Widget _buildAchievementsSection( BuildContext context, List achievements, ) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Recent Achievements', style: Theme.of(context).textTheme.titleLarge, ), TextButton( onPressed: () => context.go('/achievements'), child: const Text('View All'), ), ], ), const SizedBox(height: 16), SizedBox( height: 120, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: achievements.length, itemBuilder: (context, index) { return Padding( padding: const EdgeInsets.only(right: 16), child: AchievementBadge( achievement: achievements[index], onTap: () => context.go('/achievements'), ), ); }, ), ), ], ); } } ``` **Steps:** - [ ] Create all widget files - [ ] Implement ProfileUserHeader - [ ] Implement QuickStatsGrid - [ ] Implement StreakCard - [ ] Implement PackProgressCard - [ ] Implement AchievementBadge - [ ] Rewrite ProfilePage with new layout - [ ] Add skeleton loading states - [ ] Add error states - [ ] Make responsive (mobile/tablet/desktop) - [ ] Add animations - [ ] Write widget tests --- ## Phase 4: UI Pages - Statistics Pages ### Task F4.1: Create WordsStatisticsPage **Estimated Time:** 6-8 hours **File:** `lib/presentation/pages/statistics/words_statistics_page.dart` (new) **Structure:** - AppBar with search - Filter bar (pack, sort, needs review toggle) - Paginated list of WordStatisticsCard - Load more button **Components:** 1. `WordStatisticsCard` (`widgets/word_statistics_card.dart`) 2. `WordsFilterBar` (`widgets/words_filter_bar.dart`) **Steps:** - [ ] Create WordsStatisticsPage - [ ] Create WordStatisticsCard - [ ] Create WordsFilterBar - [ ] Implement pagination - [ ] Implement search - [ ] Implement filtering and sorting - [ ] Add empty state - [ ] Add loading skeleton - [ ] Write widget tests --- ### Task F4.2: Create PacksStatisticsPage **Estimated Time:** 8-10 hours **File:** `lib/presentation/pages/statistics/packs_statistics_page.dart` (new) **Structure:** - AppBar with sort menu - Grid/List of PackStatisticsCard - Detailed page for each pack **Additional Page:** **File:** `lib/presentation/pages/statistics/pack_statistics_details_page.dart` **Structure:** - Pack header with overall stats - Progress timeline chart - Cards list with individual progress - Study history timeline **Steps:** - [ ] Create PacksStatisticsPage - [ ] Create PackStatisticsDetailsPage - [ ] Create PackHeaderCard widget - [ ] Create ProgressTimelineChart widget - [ ] Create CardProgressItem widget - [ ] Implement navigation - [ ] Add empty state - [ ] Write widget tests --- ### Task F4.3: Create AchievementsPage **Estimated Time:** 6-8 hours **File:** `lib/presentation/pages/achievements/achievements_page.dart` (new) **Structure:** - AppBar with progress indicator (X/Y unlocked) - Tabs (All / Unlocked / Locked) - Grid of AchievementCard - Unlock animation for new achievements **Components:** 1. `AchievementCard` (detailed card, not just badge) 2. `AchievementUnlockDialog` - shown when new achievement unlocked **Steps:** - [ ] Create AchievementsPage - [ ] Create AchievementCard widget - [ ] Create AchievementUnlockDialog - [ ] Implement tabs filtering - [ ] Add unlock animations - [ ] Add confetti effect for unlocks - [ ] Write widget tests --- ## Phase 5: Settings Page ### Task F5.1: Create SettingsPage **Estimated Time:** 10-12 hours **File:** `lib/presentation/pages/settings/settings_page.dart` (new) **Structure:** - Appearance section (theme, color, font size, language) - Learning section (daily goal, reminders, auto-play, etc.) - Privacy section (analytics, ads) - Account section (email, name, password, delete) - Data section (export, import, clear cache) - About section (version, terms, privacy policy) **Components:** 1. **SettingsSection** (`widgets/settings_section.dart`) 2. **SettingsTile** (`widgets/settings_tile.dart`) 3. **ThemeSelector** (`widgets/theme_selector.dart`) 4. **ColorPicker** (`widgets/color_picker_widget.dart`) 5. **TimePickerSetting** (`widgets/time_picker_setting.dart`) **Steps:** - [ ] Create SettingsPage with all sections - [ ] Create SettingsSection widget - [ ] Create SettingsTile widget - [ ] Create ThemeSelector widget - [ ] Create ColorPicker widget - [ ] Create TimePickerSetting widget - [ ] Implement settings save/load - [ ] Add confirmation dialogs for destructive actions - [ ] Write widget tests --- ### Task F5.2: Extend UserSettingsDto **Estimated Time:** 2-3 hours **File:** `mnemo_cards_common/lib/src/dtos/user/settings/user_settings_dto.dart` **Add Fields:** ```dart @JsonSerializable() @CopyWith() class UserSettingsDto { // Appearance final String theme; // 'light', 'dark', 'system' final String? primaryColor; final double fontSize; // 0.8 - 1.2 final String language; // Learning final int dailyGoalWords; final bool reminderEnabled; final String? reminderTime; final bool autoPlayAudio; final bool showTranslations; final int cardsPerSession; // Privacy final bool analyticsEnabled; final bool personalizedAdsEnabled; // Notifications final bool pushNotificationsEnabled; final bool emailNotificationsEnabled; } ``` **Steps:** - [ ] Add new fields to UserSettingsDto - [ ] Run codegen - [ ] Update backend to support new fields - [ ] Write tests --- ### Task F5.3: Create SettingsStateManager **Estimated Time:** 2 hours **File:** `lib/domain/state/settings_state_manager.dart` (new) ```dart class SettingsStateManager extends StateManager { final HttpRepositoryV2 _repository; final SharedPreferences _prefs; SettingsStateManager(this._repository, this._prefs) : super(_loadFromPrefs(_prefs)); static UserSettingsDto _loadFromPrefs(SharedPreferences prefs) { // Load from local storage } Future updateSettings(UserSettingsDto settings) => handle((emit) async { emit(settings); await _saveToPrefs(settings); await _repository.updateUserSettings(settings); }); Future _saveToPrefs(UserSettingsDto settings) async { // Save to local storage } } ``` **Steps:** - [ ] Create SettingsStateManager - [ ] Implement local storage - [ ] Implement server sync - [ ] Add to DI - [ ] Write unit tests --- ### Task F5.4: Apply Settings Throughout App **Estimated Time:** 6-8 hours **Apply Settings In:** 1. **Theme** - Update ThemeStateManager 2. **Font Size** - Apply scaling factor 3. **Learning** - Use in CardFlipper, tests 4. **Daily Goal** - Show on ProfilePage **Files to Modify:** - `lib/domain/state/theme_state_manager.dart` - `lib/presentation/widgets/card_flipper/card_flipper.dart` - `lib/presentation/pages/profile/profile_page.dart` **Steps:** - [ ] Update ThemeStateManager to support custom colors - [ ] Apply font size scaling - [ ] Use learning settings in CardFlipper - [ ] Show daily goal tracking on ProfilePage - [ ] Implement reminder notifications (web) - [ ] Write tests --- ## Phase 6: Polish and Animations ### Task F6.1: Add Animations **Estimated Time:** 6-8 hours **Animations to Add:** 1. **Page Transitions** - Hero animations 2. **Counter Animations** - Animated numbers 3. **Chart Animations** - fl_chart animations 4. **Achievement Unlock** - Confetti + scale animation 5. **Shimmer Loading** - Skeleton screens 6. **Pull to Refresh** - Custom refresh indicator **Files:** - Create `lib/presentation/animations/` directory - `animated_counter.dart` - `shimmer_loading.dart` - `achievement_confetti.dart` **Dependencies to Add:** - fl_chart - shimmer - confetti - lottie (optional) **Steps:** - [ ] Create AnimatedCounter widget - [ ] Add shimmer loaders to all pages - [ ] Add Hero animations for images - [ ] Create achievement unlock animation - [ ] Add confetti effect - [ ] Add pull-to-refresh - [ ] Write widget tests --- ## Phase 7: Testing ### Task F7.1: Unit Tests **Estimated Time:** 4-5 hours **Test Files:** - `test/domain/services/statistics_service_test.dart` - `test/domain/state/statistics_state_manager_test.dart` - `test/domain/state/settings_state_manager_test.dart` - `test/domain/models/statistics_models_test.dart` **Steps:** - [ ] Write StatisticsService tests - [ ] Write state manager tests - [ ] Write model conversion tests - [ ] Write settings logic tests --- ### Task F7.2: Widget Tests **Estimated Time:** 6-8 hours **Test Files:** - `test/presentation/pages/profile/profile_page_test.dart` - `test/presentation/pages/statistics/words_statistics_page_test.dart` - `test/presentation/pages/statistics/packs_statistics_page_test.dart` - `test/presentation/pages/achievements/achievements_page_test.dart` - `test/presentation/pages/settings/settings_page_test.dart` - `test/presentation/widgets/stats/*_test.dart` **Steps:** - [ ] Write ProfilePage tests - [ ] Write statistics pages tests - [ ] Write AchievementsPage tests - [ ] Write SettingsPage tests - [ ] Write widget tests for all custom widgets --- ### Task F7.3: Integration Tests **Estimated Time:** 4-6 hours **Test File:** `integration_test/statistics_flow_test.dart` **Tests:** - Load statistics flow - Navigate through statistics pages - Update settings flow - Achievement unlock flow **Steps:** - [ ] Create integration test file - [ ] Write statistics load test - [ ] Write navigation test - [ ] Write settings update test - [ ] Write achievement test --- ## Phase 8: Documentation ### Task F8.1: Update Documentation **Estimated Time:** 2-3 hours **Files to Update:** - `PROGRESS.md` - Add completed work - `TODO.md` - Update task statuses - `README.md` - Add new features documentation - Create `STATISTICS_UI_GUIDE.md` - UI component documentation **Steps:** - [ ] Update PROGRESS.md with detailed changes - [ ] Mark completed tasks in TODO.md - [ ] Update README with new features - [ ] Create UI guide with screenshots --- ## Summary **Total Frontend Estimated Time:** 93-119 hours **Priority Order:** 1. **Phase 1** - Services (10-13 hours) ✅ HIGHEST 2. **Phase 3** - Profile UI (12-15 hours) ✅ HIGHEST 3. **Phase 5.1-5.3** - Settings Page (14-17 hours) ✅ HIGH 4. **Phase 2** - Stats Widgets (6-8 hours) 🟡 MEDIUM 5. **Phase 4** - Statistics Pages (20-26 hours) 🟡 MEDIUM 6. **Phase 5.4** - Apply Settings (6-8 hours) 🟡 MEDIUM 7. **Phase 6** - Animations (6-8 hours) 🟢 LOW 8. **Phase 7** - Testing (14-19 hours) 🟢 LOW 9. **Phase 8** - Documentation (2-3 hours) 🟢 LOW **Dependencies:** - Phase 1 must be done first (services) - Phase 2 needed for Phase 3 (widgets for profile) - Phase 5.1-5.3 for settings - Phase 6 can be done in parallel with other UI work - Phase 7 should be done alongside development - Phase 8 done last --- **Start Date:** TBD **Target Completion:** TBD **Current Status:** Planning Complete, Ready to Start