diff --git a/lib/forms/matched_screen.dart b/lib/forms/matched_screen.dart index 8f0cb4e..58946f1 100644 --- a/lib/forms/matched_screen.dart +++ b/lib/forms/matched_screen.dart @@ -1,19 +1,19 @@ import 'package:flutter/material.dart'; class MatchedScreen extends StatelessWidget { - final String user1Name; - final String user2Name; - final String user1ImageUrl; - final String user2ImageUrl; + final String currentUserName; + final String otherUserName; + final String currentUserImageUrl; + final String otherUserImageUrl; final VoidCallback onMessageButtonPressed; final VoidCallback onContinueButtonPressed; const MatchedScreen({ super.key, - required this.user1Name, - required this.user2Name, - required this.user1ImageUrl, - required this.user2ImageUrl, + required this.currentUserName, + required this.otherUserName, + required this.currentUserImageUrl, + required this.otherUserImageUrl, required this.onMessageButtonPressed, required this.onContinueButtonPressed, }); @@ -28,7 +28,7 @@ class MatchedScreen extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - 'You and $user2Name have liked each other!', + 'You and $otherUserName have liked each other!', style: const TextStyle(fontSize: 24), textAlign: TextAlign.center, ), @@ -36,27 +36,27 @@ class MatchedScreen extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - // TODO imageUrl cant be null or empty with NetworkImage + // imageUrl cant be null or empty with NetworkImage CircleAvatar( backgroundColor: Colors.blueGrey[300], - backgroundImage: (user1ImageUrl.isEmpty) + backgroundImage: (currentUserImageUrl.isEmpty) ? null - : NetworkImage(user1ImageUrl), + : NetworkImage(currentUserImageUrl), radius: 50, ), const SizedBox(width: 24), CircleAvatar( backgroundColor: Colors.blueGrey[300], - backgroundImage: (user2ImageUrl.isEmpty) + backgroundImage: (otherUserImageUrl.isEmpty) ? null - : NetworkImage(user2ImageUrl), + : NetworkImage(otherUserImageUrl), radius: 50, ), ], ), const SizedBox(height: 24), Text( - '$user1Name and $user2Name', + '$currentUserName and $otherUserName', style: const TextStyle(fontSize: 20), textAlign: TextAlign.center, ), diff --git a/lib/models/user_profile.dart b/lib/models/user_profile.dart index 8091caf..6579b87 100644 --- a/lib/models/user_profile.dart +++ b/lib/models/user_profile.dart @@ -8,9 +8,11 @@ class UserProfile { final String id; final String uid; final String email; - final String name; + String name; final String firstName; final String lastName; + String? profilePictureUrl; + String? bio; final String risk; final List skills; final List skillsSought; @@ -24,6 +26,8 @@ class UserProfile { required this.name, required this.firstName, required this.lastName, + this.profilePictureUrl, + this.bio, required this.risk, required this.skills, required this.skillsSought, @@ -45,6 +49,8 @@ class UserProfile { skillsSought: List.from(data[Constants.dbFieldUsersSkillsSought] ?? []), risk: data[Constants.dbFieldUsersRiskTolerance] ?? '', + profilePictureUrl: data[Constants.dbFieldUsersProfilePic], + bio: data[Constants.dbFieldUsersBio], languages: [], locations: {}, ); diff --git a/lib/pages/edit_profile_page.dart b/lib/pages/edit_profile_page.dart index fb66d73..7d4271c 100644 --- a/lib/pages/edit_profile_page.dart +++ b/lib/pages/edit_profile_page.dart @@ -6,9 +6,10 @@ import 'package:image_picker/image_picker.dart'; import 'dart:io'; import '../constants.dart'; +import '../models/user_profile.dart'; class EditProfilePage extends StatefulWidget { - final Map userData; + final UserProfile userData; const EditProfilePage({super.key, required this.userData}); @@ -26,12 +27,10 @@ class EditProfilePageState extends State { @override void initState() { super.initState(); - _nameController = TextEditingController( - text: widget.userData[Constants.dbFieldUsersName]); - _bioController = - TextEditingController(text: widget.userData[Constants.dbFieldUsersBio]); - if (widget.userData[Constants.dbFieldUsersProfilePic] != null) { - profileImageUrl = widget.userData[Constants.dbFieldUsersProfilePic]; + _nameController = TextEditingController(text: widget.userData.name); + _bioController = TextEditingController(text: widget.userData.bio); + if (widget.userData.profilePictureUrl != null) { + profileImageUrl = widget.userData.profilePictureUrl; } } @@ -48,11 +47,15 @@ class EditProfilePageState extends State { void _clearProfileImage() { setState(() { _profileImage = null; - widget.userData[Constants.dbFieldUsersProfilePic] = null; + profileImageUrl = null; + widget.userData.profilePictureUrl = null; }); } Future _saveProfile() async { + String nameTrim = _nameController.text.trim(); + String bioTrim = _bioController.text.trim(); + if (_formKey.currentState!.validate()) { String uid = FirebaseAuth.instance.currentUser!.uid; @@ -63,31 +66,32 @@ class EditProfilePageState extends State { .child(uid); // filename = userid await storageRef.putFile(_profileImage!); profileImageUrl = await storageRef.getDownloadURL(); - } else { - profileImageUrl = null; } - String name = _nameController.text; - String bio = _bioController.text; + Map resultValues = { + Constants.dbFieldUsersName: nameTrim, + Constants.dbFieldUsersBio: bioTrim, + Constants.dbFieldUsersProfilePic: profileImageUrl, + }; await FirebaseFirestore.instance .collection(Constants.dbCollectionUsers) .doc(uid) - .update({ - Constants.dbFieldUsersName: name, - Constants.dbFieldUsersBio: bio, - //if (profileImageUrl != null) - Constants.dbFieldUsersProfilePic: profileImageUrl, - }); + .update(resultValues); - Navigator.pop(context, { - Constants.dbFieldUsersProfilePic: profileImageUrl, - Constants.dbFieldUsersName: name, - Constants.dbFieldUsersBio: bio, - }); + _close(resultValues); } } + /// close this page and return selected values + void _close(Map map) { + Navigator.pop(context, { + Constants.dbFieldUsersProfilePic: map[Constants.dbFieldUsersProfilePic], + Constants.dbFieldUsersName: map[Constants.dbFieldUsersName], + Constants.dbFieldUsersBio: map[Constants.dbFieldUsersBio], + }); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -105,64 +109,9 @@ class EditProfilePageState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ - GestureDetector( - onTap: _pickImage, - child: Stack( - children: [ - CircleAvatar( - radius: 50, - backgroundImage: _profileImage != null - ? FileImage(_profileImage!) as ImageProvider - : (widget.userData[ - Constants.dbFieldUsersProfilePic] != - null - ? NetworkImage(widget.userData[ - Constants.dbFieldUsersProfilePic]) - as ImageProvider - : null), - child: ClipOval( - child: _profileImage == null && - widget.userData[Constants - .dbFieldUsersProfilePic] == - null - ? const Icon(Icons.person, size: 50) - : SizedBox( - width: 100, - height: 100, - child: _profileImage != null - ? Image.file( - _profileImage!, - fit: BoxFit.cover, - ) - : (widget.userData[Constants - .dbFieldUsersProfilePic] != - null - ? Image.network( - widget.userData[Constants - .dbFieldUsersProfilePic], - fit: BoxFit.cover, - ) - : null), - ), - ), - ), - Positioned( - bottom: 0, - right: 0, - child: IconButton( - icon: Ink( - decoration: ShapeDecoration( - color: Theme.of(context).colorScheme.primary, - shape: const CircleBorder(), - ), - child: const Icon(Icons.edit)), - onPressed: _pickImage, - ), - ), - ], - ), - ), - if (_profileImage != null) + buildAvatar(context), + if (_profileImage != null || + widget.userData.profilePictureUrl != null) IconButton( icon: Ink( decoration: const ShapeDecoration( @@ -184,7 +133,7 @@ class EditProfilePageState extends State { controller: _nameController, decoration: const InputDecoration(labelText: 'Name'), validator: (value) { - if (value == null || value.isEmpty) { + if (value == null || value.trim().isEmpty) { return 'Please enter a name'; } return null; @@ -195,6 +144,7 @@ class EditProfilePageState extends State { controller: _bioController, decoration: const InputDecoration(labelText: 'Bio'), maxLines: 3, + maxLength: 4096, ), const SizedBox(height: 16), ElevatedButton( @@ -207,4 +157,56 @@ class EditProfilePageState extends State { ), ); } + + Widget buildAvatar(BuildContext context) { + return GestureDetector( + onTap: _pickImage, + child: Stack( + children: [ + CircleAvatar( + radius: 50, + backgroundImage: _profileImage != null + ? FileImage(_profileImage!) as ImageProvider + : (widget.userData.profilePictureUrl != null + ? NetworkImage(widget.userData.profilePictureUrl!) + as ImageProvider + : null), + child: ClipOval( + child: _profileImage == null && + widget.userData.profilePictureUrl == null + ? const Icon(Icons.person, size: 50) + : SizedBox( + width: 100, + height: 100, + child: _profileImage != null + ? Image.file( + _profileImage!, + fit: BoxFit.cover, + ) + : (widget.userData.profilePictureUrl != null + ? Image.network( + widget.userData.profilePictureUrl!, + fit: BoxFit.cover, + ) + : null), + ), + ), + ), + Positioned( + bottom: 0, + right: 0, + child: IconButton( + icon: Ink( + decoration: ShapeDecoration( + color: Theme.of(context).colorScheme.primary, + shape: const CircleBorder(), + ), + child: const Icon(Icons.edit)), + onPressed: _pickImage, + ), + ), + ], + ), + ); + } } diff --git a/lib/pages/registration_complete_page.dart b/lib/pages/registration_complete_page.dart index 7e4ffc5..d062eca 100644 --- a/lib/pages/registration_complete_page.dart +++ b/lib/pages/registration_complete_page.dart @@ -12,36 +12,46 @@ class RegistrationCompletePage extends StatelessWidget { title: const Text('Registration Complete'), ), body: Center( - child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.check_circle, - color: Colors.green, - size: 100, - ), - const SizedBox(height: 20), - const Text( - 'Registration completed!', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 80), - const Text( - "You can enjoy the app now.", - style: TextStyle(fontSize: 18), - ), - const SizedBox(height: 20), - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => HomePage()), - ); - }, - child: const Text("S T A R T"), - ), - ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.check_circle, + color: Colors.green, + size: 100, + ), + const SizedBox(height: 20), + const Text( + 'Registration completed!', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 40), + const Text( + 'Additional information such as a short biography or a ' + 'profile picture can be entered in the profile options.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 18), + ), + const SizedBox(height: 80), + const Text( + 'You can enjoy the app now.', + style: TextStyle(fontSize: 18), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => HomePage()), + ); + }, + child: const Text('S T A R T'), + ), + ], + ), ), ), ), diff --git a/lib/pages/user_matching_page.dart b/lib/pages/user_matching_page.dart index 1f5d672..7c548c8 100644 --- a/lib/pages/user_matching_page.dart +++ b/lib/pages/user_matching_page.dart @@ -86,10 +86,8 @@ class UserMatchingPageState extends State { final languagesSnapshot = await userDoc.reference .collection(Constants.dbCollectionLanguages) .get(); - final locationsSnapshot = await userDoc.reference - .collection(Constants.dbCollectionLocations) - .get(); + // get languages final languages = languagesSnapshot.docs.map((doc) { final data = doc.data(); return Language( @@ -100,6 +98,10 @@ class UserMatchingPageState extends State { ); }).toList(); + // get locations + final locationsSnapshot = await userDoc.reference + .collection(Constants.dbCollectionLocations) + .get(); final mainDoc = locationsSnapshot.docs .firstWhereOrNull((doc) => doc.id == Constants.dbDocMainLocation); final secondaryDoc = locationsSnapshot.docs @@ -112,21 +114,10 @@ class UserMatchingPageState extends State { : null, }; - final data = userDoc.data(); - UserProfile userProfile = UserProfile( - id: userDoc.id, - uid: data[Constants.dbFieldUsersID] ?? '', - email: data[Constants.dbFieldUsersEmail] ?? '', - name: data[Constants.dbFieldUsersName] ?? '', - firstName: data[Constants.dbFieldUsersFirstName] ?? '', - lastName: data[Constants.dbFieldUsersLastName] ?? '', - skills: List.from(data[Constants.dbFieldUsersSkills] ?? []), - skillsSought: - List.from(data[Constants.dbFieldUsersSkillsSought] ?? []), - risk: data[Constants.dbFieldUsersRiskTolerance] ?? '', - languages: languages, - locations: locations, - ); + // create userProfile including the data above + UserProfile userProfile = UserProfile.fromDocument(userDoc); + userProfile.locations.addAll(locations); + userProfile.languages.addAll(languages); // add profiles accordingly allUsers.add(userProfile); @@ -194,12 +185,12 @@ class UserMatchingPageState extends State { ); } - UserProfile getUserProfile(String userId) { + UserProfile _getUserProfile(String userId) { return userProfiles.firstWhere((x) => x.uid == userId); } /// Check whether the swiped user has also swiped to the right - Future _checkForMatch(swipedUserId) async { + Future _checkForMatch(String swipedUserId) async { String currentUserId = _authService.getCurrentUser()!.uid; final QuerySnapshot matchSnapshot = await _firestore @@ -212,6 +203,7 @@ class UserMatchingPageState extends State { if (matchSnapshot.docs.isNotEmpty) { // save match for both users + // using set method (with UserID) instead of add (with autogenerated ID) final matchesCurrentUser = _firestore .collection(Constants.dbCollectionUsers) .doc(currentUserId) @@ -221,12 +213,12 @@ class UserMatchingPageState extends State { .doc(swipedUserId) .collection(Constants.dbCollectionMatches); - await matchesCurrentUser.add({ + await matchesCurrentUser.doc(swipedUserId).set({ 'otherUserId': swipedUserId, 'timestamp': FieldValue.serverTimestamp(), }); - await matchesSwipedUser.add({ + await matchesSwipedUser.doc(currentUserId).set({ 'otherUserId': currentUserId, 'timestamp': FieldValue.serverTimestamp(), }); @@ -235,21 +227,24 @@ class UserMatchingPageState extends State { // TODO Notify other user? // showMatchedScreen(currentUserId, swipedUserId); + + // Remove matched user from the list of potential users + potentialUserProfiles.removeWhere((x) => x.uid == swipedUserId); } } showMatchedScreen(String currentUserId, String swipedUserId) { - UserProfile currentUser = getUserProfile(currentUserId); - UserProfile swipedUser = getUserProfile(swipedUserId); + UserProfile currentUser = _getUserProfile(currentUserId); + UserProfile swipedUser = _getUserProfile(swipedUserId); Navigator.push( context, MaterialPageRoute( builder: (context) => MatchedScreen( - user1Name: swipedUser.name, - user2Name: currentUser.name, - user1ImageUrl: '', // swipedUser.profilePicture, - user2ImageUrl: '', // currentUser.profilePicture, + currentUserName: currentUser.name, + otherUserName: swipedUser.name, + currentUserImageUrl: currentUser.profilePictureUrl ?? '', + otherUserImageUrl: swipedUser.profilePictureUrl ?? '', onMessageButtonPressed: () { Navigator.pop(context); // Close the MatchedScreen Navigator.push( @@ -270,37 +265,6 @@ class UserMatchingPageState extends State { ); } - double _shortestDistanceBetweenUsers( - UserProfile currentUser, UserProfile otherUser) { - try { - if (currentUser.locations.isEmpty || otherUser.locations.isEmpty) { - return double.nan; - } - - double shortestDistance = double.nan; - // locations currentUser - for (var loc1 in currentUser.locations.values) { - if (loc1 != null && loc1.latitude != null && loc1.longitude != null) { - for (var loc2 in otherUser.locations.values) { - if (loc2 != null && - loc2.latitude != null && - loc2.longitude != null) { - double distance = calculateDistance(loc1.latitude!, - loc1.longitude!, loc2.latitude!, loc2.longitude!); - if (shortestDistance.isNaN || distance < shortestDistance) { - shortestDistance = distance; - } - } - } - } - } - - return shortestDistance; - } catch (e) { - return double.nan; - } - } - @override Widget build(BuildContext context) { if (potentialUserProfiles.isEmpty) { @@ -444,7 +408,7 @@ class UserMatchingPageState extends State { Text( 'Second home: ${userProfile.locations[Constants.dbDocSecondLocation]?.locality ?? 'N/A'}'), Text( - 'Shortest distance: ${_shortestDistanceBetweenUsers(currentUserProfile!, userProfile).toStringAsFixed(0)} km'), + 'Shortest distance: ${shortestDistanceBetweenUsers(currentUserProfile!, userProfile).toStringAsFixed(0)} km'), ], ), ), diff --git a/lib/pages/user_profile_page.dart b/lib/pages/user_profile_page.dart index da3db94..da0e052 100644 --- a/lib/pages/user_profile_page.dart +++ b/lib/pages/user_profile_page.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; + import '../constants.dart'; +import '../models/user_profile.dart'; +import '../services/user_service.dart'; import 'edit_profile_page.dart'; class UserProfilePage extends StatefulWidget { @@ -13,25 +15,23 @@ class UserProfilePage extends StatefulWidget { class _UserProfilePageState extends State { String? profileImageUrl; // Track the profile image URL + late UserProfile myData; bool isLoading = true; - late Map userData; @override void initState() { super.initState(); - _loadUserData(); // Load user data on initialization + // Load user data on initialization + _loadUserData(); } Future _loadUserData() async { - DocumentSnapshot userDoc = await FirebaseFirestore.instance - .collection(Constants.dbCollectionUsers) - .doc(FirebaseAuth.instance.currentUser!.uid) - .get(); + myData = await UserService.getUserProfileById( + FirebaseAuth.instance.currentUser!.uid); setState(() { - userData = userDoc.data() as Map; // Initialize the profile image URL - profileImageUrl = userData[Constants.dbFieldUsersProfilePic]; + profileImageUrl = myData.profilePictureUrl; // Set loading to false once data is loaded isLoading = false; }); @@ -41,17 +41,18 @@ class _UserProfilePageState extends State { final updatedUserData = await Navigator.push( context, MaterialPageRoute( - builder: (context) => EditProfilePage(userData: userData), + builder: (context) => EditProfilePage(userData: myData), ), ); if (updatedUserData != null) { setState(() { + // above Type of updatedUserData is dynamic, so check EditProfilePage profileImageUrl = updatedUserData[Constants.dbFieldUsersProfilePic]; - userData[Constants.dbFieldUsersName] = - updatedUserData[Constants.dbFieldUsersName]; - userData[Constants.dbFieldUsersBio] = - updatedUserData[Constants.dbFieldUsersBio]; + myData.profilePictureUrl = + updatedUserData[Constants.dbFieldUsersProfilePic]; + myData.name = updatedUserData[Constants.dbFieldUsersName]; + myData.bio = updatedUserData[Constants.dbFieldUsersBio]; }); } } @@ -68,7 +69,6 @@ class _UserProfilePageState extends State { padding: const EdgeInsets.all(16.0), child: SingleChildScrollView( child: Column( - //crossAxisAlignment: CrossAxisAlignment.start, children: [ Align( alignment: Alignment.bottomRight, @@ -88,18 +88,21 @@ class _UserProfilePageState extends State { : null, ), const SizedBox(height: 16), - Text(userData[Constants.dbFieldUsersName] ?? 'Name', - style: const TextStyle(fontSize: 24)), - Text(userData[Constants.dbFieldUsersEmail] ?? 'Email', - style: const TextStyle(fontSize: 16)), - const SizedBox(height: 16), + Text(myData.name, style: const TextStyle(fontSize: 24)), + Text(myData.email, style: const TextStyle(fontSize: 16)), + const SizedBox(height: 32), Align( alignment: Alignment.centerLeft, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('Bio'), - Text(userData[Constants.dbFieldUsersBio] ?? 'N/A', + Text( + 'Short description of yourself', + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + ), + ), + Text(myData.bio ?? 'N/A', style: const TextStyle(fontSize: 16)), ], ), diff --git a/lib/services/user_service.dart b/lib/services/user_service.dart index b135d19..0dd58e1 100644 --- a/lib/services/user_service.dart +++ b/lib/services/user_service.dart @@ -42,15 +42,13 @@ class UserService { .get(); if (userDoc.exists && userDoc.data() != null) { - Map userData = - userDoc.data()! as Map; // Explicit cast + Map userData = userDoc.data()! as Map; List? skills; if (skillsSought) { skills = userData[Constants.dbFieldUsersSkillsSought]; } else { - skills = userData[ - Constants.dbFieldUsersSkills]; //as List?; // Explicit cast + skills = userData[Constants.dbFieldUsersSkills]; } if (skills != null && skills.isNotEmpty) { @@ -92,14 +90,6 @@ class UserService { /// Get UserProfile for given [userId] static Future getUserProfileById(String userId) async { - DocumentSnapshot doc = await FirebaseFirestore.instance - .collection(Constants.dbCollectionUsers) - .doc(userId) - .get(); - return UserProfile.fromDocument(doc); - } - - static Future getDataById(String userId) async { FirebaseFirestore firestore = FirebaseFirestore.instance; DocumentSnapshot userDoc = await firestore diff --git a/lib/utils/math.dart b/lib/utils/math.dart index b99a219..b7791f1 100644 --- a/lib/utils/math.dart +++ b/lib/utils/math.dart @@ -1,5 +1,7 @@ import 'dart:math'; +import '../models/user_profile.dart'; + /// /// Convert decimal coordinate to degrees minutes seconds (DMS). /// @@ -45,3 +47,35 @@ double calculateDistance(double lat1, double lon1, double lat2, double lon2) { double _degreesToRadians(double degrees) { return degrees * pi / 180; } + +/// +/// Shortest distance between two users locations +/// +double shortestDistanceBetweenUsers( + UserProfile currentUser, UserProfile otherUser) { + try { + if (currentUser.locations.isEmpty || otherUser.locations.isEmpty) { + return double.nan; + } + + double shortestDistance = double.nan; + // locations currentUser + for (var loc1 in currentUser.locations.values) { + if (loc1 != null && loc1.latitude != null && loc1.longitude != null) { + for (var loc2 in otherUser.locations.values) { + if (loc2 != null && loc2.latitude != null && loc2.longitude != null) { + double distance = calculateDistance(loc1.latitude!, loc1.longitude!, + loc2.latitude!, loc2.longitude!); + if (shortestDistance.isNaN || distance < shortestDistance) { + shortestDistance = distance; + } + } + } + } + } + + return shortestDistance; + } catch (e) { + return double.nan; + } +}