LinkedIn, Facebook and Xing links added in user profile.
parent
b5aa8d93fb
commit
7b82c1cbb8
|
@ -0,0 +1,59 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' show Clipboard, ClipboardData;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../utils/helper_dialogs.dart';
|
||||
|
||||
class ExternalLinkWidget extends StatelessWidget {
|
||||
final String url;
|
||||
final BuildContext context;
|
||||
|
||||
const ExternalLinkWidget(
|
||||
{required this.url, required this.context, super.key});
|
||||
|
||||
Future<void> _onTap() async {
|
||||
bool? confirm = await showConfirmationDialog(
|
||||
context,
|
||||
'External Link',
|
||||
'You are about to open an external link. '
|
||||
'Please make sure you trust the link before proceeding.\n'
|
||||
'$url\n'
|
||||
'Do you want to open this external link?\n\n'
|
||||
'Hint: Long press to copy the link to your clipboard instead of opening it.',
|
||||
);
|
||||
if (confirm == true) {
|
||||
Uri weblink = Uri.parse(url);
|
||||
if (!await launchUrl(weblink)) {
|
||||
if (context.mounted) {
|
||||
showErrorSnackBar(context, 'Could not launch $weblink');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onLongPress() {
|
||||
_copyToClipboard(context, url);
|
||||
}
|
||||
|
||||
void _copyToClipboard(BuildContext context, String text) {
|
||||
Clipboard.setData(ClipboardData(text: text)).then((_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Copied $text to clipboard')),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: _onTap,
|
||||
onLongPress: _onLongPress,
|
||||
child: Text(
|
||||
url,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(color: Colors.blue),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -58,6 +58,9 @@ class Constants {
|
|||
static const String dbFieldUsersCommunication = 'communication';
|
||||
static const String dbFieldUsersRiskTolerance = 'risk_tolerance';
|
||||
static const String dbFieldUsersSectors = 'sectors_of_interest';
|
||||
static const String dbFieldUsersUrlFacebook = 'url_facebook';
|
||||
static const String dbFieldUsersUrlLinkedIn = 'url_linkedin';
|
||||
static const String dbFieldUsersUrlXing = 'url_xing';
|
||||
|
||||
static const String dbStoragePathProfiles = 'profile_images';
|
||||
|
||||
|
|
|
@ -10,6 +10,9 @@ class UserProfile {
|
|||
final String firstName;
|
||||
final String lastName;
|
||||
String? profilePictureUrl;
|
||||
String? urlFacebook;
|
||||
String? urlLinkedIn;
|
||||
String? urlXing;
|
||||
String? bio;
|
||||
Gender? gender;
|
||||
int? born;
|
||||
|
@ -33,6 +36,9 @@ class UserProfile {
|
|||
required this.firstName,
|
||||
required this.lastName,
|
||||
this.profilePictureUrl,
|
||||
this.urlFacebook,
|
||||
this.urlLinkedIn,
|
||||
this.urlXing,
|
||||
this.bio,
|
||||
this.gender,
|
||||
this.born,
|
||||
|
|
|
@ -10,6 +10,7 @@ import 'dart:typed_data';
|
|||
|
||||
import '../constants.dart';
|
||||
import '../models/user_profile.dart';
|
||||
import '../utils/helper.dart';
|
||||
|
||||
class EditProfilePage extends StatefulWidget {
|
||||
final UserProfile userData;
|
||||
|
@ -24,6 +25,9 @@ class EditProfilePageState extends State<EditProfilePage> {
|
|||
final _formKey = GlobalKey<FormState>();
|
||||
late TextEditingController _nameController;
|
||||
late TextEditingController _bioController;
|
||||
late TextEditingController _urlFbController;
|
||||
late TextEditingController _urlLnController;
|
||||
late TextEditingController _urlXiController;
|
||||
String? profileImageUrl;
|
||||
io.File? _profileImage; // for android/ios
|
||||
Uint8List? _webProfileImage; // for web
|
||||
|
@ -33,6 +37,9 @@ class EditProfilePageState extends State<EditProfilePage> {
|
|||
super.initState();
|
||||
_nameController = TextEditingController(text: widget.userData.name);
|
||||
_bioController = TextEditingController(text: widget.userData.bio);
|
||||
_urlFbController = TextEditingController(text: widget.userData.urlFacebook);
|
||||
_urlLnController = TextEditingController(text: widget.userData.urlLinkedIn);
|
||||
_urlXiController = TextEditingController(text: widget.userData.urlXing);
|
||||
if (widget.userData.profilePictureUrl != null) {
|
||||
profileImageUrl = widget.userData.profilePictureUrl;
|
||||
}
|
||||
|
@ -135,6 +142,9 @@ class EditProfilePageState extends State<EditProfilePage> {
|
|||
Constants.dbFieldUsersName: nameTrim,
|
||||
Constants.dbFieldUsersBio: bioTrim,
|
||||
Constants.dbFieldUsersProfilePic: profileImageUrl,
|
||||
Constants.dbFieldUsersUrlFacebook: _urlFbController.text.trim(),
|
||||
Constants.dbFieldUsersUrlLinkedIn: _urlLnController.text.trim(),
|
||||
Constants.dbFieldUsersUrlXing: _urlXiController.text.trim(),
|
||||
};
|
||||
|
||||
await FirebaseFirestore.instance
|
||||
|
@ -152,6 +162,9 @@ class EditProfilePageState extends State<EditProfilePage> {
|
|||
Constants.dbFieldUsersProfilePic: map[Constants.dbFieldUsersProfilePic],
|
||||
Constants.dbFieldUsersName: map[Constants.dbFieldUsersName],
|
||||
Constants.dbFieldUsersBio: map[Constants.dbFieldUsersBio],
|
||||
Constants.dbFieldUsersUrlFacebook: map[Constants.dbFieldUsersUrlFacebook],
|
||||
Constants.dbFieldUsersUrlLinkedIn: map[Constants.dbFieldUsersUrlLinkedIn],
|
||||
Constants.dbFieldUsersUrlXing: map[Constants.dbFieldUsersUrlXing],
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -217,6 +230,32 @@ class EditProfilePageState extends State<EditProfilePage> {
|
|||
maxLength: 4096,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _urlFbController,
|
||||
decoration:
|
||||
const InputDecoration(labelText: 'Facebook Profile'),
|
||||
validator: (value) {
|
||||
return validateUrlFacebook(value);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _urlLnController,
|
||||
decoration:
|
||||
const InputDecoration(labelText: 'LinkedIn Profile'),
|
||||
validator: (value) {
|
||||
return validateUrlLinkedIn(value);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _urlXiController,
|
||||
decoration: const InputDecoration(labelText: 'Xing Profile'),
|
||||
validator: (value) {
|
||||
return validateUrlXing(value);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: _saveProfile,
|
||||
child: const Text('Save'),
|
||||
|
|
|
@ -104,31 +104,6 @@ class LikedUsersPageState extends State<LikedUsersPage> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<bool?> _showConfirmationDialog(String userId, String userName) async {
|
||||
bool? confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Confirm Removal'),
|
||||
content:
|
||||
Text('Are you sure you want to remove $userName from your list?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text('Confirm'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return confirm;
|
||||
}
|
||||
|
||||
List<MapEntry<Swipe, DocumentSnapshot>> _getSortedLikedUsersWithSwipes() {
|
||||
List<MapEntry<Swipe, DocumentSnapshot>> likedOnlyUsers = [];
|
||||
List<MapEntry<Swipe, DocumentSnapshot>> matchedUsers = [];
|
||||
|
@ -320,9 +295,10 @@ class LikedUsersPageState extends State<LikedUsersPage> {
|
|||
user.data() as Map<String, dynamic>;
|
||||
bool hasName = userMap.containsKey(Constants.dbFieldUsersName);
|
||||
|
||||
bool? confirm = await _showConfirmationDialog(
|
||||
user.id,
|
||||
(hasName ? user[Constants.dbFieldUsersName] : 'Name: n/a'),
|
||||
bool? confirm = await showConfirmationDialog(
|
||||
context,
|
||||
'Confirm Removal',
|
||||
'Are you sure you want to remove ${(hasName ? user[Constants.dbFieldUsersName] : 'Name: n/a')} from your list?',
|
||||
);
|
||||
|
||||
if (confirm == true) {
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'package:expandable_text/expandable_text.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
|
||||
import '../components/external_link_widget.dart';
|
||||
import '../components/language_list.dart';
|
||||
import '../constants.dart';
|
||||
import '../enumerations.dart';
|
||||
|
@ -73,6 +74,9 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
|||
updatedUserData[Constants.dbFieldUsersProfilePic];
|
||||
myData.name = updatedUserData[Constants.dbFieldUsersName];
|
||||
myData.bio = updatedUserData[Constants.dbFieldUsersBio];
|
||||
myData.urlFacebook = updatedUserData[Constants.dbFieldUsersUrlFacebook];
|
||||
myData.urlLinkedIn = updatedUserData[Constants.dbFieldUsersUrlLinkedIn];
|
||||
myData.urlXing = updatedUserData[Constants.dbFieldUsersUrlXing];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -318,6 +322,12 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
|||
],
|
||||
),
|
||||
if (isOwner) Text(myData.email, style: const TextStyle(fontSize: 16)),
|
||||
if (myData.urlXing != null && myData.urlXing!.isNotEmpty)
|
||||
ExternalLinkWidget(url: myData.urlXing!, context: context),
|
||||
if (myData.urlLinkedIn != null && myData.urlLinkedIn!.isNotEmpty)
|
||||
ExternalLinkWidget(url: myData.urlLinkedIn!, context: context),
|
||||
if (myData.urlFacebook != null && myData.urlFacebook!.isNotEmpty)
|
||||
ExternalLinkWidget(url: myData.urlFacebook!, context: context),
|
||||
const SizedBox(height: 32),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
|
|
|
@ -206,6 +206,9 @@ class UserService {
|
|||
communication: communication,
|
||||
workValues: works,
|
||||
profilePictureUrl: data[Constants.dbFieldUsersProfilePic],
|
||||
urlFacebook: data[Constants.dbFieldUsersUrlFacebook],
|
||||
urlLinkedIn: data[Constants.dbFieldUsersUrlLinkedIn],
|
||||
urlXing: data[Constants.dbFieldUsersUrlXing],
|
||||
bio: data[Constants.dbFieldUsersBio],
|
||||
gender: Gender.values[data[Constants.dbFieldUsersGender] ?? 0],
|
||||
born: data[Constants.dbFieldUsersYearBorn],
|
||||
|
|
|
@ -100,3 +100,63 @@ String getDisplayText(dynamic option) {
|
|||
// Fallback to default toString if not an enum
|
||||
return option.toString().split('.').last;
|
||||
}
|
||||
|
||||
/// Returns null if the given [value] is a valid Facebook Url,
|
||||
/// else an error message is returned.
|
||||
String? validateUrlFacebook(String? value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return null;
|
||||
}
|
||||
if (!value.startsWith('http:') && !value.startsWith('https:')) {
|
||||
return 'Link does not start with http(s):';
|
||||
}
|
||||
RegExp regex1 = RegExp(
|
||||
r'^(?:https?://)?(?:www\.)?facebook.com/(?:profile\.php\?id=)?[0-9]+$');
|
||||
RegExp regex2 = RegExp(
|
||||
r'^(?:https?://)?(?:www\.)?(?:facebook|fb)\.com/(?![A-z]+\.php)(?!marketplace|gaming|watch|me|messages|help|search|groups)[A-z0-9_\-.]+/?$');
|
||||
if (!regex1.hasMatch(value) && !regex2.hasMatch(value)) {
|
||||
return 'Unknown or invalid Facebook URL format.';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns null if the given [value] is a valid LinkedIn Url,
|
||||
/// else an error message is returned.
|
||||
String? validateUrlLinkedIn(String? value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return null; // ok, nothing to validate
|
||||
}
|
||||
if (!value.startsWith('http:') && !value.startsWith('https:')) {
|
||||
return 'Link does not start with http(s):';
|
||||
}
|
||||
// RegEx source https://github.com/lorey/social-media-profiles-regexs?tab=readme-ov-file#linkedin
|
||||
RegExp regex1 = RegExp(
|
||||
r'^(?:https?://)?(?:\w+\.)?linkedin\.com/(?:(company)|(school))/[A-z0-9-À-ÿ.]+/?$');
|
||||
RegExp regex2 = RegExp(
|
||||
r'^(?:https?://)?(?:\w+\.)?linkedin\.com/feed/update/urn:li:activity:[0-9]+/?$');
|
||||
RegExp regex3 =
|
||||
RegExp(r'^(?:https?://)?(?:\w+\.)?linkedin\.com/in/[\w\-_À-ÿ%]+/?$');
|
||||
if (!regex1.hasMatch(value) &&
|
||||
!regex2.hasMatch(value) &&
|
||||
!regex3.hasMatch(value)) {
|
||||
return 'Unknown or invalid LinkedIn URL format.';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns null if the given [value] is a valid Xing Url,
|
||||
/// else an error message is returned.
|
||||
String? validateUrlXing(String? value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return null;
|
||||
}
|
||||
if (!value.startsWith('http:') && !value.startsWith('https:')) {
|
||||
return 'Link does not start with http(s):';
|
||||
}
|
||||
RegExp regex =
|
||||
RegExp(r'^(?:https?://)?(?:www\.)?xing.com/profile/[A-z0-9-_]+$');
|
||||
if (!regex.hasMatch(value)) {
|
||||
return 'Unknown or invalid Xing URL format.';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -41,3 +41,30 @@ void showErrorSnackBar(BuildContext context, String message) {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Show a confirmation dialog and return [true] on confirm,
|
||||
/// [false] on cancel, or null otherwise.
|
||||
Future<bool?> showConfirmationDialog(
|
||||
BuildContext context, String title, String content) async {
|
||||
bool? confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(content),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text('Confirm'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return confirm;
|
||||
}
|
||||
|
|
64
pubspec.lock
64
pubspec.lock
|
@ -829,6 +829,70 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.3"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -30,6 +30,7 @@ dependencies:
|
|||
expandable_text: ^2.3.0
|
||||
shared_preferences: ^2.2.3
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
url_launcher: ^6.3.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
Loading…
Reference in New Issue