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 { List 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 _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.from(data[Constants.dbFieldUsersSkills] ?? []), skillsSought: List.from(data[Constants.dbFieldUsersSkillsSought] ?? []), risk: data[Constants.dbFieldUsersRiskTolerance] ?? '', languages: languages, locations: locations, ); }).toList()); setState(() { userProfiles = users; }); } MyLocation? _createLocationFromDoc(Map? 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 _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 _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), ), ), ), ); } }