diff --git a/d b/d new file mode 100644 index 00000000..cfb13278 --- /dev/null +++ b/d @@ -0,0 +1,79 @@ +86b3463 (HEAD -> dev-led, origin/main, origin/dev-led, origin/HEAD, main) style: improve user profile UI/UX +3b59a19 feat: reset password via email +0eb834e dev: very rough release candidate v0.0.1 +a8ca87d chore: sync git +ea5178b Quiz mode and upload feature (#28) +de56798 dev: basic profile screen +59b56f3 dev: auth sign-out option +d3a2d13 dev: backend code for email & password authentication +cc03ebc dev: other experimental rich text editor using Markdown (w/ latex support) +930ece0 style: improve layout and visual elements in email verification screen +d5c7456 dev: backend code for email verification +ed8ace7 dev: add working BE code for creating a user +3cb413d dev: add UserEntity +3b2e6d8 (origin/dev) chore: daily git sync +40533dd dev: explore the viability of flutter_quill text editor (experimental) +9558e2f deps: add flutter_quill and set proper configurations +cd06c58 style: add signup screen and validators +edb7f15 dev: git daily sync +e97d12f style: add reset password screen, password reset email confirmation, and validators +7dfe0ec style: add login screen +cbb8640 refactor: convert Entity and Params interfaces to base classes +cd01dd1 dev: add custom Failure classes +dbe2873 refactor: improve custom Exception classes +f68d955 dev: add utility exception classes (auth and db) +c05e662 dev: setup the folder structure for the core features: auth, app_settings, and system +76928c8 style: setup general screens +664bd8f dev: add no_params class and use case interface +545489d dev: add failure and params interfaces +6c219de dev: add entity and exception interfaces +686e5c8 style: add icon launcher for android and web +272adc3 style: implement a native splash screen (supports light and dark mode) +bcedf26 dev: clean assets folder and add sg-scholar's guide logos +3b35948 dev: refactor app_router and upgrade go_router dependency +5dd9766 dev: implement a nested and stateful router (using go_router and NavigationBar) +d59ab29 style: add different app themes +779c18b dev: add a ServiceLocator (using get_it package) +3212041 dev: add UpCampuses enum +7d3bad7 dev: add StorageTypes enum +67c4a8b dev: add Genders and Sex enums +3070e29 dev: add OnlineTimeSource enum +eb79b9c dev: add AccountTypes enum +c7e8640 dev: add AppColors utility class +7a0aed5 fix: add flutter cli in github ci +b46554b fix: enable firebase experimental web framework support (2) +45a3b2a fix: enable firebase experimental web framework support +fdbe1b0 fix: remove npm run build error +1815a27 fix: remove npm ci error (2) via generation of package-lock.json +4dc3927 fix: remove npm ci error +af535cf feat: enable firebase web app hosting +fc57e15 fix: add multidex support +f0c1528 deps: add go_router, bloc, and tes oriented dev dependencies +774b890 deps: add relevant firebase features +a726300 deps: initialize firebase core +6fec13f chore: remove remnant node_modules dir +a4011e5 chore: add foundational assets +a2c1c49 chore: initialize flutter project +caf3db3 chore: Add LICENSE +2d9e7c0 Update README.md +c910fd2 Update README.md +96d01e6 chore: wipe repo (change from nuxt to flutter) +cd12869 Update README.md +5f9cc43 Initialize firebase hosting +6221d0e Add firebase +b83e093 Add basic email verification logic +285bc5d Add basic forgot password logic +dc03fcb Add basic login auth logic (using Appwrite) +f761b88 Add email verification page view +83ca2c1 Add sign-up page view +293f5cf Add forgot password page view +bd76adb Add login page view +0ea4124 Update README.md +3bf6648 feat: Initialize project - Led (#20) +42d4622 Initialize WebStorm IDE dev environment +cdd7465 Create README.md +1bd46ad Create SECURITY.md (#2) +44a9f4b Update FUNDING.yml +03ca1d5 Create FUNDING.yml +60caecf Update issue templates +072f3a6 Initial commit diff --git a/lib/core/models/firestore_model.dart b/lib/core/models/firestore_model.dart index 3e389584..fa508e7c 100644 --- a/lib/core/models/firestore_model.dart +++ b/lib/core/models/firestore_model.dart @@ -4,6 +4,9 @@ import 'package:scholars_guide/core/models/question_model.dart'; class FireStore { //* Collection names in the Firestore + static const solutionsCollection = 'solutions'; + static const commentsCollection = 'comments'; + static const additionalQuestionDataRef = 'additionalQuestionsData'; static const subjects = { SUBJ.MATH: 'mathematicsQuestions', SUBJ.SCIENCE: 'scienceQuestions', @@ -15,6 +18,8 @@ class FireStore { //* Field names of each document in the Firestore // Question related fields static const String question = 'question'; + static const String solutionRef = 'solutionsRef'; + static const String solutionData = 'solutionData'; static const String options = 'choices'; static const String correctIndex = 'correctChoiceKey'; // static const String questionReference = 'additionalQuestionDataRef'; diff --git a/lib/core/models/question_model.dart b/lib/core/models/question_model.dart index 23aa2c5c..0ecbdde7 100644 --- a/lib/core/models/question_model.dart +++ b/lib/core/models/question_model.dart @@ -2,23 +2,41 @@ import 'dart:math'; +import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:scholars_guide/core/models/firestore_model.dart'; enum SUBJ { MATH, SCIENCE, READING, LANGUAGE, ALL } class Question { - final String question; - final List options; - final int correctIndex; - final SUBJ subject; - - const Question({ + Question({ required this.question, required this.options, required this.correctIndex, required this.subject, + this.id = '', + this.solution = '', + this.solutionRef, + this.createdBy, }); + final String question; + final List options; + final int correctIndex; + final SUBJ subject; + String id; + String solution; + DocumentReference? solutionRef; + + DocumentReference? createdBy; + final FieldValue createdAt = FieldValue.serverTimestamp(); + + static var updatedAt = FieldValue.serverTimestamp(); + static const String updatedBy = ''; // ! This is a placeholder + + static bool isVerified = false; + static const String verifiedAt = ''; + static const String verifiedBy = ''; + void printQuestion() { print('Question: $question'); print('Options: $options'); @@ -26,20 +44,22 @@ class Question { } // Returns a Question Class using data fetched from the Firestore - factory Question.fromMap(Map data, SUBJ subject) { + factory Question.fromMap(String id, Map data, SUBJ subject) { List temp = []; for (var val in data[FireStore.options].values) { temp.add(val); } - - String temp2 = temp[int.parse(data[FireStore.correctIndex])]; + String temp2 = temp[int.parse(data[FireStore.correctIndex])]; temp.shuffle(Random()); return Question( + id: id, subject: subject, question: data[FireStore.question], + solutionRef: data[FireStore.solutionRef], options: temp, - correctIndex: temp.indexWhere((element) => element == temp2)); + correctIndex: temp.indexWhere((element) => element == temp2), + createdBy: data[FireStore.createdBy]); } // Converts the Question Class to a json like file to be stored in the Firestore @@ -53,6 +73,9 @@ class Question { '3': options[3], }, FireStore.correctIndex: correctIndex.toString(), + FireStore.solutionRef: solutionRef, + FireStore.createdBy: createdBy, + FireStore.createdAt: createdAt, }; } diff --git a/lib/features/quiz_mode/data/data_sources/remote_data_source/fetch_questions.dart b/lib/features/quiz_mode/data/data_sources/remote_data_source/fetch_questions.dart deleted file mode 100644 index 220aad2f..00000000 --- a/lib/features/quiz_mode/data/data_sources/remote_data_source/fetch_questions.dart +++ /dev/null @@ -1,22 +0,0 @@ - -// import 'package:scholars_guide/service_locator/service_locator.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; - -Future fetchQuestions(String subject) { - return FirebaseFirestore.instance.collection(subject).get(); -} - - - -// FirebaseFirestore.instance.collection('math-questions') -// .doc(blank means random generate) -// .get() -// .set( some json data) -// .snapshots() returns - - -// json example = { -// "question": "What is 2 + 2?", -// "options": ["1", "2", "3", "4"], -// "answer": "4" -// } \ No newline at end of file diff --git a/lib/features/quiz_mode/data/repositories/quiz_mode_repository_impl.dart b/lib/features/quiz_mode/data/repositories/quiz_mode_repository_impl.dart index db778355..4c21222e 100644 --- a/lib/features/quiz_mode/data/repositories/quiz_mode_repository_impl.dart +++ b/lib/features/quiz_mode/data/repositories/quiz_mode_repository_impl.dart @@ -3,29 +3,37 @@ // import 'package:scholars_guide/service_locator/service_locator.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:firebase_core/firebase_core.dart'; import 'package:scholars_guide/core/models/firestore_model.dart'; import 'package:scholars_guide/core/models/question_model.dart'; import 'package:scholars_guide/features/quiz_mode/domain/repositories_contract/quiz_mode_repository_contract.dart'; -import 'package:scholars_guide/firebase_options.dart'; -class QuizModeRepositoryImpl implements QuizModeRepositoryContract{ +import '../../../../service_locator/service_locator.dart'; + +class QuizModeRepositoryImpl implements QuizModeRepositoryContract { const QuizModeRepositoryImpl(); - + @override Future> collectQuestions({required SUBJ subj}) async { + final dbService = services(); List questions = []; - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform) - .whenComplete(() => FirebaseFirestore.instance - .collection(FireStore.SUBJ2subject(subj)) - .get() - .then((snapshot) => snapshot.docs - .map((e) => questions.add(Question.fromMap(e.data(), subj))).toList())); + // * Fetching the questions from the Firestore + await dbService + .collection(FireStore.SUBJ2subject(subj)) + .get() + .then((snapshot) { + print("GETTING QUESTIONS"); + snapshot.docs + .map((e) => questions.add(Question.fromMap(e.id, e.data(), subj))) + .toList(); + }); - // TODO: Update imports and change actual code below once get_it is done - // await fetchQuestions(subject).then((snapshot) => snapshot.docs.map((e) => questions.add(Question.fromMap(e.data(), subj)))); - print("DONE COLLECTING QUESTIONS ========================================"); // ! Debugging - return questions; + // * Fetching the solutions from the Firestore + for (Question q in questions) { + await q.solutionRef?.get().then((value) { + q.solution = value[FireStore.solutionData]; + }); } + return questions; } +} diff --git a/lib/features/quiz_mode/domain/usecases/select_questions.dart b/lib/features/quiz_mode/domain/usecases/select_questions.dart index 20df0cad..c85c2e24 100644 --- a/lib/features/quiz_mode/domain/usecases/select_questions.dart +++ b/lib/features/quiz_mode/domain/usecases/select_questions.dart @@ -13,7 +13,6 @@ class ChooseQuestions { List totalQuestions = await const QuizModeRepositoryImpl().collectQuestions(subj: subj); totalQuestions.shuffle(Random()); - // TODO: randomize the choices as well for each question return totalQuestions.sublist( 0, diff --git a/lib/features/quiz_mode/presentation/pages/finished_quiz_page.dart b/lib/features/quiz_mode/presentation/pages/finished_quiz_page.dart index 74e915ba..ac677d71 100644 --- a/lib/features/quiz_mode/presentation/pages/finished_quiz_page.dart +++ b/lib/features/quiz_mode/presentation/pages/finished_quiz_page.dart @@ -1,50 +1,107 @@ // ignore_for_file: prefer_const_constructors import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:scholars_guide/core/models/question_model.dart'; import 'package:scholars_guide/features/quiz_mode/presentation/state_management/quiz_card/quiz_card_cubit.dart'; import 'package:scholars_guide/features/quiz_mode/presentation/widgets/quiz_widgets/question_display.dart'; -import 'ready_quiz_page.dart'; - class FinishedQuizPage extends StatelessWidget { - const FinishedQuizPage({super.key, required this.subjectQuestionsMap, required this.subject}); - - final Map> subjectQuestionsMap; - final SUBJ subject; + const FinishedQuizPage({super.key}); @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Quiz Results'), - ), - body: Center( - child: Column( - children: [ - Text("Congrats on finishing! Let's see how you did"), + final extraMap = GoRouterState.of(context).extra as Map; + final SUBJ subject = extraMap['subject'] as SUBJ; + final Map> subjectQuestionsMap = + extraMap['subjectQuestionsMap'] as Map>; - QuestionDisplay( - subjectQuestionsMap: subjectQuestionsMap, - subject: subject), + final int score = + countCorrect(quizCardCubitList: subjectQuestionsMap[subject]!); + final int scorePercentage = + ((score / subjectQuestionsMap[subject]!.length) * 100).round(); - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => (ReadyQuizPage())), - ); - }, - child: Text('Take Another Quiz'), + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: const Text('Quiz Results'), + ), + body: SingleChildScrollView( + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(10.0), + margin: EdgeInsets.only(top: 20, bottom: 10), + child: Text( + "Congrats on finishing! Let's see how you did", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), - ElevatedButton( - onPressed: () { - // TODO: Push to the home page - }, - child: Text("Back to the home page"), + ), + Card( + child: Container( + width: MediaQuery.of(context).size.width * 0.5, + margin: EdgeInsets.only(top: 20, bottom: 10), + child: Column( + children: [ + Text( + "Score", + style: + TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + Container( + padding: EdgeInsets.all(10), + child: Text( + "$score/${subjectQuestionsMap[subject]?.length}", + style: TextStyle( + color: scorePercentage <= 60 + ? Colors.red + : scorePercentage <= 80 + ? Colors.orange + : Colors.green, + fontSize: 20, + fontWeight: FontWeight.bold), + ), + ), + ElevatedButton( + onPressed: () { + GoRouter.of(context).go( + '/quiz-mode/solutions-quiz', + extra: { + 'subjectQuestionsMap': subjectQuestionsMap, + 'subject': subject + }, + ); + }, + child: Text("Review"), + ), + ], + ), ), - ], - ), - )); + ), + QuestionDisplay( + subjectQuestionsMap: subjectQuestionsMap, subject: subject), + ElevatedButton( + onPressed: () { + GoRouter.of(context).go('/quiz-mode'); + }, + child: Text('Take Another Quiz'), + ), + SizedBox( + height: 20, + ), + ], + ), + ), + ); + } +} + +int countCorrect({required List quizCardCubitList}) { + int count = 0; + for (QuizCardCubit cubit in quizCardCubitList) { + if (cubit.state.chosenIndex == cubit.correctIndex) { + count++; + } } + return count; } diff --git a/lib/features/quiz_mode/presentation/pages/quiz_page.dart b/lib/features/quiz_mode/presentation/pages/quiz_page.dart index a251ed2b..fa997f1d 100644 --- a/lib/features/quiz_mode/presentation/pages/quiz_page.dart +++ b/lib/features/quiz_mode/presentation/pages/quiz_page.dart @@ -1,21 +1,20 @@ // ignore_for_file: prefer_const_literals_to_create_immutables, prefer_const_constructors, curly_braces_in_flow_control_structures import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; import 'package:scholars_guide/core/models/question_model.dart'; -import 'package:scholars_guide/features/quiz_mode/presentation/pages/ready_quiz_page.dart'; import 'package:scholars_guide/features/quiz_mode/presentation/state_management/quiz/quiz_bloc.dart'; -import 'package:scholars_guide/features/quiz_mode/presentation/widgets/quiz_page_widgets/confirm_cancel_quiz_display.dart'; -import 'package:scholars_guide/features/quiz_mode/presentation/widgets/quiz_page_widgets/confirm_submit_quiz_display.dart'; -import 'package:scholars_guide/features/quiz_mode/presentation/widgets/quiz_page_widgets/confirm_timeout_quiz_display.dart'; +import 'package:scholars_guide/features/quiz_mode/presentation/widgets/quiz_page_widgets/confirm_cancel_quiz_dialogue.dart'; +import 'package:scholars_guide/features/quiz_mode/presentation/widgets/quiz_page_widgets/confirm_submit_quiz_dialogue.dart'; import 'package:scholars_guide/features/quiz_mode/presentation/widgets/quiz_page_widgets/question_loading_display.dart'; import 'package:scholars_guide/features/quiz_mode/presentation/widgets/quiz_page_widgets/timer_display.dart'; import 'package:scholars_guide/features/quiz_mode/presentation/widgets/quiz_widgets/question_display.dart'; class QuizPage extends StatefulWidget { - const QuizPage({super.key, required this.subject}); + const QuizPage({super.key}); - final SUBJ subject; final int numQuestions = 10; // ! For now, only 10 question @override @@ -25,61 +24,84 @@ class QuizPage extends StatefulWidget { class _QuizPageState extends State { @override Widget build(BuildContext context) { + final subject = GoRouterState.of(context).extra as SUBJ; + return BlocProvider( - create: (context) => QuizBloc() - ..add(QuizLoadQuestions( - subject: widget.subject, numQuestions: widget.numQuestions)), - child: Builder(builder: (builderContext) { + create: (context) => QuizBloc() + ..add( + QuizLoadQuestions( + subject: subject, + numQuestions: widget.numQuestions, + ), + ), + child: Builder( + builder: (builderContext) { return Scaffold( - appBar: AppBar( - title: Text('${Question.SUBJ2string(widget.subject)} Quiz'), - leading: IconButton( - icon: Icon(Icons.arrow_back), - onPressed: () { - BlocProvider.of(builderContext) - .add(QuizCancelBtnPressed()); + appBar: AppBar( + title: Text('${Question.SUBJ2string(subject)} Quiz'), + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () { + showDialog( + context: context, + builder: (BuildContext buildContext) { + return ConfirmCancelQuizDialogue(); }, - )), - body: Center(child: BlocBuilder( - builder: (quizBlocContext, state) { - if (state is QuizLoading) { - return QuestionLoadingDisplay(); - } else if (state is QuizOngoing) { - return Column( - children: [ - // * Display the questions - QuestionDisplay( - subjectQuestionsMap: quizBlocContext - .read() - .subjectQuestionsMap, - subject: widget.subject), - - // * Display the submit button - ElevatedButton( - onPressed: () { - quizBlocContext + ); + }, + ), + ), + body: BlocBuilder( + builder: (quizBlocContext, state) { + if (state is QuizLoading) { + return QuestionLoadingDisplay(); + } else if (state is QuizOngoing) { + return Stack( + children: [ + SingleChildScrollView( + child: Column( + children: [ + // * Display the questions + QuestionDisplay( + subjectQuestionsMap: quizBlocContext .read() - .add(QuizFinishBtnPressed()); - }, - child: Text("Submit")), + .subjectQuestionsMap, + subject: subject, + ), - // * Display the timer - TimerDisplay(), - ], - ); - } else if (state is QuizFinishConfirmation) { - return ConfirmSubmitQuizDisplay(); - } else if (state is QuizCancelConfirmation) { - return ConfirmCancelQuizDisplay(); - } else if (state is QuizOutOfTime) { - return (ConfirmTimeOutQuizDisplay()); - } else if (state is QuizError) { - return ReadyQuizPage(); - } - return Text( - "SOMETHING WENT WRONG! (No Quiz Bloc State Matched)"); - }, - ))); - })); + // * Display the submit button + ElevatedButton( + onPressed: () { + showDialog( + context: context, + builder: (BuildContext buildContext) { + return ConfirmSubmitQuizDialogue( + quizBloc: + quizBlocContext.read(), + ); + }, + ); + }, + child: Text("Submit"), + ), + ], + ), + ), + + // * Display the timer + TimerDisplay(), + ], + ); + } + + GoRouter.of(context).go('/quiz-mode'); + return Text( + "SOMETHING WENT WRONG! (No Quiz Bloc State Matched)"); + }, + ), + ); + }, + ), + ); } } diff --git a/lib/features/quiz_mode/presentation/pages/ready_quiz_page.dart b/lib/features/quiz_mode/presentation/pages/ready_quiz_page.dart index 9b1abfb0..b50438ce 100644 --- a/lib/features/quiz_mode/presentation/pages/ready_quiz_page.dart +++ b/lib/features/quiz_mode/presentation/pages/ready_quiz_page.dart @@ -1,10 +1,6 @@ // ignore_for_file: prefer_const_constructors import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:scholars_guide/features/quiz_mode/presentation/widgets/ready_quiz_page_widgets/choose_subject_confirm_display.dart'; - -import '../state_management/ready_quiz/ready_quiz_cubit.dart'; import '../widgets/ready_quiz_page_widgets/choose_subject_display.dart'; class ReadyQuizPage extends StatelessWidget { @@ -14,18 +10,11 @@ class ReadyQuizPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( + automaticallyImplyLeading: false, title: const Text('Submit Questions'), ), body: Center( - child: BlocProvider( - create: (context) => ReadyQuizCubit(), - child: BlocBuilder( - builder: (readyQuizCubitContext, state) { - if (state is ReadyQuizSubjectChosen) { - return ChooseSubjectConfirmDisplay(subject: state.subject); - } - return ChooseSubjectDisplay(); - })), + child: ChooseSubjectDisplay(), )); } } diff --git a/lib/features/quiz_mode/presentation/pages/solutions_quiz_page.dart b/lib/features/quiz_mode/presentation/pages/solutions_quiz_page.dart new file mode 100644 index 00000000..f1749ecd --- /dev/null +++ b/lib/features/quiz_mode/presentation/pages/solutions_quiz_page.dart @@ -0,0 +1,59 @@ +// ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:scholars_guide/core/models/question_model.dart'; +import 'package:scholars_guide/features/quiz_mode/presentation/state_management/quiz_card/quiz_card_cubit.dart'; +import 'package:scholars_guide/features/quiz_mode/presentation/state_management/solution_quiz/solution_quiz_cubit.dart'; +import 'package:scholars_guide/features/quiz_mode/presentation/widgets/quiz_page_widgets/question_loading_display.dart'; +import 'package:scholars_guide/features/quiz_mode/presentation/widgets/solution_quiz_page_widgets/solution_card_display.dart'; + +class SolutionsQuizPage extends StatefulWidget { + const SolutionsQuizPage({super.key}); + + @override + State createState() => _SolutionsQuizPageState(); +} + +class _SolutionsQuizPageState extends State { + @override + Widget build(BuildContext context) { + final extraMap = GoRouterState.of(context).extra as Map; + final SUBJ subject = extraMap['subject'] as SUBJ; + final Map> subjectQuestionsMap = + extraMap['subjectQuestionsMap'] as Map>; + + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text('Solutions Page'), + ), + body: BlocProvider( + create: (providerContext) => SolutionQuizCubit()..loadSolutions(), + child: BlocBuilder( + builder: (solutionQuizCubitContext, state) { + List questions = subjectQuestionsMap[subject]!; + + if (state is SolutionQuizLoad) { + return QuestionLoadingDisplay(); + } else if (state is SolutionQuizShown) { + return PageView.builder( + itemCount: questions.length, + itemBuilder: (context, index) => SolutionCardDisplay( + question: questions[index].question, + answer: questions[index] + .optionsArray[questions[index].correctIndex], + solution: questions[index].solution, + ), + ); + } + return Center( + child: Text('Something went wrong'), + ); + }, + ), + ), + ); + } +} diff --git a/lib/features/quiz_mode/presentation/state_management/quiz/quiz_bloc.dart b/lib/features/quiz_mode/presentation/state_management/quiz/quiz_bloc.dart index 20b863bb..57316565 100644 --- a/lib/features/quiz_mode/presentation/state_management/quiz/quiz_bloc.dart +++ b/lib/features/quiz_mode/presentation/state_management/quiz/quiz_bloc.dart @@ -26,7 +26,8 @@ class QuizBloc extends Bloc { .map((e) => QuizCardCubit( correctIndex: e.correctIndex, optionsArray: e.options, - question: e.question)) + question: e.question, + solution: e.solution)) .toList()); }); } else { @@ -39,7 +40,8 @@ class QuizBloc extends Bloc { .map((e) => QuizCardCubit( correctIndex: e.correctIndex, optionsArray: e.options, - question: e.question)) + question: e.question, + solution: e.solution)) .toList() }; } @@ -47,29 +49,10 @@ class QuizBloc extends Bloc { emit(QuizOngoing()); }); - // * For submitting the quiz - on((event, emit) { - emit(QuizFinishConfirmation()); - }); - on((event, emit) { - emit(QuizOngoing()); - }); - on((event, emit) { - emit(QuizOutOfTime()); - }); - - // * For cancelling the quiz - on((event, emit) { - emit(QuizCancelConfirmation()); - }); - on((event, emit) { - emit(QuizOngoing()); - }); - // * Only for quiz taking all subjects. // TODO: Implement the logic for the next and previous page. - on((event, emit) {}); - on((event, emit) {}); + // on((event, emit) {}); + // on((event, emit) {}); } late SUBJ subject; diff --git a/lib/features/quiz_mode/presentation/state_management/quiz/quiz_event.dart b/lib/features/quiz_mode/presentation/state_management/quiz/quiz_event.dart index b3739676..e336f702 100644 --- a/lib/features/quiz_mode/presentation/state_management/quiz/quiz_event.dart +++ b/lib/features/quiz_mode/presentation/state_management/quiz/quiz_event.dart @@ -17,16 +17,6 @@ class QuizLoadQuestions extends QuizEvent { List get props => [subject, numQuestions]; } -class QuizFinishBtnPressed extends QuizEvent {} - -class QuizConfirmFinishBtnPressed extends QuizEvent {} - -class QuizCancelBtnPressed extends QuizEvent {} - -class QuizConfirmCancelBtnPressed extends QuizEvent {} - -class QuizTimeFinished extends QuizEvent {} - class QuizNextPageBtnPressed extends QuizEvent {} class QuizPreviousPageBtnPressed extends QuizEvent {} diff --git a/lib/features/quiz_mode/presentation/state_management/quiz/quiz_state.dart b/lib/features/quiz_mode/presentation/state_management/quiz/quiz_state.dart index 888b8f87..3a85b578 100644 --- a/lib/features/quiz_mode/presentation/state_management/quiz/quiz_state.dart +++ b/lib/features/quiz_mode/presentation/state_management/quiz/quiz_state.dart @@ -11,10 +11,4 @@ final class QuizLoading extends QuizState {} final class QuizOngoing extends QuizState {} -final class QuizFinishConfirmation extends QuizState {} - -final class QuizCancelConfirmation extends QuizState {} - -final class QuizOutOfTime extends QuizState {} - final class QuizError extends QuizState {} diff --git a/lib/features/quiz_mode/presentation/state_management/quiz_card/quiz_card_cubit.dart b/lib/features/quiz_mode/presentation/state_management/quiz_card/quiz_card_cubit.dart index 034feea4..b8807f80 100644 --- a/lib/features/quiz_mode/presentation/state_management/quiz_card/quiz_card_cubit.dart +++ b/lib/features/quiz_mode/presentation/state_management/quiz_card/quiz_card_cubit.dart @@ -8,15 +8,19 @@ class QuizCardCubit extends Cubit { required this.correctIndex, required this.optionsArray, required this.question, + required this.solution, }) : super(QuizCardUnanswered()); final int correctIndex; final List optionsArray; final String question; + final String solution; void chooseOption({required int index}) { emit(QuizCardUnanswered()); emit(QuizCardAnswered(chosenIndex: index)); } - void revealAnswer({required int index}) => emit(QuizCardRevealed(chosenIndex: index)); + + void revealAnswer({required int index}) => + emit(QuizCardRevealed(chosenIndex: index)); } diff --git a/lib/features/quiz_mode/presentation/state_management/quiz_card/quiz_card_state.dart b/lib/features/quiz_mode/presentation/state_management/quiz_card/quiz_card_state.dart index c7c91215..d3af299b 100644 --- a/lib/features/quiz_mode/presentation/state_management/quiz_card/quiz_card_state.dart +++ b/lib/features/quiz_mode/presentation/state_management/quiz_card/quiz_card_state.dart @@ -1,10 +1,14 @@ +// ignore_for_file: overridden_fields, annotate_overrides + part of 'quiz_card_cubit.dart'; sealed class QuizCardState extends Equatable { const QuizCardState(); + final int chosenIndex = -1; + @override - List get props => []; + List get props => [chosenIndex]; } final class QuizCardUnanswered extends QuizCardState {} diff --git a/lib/features/quiz_mode/presentation/state_management/ready_quiz/ready_quiz_cubit.dart b/lib/features/quiz_mode/presentation/state_management/ready_quiz/ready_quiz_cubit.dart deleted file mode 100644 index 7ce8333e..00000000 --- a/lib/features/quiz_mode/presentation/state_management/ready_quiz/ready_quiz_cubit.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:bloc/bloc.dart'; -import 'package:equatable/equatable.dart'; - -import '../../../../../core/models/question_model.dart'; - -part 'ready_quiz_state.dart'; - -class ReadyQuizCubit extends Cubit { - ReadyQuizCubit() : super(ReadyQuizInitial()); - - void chooseMath() => emit(const ReadyQuizSubjectChosen(subject: SUBJ.MATH)); - void chooseScience() => emit(const ReadyQuizSubjectChosen(subject: SUBJ.SCIENCE)); - void chooseReading() => emit(const ReadyQuizSubjectChosen(subject: SUBJ.READING)); - void chooseLanguage() => emit(const ReadyQuizSubjectChosen(subject: SUBJ.LANGUAGE)); - void chooseAll() => emit(const ReadyQuizSubjectChosen(subject: SUBJ.ALL)); - void chooseAgain() => emit(ReadyQuizInitial()); -} diff --git a/lib/features/quiz_mode/presentation/state_management/ready_quiz/ready_quiz_state.dart b/lib/features/quiz_mode/presentation/state_management/ready_quiz/ready_quiz_state.dart deleted file mode 100644 index c75bb39c..00000000 --- a/lib/features/quiz_mode/presentation/state_management/ready_quiz/ready_quiz_state.dart +++ /dev/null @@ -1,22 +0,0 @@ -part of 'ready_quiz_cubit.dart'; - -sealed class ReadyQuizState extends Equatable { - const ReadyQuizState(); - - @override - List get props => []; -} - -// Initial state shows 5 options in the ready quiz page. -class ReadyQuizInitial extends ReadyQuizState {} - -// Once the user chooses a subject, the state changes to ReadyQuizSubjectChosen. -// User will choose to confirm or not. -class ReadyQuizSubjectChosen extends ReadyQuizState { - const ReadyQuizSubjectChosen({required this.subject}); - - final SUBJ subject; - - @override - List get props => [subject]; -} diff --git a/lib/features/quiz_mode/presentation/state_management/solution_quiz/solution_quiz_cubit.dart b/lib/features/quiz_mode/presentation/state_management/solution_quiz/solution_quiz_cubit.dart new file mode 100644 index 00000000..6412229a --- /dev/null +++ b/lib/features/quiz_mode/presentation/state_management/solution_quiz/solution_quiz_cubit.dart @@ -0,0 +1,17 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'solution_quiz_state.dart'; + +class SolutionQuizCubit extends Cubit { + SolutionQuizCubit() : super(SolutionQuizLoad()); + + void loadSolutions() async { + await placeholder(); + emit(SolutionQuizShown()); + } +} + +Future placeholder() async { + await Future.delayed(Duration(seconds: 3)); +} diff --git a/lib/features/quiz_mode/presentation/state_management/solution_quiz/solution_quiz_state.dart b/lib/features/quiz_mode/presentation/state_management/solution_quiz/solution_quiz_state.dart new file mode 100644 index 00000000..848ab063 --- /dev/null +++ b/lib/features/quiz_mode/presentation/state_management/solution_quiz/solution_quiz_state.dart @@ -0,0 +1,12 @@ +part of 'solution_quiz_cubit.dart'; + +sealed class SolutionQuizState extends Equatable { + const SolutionQuizState(); + + @override + List get props => []; +} + +final class SolutionQuizLoad extends SolutionQuizState {} + +final class SolutionQuizShown extends SolutionQuizState {} diff --git a/lib/features/quiz_mode/presentation/widgets/quiz_page_widgets/confirm_cancel_quiz_dialogue.dart b/lib/features/quiz_mode/presentation/widgets/quiz_page_widgets/confirm_cancel_quiz_dialogue.dart new file mode 100644 index 00000000..842595db --- /dev/null +++ b/lib/features/quiz_mode/presentation/widgets/quiz_page_widgets/confirm_cancel_quiz_dialogue.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class ConfirmCancelQuizDialogue extends StatelessWidget { + const ConfirmCancelQuizDialogue({super.key}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Cancel Confirmation'), + content: const Text("You can't go back after this! Are you sure?"), + actions: [ + ElevatedButton( + child: const Text('Continue Quiz'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ElevatedButton( + child: const Text('Go Back'), + onPressed: () { + Navigator.of(context).pop(); + GoRouter.of(context).go('/quiz-mode'); + }, + ), + ], + ); + } +} diff --git a/lib/features/quiz_mode/presentation/widgets/quiz_page_widgets/confirm_cancel_quiz_display.dart b/lib/features/quiz_mode/presentation/widgets/quiz_page_widgets/confirm_cancel_quiz_display.dart deleted file mode 100644 index e808a349..00000000 --- a/lib/features/quiz_mode/presentation/widgets/quiz_page_widgets/confirm_cancel_quiz_display.dart +++ /dev/null @@ -1,44 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:scholars_guide/features/quiz_mode/presentation/pages/ready_quiz_page.dart'; -import 'package:scholars_guide/features/quiz_mode/presentation/state_management/quiz/quiz_bloc.dart'; - -class ConfirmCancelQuizDisplay extends StatelessWidget { - const ConfirmCancelQuizDisplay({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Container( - width: double.infinity, - height: double.infinity, - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.6), - ), - child: AlertDialog( - title: const Text('Cancel Confirmation'), - content: const Text("You can't go back after this! Are you sure?"), - actions: [ - ElevatedButton( - child: const Text('Continue Quiz'), - onPressed: () { - context.read().add(QuizConfirmCancelBtnPressed()); - }, - ), - ElevatedButton( - child: const Text('Go Back'), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => (ReadyQuizPage())), - ); - }, - ), - ], - )), - ); - } -} \ No newline at end of file diff --git a/lib/features/quiz_mode/presentation/widgets/quiz_page_widgets/confirm_submit_quiz_dialogue.dart b/lib/features/quiz_mode/presentation/widgets/quiz_page_widgets/confirm_submit_quiz_dialogue.dart new file mode 100644 index 00000000..5302f5d9 --- /dev/null +++ b/lib/features/quiz_mode/presentation/widgets/quiz_page_widgets/confirm_submit_quiz_dialogue.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:scholars_guide/core/models/question_model.dart'; +import 'package:scholars_guide/features/quiz_mode/presentation/state_management/quiz/quiz_bloc.dart'; +import 'package:scholars_guide/features/quiz_mode/presentation/state_management/quiz_card/quiz_card_cubit.dart'; + +class ConfirmSubmitQuizDialogue extends StatelessWidget { + const ConfirmSubmitQuizDialogue({super.key, required this.quizBloc}); + + final QuizBloc quizBloc; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Submit Confirmation'), + content: const Text("You can't go back after this! Are you sure?"), + actions: [ + ElevatedButton( + child: const Text('Submit Quiz'), + onPressed: () { + final SUBJ subject = quizBloc.subject; + Map> subjectQuestionsMap = + quizBloc.subjectQuestionsMap; + + for (List subject in subjectQuestionsMap.values) { + for (QuizCardCubit questionCubit in subject) { + if (questionCubit.state is QuizCardUnanswered) { + questionCubit.revealAnswer(index: -1); + } + if (questionCubit.state is QuizCardAnswered) { + questionCubit.revealAnswer( + index: questionCubit.state.props[0] as int); + } + } + } + Navigator.of(context).pop(); + GoRouter.of(context).go('/quiz-mode/finished-quiz', extra: { + 'subjectQuestionsMap': subjectQuestionsMap, + 'subject': subject + }); + }, + ), + ElevatedButton( + child: const Text('Go Back'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + } +} diff --git a/lib/features/quiz_mode/presentation/widgets/quiz_page_widgets/confirm_submit_quiz_display.dart b/lib/features/quiz_mode/presentation/widgets/quiz_page_widgets/confirm_submit_quiz_display.dart deleted file mode 100644 index aaba9e2d..00000000 --- a/lib/features/quiz_mode/presentation/widgets/quiz_page_widgets/confirm_submit_quiz_display.dart +++ /dev/null @@ -1,66 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:scholars_guide/core/models/question_model.dart'; -import 'package:scholars_guide/features/quiz_mode/presentation/pages/finished_quiz_page.dart'; -import 'package:scholars_guide/features/quiz_mode/presentation/state_management/quiz/quiz_bloc.dart'; -import 'package:scholars_guide/features/quiz_mode/presentation/state_management/quiz_card/quiz_card_cubit.dart'; - -class ConfirmSubmitQuizDisplay extends StatelessWidget { - const ConfirmSubmitQuizDisplay({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Container( - width: double.infinity, - height: double.infinity, - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.6), - ), - child: AlertDialog( - title: const Text('Submit Confirmation'), - content: const Text("You can't go back after this! Are you sure?"), - actions: [ - ElevatedButton( - child: const Text('Submit Quiz'), - onPressed: () { - Map> subjectQuestionsMap = - context.read().subjectQuestionsMap; - - for (List subject - in subjectQuestionsMap.values) { - - for (QuizCardCubit questionCubit in subject) { - if (questionCubit.state is QuizCardUnanswered) { - questionCubit.revealAnswer( - index: -1); - } - if (questionCubit.state is QuizCardAnswered) { - questionCubit.revealAnswer( - index: questionCubit.state.props[0] as int); - } - } - } - Navigator.push( - context, - MaterialPageRoute( - builder: (newContext) => (FinishedQuizPage( - subjectQuestionsMap: - context.read().subjectQuestionsMap, - subject: context.read().subject)), - )); - }, - ), - ElevatedButton( - child: const Text('Go Back'), - onPressed: () { - context.read().add(QuizConfirmFinishBtnPressed()); - }, - ), - ], - )), - ); - } -} diff --git a/lib/features/quiz_mode/presentation/widgets/quiz_page_widgets/confirm_timeout_quiz_dialogue.dart b/lib/features/quiz_mode/presentation/widgets/quiz_page_widgets/confirm_timeout_quiz_dialogue.dart new file mode 100644 index 00000000..a16093d9 --- /dev/null +++ b/lib/features/quiz_mode/presentation/widgets/quiz_page_widgets/confirm_timeout_quiz_dialogue.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:scholars_guide/core/models/question_model.dart'; +import 'package:scholars_guide/features/quiz_mode/presentation/state_management/quiz/quiz_bloc.dart'; +import 'package:scholars_guide/features/quiz_mode/presentation/state_management/quiz_card/quiz_card_cubit.dart'; + +class ConfirmTimeoutQuizDialogue extends StatelessWidget { + const ConfirmTimeoutQuizDialogue({super.key, required this.quizBloc}); + + final QuizBloc quizBloc; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Out of Time'), + content: const Text("Time's up! You did great"), + actions: [ + ElevatedButton( + child: const Text('Submit Quiz'), + onPressed: () { + final SUBJ subject = quizBloc.subject; + Map> subjectQuestionsMap = + quizBloc.subjectQuestionsMap; + + for (List subject in subjectQuestionsMap.values) { + for (QuizCardCubit questionCubit in subject) { + if (questionCubit.state is QuizCardUnanswered) { + questionCubit.revealAnswer(index: -1); + } + if (questionCubit.state is QuizCardAnswered) { + questionCubit.revealAnswer( + index: questionCubit.state.props[0] as int); + } + } + } + + Navigator.of(context).pop(); + GoRouter.of(context).go('/quiz-mode/finished-quiz', extra: { + 'subjectQuestionsMap': subjectQuestionsMap, + 'subject': subject + }); + }, + ), + ], + ); + } +} diff --git a/lib/features/quiz_mode/presentation/widgets/quiz_page_widgets/confirm_timeout_quiz_display.dart b/lib/features/quiz_mode/presentation/widgets/quiz_page_widgets/confirm_timeout_quiz_display.dart deleted file mode 100644 index 5065cc6b..00000000 --- a/lib/features/quiz_mode/presentation/widgets/quiz_page_widgets/confirm_timeout_quiz_display.dart +++ /dev/null @@ -1,41 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:scholars_guide/features/quiz_mode/presentation/pages/finished_quiz_page.dart'; -import 'package:scholars_guide/features/quiz_mode/presentation/state_management/quiz/quiz_bloc.dart'; - -class ConfirmTimeOutQuizDisplay extends StatelessWidget { - const ConfirmTimeOutQuizDisplay({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Container( - width: double.infinity, - height: double.infinity, - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.6), - ), - child: AlertDialog( - title: const Text('Out of Time'), - content: const Text("You did great! Let's see you did"), - actions: [ - ElevatedButton( - child: const Text('Submit Quiz'), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => (FinishedQuizPage( - subjectQuestionsMap: - context.read().subjectQuestionsMap, - subject: context.read().subject))), - ); - }, - ), - ], - )), - ); - } -} diff --git a/lib/features/quiz_mode/presentation/widgets/quiz_page_widgets/question_loading_display.dart b/lib/features/quiz_mode/presentation/widgets/quiz_page_widgets/question_loading_display.dart index fe953c40..026cb21a 100644 --- a/lib/features/quiz_mode/presentation/widgets/quiz_page_widgets/question_loading_display.dart +++ b/lib/features/quiz_mode/presentation/widgets/quiz_page_widgets/question_loading_display.dart @@ -5,6 +5,11 @@ class QuestionLoadingDisplay extends StatelessWidget { @override Widget build(BuildContext context) { - return const Center(child: CircularProgressIndicator(color: Colors.white, backgroundColor: Colors.yellow,)); + return const Center( + child: CircularProgressIndicator( + color: Colors.white, + backgroundColor: Color.fromARGB(255, 145, 133, 165), + ), + ); } } diff --git a/lib/features/quiz_mode/presentation/widgets/quiz_page_widgets/timer_display.dart b/lib/features/quiz_mode/presentation/widgets/quiz_page_widgets/timer_display.dart index bb36efb8..6c492c93 100644 --- a/lib/features/quiz_mode/presentation/widgets/quiz_page_widgets/timer_display.dart +++ b/lib/features/quiz_mode/presentation/widgets/quiz_page_widgets/timer_display.dart @@ -2,7 +2,9 @@ import 'package:circular_countdown_timer/circular_countdown_timer.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_portal/flutter_portal.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:scholars_guide/features/quiz_mode/presentation/state_management/quiz/quiz_bloc.dart'; +import 'package:scholars_guide/features/quiz_mode/presentation/widgets/quiz_page_widgets/confirm_timeout_quiz_dialogue.dart'; class TimerDisplay extends StatefulWidget { const TimerDisplay({super.key}); @@ -12,39 +14,13 @@ class TimerDisplay extends StatefulWidget { } class _TimerDisplayState extends State { - // final CountDownController _controller = CountDownController(); - @override Widget build(BuildContext context) { - return Expanded( - flex: 0, - child: Align( - alignment: Alignment.bottomRight, - child: TimerOverlay(), - )); - } -} - -class TimerOverlay extends StatefulWidget { - const TimerOverlay({super.key}); - - @override - State createState() => _TimerOverlayState(); -} - -class _TimerOverlayState extends State { - @override - Widget build(BuildContext context) { - return Portal( - child: PortalTarget( - visible: true, - anchor: const Aligned( - follower: Alignment.bottomRight, - target: Alignment.bottomCenter, - ), - portalFollower: Container( - margin: EdgeInsets.only(right: 20), child: CountdownTimer()), - child: SizedBox())); + return Positioned( + right: MediaQuery.of(context).size.width * 0.05, + bottom: MediaQuery.of(context).size.width * 0.05, + child: CountdownTimer(), + ); } } @@ -59,34 +35,43 @@ class _CountdownTimerState extends State { @override Widget build(BuildContext context) { return CircularCountDownTimer( - duration: 60, + duration: 60 * 10, // ! 10 minutes muna initialDuration: 0, controller: CountDownController(), - width: MediaQuery.of(context).size.width / 6, - height: MediaQuery.of(context).size.height / 6, - ringColor: Colors.grey[300]!, + width: MediaQuery.of(context).size.width / 7, + height: MediaQuery.of(context).size.height / 7, + ringColor: Colors.indigo.withOpacity(0.0), ringGradient: null, - fillColor: Colors.grey[600]!, + fillColor: Colors.indigo.withOpacity(0.5), fillGradient: null, - backgroundColor: Colors.grey[500], + backgroundColor: Colors.indigo.withOpacity(0.3), backgroundGradient: null, - strokeWidth: 10.0, + strokeWidth: 5.0, strokeCap: StrokeCap.round, textStyle: TextStyle( - fontSize: 20.0, color: Colors.white, fontWeight: FontWeight.bold), + fontSize: 20.0, + color: Colors.indigo.withOpacity(0.8), + fontWeight: FontWeight.bold), textFormat: CountdownTextFormat.S, isReverse: true, isReverseAnimation: true, isTimerTextShown: true, autoStart: true, onStart: () { - debugPrint('Countdown Started'); + // debugPrint('Countdown Started'); }, onComplete: () { - debugPrint('Countdown Ended'); + // debugPrint('Countdown Ended'); + showDialog( + barrierDismissible: false, + context: context, + builder: (BuildContext buildContext) { + return ConfirmTimeoutQuizDialogue( + quizBloc: context.read()); + }); }, onChange: (String timeStamp) { - debugPrint('Countdown Changed $timeStamp'); + // debugPrint('Countdown Changed $timeStamp'); }, timeFormatterFunction: (defaultFormatterFunction, duration) { if (duration.inSeconds == 0) { diff --git a/lib/features/quiz_mode/presentation/widgets/quiz_widgets/question_card_choices_display.dart b/lib/features/quiz_mode/presentation/widgets/quiz_widgets/question_card_choices_display.dart index 05c8e0c7..892b8e6e 100644 --- a/lib/features/quiz_mode/presentation/widgets/quiz_widgets/question_card_choices_display.dart +++ b/lib/features/quiz_mode/presentation/widgets/quiz_widgets/question_card_choices_display.dart @@ -1,12 +1,14 @@ -// ignore_for_file: prefer_const_constructors +// ignore_for_file: prefer_const_constructors, prefer_typing_uninitialized_variables, must_be_immutable import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:flutter_markdown_latex/flutter_markdown_latex.dart'; import 'package:scholars_guide/features/quiz_mode/presentation/state_management/quiz_card/quiz_card_cubit.dart'; +import 'package:markdown/markdown.dart' as md; class QuestionCardChoicesDisplay extends StatefulWidget { - const QuestionCardChoicesDisplay( - {super.key, required this.bloc}); + const QuestionCardChoicesDisplay({super.key, required this.bloc}); final QuizCardCubit bloc; @@ -23,80 +25,158 @@ class _QuestionCardChoicesDisplayState bloc: widget.bloc, builder: (choicesContext, state) { List choices = widget.bloc.optionsArray; - List buttons = []; + List buttons = []; for (int i = 0; i < choices.length; i++) { String c = choices[i]; - ElevatedButton button; + ChoiceButton button; if (state is QuizCardUnanswered) { - button = _createChoiceButton( - choice: c, - func: () { - widget.bloc.chooseOption(index: i); - }, - context: context); + button = ChoiceButton( + choice: c, + func: () { + widget.bloc.chooseOption(index: i); + }, + ); } else if (state is QuizCardAnswered) { - button = _createChoiceButton( - choice: c, - func: () { - widget.bloc.chooseOption(index: i); - }, - isChosen: state.chosenIndex == i, - context: - context); // ! This is the line that needs to be changed + button = ChoiceButton( + choice: c, + func: () { + widget.bloc.chooseOption(index: i); + }, + isChosen: state.chosenIndex == i, + isCorrect: widget.bloc.correctIndex == state.chosenIndex, + ); } else if (state is QuizCardRevealed) { - button = _createChoiceButton( - choice: c, - func: () {}, - isChosen: state.chosenIndex == i, - isCorrect: widget.bloc.correctIndex == state.chosenIndex, - isRevealed: true, - context: context); + button = ChoiceButton( + choice: c, + func: () {}, + isChosen: state.chosenIndex == i, + isCorrect: widget.bloc.correctIndex == state.chosenIndex, + isRevealed: true, + ); } else { - button = - _createChoiceButton(choice: c, func: () {}, context: context); + button = ChoiceButton( + choice: c, + func: () {}, + ); } buttons.add(button); } - return Expanded( - flex: 2, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Column(children: buttons.sublist(0, 2)), - Column(children: buttons.sublist(2)) - ], - ), - ); + return Column(children: buttons); }); } } -ElevatedButton _createChoiceButton( - {required String choice, - bool isChosen = false, - bool isCorrect = false, - bool isRevealed = false, - required func, - required context}) { - Color color = Colors.black; - if (isRevealed) { - color = isCorrect ? isChosen? Colors.green: Colors.black : isChosen ? Colors.red : Colors.black; - } else if (isChosen) { - color = Colors.blue; +class ChoiceButton extends StatelessWidget { + ChoiceButton({ + super.key, + required this.choice, + required this.func, + this.isChosen = false, + this.isCorrect = false, + this.isRevealed = false, + }); + + bool isChosen; + bool isCorrect; + bool isRevealed; + final String choice; + final func; + + @override + Widget build(BuildContext context) { + Color color = Colors.black; + if (isRevealed) { + color = isCorrect + ? isChosen + ? Colors.green + : Colors.black + : isChosen + ? Colors.red + : Colors.black; + } else if (isChosen) { + color = Colors.indigoAccent; + } + + return Container( + width: MediaQuery.of(context).size.width * 0.7, + margin: EdgeInsets.only(top: 2.0, bottom: 2.0), + child: ElevatedButton( + style: ButtonStyle( + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + side: BorderSide(color: color), + ), + ), + ), + onPressed: isRevealed ? null : func, + child: TextMarkdown(text: choice), + ), + ); + } +} + +// ElevatedButton _createChoiceButton( +// {required String choice, +// bool isChosen = false, +// bool isCorrect = false, +// bool isRevealed = false, +// required func, +// required context}) { +// Color color = Colors.black; +// if (isRevealed) { +// color = isCorrect +// ? isChosen +// ? Colors.green +// : Colors.black +// : isChosen +// ? Colors.red +// : Colors.black; +// } else if (isChosen) { +// color = Colors.indigoAccent; +// } +// return ElevatedButton( +// style: ButtonStyle( +// shape: MaterialStateProperty.all( +// RoundedRectangleBorder( +// borderRadius: BorderRadius.circular(10.0), +// side: BorderSide(color: color), +// ), +// ), +// ), +// onPressed: isRevealed ? null : func, +// child: TextMarkdown(text: choice), +// ); +// } + +class TextMarkdown extends StatelessWidget { + const TextMarkdown({ + super.key, + required this.text, + }); + + final String text; + + @override + Widget build(BuildContext context) { + return Markdown( + padding: EdgeInsets.all(0.0), + shrinkWrap: true, + data: text, + builders: { + 'latex': LatexElementBuilder(), + }, + styleSheet: MarkdownStyleSheet( + textAlign: WrapAlignment.center, + p: const TextStyle(fontSize: 15.0), + ), + extensionSet: md.ExtensionSet( + [LatexBlockSyntax()], + [LatexInlineSyntax()], + ), + ); } - return ElevatedButton( - style: ButtonStyle( - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(7.0), - side: BorderSide(color: color))), - minimumSize: MaterialStateProperty.all( - Size(MediaQuery.of(context).size.width * 0.4, 40.0)), - ), - onPressed: isRevealed ? null : func, - child: Text(choice, style: TextStyle(fontSize: 15.0)), - ); } diff --git a/lib/features/quiz_mode/presentation/widgets/quiz_widgets/question_card_display.dart b/lib/features/quiz_mode/presentation/widgets/quiz_widgets/question_card_display.dart index 2b116dc7..654de70c 100644 --- a/lib/features/quiz_mode/presentation/widgets/quiz_widgets/question_card_display.dart +++ b/lib/features/quiz_mode/presentation/widgets/quiz_widgets/question_card_display.dart @@ -25,20 +25,17 @@ class QuestionCardDisplay extends StatefulWidget { class _QuestionCardDisplayState extends State { @override Widget build(BuildContext context) { - return Container( - height: 300, - decoration: BoxDecoration( - color: Colors.grey[400], - borderRadius: BorderRadius.circular(7.0), - ), - // alignment: Alignment.center, + return Card( margin: EdgeInsets.all(10.0), - child: Column( - children: [ - QuestionCardQuestionDisplay(question: widget.question), - QuestionCardChoicesDisplay( - bloc: widget.bloc) - ], + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + QuestionCardQuestionDisplay(question: widget.question), + QuestionCardChoicesDisplay(bloc: widget.bloc), + SizedBox(height: 15.0) + ], + ), )); } } diff --git a/lib/features/quiz_mode/presentation/widgets/quiz_widgets/question_card_question_display.dart b/lib/features/quiz_mode/presentation/widgets/quiz_widgets/question_card_question_display.dart index f6aa5be3..df4cb426 100644 --- a/lib/features/quiz_mode/presentation/widgets/quiz_widgets/question_card_question_display.dart +++ b/lib/features/quiz_mode/presentation/widgets/quiz_widgets/question_card_question_display.dart @@ -1,4 +1,9 @@ +// ignore_for_file: prefer_const_constructors + import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:flutter_markdown_latex/flutter_markdown_latex.dart'; +import 'package:markdown/markdown.dart' as md; class QuestionCardQuestionDisplay extends StatelessWidget { const QuestionCardQuestionDisplay({super.key, required this.question}); @@ -7,13 +12,35 @@ class QuestionCardQuestionDisplay extends StatelessWidget { @override Widget build(BuildContext context) { - return Expanded( - flex: 3, - child: Align( - alignment: Alignment.center, - child: Container( - margin: const EdgeInsets.only(left: 25.0, right: 25.0), - child: - Text(question, style: const TextStyle(fontSize: 17.0))))); + return TextMarkdown(text: question); + } +} + +class TextMarkdown extends StatelessWidget { + const TextMarkdown({ + super.key, + required this.text, + }); + + final String text; + + @override + Widget build(BuildContext context) { + return Markdown( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + data: text, + styleSheet: MarkdownStyleSheet( + textAlign: WrapAlignment.center, + p: const TextStyle(fontSize: 17.0), + ), + builders: { + 'latex': LatexElementBuilder(), + }, + extensionSet: md.ExtensionSet( + [LatexBlockSyntax()], + [LatexInlineSyntax()], + ), + ); } } diff --git a/lib/features/quiz_mode/presentation/widgets/quiz_widgets/question_display.dart b/lib/features/quiz_mode/presentation/widgets/quiz_widgets/question_display.dart index fe9e0133..6615ae09 100644 --- a/lib/features/quiz_mode/presentation/widgets/quiz_widgets/question_display.dart +++ b/lib/features/quiz_mode/presentation/widgets/quiz_widgets/question_display.dart @@ -9,9 +9,7 @@ import 'question_card_display.dart'; // Holds multiple question answer cards class QuestionDisplay extends StatefulWidget { const QuestionDisplay( - {super.key, - required this.subjectQuestionsMap, - required this.subject}); + {super.key, required this.subjectQuestionsMap, required this.subject}); final SUBJ subject; final Map> subjectQuestionsMap; @@ -24,16 +22,17 @@ class _QuestionDisplayState extends State { @override Widget build(BuildContext context) { List questions = widget.subjectQuestionsMap[widget.subject]!; - return Expanded( - child: ListView.builder( - itemCount: widget.subjectQuestionsMap[widget.subject]?.length, - itemBuilder: (context, index) { - return QuestionCardDisplay( - bloc: questions[index], - choices: questions[index].optionsArray, - correctIndex: questions[index].correctIndex, - question: questions[index].question); - }), + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: questions.length, + itemBuilder: (context, index) { + return QuestionCardDisplay( + bloc: questions[index], + choices: questions[index].optionsArray, + correctIndex: questions[index].correctIndex, + question: questions[index].question); + }, ); } } diff --git a/lib/features/quiz_mode/presentation/widgets/ready_quiz_page_widgets/choose_subject_confirm_display.dart b/lib/features/quiz_mode/presentation/widgets/ready_quiz_page_widgets/choose_subject_confirm_display.dart deleted file mode 100644 index 0accdf9f..00000000 --- a/lib/features/quiz_mode/presentation/widgets/ready_quiz_page_widgets/choose_subject_confirm_display.dart +++ /dev/null @@ -1,48 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:scholars_guide/core/models/question_model.dart'; -import 'package:scholars_guide/features/quiz_mode/presentation/pages/quiz_page.dart'; -import 'package:scholars_guide/features/quiz_mode/presentation/state_management/ready_quiz/ready_quiz_cubit.dart'; - -class ChooseSubjectConfirmDisplay extends StatelessWidget { - const ChooseSubjectConfirmDisplay({super.key, required this.subject}); - - final SUBJ subject; - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Container( - width: double.infinity, - height: double.infinity, - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.6), - ), - child: AlertDialog( - title: Text('Confirmation'), - content: Text( - "Let's start answering ${Question.SUBJ2string(subject)}! Are you ready?"), - actions: [ - ElevatedButton( - child: Text('Start'), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => (QuizPage(subject: subject))), - ); - }, - ), - ElevatedButton( - child: Text('Go Back'), - onPressed: () { - BlocProvider.of(context).chooseAgain(); - }, - ), - ], - )), - ); - } -} diff --git a/lib/features/quiz_mode/presentation/widgets/ready_quiz_page_widgets/choose_subject_display.dart b/lib/features/quiz_mode/presentation/widgets/ready_quiz_page_widgets/choose_subject_display.dart index c2976893..a954a7d2 100644 --- a/lib/features/quiz_mode/presentation/widgets/ready_quiz_page_widgets/choose_subject_display.dart +++ b/lib/features/quiz_mode/presentation/widgets/ready_quiz_page_widgets/choose_subject_display.dart @@ -1,8 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_portal/flutter_portal.dart'; - -import '../../state_management/ready_quiz/ready_quiz_cubit.dart'; +import 'package:scholars_guide/features/quiz_mode/presentation/widgets/ready_quiz_page_widgets/subject_chosen_dialogue.dart'; class ChooseSubjectDisplay extends StatefulWidget { const ChooseSubjectDisplay({super.key}); @@ -14,18 +11,20 @@ class ChooseSubjectDisplay extends StatefulWidget { class _ChooseSubjectDisplayState extends State { @override Widget build(BuildContext context) { - return Portal( - child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text( - "Choose a subject to start the quiz", - style: TextStyle(fontSize: 20), - ), - // buildElevatedButton("UPCAT Challenge", context), // TODO: Implement UPCAT Challenge - buildElevatedButton("Math", context), - buildElevatedButton("Science", context), - buildElevatedButton("Language Proficiency", context), - buildElevatedButton("Reading Comprehension", context), - ])); + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + "Choose a subject to start the quiz", + style: TextStyle(fontSize: 20), + ), + // buildElevatedButton("UPCAT Challenge", context), // TODO: Implement UPCAT Challenge + buildElevatedButton("Math", context), + buildElevatedButton("Science", context), + buildElevatedButton("Language Proficiency", context), + buildElevatedButton("Reading Comprehension", context), + ], + ); } } @@ -36,25 +35,13 @@ Container buildElevatedButton(String text, BuildContext context) { width: 220, child: ElevatedButton( onPressed: () { - switch (text) { - case "Math": - context.read().chooseMath(); - break; - case "Science": - context.read().chooseScience(); - break; - case "Language Proficiency": - context.read().chooseLanguage(); - break; - case "Reading Comprehension": - context.read().chooseReading(); - break; - case "UPCAT Challenge": - // context.read().chooseAll(); - break; - default: - context.read().chooseAll(); - } + showDialog( + context: context, + builder: (BuildContext buildContext) { + return SubjectChosenDialogue( + subjectTest: text, + ); + }); }, child: Text(text))); } diff --git a/lib/features/quiz_mode/presentation/widgets/ready_quiz_page_widgets/subject_chosen_dialogue.dart b/lib/features/quiz_mode/presentation/widgets/ready_quiz_page_widgets/subject_chosen_dialogue.dart new file mode 100644 index 00000000..b9c19bee --- /dev/null +++ b/lib/features/quiz_mode/presentation/widgets/ready_quiz_page_widgets/subject_chosen_dialogue.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:scholars_guide/core/models/question_model.dart'; + +class SubjectChosenDialogue extends StatelessWidget { + const SubjectChosenDialogue({super.key, required this.subjectTest}); + + final String subjectTest; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Confirmation'), + content: Text("Let's start answering $subjectTest! Are you ready?"), + actions: [ + ElevatedButton( + child: const Text('Start'), + onPressed: () { + Navigator.of(context).pop(); + GoRouter.of(context).go('/quiz-mode/start-quiz', + extra: Question.string2SUBJ(subjectTest)); + }, + ), + ElevatedButton( + child: const Text('Go Back'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + } +} diff --git a/lib/features/quiz_mode/presentation/widgets/solution_quiz_page_widgets/solution_card_display.dart b/lib/features/quiz_mode/presentation/widgets/solution_quiz_page_widgets/solution_card_display.dart new file mode 100644 index 00000000..4c172c7b --- /dev/null +++ b/lib/features/quiz_mode/presentation/widgets/solution_quiz_page_widgets/solution_card_display.dart @@ -0,0 +1,118 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:flutter_markdown_latex/flutter_markdown_latex.dart'; +import 'package:markdown/markdown.dart' as md; + +class SolutionCardDisplay extends StatefulWidget { + const SolutionCardDisplay( + {super.key, + required this.question, + required this.answer, + required this.solution}); + + final String question; + final String answer; + final String solution; + + @override + State createState() => _SolutionCardDisplayState(); +} + +class _SolutionCardDisplayState extends State { + @override + Widget build(BuildContext context) { + return Card( + margin: EdgeInsets.all(10.0), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + padding: EdgeInsets.all(20.0), + child: Column( + children: [ + TextTitle(title: "Question"), + TextMarkdown( + text: widget.question, + styleSheet: MarkdownStyleSheet( + textAlign: WrapAlignment.center, + p: const TextStyle( + fontSize: 20.0, fontWeight: FontWeight.bold), + ), + ), + TextTitle(title: "Answer"), + TextMarkdown( + text: widget.answer, + styleSheet: MarkdownStyleSheet( + textAlign: WrapAlignment.center, + p: const TextStyle( + fontSize: 20.0, fontWeight: FontWeight.bold), + ), + ), + TextTitle(title: "Solution"), + TextMarkdown( + text: widget.solution == "" + ? "No solution yet provided" + : widget.solution, + styleSheet: MarkdownStyleSheet( + textAlign: WrapAlignment.center, + p: const TextStyle(fontSize: 18), + ), + ) + ], + ), + ), + ), + ); + } +} + +class TextTitle extends StatelessWidget { + const TextTitle({super.key, required this.title}); + + final String title; + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only(bottom: 10.0), + child: Text(title, + style: TextStyle( + fontSize: 20.0, + fontWeight: FontWeight.bold, + color: Color.fromARGB(255, 95, 55, 213), + )), + ); + } +} + +class TextMarkdown extends StatelessWidget { + const TextMarkdown({ + super.key, + required this.text, + required this.styleSheet, + }); + + final String text; + final MarkdownStyleSheet styleSheet; + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only(bottom: 20.0), + child: Markdown( + padding: EdgeInsets.all(0.0), + shrinkWrap: true, + data: text, + builders: { + 'latex': LatexElementBuilder(), + }, + styleSheet: styleSheet, + extensionSet: md.ExtensionSet( + [LatexBlockSyntax()], + [LatexInlineSyntax()], + ), + ), + ); + } +} diff --git a/lib/features/quiz_upload/data/repositories/quiz_upload_repository_impl.dart b/lib/features/quiz_upload/data/repositories/quiz_upload_repository_impl.dart index 8d626543..009c046f 100644 --- a/lib/features/quiz_upload/data/repositories/quiz_upload_repository_impl.dart +++ b/lib/features/quiz_upload/data/repositories/quiz_upload_repository_impl.dart @@ -3,12 +3,14 @@ // import 'package:scholars_guide/service_locator/service_locator.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_auth/firebase_auth.dart'; import 'package:scholars_guide/core/models/firestore_model.dart'; import 'package:scholars_guide/core/models/question_model.dart'; + import 'package:scholars_guide/features/quiz_upload/domain/repositories_contract/quiz_upload_repository_contract.dart'; import 'package:scholars_guide/features/quiz_upload/presentation/state_management/quiz_input/quiz_input_cubit.dart'; -import 'package:scholars_guide/firebase_options.dart'; + +import '../../../../service_locator/service_locator.dart'; class QuizUploadRepositoryImpl implements QuizUploadRepositoryContract { const QuizUploadRepositoryImpl(); @@ -17,28 +19,43 @@ class QuizUploadRepositoryImpl implements QuizUploadRepositoryContract { Future uploadQuestions( {required List questionsToUpload, required SUBJ subjToUpload}) async { + final authService = services(); + final dbService = services(); + + final DocumentReference userRef = + dbService.collection('users').doc(authService.currentUser?.uid); + late bool uploadSucess = true; - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform) - .whenComplete(() { - for (QuizInputCubit questionCubit in questionsToUpload) { - Question question = Question( - question: questionCubit.state.question, - options: questionCubit.state.options, - correctIndex: int.parse(questionCubit.state.answerIndex), - subject: subjToUpload); - - FirebaseFirestore.instance - .collection(FireStore.SUBJ2subject(subjToUpload)) - .add(question.toMap()) - .then((value) => print('Question added to the database')) - .catchError((error) { - print('Failed to add question: $error'); - uploadSucess = false; - }); - } - }); + print('Username uid is: ${authService.currentUser?.uid}'); + + for (QuizInputCubit questionCubit in questionsToUpload) { + // * Uploading the solutions first to the database + DocumentReference solRef = await dbService.collection('solutions').add({ + FireStore.solutionData: questionCubit.solution, + FireStore.createdBy: userRef, + FireStore.createdAt: FieldValue.serverTimestamp(), + }); + + // * Uploading the questions to the database w/ solutions reference + Question question = Question( + question: questionCubit.question, + solutionRef: solRef, + options: questionCubit.options, + correctIndex: int.parse(questionCubit.answerIndex), + subject: subjToUpload, + createdBy: userRef); + + dbService + .collection(FireStore.SUBJ2subject(subjToUpload)) + .add(question.toMap()) + .then((value) => + print('[SUCCESS] | Question added to the database: $value')) + .catchError((error) { + print('[ERROR] | Failed to add question: $error'); + uploadSucess = false; + }); + } return uploadSucess; } diff --git a/lib/features/quiz_upload/presentation/pages/upload_questions_page.dart b/lib/features/quiz_upload/presentation/pages/upload_questions_page.dart index b457e886..7a4070c5 100644 --- a/lib/features/quiz_upload/presentation/pages/upload_questions_page.dart +++ b/lib/features/quiz_upload/presentation/pages/upload_questions_page.dart @@ -1,13 +1,13 @@ // ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:scholars_guide/features/quiz_upload/presentation/state_management/quiz_input_page/quiz_input_page_bloc.dart'; import 'package:scholars_guide/features/quiz_upload/presentation/widgets/quiz_input_page_widgets/add_or_submit_display.dart'; import 'package:scholars_guide/features/quiz_upload/presentation/widgets/quiz_input_page_widgets/change_subject_display.dart'; -import 'package:scholars_guide/features/quiz_upload/presentation/widgets/quiz_input_page_widgets/confirm_cancel_quiz_input_display.dart'; -import 'package:scholars_guide/features/quiz_upload/presentation/widgets/quiz_input_page_widgets/confirm_submit_quiz_input_display.dart'; import 'package:scholars_guide/features/quiz_upload/presentation/widgets/quiz_input_widgets/question_input_display.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:scholars_guide/features/quiz_upload/presentation/widgets/quiz_input_widgets/question_preview_display.dart'; class UploadQuestionPage extends StatefulWidget { const UploadQuestionPage({super.key}); @@ -19,43 +19,68 @@ class UploadQuestionPage extends StatefulWidget { class _UploadQuestionPageState extends State { @override Widget build(BuildContext context) { - return BlocProvider( - create: (blocContext) => QuizInputPageBloc(), - child: GestureDetector( - onTap: () { - FocusScope.of(context).unfocus(); - }, - child: Scaffold( - appBar: AppBar( - title: const Text('Upload Questions'), - ), - body: BlocBuilder( - builder: (blocBuilderContext, state) { - if (state is QuizInputPageQuestionsAdd) { - return Center( - // Possible use ListView to have a scrollable navigation - child: Column( + return DefaultTabController( + initialIndex: 0, + length: 2, + child: BlocProvider( + create: (blocContext) => QuizInputPageBloc(), + child: GestureDetector( + onTap: () { + FocusScope.of(context).unfocus(); + }, + child: BlocBuilder( + builder: (blocBuilderContext, state) { + if (state is QuizInputPageQuestionsAdd) { + return Scaffold( + appBar: AppBar( + title: const Text('Upload Questions'), + bottom: TabBar( + tabs: const [ + Tab( + icon: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.edit_outlined), + SizedBox(width: 8.0), + Text('Edit'), + ], + ), + ), + Tab( + icon: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.preview_outlined), + SizedBox(width: 8.0), + Text('Preview'), + ], + ), + ), + ], + ), + ), + body: TabBarView( children: [ - Container( - margin: EdgeInsets.only(top: 10.0), + SingleChildScrollView( + child: Column( + children: [ + ChangeSubjectDisplay(), + QuestionInputDisplay(), + AddOrSubmitDisplay(), + ], + ), ), - ChangeSubjectDisplay(), - QuestionInputDisplay(), - AddOrSubmitDisplay(), - Container( - margin: EdgeInsets.only(bottom: 20.0), + SingleChildScrollView( + child: QuestionPreviewDisplay(), ), ], - )); - } else if (state is QuizInputPageConfirmSubmit) { - return ConfirmSubmitQuizInputDisplay(); - } else if (state is QuizInputPageConfirmCancel) { - return ConfirmCancelQuizInputDisplay(); - } - - return Text("Something went wrong! (state not found)"); - }, - )), + ), + ); + } + return Text("Something went wrong! (state not found)"); + }, + ), + ), ), ); } diff --git a/lib/features/quiz_upload/presentation/pages/upload_success_page.dart b/lib/features/quiz_upload/presentation/pages/upload_success_page.dart index 860f678c..180110f3 100644 --- a/lib/features/quiz_upload/presentation/pages/upload_success_page.dart +++ b/lib/features/quiz_upload/presentation/pages/upload_success_page.dart @@ -1,6 +1,7 @@ // ignore_for_file: prefer_const_constructors import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:scholars_guide/core/models/question_model.dart'; import 'package:scholars_guide/features/quiz_upload/presentation/state_management/quiz_input/quiz_input_cubit.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -9,11 +10,7 @@ import 'package:scholars_guide/features/quiz_upload/presentation/widgets/upload_ import 'package:scholars_guide/features/quiz_upload/presentation/widgets/upload_quiz_widgets/upload_loading_display.dart'; class UploadSuccessPage extends StatefulWidget { - const UploadSuccessPage( - {super.key, required this.questionsToUpload, required this.subjToUpload}); - - final List questionsToUpload; - final SUBJ subjToUpload; + const UploadSuccessPage({super.key}); @override State createState() => _UploadSuccessPageState(); @@ -22,12 +19,17 @@ class UploadSuccessPage extends StatefulWidget { class _UploadSuccessPageState extends State { @override Widget build(BuildContext buildContext) { + final extraMap = GoRouterState.of(context).extra as Map; + final List questionsToUpload = extraMap['questionsToUpload'] as List; + final SUBJ subjToUpload = extraMap['subjToUpload'] as SUBJ; + return BlocProvider( create: (providerContext) => UploadQuizCubit( - questions: widget.questionsToUpload, subj: widget.subjToUpload) + questions: questionsToUpload, subj: subjToUpload) ..uploadQuiz(), child: Scaffold( appBar: AppBar( + automaticallyImplyLeading: false, title: const Text('Upload Questions'), ), body: BlocBuilder( diff --git a/lib/features/quiz_upload/presentation/state_management/quiz_input/quiz_input_cubit.dart b/lib/features/quiz_upload/presentation/state_management/quiz_input/quiz_input_cubit.dart index 66c20845..6bc37851 100644 --- a/lib/features/quiz_upload/presentation/state_management/quiz_input/quiz_input_cubit.dart +++ b/lib/features/quiz_upload/presentation/state_management/quiz_input/quiz_input_cubit.dart @@ -1,31 +1,21 @@ - import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; part 'quiz_input_state.dart'; class QuizInputCubit extends Cubit { - QuizInputCubit() - : super(const QuizInputState( - question: '', - options: ['', '', '', ''], - answerIndex: '0', - questionNonEmpty: false, - optionsNonEmpty: [false, false, false, false], - )); - - void questionChanged({required String question}) { - // print("Question changed to $question"); - emit(state.copyWith(question: question)); - } + QuizInputCubit() : super(const QuizInputState()); - void optionChanged({required List options, required List optionsNonEmpty}) { - // print("Options changed to $options"); - emit(state.copyWith(options: options, optionsNonEmpty: optionsNonEmpty)); + void refresh() { + emit(QuizInputRefresh()); + emit(QuizInputState()); } - void answerChanged({required String answerIndex}) { - // print("Answer changed to $answerIndex"); - emit(state.copyWith(answerIndex: answerIndex)); - } + String question = ''; + String solution = ''; + List options = ['', '', '', '']; + String answerIndex = '0'; + bool questionNonEmpty = false; + bool solutionNonEmpty = false; + List optionsNonEmpty = [false, false, false, false]; } diff --git a/lib/features/quiz_upload/presentation/state_management/quiz_input/quiz_input_state.dart b/lib/features/quiz_upload/presentation/state_management/quiz_input/quiz_input_state.dart index 75b72202..0b9d99d5 100644 --- a/lib/features/quiz_upload/presentation/state_management/quiz_input/quiz_input_state.dart +++ b/lib/features/quiz_upload/presentation/state_management/quiz_input/quiz_input_state.dart @@ -1,39 +1,9 @@ part of 'quiz_input_cubit.dart'; class QuizInputState extends Equatable { - const QuizInputState({ - required this.question, - required this.options, - required this.answerIndex, - required this.questionNonEmpty, - required this.optionsNonEmpty, - }); - - final String question; - final List options; - final String answerIndex; - - // Empty checker - final bool questionNonEmpty; - final List optionsNonEmpty; - + const QuizInputState(); @override - List get props => [question, options, answerIndex]; - - QuizInputState copyWith({ - String? question, - List? options, - String? answerIndex, - bool? questionNonEmpty, - List? optionsNonEmpty, - }) { - return QuizInputState( - question: question ?? this.question, - options: options ?? this.options, - answerIndex: answerIndex ?? this.answerIndex, - questionNonEmpty: question?.isNotEmpty ?? this.questionNonEmpty, - optionsNonEmpty: - options?.map((e) => e.isNotEmpty).toList() ?? this.optionsNonEmpty, - ); - } + List get props => []; } + +class QuizInputRefresh extends QuizInputState {} diff --git a/lib/features/quiz_upload/presentation/state_management/quiz_input_page/quiz_input_page_bloc.dart b/lib/features/quiz_upload/presentation/state_management/quiz_input_page/quiz_input_page_bloc.dart index e7369e5b..3c944b05 100644 --- a/lib/features/quiz_upload/presentation/state_management/quiz_input_page/quiz_input_page_bloc.dart +++ b/lib/features/quiz_upload/presentation/state_management/quiz_input_page/quiz_input_page_bloc.dart @@ -20,18 +20,22 @@ class QuizInputPageBloc extends Bloc { // * For submitting the quiz upload on((event, emit) { - _printQuestions(); - if (_questionsNotEmpty() && questions.isNotEmpty) { revealBlanks = false; - emit(QuizInputPageConfirmSubmit()); } else { revealBlanks = true; emit(QuizInputPageRefresh()); emit(QuizInputPageQuestionsAdd()); } + _printQuestions(); }); - on((event, emit) { + + // * For resetting the quiz upload after submitting + on((event, emit) { + questions = [QuizInputCubit()]; + subject = SUBJ.MATH; + revealBlanks = false; + emit(QuizInputPageRefresh()); emit(QuizInputPageQuestionsAdd()); }); @@ -60,10 +64,14 @@ class QuizInputPageBloc extends Bloc { SUBJ subject = SUBJ.MATH; bool revealBlanks = false; + bool isSubmittable() { + return _questionsNotEmpty() && questions.isNotEmpty; + } + bool _questionsNotEmpty() { for (var question in questions) { - if (!question.state.questionNonEmpty) return false; - for (var option in question.state.optionsNonEmpty) { + if (!question.questionNonEmpty) return false; + for (var option in question.optionsNonEmpty) { if (!option) return false; } } @@ -75,11 +83,13 @@ class QuizInputPageBloc extends Bloc { for (var question in questions) { print( " ====================================== Printing question ======================================"); - print("Question: ${question.state.question}"); - print("Options: ${question.state.options}"); - print("Answer: ${question.state.answerIndex}"); - print("Question non empty: ${question.state.questionNonEmpty}"); - print("Options non empty: ${question.state.optionsNonEmpty}"); + print("Question: ${question.question}"); + print("Solution: ${question.solution}"); + print("Options: ${question.options}"); + print("Answer: ${question.answerIndex}"); + print("Question non empty: ${question.questionNonEmpty}"); + print("Solution non empty: ${question.solutionNonEmpty}"); + print("Options non empty: ${question.optionsNonEmpty}"); } } } diff --git a/lib/features/quiz_upload/presentation/state_management/quiz_input_page/quiz_input_page_event.dart b/lib/features/quiz_upload/presentation/state_management/quiz_input_page/quiz_input_page_event.dart index ac7ec335..9519bbfa 100644 --- a/lib/features/quiz_upload/presentation/state_management/quiz_input_page/quiz_input_page_event.dart +++ b/lib/features/quiz_upload/presentation/state_management/quiz_input_page/quiz_input_page_event.dart @@ -18,6 +18,8 @@ class QuizInputPageChangedSubject extends QuizInputPageEvent { class QuizInputPageSubmitBtnPressed extends QuizInputPageEvent {} class QuizInputPageCancelSubmitBtnPressed extends QuizInputPageEvent {} +class QuizInputPageReset extends QuizInputPageEvent {} + class QuizInputPageBackBtnPressed extends QuizInputPageEvent {} class QuizInputPageCancelBackBtnPressed extends QuizInputPageEvent {} diff --git a/lib/features/quiz_upload/presentation/state_management/quiz_input_page/quiz_input_page_state.dart b/lib/features/quiz_upload/presentation/state_management/quiz_input_page/quiz_input_page_state.dart index 84331ce8..d306d47c 100644 --- a/lib/features/quiz_upload/presentation/state_management/quiz_input_page/quiz_input_page_state.dart +++ b/lib/features/quiz_upload/presentation/state_management/quiz_input_page/quiz_input_page_state.dart @@ -11,6 +11,4 @@ class QuizInputPageRefresh extends QuizInputPageState {} class QuizInputPageQuestionsAdd extends QuizInputPageState {} -class QuizInputPageConfirmSubmit extends QuizInputPageState {} - class QuizInputPageConfirmCancel extends QuizInputPageState {} diff --git a/lib/features/quiz_upload/presentation/widgets/quiz_input_page_widgets/add_or_submit_display.dart b/lib/features/quiz_upload/presentation/widgets/quiz_input_page_widgets/add_or_submit_display.dart index 40d8bf52..ceb33fa1 100644 --- a/lib/features/quiz_upload/presentation/widgets/quiz_input_page_widgets/add_or_submit_display.dart +++ b/lib/features/quiz_upload/presentation/widgets/quiz_input_page_widgets/add_or_submit_display.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:scholars_guide/features/quiz_upload/presentation/widgets/quiz_input_page_widgets/confirm_submit_quiz_input_dialogue.dart'; import '../../state_management/quiz_input_page/quiz_input_page_bloc.dart'; @@ -8,24 +9,29 @@ class AddOrSubmitDisplay extends StatelessWidget { @override Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - onPressed: () { - context.read().add(QuizInputPageAddBtnPressed()); - }, - child: const Text('Add Question'), - ), - ElevatedButton( - onPressed: () { - context - .read() - .add(QuizInputPageSubmitBtnPressed()); - }, - child: const Text('Submit Questions'), - ) - ], + return Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: () { + context.read().add(QuizInputPageAddBtnPressed()); + }, + child: const Text('Add Question'), + ), + ElevatedButton( + onPressed: () { + showDialog( + context: context, + builder: (BuildContext buildContext) { + return ConfirmSubmitQuizInputDialogue(quizInputPageBloc: context.read()); + }); + }, + child: const Text('Submit Questions'), + ) + ], + ), ); } } diff --git a/lib/features/quiz_upload/presentation/widgets/quiz_input_page_widgets/change_subject_display.dart b/lib/features/quiz_upload/presentation/widgets/quiz_input_page_widgets/change_subject_display.dart index 8ab6a357..87fc5b7a 100644 --- a/lib/features/quiz_upload/presentation/widgets/quiz_input_page_widgets/change_subject_display.dart +++ b/lib/features/quiz_upload/presentation/widgets/quiz_input_page_widgets/change_subject_display.dart @@ -32,10 +32,10 @@ class _ChangeSubjectDisplayState extends State { icon: Icon(Icons.arrow_drop_down), iconSize: 24, elevation: 16, - style: TextStyle(color: Colors.deepPurple), // ! Panget UI + style: TextStyle(color: Color.fromARGB(255, 63, 86, 169)), underline: Container( height: 2, - color: Colors.deepPurpleAccent, + color: Color.fromARGB(255, 63, 86, 169), ), value: Question.SUBJ2string( context.read().subject), diff --git a/lib/features/quiz_upload/presentation/widgets/quiz_input_page_widgets/confirm_cancel_quiz_input_dialogue.dart b/lib/features/quiz_upload/presentation/widgets/quiz_input_page_widgets/confirm_cancel_quiz_input_dialogue.dart new file mode 100644 index 00000000..afda315b --- /dev/null +++ b/lib/features/quiz_upload/presentation/widgets/quiz_input_page_widgets/confirm_cancel_quiz_input_dialogue.dart @@ -0,0 +1,49 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:scholars_guide/features/quiz_mode/presentation/pages/ready_quiz_page.dart'; +import 'package:scholars_guide/features/quiz_upload/presentation/state_management/quiz_input_page/quiz_input_page_bloc.dart'; + +// ! Currently not used +class ConfirmCancelQuizInputDisplay extends StatelessWidget { + const ConfirmCancelQuizInputDisplay({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.6), + ), + child: AlertDialog( + title: const Text('Cancel Quiz Input'), + content: const Text("Your inputs will be discared! Are you sure?"), + actions: [ + ElevatedButton( + child: const Text('Continue Quiz Input'), + onPressed: () { + context + .read() + .add(QuizInputPageCancelBackBtnPressed()); + }, + ), + ElevatedButton( + child: const Text('Go Back'), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => (ReadyQuizPage()), + ), // ! Change to home page + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/quiz_upload/presentation/widgets/quiz_input_page_widgets/confirm_cancel_quiz_input_display.dart b/lib/features/quiz_upload/presentation/widgets/quiz_input_page_widgets/confirm_cancel_quiz_input_display.dart deleted file mode 100644 index f045a13d..00000000 --- a/lib/features/quiz_upload/presentation/widgets/quiz_input_page_widgets/confirm_cancel_quiz_input_display.dart +++ /dev/null @@ -1,44 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:scholars_guide/features/quiz_mode/presentation/pages/ready_quiz_page.dart'; -import 'package:scholars_guide/features/quiz_upload/presentation/state_management/quiz_input_page/quiz_input_page_bloc.dart'; - -class ConfirmCancelQuizInputDisplay extends StatelessWidget { - const ConfirmCancelQuizInputDisplay({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Container( - width: double.infinity, - height: double.infinity, - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.6), - ), - child: AlertDialog( - title: const Text('Cancel Quiz Input'), - content: const Text("Your inputs will be discared! Are you sure?"), - actions: [ - ElevatedButton( - child: const Text('Continue Quiz Input'), - onPressed: () { - context.read().add(QuizInputPageCancelBackBtnPressed()); - }, - ), - ElevatedButton( - child: const Text('Go Back'), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => (ReadyQuizPage())), // ! Change to home page - ); - }, - ), - ], - )), - ); - } -} \ No newline at end of file diff --git a/lib/features/quiz_upload/presentation/widgets/quiz_input_page_widgets/confirm_submit_quiz_input_dialogue.dart b/lib/features/quiz_upload/presentation/widgets/quiz_input_page_widgets/confirm_submit_quiz_input_dialogue.dart new file mode 100644 index 00000000..ffd13c30 --- /dev/null +++ b/lib/features/quiz_upload/presentation/widgets/quiz_input_page_widgets/confirm_submit_quiz_input_dialogue.dart @@ -0,0 +1,44 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:scholars_guide/features/quiz_upload/presentation/state_management/quiz_input_page/quiz_input_page_bloc.dart'; + +class ConfirmSubmitQuizInputDialogue extends StatelessWidget { + const ConfirmSubmitQuizInputDialogue( + {super.key, required this.quizInputPageBloc}); + + final QuizInputPageBloc quizInputPageBloc; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Submit Confirmation'), + content: const Text("Your questions will be submitted! Are you sure?"), + actions: [ + ElevatedButton( + child: const Text('Submit Questions'), + onPressed: () { + Navigator.of(context).pop(); + quizInputPageBloc.add(QuizInputPageSubmitBtnPressed()); + + if (quizInputPageBloc.isSubmittable()) { + GoRouter.of(context) + .go('/quiz-upload/finished-quiz-upload', extra: { + 'questionsToUpload': quizInputPageBloc.questions, + 'subjToUpload': quizInputPageBloc.subject, + }); + quizInputPageBloc.add(QuizInputPageReset()); + } + }, + ), + ElevatedButton( + child: const Text('Go Back'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + } +} diff --git a/lib/features/quiz_upload/presentation/widgets/quiz_input_page_widgets/confirm_submit_quiz_input_display.dart b/lib/features/quiz_upload/presentation/widgets/quiz_input_page_widgets/confirm_submit_quiz_input_display.dart deleted file mode 100644 index 2fff7eb2..00000000 --- a/lib/features/quiz_upload/presentation/widgets/quiz_input_page_widgets/confirm_submit_quiz_input_display.dart +++ /dev/null @@ -1,53 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:scholars_guide/features/quiz_upload/presentation/pages/upload_success_page.dart'; -import 'package:scholars_guide/features/quiz_upload/presentation/state_management/quiz_input_page/quiz_input_page_bloc.dart'; - -class ConfirmSubmitQuizInputDisplay extends StatelessWidget { - const ConfirmSubmitQuizInputDisplay({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Container( - width: double.infinity, - height: double.infinity, - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.6), - ), - child: AlertDialog( - title: const Text('Submit Confirmation'), - content: - const Text("Your questions will be submitted! Are you sure?"), - actions: [ - ElevatedButton( - child: const Text('Submit Questions'), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (newContext) => (UploadSuccessPage( - questionsToUpload: - context.read().questions, - subjToUpload: context - .read() - .subject, - )), - )); - }, - ), - ElevatedButton( - child: const Text('Go Back'), - onPressed: () { - context - .read() - .add(QuizInputPageCancelSubmitBtnPressed()); - }, - ), - ], - )), - ); - } -} diff --git a/lib/features/quiz_upload/presentation/widgets/quiz_input_widgets/question_input_card_display.dart b/lib/features/quiz_upload/presentation/widgets/quiz_input_widgets/question_input_card_display.dart index 241d7971..79d82170 100644 --- a/lib/features/quiz_upload/presentation/widgets/quiz_input_widgets/question_input_card_display.dart +++ b/lib/features/quiz_upload/presentation/widgets/quiz_input_widgets/question_input_card_display.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:scholars_guide/features/quiz_upload/presentation/state_management/quiz_input/quiz_input_cubit.dart'; import 'package:scholars_guide/features/quiz_upload/presentation/state_management/quiz_input_page/quiz_input_page_bloc.dart'; @@ -20,77 +21,98 @@ class _QuestionInputCardState extends State { Widget build(BuildContext context) { QuizInputPageBloc quizInputPageBloc = context.read(); - TextEditingController questionController = - TextEditingController(text: widget.questionCubit.state.question); - - questionController.selection = TextSelection.fromPosition( - TextPosition( - offset: questionController.text.length, - ), - ); + QuizInputCubit questionCubit = widget.questionCubit; - return Container( - width: MediaQuery.of(context).size.width * 0.90, - height: 430, - padding: EdgeInsets.only(top: 10.0, bottom: 10.0), - child: Container( - decoration: BoxDecoration( - color: Colors.deepOrange[100], // ! Panget UI - borderRadius: BorderRadius.circular(10.0), - ), - child: Column( - children: [ - Container( - padding: EdgeInsets.all(8.0), - height: 200, - child: TextField( - decoration: InputDecoration( - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(7.0), - borderSide: BorderSide( - color: quizInputPageBloc.revealBlanks - ? widget.questionCubit.state.questionNonEmpty - ? Colors.black - : Colors.red - : Colors.black, - width: 2), - ), - hintText: "Enter the question here", - filled: true, - fillColor: Colors.white, + TextEditingController questionController = + TextEditingController(text: questionCubit.question); + + TextEditingController solutionController = + TextEditingController(text: questionCubit.solution); + + return Card( + child: FormBuilder( + child: Column( + children: [ + Container( + padding: EdgeInsets.all(8.0), + height: 200, + child: TextField( + decoration: InputDecoration( + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(7.0), + borderSide: BorderSide( + color: quizInputPageBloc.revealBlanks + ? questionCubit.questionNonEmpty + ? Color.fromARGB(255, 178, 178, 178) + : Colors.red + : Color.fromARGB(255, 178, 178, 178), + width: 1), ), - maxLines: null, - expands: true, - controller: questionController, - onChanged: (questionInput) { - widget.questionCubit - .questionChanged(question: questionInput); - }, + hintText: "Enter the question here", + filled: true, + fillColor: Colors.white, ), + maxLines: null, + expands: true, + controller: questionController, + onChanged: (questionInput) { + questionCubit.question = questionInput; + questionCubit.questionNonEmpty = questionInput.isNotEmpty; + }, ), - QuestionInputOptions( - questionCubit: widget.questionCubit, - optionIndex: 0, + ), + QuestionInputOptions( + questionCubit: questionCubit, + optionIndex: 0, + revealBlank: quizInputPageBloc.revealBlanks && + !questionCubit.optionsNonEmpty[0], + ), + QuestionInputOptions( + questionCubit: questionCubit, + optionIndex: 1, revealBlank: quizInputPageBloc.revealBlanks && - !widget.questionCubit.state.optionsNonEmpty[0], + !questionCubit.optionsNonEmpty[1]), + QuestionInputOptions( + questionCubit: questionCubit, + optionIndex: 2, + revealBlank: quizInputPageBloc.revealBlanks && + !questionCubit.optionsNonEmpty[2]), + QuestionInputOptions( + questionCubit: questionCubit, + optionIndex: 3, + revealBlank: quizInputPageBloc.revealBlanks && + !questionCubit.optionsNonEmpty[3]), + Container( + padding: EdgeInsets.all(8.0), + height: 200, + child: TextField( + decoration: InputDecoration( + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(7.0), + borderSide: BorderSide( + color: quizInputPageBloc.revealBlanks + ? questionCubit.solutionNonEmpty + ? Color.fromARGB(255, 178, 178, 178) + : Colors.red + : Color.fromARGB(255, 178, 178, 178), + width: 1), + ), + hintText: "Enter the solution here", + filled: true, + fillColor: Colors.white, + ), + maxLines: null, + expands: true, + controller: solutionController, + onChanged: (solutionInput) { + questionCubit.solution = solutionInput; + questionCubit.solutionNonEmpty = solutionInput.isNotEmpty; + }, ), - QuestionInputOptions( - questionCubit: widget.questionCubit, - optionIndex: 1, - revealBlank: quizInputPageBloc.revealBlanks && - !widget.questionCubit.state.optionsNonEmpty[1]), - QuestionInputOptions( - questionCubit: widget.questionCubit, - optionIndex: 2, - revealBlank: quizInputPageBloc.revealBlanks && - !widget.questionCubit.state.optionsNonEmpty[2]), - QuestionInputOptions( - questionCubit: widget.questionCubit, - optionIndex: 3, - revealBlank: quizInputPageBloc.revealBlanks && - !widget.questionCubit.state.optionsNonEmpty[3]), - ], - )), + ), + ], + ), + ), ); } } @@ -111,72 +133,69 @@ class QuestionInputOptions extends StatefulWidget { } class _QuestionInputOptionsState extends State { - bool correctAnswer = true; @override Widget build(BuildContext context) { - TextEditingController optionController = TextEditingController( - text: widget.questionCubit.state.options[widget.optionIndex]); + QuizInputCubit questionCubit = widget.questionCubit; + int optionIndex = widget.optionIndex; + bool revealBlank = widget.revealBlank; - optionController.selection = TextSelection.fromPosition( - TextPosition( - offset: optionController.text.length, - ), - ); + TextEditingController optionController = + TextEditingController(text: questionCubit.options[optionIndex]); return Container( - padding: EdgeInsets.all(2.0), - margin: EdgeInsets.only(left: 30.0), - width: MediaQuery.of(context).size.width * 0.70, - height: 50, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - child: TextField( - decoration: InputDecoration( - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(7.0), - borderSide: BorderSide( - color: widget.revealBlank ? Colors.red : Colors.black, - width: 2), - ), - hintText: "Enter one of the options here", - filled: true, - fillColor: Colors.white, + padding: EdgeInsets.all(2.0), + margin: EdgeInsets.only(left: 30.0), + width: MediaQuery.of(context).size.width * 0.90, + height: 50, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: TextField( + decoration: InputDecoration( + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(7.0), + borderSide: BorderSide( + color: revealBlank + ? Colors.red + : Color.fromARGB(255, 178, 178, 178), + width: 1), ), - controller: optionController, - onChanged: (optionInput) { - List options = - List.from(widget.questionCubit.state.options); - options[widget.optionIndex] = optionInput; - - List optionsNonEmpty = - List.from(widget.questionCubit.state.optionsNonEmpty); - optionsNonEmpty[widget.optionIndex] = - options[widget.optionIndex].isNotEmpty; - - widget.questionCubit.optionChanged( - options: options, optionsNonEmpty: optionsNonEmpty); - }, + hintText: "Enter one of the options here", + filled: true, + fillColor: Colors.white, ), - ), - IconButton( - icon: Icon(correctAnswer - ? Icons.check_circle - : Icons.check_circle_outline_rounded), - color: widget.optionIndex.toString() == - widget.questionCubit.state.answerIndex - ? Colors.green - : Colors.black, - onPressed: () { - if (widget.optionIndex.toString() != - widget.questionCubit.state.answerIndex) { - widget.questionCubit.answerChanged( - answerIndex: widget.optionIndex.toString()); - } + controller: optionController, + onChanged: (optionInput) { + List options = List.from(questionCubit.options); + options[optionIndex] = optionInput; + + List optionsNonEmpty = + List.from(questionCubit.optionsNonEmpty); + optionsNonEmpty[optionIndex] = optionInput.isNotEmpty; + + questionCubit.options = options; + questionCubit.optionsNonEmpty = optionsNonEmpty; }, ), - ], - )); + ), + BlocBuilder( + bloc: questionCubit, + builder: (context, state) { + return IconButton( + icon: Icon(Icons.check_circle_outline_rounded), + color: optionIndex.toString() == questionCubit.answerIndex + ? Colors.green + : Color.fromARGB(255, 108, 108, 108), + onPressed: () { + questionCubit.refresh(); + questionCubit.answerIndex = optionIndex.toString(); + }, + ); + }, + ), + ], + ), + ); } } diff --git a/lib/features/quiz_upload/presentation/widgets/quiz_input_widgets/question_input_display.dart b/lib/features/quiz_upload/presentation/widgets/quiz_input_widgets/question_input_display.dart index d4f21635..0f331244 100644 --- a/lib/features/quiz_upload/presentation/widgets/quiz_input_widgets/question_input_display.dart +++ b/lib/features/quiz_upload/presentation/widgets/quiz_input_widgets/question_input_display.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:scholars_guide/features/quiz_upload/presentation/state_management/quiz_input/quiz_input_cubit.dart'; import 'package:scholars_guide/features/quiz_upload/presentation/state_management/quiz_input_page/quiz_input_page_bloc.dart'; import 'package:scholars_guide/features/quiz_upload/presentation/widgets/quiz_input_widgets/question_input_card_display.dart'; @@ -17,43 +16,35 @@ class QuestionInputDisplay extends StatefulWidget { class _QuestionInputDisplayState extends State { @override Widget build(BuildContext buildContext) { - return Expanded( - child: ListView.builder( - itemCount: buildContext.read().questions.length, - itemBuilder: (listViewBuilderContext, index) { - return Center( - child: Dismissible( - background: Container( - width: 50.0, - height: 50.0, - color: Colors.red, - alignment: Alignment.centerRight, - padding: EdgeInsets.only(right: 20.0), - margin: EdgeInsets.only(bottom: 10.0), - child: Icon(Icons.delete), - ), - direction: DismissDirection.endToStart, - key: ValueKey(index), - child: BlocBuilder( - bloc: buildContext.read().questions[index], - builder: (context, state) { - return QuestionInputCard( - questionCubit: buildContext - .read() - .questions[index]); - }, - ), - onDismissed: (_) { - buildContext.read().add( - QuizInputPageDeleteBtnPressed( - questionCubit: buildContext - .read() - .questions[index])); - }, - ), - ); - }, - ), + return ListView.builder( + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.all(5.0), + shrinkWrap: true, + itemCount: buildContext.read().questions.length, + itemBuilder: (_, index) { + return Dismissible( + background: Container( + color: Colors.white, + alignment: Alignment.centerRight, + padding: EdgeInsets.only(right: 20.0), + margin: EdgeInsets.only(bottom: 10.0), + child: Icon(Icons.delete), + ), + direction: DismissDirection.endToStart, + key: UniqueKey(), + child: QuestionInputCard( + questionCubit: + buildContext.read().questions[index]), + onDismissed: (_) { + buildContext.read().add( + QuizInputPageDeleteBtnPressed( + questionCubit: + buildContext.read().questions[index], + ), + ); + }, + ); + }, ); } } diff --git a/lib/features/quiz_upload/presentation/widgets/quiz_input_widgets/question_preview_card_display.dart b/lib/features/quiz_upload/presentation/widgets/quiz_input_widgets/question_preview_card_display.dart new file mode 100644 index 00000000..2a2555d2 --- /dev/null +++ b/lib/features/quiz_upload/presentation/widgets/quiz_input_widgets/question_preview_card_display.dart @@ -0,0 +1,126 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:flutter_markdown_latex/flutter_markdown_latex.dart'; +import 'package:scholars_guide/features/quiz_upload/presentation/state_management/quiz_input/quiz_input_cubit.dart'; +import 'package:markdown/markdown.dart' as md; + +class QuestionPreviewCardDisplay extends StatelessWidget { + const QuestionPreviewCardDisplay({super.key, required this.questionCubit}); + + final QuizInputCubit questionCubit; + + @override + Widget build(BuildContext context) { + int correctIndex = int.parse(questionCubit.answerIndex); + + return Card( + margin: EdgeInsets.all(10.0), + child: Column( + children: [ + // Question + TextContainer(text: questionCubit.question), + + // Choices + OptionContainer( + text: "A: ${questionCubit.options[0]}", + isCorrect: 0 == correctIndex), + OptionContainer( + text: "B: ${questionCubit.options[1]}", + isCorrect: 1 == correctIndex), + OptionContainer( + text: "C: ${questionCubit.options[2]}", + isCorrect: 2 == correctIndex), + OptionContainer( + text: "D: ${questionCubit.options[3]}", + isCorrect: 3 == correctIndex), + + // Solution + TextContainer(text: questionCubit.solution), + ], + ), + ); + } +} + +class TextContainer extends StatelessWidget { + const TextContainer({ + super.key, + required this.text, + }); + + final String text; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + border: Border.all( + color: Color.fromARGB(255, 178, 178, 178), + ), + borderRadius: BorderRadius.circular(10.0)), + constraints: BoxConstraints( + minHeight: MediaQuery.of(context).size.height * 0.1, + ), + margin: EdgeInsets.all(8.0), + child: TextMarkdown(text: text), + ); + } +} + +class OptionContainer extends StatelessWidget { + const OptionContainer({ + super.key, + required this.text, + required this.isCorrect, + }); + + final String text; + final bool isCorrect; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + border: Border.all( + color: + isCorrect ? Colors.green : Color.fromARGB(255, 178, 178, 178), + ), + borderRadius: BorderRadius.circular(10.0)), + margin: EdgeInsets.all(2.0), + constraints: BoxConstraints( + minHeight: MediaQuery.of(context).size.height * 0.02, + ), + width: MediaQuery.of(context).size.width * 0.8, + child: TextMarkdown(text: text), + ); + } +} + +class TextMarkdown extends StatelessWidget { + const TextMarkdown({ + super.key, + required this.text, + }); + + final String text; + + @override + Widget build(BuildContext context) { + return Markdown( + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + data: text, + builders: { + 'latex': LatexElementBuilder(), + }, + extensionSet: md.ExtensionSet( + [LatexBlockSyntax()], + [LatexInlineSyntax()], + ), + ); + } +} diff --git a/lib/features/quiz_upload/presentation/widgets/quiz_input_widgets/question_preview_display.dart b/lib/features/quiz_upload/presentation/widgets/quiz_input_widgets/question_preview_display.dart new file mode 100644 index 00000000..157f0355 --- /dev/null +++ b/lib/features/quiz_upload/presentation/widgets/quiz_input_widgets/question_preview_display.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:scholars_guide/features/quiz_upload/presentation/state_management/quiz_input_page/quiz_input_page_bloc.dart'; +import 'package:scholars_guide/features/quiz_upload/presentation/widgets/quiz_input_widgets/question_preview_card_display.dart'; + +class QuestionPreviewDisplay extends StatelessWidget { + const QuestionPreviewDisplay({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: List.generate( + context.read().questions.length, + (index) { + return QuestionPreviewCardDisplay( + questionCubit: context.read().questions[index], + ); + }, + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/quiz_upload/presentation/widgets/upload_quiz_widgets/upload_complete_display.dart b/lib/features/quiz_upload/presentation/widgets/upload_quiz_widgets/upload_complete_display.dart index 47ff2e3e..da5e9e41 100644 --- a/lib/features/quiz_upload/presentation/widgets/upload_quiz_widgets/upload_complete_display.dart +++ b/lib/features/quiz_upload/presentation/widgets/upload_quiz_widgets/upload_complete_display.dart @@ -1,8 +1,7 @@ // ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables, prefer_const_constructors_in_immutables import 'package:flutter/material.dart'; -import 'package:scholars_guide/features/quiz_mode/presentation/pages/ready_quiz_page.dart'; -import 'package:scholars_guide/features/quiz_upload/presentation/pages/upload_questions_page.dart'; +import 'package:go_router/go_router.dart'; class UploadCompleteDisplay extends StatelessWidget { UploadCompleteDisplay({super.key}); @@ -14,24 +13,20 @@ class UploadCompleteDisplay extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Text("Upload Success!"), - // TODO: Show results / Show all questions uploaded by user + + // Todo: Show results / Show all questions uploaded by user + ElevatedButton( onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => (UploadQuestionPage())), - ); + GoRouter.of(context).go('/quiz-upload'); }, child: Text("Add more questions")), + ElevatedButton( - onPressed: () {}, - child: Text("Go Back to Home Page")), // ! Add a route to home page - ElevatedButton(onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => (ReadyQuizPage())), - ); - }, child: Text("Take a quiz now!")) + onPressed: () { + GoRouter.of(context).go('/quiz-mode'); + }, + child: Text("Take a quiz now!")) ], )); } diff --git a/lib/router/app_router.dart b/lib/router/app_router.dart index c5e94740..f2891569 100644 --- a/lib/router/app_router.dart +++ b/lib/router/app_router.dart @@ -1,7 +1,11 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:scholars_guide/features/quiz_mode/presentation/pages/finished_quiz_page.dart'; +import 'package:scholars_guide/features/quiz_mode/presentation/pages/quiz_page.dart'; import 'package:scholars_guide/features/quiz_mode/presentation/pages/ready_quiz_page.dart'; +import 'package:scholars_guide/features/quiz_mode/presentation/pages/solutions_quiz_page.dart'; import 'package:scholars_guide/features/quiz_upload/presentation/pages/upload_questions_page.dart'; +import 'package:scholars_guide/features/quiz_upload/presentation/pages/upload_success_page.dart'; import '../core/features/auth/presentation/screens/email_verification_screen.dart'; import '../core/features/auth/presentation/screens/forgot_password_screen.dart'; @@ -122,6 +126,26 @@ class AppRouter { child: ReadyQuizPage(), ); }, + routes: [ + GoRoute( + path: 'start-quiz', + pageBuilder: (context, state) => const NoTransitionPage( + child: QuizPage(), + ), + ), + GoRoute( + path: 'finished-quiz', + pageBuilder: (context, state) => const NoTransitionPage( + child: FinishedQuizPage(), + ), + ), + GoRoute( + path: 'solutions-quiz', + pageBuilder: (context, state) => const NoTransitionPage( + child: SolutionsQuizPage(), + ), + ), + ], ), ], ), @@ -138,6 +162,14 @@ class AppRouter { child: UploadQuestionPage(), ); }, + routes: [ + GoRoute( + path: 'finished-quiz-upload', + pageBuilder: (context, state) => const NoTransitionPage( + child: UploadSuccessPage(), + ), + ), + ], ), ], ), diff --git a/pubspec.lock b/pubspec.lock index 812d5f12..20ab4648 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -499,14 +499,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.10" - flutter_portal: - dependency: "direct main" - description: - name: flutter_portal - sha256: "4601b3dc24f385b3761721bd852a3f6c09cddd4e943dd184ed58ee1f43006257" - url: "https://pub.dev" - source: hosted - version: "1.1.4" flutter_quill: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 6ecd1342..77272c05 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -98,10 +98,6 @@ dependencies: # An animated circular countdown using Circular Countdown Timer. # Used for quiz mode's countdown timer. circular_countdown_timer: ^0.2.3 - - # This library is an enhancement and replacement to Flutter's - # built-in Overlay/OverlayEntry. Intuitive and declarative. - flutter_portal: ^1.1.4 # This package helps in creation of forms in Flutter by removing the boilerplate # code, reusing validation, react to changes, and collect final user input.