import 'dart:convert'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cofounderella/components/my_button.dart'; import 'package:cofounderella/constants.dart'; import 'package:cofounderella/models/language.dart'; import 'package:cofounderella/models/language_setting.dart'; import 'package:cofounderella/components/location_selector.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:cofounderella/services/auth/auth_service.dart'; import 'package:cofounderella/models/location.dart'; class UserDataPage extends StatefulWidget { const UserDataPage({super.key}); @override State createState() => _UserDataPageState(); } enum Gender { none, male, female, divers } class _UserDataPageState extends State { MyLocation? _selectedLocation; List languagesList = []; final List _selectedLanguages = []; // get instance of firestore and auth final FirebaseFirestore _firestore = FirebaseFirestore.instance; final AuthService _authService = AuthService(); int? _selectedYear; int? _yearFromDb; Gender genderView = Gender.none; int _genderFromDb = 0; List _languagesFromDb = []; @override void initState() { super.initState(); // load settings from database _fetchSettings(); } Future _fetchSettings() async { try { // Fetch user ID String currentUserId = _authService.getCurrentUser()!.uid; // Fetch user document fields (email, uid, gender, ...) from database DocumentSnapshot userSnapshot = await _firestore .collection(Constants.dbCollectionUsers) .doc(currentUserId) .get(); // Extract gender and birth year _genderFromDb = userSnapshot[Constants.dbFieldUsersGender]; _yearFromDb = userSnapshot .data() .toString() .contains(Constants.dbFieldUsersYearBorn) ? userSnapshot[Constants.dbFieldUsersYearBorn] : null; // Fetch languages QuerySnapshot languagesSnapshot = await _firestore .collection(Constants.dbCollectionUsers) .doc(currentUserId) .collection(Constants.dbCollectionLanguages) .get(); List userLanguages = []; for (var doc in languagesSnapshot.docs) { //languages.add(Language.fromDocument(doc)); userLanguages.add(Language( code: doc.id, // aka doc['code'], name: doc['name'], nativeName: doc['nativeName'], iconFile: doc['iconFile'], )); } setState(() { genderView = Gender.values[_genderFromDb]; _selectedYear = _yearFromDb; _languagesFromDb = userLanguages; }); // Load data from JSON file when the widget initializes loadLanguagesFromJson(); } catch (error) { print("Error fetching settings: $error"); } } /// Loads the languages to display Future loadLanguagesFromJson() async { // Load JSON data from assets String jsonData = await rootBundle.loadString(Constants.pathLanguagesJson); // Parse JSON into a list of objects List jsonList = await json.decode(jsonData); // Create LanguageSetting objects from JSON data languagesList = jsonList.map((item) { Language language = Language( code: item['code'], name: item['name'], nativeName: item['nativeName'], iconFile: item['iconFile'], ); return LanguageSetting( language: language, isSelected: _languagesFromDb.any( (language) => language.code == item['code'], ), ); }).toList(); // Update the UI after loading data setState(() { if (_selectedLanguages.isEmpty) { _selectedLanguages.addAll(_languagesFromDb); } }); } void saveUserData(BuildContext context) async { // 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); 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 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()); } // 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"); } } /// Deletes documents from collection that are not part of the provided list Future deleteUnusedDocuments( CollectionReference refCollection, List idsToKeep) async { // Fetch the existing documents from the database QuerySnapshot snapshot = await refCollection.get(); // Loop through each document in the collection for (QueryDocumentSnapshot doc in snapshot.docs) { String documentId = doc.id; // If the language code is not in the provided list, delete the document if (!idsToKeep.contains(documentId)) { await doc.reference.delete(); } } } void updateSelectedGender(Set newSelection) { setState(() { // By default there is only a single segment that can be // selected at one time, so its value is always the first // item in the selected set. genderView = newSelection.first; }); } @override Widget build(BuildContext context) { if (languagesList.isEmpty) { return const Center(child: CircularProgressIndicator()); } else { return Scaffold( appBar: AppBar( title: const Text("User Data"), centerTitle: true, ), body: ListView( children: [ const Text( 'Location', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), Padding( padding: const EdgeInsets.all(16.0), child: LocationSelector( onLocationChanged: (location) { setState(() { _selectedLocation = location; }); }, ), ), // TODO Show and handle selected location if (_selectedLocation != null) ...[ Text(style: const TextStyle(backgroundColor: Colors.yellow), 'Selected Location: ${_selectedLocation!.toString()}'), Text(style: const TextStyle(backgroundColor: Colors.yellowAccent), 'Coordinates: ${_selectedLocation!.toStringDegree()}'), ], 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( 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'), initialSelection: _yearFromDb, ), ], ), Text( 'Gender (${genderView.name} selected)', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), 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 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)), ) ], ), ); } } Widget buildSingleCheckbox(LanguageSetting languageSetting) => buildCheckbox( languageSetting: languageSetting, onClicked: () { setState(() { final newValue = !languageSetting.isSelected; languageSetting.isSelected = newValue; if (languageSetting.isSelected && !_selectedLanguages.contains(languageSetting.language)) { _selectedLanguages.add(languageSetting.language); } else if (languageSetting.isSelected == false) { _selectedLanguages.removeWhere( (element) => element.code == languageSetting.language.code); } }); }, ); Widget buildCheckbox({ required LanguageSetting languageSetting, required VoidCallback onClicked, }) => ListTile( tileColor: Theme.of(context).colorScheme.secondary, onTap: onClicked, leading: SizedBox( width: 48, height: 32, child: SvgPicture.asset( languageSetting.language.iconFile, ), ), title: Text( languageSetting.language.nativeName, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), subtitle: Text(languageSetting.language.name), trailing: Checkbox( value: languageSetting.isSelected, onChanged: (value) => onClicked(), ), ); }