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:
WordStatisticsCard(widgets/word_statistics_card.dart)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:
AchievementCard(detailed card, not just badge)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:
- SettingsSection (
widgets/settings_section.dart) - SettingsTile (
widgets/settings_tile.dart) - ThemeSelector (
widgets/theme_selector.dart) - ColorPicker (
widgets/color_picker_widget.dart) - 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:
- Theme - Update ThemeStateManager
- Font Size - Apply scaling factor
- Learning - Use in CardFlipper, tests
- Daily Goal - Show on ProfilePage
Files to Modify:
lib/domain/state/theme_state_manager.dartlib/presentation/widgets/card_flipper/card_flipper.dartlib/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:
- Page Transitions - Hero animations
- Counter Animations - Animated numbers
- Chart Animations - fl_chart animations
- Achievement Unlock - Confetti + scale animation
- Shimmer Loading - Skeleton screens
- Pull to Refresh - Custom refresh indicator
Files:
- Create
lib/presentation/animations/directory animated_counter.dartshimmer_loading.dartachievement_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.darttest/domain/state/statistics_state_manager_test.darttest/domain/state/settings_state_manager_test.darttest/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.darttest/presentation/pages/statistics/words_statistics_page_test.darttest/presentation/pages/statistics/packs_statistics_page_test.darttest/presentation/pages/achievements/achievements_page_test.darttest/presentation/pages/settings/settings_page_test.darttest/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 workTODO.md- Update task statusesREADME.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:
- Phase 1 - Services (10-13 hours) ✅ HIGHEST
- Phase 3 - Profile UI (12-15 hours) ✅ HIGHEST
- Phase 5.1-5.3 - Settings Page (14-17 hours) ✅ HIGH
- Phase 2 - Stats Widgets (6-8 hours) 🟡 MEDIUM
- Phase 4 - Statistics Pages (20-26 hours) 🟡 MEDIUM
- Phase 5.4 - Apply Settings (6-8 hours) 🟡 MEDIUM
- Phase 6 - Animations (6-8 hours) 🟢 LOW
- Phase 7 - Testing (14-19 hours) 🟢 LOW
- 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