ProfilePicture using firebase_storage and image_picker
parent
a0f9dd43da
commit
f257bfbabb
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import '../pages/home_page.dart';
|
import '../pages/home_page.dart';
|
||||||
import '../pages/user_data_page.dart';
|
import '../pages/user_data_page.dart';
|
||||||
import '../pages/settings_page.dart';
|
import '../pages/settings_page.dart';
|
||||||
|
import '../pages/user_matching_page.dart';
|
||||||
import '../pages/user_profile_page.dart';
|
import '../pages/user_profile_page.dart';
|
||||||
import '../services/auth/auth_service.dart';
|
import '../services/auth/auth_service.dart';
|
||||||
import 'feedback_dialog.dart';
|
import 'feedback_dialog.dart';
|
||||||
|
@ -67,7 +68,7 @@ class MyDrawer extends StatelessWidget {
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (BuildContext context) =>
|
builder: (BuildContext context) =>
|
||||||
const UserProfilePage(),
|
const UserMatchingPage(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -103,11 +104,29 @@ class MyDrawer extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 25),
|
||||||
|
child: ListTile(
|
||||||
|
title: const Text("My Profile Settings"),
|
||||||
|
leading: const Icon(Icons.edit_note),
|
||||||
|
onTap: () {
|
||||||
|
// pop the drawer first, then navigate to destination
|
||||||
|
Navigator.pop(context);
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const UserProfilePage(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// TODO TESTING - user data tile
|
// TODO TESTING - user data tile
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 25),
|
padding: const EdgeInsets.only(left: 25),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: const Text("User Data"),
|
title: const Text("TESTING - User Data"),
|
||||||
leading: const Icon(Icons.supervised_user_circle),
|
leading: const Icon(Icons.supervised_user_circle),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// pop the drawer first, then navigate to destination
|
// pop the drawer first, then navigate to destination
|
||||||
|
|
|
@ -32,8 +32,10 @@ class Constants {
|
||||||
static const String dbFieldUsersName = 'name';
|
static const String dbFieldUsersName = 'name';
|
||||||
static const String dbFieldUsersFirstName = 'firstname';
|
static const String dbFieldUsersFirstName = 'firstname';
|
||||||
static const String dbFieldUsersLastName = 'lastname';
|
static const String dbFieldUsersLastName = 'lastname';
|
||||||
|
static const String dbFieldUsersBio = 'bio';
|
||||||
static const String dbFieldUsersGender = 'gender';
|
static const String dbFieldUsersGender = 'gender';
|
||||||
static const String dbFieldUsersYearBorn = 'born';
|
static const String dbFieldUsersYearBorn = 'born';
|
||||||
|
static const String dbFieldUsersProfilePic = 'profilePictureUrl';
|
||||||
static const String dbFieldUsersSkills = 'skills';
|
static const String dbFieldUsersSkills = 'skills';
|
||||||
static const String dbFieldUsersSkillsSought = 'skills_sought';
|
static const String dbFieldUsersSkillsSought = 'skills_sought';
|
||||||
static const String dbFieldUsersAvailability = 'availability';
|
static const String dbFieldUsersAvailability = 'availability';
|
||||||
|
@ -43,5 +45,7 @@ class Constants {
|
||||||
static const String dbFieldUsersCommunication = 'communication';
|
static const String dbFieldUsersCommunication = 'communication';
|
||||||
static const String dbFieldUsersRiskTolerance = 'risk_tolerance';
|
static const String dbFieldUsersRiskTolerance = 'risk_tolerance';
|
||||||
|
|
||||||
|
static const String dbStoragePathProfiles = 'profile_images';
|
||||||
|
|
||||||
static const String pathLanguagesJson = 'lib/assets/languages.json';
|
static const String pathLanguagesJson = 'lib/assets/languages.json';
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import 'package:cofounderella/pages/user_vision_page.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../../enumerations.dart';
|
import '../../enumerations.dart';
|
||||||
import '../../services/auth/auth_service.dart';
|
import '../../services/auth/auth_service.dart';
|
||||||
import '../../services/user_service.dart';
|
import '../../services/user_service.dart';
|
||||||
|
import '../pages/user_vision_page.dart';
|
||||||
import 'profile_category_form.dart';
|
import 'profile_category_form.dart';
|
||||||
|
|
||||||
class SkillsForm extends StatelessWidget {
|
class SkillsForm extends StatelessWidget {
|
||||||
|
@ -21,7 +21,7 @@ class SkillsForm extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FutureBuilder<List<SkillOption>>(
|
return FutureBuilder<List<SkillOption>>(
|
||||||
future: UserService().getSkillsFromFirebase(
|
future: UserService.getSkillsFromFirebase(
|
||||||
skillsSought,
|
skillsSought,
|
||||||
_authService.getCurrentUser()!.uid,
|
_authService.getCurrentUser()!.uid,
|
||||||
), // Fetch skills from Firebase
|
), // Fetch skills from Firebase
|
||||||
|
@ -49,7 +49,7 @@ class SkillsForm extends StatelessWidget {
|
||||||
userSkills ?? [], // Pass pre-selected skills to the form
|
userSkills ?? [], // Pass pre-selected skills to the form
|
||||||
onSave: (selectedOptions) async {
|
onSave: (selectedOptions) async {
|
||||||
// Handle saving selected options
|
// Handle saving selected options
|
||||||
bool success = await UserService().saveSkillsToFirebase(
|
bool success = await UserService.saveSkillsToFirebase(
|
||||||
selectedOptions.cast<SkillOption>(),
|
selectedOptions.cast<SkillOption>(),
|
||||||
skillsSought,
|
skillsSought,
|
||||||
_authService.getCurrentUser()!.uid,
|
_authService.getCurrentUser()!.uid,
|
||||||
|
@ -63,7 +63,8 @@ class SkillsForm extends StatelessWidget {
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
// set following registration page HERE
|
// set following registration page HERE
|
||||||
builder: (context) => MatchingForm(isRegProcess: isRegProcess),
|
builder: (context) =>
|
||||||
|
MatchingForm(isRegProcess: isRegProcess),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (isRegProcess) {
|
} else if (isRegProcess) {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
|
||||||
class Language {
|
class Language {
|
||||||
final String code;
|
final String code;
|
||||||
final String name;
|
final String name;
|
||||||
|
@ -21,6 +23,16 @@ class Language {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
factory Language.fromDocument(DocumentSnapshot doc) {
|
||||||
|
Map<String, dynamic> data = doc.data() as Map<String, dynamic>;
|
||||||
|
return Language(
|
||||||
|
code: data['code'] ?? '',
|
||||||
|
name: data['name'] ?? '',
|
||||||
|
nativeName: data['nativeName'] ?? '',
|
||||||
|
iconFile: data['iconFile'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => code.hashCode;
|
int get hashCode => code.hashCode;
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
import '../constants.dart';
|
import '../constants.dart';
|
||||||
import '../utils/math.dart';
|
import '../utils/math.dart';
|
||||||
|
|
||||||
|
@ -26,6 +27,20 @@ class MyLocation {
|
||||||
required this.longitude,
|
required this.longitude,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
factory MyLocation.fromDocument(DocumentSnapshot doc) {
|
||||||
|
Map<String, dynamic> data = doc.data() as Map<String, dynamic>;
|
||||||
|
return MyLocation(
|
||||||
|
street: data[Constants.dbFieldLocationStreet] ?? '',
|
||||||
|
country: data[Constants.dbFieldLocationCountry] ?? '',
|
||||||
|
administrativeArea: data[Constants.dbFieldLocationArea],
|
||||||
|
locality: data[Constants.dbFieldLocationLocality] ?? '',
|
||||||
|
subLocality: data[Constants.dbFieldLocationSubLocality],
|
||||||
|
postalCode: data[Constants.dbFieldLocationPostalCode],
|
||||||
|
latitude: (data[Constants.dbFieldLocationLatitude] as num?)?.toDouble(),
|
||||||
|
longitude: (data[Constants.dbFieldLocationLongitude] as num?)?.toDouble(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// convert to a map
|
// convert to a map
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -0,0 +1,210 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
|
import 'package:firebase_storage/firebase_storage.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import '../constants.dart';
|
||||||
|
|
||||||
|
class EditProfilePage extends StatefulWidget {
|
||||||
|
final Map<String, dynamic> userData;
|
||||||
|
|
||||||
|
const EditProfilePage({super.key, required this.userData});
|
||||||
|
|
||||||
|
@override
|
||||||
|
EditProfilePageState createState() => EditProfilePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class EditProfilePageState extends State<EditProfilePage> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
late TextEditingController _nameController;
|
||||||
|
late TextEditingController _bioController;
|
||||||
|
late String? profileImageUrl;
|
||||||
|
File? _profileImage;
|
||||||
|
|
||||||
|
@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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickImage() async {
|
||||||
|
final pickedFile =
|
||||||
|
await ImagePicker().pickImage(source: ImageSource.gallery);
|
||||||
|
if (pickedFile != null) {
|
||||||
|
setState(() {
|
||||||
|
_profileImage = File(pickedFile.path);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _clearProfileImage() {
|
||||||
|
setState(() {
|
||||||
|
_profileImage = null;
|
||||||
|
widget.userData[Constants.dbFieldUsersProfilePic] = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveProfile() async {
|
||||||
|
if (_formKey.currentState!.validate()) {
|
||||||
|
String uid = FirebaseAuth.instance.currentUser!.uid;
|
||||||
|
|
||||||
|
if (_profileImage != null) {
|
||||||
|
final storageRef = FirebaseStorage.instance
|
||||||
|
.ref()
|
||||||
|
.child(Constants.dbStoragePathProfiles)
|
||||||
|
.child(uid); // filename = userid
|
||||||
|
await storageRef.putFile(_profileImage!);
|
||||||
|
profileImageUrl = await storageRef.getDownloadURL();
|
||||||
|
} else {
|
||||||
|
profileImageUrl = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String name = _nameController.text;
|
||||||
|
String bio = _bioController.text;
|
||||||
|
|
||||||
|
await FirebaseFirestore.instance
|
||||||
|
.collection(Constants.dbCollectionUsers)
|
||||||
|
.doc(uid)
|
||||||
|
.update({
|
||||||
|
Constants.dbFieldUsersName: name,
|
||||||
|
Constants.dbFieldUsersBio: bio,
|
||||||
|
//if (profileImageUrl != null)
|
||||||
|
Constants.dbFieldUsersProfilePic: profileImageUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
Navigator.pop(context, {
|
||||||
|
Constants.dbFieldUsersProfilePic: profileImageUrl,
|
||||||
|
Constants.dbFieldUsersName: name,
|
||||||
|
Constants.dbFieldUsersBio: bio,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Edit Profile'),
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Row(
|
||||||
|
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)
|
||||||
|
IconButton(
|
||||||
|
icon: Ink(
|
||||||
|
decoration: const ShapeDecoration(
|
||||||
|
shape: CircleBorder(),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.delete,
|
||||||
|
color: Colors.red,
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: _clearProfileImage,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: _nameController,
|
||||||
|
decoration: const InputDecoration(labelText: 'Name'),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Please enter a name';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: _bioController,
|
||||||
|
decoration: const InputDecoration(labelText: 'Bio'),
|
||||||
|
maxLines: 3,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _saveProfile,
|
||||||
|
child: const Text('Save'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,7 +31,7 @@ class RegisterPage extends StatelessWidget {
|
||||||
.signUpWithEmailPassword(
|
.signUpWithEmailPassword(
|
||||||
_emailController.text, _passwordController.text)
|
_emailController.text, _passwordController.text)
|
||||||
.then((userCredential) {
|
.then((userCredential) {
|
||||||
UserService().saveUserData(userCredential, _emailController.text,
|
UserService.saveUserData(userCredential, _emailController.text,
|
||||||
_nameController.text, _lastnameController.text);
|
_nameController.text, _lastnameController.text);
|
||||||
});
|
});
|
||||||
} on FirebaseAuthException catch (e) {
|
} on FirebaseAuthException catch (e) {
|
||||||
|
|
|
@ -0,0 +1,604 @@
|
||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:swipable_stack/swipable_stack.dart';
|
||||||
|
|
||||||
|
import '../constants.dart';
|
||||||
|
import '../forms/matched_screen.dart';
|
||||||
|
import '../models/language.dart';
|
||||||
|
import '../models/location.dart';
|
||||||
|
import '../models/user_profile.dart';
|
||||||
|
import '../services/auth/auth_service.dart';
|
||||||
|
import '../utils/math.dart';
|
||||||
|
import 'chat_page.dart';
|
||||||
|
|
||||||
|
class UserMatchingPage extends StatefulWidget {
|
||||||
|
const UserMatchingPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
UserMatchingPageState createState() => UserMatchingPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserMatchingPageState extends State<UserMatchingPage> {
|
||||||
|
/// The current's user profile
|
||||||
|
UserProfile? currentUserProfile;
|
||||||
|
|
||||||
|
/// List with [all] user profiles
|
||||||
|
List<UserProfile> userProfiles = [];
|
||||||
|
|
||||||
|
/// List of user profiles to show.
|
||||||
|
List<UserProfile> potentialUserProfiles = [];
|
||||||
|
|
||||||
|
late final SwipableStackController _controller;
|
||||||
|
|
||||||
|
// get instance of firestore and auth
|
||||||
|
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||||
|
final AuthService _authService = AuthService();
|
||||||
|
|
||||||
|
void _listenController() => setState(() {});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = SwipableStackController()..addListener(_listenController);
|
||||||
|
_fetchUserProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller
|
||||||
|
..removeListener(_listenController)
|
||||||
|
..dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _fetchUserProfiles() async {
|
||||||
|
final String currentUserId = _authService.getCurrentUser()!.uid;
|
||||||
|
List<UserProfile> allUsers = [];
|
||||||
|
List<UserProfile> showProfiles = [];
|
||||||
|
|
||||||
|
final usersSnapshot = await FirebaseFirestore.instance
|
||||||
|
.collection(Constants.dbCollectionUsers)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
// Fetch the list of profiles the current user has already swiped
|
||||||
|
final QuerySnapshot swipesSnapshot = await FirebaseFirestore.instance
|
||||||
|
.collection(Constants.dbCollectionUsers)
|
||||||
|
.doc(currentUserId)
|
||||||
|
.collection(Constants.dbCollectionSwipes)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
final Set<String> likedUserIds = swipesSnapshot.docs
|
||||||
|
.where((doc) => doc['liked'] == true)
|
||||||
|
.map((doc) => doc.id)
|
||||||
|
.toSet();
|
||||||
|
|
||||||
|
final DateTime thresholdDate =
|
||||||
|
DateTime.now().subtract(const Duration(hours: 24));
|
||||||
|
final Set<String> dislikedUserIds = swipesSnapshot.docs
|
||||||
|
.where((doc) =>
|
||||||
|
doc['liked'] == false &&
|
||||||
|
(doc['timestamp'] as Timestamp).toDate().isAfter(thresholdDate))
|
||||||
|
.map((doc) => doc.id)
|
||||||
|
.toSet();
|
||||||
|
|
||||||
|
for (var userDoc in usersSnapshot.docs) {
|
||||||
|
final languagesSnapshot = await userDoc.reference
|
||||||
|
.collection(Constants.dbCollectionLanguages)
|
||||||
|
.get();
|
||||||
|
final locationsSnapshot = await userDoc.reference
|
||||||
|
.collection(Constants.dbCollectionLocations)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
final languages = languagesSnapshot.docs.map((doc) {
|
||||||
|
final data = doc.data();
|
||||||
|
return Language(
|
||||||
|
code: data['code'],
|
||||||
|
name: data['name'],
|
||||||
|
nativeName: data['nativeName'],
|
||||||
|
iconFile: data['iconFile'],
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
final mainDoc = locationsSnapshot.docs
|
||||||
|
.firstWhereOrNull((doc) => doc.id == Constants.dbDocMainLocation);
|
||||||
|
final secondaryDoc = locationsSnapshot.docs
|
||||||
|
.firstWhereOrNull((doc) => doc.id == Constants.dbDocSecondLocation);
|
||||||
|
final locations = {
|
||||||
|
Constants.dbDocMainLocation:
|
||||||
|
mainDoc != null ? _createLocationFromDoc(mainDoc.data()) : null,
|
||||||
|
Constants.dbDocSecondLocation: secondaryDoc != null
|
||||||
|
? _createLocationFromDoc(secondaryDoc.data())
|
||||||
|
: 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<String>.from(data[Constants.dbFieldUsersSkills] ?? []),
|
||||||
|
skillsSought:
|
||||||
|
List<String>.from(data[Constants.dbFieldUsersSkillsSought] ?? []),
|
||||||
|
risk: data[Constants.dbFieldUsersRiskTolerance] ?? '',
|
||||||
|
languages: languages,
|
||||||
|
locations: locations,
|
||||||
|
);
|
||||||
|
|
||||||
|
// add profiles accordingly
|
||||||
|
allUsers.add(userProfile);
|
||||||
|
// Exclude (1) the current user's profile, (2) the already liked profiles
|
||||||
|
// and (3) users that were disliked less than 24 hours ago
|
||||||
|
if (userDoc.id != currentUserId &&
|
||||||
|
!likedUserIds.contains(userDoc.id) &&
|
||||||
|
!dislikedUserIds.contains(userDoc.id)) {
|
||||||
|
showProfiles.add(userProfile);
|
||||||
|
}
|
||||||
|
} // end for
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
userProfiles = allUsers;
|
||||||
|
potentialUserProfiles = showProfiles;
|
||||||
|
currentUserProfile =
|
||||||
|
allUsers.firstWhereOrNull((x) => x.uid == currentUserId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
MyLocation? _createLocationFromDoc(Map<String, dynamic>? data) {
|
||||||
|
if (data == null || data.isEmpty) return null;
|
||||||
|
|
||||||
|
return MyLocation(
|
||||||
|
street: data[Constants.dbFieldLocationStreet],
|
||||||
|
country: data[Constants.dbFieldLocationCountry],
|
||||||
|
administrativeArea: data[Constants.dbFieldLocationArea],
|
||||||
|
locality: data[Constants.dbFieldLocationLocality],
|
||||||
|
subLocality: data[Constants.dbFieldLocationSubLocality],
|
||||||
|
postalCode: data[Constants.dbFieldLocationPostalCode],
|
||||||
|
latitude: data[Constants.dbFieldLocationLatitude],
|
||||||
|
longitude: data[Constants.dbFieldLocationLongitude],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _swipeLeft() {
|
||||||
|
_controller.next(
|
||||||
|
swipeDirection: SwipeDirection.left, duration: Durations.extralong1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _swipeRight() {
|
||||||
|
_controller.next(
|
||||||
|
swipeDirection: SwipeDirection.right, duration: Durations.extralong4);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _skip() {
|
||||||
|
_controller.next(
|
||||||
|
swipeDirection: SwipeDirection.up, duration: Durations.long4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save swipe status to database
|
||||||
|
Future<void> _saveSwipeAction(
|
||||||
|
String swipedUserId, SwipeDirection direction) async {
|
||||||
|
await _firestore
|
||||||
|
.collection(Constants.dbCollectionUsers)
|
||||||
|
.doc(_authService.getCurrentUser()!.uid)
|
||||||
|
.collection(Constants.dbCollectionSwipes)
|
||||||
|
.doc(swipedUserId) // UserID instead of autogenerated ID
|
||||||
|
.set(
|
||||||
|
{
|
||||||
|
'swipedId': swipedUserId,
|
||||||
|
'liked': direction == SwipeDirection.right,
|
||||||
|
'timestamp': FieldValue.serverTimestamp(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
UserProfile getUserProfile(String userId) {
|
||||||
|
return userProfiles.firstWhere((x) => x.uid == userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether the swiped user has also swiped to the right
|
||||||
|
Future<void> _checkForMatch(swipedUserId) async {
|
||||||
|
String currentUserId = _authService.getCurrentUser()!.uid;
|
||||||
|
|
||||||
|
final QuerySnapshot matchSnapshot = await _firestore
|
||||||
|
.collection(Constants.dbCollectionUsers)
|
||||||
|
.doc(swipedUserId)
|
||||||
|
.collection(Constants.dbCollectionSwipes)
|
||||||
|
.where('swipedId', isEqualTo: currentUserId)
|
||||||
|
.where('liked', isEqualTo: true)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (matchSnapshot.docs.isNotEmpty) {
|
||||||
|
// save match for both users
|
||||||
|
final matchesCurrentUser = _firestore
|
||||||
|
.collection(Constants.dbCollectionUsers)
|
||||||
|
.doc(currentUserId)
|
||||||
|
.collection(Constants.dbCollectionMatches);
|
||||||
|
final matchesSwipedUser = _firestore
|
||||||
|
.collection(Constants.dbCollectionUsers)
|
||||||
|
.doc(swipedUserId)
|
||||||
|
.collection(Constants.dbCollectionMatches);
|
||||||
|
|
||||||
|
await matchesCurrentUser.add({
|
||||||
|
'otherUserId': swipedUserId,
|
||||||
|
'timestamp': FieldValue.serverTimestamp(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await matchesSwipedUser.add({
|
||||||
|
'otherUserId': currentUserId,
|
||||||
|
'timestamp': FieldValue.serverTimestamp(),
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// TODO Notify other user?
|
||||||
|
//
|
||||||
|
showMatchedScreen(currentUserId, swipedUserId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showMatchedScreen(String currentUserId, String 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,
|
||||||
|
onMessageButtonPressed: () {
|
||||||
|
Navigator.pop(context); // Close the MatchedScreen
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => ChatPage(
|
||||||
|
receiverEmail: swipedUser.email,
|
||||||
|
receiverID: swipedUser.uid,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onContinueButtonPressed: () {
|
||||||
|
Navigator.pop(context); // Close the MatchedScreen
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('User Profiles')),
|
||||||
|
body: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.person_search, size: 64),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
// Loading...
|
||||||
|
if (userProfiles.isEmpty) ...[
|
||||||
|
const CircularProgressIndicator(),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
const Text(
|
||||||
|
'Loading data, please wait...',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
Text(
|
||||||
|
userProfiles.length > 1
|
||||||
|
? 'No new profiles available yet.'
|
||||||
|
: 'No profiles available at the moment.',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 18,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 60),
|
||||||
|
const Text(
|
||||||
|
'Please check back later, perhaps tomorrow.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Find your Match')),
|
||||||
|
body: SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: SwipableStack(
|
||||||
|
detectableSwipeDirections: const {
|
||||||
|
SwipeDirection.right,
|
||||||
|
SwipeDirection.left,
|
||||||
|
SwipeDirection.up,
|
||||||
|
},
|
||||||
|
controller: _controller,
|
||||||
|
stackClipBehaviour: Clip.none,
|
||||||
|
swipeAnchor: SwipeAnchor.bottom,
|
||||||
|
// item count +1 for additional end page
|
||||||
|
itemCount: potentialUserProfiles.length + 1,
|
||||||
|
onSwipeCompleted: (index, direction) {
|
||||||
|
//
|
||||||
|
// Swipe logic
|
||||||
|
//
|
||||||
|
String swipedUserId =
|
||||||
|
potentialUserProfiles[_controller.currentIndex].id;
|
||||||
|
if (direction == SwipeDirection.right) {
|
||||||
|
_saveSwipeAction(swipedUserId, SwipeDirection.right);
|
||||||
|
_checkForMatch(swipedUserId);
|
||||||
|
} else if (direction == SwipeDirection.left) {
|
||||||
|
_saveSwipeAction(swipedUserId, SwipeDirection.left);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
horizontalSwipeThreshold: 0.8,
|
||||||
|
verticalSwipeThreshold: 0.8,
|
||||||
|
builder: (context, properties) {
|
||||||
|
if (properties.index == potentialUserProfiles.length) {
|
||||||
|
return Center(
|
||||||
|
child: _buildLastCard(), // last card
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final userProfile = potentialUserProfiles[
|
||||||
|
properties.index % potentialUserProfiles.length];
|
||||||
|
return Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(25.0),
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
_buildUserCard(userProfile),
|
||||||
|
if (properties.stackIndex == 0 &&
|
||||||
|
properties.direction != null)
|
||||||
|
CardOverlay(
|
||||||
|
swipeProgress: properties.swipeProgress,
|
||||||
|
direction: properties.direction!,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildSwipeButtons(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildUserCard(UserProfile userProfile) {
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(userProfile.name, style: const TextStyle(fontSize: 24)),
|
||||||
|
Text(userProfile.email, style: const TextStyle(fontSize: 24)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Has skills and experience in: ${userProfile.skills.join(', ')}'),
|
||||||
|
Text(
|
||||||
|
'Seeks someone with skills in: ${userProfile.skillsSought.join(', ')}'),
|
||||||
|
Text('Risk type: ${userProfile.risk}'),
|
||||||
|
Text(
|
||||||
|
'Speaks: ${userProfile.languages.map((lang) => lang.name).join(', ')}'),
|
||||||
|
Text(
|
||||||
|
'Lives in: ${userProfile.locations[Constants.dbDocMainLocation]?.locality ?? 'N/A'}'),
|
||||||
|
Text(
|
||||||
|
'Coordinates: ${userProfile.locations[Constants.dbDocMainLocation]?.latitude} ${userProfile.locations[Constants.dbDocMainLocation]?.longitude}'),
|
||||||
|
Text(
|
||||||
|
'Second home: ${userProfile.locations[Constants.dbDocSecondLocation]?.locality ?? 'N/A'}'),
|
||||||
|
Text(
|
||||||
|
'Shortest distance: ${_shortestDistanceBetweenUsers(currentUserProfile!, userProfile).toStringAsFixed(0)} km'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLastCard() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
// Show reached end message and restart button
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.person_search, size: 64),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
const Text('You\'ve viewed all available profiles.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
||||||
|
const SizedBox(height: 60),
|
||||||
|
const Text(
|
||||||
|
'Would you like to do another run and see the remaining profiles again?',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
// Restart swiping from the beginning
|
||||||
|
_controller.currentIndex = 0;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Text('Another run'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSwipeButtons() {
|
||||||
|
return Positioned(
|
||||||
|
bottom: 16,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
FloatingActionButton(
|
||||||
|
heroTag: 'myFABUndo',
|
||||||
|
tooltip: 'Undo last action',
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
onPressed: () {
|
||||||
|
_controller.rewind(duration: Durations.extralong4);
|
||||||
|
},
|
||||||
|
child: const Icon(Icons.undo),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 72,
|
||||||
|
height: 72,
|
||||||
|
child: FloatingActionButton(
|
||||||
|
heroTag: 'myFABSwipeLeft',
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
onPressed: _swipeLeft,
|
||||||
|
child: Stack(
|
||||||
|
// to deal with icons inner transparency
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 48.0,
|
||||||
|
height: 48.0,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white70,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Icon(Icons.cancel, color: Colors.red, size: 72),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 72,
|
||||||
|
height: 72,
|
||||||
|
child: FloatingActionButton(
|
||||||
|
heroTag: 'myFABSwipeRight',
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
onPressed: _swipeRight,
|
||||||
|
child: Stack(
|
||||||
|
// to deal with icons inner transparency
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 48.0,
|
||||||
|
height: 48.0,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white70,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Icon(Icons.check_circle, color: Colors.green, size: 72),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FloatingActionButton(
|
||||||
|
tooltip: 'Skip profile',
|
||||||
|
heroTag: 'myFABSkip',
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
onPressed: _skip,
|
||||||
|
child: const Icon(Icons.skip_next),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CardOverlay extends StatelessWidget {
|
||||||
|
final double swipeProgress;
|
||||||
|
final SwipeDirection direction;
|
||||||
|
|
||||||
|
const CardOverlay({
|
||||||
|
super.key,
|
||||||
|
required this.swipeProgress,
|
||||||
|
required this.direction,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Positioned.fill(
|
||||||
|
bottom: 300,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: swipeProgress.abs().clamp(0.0, 1.0),
|
||||||
|
child: Align(
|
||||||
|
alignment: direction == SwipeDirection.right
|
||||||
|
? Alignment.centerLeft
|
||||||
|
: (direction == SwipeDirection.left
|
||||||
|
? Alignment.centerRight
|
||||||
|
: Alignment.center),
|
||||||
|
child: Icon(
|
||||||
|
direction == SwipeDirection.right
|
||||||
|
? Icons.thumb_up
|
||||||
|
: (direction == SwipeDirection.left
|
||||||
|
? Icons.thumb_down
|
||||||
|
: Icons.skip_next),
|
||||||
|
size: 100,
|
||||||
|
color: direction == SwipeDirection.right
|
||||||
|
? Colors.green
|
||||||
|
: (direction == SwipeDirection.left ? Colors.red : Colors.blue),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,566 +1,116 @@
|
||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:swipable_stack/swipable_stack.dart';
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import '../constants.dart';
|
import '../constants.dart';
|
||||||
import '../forms/matched_screen.dart';
|
import 'edit_profile_page.dart';
|
||||||
import '../models/language.dart';
|
|
||||||
import '../models/location.dart';
|
|
||||||
import '../models/user_profile.dart';
|
|
||||||
import '../services/auth/auth_service.dart';
|
|
||||||
import 'chat_page.dart';
|
|
||||||
|
|
||||||
class UserProfilePage extends StatefulWidget {
|
class UserProfilePage extends StatefulWidget {
|
||||||
const UserProfilePage({super.key});
|
const UserProfilePage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
UserProfilePageState createState() => UserProfilePageState();
|
State<UserProfilePage> createState() => _UserProfilePageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserProfilePageState extends State<UserProfilePage> {
|
class _UserProfilePageState extends State<UserProfilePage> {
|
||||||
/// The current's user profile
|
String? profileImageUrl; // Track the profile image URL
|
||||||
UserProfile? currentUserProfile;
|
bool isLoading = true;
|
||||||
|
late Map<String, dynamic> userData;
|
||||||
/// List with [all] user profiles
|
|
||||||
List<UserProfile> userProfiles = [];
|
|
||||||
|
|
||||||
/// List of user profiles to show.
|
|
||||||
List<UserProfile> potentialUserProfiles = [];
|
|
||||||
|
|
||||||
late final SwipableStackController _controller;
|
|
||||||
|
|
||||||
// get instance of firestore and auth
|
|
||||||
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
|
||||||
final AuthService _authService = AuthService();
|
|
||||||
|
|
||||||
void _listenController() => setState(() {});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_controller = SwipableStackController()..addListener(_listenController);
|
_loadUserData(); // Load user data on initialization
|
||||||
_fetchUserProfiles();
|
}
|
||||||
|
|
||||||
|
Future<void> _loadUserData() async {
|
||||||
|
DocumentSnapshot userDoc = await FirebaseFirestore.instance
|
||||||
|
.collection(Constants.dbCollectionUsers)
|
||||||
|
.doc(FirebaseAuth.instance.currentUser!.uid)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
userData = userDoc.data() as Map<String, dynamic>;
|
||||||
|
// Initialize the profile image URL
|
||||||
|
profileImageUrl = userData[Constants.dbFieldUsersProfilePic];
|
||||||
|
// Set loading to false once data is loaded
|
||||||
|
isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void editNameInfo() async {
|
||||||
|
final updatedUserData = await Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => EditProfilePage(userData: userData),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (updatedUserData != null) {
|
||||||
|
setState(() {
|
||||||
|
profileImageUrl = updatedUserData[Constants.dbFieldUsersProfilePic];
|
||||||
|
userData[Constants.dbFieldUsersName] =
|
||||||
|
updatedUserData[Constants.dbFieldUsersName];
|
||||||
|
userData[Constants.dbFieldUsersBio] =
|
||||||
|
updatedUserData[Constants.dbFieldUsersBio];
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
Widget build(BuildContext context) {
|
||||||
_controller
|
return Scaffold(
|
||||||
..removeListener(_listenController)
|
appBar: AppBar(
|
||||||
..dispose();
|
title: const Text('User Profile'),
|
||||||
super.dispose();
|
),
|
||||||
}
|
body: isLoading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
Future<void> _fetchUserProfiles() async {
|
: Padding(
|
||||||
final String currentUserId = _authService.getCurrentUser()!.uid;
|
padding: const EdgeInsets.all(16.0),
|
||||||
List<UserProfile> allUsers = [];
|
child: SingleChildScrollView(
|
||||||
List<UserProfile> showProfiles = [];
|
child: Column(
|
||||||
|
//crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
final usersSnapshot = await FirebaseFirestore.instance
|
children: [
|
||||||
.collection(Constants.dbCollectionUsers)
|
Align(
|
||||||
.get();
|
alignment: Alignment.bottomRight,
|
||||||
|
child: OutlinedButton.icon(
|
||||||
// Fetch the list of profiles the current user has already swiped
|
label: const Text('Edit'),
|
||||||
final QuerySnapshot swipesSnapshot = await FirebaseFirestore.instance
|
icon: const Icon(Icons.edit),
|
||||||
.collection(Constants.dbCollectionUsers)
|
onPressed: editNameInfo,
|
||||||
.doc(currentUserId)
|
),
|
||||||
.collection(Constants.dbCollectionSwipes)
|
),
|
||||||
.get();
|
CircleAvatar(
|
||||||
|
radius: 50,
|
||||||
final Set<String> likedUserIds = swipesSnapshot.docs
|
backgroundImage: profileImageUrl != null
|
||||||
.where((doc) => doc['liked'] == true)
|
? NetworkImage(profileImageUrl!)
|
||||||
.map((doc) => doc.id)
|
: null,
|
||||||
.toSet();
|
child: profileImageUrl == null
|
||||||
|
? const Icon(Icons.person, size: 50)
|
||||||
final DateTime thresholdDate =
|
|
||||||
DateTime.now().subtract(const Duration(hours: 24));
|
|
||||||
final Set<String> dislikedUserIds = swipesSnapshot.docs
|
|
||||||
.where((doc) =>
|
|
||||||
doc['liked'] == false &&
|
|
||||||
(doc['timestamp'] as Timestamp).toDate().isAfter(thresholdDate))
|
|
||||||
.map((doc) => doc.id)
|
|
||||||
.toSet();
|
|
||||||
|
|
||||||
for (var userDoc in usersSnapshot.docs) {
|
|
||||||
final languagesSnapshot = await userDoc.reference
|
|
||||||
.collection(Constants.dbCollectionLanguages)
|
|
||||||
.get();
|
|
||||||
final locationsSnapshot = await userDoc.reference
|
|
||||||
.collection(Constants.dbCollectionLocations)
|
|
||||||
.get();
|
|
||||||
|
|
||||||
final languages = languagesSnapshot.docs.map((doc) {
|
|
||||||
final data = doc.data();
|
|
||||||
return Language(
|
|
||||||
code: data['code'],
|
|
||||||
name: data['name'],
|
|
||||||
nativeName: data['nativeName'],
|
|
||||||
iconFile: data['iconFile'],
|
|
||||||
);
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
final mainDoc = locationsSnapshot.docs
|
|
||||||
.firstWhereOrNull((doc) => doc.id == Constants.dbDocMainLocation);
|
|
||||||
final secondaryDoc = locationsSnapshot.docs
|
|
||||||
.firstWhereOrNull((doc) => doc.id == Constants.dbDocSecondLocation);
|
|
||||||
final locations = {
|
|
||||||
Constants.dbDocMainLocation:
|
|
||||||
mainDoc != null ? _createLocationFromDoc(mainDoc.data()) : null,
|
|
||||||
Constants.dbDocSecondLocation: secondaryDoc != null
|
|
||||||
? _createLocationFromDoc(secondaryDoc.data())
|
|
||||||
: null,
|
: 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<String>.from(data[Constants.dbFieldUsersSkills] ?? []),
|
|
||||||
skillsSought:
|
|
||||||
List<String>.from(data[Constants.dbFieldUsersSkillsSought] ?? []),
|
|
||||||
risk: data[Constants.dbFieldUsersRiskTolerance] ?? '',
|
|
||||||
languages: languages,
|
|
||||||
locations: locations,
|
|
||||||
);
|
|
||||||
|
|
||||||
// add profiles accordingly
|
|
||||||
allUsers.add(userProfile);
|
|
||||||
// Exclude (1) the current user's profile, (2) the already liked profiles
|
|
||||||
// and (3) users that were disliked less than 24 hours ago
|
|
||||||
if (userDoc.id != currentUserId &&
|
|
||||||
!likedUserIds.contains(userDoc.id) &&
|
|
||||||
!dislikedUserIds.contains(userDoc.id)) {
|
|
||||||
showProfiles.add(userProfile);
|
|
||||||
}
|
|
||||||
} // end for
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
userProfiles = allUsers;
|
|
||||||
potentialUserProfiles = showProfiles;
|
|
||||||
currentUserProfile =
|
|
||||||
allUsers.firstWhereOrNull((x) => x.uid == currentUserId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
MyLocation? _createLocationFromDoc(Map<String, dynamic>? data) {
|
|
||||||
if (data == null || data.isEmpty) return null;
|
|
||||||
|
|
||||||
return MyLocation(
|
|
||||||
street: data[Constants.dbFieldLocationStreet],
|
|
||||||
country: data[Constants.dbFieldLocationCountry],
|
|
||||||
administrativeArea: data[Constants.dbFieldLocationArea],
|
|
||||||
locality: data[Constants.dbFieldLocationLocality],
|
|
||||||
subLocality: data[Constants.dbFieldLocationSubLocality],
|
|
||||||
postalCode: data[Constants.dbFieldLocationPostalCode],
|
|
||||||
latitude: data[Constants.dbFieldLocationLatitude],
|
|
||||||
longitude: data[Constants.dbFieldLocationLongitude],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _swipeLeft() {
|
|
||||||
_controller.next(
|
|
||||||
swipeDirection: SwipeDirection.left, duration: Durations.extralong1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _swipeRight() {
|
|
||||||
_controller.next(
|
|
||||||
swipeDirection: SwipeDirection.right, duration: Durations.extralong4);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _skip() {
|
|
||||||
_controller.next(
|
|
||||||
swipeDirection: SwipeDirection.up, duration: Durations.long4);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Save swipe status to database
|
|
||||||
Future<void> _saveSwipeAction(
|
|
||||||
String swipedUserId, SwipeDirection direction) async {
|
|
||||||
await _firestore
|
|
||||||
.collection(Constants.dbCollectionUsers)
|
|
||||||
.doc(_authService.getCurrentUser()!.uid)
|
|
||||||
.collection(Constants.dbCollectionSwipes)
|
|
||||||
.doc(swipedUserId) // UserID instead of autogenerated ID
|
|
||||||
.set(
|
|
||||||
{
|
|
||||||
'swipedId': swipedUserId,
|
|
||||||
'liked': direction == SwipeDirection.right,
|
|
||||||
'timestamp': FieldValue.serverTimestamp(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
UserProfile getUserProfile(String userId) {
|
|
||||||
return userProfiles.firstWhere((x) => x.uid == userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check whether the swiped user has also swiped to the right
|
|
||||||
Future<void> _checkForMatch(swipedUserId) async {
|
|
||||||
String currentUserId = _authService.getCurrentUser()!.uid;
|
|
||||||
|
|
||||||
final QuerySnapshot matchSnapshot = await _firestore
|
|
||||||
.collection(Constants.dbCollectionUsers)
|
|
||||||
.doc(swipedUserId)
|
|
||||||
.collection(Constants.dbCollectionSwipes)
|
|
||||||
.where('swipedId', isEqualTo: currentUserId)
|
|
||||||
.where('liked', isEqualTo: true)
|
|
||||||
.get();
|
|
||||||
|
|
||||||
if (matchSnapshot.docs.isNotEmpty) {
|
|
||||||
// save match for both users
|
|
||||||
final matchesCurrentUser = _firestore
|
|
||||||
.collection(Constants.dbCollectionUsers)
|
|
||||||
.doc(currentUserId)
|
|
||||||
.collection(Constants.dbCollectionMatches);
|
|
||||||
final matchesSwipedUser = _firestore
|
|
||||||
.collection(Constants.dbCollectionUsers)
|
|
||||||
.doc(swipedUserId)
|
|
||||||
.collection(Constants.dbCollectionMatches);
|
|
||||||
|
|
||||||
await matchesCurrentUser.add({
|
|
||||||
'otherUserId': swipedUserId,
|
|
||||||
'timestamp': FieldValue.serverTimestamp(),
|
|
||||||
});
|
|
||||||
|
|
||||||
await matchesSwipedUser.add({
|
|
||||||
'otherUserId': currentUserId,
|
|
||||||
'timestamp': FieldValue.serverTimestamp(),
|
|
||||||
});
|
|
||||||
|
|
||||||
//
|
|
||||||
// TODO Notify other user?
|
|
||||||
//
|
|
||||||
showMatchedScreen(currentUserId, swipedUserId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showMatchedScreen(String currentUserId, String 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,
|
|
||||||
onMessageButtonPressed: () {
|
|
||||||
Navigator.pop(context); // Close the MatchedScreen
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => ChatPage(
|
|
||||||
receiverEmail: swipedUser.email,
|
|
||||||
receiverID: swipedUser.uid,
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 16),
|
||||||
);
|
Text(userData[Constants.dbFieldUsersName] ?? 'Name',
|
||||||
},
|
style: const TextStyle(fontSize: 24)),
|
||||||
onContinueButtonPressed: () {
|
Text(userData[Constants.dbFieldUsersEmail] ?? 'Email',
|
||||||
Navigator.pop(context); // Close the MatchedScreen
|
style: const TextStyle(fontSize: 16)),
|
||||||
},
|
const SizedBox(height: 16),
|
||||||
),
|
Align(
|
||||||
),
|
alignment: Alignment.centerLeft,
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (potentialUserProfiles.isEmpty) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(title: const Text('User Profiles')),
|
|
||||||
body: Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.person_search, size: 64),
|
const Text('Bio'),
|
||||||
const SizedBox(height: 20),
|
Text(userData[Constants.dbFieldUsersBio] ?? 'N/A',
|
||||||
// Loading...
|
style: const TextStyle(fontSize: 16)),
|
||||||
if (userProfiles.isEmpty) ...[
|
|
||||||
const CircularProgressIndicator(),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
const Text(
|
|
||||||
'Loading data, please wait...',
|
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
|
||||||
),
|
|
||||||
] else ...[
|
|
||||||
Text(
|
|
||||||
userProfiles.length > 1
|
|
||||||
? 'No new profiles available yet.'
|
|
||||||
: 'No profiles available at the moment.',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 18,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 60),
|
|
||||||
const Text(
|
|
||||||
'Please check back later, perhaps tomorrow.',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Divider(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
);
|
const SizedBox(height: 16),
|
||||||
}
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(title: const Text('Find your Match')),
|
|
||||||
body: SafeArea(
|
|
||||||
top: false,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
Positioned.fill(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: SwipableStack(
|
|
||||||
detectableSwipeDirections: const {
|
|
||||||
SwipeDirection.right,
|
|
||||||
SwipeDirection.left,
|
|
||||||
SwipeDirection.up,
|
|
||||||
},
|
|
||||||
controller: _controller,
|
|
||||||
stackClipBehaviour: Clip.none,
|
|
||||||
swipeAnchor: SwipeAnchor.bottom,
|
|
||||||
// item count +1 for additional end page
|
|
||||||
itemCount: potentialUserProfiles.length + 1,
|
|
||||||
onSwipeCompleted: (index, direction) {
|
|
||||||
//
|
|
||||||
// Swipe logic
|
|
||||||
//
|
|
||||||
String swipedUserId =
|
|
||||||
potentialUserProfiles[_controller.currentIndex].id;
|
|
||||||
if (direction == SwipeDirection.right) {
|
|
||||||
_saveSwipeAction(swipedUserId, SwipeDirection.right);
|
|
||||||
_checkForMatch(swipedUserId);
|
|
||||||
} else if (direction == SwipeDirection.left) {
|
|
||||||
_saveSwipeAction(swipedUserId, SwipeDirection.left);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
horizontalSwipeThreshold: 0.8,
|
|
||||||
verticalSwipeThreshold: 0.8,
|
|
||||||
builder: (context, properties) {
|
|
||||||
if (properties.index == potentialUserProfiles.length) {
|
|
||||||
return Center(
|
|
||||||
child: _buildLastCard(), // last card
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final userProfile = potentialUserProfiles[
|
|
||||||
properties.index % potentialUserProfiles.length];
|
|
||||||
return Container(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(25.0),
|
|
||||||
color: Colors.blue,
|
|
||||||
),
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
_buildUserCard(userProfile),
|
|
||||||
if (properties.stackIndex == 0 &&
|
|
||||||
properties.direction != null)
|
|
||||||
CardOverlay(
|
|
||||||
swipeProgress: properties.swipeProgress,
|
|
||||||
direction: properties.direction!,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
_buildSwipeButtons(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildUserCard(UserProfile userProfile) {
|
|
||||||
return Card(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(userProfile.name, style: const TextStyle(fontSize: 24)),
|
|
||||||
Text(userProfile.email, style: const TextStyle(fontSize: 24)),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'Has skills and experience in: ${userProfile.skills.join(', ')}'),
|
|
||||||
Text(
|
|
||||||
'Seeks someone with skills in: ${userProfile.skillsSought.join(', ')}'),
|
|
||||||
Text('Risk type: ${userProfile.risk}'),
|
|
||||||
Text(
|
|
||||||
'Speaks: ${userProfile.languages.map((lang) => lang.name).join(', ')}'),
|
|
||||||
Text(
|
|
||||||
'Lives in: ${userProfile.locations[Constants.dbDocMainLocation]?.locality ?? 'N/A'}'),
|
|
||||||
Text(
|
|
||||||
'Second home: ${userProfile.locations[Constants.dbDocSecondLocation]?.locality ?? 'N/A'}'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildLastCard() {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
// Show reached end message and restart button
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.person_search, size: 64),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
const Text('You\'ve viewed all available profiles.',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
|
||||||
const SizedBox(height: 60),
|
|
||||||
const Text(
|
|
||||||
'Would you like to do another run and see the remaining profiles again?',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
// Restart swiping from the beginning
|
|
||||||
_controller.currentIndex = 0;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: const Text('Another run'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSwipeButtons() {
|
|
||||||
return Positioned(
|
|
||||||
bottom: 16,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
children: [
|
|
||||||
FloatingActionButton(
|
|
||||||
heroTag: 'myFABUndo',
|
|
||||||
tooltip: 'Undo last action',
|
|
||||||
shape: const CircleBorder(),
|
|
||||||
onPressed: () {
|
|
||||||
_controller.rewind(duration: Durations.extralong4);
|
|
||||||
},
|
|
||||||
child: const Icon(Icons.undo),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: 72,
|
|
||||||
height: 72,
|
|
||||||
child: FloatingActionButton(
|
|
||||||
heroTag: 'myFABSwipeLeft',
|
|
||||||
shape: const CircleBorder(),
|
|
||||||
onPressed: _swipeLeft,
|
|
||||||
child: Stack(
|
|
||||||
// to deal with icons inner transparency
|
|
||||||
alignment: Alignment.center,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 48.0,
|
|
||||||
height: 48.0,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Colors.white70,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Icon(Icons.cancel, color: Colors.red, size: 72),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: 72,
|
|
||||||
height: 72,
|
|
||||||
child: FloatingActionButton(
|
|
||||||
heroTag: 'myFABSwipeRight',
|
|
||||||
shape: const CircleBorder(),
|
|
||||||
onPressed: _swipeRight,
|
|
||||||
child: Stack(
|
|
||||||
// to deal with icons inner transparency
|
|
||||||
alignment: Alignment.center,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 48.0,
|
|
||||||
height: 48.0,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Colors.white70,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Icon(Icons.check_circle, color: Colors.green, size: 72),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
FloatingActionButton(
|
|
||||||
tooltip: 'Skip profile',
|
|
||||||
heroTag: 'myFABSkip',
|
|
||||||
shape: const CircleBorder(),
|
|
||||||
onPressed: _skip,
|
|
||||||
child: const Icon(Icons.skip_next),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CardOverlay extends StatelessWidget {
|
|
||||||
final double swipeProgress;
|
|
||||||
final SwipeDirection direction;
|
|
||||||
|
|
||||||
const CardOverlay({
|
|
||||||
super.key,
|
|
||||||
required this.swipeProgress,
|
|
||||||
required this.direction,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Positioned.fill(
|
|
||||||
bottom: 300,
|
|
||||||
child: Opacity(
|
|
||||||
opacity: swipeProgress.abs().clamp(0.0, 1.0),
|
|
||||||
child: Align(
|
|
||||||
alignment: direction == SwipeDirection.right
|
|
||||||
? Alignment.centerLeft
|
|
||||||
: (direction == SwipeDirection.left
|
|
||||||
? Alignment.centerRight
|
|
||||||
: Alignment.center),
|
|
||||||
child: Icon(
|
|
||||||
direction == SwipeDirection.right
|
|
||||||
? Icons.thumb_up
|
|
||||||
: (direction == SwipeDirection.left
|
|
||||||
? Icons.thumb_down
|
|
||||||
: Icons.skip_next),
|
|
||||||
size: 100,
|
|
||||||
color: direction == SwipeDirection.right
|
|
||||||
? Colors.green
|
|
||||||
: (direction == SwipeDirection.left ? Colors.red : Colors.blue),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,11 +2,14 @@ import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import '../constants.dart';
|
import '../constants.dart';
|
||||||
import '../enumerations.dart';
|
import '../enumerations.dart';
|
||||||
|
import '../models/language.dart';
|
||||||
|
import '../models/location.dart';
|
||||||
|
import '../models/user_profile.dart';
|
||||||
|
|
||||||
class UserService {
|
class UserService {
|
||||||
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
UserService._(); // Private constructor to prevent instantiation
|
||||||
|
|
||||||
Future<void> saveUserData(UserCredential userCredential, String email,
|
static Future<void> saveUserData(UserCredential userCredential, String email,
|
||||||
String firstname, String lastname) async {
|
String firstname, String lastname) async {
|
||||||
// create full name
|
// create full name
|
||||||
String fullName = (firstname.isNotEmpty && lastname.isNotEmpty)
|
String fullName = (firstname.isNotEmpty && lastname.isNotEmpty)
|
||||||
|
@ -16,7 +19,7 @@ class UserService {
|
||||||
: (lastname.isNotEmpty ? lastname : ''));
|
: (lastname.isNotEmpty ? lastname : ''));
|
||||||
|
|
||||||
// save user info to users document
|
// save user info to users document
|
||||||
await _firestore
|
await FirebaseFirestore.instance
|
||||||
.collection(Constants.dbCollectionUsers)
|
.collection(Constants.dbCollectionUsers)
|
||||||
.doc(userCredential.user!.uid)
|
.doc(userCredential.user!.uid)
|
||||||
.set(
|
.set(
|
||||||
|
@ -30,10 +33,10 @@ class UserService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<SkillOption>> getSkillsFromFirebase(
|
static Future<List<SkillOption>> getSkillsFromFirebase(
|
||||||
bool skillsSought, String userId) async {
|
bool skillsSought, String userId) async {
|
||||||
// Fetch skills from Firestore
|
// Fetch skills from Firestore
|
||||||
DocumentSnapshot userDoc = await _firestore
|
DocumentSnapshot userDoc = await FirebaseFirestore.instance
|
||||||
.collection(Constants.dbCollectionUsers)
|
.collection(Constants.dbCollectionUsers)
|
||||||
.doc(userId)
|
.doc(userId)
|
||||||
.get();
|
.get();
|
||||||
|
@ -63,7 +66,7 @@ class UserService {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> saveSkillsToFirebase(List<SkillOption> selectedOptions,
|
static Future<bool> saveSkillsToFirebase(List<SkillOption> selectedOptions,
|
||||||
bool skillsSought, String userId) async {
|
bool skillsSought, String userId) async {
|
||||||
try {
|
try {
|
||||||
// Convert enum values to strings, removing leading EnumType with split
|
// Convert enum values to strings, removing leading EnumType with split
|
||||||
|
@ -76,7 +79,7 @@ class UserService {
|
||||||
? Constants.dbFieldUsersSkillsSought
|
? Constants.dbFieldUsersSkillsSought
|
||||||
: Constants.dbFieldUsersSkills;
|
: Constants.dbFieldUsersSkills;
|
||||||
|
|
||||||
_firestore
|
FirebaseFirestore.instance
|
||||||
.collection(Constants.dbCollectionUsers)
|
.collection(Constants.dbCollectionUsers)
|
||||||
.doc(userId)
|
.doc(userId)
|
||||||
.update({keyToUpdate: skills});
|
.update({keyToUpdate: skills});
|
||||||
|
@ -86,4 +89,48 @@ class UserService {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get UserProfile for given [userId]
|
||||||
|
static Future<UserProfile> getUserProfileById(String userId) async {
|
||||||
|
DocumentSnapshot doc = await FirebaseFirestore.instance
|
||||||
|
.collection(Constants.dbCollectionUsers)
|
||||||
|
.doc(userId)
|
||||||
|
.get();
|
||||||
|
return UserProfile.fromDocument(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<UserProfile> getDataById(String userId) async {
|
||||||
|
FirebaseFirestore firestore = FirebaseFirestore.instance;
|
||||||
|
|
||||||
|
DocumentSnapshot userDoc = await firestore
|
||||||
|
.collection(Constants.dbCollectionUsers)
|
||||||
|
.doc(userId)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
QuerySnapshot languagesSnapshot = await firestore
|
||||||
|
.collection(Constants.dbCollectionUsers)
|
||||||
|
.doc(userId)
|
||||||
|
.collection(Constants.dbCollectionLanguages)
|
||||||
|
.get();
|
||||||
|
List<Language> languages = languagesSnapshot.docs
|
||||||
|
.map((doc) => Language.fromDocument(doc))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
QuerySnapshot locationsSnapshot = await firestore
|
||||||
|
.collection(Constants.dbCollectionUsers)
|
||||||
|
.doc(userId)
|
||||||
|
.collection(Constants.dbCollectionLocations)
|
||||||
|
.get();
|
||||||
|
Map<String, MyLocation?> locations = {
|
||||||
|
for (var doc in locationsSnapshot.docs)
|
||||||
|
doc.id: MyLocation.fromDocument(doc)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fill UserProfile including its sub collections
|
||||||
|
UserProfile userProfile = UserProfile.fromDocument(userDoc);
|
||||||
|
userProfile.languages.addAll(languages);
|
||||||
|
userProfile.locations.addAll(locations);
|
||||||
|
|
||||||
|
return userProfile;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
158
pubspec.lock
158
pubspec.lock
|
@ -5,10 +5,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _flutterfire_internals
|
name: _flutterfire_internals
|
||||||
sha256: "3dee3db3468c5f4640a4e8aa9c1e22561c298976d8c39ed2fdd456a9a3db26e1"
|
sha256: "37a42d06068e2fe3deddb2da079a8c4d105f241225ba27b7122b37e9865fd8f7"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.32"
|
version: "1.3.35"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -81,6 +81,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.18.0"
|
version: "1.18.0"
|
||||||
|
cross_file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cross_file
|
||||||
|
sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.4+1"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -105,6 +113,38 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.1"
|
||||||
|
file_selector_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_linux
|
||||||
|
sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.2+1"
|
||||||
|
file_selector_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_macos
|
||||||
|
sha256: f42eacb83b318e183b1ae24eead1373ab1334084404c8c16e0354f9a3e55d385
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.4"
|
||||||
|
file_selector_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_platform_interface
|
||||||
|
sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.6.2"
|
||||||
|
file_selector_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_windows
|
||||||
|
sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.3+1"
|
||||||
firebase_auth:
|
firebase_auth:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -133,10 +173,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: firebase_core
|
name: firebase_core
|
||||||
sha256: "4aef2a23d0f3265545807d68fbc2f76a6b994ca3c778d88453b99325abd63284"
|
sha256: "26de145bb9688a90962faec6f838247377b0b0d32cc0abecd9a4e43525fc856c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.30.1"
|
version: "2.32.0"
|
||||||
firebase_core_platform_interface:
|
firebase_core_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -149,10 +189,34 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_core_web
|
name: firebase_core_web
|
||||||
sha256: "67f2fcc600fc78c2f731c370a3a5e6c87ee862e3a2fba6f951eca6d5dafe5c29"
|
sha256: "43d9e951ac52b87ae9cc38ecdcca1e8fa7b52a1dd26a96085ba41ce5108db8e9"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.16.0"
|
version: "2.17.0"
|
||||||
|
firebase_storage:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: firebase_storage
|
||||||
|
sha256: "2ae478ceec9f458c1bcbf0ee3e0100e4e909708979e83f16d5d9fba35a5b42c1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.7.7"
|
||||||
|
firebase_storage_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: firebase_storage_platform_interface
|
||||||
|
sha256: "4e18662e6a66e2e0e181c06f94707de06d5097d70cfe2b5141bf64660c5b5da9"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.1.22"
|
||||||
|
firebase_storage_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: firebase_storage_web
|
||||||
|
sha256: "3a44aacd38a372efb159f6fe36bb4a7d79823949383816457fd43d3d47602a53"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.9.7"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -174,6 +238,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.2"
|
version: "3.0.2"
|
||||||
|
flutter_plugin_android_lifecycle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_plugin_android_lifecycle
|
||||||
|
sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.19"
|
||||||
flutter_svg:
|
flutter_svg:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -288,6 +360,70 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.2"
|
version: "4.0.2"
|
||||||
|
image_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: image_picker
|
||||||
|
sha256: "33974eca2e87e8b4e3727f1b94fa3abcb25afe80b6bc2c4d449a0e150aedf720"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
|
image_picker_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_android
|
||||||
|
sha256: "0f57fee1e8bfadf8cc41818bbcd7f72e53bb768a54d9496355d5e8a5681a19f1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.12+1"
|
||||||
|
image_picker_for_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_for_web
|
||||||
|
sha256: "5d6eb13048cd47b60dbf1a5495424dea226c5faf3950e20bf8120a58efb5b5f3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.4"
|
||||||
|
image_picker_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_ios
|
||||||
|
sha256: "4824d8c7f6f89121ef0122ff79bb00b009607faecc8545b86bca9ab5ce1e95bf"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.11+2"
|
||||||
|
image_picker_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_linux
|
||||||
|
sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.1+1"
|
||||||
|
image_picker_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_macos
|
||||||
|
sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.1+1"
|
||||||
|
image_picker_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_platform_interface
|
||||||
|
sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.10.0"
|
||||||
|
image_picker_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_windows
|
||||||
|
sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.1+1"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -344,6 +480,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.11.0"
|
version: "1.11.0"
|
||||||
|
mime:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: mime
|
||||||
|
sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.5"
|
||||||
nested:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -543,4 +687,4 @@ packages:
|
||||||
version: "6.5.0"
|
version: "6.5.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.3.3 <4.0.0"
|
dart: ">=3.3.3 <4.0.0"
|
||||||
flutter: ">=3.7.0-0"
|
flutter: ">=3.19.0"
|
||||||
|
|
|
@ -44,6 +44,8 @@ dependencies:
|
||||||
geocoding: ^3.0.0
|
geocoding: ^3.0.0
|
||||||
collection: ^1.18.0
|
collection: ^1.18.0
|
||||||
swipable_stack: ^2.0.0
|
swipable_stack: ^2.0.0
|
||||||
|
image_picker: ^1.1.1
|
||||||
|
firebase_storage: ^11.7.7
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
Loading…
Reference in New Issue