1594 lines
39 KiB
Markdown
1594 lines
39 KiB
Markdown
# 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
|
|
|