diff --git a/edu_play/.metadata b/edu_play/.metadata index e90f3ae..644060a 100644 --- a/edu_play/.metadata +++ b/edu_play/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "ac4e799d237041cf905519190471f657b657155a" + revision: "3b62efc2a3da49882f43c372e0bc53daef7295a6" channel: "stable" project_type: app @@ -13,14 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: ac4e799d237041cf905519190471f657b657155a - base_revision: ac4e799d237041cf905519190471f657b657155a - - platform: android - create_revision: ac4e799d237041cf905519190471f657b657155a - base_revision: ac4e799d237041cf905519190471f657b657155a - - platform: ios - create_revision: ac4e799d237041cf905519190471f657b657155a - base_revision: ac4e799d237041cf905519190471f657b657155a + create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 + base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 + - platform: web + create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 + base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 # User provided section diff --git a/edu_play/lib/data/mock_data.dart b/edu_play/lib/data/mock_data.dart new file mode 100644 index 0000000..047f332 --- /dev/null +++ b/edu_play/lib/data/mock_data.dart @@ -0,0 +1,19 @@ +import 'package:edu_play/models/lesson_model.dart'; + +final List mockGrades = [ + Grade(id: '1', name: 'Primary 1'), + Grade(id: '2', name: 'Primary 2'), + Grade(id: '3', name: 'Primary 3'), +]; + +final List mockSubjects = [ + Subject(id: 'math', name: 'Mathematics', icon: 'numbers'), + Subject(id: 'english', name: 'English', icon: 'book'), + Subject(id: 'science', name: 'Basic Science', icon: 'science'), +]; + +final List mockTopics = [ + Topic(id: 't1', subjectId: 'math', name: 'Addition'), + Topic(id: 't2', subjectId: 'math', name: 'Subtraction'), + Topic(id: 't3', subjectId: 'english', name: 'Nouns'), +]; diff --git a/edu_play/lib/main.dart b/edu_play/lib/main.dart index 5b51a56..85d808a 100644 --- a/edu_play/lib/main.dart +++ b/edu_play/lib/main.dart @@ -1,39 +1,35 @@ -import 'package:edu_play/screens/onboarding_screen.dart'; +import 'package:edu_play/main_wrapper.dart'; +import 'package:edu_play/services/auth_service.dart'; +import 'package:edu_play/styles/app_theme.dart'; import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:provider/provider.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Initialize Hive for local storage + await Hive.initFlutter(); + await Hive.openBox('settings'); // Box for app settings and user session -void main() { runApp(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, + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => AuthService()), + ], + child: MaterialApp( + title: 'EduPlay', + theme: AppTheme.lightTheme, + home: const MainWrapper(), + debugShowCheckedModeBanner: false, ), - home: const OnboardingScreen(), - debugShowCheckedModeBanner: false, ); } } \ No newline at end of file diff --git a/edu_play/lib/main_wrapper.dart b/edu_play/lib/main_wrapper.dart new file mode 100644 index 0000000..6040d89 --- /dev/null +++ b/edu_play/lib/main_wrapper.dart @@ -0,0 +1,34 @@ +import 'package:edu_play/models/user_model.dart'; +import 'package:edu_play/screens/admin/admin_dashboard_screen.dart'; +import 'package:edu_play/screens/onboarding_screen.dart'; +import 'package:edu_play/screens/parent/parent_dashboard_screen.dart'; +import 'package:edu_play/screens/student/student_dashboard_screen.dart'; +import 'package:edu_play/screens/teacher/teacher_dashboard_screen.dart'; +import 'package:edu_play/services/auth_service.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class MainWrapper extends StatelessWidget { + const MainWrapper({super.key}); + + @override + Widget build(BuildContext context) { + final authService = Provider.of(context); + + if (!authService.isAuthenticated) { + return const OnboardingScreen(); + } + + final user = authService.currentUser!; + switch (user.role) { + case UserRole.student: + return const StudentDashboardScreen(); + case UserRole.parent: + return const ParentDashboardScreen(); + case UserRole.teacher: + return const TeacherDashboardScreen(); + case UserRole.admin: + return const AdminDashboardScreen(); + } + } +} diff --git a/edu_play/lib/models/lesson_model.dart b/edu_play/lib/models/lesson_model.dart index c11f567..846b7da 100644 --- a/edu_play/lib/models/lesson_model.dart +++ b/edu_play/lib/models/lesson_model.dart @@ -1,12 +1,38 @@ -// Represents a single lesson in a topic. +class Grade { + final String id; + final String name; // e.g., "Primary 1" + + Grade({required this.id, required this.name}); +} + +class Subject { + final String id; + final String name; // e.g., "Mathematics" + final String icon; + + Subject({required this.id, required this.name, required this.icon}); +} + +class Topic { + final String id; + final String subjectId; + final String name; // e.g., "Addition" + + Topic({required this.id, required this.subjectId, required this.name}); +} + 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, + required this.mediaUrls, }); -} \ 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..efc5560 100644 --- a/edu_play/lib/models/quiz_model.dart +++ b/edu_play/lib/models/quiz_model.dart @@ -1,14 +1,27 @@ -// Represents a single quiz question. -class QuizItem { +class QuizQuestion { final String id; final String question; final List options; - final String correctAnswer; + final int correctAnswerIndex; + final String explanation; - QuizItem({ + QuizQuestion({ required this.id, required this.question, required this.options, - required this.correctAnswer, + required this.correctAnswerIndex, + required this.explanation, }); -} \ No newline at end of file +} + +class Quiz { + final String id; + final String lessonId; + final List questions; + + Quiz({ + required this.id, + required this.lessonId, + required this.questions, + }); +} diff --git a/edu_play/lib/models/reward_model.dart b/edu_play/lib/models/reward_model.dart index 7fc5de8..a054d59 100644 --- a/edu_play/lib/models/reward_model.dart +++ b/edu_play/lib/models/reward_model.dart @@ -1,14 +1,27 @@ -// Represents a reward, such as a badge or avatar item. -class Reward { +class Badge { final String id; final String name; final String description; + final String iconUrl; + + Badge({ + required this.id, + required this.name, + required this.description, + required this.iconUrl, + }); +} + +class Reward { + final String id; + final String name; + final int costXP; final String imageUrl; Reward({ required this.id, required this.name, - required this.description, + required this.costXP, required this.imageUrl, }); -} \ No newline at end of file +} diff --git a/edu_play/lib/models/user_model.dart b/edu_play/lib/models/user_model.dart index 94a697a..bfb00b8 100644 --- a/edu_play/lib/models/user_model.dart +++ b/edu_play/lib/models/user_model.dart @@ -1,13 +1,56 @@ -// Represents a user in the EduPlay app. -// This will be expanded to include properties for each user role. -class User { +enum UserRole { + student, + parent, + teacher, + admin, +} + +class UserModel { final String id; final String email; - final String role; // "Student", "Parent", "Teacher", "Admin" + final String name; + final UserRole role; + final String? schoolCode; + + // Student specific fields + final int? xp; + final int? streak; + final String? grade; // e.g., "Primary 1" - User({ + UserModel({ required this.id, required this.email, + required this.name, required this.role, + this.schoolCode, + this.xp, + this.streak, + this.grade, }); -} \ No newline at end of file + + factory UserModel.fromJson(Map json) { + return UserModel( + id: json['id'], + email: json['email'], + name: json['name'], + role: UserRole.values.firstWhere((e) => e.toString().split('.').last == json['role']), + schoolCode: json['schoolCode'], + xp: json['xp'], + streak: json['streak'], + grade: json['grade'], + ); + } + + Map toJson() { + return { + 'id': id, + 'email': email, + 'name': name, + 'role': role.toString().split('.').last, + 'schoolCode': schoolCode, + 'xp': xp, + 'streak': streak, + 'grade': grade, + }; + } +} diff --git a/edu_play/lib/screens/admin/admin_dashboard_screen.dart b/edu_play/lib/screens/admin/admin_dashboard_screen.dart new file mode 100644 index 0000000..cfe1b52 --- /dev/null +++ b/edu_play/lib/screens/admin/admin_dashboard_screen.dart @@ -0,0 +1,154 @@ +import 'package:edu_play/services/auth_service.dart'; +import 'package:edu_play/styles/app_colors.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class AdminDashboardScreen extends StatelessWidget { + const AdminDashboardScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Admin Panel'), + actions: [ + IconButton( + icon: const Icon(Icons.logout), + onPressed: () => Provider.of(context, listen: false).logout(), + ), + ], + ), + body: ListView( + padding: const EdgeInsets.all(24.0), + children: [ + const _SchoolSummaryCard( + schoolName: 'Lekki British School', + schoolCode: 'LBS-2024-XP', + totalStudents: 1200, + totalTeachers: 85, + ), + const SizedBox(height: 32), + const _AdminActionTile( + title: 'Manage Teachers', + icon: Icons.people, + subtitle: 'Add, remove or edit teacher profiles', + ), + const SizedBox(height: 12), + const _AdminActionTile( + title: 'School Analytics', + icon: Icons.bar_chart, + subtitle: 'Overview of school-wide performance', + ), + const SizedBox(height: 12), + const _AdminActionTile( + title: 'Content Configuration', + icon: Icons.settings_suggest, + subtitle: 'Enable/disable subjects or grades', + ), + const SizedBox(height: 12), + const _AdminActionTile( + title: 'Subscription & Billing', + icon: Icons.payments, + subtitle: 'Manage school-wide premium status', + ), + ], + ), + ); + } +} + +class _SchoolSummaryCard extends StatelessWidget { + final String schoolName; + final String schoolCode; + final int totalStudents; + final int totalTeachers; + + const _SchoolSummaryCard({ + required this.schoolName, + required this.schoolCode, + required this.totalStudents, + required this.totalTeachers, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: AppColors.textDark, + borderRadius: BorderRadius.circular(24), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(schoolName, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 22)), + const SizedBox(height: 4), + Row( + children: [ + const Text('CODE: ', style: TextStyle(color: AppColors.textGrey, fontSize: 14)), + Text(schoolCode, style: const TextStyle(color: AppColors.accentWarmYellow, fontWeight: FontWeight.bold, fontSize: 14)), + ], + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _StatItem(label: 'Students', value: totalStudents.toString()), + _StatItem(label: 'Teachers', value: totalTeachers.toString()), + ], + ), + ], + ), + ); + } +} + +class _StatItem extends StatelessWidget { + final String label; + final String value; + + const _StatItem({required this.label, required this.value}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text(value, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 20)), + Text(label, style: const TextStyle(color: AppColors.textGrey, fontSize: 12)), + ], + ); + } +} + +class _AdminActionTile extends StatelessWidget { + final String title; + final String subtitle; + final IconData icon; + + const _AdminActionTile({ + required this.title, + required this.subtitle, + required this.icon, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppColors.primarySkyBlue.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, color: AppColors.primarySkyBlue), + ), + title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), + subtitle: Text(subtitle, style: const TextStyle(fontSize: 12)), + trailing: const Icon(Icons.chevron_right), + shape: RoundedRectangleBorder( + side: BorderSide(color: Colors.grey.shade200), + borderRadius: BorderRadius.circular(16), + ), + ); + } +} diff --git a/edu_play/lib/screens/admin_dashboard_screen.dart b/edu_play/lib/screens/admin_dashboard_screen.dart deleted file mode 100644 index 657cf8f..0000000 --- a/edu_play/lib/screens/admin_dashboard_screen.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter/material.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'), - ), - ); - } -} \ No newline at end of file diff --git a/edu_play/lib/screens/onboarding_screen.dart b/edu_play/lib/screens/onboarding_screen.dart index 02ded90..28eb670 100644 --- a/edu_play/lib/screens/onboarding_screen.dart +++ b/edu_play/lib/screens/onboarding_screen.dart @@ -1,14 +1,166 @@ +import 'package:edu_play/models/user_model.dart'; +import 'package:edu_play/services/auth_service.dart'; +import 'package:edu_play/styles/app_colors.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; -class OnboardingScreen extends StatelessWidget { +class OnboardingScreen extends StatefulWidget { const OnboardingScreen({super.key}); + @override + State createState() => _OnboardingScreenState(); +} + +class _OnboardingScreenState extends State { + UserRole? _selectedRole; + + void _handleJoin(BuildContext context) { + if (_selectedRole == null) return; + + final authService = Provider.of(context, listen: false); + + // Create a mock user based on selected role + final mockUser = UserModel( + id: 'user_123', + email: 'test@eduplay.ng', + name: 'Tunde', + role: _selectedRole!, + xp: _selectedRole == UserRole.student ? 100 : null, + streak: _selectedRole == UserRole.student ? 5 : null, + grade: _selectedRole == UserRole.student ? 'Primary 4' : null, + ); + + authService.login(mockUser); + + // Navigation will be handled by a wrapper or direct push + // For now, let's just push to the correct dashboard + _navigateToDashboard(context, _selectedRole!); + } + + void _navigateToDashboard(BuildContext context, UserRole role) { + // Auth state change in Provider will trigger MainWrapper to rebuild + // No need to manually navigate if we use a wrapper, but let's just make sure it triggers. + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 40), + Text( + 'Welcome to EduPlay!', + style: Theme.of(context).textTheme.displayMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + const Text( + 'Gamified learning designed for Nigerian students. Choose your role to get started.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 16, color: AppColors.textGrey), + ), + const SizedBox(height: 48), + Expanded( + child: GridView.count( + crossAxisCount: 2, + mainAxisSpacing: 16, + crossAxisSpacing: 16, + children: [ + _RoleCard( + title: 'Student', + icon: Icons.school, + isSelected: _selectedRole == UserRole.student, + onTap: () => setState(() => _selectedRole = UserRole.student), + ), + _RoleCard( + title: 'Parent', + icon: Icons.family_restroom, + isSelected: _selectedRole == UserRole.parent, + onTap: () => setState(() => _selectedRole = UserRole.parent), + ), + _RoleCard( + title: 'Teacher', + icon: Icons.person, + isSelected: _selectedRole == UserRole.teacher, + onTap: () => setState(() => _selectedRole = UserRole.teacher), + ), + _RoleCard( + title: 'Admin', + icon: Icons.admin_panel_settings, + isSelected: _selectedRole == UserRole.admin, + onTap: () => setState(() => _selectedRole = UserRole.admin), + ), + ], + ), + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: _selectedRole != null ? () => _handleJoin(context) : null, + child: const Text('Get Started'), + ), + const SizedBox(height: 20), + ], + ), + ), + ), + ); + } +} + +class _RoleCard extends StatelessWidget { + final String title; + final IconData icon; + final bool isSelected; + final VoidCallback onTap; + + const _RoleCard({ + required this.title, + required this.icon, + required this.isSelected, + required this.onTap, + }); + @override Widget build(BuildContext context) { - return const Scaffold( - body: Center( - child: Text('Onboarding Screen'), + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + decoration: BoxDecoration( + color: isSelected ? AppColors.primarySkyBlue : Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: isSelected ? AppColors.primarySkyBlue : Colors.grey.shade300, + width: 2, + ), + boxShadow: isSelected + ? [BoxShadow(color: AppColors.primarySkyBlue.withOpacity(0.3), blurRadius: 8, offset: const Offset(0, 4))] + : null, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + size: 48, + color: isSelected ? Colors.white : AppColors.primarySkyBlue, + ), + const SizedBox(height: 12), + Text( + title, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + color: isSelected ? Colors.white : AppColors.textDark, + ), + ), + ], + ), ), ); } -} \ No newline at end of file +} diff --git a/edu_play/lib/screens/parent/parent_dashboard_screen.dart b/edu_play/lib/screens/parent/parent_dashboard_screen.dart new file mode 100644 index 0000000..b2b0d49 --- /dev/null +++ b/edu_play/lib/screens/parent/parent_dashboard_screen.dart @@ -0,0 +1,157 @@ +import 'package:edu_play/services/auth_service.dart'; +import 'package:edu_play/styles/app_colors.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class ParentDashboardScreen extends StatelessWidget { + const ParentDashboardScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Parent Dashboard'), + actions: [ + IconButton( + icon: const Icon(Icons.logout), + onPressed: () => Provider.of(context, listen: false).logout(), + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _ChildOverviewCard( + name: 'Tunde', + grade: 'Primary 4', + completionRate: 0.75, + lastActive: 'Today, 10:30 AM', + ), + const SizedBox(height: 32), + Text('Learning Progress', style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 16), + const _ProgressItem(subject: 'Mathematics', progress: 0.8), + const SizedBox(height: 12), + const _ProgressItem(subject: 'English Language', progress: 0.6), + const SizedBox(height: 12), + const _ProgressItem(subject: 'Basic Science', progress: 0.4), + const Spacer(), + ElevatedButton.icon( + onPressed: () {}, + icon: const Icon(Icons.picture_as_pdf), + label: const Text('Export Monthly Report'), + ), + ], + ), + ), + ); + } +} + +class _ChildOverviewCard extends StatelessWidget { + final String name; + final String grade; + final double completionRate; + final String lastActive; + + const _ChildOverviewCard({ + required this.name, + required this.grade, + required this.completionRate, + required this.lastActive, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [AppColors.primarySkyBlue, Color(0xFF60A5FA)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(24), + ), + child: Column( + children: [ + Row( + children: [ + const CircleAvatar( + radius: 30, + backgroundColor: Colors.white, + child: Icon(Icons.person, size: 40, color: AppColors.primarySkyBlue), + ), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 20), + ), + Text( + grade, + style: TextStyle(color: Colors.white.withOpacity(0.9), fontSize: 14), + ), + ], + ), + ], + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Overall Completion', style: TextStyle(color: Colors.white)), + Text('${(completionRate * 100).toInt()}%', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), + ], + ), + const SizedBox(height: 8), + LinearProgressIndicator( + value: completionRate, + backgroundColor: Colors.white.withOpacity(0.3), + color: AppColors.accentWarmYellow, + borderRadius: BorderRadius.circular(10), + ), + const SizedBox(height: 12), + Text( + 'Last active: $lastActive', + style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 12), + ), + ], + ), + ); + } +} + +class _ProgressItem extends StatelessWidget { + final String subject; + final double progress; + + const _ProgressItem({required this.subject, required this.progress}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(subject, style: const TextStyle(fontWeight: FontWeight.w500)), + Text('${(progress * 100).toInt()}%'), + ], + ), + const SizedBox(height: 6), + LinearProgressIndicator( + value: progress, + backgroundColor: Colors.grey.shade200, + color: AppColors.primarySkyBlue, + borderRadius: BorderRadius.circular(10), + ), + ], + ); + } +} diff --git a/edu_play/lib/screens/parent_dashboard_screen.dart b/edu_play/lib/screens/parent_dashboard_screen.dart deleted file mode 100644 index 74e9ad9..0000000 --- a/edu_play/lib/screens/parent_dashboard_screen.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter/material.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'), - ), - ); - } -} \ No newline at end of file diff --git a/edu_play/lib/screens/student/lesson_view_screen.dart b/edu_play/lib/screens/student/lesson_view_screen.dart new file mode 100644 index 0000000..af651f9 --- /dev/null +++ b/edu_play/lib/screens/student/lesson_view_screen.dart @@ -0,0 +1,51 @@ +import 'package:edu_play/models/lesson_model.dart'; +import 'package:edu_play/styles/app_colors.dart'; +import 'package:flutter/material.dart'; + +class LessonViewScreen extends StatelessWidget { + final Lesson lesson; + + const LessonViewScreen({super.key, required this.lesson}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(lesson.title), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (lesson.mediaUrls.isNotEmpty) + Container( + height: 200, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(16), + ), + child: const Icon(Icons.image, size: 64, color: AppColors.textGrey), + ), + const SizedBox(height: 24), + Text( + lesson.title, + style: Theme.of(context).textTheme.displaySmall?.copyWith(fontSize: 24), + ), + const SizedBox(height: 16), + Text( + lesson.content, + style: const TextStyle(fontSize: 16, height: 1.6), + ), + const SizedBox(height: 40), + ElevatedButton( + onPressed: () => Navigator.pop(context), + child: const Text('Mark as Complete'), + ), + ], + ), + ), + ); + } +} diff --git a/edu_play/lib/screens/student/profile_screen.dart b/edu_play/lib/screens/student/profile_screen.dart new file mode 100644 index 0000000..a273684 --- /dev/null +++ b/edu_play/lib/screens/student/profile_screen.dart @@ -0,0 +1,103 @@ +import 'package:edu_play/services/auth_service.dart'; +import 'package:edu_play/styles/app_colors.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class ProfileScreen extends StatelessWidget { + const ProfileScreen({super.key}); + + @override + Widget build(BuildContext context) { + final user = Provider.of(context).currentUser!; + + return Scaffold( + appBar: AppBar(title: const Text('My Profile')), + body: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + const CircleAvatar( + radius: 60, + backgroundColor: AppColors.primarySkyBlue, + child: Icon(Icons.person, size: 80, color: Colors.white), + ), + const SizedBox(height: 16), + Text(user.name, style: Theme.of(context).textTheme.displaySmall), + Text(user.grade ?? '', style: const TextStyle(color: AppColors.textGrey, fontSize: 18)), + const SizedBox(height: 32), + _ProfileStatRow(xp: user.xp ?? 0, streak: user.streak ?? 0), + const SizedBox(height: 32), + const Divider(), + _ProfileMenuTile(title: 'Avatar Customization', icon: Icons.face, onTap: () {}), + _ProfileMenuTile(title: 'My Badges', icon: Icons.emoji_events, onTap: () {}), + _ProfileMenuTile(title: 'Settings', icon: Icons.settings, onTap: () {}), + _ProfileMenuTile( + title: 'Logout', + icon: Icons.logout, + color: AppColors.secondaryCoralRed, + onTap: () => Provider.of(context, listen: false).logout(), + ), + ], + ), + ), + ); + } +} + +class _ProfileStatRow extends StatelessWidget { + final int xp; + final int streak; + + const _ProfileStatRow({required this.xp, required this.streak}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _StatItem(label: 'Total XP', value: xp.toString(), icon: Icons.bolt, color: AppColors.accentWarmYellow), + _StatItem(label: 'Day Streak', value: streak.toString(), icon: Icons.local_fire_department, color: AppColors.secondaryCoralRed), + ], + ); + } +} + +class _StatItem extends StatelessWidget { + final String label; + final String value; + final IconData icon; + final Color color; + + const _StatItem({required this.label, required this.value, required this.icon, required this.color}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Icon(icon, color: color, size: 32), + const SizedBox(height: 8), + Text(value, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 24)), + Text(label, style: const TextStyle(color: AppColors.textGrey)), + ], + ); + } +} + +class _ProfileMenuTile extends StatelessWidget { + final String title; + final IconData icon; + final VoidCallback onTap; + final Color? color; + + const _ProfileMenuTile({required this.title, required this.icon, required this.onTap, this.color}); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: Icon(icon, color: color ?? AppColors.primarySkyBlue), + title: Text(title, style: TextStyle(color: color)), + trailing: const Icon(Icons.chevron_right), + onTap: onTap, + ); + } +} diff --git a/edu_play/lib/screens/student/quiz_screen.dart b/edu_play/lib/screens/student/quiz_screen.dart new file mode 100644 index 0000000..4a010d2 --- /dev/null +++ b/edu_play/lib/screens/student/quiz_screen.dart @@ -0,0 +1,172 @@ +import 'package:edu_play/models/quiz_model.dart'; +import 'package:edu_play/styles/app_colors.dart'; +import 'package:flutter/material.dart'; + +class QuizScreen extends StatefulWidget { + final Quiz quiz; + + const QuizScreen({super.key, required this.quiz}); + + @override + State createState() => _QuizScreenState(); +} + +class _QuizScreenState extends State { + int _currentQuestionIndex = 0; + int? _selectedOptionIndex; + bool _isAnswered = false; + int _score = 0; + + void _handleOptionSelect(int index) { + if (_isAnswered) return; + setState(() { + _selectedOptionIndex = index; + }); + } + + void _checkAnswer() { + if (_selectedOptionIndex == null) return; + + setState(() { + _isAnswered = true; + if (_selectedOptionIndex == widget.quiz.questions[_currentQuestionIndex].correctAnswerIndex) { + _score++; + } + }); + } + + void _nextQuestion() { + if (_currentQuestionIndex < widget.quiz.questions.length - 1) { + setState(() { + _currentQuestionIndex++; + _selectedOptionIndex = null; + _isAnswered = false; + }); + } else { + _showResults(); + } + } + + void _showResults() { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + title: const Text('Quiz Complete!'), + content: Text('You scored $_score out of ${widget.quiz.questions.length}'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); // Pop dialog + Navigator.of(context).pop(); // Pop quiz screen + }, + child: const Text('Finish'), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final question = widget.quiz.questions[_currentQuestionIndex]; + + return Scaffold( + appBar: AppBar( + title: Text('Question ${_currentQuestionIndex + 1}/${widget.quiz.questions.length}'), + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.of(context).pop(), + ), + ), + body: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LinearProgressIndicator( + value: (_currentQuestionIndex + 1) / widget.quiz.questions.length, + backgroundColor: Colors.grey.shade200, + color: AppColors.primarySkyBlue, + borderRadius: BorderRadius.circular(10), + ), + const SizedBox(height: 40), + Text( + question.question, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 32), + ...List.generate(question.options.length, (index) { + final isSelected = _selectedOptionIndex == index; + final isCorrect = question.correctAnswerIndex == index; + + Color borderColor = Colors.grey.shade300; + Color bgColor = Colors.white; + + if (_isAnswered) { + if (isCorrect) { + borderColor = AppColors.secondaryGreen; + bgColor = AppColors.secondaryGreen.withOpacity(0.1); + } else if (isSelected) { + borderColor = AppColors.secondaryCoralRed; + bgColor = AppColors.secondaryCoralRed.withOpacity(0.1); + } + } else if (isSelected) { + borderColor = AppColors.primarySkyBlue; + bgColor = AppColors.primarySkyBlue.withOpacity(0.1); + } + + return GestureDetector( + onTap: () => _handleOptionSelect(index), + child: Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: borderColor, width: 2), + ), + child: Row( + children: [ + Text( + String.fromCharCode(65 + index), + style: TextStyle( + fontWeight: FontWeight.bold, + color: isSelected ? AppColors.primarySkyBlue : AppColors.textGrey, + ), + ), + const SizedBox(width: 16), + Expanded(child: Text(question.options[index])), + if (_isAnswered && isCorrect) + const Icon(Icons.check_circle, color: AppColors.secondaryGreen), + if (_isAnswered && isSelected && !isCorrect) + const Icon(Icons.cancel, color: AppColors.secondaryCoralRed), + ], + ), + ), + ); + }), + const Spacer(), + if (_isAnswered) + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + 'Explanation: ${question.explanation}', + style: const TextStyle(fontSize: 14, fontStyle: FontStyle.italic), + ), + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: _selectedOptionIndex != null ? (_isAnswered ? _nextQuestion : _checkAnswer) : null, + child: Text(_isAnswered ? 'Next Question' : 'Check Answer'), + ), + ], + ), + ), + ); + } +} diff --git a/edu_play/lib/screens/student/student_dashboard_screen.dart b/edu_play/lib/screens/student/student_dashboard_screen.dart new file mode 100644 index 0000000..16e7b67 --- /dev/null +++ b/edu_play/lib/screens/student/student_dashboard_screen.dart @@ -0,0 +1,307 @@ +import 'package:edu_play/data/mock_data.dart'; +import 'package:edu_play/models/user_model.dart'; +import 'package:edu_play/screens/student/profile_screen.dart'; +import 'package:edu_play/screens/student/subject_lessons_screen.dart'; +import 'package:edu_play/services/auth_service.dart'; +import 'package:edu_play/styles/app_colors.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class StudentDashboardScreen extends StatefulWidget { + const StudentDashboardScreen({super.key}); + + @override + State createState() => _StudentDashboardScreenState(); +} + +class _StudentDashboardScreenState extends State { + int _currentIndex = 0; + + @override + Widget build(BuildContext context) { + final user = Provider.of(context).currentUser!; + + return Scaffold( + body: _currentIndex == 0 ? _buildHome(context, user) : const ProfileScreen(), + bottomNavigationBar: BottomNavigationBar( + currentIndex: _currentIndex, + onTap: (index) => setState(() => _currentIndex = index), + items: const [ + BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'), + BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'), + ], + ), + ); + } + + Widget _buildHome(BuildContext context, UserModel user) { + return CustomScrollView( + slivers: [ + SliverAppBar( + expandedHeight: 200.0, + floating: false, + pinned: true, + flexibleSpace: FlexibleSpaceBar( + title: Text('Hi, ${user.name}!'), + background: Container( + color: AppColors.primarySkyBlue, + child: Padding( + padding: const EdgeInsets.only(top: 80, left: 24, right: 24), + child: Row( + children: [ + _StatCard( + icon: Icons.bolt, + value: '${user.xp}', + label: 'XP', + color: AppColors.accentWarmYellow, + ), + const SizedBox(width: 16), + _StatCard( + icon: Icons.local_fire_department, + value: '${user.streak}', + label: 'Days', + color: AppColors.secondaryCoralRed, + ), + ], + ), + ), + ), + ), + actions: [ + IconButton( + icon: const Icon(Icons.logout, color: Colors.white), + onPressed: () => Provider.of(context, listen: false).logout(), + ), + ], + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Daily Missions', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 16), + const _MissionCard( + title: 'Math Whiz', + description: 'Complete 2 addition lessons', + progress: 0.5, + ), + const SizedBox(height: 12), + const _MissionCard( + title: 'Word Master', + description: 'Pass the Nouns quiz', + progress: 0.0, + ), + const SizedBox(height: 32), + Text( + 'Jump Back In', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 16), + _SubjectCard( + title: 'Mathematics', + grade: user.grade ?? 'Primary 4', + icon: Icons.numbers, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => SubjectLessonsScreen(subject: mockSubjects[0]), + ), + ); + }, + ), + const SizedBox(height: 12), + _SubjectCard( + title: 'English Language', + grade: user.grade ?? 'Primary 4', + icon: Icons.book, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => SubjectLessonsScreen(subject: mockSubjects[1]), + ), + ); + }, + ), + ], + ), + ), + ), + ], + ); + } +} + +class _StatCard extends StatelessWidget { + final IconData icon; + final String value; + final String label; + final Color color; + + const _StatCard({ + required this.icon, + required this.value, + required this.label, + required this.color, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(16), + ), + child: Row( + children: [ + Icon(icon, color: color, size: 24), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + value, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + Text( + label, + style: TextStyle( + color: Colors.white.withOpacity(0.8), + fontSize: 12, + ), + ), + ], + ), + ], + ), + ); + } +} + +class _MissionCard extends StatelessWidget { + final String title; + final String description; + final double progress; + + const _MissionCard({ + required this.title, + required this.description, + required this.progress, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + Text( + '${(progress * 100).toInt()}%', + style: const TextStyle(color: AppColors.primarySkyBlue, fontWeight: FontWeight.bold), + ), + ], + ), + const SizedBox(height: 4), + Text(description, style: const TextStyle(color: AppColors.textGrey, fontSize: 14)), + const SizedBox(height: 12), + LinearProgressIndicator( + value: progress, + backgroundColor: Colors.grey.shade200, + color: AppColors.secondaryGreen, + borderRadius: BorderRadius.circular(10), + ), + ], + ), + ); + } +} + +class _SubjectCard extends StatelessWidget { + final String title; + final String grade; + final IconData icon; + final VoidCallback onTap; + + const _SubjectCard({ + required this.title, + required this.grade, + required this.icon, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(16), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.grey.shade200), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.primarySkyBlue.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, color: AppColors.primarySkyBlue), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + Text( + grade, + style: const TextStyle(color: AppColors.textGrey, fontSize: 14), + ), + ], + ), + ), + const Icon(Icons.chevron_right, color: AppColors.textGrey), + ], + ), + ), + ); + } +} diff --git a/edu_play/lib/screens/student/subject_lessons_screen.dart b/edu_play/lib/screens/student/subject_lessons_screen.dart new file mode 100644 index 0000000..ac19f28 --- /dev/null +++ b/edu_play/lib/screens/student/subject_lessons_screen.dart @@ -0,0 +1,157 @@ +import 'package:edu_play/models/lesson_model.dart'; +import 'package:edu_play/models/quiz_model.dart'; +import 'package:edu_play/data/mock_data.dart'; +import 'package:edu_play/screens/student/lesson_view_screen.dart'; +import 'package:edu_play/screens/student/quiz_screen.dart'; +import 'package:edu_play/styles/app_colors.dart'; +import 'package:flutter/material.dart'; + +class SubjectLessonsScreen extends StatelessWidget { + final Subject subject; + + const SubjectLessonsScreen({super.key, required this.subject}); + + @override + Widget build(BuildContext context) { + final topics = mockTopics.where((t) => t.subjectId == subject.id).toList(); + + return Scaffold( + appBar: AppBar( + title: Text(subject.name), + ), + body: ListView.builder( + padding: const EdgeInsets.all(24), + itemCount: topics.length, + itemBuilder: (context, index) { + final topic = topics[index]; + return _TopicExpansionTile(topic: topic, subjectName: subject.name); + }, + ), + ); + } +} + +class _TopicExpansionTile extends StatelessWidget { + final Topic topic; + final String subjectName; + + const _TopicExpansionTile({required this.topic, required this.subjectName}); + + @override + Widget build(BuildContext context) { + return Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: Colors.grey.shade200), + ), + margin: const EdgeInsets.only(bottom: 16), + child: ExpansionTile( + title: Text(topic.name, style: const TextStyle(fontWeight: FontWeight.bold)), + leading: const Icon(Icons.folder_open, color: AppColors.primarySkyBlue), + children: [ + _LessonTile( + title: 'Introduction to ${topic.name}', + isCompleted: true, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => LessonViewScreen( + lesson: Lesson( + id: 'l1', + topicId: topic.id, + title: 'Introduction to ${topic.name}', + content: 'In this lesson, we will learn the basics of ${topic.name}. ' + 'It is a fundamental concept in $subjectName and will help you solve many problems. ' + 'Pay close attention to the examples!', + mediaUrls: ['https://example.com/image.png'], + ), + ), + ), + ); + }, + ), + _LessonTile( + title: 'Advanced ${topic.name}', + isCompleted: false, + onTap: () {}, + ), + _QuizTile( + title: '${topic.name} Challenge', + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => QuizScreen( + quiz: Quiz( + id: 'q1', + lessonId: 'l1', + questions: [ + QuizQuestion( + id: 'qq1', + question: 'What is 5 + 7?', + options: ['10', '11', '12', '13'], + correctAnswerIndex: 2, + explanation: '5 plus 7 equals 12.', + ), + QuizQuestion( + id: 'qq2', + question: 'If you have 3 apples and buy 4 more, how many do you have?', + options: ['6', '7', '8', '9'], + correctAnswerIndex: 1, + explanation: '3 + 4 = 7.', + ), + ], + ), + ), + ), + ); + }, + ), + ], + ), + ); + } +} + +class _LessonTile extends StatelessWidget { + final String title; + final bool isCompleted; + final VoidCallback onTap; + + const _LessonTile({ + required this.title, + required this.isCompleted, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(title, style: const TextStyle(fontSize: 14)), + trailing: Icon( + isCompleted ? Icons.check_circle : Icons.play_circle_outline, + color: isCompleted ? AppColors.secondaryGreen : AppColors.primarySkyBlue, + ), + onTap: onTap, + ); + } +} + +class _QuizTile extends StatelessWidget { + final String title; + final VoidCallback onTap; + + const _QuizTile({required this.title, required this.onTap}); + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), + leading: const Icon(Icons.help_outline, color: AppColors.accentWarmYellow), + trailing: const Icon(Icons.star, color: AppColors.accentWarmYellow), + onTap: onTap, + ); + } +} diff --git a/edu_play/lib/screens/student_dashboard_screen.dart b/edu_play/lib/screens/student_dashboard_screen.dart deleted file mode 100644 index afc4bba..0000000 --- a/edu_play/lib/screens/student_dashboard_screen.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter/material.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'), - ), - ); - } -} \ No newline at end of file diff --git a/edu_play/lib/screens/teacher/teacher_dashboard_screen.dart b/edu_play/lib/screens/teacher/teacher_dashboard_screen.dart new file mode 100644 index 0000000..6bf98bb --- /dev/null +++ b/edu_play/lib/screens/teacher/teacher_dashboard_screen.dart @@ -0,0 +1,165 @@ +import 'package:edu_play/services/auth_service.dart'; +import 'package:edu_play/styles/app_colors.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class TeacherDashboardScreen extends StatelessWidget { + const TeacherDashboardScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Teacher Dashboard'), + actions: [ + IconButton( + icon: const Icon(Icons.logout), + onPressed: () => Provider.of(context, listen: false).logout(), + ), + ], + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Class Performance (JSS 1)', style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 16), + const _AnalyticsCard( + title: 'Avg. Quiz Score', + value: '82%', + trend: '+5%', + color: AppColors.secondaryGreen, + ), + const SizedBox(height: 12), + const _AnalyticsCard( + title: 'Active Students', + value: '24/30', + trend: 'Stable', + color: AppColors.primarySkyBlue, + ), + const SizedBox(height: 32), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Recent Assignments', style: Theme.of(context).textTheme.titleLarge), + TextButton(onPressed: () {}, child: const Text('View All')), + ], + ), + const SizedBox(height: 12), + const _AssignmentTile( + title: 'Algebra Intro', + dueDate: 'Due Tomorrow', + submittedCount: 18, + totalCount: 30, + ), + const SizedBox(height: 12), + const _AssignmentTile( + title: 'Grammar: Nouns', + dueDate: 'Due in 3 days', + submittedCount: 5, + totalCount: 30, + ), + const SizedBox(height: 32), + ElevatedButton( + onPressed: () {}, + child: const Text('Create New Assignment'), + ), + ], + ), + ), + ); + } +} + +class _AnalyticsCard extends StatelessWidget { + final String title; + final String value; + final String trend; + final Color color; + + const _AnalyticsCard({ + required this.title, + required this.value, + required this.trend, + required this.color, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: color.withOpacity(0.3)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: const TextStyle(color: AppColors.textGrey, fontSize: 14)), + const SizedBox(height: 4), + Text(value, style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 24)), + ], + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + trend, + style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold), + ), + ), + ], + ), + ); + } +} + +class _AssignmentTile extends StatelessWidget { + final String title; + final String dueDate; + final int submittedCount; + final int totalCount; + + const _AssignmentTile({ + required this.title, + required this.dueDate, + required this.submittedCount, + required this.totalCount, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.grey.shade200), + ), + child: Row( + children: [ + const Icon(Icons.assignment, color: AppColors.primarySkyBlue), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), + Text(dueDate, style: const TextStyle(color: AppColors.secondaryCoralRed, fontSize: 12)), + ], + ), + ), + Text('$submittedCount/$totalCount', style: const TextStyle(fontWeight: FontWeight.bold)), + ], + ), + ); + } +} diff --git a/edu_play/lib/screens/teacher_dashboard_screen.dart b/edu_play/lib/screens/teacher_dashboard_screen.dart deleted file mode 100644 index b80d396..0000000 --- a/edu_play/lib/screens/teacher_dashboard_screen.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter/material.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'), - ), - ); - } -} \ No newline at end of file diff --git a/edu_play/lib/services/auth_service.dart b/edu_play/lib/services/auth_service.dart new file mode 100644 index 0000000..18ead3d --- /dev/null +++ b/edu_play/lib/services/auth_service.dart @@ -0,0 +1,35 @@ +import 'package:edu_play/models/user_model.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hive_flutter/hive_flutter.dart'; + +class AuthService extends ChangeNotifier { + UserModel? _currentUser; + final Box _settingsBox = Hive.box('settings'); + + UserModel? get currentUser => _currentUser; + bool get isAuthenticated => _currentUser != null; + + AuthService() { + _loadUser(); + } + + void _loadUser() { + final userData = _settingsBox.get('user'); + if (userData != null) { + _currentUser = UserModel.fromJson(Map.from(userData)); + notifyListeners(); + } + } + + Future login(UserModel user) async { + _currentUser = user; + await _settingsBox.put('user', user.toJson()); + notifyListeners(); + } + + Future logout() async { + _currentUser = null; + await _settingsBox.delete('user'); + notifyListeners(); + } +} diff --git a/edu_play/lib/styles/app_colors.dart b/edu_play/lib/styles/app_colors.dart new file mode 100644 index 0000000..12e1b28 --- /dev/null +++ b/edu_play/lib/styles/app_colors.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); + + static const Color backgroundLight = Color(0xFFF8FAFC); + static const Color textDark = Color(0xFF1E293B); + static const Color textGrey = Color(0xFF64748B); +} diff --git a/edu_play/lib/styles/app_theme.dart b/edu_play/lib/styles/app_theme.dart new file mode 100644 index 0000000..b8d3ae8 --- /dev/null +++ b/edu_play/lib/styles/app_theme.dart @@ -0,0 +1,57 @@ +import 'package:edu_play/styles/app_colors.dart'; +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class AppTheme { + static ThemeData get lightTheme { + return ThemeData( + useMaterial3: true, + colorScheme: ColorScheme.fromSeed( + seedColor: AppColors.primarySkyBlue, + primary: AppColors.primarySkyBlue, + secondary: AppColors.accentWarmYellow, + error: AppColors.secondaryCoralRed, + surface: AppColors.backgroundLight, + ), + textTheme: GoogleFonts.nunitoTextTheme().copyWith( + displayLarge: GoogleFonts.poppins( + fontWeight: FontWeight.bold, + color: AppColors.textDark, + ), + displayMedium: GoogleFonts.poppins( + fontWeight: FontWeight.bold, + color: AppColors.textDark, + ), + titleLarge: GoogleFonts.poppins( + fontWeight: FontWeight.w600, + color: AppColors.textDark, + ), + ), + scaffoldBackgroundColor: AppColors.backgroundLight, + appBarTheme: const AppBarTheme( + backgroundColor: Colors.transparent, + elevation: 0, + centerTitle: true, + titleTextStyle: TextStyle( + color: AppColors.textDark, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primarySkyBlue, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + textStyle: GoogleFonts.poppins( + fontWeight: FontWeight.w600, + fontSize: 16, + ), + ), + ), + ); + } +} diff --git a/edu_play/pubspec.lock b/edu_play/pubspec.lock index fa6ef73..d696a0d 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: @@ -401,10 +401,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..19eb40b 100644 --- a/edu_play/test/widget_test.dart +++ b/edu_play/test/widget_test.dart @@ -1,30 +1,27 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - import 'package:edu_play/main.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:flutter/material.dart'; +import 'dart:io'; void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); + // Setup Hive for testing + setUp(() async { + final tempDir = await Directory.systemTemp.createTemp(); + Hive.init(tempDir.path); + await Hive.openBox('settings'); + }); - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); + tearDown(() async { + await Hive.close(); + }); - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); + testWidgets('App starts with Onboarding Screen', (WidgetTester tester) async { + await tester.pumpWidget(const MyApp()); + await tester.pumpAndSettle(); - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); + // Verify that we are on the onboarding screen + expect(find.text('Welcome to EduPlay!'), findsOneWidget); + expect(find.text('Student'), findsOneWidget); }); } diff --git a/edu_play/web/favicon.png b/edu_play/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/edu_play/web/favicon.png differ diff --git a/edu_play/web/icons/Icon-192.png b/edu_play/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/edu_play/web/icons/Icon-192.png differ diff --git a/edu_play/web/icons/Icon-512.png b/edu_play/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/edu_play/web/icons/Icon-512.png differ diff --git a/edu_play/web/icons/Icon-maskable-192.png b/edu_play/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/edu_play/web/icons/Icon-maskable-192.png differ diff --git a/edu_play/web/icons/Icon-maskable-512.png b/edu_play/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/edu_play/web/icons/Icon-maskable-512.png differ diff --git a/edu_play/web/index.html b/edu_play/web/index.html new file mode 100644 index 0000000..1415161 --- /dev/null +++ b/edu_play/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + edu_play + + + + + + diff --git a/edu_play/web/manifest.json b/edu_play/web/manifest.json new file mode 100644 index 0000000..089f8d3 --- /dev/null +++ b/edu_play/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "edu_play", + "short_name": "edu_play", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +}