LinkedIn, Facebook and Xing links added in user profile.

master
Rafael 2024-06-24 04:41:17 +02:00
parent b5aa8d93fb
commit 7b82c1cbb8
11 changed files with 276 additions and 28 deletions

View File

@ -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),
),
);
}
}

View File

@ -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';

View File

@ -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,

View File

@ -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'),

View File

@ -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) {

View File

@ -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,

View File

@ -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],

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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:

View File

@ -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: