mnemo_cards/mnemo_cards_web_v2/STATISTICS_TASKS.md

1595 lines
39 KiB
Markdown
Raw Normal View History

2025-11-10 23:55:41 +00:00
# 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<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:**
```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<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:**
```dart
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)
```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<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)
```dart
@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)
```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<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)
```dart
@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)
```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<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`
```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`
```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<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`
```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<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:**
```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<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