From 72fcb82863f0651f3a2c3e2fd0df92398df76179 Mon Sep 17 00:00:00 2001 From: Adam Dybcio Date: Sun, 5 Oct 2025 17:52:41 +0200 Subject: [PATCH 1/2] feat: Implement achievements system with events, states, and UI components - Added AchievementsEvent and AchievementsState classes for managing achievements logic. - Created AchievementPage with a responsive AppBar and layout for displaying achievements. - Introduced AchievementOverlay for displaying achievement notifications. - Developed AchievementWidget to visually represent achievements with animations. - Integrated achievements tracking in QuizSummary and QuizSummaryStats, triggering achievement checks based on user performance. - Added localization strings for various achievement titles and descriptions. - Updated main screen to include AchievementPage in navigation. - Introduced enums for AchievementCategory and AchievementLevel to categorize achievements. - Enhanced Helpers utility to show achievement dialogs. --- lib/di.dart | 2 + .../data/constants/achievements_list.dart | 206 +++++++++++++ .../achievements/data/model/achievement.dart | 18 ++ .../presentation/blocs/achievements_bloc.dart | 230 ++++++++++++++ .../blocs/achievements_event.dart | 17 ++ .../blocs/achievements_state.dart | 57 ++++ .../presentation/pages/achievement_page.dart | 74 ++++- .../widgets/achievement_overlay.dart | 61 ++++ .../widgets/achievement_widget.dart | 242 +++++++++++++++ .../presentation/widgets/quiz_summary.dart | 133 ++++---- .../widgets/quiz_summary_stats.dart | 60 +++- lib/l10n/app_en.arb | 50 ++- lib/l10n/app_localizations.dart | 288 ++++++++++++++++++ lib/l10n/app_localizations_en.dart | 161 ++++++++++ lib/main_screen.dart | 3 +- lib/utils/enums.dart | 3 + lib/utils/helpers.dart | 9 + 17 files changed, 1545 insertions(+), 69 deletions(-) create mode 100644 lib/features/achievements/data/constants/achievements_list.dart create mode 100644 lib/features/achievements/data/model/achievement.dart create mode 100644 lib/features/achievements/presentation/blocs/achievements_bloc.dart create mode 100644 lib/features/achievements/presentation/blocs/achievements_event.dart create mode 100644 lib/features/achievements/presentation/blocs/achievements_state.dart create mode 100644 lib/features/achievements/presentation/widgets/achievement_overlay.dart create mode 100644 lib/features/achievements/presentation/widgets/achievement_widget.dart create mode 100644 lib/utils/enums.dart diff --git a/lib/di.dart b/lib/di.dart index f45d447..f50104f 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -2,6 +2,7 @@ import 'package:get_it/get_it.dart'; import 'package:http/http.dart' as http; import 'package:quiz_master/core/presentation/blocs/page_index_cubit.dart'; import 'package:quiz_master/core/presentation/blocs/theme_cubit.dart'; +import 'package:quiz_master/features/achievements/presentation/blocs/achievements_bloc.dart'; import 'package:quiz_master/features/browse/presentation/blocs/browse/browse_bloc.dart'; import 'package:quiz_master/features/leveling/presentation/blocs/user_level/user_level_bloc.dart'; import 'package:quiz_master/features/quiz/data/datasource/quiz_remote_data_source.dart'; @@ -29,4 +30,5 @@ void setup() { sl.registerLazySingleton(() => UserLevelBloc()); sl.registerLazySingleton(() => BrowseBloc()); + sl.registerLazySingleton(() => AchievementsBloc()); } diff --git a/lib/features/achievements/data/constants/achievements_list.dart b/lib/features/achievements/data/constants/achievements_list.dart new file mode 100644 index 0000000..5de5035 --- /dev/null +++ b/lib/features/achievements/data/constants/achievements_list.dart @@ -0,0 +1,206 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:quiz_master/features/achievements/data/model/achievement.dart'; +import 'package:quiz_master/l10n/app_localizations.dart'; +import 'package:quiz_master/utils/enums.dart'; + +List getAchievements(BuildContext context) { + return [ + Achievement( + category: AchievementCategory.leveling, + level: AchievementLevel.bronze, + title: AppLocalizations.of(context)!.achievementLevelingBronzeTitle, + description: AppLocalizations.of(context)!.achievementLevelingBronzeDesc, + icon: FontAwesomeIcons.medal, + ), + Achievement( + category: AchievementCategory.leveling, + level: AchievementLevel.silver, + title: AppLocalizations.of(context)!.achievementLevelingSilverTitle, + description: AppLocalizations.of(context)!.achievementLevelingSilverDesc, + icon: FontAwesomeIcons.medal, + ), + Achievement( + category: AchievementCategory.leveling, + level: AchievementLevel.gold, + title: AppLocalizations.of(context)!.achievementLevelingGoldTitle, + description: AppLocalizations.of(context)!.achievementLevelingGoldDesc, + icon: FontAwesomeIcons.medal, + ), + Achievement( + category: AchievementCategory.leveling, + level: AchievementLevel.platinum, + title: AppLocalizations.of(context)!.achievementLevelingPlatinumTitle, + description: AppLocalizations.of( + context, + )!.achievementLevelingPlatinumDesc, + icon: FontAwesomeIcons.medal, + ), + Achievement( + category: AchievementCategory.leveling, + level: AchievementLevel.diamond, + title: AppLocalizations.of(context)!.achievementLevelingDiamondTitle, + description: AppLocalizations.of(context)!.achievementLevelingDiamondDesc, + icon: FontAwesomeIcons.medal, + ), + Achievement( + category: AchievementCategory.leveling, + level: AchievementLevel.master, + title: AppLocalizations.of(context)!.achievementLevelingMasterTitle, + description: AppLocalizations.of(context)!.achievementLevelingMasterDesc, + icon: FontAwesomeIcons.medal, + ), + Achievement( + category: AchievementCategory.answerMaster, + level: AchievementLevel.bronze, + title: AppLocalizations.of(context)!.achievementAnswerMasterBronzeTitle, + description: AppLocalizations.of( + context, + )!.achievementAnswerMasterBronzeDesc, + icon: FontAwesomeIcons.lightbulb, + ), + Achievement( + category: AchievementCategory.answerMaster, + level: AchievementLevel.silver, + title: AppLocalizations.of(context)!.achievementAnswerMasterSilverTitle, + description: AppLocalizations.of( + context, + )!.achievementAnswerMasterSilverDesc, + icon: FontAwesomeIcons.lightbulb, + ), + Achievement( + category: AchievementCategory.answerMaster, + level: AchievementLevel.gold, + title: AppLocalizations.of(context)!.achievementAnswerMasterGoldTitle, + description: AppLocalizations.of( + context, + )!.achievementAnswerMasterGoldDesc, + icon: FontAwesomeIcons.lightbulb, + ), + Achievement( + category: AchievementCategory.answerMaster, + level: AchievementLevel.platinum, + title: AppLocalizations.of(context)!.achievementAnswerMasterPlatinumTitle, + description: AppLocalizations.of( + context, + )!.achievementAnswerMasterPlatinumDesc, + icon: FontAwesomeIcons.lightbulb, + ), + Achievement( + category: AchievementCategory.answerMaster, + level: AchievementLevel.diamond, + title: AppLocalizations.of(context)!.achievementAnswerMasterDiamondTitle, + description: AppLocalizations.of( + context, + )!.achievementAnswerMasterDiamondDesc, + icon: FontAwesomeIcons.lightbulb, + ), + Achievement( + category: AchievementCategory.answerMaster, + level: AchievementLevel.master, + title: AppLocalizations.of(context)!.achievementAnswerMasterMasterTitle, + description: AppLocalizations.of( + context, + )!.achievementAnswerMasterMasterDesc, + icon: FontAwesomeIcons.lightbulb, + ), + Achievement( + category: AchievementCategory.quizAbuser, + level: AchievementLevel.bronze, + title: AppLocalizations.of(context)!.achievementQuizAbuserBronzeTitle, + description: AppLocalizations.of( + context, + )!.achievementQuizAbuserBronzeDesc, + icon: FontAwesomeIcons.fire, + ), + Achievement( + category: AchievementCategory.quizAbuser, + level: AchievementLevel.silver, + title: AppLocalizations.of(context)!.achievementQuizAbuserSilverTitle, + description: AppLocalizations.of( + context, + )!.achievementQuizAbuserSilverDesc, + icon: FontAwesomeIcons.fire, + ), + Achievement( + category: AchievementCategory.quizAbuser, + level: AchievementLevel.gold, + title: AppLocalizations.of(context)!.achievementQuizAbuserGoldTitle, + description: AppLocalizations.of(context)!.achievementQuizAbuserGoldDesc, + icon: FontAwesomeIcons.fire, + ), + Achievement( + category: AchievementCategory.quizAbuser, + level: AchievementLevel.platinum, + title: AppLocalizations.of(context)!.achievementQuizAbuserPlatinumTitle, + description: AppLocalizations.of( + context, + )!.achievementQuizAbuserPlatinumDesc, + icon: FontAwesomeIcons.fire, + ), + Achievement( + category: AchievementCategory.quizAbuser, + level: AchievementLevel.diamond, + title: AppLocalizations.of(context)!.achievementQuizAbuserDiamondTitle, + description: AppLocalizations.of( + context, + )!.achievementQuizAbuserDiamondDesc, + icon: FontAwesomeIcons.fire, + ), + Achievement( + category: AchievementCategory.quizAbuser, + level: AchievementLevel.master, + title: AppLocalizations.of(context)!.achievementQuizAbuserMasterTitle, + description: AppLocalizations.of( + context, + )!.achievementQuizAbuserMasterDesc, + icon: FontAwesomeIcons.fire, + ), + Achievement( + category: AchievementCategory.speedster, + level: AchievementLevel.bronze, + title: AppLocalizations.of(context)!.achievementSpeedsterBronzeTitle, + description: AppLocalizations.of(context)!.achievementSpeedsterBronzeDesc, + icon: FontAwesomeIcons.gaugeHigh, + ), + Achievement( + category: AchievementCategory.speedster, + level: AchievementLevel.silver, + title: AppLocalizations.of(context)!.achievementSpeedsterSilverTitle, + description: AppLocalizations.of(context)!.achievementSpeedsterSilverDesc, + icon: FontAwesomeIcons.gaugeHigh, + ), + Achievement( + category: AchievementCategory.speedster, + level: AchievementLevel.gold, + title: AppLocalizations.of(context)!.achievementSpeedsterGoldTitle, + description: AppLocalizations.of(context)!.achievementSpeedsterGoldDesc, + icon: FontAwesomeIcons.gaugeHigh, + ), + Achievement( + category: AchievementCategory.speedster, + level: AchievementLevel.platinum, + title: AppLocalizations.of(context)!.achievementSpeedsterPlatinumTitle, + description: AppLocalizations.of( + context, + )!.achievementSpeedsterPlatinumDesc, + icon: FontAwesomeIcons.gaugeHigh, + ), + Achievement( + category: AchievementCategory.speedster, + level: AchievementLevel.diamond, + title: AppLocalizations.of(context)!.achievementSpeedsterDiamondTitle, + description: AppLocalizations.of( + context, + )!.achievementSpeedsterDiamondDesc, + icon: FontAwesomeIcons.gaugeHigh, + ), + Achievement( + category: AchievementCategory.speedster, + level: AchievementLevel.master, + title: AppLocalizations.of(context)!.achievementSpeedsterMasterTitle, + description: AppLocalizations.of(context)!.achievementSpeedsterMasterDesc, + icon: FontAwesomeIcons.gaugeHigh, + ), + ]; +} diff --git a/lib/features/achievements/data/model/achievement.dart b/lib/features/achievements/data/model/achievement.dart new file mode 100644 index 0000000..1fd68f9 --- /dev/null +++ b/lib/features/achievements/data/model/achievement.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:quiz_master/utils/enums.dart'; + +class Achievement { + final AchievementCategory category; + final AchievementLevel level; + final String title; + final String description; + final IconData icon; + + const Achievement({ + required this.category, + required this.level, + required this.title, + required this.description, + required this.icon, + }); +} diff --git a/lib/features/achievements/presentation/blocs/achievements_bloc.dart b/lib/features/achievements/presentation/blocs/achievements_bloc.dart new file mode 100644 index 0000000..694d7ba --- /dev/null +++ b/lib/features/achievements/presentation/blocs/achievements_bloc.dart @@ -0,0 +1,230 @@ +import 'package:hydrated_bloc/hydrated_bloc.dart'; +import 'package:quiz_master/utils/enums.dart'; + +part 'achievements_event.dart'; +part 'achievements_state.dart'; + +class AchievementsBloc + extends HydratedBloc { + AchievementsBloc() : super(const AchievementsState()) { + on(_onCheckAchievements); + } + + void _onCheckAchievements( + CheckAchievements event, + Emitter emit, + ) { + final unlocked = Map.from( + state.unlockedLevels, + ); + + final totalQuizzes = state.totalQuizzes + event.quizCount; + final totalCorrectAnswers = + state.totalCorrectAnswers + event.correctAnswers; + final currentLevel = event.level; + final bestQuizTimeSeconds = + event.quizTimeSeconds < state.bestQuizTimeSeconds + ? event.quizTimeSeconds + : state.bestQuizTimeSeconds; + + if (currentLevel >= 5) { + _unlockIfHigher( + unlocked, + AchievementCategory.leveling, + AchievementLevel.bronze, + ); + } + if (currentLevel >= 10) { + _unlockIfHigher( + unlocked, + AchievementCategory.leveling, + AchievementLevel.silver, + ); + } + if (currentLevel >= 20) { + _unlockIfHigher( + unlocked, + AchievementCategory.leveling, + AchievementLevel.gold, + ); + } + if (currentLevel >= 30) { + _unlockIfHigher( + unlocked, + AchievementCategory.leveling, + AchievementLevel.platinum, + ); + } + if (currentLevel >= 50) { + _unlockIfHigher( + unlocked, + AchievementCategory.leveling, + AchievementLevel.diamond, + ); + } + if (currentLevel >= 100) { + _unlockIfHigher( + unlocked, + AchievementCategory.leveling, + AchievementLevel.master, + ); + } + + if (totalCorrectAnswers >= 10) { + _unlockIfHigher( + unlocked, + AchievementCategory.answerMaster, + AchievementLevel.bronze, + ); + } + if (totalCorrectAnswers >= 25) { + _unlockIfHigher( + unlocked, + AchievementCategory.answerMaster, + AchievementLevel.silver, + ); + } + if (totalCorrectAnswers >= 50) { + _unlockIfHigher( + unlocked, + AchievementCategory.answerMaster, + AchievementLevel.gold, + ); + } + if (totalCorrectAnswers >= 100) { + _unlockIfHigher( + unlocked, + AchievementCategory.answerMaster, + AchievementLevel.platinum, + ); + } + if (totalCorrectAnswers >= 200) { + _unlockIfHigher( + unlocked, + AchievementCategory.answerMaster, + AchievementLevel.diamond, + ); + } + if (totalCorrectAnswers >= 500) { + _unlockIfHigher( + unlocked, + AchievementCategory.answerMaster, + AchievementLevel.master, + ); + } + + if (totalQuizzes >= 5) { + _unlockIfHigher( + unlocked, + AchievementCategory.quizAbuser, + AchievementLevel.bronze, + ); + } + if (totalQuizzes >= 10) { + _unlockIfHigher( + unlocked, + AchievementCategory.quizAbuser, + AchievementLevel.silver, + ); + } + if (totalQuizzes >= 25) { + _unlockIfHigher( + unlocked, + AchievementCategory.quizAbuser, + AchievementLevel.gold, + ); + } + if (totalQuizzes >= 50) { + _unlockIfHigher( + unlocked, + AchievementCategory.quizAbuser, + AchievementLevel.platinum, + ); + } + if (totalQuizzes >= 100) { + _unlockIfHigher( + unlocked, + AchievementCategory.quizAbuser, + AchievementLevel.diamond, + ); + } + if (totalQuizzes >= 250) { + _unlockIfHigher( + unlocked, + AchievementCategory.quizAbuser, + AchievementLevel.master, + ); + } + + if (bestQuizTimeSeconds <= 60) { + _unlockIfHigher( + unlocked, + AchievementCategory.speedster, + AchievementLevel.bronze, + ); + } + if (bestQuizTimeSeconds <= 45) { + _unlockIfHigher( + unlocked, + AchievementCategory.speedster, + AchievementLevel.silver, + ); + } + if (bestQuizTimeSeconds <= 30) { + _unlockIfHigher( + unlocked, + AchievementCategory.speedster, + AchievementLevel.gold, + ); + } + if (bestQuizTimeSeconds <= 20) { + _unlockIfHigher( + unlocked, + AchievementCategory.speedster, + AchievementLevel.platinum, + ); + } + if (bestQuizTimeSeconds <= 10) { + _unlockIfHigher( + unlocked, + AchievementCategory.speedster, + AchievementLevel.diamond, + ); + } + if (bestQuizTimeSeconds <= 5) { + _unlockIfHigher( + unlocked, + AchievementCategory.speedster, + AchievementLevel.master, + ); + } + + emit( + state.copyWith( + unlockedLevels: unlocked, + totalQuizzes: totalQuizzes, + totalCorrectAnswers: totalCorrectAnswers, + currentLevel: currentLevel, + bestQuizTimeSeconds: bestQuizTimeSeconds, + ), + ); + } + + void _unlockIfHigher( + Map map, + AchievementCategory category, + AchievementLevel level, + ) { + final current = map[category]; + if (current == null || level.index > current.index) { + map[category] = level; + } + } + + @override + AchievementsState? fromJson(Map json) => + AchievementsState.fromMap(json); + + @override + Map? toJson(AchievementsState state) => state.toMap(); +} diff --git a/lib/features/achievements/presentation/blocs/achievements_event.dart b/lib/features/achievements/presentation/blocs/achievements_event.dart new file mode 100644 index 0000000..cff1b20 --- /dev/null +++ b/lib/features/achievements/presentation/blocs/achievements_event.dart @@ -0,0 +1,17 @@ +part of 'achievements_bloc.dart'; + +abstract class AchievementsEvent {} + +class CheckAchievements extends AchievementsEvent { + final int quizCount; + final int correctAnswers; + final int level; + final int quizTimeSeconds; + + CheckAchievements({ + required this.quizCount, + required this.correctAnswers, + required this.level, + required this.quizTimeSeconds, + }); +} diff --git a/lib/features/achievements/presentation/blocs/achievements_state.dart b/lib/features/achievements/presentation/blocs/achievements_state.dart new file mode 100644 index 0000000..561af63 --- /dev/null +++ b/lib/features/achievements/presentation/blocs/achievements_state.dart @@ -0,0 +1,57 @@ +part of 'achievements_bloc.dart'; + +class AchievementsState { + final Map unlockedLevels; + final int totalQuizzes; + final int totalCorrectAnswers; + final int currentLevel; + final int bestQuizTimeSeconds; + + const AchievementsState({ + this.unlockedLevels = const {}, + this.totalQuizzes = 0, + this.totalCorrectAnswers = 0, + this.currentLevel = 1, + this.bestQuizTimeSeconds = 9999, + }); + + AchievementsState copyWith({ + Map? unlockedLevels, + int? totalQuizzes, + int? totalCorrectAnswers, + int? currentLevel, + int? bestQuizTimeSeconds, + }) { + return AchievementsState( + unlockedLevels: unlockedLevels ?? this.unlockedLevels, + totalQuizzes: totalQuizzes ?? this.totalQuizzes, + totalCorrectAnswers: totalCorrectAnswers ?? this.totalCorrectAnswers, + currentLevel: currentLevel ?? this.currentLevel, + bestQuizTimeSeconds: bestQuizTimeSeconds ?? this.bestQuizTimeSeconds, + ); + } + + Map toMap() => { + 'unlockedLevels': unlockedLevels.map((k, v) => MapEntry(k.name, v.name)), + 'totalQuizzes': totalQuizzes, + 'totalCorrectAnswers': totalCorrectAnswers, + 'currentLevel': currentLevel, + 'bestQuizTimeSeconds': bestQuizTimeSeconds, + }; + + factory AchievementsState.fromMap(Map map) { + final raw = (map['unlockedLevels'] as Map?)?.cast() ?? {}; + final parsed = {}; + for (final e in raw.entries) { + parsed[AchievementCategory.values.byName(e.key)] = AchievementLevel.values + .byName(e.value); + } + return AchievementsState( + unlockedLevels: parsed, + totalQuizzes: map['totalQuizzes'] ?? 0, + totalCorrectAnswers: map['totalCorrectAnswers'] ?? 0, + currentLevel: map['currentLevel'] ?? 1, + bestQuizTimeSeconds: map['bestQuizTimeSeconds'] ?? 9999, + ); + } +} diff --git a/lib/features/achievements/presentation/pages/achievement_page.dart b/lib/features/achievements/presentation/pages/achievement_page.dart index d492439..f038478 100644 --- a/lib/features/achievements/presentation/pages/achievement_page.dart +++ b/lib/features/achievements/presentation/pages/achievement_page.dart @@ -1,10 +1,82 @@ import 'package:flutter/material.dart'; +import 'package:quiz_master/core/presentation/widgets/responsive.dart'; +import 'package:quiz_master/core/presentation/widgets/theme_toggle_button.dart'; +import 'package:quiz_master/features/leveling/presentation/widgets/level_card.dart'; +import 'package:quiz_master/l10n/app_localizations.dart'; +import 'package:quiz_master/utils/helpers.dart'; class AchievementPage extends StatelessWidget { const AchievementPage({super.key}); @override Widget build(BuildContext context) { - return const Placeholder(); + return Scaffold( + appBar: AppBar( + animateColor: true, + title: Padding( + padding: EdgeInsets.only( + left: Responsive.isMobile(context) + ? 0 + : Responsive.isTablet(context) + ? 16 + : MediaQuery.of(context).size.width * 0.15, + ), + child: ShaderMask( + shaderCallback: (bounds) => const LinearGradient( + colors: [Color(0xFFFF6EC7), Color(0xFF9B59B6)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ).createShader(Rect.fromLTWH(0, 0, bounds.width, bounds.height)), + child: Text( + AppLocalizations.of(context)!.appTitle, + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + actionsPadding: EdgeInsets.only( + right: Responsive.isMobile(context) + ? 0 + : Responsive.isTablet(context) + ? 16 + : MediaQuery.of(context).size.width * 0.15, + ), + actions: [ + ThemeToggleButton(), + SizedBox( + width: Responsive.isMobile(context) + ? 6 + : Responsive.isTablet(context) + ? 8 + : 16, + ), + LevelCard(), + ], + ), + body: SizedBox.expand( + child: Container( + decoration: BoxDecoration( + gradient: Helpers.isDarkMode(context) + ? const LinearGradient( + colors: [Color(0xFF000000), Color(0xFF2E1A3F)], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ) + : null, + ), + child: SingleChildScrollView( + padding: EdgeInsets.all(16), + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [], + ), + ), + ), + ), + ), + ); } } diff --git a/lib/features/achievements/presentation/widgets/achievement_overlay.dart b/lib/features/achievements/presentation/widgets/achievement_overlay.dart new file mode 100644 index 0000000..f536b30 --- /dev/null +++ b/lib/features/achievements/presentation/widgets/achievement_overlay.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:quiz_master/core/presentation/widgets/responsive.dart'; +import 'package:quiz_master/features/achievements/data/model/achievement.dart'; +import 'package:quiz_master/features/achievements/presentation/widgets/achievement_widget.dart'; + +class AchievementOverlay { + static final AchievementOverlay _instance = AchievementOverlay._internal(); + factory AchievementOverlay() => _instance; + AchievementOverlay._internal(); + + final List _queue = []; + OverlayEntry? _currentEntry; + + void show(BuildContext context, {required Achievement achievement}) { + _queue.add(achievement); + _showNext(context); + } + + void _showNext(BuildContext context) { + if (_currentEntry != null || _queue.isEmpty) return; + + final achievement = _queue.removeAt(0); + + final isMobile = Responsive.isMobile(context); + final isTablet = Responsive.isTablet(context); + + final bottomOffset = isMobile ? 90.0 : 20.0; + final rightOffset = isMobile + ? 10.0 + : isTablet + ? 30.0 + : 40.0; + + _currentEntry = OverlayEntry( + builder: (context) { + return Positioned( + bottom: bottomOffset, + right: rightOffset, + left: isMobile ? 10.0 : null, + child: SafeArea( + child: Align( + alignment: isMobile + ? Alignment.bottomCenter + : Alignment.bottomRight, + child: AchievementWidget( + achievement: achievement, + onDismiss: () { + _currentEntry?.remove(); + _currentEntry = null; + _showNext(context); + }, + ), + ), + ), + ); + }, + ); + + Overlay.of(context).insert(_currentEntry!); + } +} diff --git a/lib/features/achievements/presentation/widgets/achievement_widget.dart b/lib/features/achievements/presentation/widgets/achievement_widget.dart new file mode 100644 index 0000000..5068ca6 --- /dev/null +++ b/lib/features/achievements/presentation/widgets/achievement_widget.dart @@ -0,0 +1,242 @@ +import 'package:flutter/material.dart'; +import 'package:quiz_master/core/presentation/widgets/responsive.dart'; +import 'package:quiz_master/features/achievements/data/model/achievement.dart'; +import 'package:quiz_master/utils/enums.dart'; + +class AchievementWidget extends StatefulWidget { + final Achievement achievement; + final VoidCallback onDismiss; + + const AchievementWidget({ + super.key, + required this.achievement, + required this.onDismiss, + }); + + @override + State createState() => AchievementWidgetState(); +} + +class AchievementWidgetState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _offsetAnim; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _offsetAnim = Tween( + begin: const Offset(1, 0), + end: Offset.zero, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutBack)); + + _controller.forward(); + + Future.delayed(const Duration(seconds: 4), () { + _controller.reverse().then((_) => widget.onDismiss()); + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final isMobile = Responsive.isMobile(context); + final width = isMobile ? 250.0 : 350.0; + + final levelStyles = { + AchievementLevel.bronze: _LevelStyle( + gradient: [Colors.brown.shade700, Colors.brown.shade400], + border: Colors.orange.shade200, + iconColor: Colors.orange.shade300, + glow: Colors.orange.withAlpha(75), + ), + AchievementLevel.silver: _LevelStyle( + gradient: [Colors.grey.shade800, Colors.grey.shade500], + border: Colors.white54, + iconColor: Colors.white70, + glow: Colors.white.withAlpha(75), + ), + AchievementLevel.gold: _LevelStyle( + gradient: [Color(0xFFB8860B), Color(0xFFFFD700)], + border: Color(0xFFE0C068), + iconColor: Color(0xFFFFE29A), + glow: Color(0x33FFD700), + ), + AchievementLevel.platinum: _LevelStyle( + gradient: [Colors.blueGrey.shade800, Colors.blueGrey.shade500], + border: Colors.cyanAccent, + iconColor: Colors.cyanAccent.shade100, + glow: Colors.cyanAccent.withAlpha(100), + ), + AchievementLevel.diamond: _LevelStyle( + gradient: [Color(0xFF00BCD4), Color(0xFF008BA3)], + border: Color(0xFFB2EBF2), + iconColor: Colors.white70, + glow: Color(0x2200BCD4), + shine: true, + ), + AchievementLevel.master: _LevelStyle( + gradient: [Color(0xFF5D3A7D), Color(0xFF3F1E5C)], + border: Color(0xFFBB86FC), + iconColor: Color(0xFFD1B3FF), + glow: Color(0x33BB86FC), + shine: true, + ), + }; + + final style = levelStyles[widget.achievement.level]!; + + return SlideTransition( + position: _offsetAnim, + child: Stack( + children: [ + if (style.shine) + Positioned.fill( + child: AnimatedBuilder( + animation: _controller, + builder: (context, _) { + final shimmerPosition = (_controller.value * 2 - 1).clamp( + 0.0, + 1.0, + ); + return ShaderMask( + shaderCallback: (rect) { + return LinearGradient( + colors: [ + Colors.white.withAlpha(0), + Colors.white.withAlpha(90), + Colors.white.withAlpha(0), + ], + stops: [ + shimmerPosition - 0.3, + shimmerPosition, + shimmerPosition + 0.3, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ).createShader(rect); + }, + blendMode: BlendMode.srcATop, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: LinearGradient( + colors: style.gradient, + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + ), + ); + }, + ), + ), + Material( + elevation: 10, + borderRadius: BorderRadius.circular(16), + color: Colors.transparent, + child: Container( + width: width, + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: LinearGradient( + colors: style.gradient, + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + boxShadow: [ + BoxShadow( + color: style.glow, + blurRadius: 20, + spreadRadius: 2, + offset: const Offset(0, 6), + ), + ], + border: Border.all(color: style.border, width: 1.8), + ), + child: Row( + children: [ + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: style.border, width: 2), + gradient: LinearGradient( + colors: [ + Colors.white.withAlpha(50), + Colors.white.withAlpha(8), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + padding: const EdgeInsets.all(10), + child: Icon( + widget.achievement.icon, + color: style.iconColor, + size: 32, + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.achievement.title.toUpperCase(), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w800, + fontSize: 15, + letterSpacing: 1, + shadows: [ + Shadow(blurRadius: 6, color: Colors.black54), + ], + ), + ), + const SizedBox(height: 6), + Text( + widget.achievement.description, + style: const TextStyle( + color: Colors.white70, + fontSize: 13, + height: 1.3, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _LevelStyle { + final List gradient; + final Color border; + final Color iconColor; + final Color glow; + final bool shine; + + _LevelStyle({ + required this.gradient, + required this.border, + required this.iconColor, + required this.glow, + this.shine = false, + }); +} diff --git a/lib/features/quiz/presentation/widgets/quiz_summary.dart b/lib/features/quiz/presentation/widgets/quiz_summary.dart index 7e86118..f4b83c7 100644 --- a/lib/features/quiz/presentation/widgets/quiz_summary.dart +++ b/lib/features/quiz/presentation/widgets/quiz_summary.dart @@ -1,5 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:quiz_master/di.dart'; +import 'package:quiz_master/features/achievements/data/constants/achievements_list.dart'; +import 'package:quiz_master/features/achievements/presentation/blocs/achievements_bloc.dart'; import 'package:quiz_master/features/leveling/data/constants/difficulties.dart'; import 'package:quiz_master/features/leveling/presentation/pages/leveling_difficulty_page.dart'; import 'package:quiz_master/features/quiz/domain/entity/question.dart'; @@ -7,6 +11,7 @@ import 'package:quiz_master/features/quiz/presentation/widgets/home_button.dart' import 'package:quiz_master/features/quiz/presentation/widgets/quiz_answer_review.dart'; import 'package:quiz_master/features/quiz/presentation/widgets/quiz_summary_stats.dart'; import 'package:quiz_master/l10n/app_localizations.dart'; +import 'package:quiz_master/utils/enums.dart'; import 'package:quiz_master/utils/helpers.dart'; class QuizSummary extends StatefulWidget { @@ -61,61 +66,85 @@ class _QuizSummaryState extends State ? widget.score / widget.totalQuestions : 0; - return SizedBox.expand( - child: SingleChildScrollView( - child: Column( - children: [ - const SizedBox(height: 48), - _buildAnimatedCard(context, percentage), - const SizedBox(height: 24), - QuizSummaryStats( - score: widget.score, - difficulty: widget.difficulty, - totalQuestions: widget.totalQuestions, - totalTime: widget.totalTime, - answerTimes: widget.answerTimes, - ), - const SizedBox(height: 24), - QuizAnswerReview( - questions: widget.questions, - userAnswers: widget.userAnswers, - ), - const SizedBox(height: 24), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.deepPurpleAccent, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric( - horizontal: 48, - vertical: 24, + Map previousUnlockedLevels = + sl().state.unlockedLevels; + + return BlocListener( + bloc: sl(), + listener: (achievementsContext, achievementsState) { + achievementsState.unlockedLevels.forEach((category, newLevel) { + final oldLevel = previousUnlockedLevels[category]; + + if (oldLevel == null || newLevel.index > oldLevel.index) { + final achievement = getAchievements(context).firstWhere( + (a) => + a.category.index == category.index && + a.level.index == newLevel.index, + orElse: () => throw Exception('Achievement not found'), + ); + + Helpers.showAchievementDialog(context, achievement: achievement); + } + }); + + previousUnlockedLevels = Map.from(achievementsState.unlockedLevels); + }, + child: SizedBox.expand( + child: SingleChildScrollView( + child: Column( + children: [ + const SizedBox(height: 48), + _buildAnimatedCard(context, percentage), + const SizedBox(height: 24), + QuizSummaryStats( + score: widget.score, + difficulty: widget.difficulty, + totalQuestions: widget.totalQuestions, + totalTime: widget.totalTime, + answerTimes: widget.answerTimes, + ), + const SizedBox(height: 24), + QuizAnswerReview( + questions: widget.questions, + userAnswers: widget.userAnswers, + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.deepPurpleAccent, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 48, + vertical: 24, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + icon: const FaIcon(FontAwesomeIcons.rotateLeft), + label: Text( + AppLocalizations.of(context)!.playAgain, + style: const TextStyle(fontSize: 14), ), + onPressed: () { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => LevelingDifficultyPage(), + ), + ); + }, ), - icon: const FaIcon(FontAwesomeIcons.rotateLeft), - label: Text( - AppLocalizations.of(context)!.playAgain, - style: const TextStyle(fontSize: 14), - ), - onPressed: () { - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (_) => LevelingDifficultyPage(), - ), - ); - }, - ), - const SizedBox(width: 16), - HomeButton(), - ], - ), - const SizedBox(height: 24), - ], + const SizedBox(width: 16), + HomeButton(), + ], + ), + const SizedBox(height: 24), + ], + ), ), ), ); diff --git a/lib/features/quiz/presentation/widgets/quiz_summary_stats.dart b/lib/features/quiz/presentation/widgets/quiz_summary_stats.dart index 2a001b8..25255cd 100644 --- a/lib/features/quiz/presentation/widgets/quiz_summary_stats.dart +++ b/lib/features/quiz/presentation/widgets/quiz_summary_stats.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:quiz_master/core/presentation/widgets/responsive.dart'; import 'package:quiz_master/di.dart'; +import 'package:quiz_master/features/achievements/presentation/blocs/achievements_bloc.dart'; import 'package:quiz_master/features/leveling/data/constants/difficulties.dart'; import 'package:quiz_master/features/leveling/presentation/blocs/user_level/user_level_bloc.dart'; import 'package:quiz_master/l10n/app_localizations.dart'; @@ -41,6 +42,15 @@ class QuizSummaryStats extends StatelessWidget { ); sl().add(AddXp(experienceEarned.toInt())); + final currentLevel = sl().state.level; + sl().add( + CheckAchievements( + quizCount: accuracy >= 80 ? 1 : 0, + correctAnswers: score, + level: currentLevel, + quizTimeSeconds: accuracy >= 80 ? totalTime.inSeconds : 9999, + ), + ); Widget buildStatTile( IconData icon, @@ -109,12 +119,6 @@ class QuizSummaryStats extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // buildStatTile( - // FontAwesomeIcons.circleCheck, - // AppLocalizations.of(context)!.accuracy, - // "$accuracy%", - // Colors.deepPurpleAccent, - // ), buildStatTile( FontAwesomeIcons.clock, AppLocalizations.of(context)!.totalTime, @@ -127,6 +131,22 @@ class QuizSummaryStats extends StatelessWidget { "${avgTimePerQuestion}s", Colors.teal, ), + buildStatTile( + FontAwesomeIcons.solidStar, + AppLocalizations.of(context)!.experienceEarned, + experienceEarned.toStringAsFixed(0), + Colors.orangeAccent, + ), + buildStatTile( + FontAwesomeIcons.bolt, + AppLocalizations.of(context)!.difficulty, + difficulty.title, + difficulty.apiValue == 'easy' + ? Colors.green + : difficulty.apiValue == 'medium' + ? Colors.yellow + : Colors.red, + ), ], ), ), @@ -135,14 +155,6 @@ class QuizSummaryStats extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - // Expanded( - // child: buildStatTile( - // FontAwesomeIcons.circleCheck, - // AppLocalizations.of(context)!.accuracy, - // "$accuracy%", - // Colors.deepPurpleAccent, - // ), - // ), Expanded( child: buildStatTile( FontAwesomeIcons.clock, @@ -159,6 +171,26 @@ class QuizSummaryStats extends StatelessWidget { Colors.teal, ), ), + Expanded( + child: buildStatTile( + FontAwesomeIcons.solidStar, + AppLocalizations.of(context)!.experienceEarned, + experienceEarned.toStringAsFixed(0), + Colors.orangeAccent, + ), + ), + Expanded( + child: buildStatTile( + FontAwesomeIcons.bolt, + AppLocalizations.of(context)!.difficulty, + difficulty.title, + difficulty.apiValue == 'easy' + ? Colors.green + : difficulty.apiValue == 'medium' + ? Colors.yellow + : Colors.red, + ), + ), ], ), ), diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 9cd5a9c..b000f37 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -102,5 +102,53 @@ "descCoolTechGadgets": "Test your knowledge of modern inventions and gadgets.", "descAnimeWorlds": "Explore anime worlds, characters, and legendary series.", "descCartoonsAnimations": "From Disney to Pixar — classic and modern animation trivia.", - "browseNoCoursesFound": "No courses found matching your search." + "browseNoCoursesFound": "No courses found matching your search.", + "achievementLevelingBronzeTitle": "Leveling Apprentice", + "achievementLevelingBronzeDesc": "Reach Level 5", + "achievementLevelingSilverTitle": "Leveling Adept", + "achievementLevelingSilverDesc": "Reach Level 10", + "achievementLevelingGoldTitle": "Leveling Expert", + "achievementLevelingGoldDesc": "Reach Level 20", + "achievementLevelingPlatinumTitle": "Leveling Master", + "achievementLevelingPlatinumDesc": "Reach Level 30", + "achievementLevelingDiamondTitle": "Leveling Legend", + "achievementLevelingDiamondDesc": "Reach Level 50", + "achievementLevelingMasterTitle": "Leveling Grandmaster", + "achievementLevelingMasterDesc": "Reach Level 100", + "achievementAnswerMasterBronzeTitle": "Answer Apprentice", + "achievementAnswerMasterBronzeDesc": "Answer 10 Questions Correctly", + "achievementAnswerMasterSilverTitle": "Answer Adept", + "achievementAnswerMasterSilverDesc": "Answer 25 Questions Correctly", + "achievementAnswerMasterGoldTitle": "Answer Expert", + "achievementAnswerMasterGoldDesc": "Answer 50 Questions Correctly", + "achievementAnswerMasterPlatinumTitle": "Answer Master", + "achievementAnswerMasterPlatinumDesc": "Answer 100 Questions Correctly", + "achievementAnswerMasterDiamondTitle": "Answer Legend", + "achievementAnswerMasterDiamondDesc": "Answer 200 Questions Correctly", + "achievementAnswerMasterMasterTitle": "Answer Grandmaster", + "achievementAnswerMasterMasterDesc": "Answer 500 Questions Correctly", + "achievementQuizAbuserBronzeTitle": "Quiz Novice", + "achievementQuizAbuserBronzeDesc": "Complete 5 quizzes with 80%+ score", + "achievementQuizAbuserSilverTitle": "Quiz Enthusiast", + "achievementQuizAbuserSilverDesc": "Complete 10 quizzes with 80%+ score", + "achievementQuizAbuserGoldTitle": "Quiz Fanatic", + "achievementQuizAbuserGoldDesc": "Complete 25 quizzes with 80%+ score", + "achievementQuizAbuserPlatinumTitle": "Quiz Maniac", + "achievementQuizAbuserPlatinumDesc": "Complete 50 quizzes with 80%+ score", + "achievementQuizAbuserDiamondTitle": "Quiz Legend", + "achievementQuizAbuserDiamondDesc": "Complete 100 quizzes with 80%+ score", + "achievementQuizAbuserMasterTitle": "Quiz Grandmaster", + "achievementQuizAbuserMasterDesc": "Complete 250 quizzes with 80%+ score", + "achievementSpeedsterBronzeTitle": "Speedster Novice", + "achievementSpeedsterBronzeDesc": "Complete a quiz under 60 seconds with 80%+ score", + "achievementSpeedsterSilverTitle": "Speedster Adept", + "achievementSpeedsterSilverDesc": "Complete a quiz under 45 seconds with 80%+ score", + "achievementSpeedsterGoldTitle": "Speedster Export", + "achievementSpeedsterGoldDesc": "Complete a quiz under 30 seconds with 80%+ score", + "achievementSpeedsterPlatinumTitle": "Speedster Master", + "achievementSpeedsterPlatinumDesc": "Complete a quiz under 20 seconds with 80%+ score", + "achievementSpeedsterDiamondTitle": "Speedster Legend", + "achievementSpeedsterDiamondDesc": "Complete a quiz under 10 seconds with 80%+ score", + "achievementSpeedsterMasterTitle": "Speedster Grandmaster", + "achievementSpeedsterMasterDesc": "Complete a quiz under 5 seconds with 80%+ score" } \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 7b91e8e..8d3b505 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -717,6 +717,294 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'No courses found matching your search.'** String get browseNoCoursesFound; + + /// No description provided for @achievementLevelingBronzeTitle. + /// + /// In en, this message translates to: + /// **'Leveling Apprentice'** + String get achievementLevelingBronzeTitle; + + /// No description provided for @achievementLevelingBronzeDesc. + /// + /// In en, this message translates to: + /// **'Reach Level 5'** + String get achievementLevelingBronzeDesc; + + /// No description provided for @achievementLevelingSilverTitle. + /// + /// In en, this message translates to: + /// **'Leveling Adept'** + String get achievementLevelingSilverTitle; + + /// No description provided for @achievementLevelingSilverDesc. + /// + /// In en, this message translates to: + /// **'Reach Level 10'** + String get achievementLevelingSilverDesc; + + /// No description provided for @achievementLevelingGoldTitle. + /// + /// In en, this message translates to: + /// **'Leveling Expert'** + String get achievementLevelingGoldTitle; + + /// No description provided for @achievementLevelingGoldDesc. + /// + /// In en, this message translates to: + /// **'Reach Level 20'** + String get achievementLevelingGoldDesc; + + /// No description provided for @achievementLevelingPlatinumTitle. + /// + /// In en, this message translates to: + /// **'Leveling Master'** + String get achievementLevelingPlatinumTitle; + + /// No description provided for @achievementLevelingPlatinumDesc. + /// + /// In en, this message translates to: + /// **'Reach Level 30'** + String get achievementLevelingPlatinumDesc; + + /// No description provided for @achievementLevelingDiamondTitle. + /// + /// In en, this message translates to: + /// **'Leveling Legend'** + String get achievementLevelingDiamondTitle; + + /// No description provided for @achievementLevelingDiamondDesc. + /// + /// In en, this message translates to: + /// **'Reach Level 50'** + String get achievementLevelingDiamondDesc; + + /// No description provided for @achievementLevelingMasterTitle. + /// + /// In en, this message translates to: + /// **'Leveling Grandmaster'** + String get achievementLevelingMasterTitle; + + /// No description provided for @achievementLevelingMasterDesc. + /// + /// In en, this message translates to: + /// **'Reach Level 100'** + String get achievementLevelingMasterDesc; + + /// No description provided for @achievementAnswerMasterBronzeTitle. + /// + /// In en, this message translates to: + /// **'Answer Apprentice'** + String get achievementAnswerMasterBronzeTitle; + + /// No description provided for @achievementAnswerMasterBronzeDesc. + /// + /// In en, this message translates to: + /// **'Answer 10 Questions Correctly'** + String get achievementAnswerMasterBronzeDesc; + + /// No description provided for @achievementAnswerMasterSilverTitle. + /// + /// In en, this message translates to: + /// **'Answer Adept'** + String get achievementAnswerMasterSilverTitle; + + /// No description provided for @achievementAnswerMasterSilverDesc. + /// + /// In en, this message translates to: + /// **'Answer 25 Questions Correctly'** + String get achievementAnswerMasterSilverDesc; + + /// No description provided for @achievementAnswerMasterGoldTitle. + /// + /// In en, this message translates to: + /// **'Answer Expert'** + String get achievementAnswerMasterGoldTitle; + + /// No description provided for @achievementAnswerMasterGoldDesc. + /// + /// In en, this message translates to: + /// **'Answer 50 Questions Correctly'** + String get achievementAnswerMasterGoldDesc; + + /// No description provided for @achievementAnswerMasterPlatinumTitle. + /// + /// In en, this message translates to: + /// **'Answer Master'** + String get achievementAnswerMasterPlatinumTitle; + + /// No description provided for @achievementAnswerMasterPlatinumDesc. + /// + /// In en, this message translates to: + /// **'Answer 100 Questions Correctly'** + String get achievementAnswerMasterPlatinumDesc; + + /// No description provided for @achievementAnswerMasterDiamondTitle. + /// + /// In en, this message translates to: + /// **'Answer Legend'** + String get achievementAnswerMasterDiamondTitle; + + /// No description provided for @achievementAnswerMasterDiamondDesc. + /// + /// In en, this message translates to: + /// **'Answer 200 Questions Correctly'** + String get achievementAnswerMasterDiamondDesc; + + /// No description provided for @achievementAnswerMasterMasterTitle. + /// + /// In en, this message translates to: + /// **'Answer Grandmaster'** + String get achievementAnswerMasterMasterTitle; + + /// No description provided for @achievementAnswerMasterMasterDesc. + /// + /// In en, this message translates to: + /// **'Answer 500 Questions Correctly'** + String get achievementAnswerMasterMasterDesc; + + /// No description provided for @achievementQuizAbuserBronzeTitle. + /// + /// In en, this message translates to: + /// **'Quiz Novice'** + String get achievementQuizAbuserBronzeTitle; + + /// No description provided for @achievementQuizAbuserBronzeDesc. + /// + /// In en, this message translates to: + /// **'Complete 5 quizzes with 80%+ score'** + String get achievementQuizAbuserBronzeDesc; + + /// No description provided for @achievementQuizAbuserSilverTitle. + /// + /// In en, this message translates to: + /// **'Quiz Enthusiast'** + String get achievementQuizAbuserSilverTitle; + + /// No description provided for @achievementQuizAbuserSilverDesc. + /// + /// In en, this message translates to: + /// **'Complete 10 quizzes with 80%+ score'** + String get achievementQuizAbuserSilverDesc; + + /// No description provided for @achievementQuizAbuserGoldTitle. + /// + /// In en, this message translates to: + /// **'Quiz Fanatic'** + String get achievementQuizAbuserGoldTitle; + + /// No description provided for @achievementQuizAbuserGoldDesc. + /// + /// In en, this message translates to: + /// **'Complete 25 quizzes with 80%+ score'** + String get achievementQuizAbuserGoldDesc; + + /// No description provided for @achievementQuizAbuserPlatinumTitle. + /// + /// In en, this message translates to: + /// **'Quiz Maniac'** + String get achievementQuizAbuserPlatinumTitle; + + /// No description provided for @achievementQuizAbuserPlatinumDesc. + /// + /// In en, this message translates to: + /// **'Complete 50 quizzes with 80%+ score'** + String get achievementQuizAbuserPlatinumDesc; + + /// No description provided for @achievementQuizAbuserDiamondTitle. + /// + /// In en, this message translates to: + /// **'Quiz Legend'** + String get achievementQuizAbuserDiamondTitle; + + /// No description provided for @achievementQuizAbuserDiamondDesc. + /// + /// In en, this message translates to: + /// **'Complete 100 quizzes with 80%+ score'** + String get achievementQuizAbuserDiamondDesc; + + /// No description provided for @achievementQuizAbuserMasterTitle. + /// + /// In en, this message translates to: + /// **'Quiz Grandmaster'** + String get achievementQuizAbuserMasterTitle; + + /// No description provided for @achievementQuizAbuserMasterDesc. + /// + /// In en, this message translates to: + /// **'Complete 250 quizzes with 80%+ score'** + String get achievementQuizAbuserMasterDesc; + + /// No description provided for @achievementSpeedsterBronzeTitle. + /// + /// In en, this message translates to: + /// **'Speedster Novice'** + String get achievementSpeedsterBronzeTitle; + + /// No description provided for @achievementSpeedsterBronzeDesc. + /// + /// In en, this message translates to: + /// **'Complete a quiz under 60 seconds with 80%+ score'** + String get achievementSpeedsterBronzeDesc; + + /// No description provided for @achievementSpeedsterSilverTitle. + /// + /// In en, this message translates to: + /// **'Speedster Adept'** + String get achievementSpeedsterSilverTitle; + + /// No description provided for @achievementSpeedsterSilverDesc. + /// + /// In en, this message translates to: + /// **'Complete a quiz under 45 seconds with 80%+ score'** + String get achievementSpeedsterSilverDesc; + + /// No description provided for @achievementSpeedsterGoldTitle. + /// + /// In en, this message translates to: + /// **'Speedster Export'** + String get achievementSpeedsterGoldTitle; + + /// No description provided for @achievementSpeedsterGoldDesc. + /// + /// In en, this message translates to: + /// **'Complete a quiz under 30 seconds with 80%+ score'** + String get achievementSpeedsterGoldDesc; + + /// No description provided for @achievementSpeedsterPlatinumTitle. + /// + /// In en, this message translates to: + /// **'Speedster Master'** + String get achievementSpeedsterPlatinumTitle; + + /// No description provided for @achievementSpeedsterPlatinumDesc. + /// + /// In en, this message translates to: + /// **'Complete a quiz under 20 seconds with 80%+ score'** + String get achievementSpeedsterPlatinumDesc; + + /// No description provided for @achievementSpeedsterDiamondTitle. + /// + /// In en, this message translates to: + /// **'Speedster Legend'** + String get achievementSpeedsterDiamondTitle; + + /// No description provided for @achievementSpeedsterDiamondDesc. + /// + /// In en, this message translates to: + /// **'Complete a quiz under 10 seconds with 80%+ score'** + String get achievementSpeedsterDiamondDesc; + + /// No description provided for @achievementSpeedsterMasterTitle. + /// + /// In en, this message translates to: + /// **'Speedster Grandmaster'** + String get achievementSpeedsterMasterTitle; + + /// No description provided for @achievementSpeedsterMasterDesc. + /// + /// In en, this message translates to: + /// **'Complete a quiz under 5 seconds with 80%+ score'** + String get achievementSpeedsterMasterDesc; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 49fd06c..115082a 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -350,4 +350,165 @@ class AppLocalizationsEn extends AppLocalizations { @override String get browseNoCoursesFound => 'No courses found matching your search.'; + + @override + String get achievementLevelingBronzeTitle => 'Leveling Apprentice'; + + @override + String get achievementLevelingBronzeDesc => 'Reach Level 5'; + + @override + String get achievementLevelingSilverTitle => 'Leveling Adept'; + + @override + String get achievementLevelingSilverDesc => 'Reach Level 10'; + + @override + String get achievementLevelingGoldTitle => 'Leveling Expert'; + + @override + String get achievementLevelingGoldDesc => 'Reach Level 20'; + + @override + String get achievementLevelingPlatinumTitle => 'Leveling Master'; + + @override + String get achievementLevelingPlatinumDesc => 'Reach Level 30'; + + @override + String get achievementLevelingDiamondTitle => 'Leveling Legend'; + + @override + String get achievementLevelingDiamondDesc => 'Reach Level 50'; + + @override + String get achievementLevelingMasterTitle => 'Leveling Grandmaster'; + + @override + String get achievementLevelingMasterDesc => 'Reach Level 100'; + + @override + String get achievementAnswerMasterBronzeTitle => 'Answer Apprentice'; + + @override + String get achievementAnswerMasterBronzeDesc => + 'Answer 10 Questions Correctly'; + + @override + String get achievementAnswerMasterSilverTitle => 'Answer Adept'; + + @override + String get achievementAnswerMasterSilverDesc => + 'Answer 25 Questions Correctly'; + + @override + String get achievementAnswerMasterGoldTitle => 'Answer Expert'; + + @override + String get achievementAnswerMasterGoldDesc => 'Answer 50 Questions Correctly'; + + @override + String get achievementAnswerMasterPlatinumTitle => 'Answer Master'; + + @override + String get achievementAnswerMasterPlatinumDesc => + 'Answer 100 Questions Correctly'; + + @override + String get achievementAnswerMasterDiamondTitle => 'Answer Legend'; + + @override + String get achievementAnswerMasterDiamondDesc => + 'Answer 200 Questions Correctly'; + + @override + String get achievementAnswerMasterMasterTitle => 'Answer Grandmaster'; + + @override + String get achievementAnswerMasterMasterDesc => + 'Answer 500 Questions Correctly'; + + @override + String get achievementQuizAbuserBronzeTitle => 'Quiz Novice'; + + @override + String get achievementQuizAbuserBronzeDesc => + 'Complete 5 quizzes with 80%+ score'; + + @override + String get achievementQuizAbuserSilverTitle => 'Quiz Enthusiast'; + + @override + String get achievementQuizAbuserSilverDesc => + 'Complete 10 quizzes with 80%+ score'; + + @override + String get achievementQuizAbuserGoldTitle => 'Quiz Fanatic'; + + @override + String get achievementQuizAbuserGoldDesc => + 'Complete 25 quizzes with 80%+ score'; + + @override + String get achievementQuizAbuserPlatinumTitle => 'Quiz Maniac'; + + @override + String get achievementQuizAbuserPlatinumDesc => + 'Complete 50 quizzes with 80%+ score'; + + @override + String get achievementQuizAbuserDiamondTitle => 'Quiz Legend'; + + @override + String get achievementQuizAbuserDiamondDesc => + 'Complete 100 quizzes with 80%+ score'; + + @override + String get achievementQuizAbuserMasterTitle => 'Quiz Grandmaster'; + + @override + String get achievementQuizAbuserMasterDesc => + 'Complete 250 quizzes with 80%+ score'; + + @override + String get achievementSpeedsterBronzeTitle => 'Speedster Novice'; + + @override + String get achievementSpeedsterBronzeDesc => + 'Complete a quiz under 60 seconds with 80%+ score'; + + @override + String get achievementSpeedsterSilverTitle => 'Speedster Adept'; + + @override + String get achievementSpeedsterSilverDesc => + 'Complete a quiz under 45 seconds with 80%+ score'; + + @override + String get achievementSpeedsterGoldTitle => 'Speedster Export'; + + @override + String get achievementSpeedsterGoldDesc => + 'Complete a quiz under 30 seconds with 80%+ score'; + + @override + String get achievementSpeedsterPlatinumTitle => 'Speedster Master'; + + @override + String get achievementSpeedsterPlatinumDesc => + 'Complete a quiz under 20 seconds with 80%+ score'; + + @override + String get achievementSpeedsterDiamondTitle => 'Speedster Legend'; + + @override + String get achievementSpeedsterDiamondDesc => + 'Complete a quiz under 10 seconds with 80%+ score'; + + @override + String get achievementSpeedsterMasterTitle => 'Speedster Grandmaster'; + + @override + String get achievementSpeedsterMasterDesc => + 'Complete a quiz under 5 seconds with 80%+ score'; } diff --git a/lib/main_screen.dart b/lib/main_screen.dart index 9797adb..e71509b 100644 --- a/lib/main_screen.dart +++ b/lib/main_screen.dart @@ -3,6 +3,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:quiz_master/core/presentation/blocs/page_index_cubit.dart'; import 'package:quiz_master/core/presentation/widgets/responsive.dart'; import 'package:quiz_master/di.dart'; +import 'package:quiz_master/features/achievements/presentation/pages/achievement_page.dart'; import 'package:quiz_master/features/browse/presentation/pages/browse_page.dart'; import 'package:quiz_master/features/home/presentation/pages/home_page.dart'; @@ -14,7 +15,7 @@ class MainScreen extends StatelessWidget { @override Widget build(BuildContext context) { - final pages = const [HomePage(), BrowsePage(), HomePage()]; + final pages = const [HomePage(), BrowsePage(), AchievementPage()]; return Responsive( mobile: BlocBuilder( diff --git a/lib/utils/enums.dart b/lib/utils/enums.dart new file mode 100644 index 0000000..7b59fda --- /dev/null +++ b/lib/utils/enums.dart @@ -0,0 +1,3 @@ +enum AchievementCategory { leveling, answerMaster, quizAbuser, speedster } + +enum AchievementLevel { bronze, silver, gold, platinum, diamond, master } diff --git a/lib/utils/helpers.dart b/lib/utils/helpers.dart index cc99587..3a4573b 100644 --- a/lib/utils/helpers.dart +++ b/lib/utils/helpers.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:quiz_master/features/achievements/data/model/achievement.dart'; +import 'package:quiz_master/features/achievements/presentation/widgets/achievement_overlay.dart'; import 'package:quiz_master/l10n/app_localizations.dart'; class Helpers { @@ -40,4 +42,11 @@ class Helpers { return xp.roundToDouble(); } + + static void showAchievementDialog( + BuildContext context, { + required Achievement achievement, + }) { + AchievementOverlay().show(context, achievement: achievement); + } } From db2dffd24b5e43408534aac305a7a59268927763 Mon Sep 17 00:00:00 2001 From: Adam Dybcio Date: Sun, 5 Oct 2025 20:08:00 +0200 Subject: [PATCH 2/2] feat: Enhance achievements system with new categories, descriptions, and UI components --- .../data/constants/achievements_list.dart | 16 ++ .../presentation/blocs/achievements_bloc.dart | 10 +- .../presentation/pages/achievement_page.dart | 8 +- .../widgets/achievement_card.dart | 208 ++++++++++++++++++ .../widgets/achievements_content.dart | 189 ++++++++++++++++ .../widgets/achievements_grid.dart | 58 +++++ lib/l10n/app_en.arb | 20 +- lib/l10n/app_localizations.dart | 70 +++++- lib/l10n/app_localizations_en.dart | 44 +++- 9 files changed, 602 insertions(+), 21 deletions(-) create mode 100644 lib/features/achievements/presentation/widgets/achievement_card.dart create mode 100644 lib/features/achievements/presentation/widgets/achievements_content.dart create mode 100644 lib/features/achievements/presentation/widgets/achievements_grid.dart diff --git a/lib/features/achievements/data/constants/achievements_list.dart b/lib/features/achievements/data/constants/achievements_list.dart index 5de5035..f74d72a 100644 --- a/lib/features/achievements/data/constants/achievements_list.dart +++ b/lib/features/achievements/data/constants/achievements_list.dart @@ -204,3 +204,19 @@ List getAchievements(BuildContext context) { ), ]; } + +String getAchievementCategoryDescription( + BuildContext context, + AchievementCategory category, +) { + switch (category) { + case AchievementCategory.leveling: + return AppLocalizations.of(context)!.achievementCategoryLevelingDesc; + case AchievementCategory.answerMaster: + return AppLocalizations.of(context)!.achievementCategoryAnswerMasterDesc; + case AchievementCategory.quizAbuser: + return AppLocalizations.of(context)!.achievementCategoryQuizAbuserDesc; + case AchievementCategory.speedster: + return AppLocalizations.of(context)!.achievementCategorySpeedsterDesc; + } +} diff --git a/lib/features/achievements/presentation/blocs/achievements_bloc.dart b/lib/features/achievements/presentation/blocs/achievements_bloc.dart index 694d7ba..c97c497 100644 --- a/lib/features/achievements/presentation/blocs/achievements_bloc.dart +++ b/lib/features/achievements/presentation/blocs/achievements_bloc.dart @@ -163,35 +163,35 @@ class AchievementsBloc AchievementLevel.bronze, ); } - if (bestQuizTimeSeconds <= 45) { + if (bestQuizTimeSeconds <= 50) { _unlockIfHigher( unlocked, AchievementCategory.speedster, AchievementLevel.silver, ); } - if (bestQuizTimeSeconds <= 30) { + if (bestQuizTimeSeconds <= 40) { _unlockIfHigher( unlocked, AchievementCategory.speedster, AchievementLevel.gold, ); } - if (bestQuizTimeSeconds <= 20) { + if (bestQuizTimeSeconds <= 30) { _unlockIfHigher( unlocked, AchievementCategory.speedster, AchievementLevel.platinum, ); } - if (bestQuizTimeSeconds <= 10) { + if (bestQuizTimeSeconds <= 25) { _unlockIfHigher( unlocked, AchievementCategory.speedster, AchievementLevel.diamond, ); } - if (bestQuizTimeSeconds <= 5) { + if (bestQuizTimeSeconds <= 20) { _unlockIfHigher( unlocked, AchievementCategory.speedster, diff --git a/lib/features/achievements/presentation/pages/achievement_page.dart b/lib/features/achievements/presentation/pages/achievement_page.dart index f038478..5271539 100644 --- a/lib/features/achievements/presentation/pages/achievement_page.dart +++ b/lib/features/achievements/presentation/pages/achievement_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:quiz_master/core/presentation/widgets/responsive.dart'; import 'package:quiz_master/core/presentation/widgets/theme_toggle_button.dart'; +import 'package:quiz_master/features/achievements/presentation/widgets/achievements_content.dart'; import 'package:quiz_master/features/leveling/presentation/widgets/level_card.dart'; import 'package:quiz_master/l10n/app_localizations.dart'; import 'package:quiz_master/utils/helpers.dart'; @@ -71,7 +72,12 @@ class AchievementPage extends StatelessWidget { child: Center( child: Column( crossAxisAlignment: CrossAxisAlignment.center, - children: [], + children: [ + ConstrainedBox( + constraints: BoxConstraints(maxWidth: 900), + child: const AchievementsContent(), + ), + ], ), ), ), diff --git a/lib/features/achievements/presentation/widgets/achievement_card.dart b/lib/features/achievements/presentation/widgets/achievement_card.dart new file mode 100644 index 0000000..4471972 --- /dev/null +++ b/lib/features/achievements/presentation/widgets/achievement_card.dart @@ -0,0 +1,208 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:quiz_master/features/achievements/data/constants/achievements_list.dart'; +import 'package:quiz_master/l10n/app_localizations.dart'; +import 'package:quiz_master/utils/enums.dart'; +import 'package:quiz_master/utils/helpers.dart'; + +class AchievementCard extends StatefulWidget { + final AchievementCategory category; + final Set unlockedLevels; + final Duration delay; + + const AchievementCard({ + super.key, + required this.category, + required this.unlockedLevels, + required this.delay, + }); + + @override + State createState() => _AchievementCardState(); +} + +class _AchievementCardState extends State { + bool _visible = false; + + @override + void initState() { + super.initState(); + Future.delayed(widget.delay, () { + if (mounted) setState(() => _visible = true); + }); + } + + Color _medalColor(AchievementLevel level) { + switch (level) { + case AchievementLevel.bronze: + return Colors.brown; + case AchievementLevel.silver: + return Colors.grey; + case AchievementLevel.gold: + return Colors.amber.shade700; + case AchievementLevel.platinum: + return Colors.blueGrey; + case AchievementLevel.diamond: + return Colors.cyan.shade700; + case AchievementLevel.master: + return Colors.purple.shade700; + } + } + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + + final allAchievements = + getAchievements( + context, + ).where((a) => a.category == widget.category).toList() + ..sort((a, b) => a.level.index.compareTo(b.level.index)); + + final achievementsMap = {for (var a in allAchievements) a.level: a}; + + AchievementLevel? bestLevel; + for (var level in AchievementLevel.values.reversed) { + if (widget.unlockedLevels.contains(level)) { + bestLevel = level; + break; + } + } + + final String? currentAchievementName = + bestLevel != null && achievementsMap[bestLevel] != null + ? achievementsMap[bestLevel]!.title + : null; + + AchievementLevel? nextLevel; + for (var level in AchievementLevel.values) { + if (!widget.unlockedLevels.contains(level)) { + nextLevel = level; + break; + } + } + + return AnimatedSlide( + offset: _visible ? Offset.zero : const Offset(0, 0.2), + duration: const Duration(milliseconds: 500), + curve: Curves.easeOutCubic, + child: AnimatedOpacity( + opacity: _visible ? 1 : 0, + duration: const Duration(milliseconds: 500), + curve: Curves.easeOut, + child: Container( + padding: const EdgeInsets.all(16), + margin: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Helpers.isDarkMode(context) + ? Colors.grey.shade900 + : Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: bestLevel != null + ? _medalColor(bestLevel).withAlpha(100) + : Colors.black12, + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + border: Border.all( + color: bestLevel != null + ? _medalColor(bestLevel) + : Helpers.isDarkMode(context) + ? Colors.white12 + : Colors.black12, + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + currentAchievementName ?? + AppLocalizations.of(context)!.notUnlockedYet, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: currentAchievementName != null + ? null + : Colors.grey.shade500, + ), + ), + if (bestLevel != null) const SizedBox(height: 8), + if (bestLevel != null) + Text( + achievementsMap[bestLevel]?.description ?? '', + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.white70 : Colors.black87, + ), + ), + const SizedBox(height: 16), + Wrap( + spacing: 8, + runSpacing: 8, + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.center, + children: AchievementLevel.values.map((level) { + final unlocked = widget.unlockedLevels.contains(level); + final isBest = level == bestLevel; + + return Container( + padding: EdgeInsets.all(isBest ? 10 : 6), + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: unlocked + ? LinearGradient( + colors: [ + _medalColor(level).withAlpha(200), + _medalColor(level), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ) + : null, + color: unlocked + ? null + : Colors.grey.shade400.withAlpha(80), + boxShadow: isBest && unlocked + ? [ + BoxShadow( + color: _medalColor(level).withAlpha(120), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ] + : [], + ), + child: Padding( + padding: EdgeInsets.all(4), + child: FaIcon( + unlocked + ? FontAwesomeIcons.trophy + : FontAwesomeIcons.lock, + size: isBest ? 22 : 18, + color: unlocked ? Colors.white : Colors.grey.shade500, + ), + ), + ); + }).toList(), + ), + const SizedBox(height: 12), + if (nextLevel != null && achievementsMap[nextLevel] != null) + Text( + '${AppLocalizations.of(context)!.nextAchievement}: ${nextLevel.name.toUpperCase()} - ${achievementsMap[nextLevel]!.description}', + style: TextStyle( + fontSize: 13, + fontStyle: FontStyle.italic, + color: isDark ? Colors.white60 : Colors.black54, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/achievements/presentation/widgets/achievements_content.dart b/lib/features/achievements/presentation/widgets/achievements_content.dart new file mode 100644 index 0000000..d368897 --- /dev/null +++ b/lib/features/achievements/presentation/widgets/achievements_content.dart @@ -0,0 +1,189 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:quiz_master/core/presentation/widgets/responsive.dart'; +import 'package:quiz_master/di.dart'; +import 'package:quiz_master/features/achievements/data/constants/achievements_list.dart'; +import 'package:quiz_master/features/achievements/presentation/blocs/achievements_bloc.dart'; +import 'package:quiz_master/features/achievements/presentation/widgets/achievements_grid.dart'; +import 'package:quiz_master/l10n/app_localizations.dart'; +import 'package:quiz_master/utils/helpers.dart'; + +class AchievementsContent extends StatefulWidget { + const AchievementsContent({super.key}); + + @override + State createState() => _AchievementsContentState(); +} + +class _AchievementsContentState extends State + with TickerProviderStateMixin { + late final AnimationController _headerController; + late final AnimationController _cardController; + late final Animation _headerOpacity; + late final Animation _cardOpacity; + late final Animation _headerOffset; + late final Animation _cardOffset; + + @override + void initState() { + super.initState(); + + _headerController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 800), + ); + + _cardController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 500), + ); + + _headerOpacity = CurvedAnimation( + parent: _headerController, + curve: Curves.easeOut, + ); + _headerOffset = Tween(begin: const Offset(0, 0.2), end: Offset.zero) + .animate( + CurvedAnimation( + parent: _headerController, + curve: Curves.easeOutCubic, + ), + ); + + _cardOpacity = CurvedAnimation( + parent: _cardController, + curve: Curves.easeOut, + ); + _cardOffset = Tween(begin: const Offset(0, 0.2), end: Offset.zero) + .animate( + CurvedAnimation(parent: _cardController, curve: Curves.easeOutCubic), + ); + + _headerController.forward().then((_) => _cardController.forward()); + } + + @override + void dispose() { + _headerController.dispose(); + _cardController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final greyColor = Helpers.isDarkMode(context) + ? Colors.grey[400] + : Colors.grey[600]; + + final achievements = getAchievements(context); + final unlocked = achievements + .where( + (a) => + ((sl() + .state + .unlockedLevels[a.category] + ?.index ?? + -1) >= + a.level.index), + ) + .toList(); + + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + SlideTransition( + position: _headerOffset, + child: FadeTransition( + opacity: _headerOpacity, + child: Column( + children: [ + ShaderMask( + shaderCallback: (bounds) => const LinearGradient( + colors: [Color(0xFFFF6EC7), Color(0xFF9B59B6)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ).createShader(Rect.fromLTWH(0, 0, 200, 70)), + child: Text( + AppLocalizations.of(context)!.achievements, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 46, + fontWeight: FontWeight.bold, + color: Colors.white, + letterSpacing: 1.1, + ), + ), + ), + const SizedBox(height: 12), + Text( + AppLocalizations.of(context)!.trackProgress, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 24, color: greyColor), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + SlideTransition( + position: _cardOffset, + child: FadeTransition( + opacity: _cardOpacity, + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + color: Colors.deepPurpleAccent, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const FaIcon( + FontAwesomeIcons.medal, + color: Colors.white, + size: 48, + ), + const SizedBox(width: 18), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context)!.unlocked, + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + Text( + '${unlocked.length} / ${achievements.length}', + style: const TextStyle( + color: Colors.white70, + fontSize: 16, + ), + ), + ], + ), + ], + ), + ), + ), + ), + ), + const SizedBox(height: 48), + Responsive( + mobile: AchievementsGrid(columns: 1, unlocked: unlocked), + tablet: AchievementsGrid(columns: 2, unlocked: unlocked), + desktop: AchievementsGrid(columns: 2, unlocked: unlocked), + ), + const SizedBox(height: 32), + ], + ), + ); + } +} diff --git a/lib/features/achievements/presentation/widgets/achievements_grid.dart b/lib/features/achievements/presentation/widgets/achievements_grid.dart new file mode 100644 index 0000000..a741ddf --- /dev/null +++ b/lib/features/achievements/presentation/widgets/achievements_grid.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:quiz_master/features/achievements/data/model/achievement.dart'; +import 'package:quiz_master/features/achievements/presentation/widgets/achievement_card.dart'; +import 'package:quiz_master/utils/enums.dart'; + +class AchievementsGrid extends StatelessWidget { + final int columns; + final List unlocked; + const AchievementsGrid({ + super.key, + required this.columns, + required this.unlocked, + }); + + @override + Widget build(BuildContext context) { + final achievementCategories = AchievementCategory.values.toList(); + final unlockedLevelsPerCategory = { + for (var category in achievementCategories) + category: unlocked + .where((a) => a.category == category) + .map((a) => a.level) + .toSet(), + }; + final sortedCategories = achievementCategories + ..sort((a, b) { + final aUnlocked = unlockedLevelsPerCategory[a]?.isNotEmpty ?? false; + final bUnlocked = unlockedLevelsPerCategory[b]?.isNotEmpty ?? false; + if (aUnlocked && !bUnlocked) return -1; + if (!aUnlocked && bUnlocked) return 1; + return a.index.compareTo(b.index); + }); + return LayoutBuilder( + builder: (context, constraints) { + final totalSpacing = (columns - 1) * 16; + final itemWidth = (constraints.maxWidth - totalSpacing) / columns; + + return Wrap( + spacing: 16, + runSpacing: 16, + children: List.generate(achievementCategories.length, (index) { + final delay = Duration(milliseconds: 100 * index); + final category = sortedCategories[index]; + final unlockedLevels = unlockedLevelsPerCategory[category] ?? {}; + return SizedBox( + width: itemWidth, + child: AchievementCard( + category: category, + unlockedLevels: unlockedLevels, + delay: delay, + ), + ); + }), + ); + }, + ); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b000f37..e7979bc 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -142,13 +142,23 @@ "achievementSpeedsterBronzeTitle": "Speedster Novice", "achievementSpeedsterBronzeDesc": "Complete a quiz under 60 seconds with 80%+ score", "achievementSpeedsterSilverTitle": "Speedster Adept", - "achievementSpeedsterSilverDesc": "Complete a quiz under 45 seconds with 80%+ score", + "achievementSpeedsterSilverDesc": "Complete a quiz under 50 seconds with 80%+ score", "achievementSpeedsterGoldTitle": "Speedster Export", - "achievementSpeedsterGoldDesc": "Complete a quiz under 30 seconds with 80%+ score", + "achievementSpeedsterGoldDesc": "Complete a quiz under 40 seconds with 80%+ score", "achievementSpeedsterPlatinumTitle": "Speedster Master", - "achievementSpeedsterPlatinumDesc": "Complete a quiz under 20 seconds with 80%+ score", + "achievementSpeedsterPlatinumDesc": "Complete a quiz under 30 seconds with 80%+ score", "achievementSpeedsterDiamondTitle": "Speedster Legend", - "achievementSpeedsterDiamondDesc": "Complete a quiz under 10 seconds with 80%+ score", + "achievementSpeedsterDiamondDesc": "Complete a quiz under 25 seconds with 80%+ score", "achievementSpeedsterMasterTitle": "Speedster Grandmaster", - "achievementSpeedsterMasterDesc": "Complete a quiz under 5 seconds with 80%+ score" + "achievementSpeedsterMasterDesc": "Complete a quiz under 20 seconds with 80%+ score", + "trackProgress": "Track your progress and unlock badges", + "unlocked": "Unlocked", + "filterAll": "All", + "filterLocked": "Locked", + "achievementCategoryLevelingDesc": "Progress through levels by completing quizzes and challenges.", + "achievementCategoryAnswerMasterDesc": "Show your mastery by answering questions correctly and consistently.", + "achievementCategoryQuizAbuserDesc": "Earn achievements for completing a high number of quizzes.", + "achievementCategorySpeedsterDesc": "Unlock achievements for completing quizzes quickly and efficiently.", + "notUnlockedYet": "Not Unlocked Yet", + "nextAchievement": "Next" } \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 8d3b505..f4cb30d 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -955,7 +955,7 @@ abstract class AppLocalizations { /// No description provided for @achievementSpeedsterSilverDesc. /// /// In en, this message translates to: - /// **'Complete a quiz under 45 seconds with 80%+ score'** + /// **'Complete a quiz under 50 seconds with 80%+ score'** String get achievementSpeedsterSilverDesc; /// No description provided for @achievementSpeedsterGoldTitle. @@ -967,7 +967,7 @@ abstract class AppLocalizations { /// No description provided for @achievementSpeedsterGoldDesc. /// /// In en, this message translates to: - /// **'Complete a quiz under 30 seconds with 80%+ score'** + /// **'Complete a quiz under 40 seconds with 80%+ score'** String get achievementSpeedsterGoldDesc; /// No description provided for @achievementSpeedsterPlatinumTitle. @@ -979,7 +979,7 @@ abstract class AppLocalizations { /// No description provided for @achievementSpeedsterPlatinumDesc. /// /// In en, this message translates to: - /// **'Complete a quiz under 20 seconds with 80%+ score'** + /// **'Complete a quiz under 30 seconds with 80%+ score'** String get achievementSpeedsterPlatinumDesc; /// No description provided for @achievementSpeedsterDiamondTitle. @@ -991,7 +991,7 @@ abstract class AppLocalizations { /// No description provided for @achievementSpeedsterDiamondDesc. /// /// In en, this message translates to: - /// **'Complete a quiz under 10 seconds with 80%+ score'** + /// **'Complete a quiz under 25 seconds with 80%+ score'** String get achievementSpeedsterDiamondDesc; /// No description provided for @achievementSpeedsterMasterTitle. @@ -1003,8 +1003,68 @@ abstract class AppLocalizations { /// No description provided for @achievementSpeedsterMasterDesc. /// /// In en, this message translates to: - /// **'Complete a quiz under 5 seconds with 80%+ score'** + /// **'Complete a quiz under 20 seconds with 80%+ score'** String get achievementSpeedsterMasterDesc; + + /// No description provided for @trackProgress. + /// + /// In en, this message translates to: + /// **'Track your progress and unlock badges'** + String get trackProgress; + + /// No description provided for @unlocked. + /// + /// In en, this message translates to: + /// **'Unlocked'** + String get unlocked; + + /// No description provided for @filterAll. + /// + /// In en, this message translates to: + /// **'All'** + String get filterAll; + + /// No description provided for @filterLocked. + /// + /// In en, this message translates to: + /// **'Locked'** + String get filterLocked; + + /// No description provided for @achievementCategoryLevelingDesc. + /// + /// In en, this message translates to: + /// **'Progress through levels by completing quizzes and challenges.'** + String get achievementCategoryLevelingDesc; + + /// No description provided for @achievementCategoryAnswerMasterDesc. + /// + /// In en, this message translates to: + /// **'Show your mastery by answering questions correctly and consistently.'** + String get achievementCategoryAnswerMasterDesc; + + /// No description provided for @achievementCategoryQuizAbuserDesc. + /// + /// In en, this message translates to: + /// **'Earn achievements for completing a high number of quizzes.'** + String get achievementCategoryQuizAbuserDesc; + + /// No description provided for @achievementCategorySpeedsterDesc. + /// + /// In en, this message translates to: + /// **'Unlock achievements for completing quizzes quickly and efficiently.'** + String get achievementCategorySpeedsterDesc; + + /// No description provided for @notUnlockedYet. + /// + /// In en, this message translates to: + /// **'Not Unlocked Yet'** + String get notUnlockedYet; + + /// No description provided for @nextAchievement. + /// + /// In en, this message translates to: + /// **'Next'** + String get nextAchievement; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 115082a..ccfc5e3 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -482,33 +482,67 @@ class AppLocalizationsEn extends AppLocalizations { @override String get achievementSpeedsterSilverDesc => - 'Complete a quiz under 45 seconds with 80%+ score'; + 'Complete a quiz under 50 seconds with 80%+ score'; @override String get achievementSpeedsterGoldTitle => 'Speedster Export'; @override String get achievementSpeedsterGoldDesc => - 'Complete a quiz under 30 seconds with 80%+ score'; + 'Complete a quiz under 40 seconds with 80%+ score'; @override String get achievementSpeedsterPlatinumTitle => 'Speedster Master'; @override String get achievementSpeedsterPlatinumDesc => - 'Complete a quiz under 20 seconds with 80%+ score'; + 'Complete a quiz under 30 seconds with 80%+ score'; @override String get achievementSpeedsterDiamondTitle => 'Speedster Legend'; @override String get achievementSpeedsterDiamondDesc => - 'Complete a quiz under 10 seconds with 80%+ score'; + 'Complete a quiz under 25 seconds with 80%+ score'; @override String get achievementSpeedsterMasterTitle => 'Speedster Grandmaster'; @override String get achievementSpeedsterMasterDesc => - 'Complete a quiz under 5 seconds with 80%+ score'; + 'Complete a quiz under 20 seconds with 80%+ score'; + + @override + String get trackProgress => 'Track your progress and unlock badges'; + + @override + String get unlocked => 'Unlocked'; + + @override + String get filterAll => 'All'; + + @override + String get filterLocked => 'Locked'; + + @override + String get achievementCategoryLevelingDesc => + 'Progress through levels by completing quizzes and challenges.'; + + @override + String get achievementCategoryAnswerMasterDesc => + 'Show your mastery by answering questions correctly and consistently.'; + + @override + String get achievementCategoryQuizAbuserDesc => + 'Earn achievements for completing a high number of quizzes.'; + + @override + String get achievementCategorySpeedsterDesc => + 'Unlock achievements for completing quizzes quickly and efficiently.'; + + @override + String get notUnlockedYet => 'Not Unlocked Yet'; + + @override + String get nextAchievement => 'Next'; }