Added SkillsForm
parent
3fa52d3e85
commit
4e2ced6802
|
@ -15,6 +15,8 @@ class Constants {
|
||||||
|
|
||||||
static const String dbFieldUsersGender = 'gender';
|
static const String dbFieldUsersGender = 'gender';
|
||||||
static const String dbFieldUsersYearBorn = 'born';
|
static const String dbFieldUsersYearBorn = 'born';
|
||||||
|
static const String dbFieldUsersSkills = 'skills';
|
||||||
|
static const String dbFieldUsersSkillsSought = 'skills_sought';
|
||||||
|
|
||||||
static const String pathLanguagesJson = 'lib/assets/languages.json';
|
static const String pathLanguagesJson = 'lib/assets/languages.json';
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
enum SkillOption {
|
||||||
|
product,
|
||||||
|
finance,
|
||||||
|
engineering,
|
||||||
|
design,
|
||||||
|
marketing,
|
||||||
|
management,
|
||||||
|
operations,
|
||||||
|
legal;
|
||||||
|
|
||||||
|
String get displayName {
|
||||||
|
switch (this) {
|
||||||
|
case SkillOption.product:
|
||||||
|
return 'Product Development';
|
||||||
|
case SkillOption.design:
|
||||||
|
return 'Design';
|
||||||
|
case SkillOption.engineering:
|
||||||
|
return 'Engineering';
|
||||||
|
case SkillOption.marketing:
|
||||||
|
return 'Sales and Marketing';
|
||||||
|
case SkillOption.finance:
|
||||||
|
return 'Finance';
|
||||||
|
case SkillOption.management:
|
||||||
|
return 'Management';
|
||||||
|
case SkillOption.operations:
|
||||||
|
return 'Operations';
|
||||||
|
case SkillOption.legal:
|
||||||
|
return 'Legal';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,153 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../helper.dart';
|
||||||
|
|
||||||
|
class ProfileCategoryForm<T> extends StatefulWidget {
|
||||||
|
final String title;
|
||||||
|
final String description;
|
||||||
|
final List<T> options; // T to make it work with our different Enums
|
||||||
|
final int minSelections;
|
||||||
|
final int maxSelections;
|
||||||
|
final Function(List<T>) onSave;
|
||||||
|
final List<T> preSelectedOptions;
|
||||||
|
final String? saveButtonText;
|
||||||
|
final bool hideSaveButton;
|
||||||
|
|
||||||
|
const ProfileCategoryForm({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.description,
|
||||||
|
required this.options,
|
||||||
|
required this.minSelections,
|
||||||
|
required this.maxSelections,
|
||||||
|
required this.onSave,
|
||||||
|
required this.preSelectedOptions,
|
||||||
|
this.saveButtonText,
|
||||||
|
this.hideSaveButton = false, // Initialize hideSaveButton parameter
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ProfileCategoryFormState<T> createState() => ProfileCategoryFormState<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProfileCategoryFormState<T> extends State<ProfileCategoryForm<T>> {
|
||||||
|
late List<T> _selectedOptions = [];
|
||||||
|
bool _isSnackBarVisible = false; // Track SnackBar visibility
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_selectedOptions = List.from(widget.preSelectedOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(widget.title),
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${widget.title}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18.0,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16,),
|
||||||
|
Text(
|
||||||
|
'${widget.description}',
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16,),
|
||||||
|
Wrap(
|
||||||
|
spacing: 8.0,
|
||||||
|
children: List.generate(widget.options.length, (index) {
|
||||||
|
final option = widget.options[index];
|
||||||
|
return ChoiceChip(
|
||||||
|
label: Text(_getDisplayText(option)),
|
||||||
|
selected: _selectedOptions.contains(option),
|
||||||
|
onSelected: (selected) {
|
||||||
|
setState(() {
|
||||||
|
if (selected) {
|
||||||
|
if (_selectedOptions.length < widget.maxSelections) {
|
||||||
|
_selectedOptions.add(option);
|
||||||
|
} else {
|
||||||
|
// Provide feedback that maximum selections reached
|
||||||
|
_showSnackBar(
|
||||||
|
'Maximum selections reached for ${widget.title}');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_selectedOptions.remove(option);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16.0),
|
||||||
|
if (!widget.hideSaveButton)
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (!equalContent(
|
||||||
|
_selectedOptions, widget.preSelectedOptions)) {
|
||||||
|
// Only save if options have changed
|
||||||
|
if (_selectedOptions.length >= widget.minSelections) {
|
||||||
|
widget.onSave(_selectedOptions);
|
||||||
|
// TODO Navigation abhängig von caller
|
||||||
|
// Navigator.pop(context); // Navigate back after saving
|
||||||
|
} else {
|
||||||
|
// Provide feedback that minimum selections not met
|
||||||
|
_showSnackBar('No selection made for ${widget.title}');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print('No changes to save for ${widget.title}.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(widget.saveButtonText ?? 'Save'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getDisplayText(dynamic option) {
|
||||||
|
// Check if the option is an enum and has a displayName property
|
||||||
|
if (option is Enum) {
|
||||||
|
final dynamicEnum = option as dynamic;
|
||||||
|
if (dynamicEnum.displayName != null) {
|
||||||
|
return dynamicEnum.displayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback to default toString if not an enum
|
||||||
|
return option.toString().split('.').last;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showSnackBar(String message) {
|
||||||
|
if (!_isSnackBarVisible) {
|
||||||
|
_isSnackBarVisible = true;
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
//duration: Duration(seconds: 2),
|
||||||
|
action: SnackBarAction(
|
||||||
|
label: 'Dismiss',
|
||||||
|
onPressed: () {
|
||||||
|
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||||
|
_isSnackBarVisible = false;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.closed
|
||||||
|
.then((value) {
|
||||||
|
_isSnackBarVisible = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
import '../../constants.dart';
|
||||||
|
import '../../enumerations.dart';
|
||||||
|
import '../../services/auth/auth_service.dart';
|
||||||
|
import 'profile_category_form.dart';
|
||||||
|
|
||||||
|
class SkillsForm extends StatelessWidget {
|
||||||
|
SkillsForm({super.key, required this.skillsSought});
|
||||||
|
|
||||||
|
// get instance of firestore and auth
|
||||||
|
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||||
|
final AuthService _authService = AuthService();
|
||||||
|
|
||||||
|
final bool skillsSought; // flag to toggle offered and sought skills
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder<List<SkillOption>>(
|
||||||
|
future: getSkillsFromFirebase(), // Fetch skills from Firebase
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
// Show loading indicator while fetching data
|
||||||
|
return const CircularProgressIndicator();
|
||||||
|
} else if (snapshot.hasError) {
|
||||||
|
return Text('Error: ${snapshot.error}');
|
||||||
|
} else {
|
||||||
|
List<SkillOption>? userSkills = snapshot.data;
|
||||||
|
return ProfileCategoryForm(
|
||||||
|
title: 'Skills',
|
||||||
|
description: skillsSought
|
||||||
|
? 'Choose up to 3 areas you are looking for in a cofounder'
|
||||||
|
: 'Select up to 3 areas in which you are skilled',
|
||||||
|
options: SkillOption.values.toList(), // Convert enum values to list
|
||||||
|
minSelections: 1,
|
||||||
|
maxSelections: 3,
|
||||||
|
preSelectedOptions:
|
||||||
|
userSkills ?? [], // Pass pre-selected skills to the form
|
||||||
|
onSave: (selectedOptions) {
|
||||||
|
// Handle saving selected options
|
||||||
|
saveSkillsToFirebase(selectedOptions.cast<SkillOption>());
|
||||||
|
// Then navigate to another screen or perform any other action???
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<SkillOption>> getSkillsFromFirebase() async {
|
||||||
|
// Fetch skills from Firestore
|
||||||
|
String currentUserId = _authService.getCurrentUser()!.uid;
|
||||||
|
DocumentSnapshot userDoc = await _firestore
|
||||||
|
.collection(Constants.dbCollectionUsers)
|
||||||
|
.doc(currentUserId)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (userDoc.exists && userDoc.data() != null) {
|
||||||
|
Map<String, dynamic> userData =
|
||||||
|
userDoc.data()! as Map<String, dynamic>; // Explicit cast
|
||||||
|
|
||||||
|
List<dynamic>? skills;
|
||||||
|
if (skillsSought) {
|
||||||
|
skills = userData[Constants.dbFieldUsersSkillsSought];
|
||||||
|
} else {
|
||||||
|
skills = userData[
|
||||||
|
Constants.dbFieldUsersSkills]; //as List<dynamic>?; // Explicit cast
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skills != null && skills.isNotEmpty) {
|
||||||
|
// Convert skills from strings to enum values
|
||||||
|
List<SkillOption> userSkills = skills
|
||||||
|
.map((skill) => SkillOption.values
|
||||||
|
.firstWhere((x) => x.toString() == 'SkillOption.$skill'))
|
||||||
|
.toList();
|
||||||
|
return userSkills;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveSkillsToFirebase(List<SkillOption> selectedOptions) {
|
||||||
|
String currentUserId = _authService.getCurrentUser()!.uid;
|
||||||
|
|
||||||
|
// Convert enum values to strings, removing leading EnumType with split
|
||||||
|
List<String> skills = selectedOptions
|
||||||
|
.map((option) => option.toString().split('.').last)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// Update the corresponding 'skills' field in the user's document
|
||||||
|
String keyToUpdate = skillsSought
|
||||||
|
? Constants.dbFieldUsersSkillsSought
|
||||||
|
: Constants.dbFieldUsersSkills;
|
||||||
|
|
||||||
|
_firestore
|
||||||
|
.collection(Constants.dbCollectionUsers)
|
||||||
|
.doc(currentUserId)
|
||||||
|
.update({
|
||||||
|
keyToUpdate: skills,
|
||||||
|
}).then((_) {
|
||||||
|
print('$keyToUpdate saved to Firebase: $skills');
|
||||||
|
}).catchError((error) {
|
||||||
|
print('Failed to save $keyToUpdate: $error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,15 @@
|
||||||
/// Convert decimal coordinate to degrees minutes seconds (DMS)
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Compare two lists by their content ignoring their elements order.
|
||||||
|
///
|
||||||
|
bool equalContent(List<dynamic> list1, List<dynamic> list2) {
|
||||||
|
return const DeepCollectionEquality.unordered().equals(list1, list2);
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Convert decimal coordinate to degrees minutes seconds (DMS).
|
||||||
|
///
|
||||||
String convertDecimalToDMS(double decimalValue) {
|
String convertDecimalToDMS(double decimalValue) {
|
||||||
bool isNegative = decimalValue < 0;
|
bool isNegative = decimalValue < 0;
|
||||||
double absoluteValue = decimalValue.abs();
|
double absoluteValue = decimalValue.abs();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:cofounderella/constants.dart';
|
import '../constants.dart';
|
||||||
import 'package:cofounderella/services/auth/auth_gate.dart';
|
import '../services/auth/auth_gate.dart';
|
||||||
import 'package:cofounderella/themes/theme_provider.dart';
|
import '../themes/theme_provider.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
|
@ -74,7 +74,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.12.2"
|
version: "3.12.2"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||||
|
|
|
@ -42,6 +42,7 @@ dependencies:
|
||||||
flutter_svg: ^2.0.10+1
|
flutter_svg: ^2.0.10+1
|
||||||
geolocator: ^11.0.0
|
geolocator: ^11.0.0
|
||||||
geocoding: ^3.0.0
|
geocoding: ^3.0.0
|
||||||
|
collection: ^1.18.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
Loading…
Reference in New Issue