From 1cb79709822c7e87f6482bc716b34c2570d9e4fd Mon Sep 17 00:00:00 2001 From: Rafael <1024481@stud.hs-mannheim.de> Date: Fri, 17 May 2024 23:08:26 +0200 Subject: [PATCH] First step of registration process --- lib/components/my_drawer.dart | 2 +- lib/enumerations.dart | 7 + lib/forms/profile_category_form.dart | 12 +- lib/forms/skills_form.dart | 83 +++-- lib/pages/user_data_page.dart | 502 +++++++++++++++------------ lib/services/auth/auth_gate.dart | 55 ++- 6 files changed, 398 insertions(+), 263 deletions(-) diff --git a/lib/components/my_drawer.dart b/lib/components/my_drawer.dart index c90c629..c3be1d6 100644 --- a/lib/components/my_drawer.dart +++ b/lib/components/my_drawer.dart @@ -104,7 +104,7 @@ class MyDrawer extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => const UserDataPage(), + builder: (context) => const UserDataPage(isRegProcess: false,), )); }, ), diff --git a/lib/enumerations.dart b/lib/enumerations.dart index 8f90f8e..7345fd3 100644 --- a/lib/enumerations.dart +++ b/lib/enumerations.dart @@ -1,3 +1,10 @@ +enum Gender { + none, + male, + female, + divers, +} + enum SkillOption { product, finance, diff --git a/lib/forms/profile_category_form.dart b/lib/forms/profile_category_form.dart index ff5b247..19d5bc6 100644 --- a/lib/forms/profile_category_form.dart +++ b/lib/forms/profile_category_form.dart @@ -3,6 +3,7 @@ import '../../helper.dart'; class ProfileCategoryForm extends StatefulWidget { final String title; + final String header; final String description; final List options; // T to make it work with our different Enums final int minSelections; @@ -15,6 +16,7 @@ class ProfileCategoryForm extends StatefulWidget { const ProfileCategoryForm({ super.key, required this.title, + required this.header, required this.description, required this.options, required this.minSelections, @@ -51,17 +53,15 @@ class ProfileCategoryFormState extends State> { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( - '${widget.title}', + widget.header, style: const TextStyle( fontSize: 18.0, fontWeight: FontWeight.bold, ), ), - const SizedBox(height: 16,), - Text( - '${widget.description}', - ), - const SizedBox(height: 16,), + const SizedBox(height: 16), + Text(widget.description), + const SizedBox(height: 16), Wrap( spacing: 8.0, children: List.generate(widget.options.length, (index) { diff --git a/lib/forms/skills_form.dart b/lib/forms/skills_form.dart index 77c4b7d..dfb2bb4 100644 --- a/lib/forms/skills_form.dart +++ b/lib/forms/skills_form.dart @@ -6,12 +6,17 @@ import '../../services/auth/auth_service.dart'; import 'profile_category_form.dart'; class SkillsForm extends StatelessWidget { - SkillsForm({super.key, required this.skillsSought}); + SkillsForm({ + super.key, + required this.isRegProcess, + required this.skillsSought, + }); // get instance of firestore and auth final FirebaseFirestore _firestore = FirebaseFirestore.instance; final AuthService _authService = AuthService(); + final bool isRegProcess; final bool skillsSought; // flag to toggle offered and sought skills @override @@ -28,18 +33,42 @@ class SkillsForm extends StatelessWidget { List? userSkills = snapshot.data; return ProfileCategoryForm( title: 'Skills', + header: + skillsSought ? 'Skills you are looking for' : 'Your own skills', description: skillsSought - ? 'Choose up to 3 areas you are looking for in a cofounder' + ? 'Choose up to 3 areas you are looking for in a co-founder' : 'Select up to 3 areas in which you are skilled', + saveButtonText: isRegProcess ? 'Save and continue' : 'Save', options: SkillOption.values.toList(), // Convert enum values to list minSelections: 1, maxSelections: 3, preSelectedOptions: userSkills ?? [], // Pass pre-selected skills to the form - onSave: (selectedOptions) { + onSave: (selectedOptions) async { // Handle saving selected options - saveSkillsToFirebase(selectedOptions.cast()); + bool success = await saveSkillsToFirebase( + selectedOptions.cast()); + // Then navigate to another screen or perform any other action??? + if (context.mounted) { + if (success) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SkillsForm( + isRegProcess: isRegProcess, + skillsSought: true, + ), + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Failed to save user skills.'), + ), + ); + } + } }, ); } @@ -80,28 +109,34 @@ class SkillsForm extends StatelessWidget { return []; } - void saveSkillsToFirebase(List selectedOptions) { - String currentUserId = _authService.getCurrentUser()!.uid; + Future saveSkillsToFirebase(List selectedOptions) async { + try { + String currentUserId = _authService.getCurrentUser()!.uid; - // Convert enum values to strings, removing leading EnumType with split - List skills = selectedOptions - .map((option) => option.toString().split('.').last) - .toList(); + // Convert enum values to strings, removing leading EnumType with split + List skills = selectedOptions + .map((option) => option.toString().split('.').last) + .toList(); - // Update the corresponding 'skills' field in the user's document - String keyToUpdate = skillsSought - ? Constants.dbFieldUsersSkillsSought - : Constants.dbFieldUsersSkills; + // Update the corresponding 'skills' field in the user's document + String keyToUpdate = skillsSought + ? Constants.dbFieldUsersSkillsSought + : Constants.dbFieldUsersSkills; - _firestore - .collection(Constants.dbCollectionUsers) - .doc(currentUserId) - .update({ - keyToUpdate: skills, - }).then((_) { - print('$keyToUpdate saved to Firebase: $skills'); - }).catchError((error) { - print('Failed to save $keyToUpdate: $error'); - }); + _firestore + .collection(Constants.dbCollectionUsers) + .doc(currentUserId) + .update({ + keyToUpdate: skills, + }).then((_) { + print('$keyToUpdate saved to Firebase: $skills'); + }).catchError((error) { + print('Failed to save $keyToUpdate: $error'); + }); + + return true; + } catch (e) { + return false; + } } } diff --git a/lib/pages/user_data_page.dart b/lib/pages/user_data_page.dart index 84d5f12..0bd516a 100644 --- a/lib/pages/user_data_page.dart +++ b/lib/pages/user_data_page.dart @@ -1,25 +1,28 @@ import 'dart:convert'; import 'package:cloud_firestore/cloud_firestore.dart'; -import '../components/my_button.dart'; -import '../constants.dart'; -import '../models/language.dart'; -import '../models/language_setting.dart'; -import '../components/location_dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import '../services/auth/auth_service.dart'; +import '../components/location_dialog.dart'; +import '../components/my_button.dart'; +import '../components/my_drawer.dart'; +import '../constants.dart'; +import '../enumerations.dart'; +import '../forms/skills_form.dart'; +import '../models/language.dart'; +import '../models/language_setting.dart'; import '../models/location.dart'; +import '../services/auth/auth_service.dart'; class UserDataPage extends StatefulWidget { - const UserDataPage({super.key}); + final bool isRegProcess; + + const UserDataPage({super.key, required this.isRegProcess}); @override State createState() => _UserDataPageState(); } -enum Gender { none, male, female, divers } - class _UserDataPageState extends State { MyLocation? _mainLocation; MyLocation? _secondaryLocation; @@ -129,7 +132,7 @@ class _UserDataPageState extends State { // Load data from JSON file when the widget initializes loadLanguagesFromJson(); } catch (error) { - print("Error fetching settings: $error"); + _showSnackBar("Error fetching settings: $error"); } } @@ -164,103 +167,99 @@ class _UserDataPageState extends State { }); } - void saveUserData(BuildContext context) async { - // Get userID from auth service - String currentUserID = _authService.getCurrentUser()!.uid; + Future saveUserData(BuildContext context) async { + try { + // Get userID from auth service + String currentUserID = _authService.getCurrentUser()!.uid; - // Get references to the current users Firebase collections - DocumentReference userRef = - _firestore.collection(Constants.dbCollectionUsers).doc(currentUserID); - CollectionReference languagesRef = - userRef.collection(Constants.dbCollectionLanguages); - CollectionReference locationsRef = - userRef.collection(Constants.dbCollectionLocations); + // Get references to the current users Firebase collections + DocumentReference userRef = + _firestore.collection(Constants.dbCollectionUsers).doc(currentUserID); + CollectionReference languagesRef = + userRef.collection(Constants.dbCollectionLanguages); + CollectionReference locationsRef = + userRef.collection(Constants.dbCollectionLocations); - if (_selectedYear != _yearFromDb) { - await userRef.update( - {'born': _selectedYear}, - ); - // update local value - _yearFromDb = _selectedYear; - } else { - print("birth year did NOT change"); - } - - // Update Gender in database - only if value has changed - if (_genderFromDb != genderView.index) { - await userRef.update( - {'gender': genderView.index}, - ); - // update local value - _genderFromDb = genderView.index; - } else { - print("gender did NOT change"); - } - - // Save locations - if (_mainLocation != _mainLocationFromDb) { - if (_mainLocation != null) { - // Update existing user document with new locations - await locationsRef - .doc(Constants.dbDocMainLocation) - .set(_mainLocation!.toMap()); + if (_selectedYear != _yearFromDb) { + await userRef.update( + {'born': _selectedYear}, + ); + // update local value + _yearFromDb = _selectedYear; } - } else { - print("main location did NOT change"); - } - if (_secondaryLocation != _secondaryLocationFromDb) { - if (_secondaryLocation != null) { - await locationsRef - .doc(Constants.dbDocSecondLocation) - .set(_secondaryLocation!.toMap()) - .then((value) => { - print("Document secondary location updated"), + // Update Gender in database - only if value has changed + if (_genderFromDb != genderView.index) { + await userRef.update( + {'gender': genderView.index}, + ); + // update local value + _genderFromDb = genderView.index; + } + + // Save locations + if (_mainLocation != _mainLocationFromDb) { + if (_mainLocation != null) { + // Update existing user document with new locations + await locationsRef + .doc(Constants.dbDocMainLocation) + .set(_mainLocation!.toMap()); + } + } + + if (_secondaryLocation != _secondaryLocationFromDb) { + if (_secondaryLocation != null) { + await locationsRef + .doc(Constants.dbDocSecondLocation) + .set(_secondaryLocation!.toMap()) + .then((value) => { + // update local values + _secondaryLocationFromDb = _secondaryLocation, + _secondLocationExists = true + }); + } else if (_secondLocationExists) { + // secondLocationExists but is null here -> delete secondary in DB + await locationsRef.doc(Constants.dbDocSecondLocation).delete().then( + (doc) => { // update local values - _secondaryLocationFromDb = _secondaryLocation, - _secondLocationExists = true - }); - } else if (_secondLocationExists) { - // secondLocationExists but is null here -> delete secondary in DB - await locationsRef.doc(Constants.dbDocSecondLocation).delete().then( - (doc) => { - print("Document secondary location deleted"), - // update local values - _secondaryLocationFromDb = null, - _secondLocationExists = false - }, - onError: (e) => print("Error updating document $e"), - ); + _secondaryLocationFromDb = null, + _secondLocationExists = false + }, + onError: (e) => _showSnackBar("Error updating document: $e"), + ); + } } - } else { - print("secondary location did NOT change"); - } - // Save Languages - only if selected values changed - if (_selectedLanguages.isEmpty) { - print("no language selected"); - } else if (_languagesFromDb.length != _selectedLanguages.length || - _selectedLanguages - .any((element) => !_languagesFromDb.contains(element))) { - // Loop through each Language object and save it to the collection - for (int i = 0; i < _selectedLanguages.length; i++) { - // Convert Language object to a map and add the map to the collection, - // using .doc(myID).set() instead of .add() to avoid duplicates. - await languagesRef - .doc(_selectedLanguages[i].code) - .set(_selectedLanguages[i].toMap()); + // Save Languages - only if selected values changed + if (_selectedLanguages.isEmpty) { + _showSnackBar("No language selected"); + return false; + } else if (_languagesFromDb.length != _selectedLanguages.length || + _selectedLanguages + .any((element) => !_languagesFromDb.contains(element))) { + // Loop through each Language object and save it to the collection + for (int i = 0; i < _selectedLanguages.length; i++) { + // Convert Language object to a map and add the map to the collection, + // using .doc(myID).set() instead of .add() to avoid duplicates. + await languagesRef + .doc(_selectedLanguages[i].code) + .set(_selectedLanguages[i].toMap()); + } + // update local variable (only if selection is not empty) + _languagesFromDb.clear(); + _languagesFromDb.addAll(_selectedLanguages); + + // List to store language codes from the provided list + List languageCodes = + _selectedLanguages.map((language) => language.code).toList(); + // Clean up languages that were not part of the provided list + await deleteUnusedDocuments(languagesRef, languageCodes); } - // update local variable (only if selection is not empty) - _languagesFromDb.clear(); - _languagesFromDb.addAll(_selectedLanguages); - // List to store language codes from the provided list - List languageCodes = - _selectedLanguages.map((language) => language.code).toList(); - // Clean up languages that were not part of the provided list - await deleteUnusedDocuments(languagesRef, languageCodes); - } else { - print("languages did NOT change"); + return true; + } catch (e) { + _showSnackBar(e.toString()); + return false; } } @@ -326,141 +325,185 @@ class _UserDataPageState extends State { title: const Text("User Data"), centerTitle: true, ), - body: ListView( - children: [ - const Text( - 'Location', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 20), - // Display selected main location - const Text( - 'Main Location:', - style: TextStyle(fontWeight: FontWeight.bold), - ), - if (_mainLocation != null) ...[ - Text(_mainLocation!.toString()), + drawer: const MyDrawer(), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: ListView( + children: [ + if (widget.isRegProcess) ...[ + const SizedBox(height: 10), + const Text( + 'Please fill in the following fields to proceed with the registration process.', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 20), + const Divider(), + ], const SizedBox(height: 10), - ] else ...[ - const Text('n/a'), - const SizedBox(height: 10), - ], - // Button to set main location - Center( - child: ElevatedButton( - onPressed: () { - _showLocationDialog(true); - }, - child: Text(_mainLocation != null - ? 'Change Main Location' - : 'Set Main Location'), - ), - ), - const SizedBox(height: 20), - // Display selected secondary location - if (_secondaryLocation != null) ...[ const Text( - 'Secondary Location:', + 'Location', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 20), + // Display selected main location + const Text( + 'Main Location:', style: TextStyle(fontWeight: FontWeight.bold), ), - Text(_secondaryLocation!.toString()), - const SizedBox(height: 10), - ], - if (_mainLocation != null) ...[ - // Button to set secondary location - ElevatedButton( - onPressed: () { - _showLocationDialog(false); - }, - child: Text(_secondaryLocation != null - ? 'Change Secondary Location' - : 'Add Secondary Location'), - ), - ], - // Display selected secondary location or remove button - if (_secondaryLocation != null) ...[ - ElevatedButton( - onPressed: _removeSecondaryLocation, - child: const Text('Remove Secondary Location'), - ), - ], - const SizedBox(height: 20), - const Text( - 'Age', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - Row( - children: [ - const Padding(padding: EdgeInsets.symmetric(horizontal: 8)), - Text(_selectedYear != null - ? '${DateTime.now().year - (_selectedYear ?? 0)} years old' - : 'undefined'), - const SizedBox(width: 20), - DropdownMenu( - initialSelection: _selectedYear, - onSelected: (int? newValue) { - setState(() { - _selectedYear = newValue; - }); + if (_mainLocation != null) ...[ + Text(_mainLocation!.toString()), + const SizedBox(height: 10), + ] else ...[ + const Text('n/a'), + const SizedBox(height: 10), + ], + // Button to set main location + Center( + child: ElevatedButton( + onPressed: () { + _showLocationDialog(true); }, - dropdownMenuEntries: List.generate(50, (index) { - return DropdownMenuEntry( - value: DateTime.now().year - 16 - index, - label: '${DateTime.now().year - 16 - index}', - ); - }), - label: const Text('birth year'), + child: Text(_mainLocation != null + ? 'Change Main Location' + : 'Set Main Location'), + ), + ), + const SizedBox(height: 20), + // Display selected secondary location + if (_secondaryLocation != null) ...[ + const Text( + 'Secondary Location:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Text(_secondaryLocation!.toString()), + const SizedBox(height: 10), + ], + if (_mainLocation != null) ...[ + // Button to set secondary location + Center( + child: ElevatedButton( + onPressed: () { + _showLocationDialog(false); + }, + child: Text(_secondaryLocation != null + ? 'Change Secondary Location' + : 'Add Secondary Location'), + ), ), ], - ), - const SizedBox(height: 20), - Text( - 'Gender (${genderView.name} selected)', - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - Center( - child: SegmentedButton( - style: SegmentedButton.styleFrom( - selectedBackgroundColor: Colors.blue, + // Display selected secondary location or remove button + if (_secondaryLocation != null) ...[ + Center( + child: ElevatedButton( + onPressed: _removeSecondaryLocation, + child: const Text('Remove Secondary Location'), + ), ), - segments: const >[ - ButtonSegment( - value: Gender.none, - label: Text('none'), - ), - ButtonSegment( - value: Gender.male, - label: Text('male'), - //icon: Icon(Icons.male_sharp), - ), - ButtonSegment( - value: Gender.female, - label: Text('female'), - //icon: Icon(Icons.female_sharp), - ), - ButtonSegment( - value: Gender.divers, - label: Text('divers'), + ], + const SizedBox(height: 20), + const Text( + 'Age', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + Row( + children: [ + const Padding(padding: EdgeInsets.symmetric(horizontal: 8)), + Text(_selectedYear != null + ? '${DateTime.now().year - (_selectedYear ?? 0)} years old' + : 'undefined age'), + const SizedBox(width: 20), + DropdownMenu( + initialSelection: _selectedYear, + onSelected: (int? newValue) { + setState(() { + _selectedYear = newValue; + }); + }, + dropdownMenuEntries: List.generate(50, (index) { + return DropdownMenuEntry( + value: DateTime.now().year - 16 - index, + label: '${DateTime.now().year - 16 - index}', + ); + }), + label: const Text('birth year'), ), ], - selected: {genderView}, - showSelectedIcon: false, - onSelectionChanged: updateSelectedGender, ), - ), - const SizedBox(height: 20), - const Divider(), - Text( - 'Language: (${_selectedLanguages.length} selected)', - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - ...languagesList.map(buildSingleCheckbox), // ... spread operator - const Divider(), - Padding( - padding: const EdgeInsets.all(8.0), - child: MyButton(text: "Save", onTap: () => saveUserData(context)), - ) - ], + const SizedBox(height: 20), + Text( + 'Gender (${genderView.name} selected)', + style: + const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + Center( + child: SegmentedButton( + style: SegmentedButton.styleFrom( + selectedBackgroundColor: Colors.blue, + ), + segments: const >[ + ButtonSegment( + value: Gender.none, + label: Text('none'), + ), + ButtonSegment( + value: Gender.male, + label: Text('male'), + //icon: Icon(Icons.male_sharp), + ), + ButtonSegment( + value: Gender.female, + label: Text('female'), + //icon: Icon(Icons.female_sharp), + ), + ButtonSegment( + value: Gender.divers, + label: Text('divers'), + ), + ], + selected: {genderView}, + showSelectedIcon: false, + onSelectionChanged: updateSelectedGender, + ), + ), + const SizedBox(height: 20), + const Divider(), + Text( + 'Language: (${_selectedLanguages.length} selected)', + style: + const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ...languagesList.map(buildSingleCheckbox), // ... spread operator + const Divider(), + Padding( + padding: const EdgeInsets.all(8.0), + child: MyButton( + text: widget.isRegProcess ? 'Save and continue' : "Save", + onTap: () async { + bool success = await saveUserData(context); + if (context.mounted) { + if (success) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SkillsForm( + isRegProcess: widget.isRegProcess, + skillsSought: false, + ), + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Failed to save user data.'), + ), + ); + } + } + }, + ), + ), + ], + ), ), ); } @@ -508,4 +551,19 @@ class _UserDataPageState extends State { onChanged: (value) => onClicked(), ), ); + + void _showSnackBar(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.red, + action: SnackBarAction( + label: 'Dismiss', + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + }, + ), + ), + ); + } } diff --git a/lib/services/auth/auth_gate.dart b/lib/services/auth/auth_gate.dart index e8595cd..9bfb920 100644 --- a/lib/services/auth/auth_gate.dart +++ b/lib/services/auth/auth_gate.dart @@ -1,9 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; -import 'package:cofounderella/services/auth/login_or_register.dart'; -import 'package:cofounderella/pages/home_page.dart'; +import 'auth_service.dart'; +import 'login_or_register.dart'; +import '../../constants.dart'; +import '../../pages/home_page.dart'; +import '../../pages/user_data_page.dart'; -class AuthGate extends StatelessWidget{ +class AuthGate extends StatelessWidget { const AuthGate({super.key}); @override @@ -11,17 +15,48 @@ class AuthGate extends StatelessWidget{ return Scaffold( body: StreamBuilder( stream: FirebaseAuth.instance.authStateChanges(), - builder: (context, snapshot){ - + builder: (context, snapshot) { // check if user is logged in or not - if(snapshot.hasData){ - return HomePage(); - } - else { + if (snapshot.hasData) { + return FutureBuilder( + // check database entries, if data is missing + // then complete registration process + // else go to HomePage + future: _checkCollectionsExist(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const CircularProgressIndicator(); + } else if (snapshot.hasData && snapshot.data == true) { + return HomePage(); + } else { + // also in case (snapshot.hasError) + return const UserDataPage(isRegProcess: true); + } + }, + ); + } else { return const LoginOrRegister(); } }, ), ); } -} \ No newline at end of file + + Future _checkCollectionsExist() async { + bool languagesExist = + await _checkUsersCollectionExists(Constants.dbCollectionLanguages); + bool locationsExist = + await _checkUsersCollectionExists(Constants.dbCollectionLocations); + return languagesExist && locationsExist; + } + + Future _checkUsersCollectionExists(String collectionName) async { + String currentUserId = AuthService().getCurrentUser()!.uid; + final collection = FirebaseFirestore.instance + .collection(Constants.dbCollectionUsers) + .doc(currentUserId) + .collection(collectionName); + final snapshot = await collection.limit(1).get(); + return snapshot.docs.isNotEmpty; + } +}