mnemo_cards/mnemo_cards_web_v2/STATISTICS_TASKS.md
2025-11-11 02:55:41 +03:00

39 KiB

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:

class HttpRepositoryV2 {
  // Existing methods...
  
  /// Get detailed user statistics
  Future<UserDataDto> getDetailedStatistics() async {
    final response = await _get('/users/me/statistics/detailed');
    return UserDataDto.fromJson(response);
  }
  
  /// Get packs statistics
  Future<List<PackProgressDto>> 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<String, dynamic>))
        .toList();
  }
  
  /// Get words statistics with pagination
  Future<Map<String, dynamic>> getWordsStatistics({
    String? packId,
    int limit = 50,
    int offset = 0,
    String sortBy = 'difficulty',
    bool needsReview = false,
  }) async {
    final queryParams = <String, String>{
      '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<Map<String, dynamic>> getTimelineStatistics({
    required String period,
    DateTime? from,
    DateTime? to,
  }) async {
    final queryParams = <String, String>{
      '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<void> recordStudySession(StudySessionDto session) async {
    await _post('/users/me/sessions', body: session.toJson());
  }
  
  /// Get achievements
  Future<List<AchievementDto>> getAchievements() async {
    final response = await _get('/users/me/achievements');
    return (response as List)
        .map((e) => AchievementDto.fromJson(e as Map<String, dynamic>))
        .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:

/// 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<Achievement> recentAchievements;
  final Map<String, PackStatistics> 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<WordStatistics> 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> dailyActivity;
  final Map<int, Duration> hourlyActivity; // hour -> duration
  final Map<int, Duration> 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:

class StatisticsService {
  final HttpRepositoryV2 _repository;
  
  StatisticsService(this._repository);
  
  /// Get detailed statistics
  Future<DetailedUserStatistics> getDetailedStatistics() async {
    final userDataDto = await _repository.getDetailedStatistics();
    return _convertToDetailedStatistics(userDataDto);
  }
  
  /// Get packs statistics
  Future<List<PackStatistics>> 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<WordsStatisticsData> 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<TimelineData> 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<void> 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<List<Achievement>> getAchievements() async {
    final dtos = await _repository.getAchievements();
    return dtos.map((dto) => Achievement.fromDto(dto)).toList();
  }
  
  /// Get new (recently unlocked) achievements
  Future<List<Achievement>> 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<PackStatistics> _convertToPackStatistics(List<PackProgressDto> dtos) {...}
  WordsStatisticsData _convertToWordsStatisticsData(Map<String, dynamic> response, int page, int pageSize) {...}
  TimelineData _convertToTimelineData(Map<String, dynamic> 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)

@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<StatisticsState> {
  final StatisticsService _service;
  
  StatisticsStateManager(this._service) 
      : super(const StatisticsState.loading());
  
  Future<void> 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<void> refreshStatistics() => loadStatistics();
}

2. PacksStatisticsStateManager

File: lib/domain/state/packs_statistics_state_manager.dart (new)

@freezed
class PacksStatisticsState with _$PacksStatisticsState {
  const factory PacksStatisticsState.loading() = _Loading;
  const factory PacksStatisticsState.loaded(List<PackStatistics> packs) = _Loaded;
  const factory PacksStatisticsState.error(String message) = _Error;
}

class PacksStatisticsStateManager extends StateManager<PacksStatisticsState> {
  final StatisticsService _service;
  
  PacksStatisticsStateManager(this._service) 
      : super(const PacksStatisticsState.loading());
  
  Future<void> 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)

@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<WordsStatisticsState> {
  final StatisticsService _service;
  
  WordsStatisticsStateManager(this._service) 
      : super(const WordsStatisticsState.loading());
  
  Future<void> 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<void> 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)

@freezed
class AchievementsState with _$AchievementsState {
  const factory AchievementsState.loading() = _Loading;
  const factory AchievementsState.loaded(List<Achievement> achievements) = _Loaded;
  const factory AchievementsState.error(String message) = _Error;
}

class AchievementsStateManager extends StateManager<AchievementsState> {
  final StatisticsService _service;
  
  AchievementsStateManager(this._service) 
      : super(const AchievementsState.loading());
  
  Future<void> 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<Achievement> get unlockedAchievements {
    final currentState = state;
    if (currentState is! _Loaded) return [];
    return currentState.achievements.where((a) => a.isUnlocked).toList();
  }
  
  List<Achievement> 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)

@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)

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

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

class ActivityHeatmap extends StatelessWidget {
  final List<DailyActivity> 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

class StreakCalendar extends StatelessWidget {
  final int currentStreak;
  final int longestStreak;
  final List<DateTime> 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

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

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

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

class StreakCard extends StatelessWidget {
  final int currentStreak;
  final int longestStreak;
  final List<DateTime> 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

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

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:

class ProfilePage extends StatefulWidget {
  const ProfilePage({super.key});
  
  @override
  State<ProfilePage> createState() => _ProfilePageState();
}

class _ProfilePageState extends State<ProfilePage> {
  @override
  void initState() {
    super.initState();
    // Load statistics on page open
    _loadStatistics();
  }
  
  void _loadStatistics() {
    final statisticsManager = context.read<StatisticsStateManager>();
    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<UserScope>(
          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<PackStatistics> 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<Achievement> 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:

@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)

class SettingsStateManager extends StateManager<UserSettingsDto> {
  final HttpRepositoryV2 _repository;
  final SharedPreferences _prefs;
  
  SettingsStateManager(this._repository, this._prefs)
      : super(_loadFromPrefs(_prefs));
  
  static UserSettingsDto _loadFromPrefs(SharedPreferences prefs) {
    // Load from local storage
  }
  
  Future<void> updateSettings(UserSettingsDto settings) => handle((emit) async {
    emit(settings);
    await _saveToPrefs(settings);
    await _repository.updateUserSettings(settings);
  });
  
  Future<void> _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