From 44cdfa49a2e0de8b38c1f7a3c2cc05d64fdcc596 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 13:34:48 +0000 Subject: [PATCH] feat: implement role-based dashboards, lessons, and quiz modules - Created modular directory structure as per project memory. - Implemented role-based navigation with MainWrapper and UserProvider. - Developed vibrant dashboards for Student, Parent, Teacher, and Admin roles. - Added functional LessonDetailScreen and QuizScreen with instant feedback. - Integrated CMSMockService for structured content delivery. - Set up offline support with Hive and placeholders for Paystack/OneSignal. - Added widget test for onboarding and role selection. Co-authored-by: iQodeIT <162815903+iQodeIT@users.noreply.github.com> --- edu_play/lib/MainWrapper.dart | 38 ++++ edu_play/lib/config/app_constants.dart | 12 ++ edu_play/lib/main.dart | 32 ++- edu_play/lib/models/grade_model.dart | 6 + edu_play/lib/models/lesson_model.dart | 4 + edu_play/lib/models/quiz_model.dart | 4 + edu_play/lib/models/subject_model.dart | 7 + edu_play/lib/models/topic_model.dart | 7 + .../lib/screens/admin_dashboard_screen.dart | 80 ++++++- .../lib/screens/lesson_detail_screen.dart | 58 +++++ edu_play/lib/screens/onboarding_screen.dart | 68 +++++- .../lib/screens/parent_dashboard_screen.dart | 107 ++++++++- edu_play/lib/screens/quiz_screen.dart | 166 ++++++++++++++ .../lib/screens/student_dashboard_screen.dart | 203 +++++++++++++++++- .../lib/screens/teacher_dashboard_screen.dart | 90 +++++++- edu_play/lib/services/cms_mock_service.dart | 53 +++++ .../lib/services/notification_service.dart | 9 + edu_play/lib/services/payment_service.dart | 15 ++ edu_play/lib/services/storage_service.dart | 21 ++ edu_play/lib/services/user_provider.dart | 27 +++ edu_play/pubspec.lock | 16 +- edu_play/test/widget_test.dart | 28 +-- 22 files changed, 996 insertions(+), 55 deletions(-) create mode 100644 edu_play/lib/MainWrapper.dart create mode 100644 edu_play/lib/config/app_constants.dart create mode 100644 edu_play/lib/models/grade_model.dart create mode 100644 edu_play/lib/models/subject_model.dart create mode 100644 edu_play/lib/models/topic_model.dart create mode 100644 edu_play/lib/screens/lesson_detail_screen.dart create mode 100644 edu_play/lib/screens/quiz_screen.dart create mode 100644 edu_play/lib/services/cms_mock_service.dart create mode 100644 edu_play/lib/services/notification_service.dart create mode 100644 edu_play/lib/services/payment_service.dart create mode 100644 edu_play/lib/services/storage_service.dart create mode 100644 edu_play/lib/services/user_provider.dart diff --git a/edu_play/lib/MainWrapper.dart b/edu_play/lib/MainWrapper.dart new file mode 100644 index 0000000..a382821 --- /dev/null +++ b/edu_play/lib/MainWrapper.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'screens/onboarding_screen.dart'; +import 'screens/student_dashboard_screen.dart'; +import 'screens/parent_dashboard_screen.dart'; +import 'screens/teacher_dashboard_screen.dart'; +import 'screens/admin_dashboard_screen.dart'; +import 'services/user_provider.dart'; + +class MainWrapper extends StatelessWidget { + const MainWrapper({super.key}); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, userProvider, child) { + final user = userProvider.user; + + if (user == null) { + return const OnboardingScreen(); + } + + switch (user.role) { + case 'Student': + return const StudentDashboardScreen(); + case 'Parent': + return const ParentDashboardScreen(); + case 'Teacher': + return const TeacherDashboardScreen(); + case 'Admin': + return const AdminDashboardScreen(); + default: + return const OnboardingScreen(); + } + }, + ); + } +} diff --git a/edu_play/lib/config/app_constants.dart b/edu_play/lib/config/app_constants.dart new file mode 100644 index 0000000..0bf983f --- /dev/null +++ b/edu_play/lib/config/app_constants.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class AppColors { + static const Color primarySkyBlue = Color(0xFF3B82F6); + static const Color accentWarmYellow = Color(0xFFFACC15); + static const Color secondaryCoralRed = Color(0xFFF87171); + static const Color secondaryGreen = Color(0xFF34D399); +} + +class AppConstants { + static const String appName = 'EduPlay'; +} diff --git a/edu_play/lib/main.dart b/edu_play/lib/main.dart index 5b51a56..fdb4f6d 100644 --- a/edu_play/lib/main.dart +++ b/edu_play/lib/main.dart @@ -1,38 +1,48 @@ -import 'package:edu_play/screens/onboarding_screen.dart'; +import 'package:edu_play/MainWrapper.dart'; +import 'package:edu_play/services/user_provider.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:provider/provider.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'services/notification_service.dart'; -void main() { - runApp(const MyApp()); +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await NotificationService.initialize(); + await Hive.initFlutter(); + await Hive.openBox('settings'); + await Hive.openBox('lessons'); + + runApp( + MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => UserProvider()), + ], + child: const MyApp(), + ), + ); } class MyApp extends StatelessWidget { const MyApp({super.key}); - // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'EduPlay', theme: ThemeData( - // Define the default brightness and colors. colorScheme: ColorScheme.fromSeed( seedColor: const Color(0xFF3B82F6), // Primary: Sky Blue primary: const Color(0xFF3B82F6), secondary: const Color(0xFFFACC15), // Accent: Warm Yellow error: const Color(0xFFF87171), // Secondary: Coral Red - // Other colors can be defined here as needed ), - - // Define the default font family. textTheme: GoogleFonts.nunitoTextTheme( Theme.of(context).textTheme, ), - - // Use Material 3 design. useMaterial3: true, ), - home: const OnboardingScreen(), + home: const MainWrapper(), debugShowCheckedModeBanner: false, ); } diff --git a/edu_play/lib/models/grade_model.dart b/edu_play/lib/models/grade_model.dart new file mode 100644 index 0000000..59b5dc5 --- /dev/null +++ b/edu_play/lib/models/grade_model.dart @@ -0,0 +1,6 @@ +class Grade { + final String id; + final String name; // e.g., Primary 1, JSS 2 + + Grade({required this.id, required this.name}); +} diff --git a/edu_play/lib/models/lesson_model.dart b/edu_play/lib/models/lesson_model.dart index c11f567..cfbf1c3 100644 --- a/edu_play/lib/models/lesson_model.dart +++ b/edu_play/lib/models/lesson_model.dart @@ -1,12 +1,16 @@ // Represents a single lesson in a topic. class Lesson { final String id; + final String topicId; final String title; final String content; + final List mediaUrls; Lesson({ required this.id, + required this.topicId, required this.title, required this.content, + this.mediaUrls = const [], }); } \ No newline at end of file diff --git a/edu_play/lib/models/quiz_model.dart b/edu_play/lib/models/quiz_model.dart index 7d89eb2..04211ba 100644 --- a/edu_play/lib/models/quiz_model.dart +++ b/edu_play/lib/models/quiz_model.dart @@ -1,14 +1,18 @@ // Represents a single quiz question. class QuizItem { final String id; + final String topicId; final String question; final List options; final String correctAnswer; + final String explanation; QuizItem({ required this.id, + required this.topicId, required this.question, required this.options, required this.correctAnswer, + required this.explanation, }); } \ No newline at end of file diff --git a/edu_play/lib/models/subject_model.dart b/edu_play/lib/models/subject_model.dart new file mode 100644 index 0000000..5769b35 --- /dev/null +++ b/edu_play/lib/models/subject_model.dart @@ -0,0 +1,7 @@ +class Subject { + final String id; + final String name; // e.g., Mathematics, English + final String gradeId; + + Subject({required this.id, required this.name, required this.gradeId}); +} diff --git a/edu_play/lib/models/topic_model.dart b/edu_play/lib/models/topic_model.dart new file mode 100644 index 0000000..e2ba3f9 --- /dev/null +++ b/edu_play/lib/models/topic_model.dart @@ -0,0 +1,7 @@ +class Topic { + final String id; + final String name; // e.g., Addition, Nouns + final String subjectId; + + Topic({required this.id, required this.name, required this.subjectId}); +} diff --git a/edu_play/lib/screens/admin_dashboard_screen.dart b/edu_play/lib/screens/admin_dashboard_screen.dart index 657cf8f..b166e83 100644 --- a/edu_play/lib/screens/admin_dashboard_screen.dart +++ b/edu_play/lib/screens/admin_dashboard_screen.dart @@ -1,14 +1,86 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../services/user_provider.dart'; +import '../config/app_constants.dart'; class AdminDashboardScreen extends StatelessWidget { const AdminDashboardScreen({super.key}); @override Widget build(BuildContext context) { - return const Scaffold( - appBar: AppBar(title: Text('Admin Dashboard')), - body: Center( - child: Text('Admin Dashboard'), + return Scaffold( + appBar: AppBar( + title: const Text('Admin Dashboard'), + backgroundColor: AppColors.secondaryCoralRed, + foregroundColor: Colors.white, + actions: [ + IconButton( + icon: const Icon(Icons.logout), + onPressed: () => Provider.of(context, listen: false).logout(), + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "System Overview", + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + Row( + children: [ + _buildStatCard('Total Schools', '12', Colors.blue), + const SizedBox(width: 16), + _buildStatCard('Total Users', '1,240', Colors.green), + ], + ), + const SizedBox(height: 24), + const Text( + "Management Tools", + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 12), + _buildManagementTile('School Management', Icons.business, () {}), + _buildManagementTile('User Oversight', Icons.people, () {}), + _buildManagementTile('Platform Configuration', Icons.settings, () {}), + _buildManagementTile('Subscription Plans', Icons.card_membership, () {}), + ], + ), + ), + ); + } + + Widget _buildStatCard(String label, String value, Color color) { + return Expanded( + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(15), + border: Border.all(color: color), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: TextStyle(color: color, fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + Text(value, style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: color)), + ], + ), + ), + ); + } + + Widget _buildManagementTile(String title, IconData icon, VoidCallback onTap) { + return Card( + child: ListTile( + leading: Icon(icon, color: AppColors.secondaryCoralRed), + title: Text(title), + trailing: const Icon(Icons.arrow_forward_ios, size: 16), + onTap: onTap, ), ); } diff --git a/edu_play/lib/screens/lesson_detail_screen.dart b/edu_play/lib/screens/lesson_detail_screen.dart new file mode 100644 index 0000000..88d706a --- /dev/null +++ b/edu_play/lib/screens/lesson_detail_screen.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import '../models/lesson_model.dart'; +import '../config/app_constants.dart'; + +class LessonDetailScreen extends StatelessWidget { + final Lesson lesson; + + const LessonDetailScreen({super.key, required this.lesson}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(lesson.title), + backgroundColor: AppColors.primarySkyBlue, + foregroundColor: Colors.white, + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (lesson.mediaUrls.isNotEmpty) + Container( + height: 200, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(12), + ), + child: const Icon(Icons.image, size: 100, color: Colors.grey), + ), + const SizedBox(height: 24), + Text( + lesson.title, + style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + Text( + lesson.content, + style: const TextStyle(fontSize: 18, height: 1.5), + ), + const SizedBox(height: 40), + ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom( + minimumSize: const Size(double.infinity, 50), + backgroundColor: AppColors.primarySkyBlue, + foregroundColor: Colors.white, + ), + child: const Text('Mark as Completed'), + ), + ], + ), + ), + ); + } +} diff --git a/edu_play/lib/screens/onboarding_screen.dart b/edu_play/lib/screens/onboarding_screen.dart index 02ded90..048159e 100644 --- a/edu_play/lib/screens/onboarding_screen.dart +++ b/edu_play/lib/screens/onboarding_screen.dart @@ -1,13 +1,75 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../services/user_provider.dart'; +import '../config/app_constants.dart'; class OnboardingScreen extends StatelessWidget { const OnboardingScreen({super.key}); @override Widget build(BuildContext context) { - return const Scaffold( - body: Center( - child: Text('Onboarding Screen'), + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.school_rounded, + size: 100, + color: AppColors.primarySkyBlue, + ), + const SizedBox(height: 24), + const Text( + 'Welcome to EduPlay!', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: AppColors.primarySkyBlue, + ), + ), + const SizedBox(height: 16), + const Text( + 'Gamified learning for Nigerian Students. Please select your role to continue.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 16, color: Colors.grey), + ), + const SizedBox(height: 48), + _buildRoleButton(context, 'Student', AppColors.primarySkyBlue), + const SizedBox(height: 12), + _buildRoleButton(context, 'Parent', AppColors.secondaryGreen), + const SizedBox(height: 12), + _buildRoleButton(context, 'Teacher', AppColors.accentWarmYellow), + const SizedBox(height: 12), + _buildRoleButton(context, 'Admin', AppColors.secondaryCoralRed), + ], + ), + ), + ), + ); + } + + Widget _buildRoleButton(BuildContext context, String role, Color color) { + return SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: color, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: () { + Provider.of(context, listen: false).setRole(role); + }, + child: Text( + 'Join as $role', + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), ), ); } diff --git a/edu_play/lib/screens/parent_dashboard_screen.dart b/edu_play/lib/screens/parent_dashboard_screen.dart index 74e9ad9..6b5a01c 100644 --- a/edu_play/lib/screens/parent_dashboard_screen.dart +++ b/edu_play/lib/screens/parent_dashboard_screen.dart @@ -1,15 +1,114 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../services/user_provider.dart'; +import '../config/app_constants.dart'; class ParentDashboardScreen extends StatelessWidget { const ParentDashboardScreen({super.key}); @override Widget build(BuildContext context) { - return const Scaffold( - appBar: AppBar(title: Text('Parent Dashboard')), - body: Center( - child: Text('Parent Dashboard'), + return Scaffold( + appBar: AppBar( + title: const Text('Parent Dashboard'), + backgroundColor: AppColors.secondaryGreen, + foregroundColor: Colors.white, + actions: [ + IconButton( + icon: const Icon(Icons.logout), + onPressed: () => Provider.of(context, listen: false).logout(), + ), + ], ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Child's Progress", + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + _buildChildSummaryCard('Tunde', 'Grade 4', 0.75), + const SizedBox(height: 24), + const Text( + "Recent Activities", + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 12), + Expanded( + child: ListView( + children: [ + _buildActivityTile('Completed Math Quiz', '10 mins ago', Icons.check_circle, Colors.green), + _buildActivityTile('Unlocked "Math Master" Badge', '1 hour ago', Icons.workspace_premium, Colors.orange), + _buildActivityTile('Started English Lesson', '2 hours ago', Icons.book, Colors.blue), + ], + ), + ), + ElevatedButton.icon( + onPressed: () {}, + icon: const Icon(Icons.picture_as_pdf), + label: const Text('Export PDF Report'), + style: ElevatedButton.styleFrom( + minimumSize: const Size(double.infinity, 50), + backgroundColor: AppColors.secondaryGreen, + foregroundColor: Colors.white, + ), + ), + ], + ), + ), + ); + } + + Widget _buildChildSummaryCard(String name, String grade, double progress) { + return Card( + elevation: 4, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Row( + children: [ + const CircleAvatar( + radius: 25, + backgroundColor: AppColors.primarySkyBlue, + child: Icon(Icons.person, color: Colors.white), + ), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(name, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + Text(grade), + ], + ), + const Spacer(), + Text('${(progress * 100).toInt()}%', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), + ], + ), + const SizedBox(height: 16), + LinearProgressIndicator( + value: progress, + backgroundColor: Colors.grey[200], + color: AppColors.secondaryGreen, + minHeight: 10, + borderRadius: BorderRadius.circular(5), + ), + ], + ), + ), + ); + } + + Widget _buildActivityTile(String title, String time, IconData icon, Color color) { + return ListTile( + leading: Icon(icon, color: color), + title: Text(title), + subtitle: Text(time), + trailing: const Icon(Icons.chevron_right), ); } } \ No newline at end of file diff --git a/edu_play/lib/screens/quiz_screen.dart b/edu_play/lib/screens/quiz_screen.dart new file mode 100644 index 0000000..ad886e0 --- /dev/null +++ b/edu_play/lib/screens/quiz_screen.dart @@ -0,0 +1,166 @@ +import 'package:flutter/material.dart'; +import '../models/quiz_model.dart'; +import '../config/app_constants.dart'; + +class QuizScreen extends StatefulWidget { + final List quizzes; + + const QuizScreen({super.key, required this.quizzes}); + + @override + State createState() => _QuizScreenState(); +} + +class _QuizScreenState extends State { + int _currentIndex = 0; + String? _selectedAnswer; + bool _isAnswered = false; + int _score = 0; + + void _submitAnswer(String answer) { + if (_isAnswered) return; + + setState(() { + _selectedAnswer = answer; + _isAnswered = true; + if (answer == widget.quizzes[_currentIndex].correctAnswer) { + _score++; + } + }); + } + + void _nextQuestion() { + if (_currentIndex < widget.quizzes.length - 1) { + setState(() { + _currentIndex++; + _selectedAnswer = null; + _isAnswered = false; + }); + } else { + _showResult(); + } + } + + void _showResult() { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + title: const Text('Quiz Completed!'), + content: Text('Your score: $_score / ${widget.quizzes.length}'), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); // Pop dialog + Navigator.pop(context); // Pop QuizScreen + }, + child: const Text('OK'), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final quiz = widget.quizzes[_currentIndex]; + + return Scaffold( + appBar: AppBar( + title: Text('Quiz: ${_currentIndex + 1}/${widget.quizzes.length}'), + backgroundColor: AppColors.secondaryCoralRed, + foregroundColor: Colors.white, + ), + body: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + quiz.question, + style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 32), + ...quiz.options.map((option) => _buildOption(option)), + const Spacer(), + if (_isAnswered) + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: _selectedAnswer == quiz.correctAnswer + ? Colors.green[100] + : Colors.red[100], + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _selectedAnswer == quiz.correctAnswer ? 'Correct!' : 'Incorrect!', + style: TextStyle( + fontWeight: FontWeight.bold, + color: _selectedAnswer == quiz.correctAnswer ? Colors.green : Colors.red, + ), + ), + const SizedBox(height: 4), + Text(quiz.explanation), + ], + ), + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: _isAnswered ? _nextQuestion : null, + style: ElevatedButton.styleFrom( + minimumSize: const Size(double.infinity, 50), + backgroundColor: AppColors.secondaryCoralRed, + foregroundColor: Colors.white, + ), + child: Text(_currentIndex < widget.quizzes.length - 1 ? 'Next Question' : 'See Results'), + ), + ], + ), + ), + ); + } + + Widget _buildOption(String option) { + final quiz = widget.quizzes[_currentIndex]; + bool isSelected = _selectedAnswer == option; + bool isCorrect = quiz.correctAnswer == option; + + Color borderColor = Colors.grey[300]!; + Color bgColor = Colors.white; + + if (_isAnswered) { + if (isCorrect) { + borderColor = Colors.green; + bgColor = Colors.green[50]!; + } else if (isSelected) { + borderColor = Colors.red; + bgColor = Colors.red[50]!; + } + } else if (isSelected) { + borderColor = AppColors.secondaryCoralRed; + } + + return Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: InkWell( + onTap: () => _submitAnswer(option), + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), + decoration: BoxDecoration( + color: bgColor, + border: Border.all(color: borderColor, width: 2), + borderRadius: BorderRadius.circular(15), + ), + child: Text( + option, + style: const TextStyle(fontSize: 18), + ), + ), + ), + ); + } +} diff --git a/edu_play/lib/screens/student_dashboard_screen.dart b/edu_play/lib/screens/student_dashboard_screen.dart index afc4bba..856ec62 100644 --- a/edu_play/lib/screens/student_dashboard_screen.dart +++ b/edu_play/lib/screens/student_dashboard_screen.dart @@ -1,14 +1,209 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../services/user_provider.dart'; +import '../services/cms_mock_service.dart'; +import '../config/app_constants.dart'; +import 'lesson_detail_screen.dart'; +import 'quiz_screen.dart'; class StudentDashboardScreen extends StatelessWidget { const StudentDashboardScreen({super.key}); @override Widget build(BuildContext context) { - return const Scaffold( - appBar: AppBar(title: Text('Student Dashboard')), - body: Center( - child: Text('Student Dashboard'), + return Scaffold( + backgroundColor: const Color(0xFFF0F9FF), // Very light blue + appBar: AppBar( + title: const Text('EduPlay Student', style: TextStyle(fontWeight: FontWeight.bold)), + backgroundColor: AppColors.primarySkyBlue, + foregroundColor: Colors.white, + actions: [ + IconButton( + icon: const Icon(Icons.logout), + onPressed: () => Provider.of(context, listen: false).logout(), + ), + ], + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildProfileSection(), + const SizedBox(height: 24), + _buildDailyMissions(), + const SizedBox(height: 24), + _buildQuickActions(context), + ], + ), + ), + bottomNavigationBar: BottomNavigationBar( + selectedItemColor: AppColors.primarySkyBlue, + unselectedItemColor: Colors.grey, + items: const [ + BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'), + BottomNavigationBarItem(icon: Icon(Icons.play_arrow), label: 'Play'), + BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'), + ], + ), + ); + } + + Widget _buildProfileSection() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + children: [ + Row( + children: [ + const CircleAvatar( + radius: 30, + backgroundColor: AppColors.accentWarmYellow, + child: Icon(Icons.face, size: 40, color: Colors.white), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Hello, Tunde!', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const Text('Level 5 Explorer', style: TextStyle(color: Colors.grey)), + ], + ), + ), + Column( + children: [ + const Icon(Icons.local_fire_department, color: Colors.orange), + const Text('5 Day Streak', style: TextStyle(fontWeight: FontWeight.bold)), + ], + ), + ], + ), + const SizedBox(height: 20), + const Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('XP Progress', style: TextStyle(fontWeight: FontWeight.bold)), + Text('450 / 1000 XP'), + ], + ), + const SizedBox(height: 8), + LinearProgressIndicator( + value: 0.45, + backgroundColor: Colors.grey[200], + color: AppColors.primarySkyBlue, + minHeight: 12, + borderRadius: BorderRadius.circular(6), + ), + ], + ), + ); + } + + Widget _buildDailyMissions() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Daily Missions', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 12), + _buildMissionCard('Math Master', 'Complete 2 Math quizzes', 0.5), + _buildMissionCard('Reading Hero', 'Read 1 English lesson', 1.0), + ], + ); + } + + Widget _buildMissionCard(String title, String subtitle, double progress) { + return Card( + margin: const EdgeInsets.only(bottom: 12), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + child: ListTile( + leading: CircleAvatar( + backgroundColor: progress == 1.0 ? AppColors.secondaryGreen : AppColors.accentWarmYellow, + child: Icon( + progress == 1.0 ? Icons.check : Icons.star, + color: Colors.white, + ), + ), + title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), + subtitle: Text(subtitle), + trailing: progress == 1.0 + ? const Text('Done!', style: TextStyle(color: AppColors.secondaryGreen, fontWeight: FontWeight.bold)) + : SizedBox( + width: 40, + height: 40, + child: CircularProgressIndicator( + value: progress, + strokeWidth: 4, + backgroundColor: Colors.grey[200], + color: AppColors.primarySkyBlue, + ), + ), + ), + ); + } + + Widget _buildQuickActions(BuildContext context) { + return GridView.count( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisCount: 2, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + children: [ + _buildActionCard(context, 'Lessons', Icons.book, AppColors.primarySkyBlue, () { + final lesson = CMSMockService.getLesson('t1'); + Navigator.push(context, MaterialPageRoute(builder: (context) => LessonDetailScreen(lesson: lesson))); + }), + _buildActionCard(context, 'Quizzes', Icons.quiz, AppColors.secondaryCoralRed, () { + final quizzes = CMSMockService.getQuizzes('t1'); + Navigator.push(context, MaterialPageRoute(builder: (context) => QuizScreen(quizzes: quizzes))); + }), + _buildActionCard(context, 'Games', Icons.videogame_asset, AppColors.secondaryGreen, () { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Games coming soon!'))); + }), + _buildActionCard(context, 'Badges', Icons.emoji_events, AppColors.accentWarmYellow, () { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Badges coming soon!'))); + }), + ], + ); + } + + Widget _buildActionCard(BuildContext context, String title, IconData icon, Color color, VoidCallback onTap) { + return InkWell( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(20), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 48, color: Colors.white), + const SizedBox(height: 8), + Text( + title, + style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold), + ), + ], + ), ), ); } diff --git a/edu_play/lib/screens/teacher_dashboard_screen.dart b/edu_play/lib/screens/teacher_dashboard_screen.dart index b80d396..15c5fa7 100644 --- a/edu_play/lib/screens/teacher_dashboard_screen.dart +++ b/edu_play/lib/screens/teacher_dashboard_screen.dart @@ -1,14 +1,96 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../services/user_provider.dart'; +import '../config/app_constants.dart'; class TeacherDashboardScreen extends StatelessWidget { const TeacherDashboardScreen({super.key}); @override Widget build(BuildContext context) { - return const Scaffold( - appBar: AppBar(title: Text('Teacher Dashboard')), - body: Center( - child: Text('Teacher Dashboard'), + return Scaffold( + appBar: AppBar( + title: const Text('Teacher Dashboard'), + backgroundColor: AppColors.accentWarmYellow, + foregroundColor: Colors.white, + actions: [ + IconButton( + icon: const Icon(Icons.logout), + onPressed: () => Provider.of(context, listen: false).logout(), + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "My Classes", + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + _buildClassCard('Primary 4A', '32 Students', 0.82), + const SizedBox(height: 24), + const Text( + "Quick Actions", + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 12), + Expanded( + child: GridView.count( + crossAxisCount: 2, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + children: [ + _buildTeacherAction(Icons.assignment, 'Assign Lesson', Colors.blue), + _buildTeacherAction(Icons.analytics, 'Class Analytics', Colors.purple), + _buildTeacherAction(Icons.message, 'Parent Messages', Colors.green), + _buildTeacherAction(Icons.event, 'Class Schedule', Colors.orange), + ], + ), + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () {}, + backgroundColor: AppColors.accentWarmYellow, + child: const Icon(Icons.add, color: Colors.white), + ), + ); + } + + Widget _buildClassCard(String name, String count, double performance) { + return Card( + child: ListTile( + leading: const CircleAvatar(backgroundColor: AppColors.accentWarmYellow, child: Icon(Icons.group, color: Colors.white)), + title: Text(name, style: const TextStyle(fontWeight: FontWeight.bold)), + subtitle: Text(count), + trailing: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('${(performance * 100).toInt()}%', style: const TextStyle(color: Colors.green, fontWeight: FontWeight.bold)), + const Text('Avg Score', style: TextStyle(fontSize: 10)), + ], + ), + ), + ); + } + + Widget _buildTeacherAction(IconData icon, String label, Color color) { + return Card( + elevation: 2, + child: InkWell( + onTap: () {}, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 40, color: color), + const SizedBox(height: 8), + Text(label, textAlign: TextAlign.center, style: const TextStyle(fontWeight: FontWeight.bold)), + ], + ), ), ); } diff --git a/edu_play/lib/services/cms_mock_service.dart b/edu_play/lib/services/cms_mock_service.dart new file mode 100644 index 0000000..7b4a7d7 --- /dev/null +++ b/edu_play/lib/services/cms_mock_service.dart @@ -0,0 +1,53 @@ +import '../models/grade_model.dart'; +import '../models/subject_model.dart'; +import '../models/topic_model.dart'; +import '../models/lesson_model.dart'; +import '../models/quiz_model.dart'; + +class CMSMockService { + static List getGrades() { + return [ + Grade(id: 'g1', name: 'Primary 1'), + Grade(id: 'g2', name: 'Primary 2'), + Grade(id: 'g3', name: 'Primary 3'), + Grade(id: 'g4', name: 'Primary 4'), + ]; + } + + static List getSubjects(String gradeId) { + return [ + Subject(id: 's1', name: 'Mathematics', gradeId: gradeId), + Subject(id: 's2', name: 'English Studies', gradeId: gradeId), + Subject(id: 's3', name: 'Basic Science', gradeId: gradeId), + ]; + } + + static List getTopics(String subjectId) { + return [ + Topic(id: 't1', name: 'Addition and Subtraction', subjectId: subjectId), + Topic(id: 't2', name: 'Nouns and Pronouns', subjectId: subjectId), + ]; + } + + static Lesson getLesson(String topicId) { + return Lesson( + id: 'l1', + topicId: topicId, + title: 'Introduction to Addition', + content: 'Addition is the process of calculating the total of two or more numbers.', + ); + } + + static List getQuizzes(String topicId) { + return [ + QuizItem( + id: 'q1', + topicId: topicId, + question: 'What is 2 + 2?', + options: ['3', '4', '5', '6'], + correctAnswer: '4', + explanation: '2 plus 2 equals 4.', + ), + ]; + } +} diff --git a/edu_play/lib/services/notification_service.dart b/edu_play/lib/services/notification_service.dart new file mode 100644 index 0000000..1a98294 --- /dev/null +++ b/edu_play/lib/services/notification_service.dart @@ -0,0 +1,9 @@ +import 'package:onesignal_flutter/onesignal_flutter.dart'; + +class NotificationService { + static Future initialize() async { + // OneSignal.Debug.setLogLevel(OSLogLevel.verbose); + // OneSignal.initialize("YOUR_ONESIGNAL_APP_ID"); + // OneSignal.Notifications.requestPermission(true); + } +} diff --git a/edu_play/lib/services/payment_service.dart b/edu_play/lib/services/payment_service.dart new file mode 100644 index 0000000..49069fd --- /dev/null +++ b/edu_play/lib/services/payment_service.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; +// import 'package:paystack_flutter_sdk/paystack_flutter_sdk.dart'; + +class PaymentService { + static Future processPayment(BuildContext context, double amount) async { + // Placeholder for Paystack payment processing + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Processing payment of ₦$amount via Paystack...')), + ); + await Future.delayed(const Duration(seconds: 2)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Payment Successful!')), + ); + } +} diff --git a/edu_play/lib/services/storage_service.dart b/edu_play/lib/services/storage_service.dart new file mode 100644 index 0000000..f645194 --- /dev/null +++ b/edu_play/lib/services/storage_service.dart @@ -0,0 +1,21 @@ +import 'package:hive_flutter/hive_flutter.dart'; + +class StorageService { + static final Box _settingsBox = Hive.box('settings'); + static final Box _lessonsBox = Hive.box('lessons'); + + static Future saveUserRole(String role) async { + await _settingsBox.put('user_role', role); + } + + static String? getUserRole() { + return _settingsBox.get('user_role'); + } + + static Future clearAll() async { + await _settingsBox.clear(); + await _lessonsBox.clear(); + } + + // Future methods to cache/retrieve lessons could go here +} diff --git a/edu_play/lib/services/user_provider.dart b/edu_play/lib/services/user_provider.dart new file mode 100644 index 0000000..cb4d226 --- /dev/null +++ b/edu_play/lib/services/user_provider.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import '../models/user_model.dart'; + +class UserProvider with ChangeNotifier { + User? _user; + + User? get user => _user; + + void setUser(User user) { + _user = user; + notifyListeners(); + } + + void setRole(String role) { + if (_user != null) { + _user = User(id: _user!.id, email: _user!.email, role: role); + } else { + _user = User(id: 'temp_id', email: 'guest@eduplay.com', role: role); + } + notifyListeners(); + } + + void logout() { + _user = null; + notifyListeners(); + } +} diff --git a/edu_play/pubspec.lock b/edu_play/pubspec.lock index fa6ef73..a50c0a2 100644 --- a/edu_play/pubspec.lock +++ b/edu_play/pubspec.lock @@ -236,10 +236,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -320,14 +320,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" - paystack_flutter_sdk: - dependency: "direct main" - description: - name: paystack_flutter_sdk - sha256: a8386bee7bed218517c580017a2f3112da1c0e298f3c559f4a3d3f1f7e233221 - url: "https://pub.dev" - source: hosted - version: "0.0.1-alpha.2" platform: dependency: transitive description: @@ -401,10 +393,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.7" typed_data: dependency: transitive description: diff --git a/edu_play/test/widget_test.dart b/edu_play/test/widget_test.dart index 635f718..f1e1862 100644 --- a/edu_play/test/widget_test.dart +++ b/edu_play/test/widget_test.dart @@ -10,21 +10,23 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:edu_play/main.dart'; +import 'package:edu_play/services/user_provider.dart'; +import 'package:provider/provider.dart'; + void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { + testWidgets('Onboarding screen smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); + await tester.pumpWidget( + MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => UserProvider()), + ], + child: const MyApp(), + ), + ); - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); + // Verify that onboarding screen is shown. + expect(find.text('Welcome to EduPlay!'), findsOneWidget); + expect(find.text('Join as Student'), findsOneWidget); }); }