444 lines
16 KiB
Dart
444 lines
16 KiB
Dart
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 'chat_page.dart';
|
|
|
|
class UserProfilePage extends StatefulWidget {
|
|
const UserProfilePage({super.key});
|
|
|
|
@override
|
|
UserProfilePageState createState() => UserProfilePageState();
|
|
}
|
|
|
|
class UserProfilePageState extends State<UserProfilePage> {
|
|
List<UserProfile> userProfiles = [];
|
|
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 querySnapshot = await FirebaseFirestore.instance
|
|
.collection(Constants.dbCollectionUsers)
|
|
.get();
|
|
final users = await Future.wait(querySnapshot.docs.map((doc) async {
|
|
final languagesSnapshot =
|
|
await doc.reference.collection(Constants.dbCollectionLanguages).get();
|
|
final locationsSnapshot =
|
|
await doc.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 = doc.data();
|
|
return UserProfile(
|
|
id: doc.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,
|
|
);
|
|
}).toList());
|
|
|
|
setState(() {
|
|
userProfiles = users;
|
|
});
|
|
}
|
|
|
|
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.extralong4);
|
|
}
|
|
|
|
void _swipeRight() {
|
|
_controller.next(
|
|
swipeDirection: SwipeDirection.right, duration: Durations.extralong4);
|
|
}
|
|
|
|
void _skip() {
|
|
_controller.next(
|
|
swipeDirection: SwipeDirection.up, duration: Durations.extralong2);
|
|
}
|
|
|
|
/// 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 current user 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 Notification and further logic, e.g. initialization of the chat
|
|
//
|
|
print(
|
|
'We have a match between ${getUserProfile(currentUserId).name} and ${getUserProfile(swipedUserId).name}');
|
|
|
|
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
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (userProfiles.isEmpty) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('User Profiles')),
|
|
body: const Center(child: CircularProgressIndicator()),
|
|
);
|
|
}
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('User Profiles')),
|
|
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,
|
|
itemCount: userProfiles.length+1, // +1 for rerun option
|
|
onSwipeCompleted: (index, direction) {
|
|
//
|
|
// Swipe logic goes here
|
|
//
|
|
String swipedUserId =
|
|
userProfiles[_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,
|
|
/*overlayBuilder: (context, properties) {
|
|
final opacity = min(properties.swipeProgress, 1.0);
|
|
final isRight = properties.direction == SwipeDirection.right;
|
|
return Opacity(
|
|
opacity: isRight ? opacity : 0,
|
|
child: CardLabel.right(),
|
|
);
|
|
},*/
|
|
builder: (context, properties) {
|
|
|
|
if (properties.index == userProfiles.length) {
|
|
return Center(
|
|
child: Column( // Show end message and restart button
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Text('That\'s all.\nDo you want to do another run?',
|
|
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)
|
|
),
|
|
const SizedBox(height: 20),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
setState(() {
|
|
_controller.currentIndex = 0; // Restart swiping from the beginning
|
|
});
|
|
},
|
|
child: const Text('Rerun'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
final userProfile =
|
|
userProfiles[properties.index % userProfiles.length];
|
|
return Container(
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(25.0),
|
|
color: Colors.blue,
|
|
),
|
|
child: Stack(
|
|
children: [
|
|
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['main']?.locality ?? 'N/A'}'),
|
|
Text(
|
|
'Second home: ${userProfile.locations['secondary']?.locality ?? 'N/A'}'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
if (properties.stackIndex == 0 &&
|
|
properties.direction != null)
|
|
CardOverlay(
|
|
swipeProgress: properties.swipeProgress,
|
|
direction: properties.direction!,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
Positioned(
|
|
bottom: 16,
|
|
left: 0,
|
|
right: 0,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
FloatingActionButton(
|
|
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(
|
|
shape: const CircleBorder(),
|
|
onPressed: _swipeLeft,
|
|
child:
|
|
const Icon(Icons.cancel, color: Colors.red, size: 64),
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: 72,
|
|
height: 72,
|
|
child: FloatingActionButton(
|
|
shape: const CircleBorder(),
|
|
onPressed: _swipeRight,
|
|
child: const Icon(Icons.check_circle,
|
|
color: Colors.green, size: 64),
|
|
),
|
|
),
|
|
FloatingActionButton(
|
|
tooltip: 'Skip profile',
|
|
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.check_circle
|
|
: (direction == SwipeDirection.left
|
|
? Icons.cancel
|
|
: Icons.skip_next),
|
|
size: 100,
|
|
color: direction == SwipeDirection.right
|
|
? Colors.green
|
|
: (direction == SwipeDirection.left ? Colors.red : Colors.blue),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|