Repository: PTLam-Thryve/quizlet_clone
Pull request #24: Feat/quizlet 22/create flashcard
Author: login: Megadebt
Labels: name: enhancement
description: New feature or request
name: Review effort 2/5
Created: 2025-03-06T19:47:15Z | Merged: 2025-03-12T09:37:24Z

### **User description**
https://github.com/user-attachments/assets/b5eaed9f-f1d6-418b-afbc-0a7962bd26a9

This branch introduces the feature of creating individual flashcards within a flashcard set. 

By pressing on the add button in the Flashcard Detail Page, you are taken to the Create Flashcard Page, which contains two fields: Question and Answer. They both have character limits and have validators, which check whether the text fields are empty or not, as well as whether the maximum text length is exceeded.


___

### **PR Type**
Enhancement, Tests


___

### **Description**
- Introduced the `CreateFlashcardPage` for adding flashcards to sets.

- Added `CreateFlashcardFormBloc` and states for managing flashcard creation.

- Enhanced `FlashCardService` with a `createFlashcard` method.

- Updated UI components and navigation for flashcard creation.


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><details><summary>11 files</summary><table>
<tr>
  <td><strong>create_flashcard_form_bloc.dart</strong><dd><code>Added bloc for managing flashcard creation logic</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
  <td><a href="https://github.com/PTLam-Thryve/quizlet_clone/pull/24/files#diff-269bd76aeabdad16106c28d6a6cc5175eee86c87c64d94938ea8c196aa45a44e">+34/-0</a>&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>create_flashcard_form_bloc_state.dart</strong><dd><code>Defined states for flashcard creation process</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
  <td><a href="https://github.com/PTLam-Thryve/quizlet_clone/pull/24/files#diff-7a800c4e809e364e507fd7366d675718d99bd35a50e73c9bf883bade55f851ca">+36/-0</a>&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>flashcard_service.dart</strong><dd><code>Enhanced service with flashcard creation functionality</code>&nbsp; &nbsp; &nbsp; </dd></td>
  <td><a href="https://github.com/PTLam-Thryve/quizlet_clone/pull/24/files#diff-24350442dd1d093581953f485fd4c1c4014856d3bacd15f8a41aca9ed923373d">+49/-12</a>&nbsp; </td>

</tr>

<tr>
  <td><strong>app_texts.dart</strong><dd><code>Added new text constants for flashcard creation</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
  <td><a href="https://github.com/PTLam-Thryve/quizlet_clone/pull/24/files#diff-2060d60c5bcac0dac733bc1fa231f0454e0275ec7b1d052250be2b589af56368">+2/-0</a>&nbsp; &nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>create_flash_card_set_page.dart</strong><dd><code>Renamed and updated flashcard set creation page</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
  <td><a href="https://github.com/PTLam-Thryve/quizlet_clone/pull/24/files#diff-20ece5abb63e0fe370b4a84953611ff62e68b80f35e9d0c78683449105e06bff">+5/-5</a>&nbsp; &nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>create_flashcard_page.dart</strong><dd><code>Added new page for creating individual flashcards</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
  <td><a href="https://github.com/PTLam-Thryve/quizlet_clone/pull/24/files#diff-7792e72954154d35a55b53a2744d3e2f9cdf0975a6686a307829c0516f388f06">+118/-0</a>&nbsp; </td>

</tr>

<tr>
  <td><strong>flashcardset_detail_page.dart</strong><dd><code>Added navigation to flashcard creation page</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
  <td><a href="https://github.com/PTLam-Thryve/quizlet_clone/pull/24/files#diff-efacafb6e8d94671027cde97274f369fbad8912b0083722ff178dafae035b6db">+21/-3</a>&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>home_page.dart</strong><dd><code>Updated navigation to renamed flashcard set creation page</code></dd></td>
  <td><a href="https://github.com/PTLam-Thryve/quizlet_clone/pull/24/files#diff-646ff82d55407a5c6068a3b59da1b553f5ef7a676b04a26b99db9179dab68559">+2/-2</a>&nbsp; &nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>flashcard_form.dart</strong><dd><code>Added reusable form widget for flashcard creation</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
  <td><a href="https://github.com/PTLam-Thryve/quizlet_clone/pull/24/files#diff-495b28eda87f9af34a97fabf4ef9c2daa67f3e544a81ffebbab990e9474d54ce">+54/-0</a>&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>answer_form.dart</strong><dd><code>Added form field for flashcard answer input</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
  <td><a href="https://github.com/PTLam-Thryve/quizlet_clone/pull/24/files#diff-b592b3588d8ca2e707da1dac145a20c8e19af9750a193e852db76a6323d2a6c9">+23/-0</a>&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>question_form.dart</strong><dd><code>Added form field for flashcard question input</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
  <td><a href="https://github.com/PTLam-Thryve/quizlet_clone/pull/24/files#diff-2eb9b6e36afa9762baeee66e4ede3213f77d357c35b986255721a50555a20bdb">+23/-0</a>&nbsp; &nbsp; </td>

</tr>
</table></details></td></tr><tr><td><strong>Additional files</strong></td><td><details><summary>1 files</summary><table>
<tr>
  <td><strong>history.sqlite</strong></td>
  <td><a href="https://github.com/PTLam-Thryve/quizlet_clone/pull/24/files#diff-68a26b10510dd469f9d142e6e72ef1c3617c83fb7cf299e6a7d1fb64ec33d79d">[link]</a>&nbsp; &nbsp; </td>

</tr>
</table></details></td></tr></tr></tbody></table>

___

> <details> <summary>  Need help?</summary><li>Type <code>/help how to ...</code> in the comments thread for any questions about Qodo Merge usage.</li><li>Check out the <a href="https://qodo-merge-docs.qodo.ai/usage-guide/">documentation</a> for more information.</li></details>


Diff:
diff --git a/.qodo/history.sqlite b/.qodo/history.sqlite
deleted file mode 100644
index ee54a64..0000000
Binary files a/.qodo/history.sqlite and /dev/null differ
diff --git a/lib/bloc/create_bloc/create_flashcard_form_bloc.dart b/lib/bloc/create_bloc/create_flashcard_form_bloc.dart
new file mode 100644
index 0000000..653dc41
--- /dev/null
+++ b/lib/bloc/create_bloc/create_flashcard_form_bloc.dart
@@ -0,0 +1,34 @@
+import 'package:cloud_firestore/cloud_firestore.dart';
+import 'package:flutter/foundation.dart';
+import 'package:quizlet_clone/bloc/create_bloc/create_flashcard_form_bloc_state.dart';
+import 'package:quizlet_clone/data/flashcard_service.dart';
+
+class CreateFlashcardFormBloc extends ChangeNotifier {
+  CreateFlashcardFormBloc(this._flashCardService);
+
+  CreateFlashcardFormBlocState _state = CreateFlashcardFormInitialState();
+
+  final FlashCardService _flashCardService;
+
+  CreateFlashcardFormBlocState get state => _state;
+
+  Future<void> createFlashcard(
+      {required String question,
+      required String answer,
+      required String flashCardSetId}) async {
+    _state = CreateFlashcardLoadingState();
+    notifyListeners();
+    try {
+      final flashCardAdded = await _flashCardService.createFlashcard(
+        question: question,
+        answer: answer,
+        flashCardSetId: flashCardSetId,
+      );
+      _state = CreateFlashcardSuccessState(flashCardAdded);
+    } on FirebaseException catch (error) {
+      throw FlashCardServiceException.fromFirebaseException(error);
+    } finally{
+      notifyListeners();
+    }
+  }
+}
diff --git a/lib/bloc/create_bloc/create_flashcard_form_bloc_state.dart b/lib/bloc/create_bloc/create_flashcard_form_bloc_state.dart
new file mode 100644
index 0000000..ecd1403
--- /dev/null
+++ b/lib/bloc/create_bloc/create_flashcard_form_bloc_state.dart
@@ -0,0 +1,36 @@
+import 'package:quizlet_clone/data/flashcard_service.dart';
+import 'package:quizlet_clone/models/flashcard.dart';
+import 'package:quizlet_clone/ui/constants/app_texts.dart';
+
+sealed class CreateFlashcardFormBlocState {
+  bool get isSuccessful => this is CreateFlashcardSuccessState;
+
+  bool get isLoading => this is CreateFlashcardLoadingState;
+}
+
+class CreateFlashcardFormInitialState extends CreateFlashcardFormBlocState {}
+
+class CreateFlashcardLoadingState extends CreateFlashcardFormBlocState {}
+
+class CreateFlashcardSuccessState extends CreateFlashcardFormBlocState {
+  CreateFlashcardSuccessState(this.flashcard,);
+
+  final Flashcard flashcard;
+}
+
+class CreateFlashcardErrorState extends CreateFlashcardFormBlocState {
+  CreateFlashcardErrorState(this.error);
+
+  final FlashCardServiceException error;
+
+  String get errorMessage {
+    switch (error) {
+      case FlashCardServicePermissionException():
+        return AppTexts.insufficientPermission;
+      case InternalFlashCardServiceException():
+        return AppTexts.internalError;
+      case GenericFlashCardServiceException():
+        return AppTexts.unknownError;
+    }
+  }
+}
diff --git a/lib/bloc/flashcard_list_bloc/flashcard_list_bloc_state.dart b/lib/bloc/flashcard_list_bloc/flashcard_list_bloc_state.dart
index c646b17..cba73e0 100644
--- a/lib/bloc/flashcard_list_bloc/flashcard_list_bloc_state.dart
+++ b/lib/bloc/flashcard_list_bloc/flashcard_list_bloc_state.dart
@@ -24,11 +24,11 @@ class FlashCardListErrorState extends FlashCardListState {
 
   String get errorMessage {
     switch (error) {
-      case InsufficientPermissionException():
+      case FlashCardServicePermissionException():
         return AppTexts.insufficientPermission;
-      case InternalErrorException():
+      case InternalFlashCardServiceException():
         return AppTexts.internalError;
-      case GenericFirestoreException():
+      case GenericFlashCardServiceException():
         return AppTexts.unknownError;
     }
   }
diff --git a/lib/data/flashcard_service.dart b/lib/data/flashcard_service.dart
index 70e911f..e71e995 100644
--- a/lib/data/flashcard_service.dart
+++ b/lib/data/flashcard_service.dart
@@ -1,25 +1,57 @@
 import 'package:cloud_firestore/cloud_firestore.dart';
 import 'package:quizlet_clone/models/flashcard.dart';
 
-class FlashCardService{
+class FlashCardService {
   final FirebaseFirestore _fireStore = FirebaseFirestore.instance;
 
-  Future<List<Flashcard>> getFlashcards(final String flashCardSetid) async{
-    try{
-      final getFromCollection = await _fireStore.collection('flashcard-sets').doc(flashCardSetid).collection('flashcards').get();
-      final flashcards = getFromCollection.docs.map((doc){
-        final question = doc.data().containsKey('question')?doc['question'].toString():'';
-        final answer = doc.data().containsKey('answer')?doc['answer'].toString():'';
+  Future<List<Flashcard>> getFlashcards(final String flashCardSetid) async {
+    try {
+      final getFromCollection = await _fireStore
+          .collection('flashcard-sets')
+          .doc(flashCardSetid)
+          .collection('flashcards')
+          .get();
+      final flashcards = getFromCollection.docs.map((doc) {
+        final question = doc.data().containsKey('question')
+            ? doc['question'].toString()
+            : '';
+        final answer =
+            doc.data().containsKey('answer') ? doc['answer'].toString() : '';
         return Flashcard(
           answer: answer,
           question: question,
-          id: doc.id,
+          id: doc.id
         );
       }).toList();
       return flashcards;
-    } on FirebaseException catch(error){
+    } on FirebaseException catch (error) {
       throw FlashCardServiceException.fromFirebaseException(error);
-    } catch(error){
+    } catch (error) {
+      rethrow;
+    }
+  }
+
+  Future<Flashcard> createFlashcard(
+      {required String question,
+      required String answer,
+      required String flashCardSetId}) async {
+    try {
+      final addedFlashcard = await _fireStore
+          .collection('flashcard-sets')
+          .doc(flashCardSetId)
+          .collection('flashcards')
+          .add({
+        'question': question,
+        'answer': answer,
+      });
+      return Flashcard(
+          id: addedFlashcard.id,
+          question: question,
+          answer: answer,
+        );
+    } on FirebaseException catch (error) {
+      throw FlashCardServiceException.fromFirebaseException(error);
+    } catch (error) {
       rethrow;
     }
   }
@@ -30,17 +62,17 @@ sealed class FlashCardServiceException implements Exception {
       FirebaseException firebaseException) {
     switch (firebaseException.code) {
       case 'insufficient-permission':
-        return InsufficientPermissionException();
+        return FlashCardServicePermissionException();
       case 'internal-error':
-        return InternalErrorException();
+        return InternalFlashCardServiceException();
       default:
-        return GenericFirestoreException();
+        return GenericFlashCardServiceException();
     }
   }
 }
 
-class InsufficientPermissionException extends FlashCardServiceException {}
+class FlashCardServicePermissionException extends FlashCardServiceException {}
 
-class InternalErrorException extends FlashCardServiceException{}
+class InternalFlashCardServiceException extends FlashCardServiceException {}
 
-class GenericFirestoreException extends FlashCardServiceException {}
\ No newline at end of file
+class GenericFlashCardServiceException extends FlashCardServiceException {}
diff --git a/lib/ui/constants/app_texts.dart b/lib/ui/constants/app_texts.dart
index 8364a2f..dacda8b 100644
--- a/lib/ui/constants/app_texts.dart
+++ b/lib/ui/constants/app_texts.dart
@@ -28,4 +28,6 @@ abstract base class AppTexts {
   static const String deleteSuccess = 'Flashcard set successfully deleted';
   static const String deleteError = 'Flashcard set could not be deleted';
   static const String flashCardSetEmpty = 'No flashcards found in this set';
+  static const String createFlashcardSuccess = 'Flashcard successfully created';
+  static const String editFlashcardSuccess = 'Flashcard successfully edited';
 }
diff --git a/lib/ui/pages/create_flash_card_page.dart b/lib/ui/pages/create_flash_card_set_page.dart
similarity index 90%
rename from lib/ui/pages/create_flash_card_page.dart
rename to lib/ui/pages/create_flash_card_set_page.dart
index fa12b0b..cfb8445 100644
--- a/lib/ui/pages/create_flash_card_page.dart
+++ b/lib/ui/pages/create_flash_card_set_page.dart
@@ -10,14 +10,14 @@ import 'package:quizlet_clone/ui/utils/show_app_snack_bar.dart';
 import 'package:quizlet_clone/ui/widgets/forms/flash_card_set_form.dart';
 import 'package:quizlet_clone/ui/widgets/loading_overlay.dart';
 
-class CreateFlashCardPage extends StatefulWidget {
-  const CreateFlashCardPage({super.key});
+class CreateFlashCardSetPage extends StatefulWidget {
+  const CreateFlashCardSetPage({super.key});
 
   @override
-  State<CreateFlashCardPage> createState() => _CreateFlashCardPageState();
+  State<CreateFlashCardSetPage> createState() => _CreateFlashCardPageState();
 }
 
-class _CreateFlashCardPageState extends State<CreateFlashCardPage> {
+class _CreateFlashCardPageState extends State<CreateFlashCardSetPage> {
   late final CreateFlashCardSetFormBloc _createFlashCardBloc;
 
   final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
@@ -66,7 +66,7 @@ class _CreateFlashCardPageState extends State<CreateFlashCardPage> {
           canPop: !bloc.state.isLoading,
           child: Scaffold(
             appBar: AppBar(
-              title: const Text('Create your Flashcard'),
+              title: const Text('Create your Flashcard set'),
             ),
             body: ListView(
               padding: const EdgeInsets.all(16),
diff --git a/lib/ui/pages/create_flashcard_page.dart b/lib/ui/pages/create_flashcard_page.dart
new file mode 100644
index 0000000..f5591c2
--- /dev/null
+++ b/lib/ui/pages/create_flashcard_page.dart
@@ -0,0 +1,118 @@
+import 'dart:async';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'package:quizlet_clone/bloc/create_bloc/create_flashcard_form_bloc.dart';
+import 'package:quizlet_clone/bloc/create_bloc/create_flashcard_form_bloc_state.dart';
+import 'package:quizlet_clone/ui/constants/app_texts.dart';
+import 'package:quizlet_clone/ui/pages/flashcardset_detail_page.dart';
+import 'package:quizlet_clone/ui/utils/show_app_snack_bar.dart';
+import 'package:quizlet_clone/ui/widgets/forms/flashcard_form.dart';
+import 'package:quizlet_clone/ui/widgets/loading_overlay.dart';
+
+class CreateFlashcardPage extends StatefulWidget {
+  const CreateFlashcardPage({required this.flashCardSetId, super.key});
+  final String flashCardSetId;
+
+  @override
+  State<CreateFlashcardPage> createState() => _CreateFlashcardPageState();
+}
+
+class _CreateFlashcardPageState extends State<CreateFlashcardPage> {
+  late final CreateFlashcardFormBloc _createFlashcardFormBloc;
+
+  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
+  final TextEditingController _questionController = TextEditingController();
+  final TextEditingController _answerController = TextEditingController();
+
+  @override
+  void initState() {
+    super.initState();
+    _createFlashcardFormBloc = context.read<CreateFlashcardFormBloc>()
+      ..addListener(createStatusListener);
+  }
+
+  @override
+  void dispose() {
+    _createFlashcardFormBloc.removeListener(createStatusListener);
+    _questionController.dispose();
+    _answerController.dispose();
+    super.dispose();
+  }
+
+  void createStatusListener() {
+    switch (_createFlashcardFormBloc.state) {
+      case CreateFlashcardSuccessState _:
+        showAppSnackBar(context,
+            message: AppTexts.createFlashcardSuccess,
+            status: SnackBarStatus.success);
+            Navigator.of(context).pop();
+        unawaited(
+          //Navigator.of(context).pop() only returns to the old version of the page instead of
+          //the new version with newly created flashcard
+          Navigator.of(context).pushReplacement(
+            MaterialPageRoute(
+              builder: (_) => FlashCardSetDetailPage(
+                flashCardSetid: widget.flashCardSetId,
+              ),
+            ),
+          ),
+        );
+      case CreateFlashcardErrorState errorState:
+        showAppSnackBar(
+          context,
+          message: errorState.errorMessage,
+          status: SnackBarStatus.error,
+        );
+      default:
+        break;
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) => Consumer<CreateFlashcardFormBloc>(
+        builder: (context, bloc, _) => LoadingOverlay(
+          isLoading: bloc.state.isLoading,
+          canPop: !bloc.state.isLoading,
+          child: Scaffold(
+            appBar: AppBar(
+              title: const Text('Create your Flashcard'),
+            ),
+            body: ListView(
+              padding: const EdgeInsets.all(16),
+              children: [
+                FlashcardForm(
+                  formKey: _formKey,
+                  questionController: _questionController,
+                  questionValidator: _validateQuestionAndAnswer,
+                  answerController: _answerController,
+                  answerValidator: _validateQuestionAndAnswer,
+                  onPressed: onCreatePressed,
+                  buttonLabel: AppTexts.create,
+                )
+              ],
+            ),
+          ),
+        ),
+      );
+  void onCreatePressed() {
+    if (_formKey.currentState?.validate() ?? false) {
+      unawaited(
+        _createFlashcardFormBloc.createFlashcard(
+          question: _questionController.text,
+          answer: _answerController.text,
+          flashCardSetId: widget.flashCardSetId,
+        ),
+      );
+    }
+  }
+}
+
+String? _validateQuestionAndAnswer(String? value) {
+  if (value == null || value.isEmpty) {
+    return 'This field cannot be empty';
+  }
+  if (value.length > 50) {
+    return 'This field cannot be more than 50 characters';
+  }
+  return null;
+}
diff --git a/lib/ui/pages/flashcardset_detail_page.dart b/lib/ui/pages/flashcardset_detail_page.dart
index 3987838..2180fd2 100644
--- a/lib/ui/pages/flashcardset_detail_page.dart
+++ b/lib/ui/pages/flashcardset_detail_page.dart
@@ -1,7 +1,11 @@
+import 'dart:async';
+
 import 'package:flutter/material.dart';
 import 'package:provider/provider.dart';
+import 'package:quizlet_clone/bloc/create_bloc/create_flashcard_form_bloc.dart';
 import 'package:quizlet_clone/bloc/flashcard_list_bloc/flashcard_list_bloc.dart';
 import 'package:quizlet_clone/data/flashcard_service.dart';
+import 'package:quizlet_clone/ui/pages/create_flashcard_page.dart';
 import 'package:quizlet_clone/ui/widgets/flashcard_list.dart';
 
 class FlashCardSetDetailPage extends StatefulWidget {
@@ -29,12 +33,26 @@ class _FlashCardSetDetailPageState extends State<FlashCardSetDetailPage> {
 
   @override
   Widget build(BuildContext context) => ChangeNotifierProvider(
-    create: (_) => _flashCardListBloc,
-    child: Scaffold(
+        create: (_) => _flashCardListBloc,
+        child: Scaffold(
           appBar: AppBar(
             title: const Text('Flashcard Detail Page'),
           ),
           body: FlashcardList(),
+          floatingActionButton: FloatingActionButton(
+            child: const Icon(Icons.add),
+            onPressed: () {
+              unawaited(Navigator.of(context).push(
+                MaterialPageRoute(
+                  builder: (context) => ChangeNotifierProvider(
+                    create: (_) => CreateFlashcardFormBloc(FlashCardService()),
+                    child: CreateFlashcardPage(
+                        flashCardSetId: widget.flashCardSetid),
+                  ),
+                ),
+              ));
+            },
+          ),
         ),
-  );
+      );
 }
diff --git a/lib/ui/pages/home_page.dart b/lib/ui/pages/home_page.dart
index 812dafe..604ef7a 100644
--- a/lib/ui/pages/home_page.dart
+++ b/lib/ui/pages/home_page.dart
@@ -8,7 +8,7 @@ import 'package:quizlet_clone/bloc/flash_card_set_list_bloc.dart';
 import 'package:quizlet_clone/data/flash_card_set_service.dart';
 import 'package:quizlet_clone/ui/constants/app_icons.dart';
 import 'package:quizlet_clone/ui/constants/app_texts.dart';
-import 'package:quizlet_clone/ui/pages/create_flash_card_page.dart';
+import 'package:quizlet_clone/ui/pages/create_flash_card_set_page.dart';
 import 'package:quizlet_clone/ui/router/app_router.dart';
 import 'package:quizlet_clone/ui/widgets/flash_card_set_list.dart';
 
@@ -70,7 +70,7 @@ class _HomePageState extends State<HomePage> {
                   builder: (context) => ChangeNotifierProvider(
                     create: (_) =>
                         CreateFlashCardSetFormBloc(FlashCardSetService()),
-                    child: CreateFlashCardPage(),
+                    child: CreateFlashCardSetPage(),
                   ),
                 ),
               ));
diff --git a/lib/ui/widgets/forms/flashcard_form.dart b/lib/ui/widgets/forms/flashcard_form.dart
new file mode 100644
index 0000000..06454fc
--- /dev/null
+++ b/lib/ui/widgets/forms/flashcard_form.dart
@@ -0,0 +1,54 @@
+import 'package:flutter/material.dart';
+import 'package:quizlet_clone/ui/widgets/forms/form_fields/answer_form.dart';
+import 'package:quizlet_clone/ui/widgets/forms/form_fields/question_form.dart';
+
+class FlashcardForm extends StatelessWidget {
+  const FlashcardForm(
+      {required this.formKey,
+      required this.questionController,
+      required this.questionValidator,
+      required this.answerController,
+      required this.answerValidator,
+      required this.onPressed,
+      this.isLoading = false,
+      required this.buttonLabel,
+      super.key});
+
+  final GlobalKey<FormState> formKey;
+
+  final TextEditingController questionController;
+
+  final FormFieldValidator<String>? questionValidator;
+
+  final TextEditingController answerController;
+
+  final FormFieldValidator<String>? answerValidator;
+
+  final String buttonLabel;
+
+  final VoidCallback? onPressed;
+
+  final bool isLoading;
+  @override
+  Widget build(BuildContext context) => Form(
+        key: formKey,
+        child: Column(
+          children: [
+            QuestionForm(
+              questionController: questionController,
+              validator: questionValidator,
+            ),
+            const SizedBox(height: 20),
+            AnswerForm(
+              answerController: answerController,
+              validator: answerValidator,
+            ),
+            const SizedBox(height: 20),
+            TextButton(
+              onPressed: onPressed,
+              child: Text(buttonLabel),
+            ),
+          ],
+        ),
+      );
+}
diff --git a/lib/ui/widgets/forms/form_fields/answer_form.dart b/lib/ui/widgets/forms/form_fields/answer_form.dart
new file mode 100644
index 0000000..fb86116
--- /dev/null
+++ b/lib/ui/widgets/forms/form_fields/answer_form.dart
@@ -0,0 +1,23 @@
+import 'package:flutter/material.dart';
+
+class AnswerForm extends StatelessWidget {
+  const AnswerForm({
+    required this.answerController,
+    required this.validator,
+    super.key,
+  });
+
+  final TextEditingController answerController;
+
+  final FormFieldValidator<String>? validator;
+
+  @override
+  Widget build(BuildContext context) => TextFormField(
+      controller: answerController,
+      decoration: const InputDecoration(labelText: 'Answer'),
+      validator: validator,
+      textInputAction: TextInputAction.next,
+      autovalidateMode: AutovalidateMode.onUserInteraction,
+      maxLength: 50,
+    );
+}
diff --git a/lib/ui/widgets/forms/form_fields/question_form.dart b/lib/ui/widgets/forms/form_fields/question_form.dart
new file mode 100644
index 0000000..e7963c6
--- /dev/null
+++ b/lib/ui/widgets/forms/form_fields/question_form.dart
@@ -0,0 +1,23 @@
+import 'package:flutter/material.dart';
+
+class QuestionForm extends StatelessWidget {
+  const QuestionForm({
+    required this.questionController,
+    required this.validator,
+    super.key,
+  });
+
+  final TextEditingController questionController;
+
+  final FormFieldValidator<String>? validator;
+
+  @override
+  Widget build(BuildContext context) => TextFormField(
+      controller: questionController,
+      decoration: const InputDecoration(labelText: 'Question'),
+      validator: validator,
+      textInputAction: TextInputAction.next,
+      autovalidateMode: AutovalidateMode.onUserInteraction,
+      maxLength: 50,
+    );
+}


Reviews:
user: login: PTLam-Thryve
body: @Megadebt,

Great job!

I really like the PR description and video preview. It is very clear what changes are in the PR.

I've added some request changes. Please take a look when you have the opportunity.
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-03-10T09:36:23Z
user: login: Megadebt
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T12:34:03Z
user: login: PTLam-Thryve
body: @Megadebt,

Great job!🔥
state: APPROVED
author_association: OWNER
submitted_at: 2025-03-12T09:37:16Z

Review comments:
path: .qodo/history.sqlite
user: login: PTLam-Thryve
body: @Megadebt,  

Please remove the `.qodo` file from the project's Git history.  

### **Why This Is Needed**  
- The `.qodo` file is related to personal developer tools and does not belong to the project.  
- Other developers working on the project don’t need it, as they may use different AI coding assistants or tools.  

Thank you!
created_at: 2025-03-10T09:16:52Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,36 @@
+import 'package:quizlet_clone/data/flashcard_service.dart';
+import 'package:quizlet_clone/models/flashcard.dart';
+import 'package:quizlet_clone/ui/constants/app_texts.dart';
+
+sealed class CreateFlashcardFormBlocState {
+  bool get isSuccessful => this is CreateSuccessState;
+
+  bool get isLoading => this is CreateLoadingState;
+}
+
+class CreateInitialState extends CreateFlashcardFormBlocState {}
path: lib/bloc/create_bloc/create_flashcard_form_bloc_state.dart
user: login: PTLam-Thryve
body: @Megadebt,  

Please rename `CreateInitialState` to `CreateFlashcardFormInitialState` and please do the same for other states.

### **Why This Is Needed**  
- **Improved Readability**: The name should clearly indicate what it represents, so we don’t need to check its usage to understand its purpose. For example, `CreateInitialState` is too general, and we have to check where it's used to know what it actually refers to.  
- **Avoid Naming Conflicts**: We may have multiple BLoCs responsible for different "create" actions. A more specific name like `CreateFlashcardFormInitialState` ensures clarity and prevents conflicts in future implementations.  

Thank you!
created_at: 2025-03-10T09:23:52Z
author_association: OWNER
diff_hunk: @@ -1,25 +1,62 @@
 import 'package:cloud_firestore/cloud_firestore.dart';
 import 'package:quizlet_clone/models/flashcard.dart';
 
-class FlashCardService{
+class FlashCardService {
   final FirebaseFirestore _fireStore = FirebaseFirestore.instance;
 
-  Future<List<Flashcard>> getFlashcards(final String flashCardSetid) async{
-    try{
-      final getFromCollection = await _fireStore.collection('flashcard-sets').doc(flashCardSetid).collection('flashcards').get();
-      final flashcards = getFromCollection.docs.map((doc){
-        final question = doc.data().containsKey('question')?doc['question'].toString():'';
-        final answer = doc.data().containsKey('answer')?doc['answer'].toString():'';
+  Future<List<Flashcard>> getFlashcards(final String flashCardSetid) async {
+    try {
+      final getFromCollection = await _fireStore
+          .collection('flashcard-sets')
+          .doc(flashCardSetid)
+          .collection('flashcards')
+          .get();
+      final flashcards = getFromCollection.docs.map((doc) {
+        final question = doc.data().containsKey('question')
+            ? doc['question'].toString()
+            : '';
+        final answer =
+            doc.data().containsKey('answer') ? doc['answer'].toString() : '';
         return Flashcard(
           answer: answer,
           question: question,
-          id: doc.id,
+          id: doc.id
         );
       }).toList();
       return flashcards;
-    } on FirebaseException catch(error){
+    } on FirebaseException catch (error) {
       throw FlashCardServiceException.fromFirebaseException(error);
-    } catch(error){
+    } catch (error) {
+      rethrow;
+    }
+  }
+
+  Future<Flashcard> createFlashcard(
+      {required String question,
+      required String answer,
+      required String flashCardSetId}) async {
+    try {
+      final addedFlashcard = await _fireStore
+          .collection('flashcard-sets')
+          .doc(flashCardSetId)
+          .collection('flashcards')
+          .add({
+        'question': question,
+        'answer': answer,
+      });
+      final retrievedFlashCard = await addedFlashcard.get();
+      if (!retrievedFlashCard.exists) {
+        throw GenericFirestoreException();
+      } else {
+        return Flashcard(
+          id: retrievedFlashCard.id,
+          question: question,
+          answer: answer,
+        );
+      }
path: lib/data/flashcard_service.dart
user: login: PTLam-Thryve
body: @Megadebt,  

Please remove the logic for retrieving the newly created flashcard from Firestore and instead return the new flashcard directly:  

```dart
return Flashcard(
        id: addedFlashcard.id,
        question: question,
        answer: answer,
      );
```

### **Why This Is Needed**  
- When adding a new document to Firestore, an exception will be thrown if something goes wrong. If the operation succeeds, Firestore already returns the `ID` of the newly created document.  
- Since Firestore guarantees that a successful creation returns an `ID`, we don't need additional logic to fetch and verify the document after creation.  

Thank you!
created_at: 2025-03-10T09:31:14Z
author_association: OWNER
diff_hunk: @@ -41,6 +78,6 @@ sealed class FlashCardServiceException implements Exception {
 
 class InsufficientPermissionException extends FlashCardServiceException {}
 
-class InternalErrorException extends FlashCardServiceException{}
+class InternalErrorException extends FlashCardServiceException {}
path: lib/data/flashcard_service.dart
user: login: PTLam-Thryve
body: @Megadebt,

Please rename `InternalErrorException` to `Internal FlashCardServiceException ` and please do the same for other states. The same reason for the change as for `CreateFlashcardFormBlocState`.

> Just additional info: In the future, we will refactor our code, so we can reuse the same exception classes in different services.
created_at: 2025-03-10T09:33:13Z
author_association: OWNER
path: .qodo/history.sqlite
user: login: Megadebt
body: @PTLam-Thryve This along with other problems you've commented on have been solved, please give it a glance when you have the chance! Have a great day ahead!
created_at: 2025-03-10T12:34:03Z
author_association: COLLABORATOR

PR comments:
user: login: Megadebt
created_at: 2025-03-06T19:48:41Z
body: @CodiumAI-Agent /review

author_association: COLLABORATOR
user: login: QodoAI-Agent
created_at: 2025-03-06T19:49:32Z
body: ## PR Reviewer Guide 🔍

#### (Review updated until commit https://github.com/PTLam-Thryve/quizlet_clone/commit/49fcc5c846e9baddf5a519322d82c0e01c2515a3)


Here are some key observations to aid the review process:

<table>
<tr><td>⏱️&nbsp;<strong>Estimated effort to review</strong>: 2 🔵🔵⚪⚪⚪</td></tr>
<tr><td>🧪&nbsp;<strong>No relevant tests</strong></td></tr>
<tr><td>🔒&nbsp;<strong>No security concerns identified</strong></td></tr>
<tr><td>⚡&nbsp;<strong>Recommended focus areas for review</strong><br><br>

<details><summary><a href='https://github.com/PTLam-Thryve/quizlet_clone/pull/24/files#diff-7792e72954154d35a55b53a2744d3e2f9cdf0975a6686a307829c0516f388f06R48-R59'><strong>Navigation Logic</strong></a>

The code uses both Navigator.pop() and Navigator.pushReplacement() in sequence. The pop() call is unnecessary since pushReplacement() already replaces the current route.
</summary>

```dart
    Navigator.of(context).pop();
unawaited(
  //Navigator.of(context).pop() only returns to the old version of the page instead of
  //the new version with newly created flashcard
  Navigator.of(context).pushReplacement(
    MaterialPageRoute(
      builder: (_) => FlashCardSetDetailPage(
        flashCardSetid: widget.flashCardSetId,
      ),
    ),
  ),
);
```

</details>

<details><summary><a href='https://github.com/PTLam-Thryve/quizlet_clone/pull/24/files#diff-24350442dd1d093581953f485fd4c1c4014856d3bacd15f8a41aca9ed923373dR48-R56'><strong>Error Handling</strong></a>

The createFlashcard method checks if retrievedFlashCard exists but doesn't handle the case where data might be missing or malformed in the retrieved document.
</summary>

```dart
if (!retrievedFlashCard.exists) {
  throw GenericFirestoreException();
} else {
  return Flashcard(
    id: retrievedFlashCard.id,
    question: question,
    answer: answer,
  );
}
```

</details>

</td></tr>
</table>

author_association: NONE
user: login: Megadebt
created_at: 2025-03-06T19:51:11Z
body: @CodiumAI-Agent /improve

author_association: COLLABORATOR
user: login: QodoAI-Agent
created_at: 2025-03-06T19:52:28Z
body: ## PR Code Suggestions ✨
<!-- 49fcc5c -->

Latest suggestions up to 49fcc5c
Explore these optional code suggestions:

<table><thead><tr><td><strong>Category</strong></td><td align=left><strong>Suggestion&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </strong></td><td align=center><strong>Impact</strong></td></tr><tbody><tr><td rowspan=2>Possible issue</td>
<td>



<details><summary>Handle errors properly<!-- not_implemented --></summary>

___


**Instead of throwing the exception, set the state to <code>CreateErrorState</code> to properly <br>handle errors in the UI. Currently, errors are thrown but not caught, which will <br>crash the app.**

[lib/bloc/create_bloc/create_flashcard_form_bloc.dart [21-32]](https://github.com/PTLam-Thryve/quizlet_clone/pull/24/files#diff-269bd76aeabdad16106c28d6a6cc5175eee86c87c64d94938ea8c196aa45a44eR21-R32)

```diff
 try {
   final flashCardAdded = await _flashCardService.createFlashcard(
     question: question,
     answer: answer,
     flashCardSetId: flashCardSetId,
   );
   _state = CreateSuccessState(flashCardAdded);
 } on FirebaseException catch (error) {
-  throw FlashCardServiceException.fromFirebaseException(error);
+  _state = CreateErrorState(FlashCardServiceException.fromFirebaseException(error));
 } finally{
   notifyListeners();
 }
```

- [ ] **Apply this suggestion** <!-- /improve --apply_suggestion=0 -->

<details><summary>Suggestion importance[1-10]: 9</summary>

__

Why: This is a critical bug fix. Currently, exceptions are thrown but not caught, which would crash the app. The suggestion correctly modifies the error handling to update the state to CreateErrorState instead of throwing, allowing the UI to properly display error messages.


</details></details></td><td align=center>High

</td></tr><tr><td>



<details><summary>Fix navigation flow<!-- not_implemented --></summary>

___


**Remove the redundant <code>Navigator.of(context).pop()</code> call before the <br><code>pushReplacement</code>. The <code>pushReplacement</code> already replaces the current route, making <br>the pop unnecessary and potentially causing navigation issues.**

[lib/ui/pages/create_flashcard_page.dart [42-59]](https://github.com/PTLam-Thryve/quizlet_clone/pull/24/files#diff-7792e72954154d35a55b53a2744d3e2f9cdf0975a6686a307829c0516f388f06R42-R59)

```diff
 void createStatusListener() {
   switch (_createFlashcardFormBloc.state) {
     case CreateSuccessState _:
       showAppSnackBar(context,
           message: AppTexts.createFlashcardSuccess,
           status: SnackBarStatus.success);
-          Navigator.of(context).pop();
       unawaited(
         //Navigator.of(context).pop() only returns to the old version of the page instead of
         //the new version with newly created flashcard
         Navigator.of(context).pushReplacement(
           MaterialPageRoute(
             builder: (_) => FlashCardSetDetailPage(
               flashCardSetid: widget.flashCardSetId,
             ),
           ),
         ),
       );
```

- [ ] **Apply this suggestion** <!-- /improve --apply_suggestion=1 -->

<details><summary>Suggestion importance[1-10]: 8</summary>

__

Why: This suggestion fixes a significant navigation issue. The redundant pop() call before pushReplacement() could cause unexpected behavior or crashes. Removing it ensures proper navigation flow when a flashcard is successfully created.


</details></details></td><td align=center>Medium

</td></tr>
<tr><td align="center" colspan="2">

- [ ] Update <!-- /improve --more_suggestions=true -->

</td><td></td></tr></tbody></table>

___

#### Previous suggestions
<details><summary>Suggestions up to commit 313fbc0</summary>
<br><table><thead><tr><td><strong>Category</strong></td><td align=left><strong>Suggestion&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </strong></td><td align=center><strong>Impact</strong></td></tr><tbody><tr><td rowspan=2>Possible issue</td>
<td>



<details><summary>Use correct document id</summary>

___


**Use the newly created document's id instead of the flashcard set id when <br>constructing a Flashcard.**

[lib/data/flashcard_service.dart [58-62]](https://github.com/PTLam-Thryve/quizlet_clone/pull/24/files#diff-24350442dd1d093581953f485fd4c1c4014856d3bacd15f8a41aca9ed923373dR58-R62)

```diff
 return Flashcard(
-  id: flashCardSetId,
+  id: retrievedFlashCard.id,
   question: question,
   answer: answer,
 );
```
<details><summary>Suggestion importance[1-10]: 9</summary>

__

Why: The suggestion correctly identifies a bug where the flashcard's id is incorrectly set to the flashcard set id; using the retrieved document id ensures each flashcard has the proper unique identifier.


</details></details></td><td align=center>High

</td></tr><tr><td>



<details><summary>Handle errors gracefully<!-- not_implemented --></summary>

___


**Update error handling by setting an error state in the catch block instead of <br>throwing, so the UI reflects failure.**

[lib/bloc/create_bloc/create_flashcard_form_bloc.dart [21-32]](https://github.com/PTLam-Thryve/quizlet_clone/pull/24/files#diff-269bd76aeabdad16106c28d6a6cc5175eee86c87c64d94938ea8c196aa45a44eR21-R32)

```diff
 try {
   final flashCardAdded = await _flashCardService.createFlashcard(...);
   _state = CreateSuccessState(flashCardAdded);
 } on FirebaseException catch (error) {
-  throw FlashCardServiceException.fromFirebaseException(error);
+  _state = CreateErrorState(FlashCardServiceException.fromFirebaseException(error));
 } finally{
   notifyListeners();
 }
```
<details><summary>Suggestion importance[1-10]: 8</summary>

__

Why: Changing the error handling to update the bloc state instead of throwing an exception improves the UI’s ability to reflect error conditions, thereby enhancing overall user experience.


</details></details></td><td align=center>Medium

</td></tr></tr></tbody></table>

</details>
author_association: NONE
user: login: Megadebt
created_at: 2025-03-06T20:29:29Z
body: @CodiumAI-Agent /review

author_association: COLLABORATOR
user: login: Megadebt
created_at: 2025-03-06T20:38:35Z
body: @PTLam-Thryve I believe I've completed the task, please take a look my code when you can!
author_association: COLLABORATOR
user: login: QodoAI-Agent
created_at: 2025-03-06T20:39:17Z
body: **[Persistent review](https://github.com/PTLam-Thryve/quizlet_clone/pull/24#issuecomment-2704797748)** updated to latest commit https://github.com/PTLam-Thryve/quizlet_clone/commit/49fcc5c846e9baddf5a519322d82c0e01c2515a3
author_association: NONE