Swipe able cards (a first draft)
parent
40581f66da
commit
95b1df39da
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import '../pages/home_page.dart';
|
||||
import '../pages/user_data_page.dart';
|
||||
import '../pages/settings_page.dart';
|
||||
import '../pages/user_profile_page.dart';
|
||||
import '../services/auth/auth_service.dart';
|
||||
import 'feedback_dialog.dart';
|
||||
|
||||
|
@ -57,7 +58,18 @@ class MyDrawer extends StatelessWidget {
|
|||
child: ListTile(
|
||||
title: const Text("Find Matches"),
|
||||
leading: const Icon(Icons.person_search),
|
||||
onTap: () {}, // TODO
|
||||
onTap: () {
|
||||
// pop the drawer
|
||||
Navigator.pop(context);
|
||||
// Navigate to UserProfile
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) =>
|
||||
const UserProfilePage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
|
|
|
@ -14,6 +14,20 @@ class Constants {
|
|||
static const String dbDocMainLocation = 'main';
|
||||
static const String dbDocSecondLocation = 'secondary';
|
||||
|
||||
static const String dbFieldLocationLatitude = 'latitude';
|
||||
static const String dbFieldLocationLongitude = 'longitude';
|
||||
static const String dbFieldLocationStreet = 'street';
|
||||
static const String dbFieldLocationCountry = 'country';
|
||||
static const String dbFieldLocationArea = 'administrativeArea';
|
||||
static const String dbFieldLocationLocality = 'locality';
|
||||
static const String dbFieldLocationSubLocality = 'subLocality';
|
||||
static const String dbFieldLocationPostalCode = 'postalCode';
|
||||
|
||||
static const String dbFieldUsersID = 'uid';
|
||||
static const String dbFieldUsersEmail = 'email';
|
||||
static const String dbFieldUsersName = 'name';
|
||||
static const String dbFieldUsersFirstName = 'firstname';
|
||||
static const String dbFieldUsersLastName = 'lastname';
|
||||
static const String dbFieldUsersGender = 'gender';
|
||||
static const String dbFieldUsersYearBorn = 'born';
|
||||
static const String dbFieldUsersSkills = 'skills';
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import '../constants.dart';
|
||||
import '../helper.dart';
|
||||
|
||||
class MyLocation {
|
||||
|
@ -28,14 +29,14 @@ class MyLocation {
|
|||
// convert to a map
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'street': street,
|
||||
'country': country,
|
||||
'administrativeArea': administrativeArea,
|
||||
'locality': locality,
|
||||
'subLocality': subLocality,
|
||||
'postalCode': postalCode,
|
||||
'latitude': latitude,
|
||||
'longitude': longitude,
|
||||
Constants.dbFieldLocationStreet: street,
|
||||
Constants.dbFieldLocationCountry: country,
|
||||
Constants.dbFieldLocationArea: administrativeArea,
|
||||
Constants.dbFieldLocationLocality: locality,
|
||||
Constants.dbFieldLocationSubLocality: subLocality,
|
||||
Constants.dbFieldLocationPostalCode: postalCode,
|
||||
Constants.dbFieldLocationLatitude: latitude,
|
||||
Constants.dbFieldLocationLongitude: longitude,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -51,8 +52,8 @@ class MyLocation {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
/// Returns: locality, country
|
||||
@override
|
||||
String toString() {
|
||||
return '$locality, $country';
|
||||
}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
import '../constants.dart';
|
||||
import 'language.dart';
|
||||
import 'location.dart';
|
||||
|
||||
class UserProfile {
|
||||
final String id;
|
||||
final String uid;
|
||||
final String email;
|
||||
final String name;
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
final String risk;
|
||||
final List<String> skills;
|
||||
final List<String> skillsSought;
|
||||
final List<Language> languages;
|
||||
final Map<String, MyLocation?> locations;
|
||||
|
||||
UserProfile({
|
||||
required this.id,
|
||||
required this.uid,
|
||||
required this.email,
|
||||
required this.name,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
required this.risk,
|
||||
required this.skills,
|
||||
required this.skillsSought,
|
||||
required this.languages,
|
||||
required this.locations,
|
||||
});
|
||||
|
||||
factory UserProfile.fromDocument(DocumentSnapshot doc) {
|
||||
Map<String, dynamic> data = doc.data() as Map<String, dynamic>;
|
||||
|
||||
return UserProfile(
|
||||
id: doc.id,
|
||||
email: data[Constants.dbFieldUsersEmail] ?? '',
|
||||
name: data[Constants.dbFieldUsersName] ?? '',
|
||||
firstName: data[Constants.dbFieldUsersFirstName] ?? '',
|
||||
lastName: data[Constants.dbFieldUsersLastName] ?? '',
|
||||
uid: data[Constants.dbFieldUsersID] ?? '',
|
||||
skills: List<String>.from(data[Constants.dbFieldUsersSkills] ?? []),
|
||||
skillsSought:
|
||||
List<String>.from(data[Constants.dbFieldUsersSkillsSought] ?? []),
|
||||
risk: data[Constants.dbFieldUsersRiskTolerance] ?? '',
|
||||
languages: [],
|
||||
locations: {},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,308 @@
|
|||
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 '../models/language.dart';
|
||||
import '../models/location.dart';
|
||||
import '../models/user_profile.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;
|
||||
|
||||
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() {
|
||||
_saveSwipeAction(userProfiles[_controller.currentIndex].id, 'dislike');
|
||||
_controller.next(
|
||||
swipeDirection: SwipeDirection.left, duration: Durations.extralong4);
|
||||
}
|
||||
|
||||
void _swipeRight() {
|
||||
_saveSwipeAction(userProfiles[_controller.currentIndex].id, 'like');
|
||||
_controller.next(
|
||||
swipeDirection: SwipeDirection.right, duration: Durations.extralong4);
|
||||
}
|
||||
|
||||
void _skip() {
|
||||
_controller.next(
|
||||
swipeDirection: SwipeDirection.up, duration: Durations.extralong2);
|
||||
}
|
||||
|
||||
void _saveSwipeAction(String userId, String action) {
|
||||
/* FirebaseFirestore.instance.collection('swipes').add({
|
||||
'userId': userId,
|
||||
'action': action,
|
||||
'timestamp': FieldValue.serverTimestamp(),
|
||||
});*/
|
||||
}
|
||||
|
||||
@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,
|
||||
onSwipeCompleted: (index, direction) {
|
||||
if (index >= userProfiles.length) {
|
||||
setState(() {
|
||||
_controller.currentIndex = 0; // again from the start
|
||||
});
|
||||
}
|
||||
},
|
||||
horizontalSwipeThreshold: 0.8,
|
||||
verticalSwipeThreshold: 0.8,
|
||||
builder: (context, properties) {
|
||||
final userProfile =
|
||||
userProfiles[properties.index % userProfiles.length];
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
color: Colors.tealAccent,
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -21,11 +21,11 @@ class UserService {
|
|||
.doc(userCredential.user!.uid)
|
||||
.set(
|
||||
{
|
||||
'uid': userCredential.user!.uid,
|
||||
'email': email,
|
||||
'firstname': firstname,
|
||||
'lastname': lastname,
|
||||
'name': fullName,
|
||||
Constants.dbFieldUsersID: userCredential.user!.uid,
|
||||
Constants.dbFieldUsersEmail: email,
|
||||
Constants.dbFieldUsersFirstName: firstname,
|
||||
Constants.dbFieldUsersLastName: lastname,
|
||||
Constants.dbFieldUsersName: fullName,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
16
pubspec.lock
16
pubspec.lock
|
@ -413,6 +413,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
sprung:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sprung
|
||||
sha256: "54322638f5e393d2b808175f7eadbaa4836a4425456e98d93c3d676dc56ebdf1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -437,6 +445,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
swipable_stack:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: swipable_stack
|
||||
sha256: b04eef070455e868b68fdd5ae98f6718e561c877348acd017c07805f1439ef19
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -43,6 +43,7 @@ dependencies:
|
|||
geolocator: ^11.0.0
|
||||
geocoding: ^3.0.0
|
||||
collection: ^1.18.0
|
||||
swipable_stack: ^2.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
Loading…
Reference in New Issue