diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 12d4d82..2b6a435 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,9 @@ + + + APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) + NSPhotoLibraryUsageDescription + This app needs access to photo library to select profile pictures + NSCameraUsageDescription + This app needs access to camera to take profile pictures CFBundleSignature ???? CFBundleVersion diff --git a/lib/components/bottom_nav_bar.dart b/lib/components/bottom_nav_bar.dart index a1981fc..6b95b2c 100644 --- a/lib/components/bottom_nav_bar.dart +++ b/lib/components/bottom_nav_bar.dart @@ -4,8 +4,10 @@ import 'package:flutter/material.dart'; class BottomNavBar extends StatelessWidget { /// The currently selected index in the navigation bar. final int currentIndex; + /// Callback when a navigation item is tapped. final ValueChanged onTap; + /// The role of the current user (affects navigation items). final String role; @@ -23,30 +25,15 @@ class BottomNavBar extends StatelessWidget { if (role == 'Organization') { items = const [ - BottomNavigationBarItem( - icon: Icon(Icons.home), - label: 'Home', - ), - BottomNavigationBarItem( - icon: Icon(Icons.people), - label: 'Players', - ), - BottomNavigationBarItem( - icon: Icon(Icons.event), - label: 'Tournaments', - ), - BottomNavigationBarItem( - icon: Icon(Icons.person), - label: 'Profile', - ), + BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'), + BottomNavigationBarItem(icon: Icon(Icons.people), label: 'Players'), + BottomNavigationBarItem(icon: Icon(Icons.event), label: 'Tournaments'), + BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'), ]; } else { // fallback items = const [ - BottomNavigationBarItem( - icon: Icon(Icons.home), - label: 'Home', - ), + BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'), BottomNavigationBarItem( icon: Icon(Icons.calendar_today), label: 'Time Table', @@ -55,10 +42,7 @@ class BottomNavBar extends StatelessWidget { icon: Icon(Icons.location_on), label: 'Tournaments', ), - BottomNavigationBarItem( - icon: Icon(Icons.person), - label: 'Profile', - ), + BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'), ]; } diff --git a/lib/components/financial_chart.dart b/lib/components/financial_chart.dart index a2836dd..6dafb67 100644 --- a/lib/components/financial_chart.dart +++ b/lib/components/financial_chart.dart @@ -10,7 +10,8 @@ class FinancialChart extends StatefulWidget { const FinancialChart({ super.key, - required this.entries, required ViewType viewType, + required this.entries, + required ViewType viewType, }); @override @@ -139,14 +140,17 @@ class _FinancialChartState extends State { bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, - getTitlesWidget: (value, meta) => Padding( - padding: const EdgeInsets.only(top: 6), - child: Text( - _getLabel(value.toInt()), - style: GoogleFonts.poppins( - fontSize: 11, fontWeight: FontWeight.w500), - ), - ), + getTitlesWidget: + (value, meta) => Padding( + padding: const EdgeInsets.only(top: 6), + child: Text( + _getLabel(value.toInt()), + style: GoogleFonts.poppins( + fontSize: 11, + fontWeight: FontWeight.w500, + ), + ), + ), ), ), leftTitles: AxisTitles( @@ -164,32 +168,34 @@ class _FinancialChartState extends State { drawVerticalLine: false, drawHorizontalLine: true, horizontalInterval: 2000, - getDrawingHorizontalLine: (value) => FlLine( - color: Colors.grey.withValues(alpha: 0.2), - strokeWidth: 1, - ), + getDrawingHorizontalLine: + (value) => FlLine( + color: Colors.grey.withValues(alpha: 0.2), + strokeWidth: 1, + ), ), borderData: FlBorderData(show: false), - barGroups: spots.map((x) { - return BarChartGroupData( - x: x, - barRods: [ - BarChartRodData( - toY: incomeMap[x] ?? 0, - width: 10, - borderRadius: BorderRadius.circular(6), - color: pastelGreen, - ), - BarChartRodData( - toY: expenseMap[x] ?? 0, - width: 10, - borderRadius: BorderRadius.circular(6), - color: pastelBlue, - ), - ], - barsSpace: 6, - ); - }).toList(), + barGroups: + spots.map((x) { + return BarChartGroupData( + x: x, + barRods: [ + BarChartRodData( + toY: incomeMap[x] ?? 0, + width: 10, + borderRadius: BorderRadius.circular(6), + color: pastelGreen, + ), + BarChartRodData( + toY: expenseMap[x] ?? 0, + width: 10, + borderRadius: BorderRadius.circular(6), + color: pastelBlue, + ), + ], + barsSpace: 6, + ); + }).toList(), ), ), ), @@ -224,30 +230,31 @@ class _FinancialChartState extends State { return SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( - children: ViewType.values.map((type) { - final isSelected = type == _viewType; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), - child: ChoiceChip( - label: Text( - _viewLabels[type]!, - style: GoogleFonts.poppins( - fontWeight: FontWeight.w600, - color: isSelected ? Colors.white : Colors.grey[800], + children: + ViewType.values.map((type) { + final isSelected = type == _viewType; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: ChoiceChip( + label: Text( + _viewLabels[type]!, + style: GoogleFonts.poppins( + fontWeight: FontWeight.w600, + color: isSelected ? Colors.white : Colors.grey[800], + ), + ), + selected: isSelected, + selectedColor: activeColor, + backgroundColor: Colors.grey.shade200, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + onSelected: (_) { + setState(() => _viewType = type); + }, ), - ), - selected: isSelected, - selectedColor: activeColor, - backgroundColor: Colors.grey.shade200, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - onSelected: (_) { - setState(() => _viewType = type); - }, - ), - ); - }).toList(), + ); + }).toList(), ), ); } @@ -260,8 +267,20 @@ class _FinancialChartState extends State { case ViewType.weekly: return "W${value}"; case ViewType.monthly: - const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', - 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + const months = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ]; return (value >= 1 && value <= 12) ? months[value - 1] : ''; case ViewType.yearly: return value.toString(); @@ -275,8 +294,7 @@ class _FinancialChartState extends State { label == "Income" ? Icons.arrow_upward : Icons.arrow_downward, color: color, ), - Text(label, - style: GoogleFonts.poppins(fontWeight: FontWeight.w600)), + Text(label, style: GoogleFonts.poppins(fontWeight: FontWeight.w600)), Text( "\$${amount.toStringAsFixed(2)}", style: GoogleFonts.poppins( diff --git a/lib/models/auth_state.dart b/lib/models/auth_state.dart index 5096b39..c6ff0db 100644 --- a/lib/models/auth_state.dart +++ b/lib/models/auth_state.dart @@ -31,7 +31,8 @@ class AuthState { return AuthState( status: status ?? this.status, errorMessage: errorMessage ?? this.errorMessage, - pendingVerificationEmail: pendingVerificationEmail ?? this.pendingVerificationEmail, + pendingVerificationEmail: + pendingVerificationEmail ?? this.pendingVerificationEmail, isResendingEmail: isResendingEmail ?? this.isResendingEmail, userRole: userRole ?? this.userRole, ); diff --git a/lib/models/financial_entry_model.dart b/lib/models/financial_entry_model.dart index 4626452..11206df 100644 --- a/lib/models/financial_entry_model.dart +++ b/lib/models/financial_entry_model.dart @@ -29,12 +29,15 @@ enum IncomeCategory { /// ```dart /// expenseCategoryToString(ExpenseCategory.food); // returns 'food' /// ``` -String expenseCategoryToString(ExpenseCategory category) => category.toString().split('.').last; +String expenseCategoryToString(ExpenseCategory category) => + category.toString().split('.').last; /// Converts a string to its corresponding [ExpenseCategory]. /// /// Throws a [StateError] if the string does not match any category. -ExpenseCategory expenseCategoryFromString(String value) => ExpenseCategory.values.firstWhere((e) => expenseCategoryToString(e) == value); +ExpenseCategory expenseCategoryFromString(String value) => ExpenseCategory + .values + .firstWhere((e) => expenseCategoryToString(e) == value); /// Converts an [IncomeCategory] to its string representation. /// @@ -42,12 +45,14 @@ ExpenseCategory expenseCategoryFromString(String value) => ExpenseCategory.value /// ```dart /// incomeCategoryToString(IncomeCategory.salary); // returns 'salary' /// ``` -String incomeCategoryToString(IncomeCategory category) => category.toString().split('.').last; +String incomeCategoryToString(IncomeCategory category) => + category.toString().split('.').last; /// Converts a string to its corresponding [IncomeCategory]. /// /// Throws a [StateError] if the string does not match any category. -IncomeCategory incomeCategoryFromString(String value) => IncomeCategory.values.firstWhere((e) => incomeCategoryToString(e) == value); +IncomeCategory incomeCategoryFromString(String value) => + IncomeCategory.values.firstWhere((e) => incomeCategoryToString(e) == value); /// Represents a financial entry, either income or expense, for a user. /// @@ -56,14 +61,19 @@ IncomeCategory incomeCategoryFromString(String value) => IncomeCategory.values.f class FinancialEntry { /// Unique identifier for the entry (usually the Firestore document ID). final String id; + /// Type of entry: 'income' or 'expense'. final String type; + /// Category of the entry (stored as string, e.g., 'food', 'salary'). final String category; + /// Amount of the entry (positive value). final double amount; + /// Date of the entry. final DateTime date; + /// Optional notes for the entry. final String? notes; @@ -113,4 +123,4 @@ class FinancialEntry { notes: map['notes'], ); } -} \ No newline at end of file +} diff --git a/lib/models/user_model.dart b/lib/models/user_model.dart index e929680..b94f2ce 100644 --- a/lib/models/user_model.dart +++ b/lib/models/user_model.dart @@ -11,6 +11,7 @@ class UserModel { final bool signupCompleted; final DateTime createdAt; final String? fcmToken; + final String? profileImage; // Added profile image field UserModel({ required this.uid, @@ -23,6 +24,7 @@ class UserModel { required this.signupCompleted, required this.createdAt, this.fcmToken, + this.profileImage, // Added to constructor }); factory UserModel.fromFirestore(DocumentSnapshot doc) { @@ -38,6 +40,7 @@ class UserModel { signupCompleted: data['signupCompleted'] ?? false, createdAt: (data['createdAt'] as Timestamp).toDate(), fcmToken: data['fcmToken'], + profileImage: data['profileImage'], // Added profile image parsing ); } @@ -52,6 +55,8 @@ class UserModel { 'signupCompleted': signupCompleted, 'createdAt': Timestamp.fromDate(createdAt), if (fcmToken != null) 'fcmToken': fcmToken, + if (profileImage != null) + 'profileImage': profileImage, // Added profile image to Firestore }; } @@ -66,6 +71,7 @@ class UserModel { bool? signupCompleted, DateTime? createdAt, String? fcmToken, + String? profileImage, // Added to copyWith }) { return UserModel( uid: uid ?? this.uid, @@ -78,6 +84,8 @@ class UserModel { signupCompleted: signupCompleted ?? this.signupCompleted, createdAt: createdAt ?? this.createdAt, fcmToken: fcmToken ?? this.fcmToken, + profileImage: + profileImage ?? this.profileImage, // Added profile image to copyWith ); } } diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 87c4755..25cb36e 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -3,18 +3,20 @@ import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import '../models/user_model.dart'; +import 'cloudinary_service.dart'; class AuthService { final FirebaseAuth _auth = FirebaseAuth.instance; final FirebaseFirestore _firestore = FirebaseFirestore.instance; + final CloudinaryService _cloudinaryService = CloudinaryService(); User? get currentUser => _auth.currentUser; Stream get authStateChanges => _auth.authStateChanges(); Future signInWithEmailAndPassword( - String email, - String password, - ) async { + String email, + String password, + ) async { return await _auth.signInWithEmailAndPassword( email: email, password: password, @@ -22,9 +24,9 @@ class AuthService { } Future createUserWithEmailAndPassword( - String email, - String password, - ) async { + String email, + String password, + ) async { return await _auth.createUserWithEmailAndPassword( email: email, password: password, @@ -70,11 +72,11 @@ class AuthService { Future getUserDataByEmail(String email) async { try { final querySnapshot = - await _firestore - .collection('users') - .where('email', isEqualTo: email) - .limit(1) - .get(); + await _firestore + .collection('users') + .where('email', isEqualTo: email) + .limit(1) + .get(); if (querySnapshot.docs.isNotEmpty) { return UserModel.fromFirestore(querySnapshot.docs.first); @@ -87,14 +89,69 @@ class AuthService { } Future updateEmailVerificationStatus( - String uid, - bool isVerified, - ) async { + String uid, + bool isVerified, + ) async { await _firestore.collection('users').doc(uid).update({ 'emailVerified': isVerified, }); } + Future updateProfileImage(String newImageUrl) async { + final uid = _auth.currentUser?.uid; + if (uid == null) { + throw Exception('No user logged in'); + } + + try { + await _firestore.collection('users').doc(uid).update({ + 'profileImage': newImageUrl, + 'updatedAt': FieldValue.serverTimestamp(), + }); + + debugPrint('Profile image updated successfully'); + } catch (e) { + debugPrint('Error updating profile image: $e'); + rethrow; + } + } + + Future removeProfileImage() async { + final uid = _auth.currentUser?.uid; + if (uid == null) { + throw Exception('No user logged in'); + } + + try { + await _firestore.collection('users').doc(uid).update({ + 'profileImage': FieldValue.delete(), + 'updatedAt': FieldValue.serverTimestamp(), + }); + + debugPrint('Profile image URL removed successfully'); + } catch (e) { + debugPrint('Error removing profile image: $e'); + rethrow; + } + } + + Future getProfileImageUrl() async { + final uid = _auth.currentUser?.uid; + if (uid == null) return null; + + try { + final userDoc = await _firestore.collection('users').doc(uid).get(); + if (userDoc.exists) { + final userData = userDoc.data() as Map; + return userData['profileImage'] as String?; + } + return null; + } catch (e) { + debugPrint('Error getting profile image URL: $e'); + return null; + } + } + Future saveFcmToken() async { try { await FirebaseMessaging.instance.requestPermission(); diff --git a/lib/services/cloudinary_service.dart b/lib/services/cloudinary_service.dart new file mode 100644 index 0000000..6ad8cd2 --- /dev/null +++ b/lib/services/cloudinary_service.dart @@ -0,0 +1,32 @@ +import 'dart:io'; +import 'package:cloudinary_public/cloudinary_public.dart'; +import 'package:flutter/foundation.dart'; + +class CloudinaryService { + static const String _cloudName = 'dgiqmo1t1'; + static const String _uploadPreset = 'flutter_unsigned'; + + final CloudinaryPublic _cloudinary = CloudinaryPublic( + _cloudName, + _uploadPreset, + cache: false, + ); + + Future uploadImage(File imageFile) async { + try { + // Upload the image with folder organization + final response = await _cloudinary.uploadFile( + CloudinaryFile.fromFile( + imageFile.path, + folder: 'profile_images', // Organizes images in a folder + ), + ); + + debugPrint('Image uploaded successfully: ${response.secureUrl}'); + return response.secureUrl; + } catch (e) { + debugPrint('Error uploading image to Cloudinary: $e'); + return null; + } + } +} diff --git a/lib/services/firestore_service.dart b/lib/services/firestore_service.dart index 06377bb..b63bbb2 100644 --- a/lib/services/firestore_service.dart +++ b/lib/services/firestore_service.dart @@ -30,9 +30,12 @@ class FirestoreService { .collection('entries') .orderBy('date', descending: true) .snapshots() - .map((snapshot) => snapshot.docs - .map((doc) => FinancialEntry.fromMap(doc.id, doc.data())) - .toList()); + .map( + (snapshot) => + snapshot.docs + .map((doc) => FinancialEntry.fromMap(doc.id, doc.data())) + .toList(), + ); } /// Updates an existing [FinancialEntry] for the current user in Firestore. diff --git a/lib/services/validation_service.dart b/lib/services/validation_service.dart index a18a1d7..3cf08cd 100644 --- a/lib/services/validation_service.dart +++ b/lib/services/validation_service.dart @@ -1,9 +1,9 @@ class ValidationService { static String? validateEmail( - String email, { - bool forceValidate = false, - bool fieldTapped = false, - }) { + String email, { + bool forceValidate = false, + bool fieldTapped = false, + }) { if (email.isEmpty && (forceValidate || fieldTapped)) { return "Email is required"; } else if (email.isNotEmpty) { @@ -18,11 +18,11 @@ class ValidationService { } static String? validatePassword( - String password, { - bool isLogin = false, - bool forceValidate = false, - bool fieldTapped = false, - }) { + String password, { + bool isLogin = false, + bool forceValidate = false, + bool fieldTapped = false, + }) { if (!isLogin) { if (password.isEmpty && (forceValidate || fieldTapped)) { return "Password is required"; @@ -48,10 +48,10 @@ class ValidationService { } static String? validateName( - String name, { - bool forceValidate = false, - bool fieldTapped = false, - }) { + String name, { + bool forceValidate = false, + bool fieldTapped = false, + }) { if (name.isEmpty && (forceValidate || fieldTapped)) { return "Full name is required"; } else if (name.isNotEmpty) { @@ -64,11 +64,11 @@ class ValidationService { } static String? validateSport( - String sport, - String role, { - bool forceValidate = false, - bool fieldTapped = false, - }) { + String sport, + String role, { + bool forceValidate = false, + bool fieldTapped = false, + }) { if (sport.isEmpty && (forceValidate || fieldTapped)) { return role == 'Doctor' ? "Specialization is required" @@ -78,21 +78,21 @@ class ValidationService { } static String? validateDob( - DateTime? dob, { - bool forceValidate = false, - bool fieldTapped = false, - }) { + DateTime? dob, { + bool forceValidate = false, + bool fieldTapped = false, + }) { if (dob == null && (forceValidate || fieldTapped)) { return "Date of birth is required"; } else if (dob != null) { final now = DateTime.now(); final age = now.year - - dob.year - - (now.month > dob.month || + dob.year - + (now.month > dob.month || (now.month == dob.month && now.day >= dob.day) - ? 0 - : 1); + ? 0 + : 1); if (age < 13) return "You must be at least 13 years old"; } return null; diff --git a/lib/viewmodels/auth_viewmodel.dart b/lib/viewmodels/auth_viewmodel.dart index 373cab6..c65799b 100644 --- a/lib/viewmodels/auth_viewmodel.dart +++ b/lib/viewmodels/auth_viewmodel.dart @@ -284,9 +284,9 @@ class AuthViewModel extends ChangeNotifier { ); final activeErrors = - _isLogin - ? [updatedErrors['email'], updatedErrors['password']] - : updatedErrors.values; + _isLogin + ? [updatedErrors['email'], updatedErrors['password']] + : updatedErrors.values; final errors = activeErrors.where((error) => error != null).toList(); if (errors.isNotEmpty) { @@ -373,7 +373,7 @@ class AuthViewModel extends ChangeNotifier { break; case 'invalid-credential': errorMessage = - "Invalid email or password. Please check your credentials."; + "Invalid email or password. Please check your credentials."; break; case 'too-many-requests': errorMessage = "Too many failed attempts. Please try again later."; @@ -402,7 +402,7 @@ class AuthViewModel extends ChangeNotifier { AuthState( status: AuthStatus.error, errorMessage: - 'Email already registered but not verified. Please check your inbox or resend verification email.', + 'Email already registered but not verified. Please check your inbox or resend verification email.', ), ); return; @@ -454,19 +454,19 @@ class AuthViewModel extends ChangeNotifier { ); if (userData != null && !userData.emailVerified) { errorMessage = - 'This email is already registered but not verified. Please check your inbox.'; + 'This email is already registered but not verified. Please check your inbox.'; } else { errorMessage = - 'This email is already registered. Please try logging in.'; + 'This email is already registered. Please try logging in.'; } break; case 'weak-password': errorMessage = - 'Your password must be at least 8 characters and contain a number.'; + 'Your password must be at least 8 characters and contain a number.'; break; case 'operation-not-allowed': errorMessage = - 'This operation is not allowed. Please contact support.'; + 'This operation is not allowed. Please contact support.'; break; default: errorMessage = @@ -480,8 +480,8 @@ class AuthViewModel extends ChangeNotifier { void _startEmailVerificationCheck() { _emailVerificationTimer = Timer.periodic(const Duration(seconds: 3), ( - timer, - ) async { + timer, + ) async { try { final user = _authService.currentUser; if (user == null) { @@ -536,10 +536,9 @@ class AuthViewModel extends ChangeNotifier { } // Set auth state with user role for navigation - setAuthState(AuthState( - status: AuthStatus.authenticated, - userRole: userData.role, - )); + setAuthState( + AuthState(status: AuthStatus.authenticated, userRole: userData.role), + ); } catch (e) { setAuthState( AuthState( @@ -575,10 +574,10 @@ class AuthViewModel extends ChangeNotifier { String errorMessage = 'Error sending verification email'; if (e.toString().contains('too-many-requests')) { errorMessage = - 'Too many requests. Please wait a moment before trying again.'; + 'Too many requests. Please wait a moment before trying again.'; } else if (e.toString().contains('network')) { errorMessage = - 'Network error. Please check your connection and try again.'; + 'Network error. Please check your connection and try again.'; } setAuthState( AuthState(status: AuthStatus.error, errorMessage: errorMessage), @@ -610,7 +609,7 @@ class AuthViewModel extends ChangeNotifier { const AuthState( status: AuthStatus.emailVerificationPending, errorMessage: - 'Email is still not verified. Please check your inbox and click the verification link first.', + 'Email is still not verified. Please check your inbox and click the verification link first.', ), ); } diff --git a/lib/views/screens/athlete/athlete_dashboard.dart b/lib/views/screens/athlete/athlete_dashboard.dart index 1c57356..a10dfbe 100644 --- a/lib/views/screens/athlete/athlete_dashboard.dart +++ b/lib/views/screens/athlete/athlete_dashboard.dart @@ -41,6 +41,11 @@ class _DashboardScreenState extends State if (response.statusCode == 200) { final data = json.decode(response.body); quotes = data; + + // Generate random index based on the actual quotes list size + setState(() { + number_ = Random().nextInt(quotes.length); + }); } else { throw Exception('Failed to load quotes'); } @@ -294,10 +299,15 @@ class _DashboardScreenState extends State padding: const EdgeInsets.all(24), child: Column( children: [ - Text("Todays Quote"), - SizedBox(height: 5), - Text( + Text("Today's Quote"), + const SizedBox(height: 5), + quotes.isNotEmpty + ? Text( "${quotes[number_]['quote']}", + style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold), + ) + : const Text( + "No quote available", style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold), ), SizedBox(height: 30), diff --git a/lib/views/screens/athlete/calendar_screen.dart b/lib/views/screens/athlete/calendar_screen.dart index c90642d..e2ee2b6 100644 --- a/lib/views/screens/athlete/calendar_screen.dart +++ b/lib/views/screens/athlete/calendar_screen.dart @@ -21,105 +21,112 @@ class _CalendarScreenState extends State { await showDialog( context: context, - builder: (ctx) => AlertDialog( - title: const Text("Add Activity"), - content: SingleChildScrollView( - child: Column( - children: [ - TextField( - controller: workController, - decoration: const InputDecoration(labelText: "Work/Activity"), - ), - const SizedBox(height: 10), - ElevatedButton( - onPressed: () async { - final picked = await showDatePicker( - context: context, - initialDate: selectedDay, - firstDate: DateTime.now(), - lastDate: DateTime.now().add(const Duration(days: 365)), - ); - if (picked != null) { - final time = await showTimePicker( - context: context, - initialTime: const TimeOfDay(hour: 9, minute: 0), - ); - if (time != null) { - setState(() { - startTime = DateTime( - picked.year, - picked.month, - picked.day, - time.hour, - time.minute, + builder: + (ctx) => AlertDialog( + title: const Text("Add Activity"), + content: SingleChildScrollView( + child: Column( + children: [ + TextField( + controller: workController, + decoration: const InputDecoration( + labelText: "Work/Activity", + ), + ), + const SizedBox(height: 10), + ElevatedButton( + onPressed: () async { + final picked = await showDatePicker( + context: context, + initialDate: selectedDay, + firstDate: DateTime.now(), + lastDate: DateTime.now().add(const Duration(days: 365)), + ); + if (picked != null) { + final time = await showTimePicker( + context: context, + initialTime: const TimeOfDay(hour: 9, minute: 0), ); - }); - } - } - }, - child: Text(startTime == null - ? "Select Start Time" - : "Start: $startTime"), + if (time != null) { + setState(() { + startTime = DateTime( + picked.year, + picked.month, + picked.day, + time.hour, + time.minute, + ); + }); + } + } + }, + child: Text( + startTime == null + ? "Select Start Time" + : "Start: $startTime", + ), + ), + const SizedBox(height: 10), + ElevatedButton( + onPressed: () async { + final picked = await showDatePicker( + context: context, + initialDate: selectedDay, + firstDate: DateTime.now(), + lastDate: DateTime.now().add(const Duration(days: 365)), + ); + if (picked != null) { + final time = await showTimePicker( + context: context, + initialTime: const TimeOfDay(hour: 10, minute: 0), + ); + if (time != null) { + setState(() { + endTime = DateTime( + picked.year, + picked.month, + picked.day, + time.hour, + time.minute, + ); + }); + } + } + }, + child: Text( + endTime == null ? "Select End Time" : "End: $endTime", + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: const Text("Cancel"), ), - const SizedBox(height: 10), ElevatedButton( onPressed: () async { - final picked = await showDatePicker( - context: context, - initialDate: selectedDay, - firstDate: DateTime.now(), - lastDate: DateTime.now().add(const Duration(days: 365)), - ); - if (picked != null) { - final time = await showTimePicker( - context: context, - initialTime: const TimeOfDay(hour: 10, minute: 0), - ); - if (time != null) { - setState(() { - endTime = DateTime( - picked.year, - picked.month, - picked.day, - time.hour, - time.minute, - ); - }); - } + if (workController.text.isNotEmpty && + startTime != null && + endTime != null) { + await FirebaseFirestore.instance + .collection('timetables') + .add({ + 'uid': uid, + 'work': workController.text, + 'startTime': Timestamp.fromDate(startTime!.toUtc()), + 'endTime': Timestamp.fromDate(endTime!.toUtc()), + 'createdAt': Timestamp.now(), + 'notified': false, + }); + Navigator.of(ctx).pop(); } }, - child: Text(endTime == null - ? "Select End Time" - : "End: $endTime"), + child: const Text("Save"), ), ], ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(ctx).pop(), - child: const Text("Cancel"), - ), - ElevatedButton( - onPressed: () async { - if (workController.text.isNotEmpty && - startTime != null && - endTime != null) { - await FirebaseFirestore.instance.collection('timetables').add({ - 'uid': uid, - 'work': workController.text, - 'startTime': Timestamp.fromDate(startTime!.toUtc()), - 'endTime': Timestamp.fromDate(endTime!.toUtc()), - 'createdAt': Timestamp.now(), - 'notified': false, - }); - Navigator.of(ctx).pop(); - } - }, - child: const Text("Save"), - ), - ], - ), ); } @@ -142,30 +149,37 @@ class _CalendarScreenState extends State { ), Expanded( child: StreamBuilder( - stream: FirebaseFirestore.instance - .collection('timetables') - .where('uid', isEqualTo: uid) - .where( - 'startTime', - isGreaterThanOrEqualTo: Timestamp.fromDate( - DateTime.now().toUtc(), - ), - ) - .where( - 'startTime', - isLessThan: Timestamp.fromDate( - DateTime.utc(selectedDay.year, selectedDay.month, selectedDay.day + 1), - ), - ) - .orderBy('startTime') - .snapshots(), + stream: + FirebaseFirestore.instance + .collection('timetables') + .where('uid', isEqualTo: uid) + .where( + 'startTime', + isGreaterThanOrEqualTo: Timestamp.fromDate( + DateTime.now().toUtc(), + ), + ) + .where( + 'startTime', + isLessThan: Timestamp.fromDate( + DateTime.utc( + selectedDay.year, + selectedDay.month, + selectedDay.day + 1, + ), + ), + ) + .orderBy('startTime') + .snapshots(), builder: (ctx, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (!snapshot.hasData || snapshot.data!.docs.isEmpty) { - return const Center(child: Text("No upcoming activities for this day.")); + return const Center( + child: Text("No upcoming activities for this day."), + ); } final docs = snapshot.data!.docs; @@ -184,7 +198,7 @@ class _CalendarScreenState extends State { ); }, ), - ) + ), ], ), floatingActionButton: FloatingActionButton( diff --git a/lib/views/screens/athlete/tournaments_screen.dart b/lib/views/screens/athlete/tournaments_screen.dart index 04b0c2b..c2c18e7 100644 --- a/lib/views/screens/athlete/tournaments_screen.dart +++ b/lib/views/screens/athlete/tournaments_screen.dart @@ -39,7 +39,8 @@ class _TournamentsScreenState extends State { if (permission == LocationPermission.deniedForever) { return Future.error( - 'Location permissions are permanently denied, we cannot request.'); + 'Location permissions are permanently denied, we cannot request.', + ); } } @@ -47,21 +48,20 @@ class _TournamentsScreenState extends State { final uid = FirebaseAuth.instance.currentUser!.uid; // Get user document - final userDoc = await FirebaseFirestore.instance - .collection('users') - .doc(uid) - .get(); + final userDoc = + await FirebaseFirestore.instance.collection('users').doc(uid).get(); final userSport = userDoc['sport'] ?? ''; final now = Timestamp.now(); // current timestamp // Fetch ONLY upcoming tournaments for the user's sport - final snapshot = await FirebaseFirestore.instance - .collection('tournaments') - .where('sport', isEqualTo: userSport) - .where('date', isGreaterThanOrEqualTo: now) - .orderBy('date') - .get(); + final snapshot = + await FirebaseFirestore.instance + .collection('tournaments') + .where('sport', isEqualTo: userSport) + .where('date', isGreaterThanOrEqualTo: now) + .orderBy('date') + .get(); return snapshot.docs.map((doc) => Tournament.fromDocument(doc)).toList(); } @@ -83,15 +83,22 @@ class _TournamentsScreenState extends State { final tournaments = snapshot.data ?? []; - final CameraPosition initialCameraPosition = tournaments.isNotEmpty - ? CameraPosition( - target: LatLng(tournaments.first.lat, tournaments.first.lng), - zoom: 10, - ) - : const CameraPosition( - target: LatLng(20.5937, 78.9629), // Default location (e.g., India) - zoom: 4, - ); + final CameraPosition initialCameraPosition = + tournaments.isNotEmpty + ? CameraPosition( + target: LatLng( + tournaments.first.lat, + tournaments.first.lng, + ), + zoom: 10, + ) + : const CameraPosition( + target: LatLng( + 20.5937, + 78.9629, + ), // Default location (e.g., India) + zoom: 4, + ); return Stack( children: [ @@ -99,15 +106,18 @@ class _TournamentsScreenState extends State { initialCameraPosition: initialCameraPosition, myLocationEnabled: true, myLocationButtonEnabled: true, - markers: tournaments - .map((tournament) => Marker( - markerId: MarkerId(tournament.id), - position: LatLng(tournament.lat, tournament.lng), - onTap: () { - _showTournamentDialog(context, tournament); - }, - )) - .toSet(), + markers: + tournaments + .map( + (tournament) => Marker( + markerId: MarkerId(tournament.id), + position: LatLng(tournament.lat, tournament.lng), + onTap: () { + _showTournamentDialog(context, tournament); + }, + ), + ) + .toSet(), ), if (tournaments.isEmpty) @@ -133,95 +143,91 @@ class _TournamentsScreenState extends State { ), ); } + void _showTournamentDialog(BuildContext context, Tournament t) { - showDialog( - context: context, - builder: (_) => AlertDialog( - backgroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - title: Text( - t.name, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.black87, - ), - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 8), - _buildInfoRow("Level", t.level), - const SizedBox(height: 6), - _buildInfoRow("Sport", t.sport), - const SizedBox(height: 6), - _buildInfoRow("Date", t.dateString), - const SizedBox(height: 6), - _buildInfoRow("Time", t.time), - const SizedBox(height: 12), - const Text( - "Address:", - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 14, - color: Colors.black87, - ), - ), - Text( - t.address, - style: const TextStyle( - fontSize: 14, - color: Colors.black54, + showDialog( + context: context, + builder: + (_) => AlertDialog( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), ), - ), - ], - ), - actions: [ - Center( - child: TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text( - 'Close', - style: TextStyle( - fontSize: 14, + title: Text( + t.name, + style: const TextStyle( + fontSize: 20, fontWeight: FontWeight.bold, - color: Colors.blue, + color: Colors.black87, ), ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8), + _buildInfoRow("Level", t.level), + const SizedBox(height: 6), + _buildInfoRow("Sport", t.sport), + const SizedBox(height: 6), + _buildInfoRow("Date", t.dateString), + const SizedBox(height: 6), + _buildInfoRow("Time", t.time), + const SizedBox(height: 12), + const Text( + "Address:", + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + color: Colors.black87, + ), + ), + Text( + t.address, + style: const TextStyle(fontSize: 14, color: Colors.black54), + ), + ], + ), + actions: [ + Center( + child: TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text( + 'Close', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.blue, + ), + ), + ), + ), + ], ), - ), - ], - ), - ); -} + ); + } -Widget _buildInfoRow(String title, String value) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "$title: ", - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 14, - color: Colors.black87, - ), - ), - Expanded( - child: Text( - value, + Widget _buildInfoRow(String title, String value) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "$title: ", style: const TextStyle( + fontWeight: FontWeight.w600, fontSize: 14, - color: Colors.black54, + color: Colors.black87, ), ), - ), - ], - ); -} + Expanded( + child: Text( + value, + style: const TextStyle(fontSize: 14, color: Colors.black54), + ), + ), + ], + ); + } } class Tournament { @@ -251,9 +257,10 @@ class Tournament { final data = doc.data() as Map; final Timestamp? dateTs = data['date']; - final String dateStr = dateTs != null - ? dateTs.toDate().toLocal().toString().split(' ')[0] - : ''; + final String dateStr = + dateTs != null + ? dateTs.toDate().toLocal().toString().split(' ')[0] + : ''; final loc = data['location'] ?? {}; diff --git a/lib/views/screens/auth_screen.dart b/lib/views/screens/auth_screen.dart index 9d35f29..530a74a 100644 --- a/lib/views/screens/auth_screen.dart +++ b/lib/views/screens/auth_screen.dart @@ -18,7 +18,8 @@ class AuthScreen extends StatefulWidget { class _AuthScreenState extends State { late AuthViewModel _viewModel; - AuthStatus? _lastAuthStatus; // Track the last auth status to prevent duplicate dialogs + AuthStatus? + _lastAuthStatus; // Track the last auth status to prevent duplicate dialogs @override void initState() { @@ -117,9 +118,9 @@ class _AuthScreenState extends State { width: double.infinity, constraints: BoxConstraints( maxWidth: - ResponsiveHelper.isLargeScreen(context) - ? 800 - : double.infinity, + ResponsiveHelper.isLargeScreen(context) + ? 800 + : double.infinity, minHeight: MediaQuery.of(context).size.height, ), padding: EdgeInsets.symmetric( @@ -127,7 +128,7 @@ class _AuthScreenState extends State { context, ), vertical: - MediaQuery.of(context).size.height * + MediaQuery.of(context).size.height * (ResponsiveHelper.isSmallScreen(context) ? 0.03 : 0.05), @@ -148,7 +149,7 @@ class _AuthScreenState extends State { ), SizedBox( height: - MediaQuery.of(context).size.height * + MediaQuery.of(context).size.height * (ResponsiveHelper.isSmallScreen(context) ? 0.03 : 0.04), diff --git a/lib/views/screens/coach/coach_dashboard.dart b/lib/views/screens/coach/coach_dashboard.dart index 93c5312..8ca7f48 100644 --- a/lib/views/screens/coach/coach_dashboard.dart +++ b/lib/views/screens/coach/coach_dashboard.dart @@ -15,7 +15,7 @@ class CoachDashboardScreen extends StatelessWidget { onPressed: () async { await signoutConfirmation(context); }, - ) + ), ], ), body: Column( diff --git a/lib/views/screens/doctor/doctor_dashboard.dart b/lib/views/screens/doctor/doctor_dashboard.dart index 7ef05c1..2fd0de0 100644 --- a/lib/views/screens/doctor/doctor_dashboard.dart +++ b/lib/views/screens/doctor/doctor_dashboard.dart @@ -26,10 +26,7 @@ class _DoctorDashboardScreenState extends State { foregroundColor: Colors.black87, title: const Text( 'Doctor Dashboard', - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 22, - ), + style: TextStyle(fontWeight: FontWeight.w600, fontSize: 22), ), actions: [ Container( @@ -44,7 +41,7 @@ class _DoctorDashboardScreenState extends State { await signoutConfirmation(context); }, ), - ) + ), ], ), body: FutureBuilder>>( @@ -56,15 +53,14 @@ class _DoctorDashboardScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Colors.blue[600]!), + valueColor: AlwaysStoppedAnimation( + Colors.blue[600]!, + ), ), const SizedBox(height: 16), Text( 'Loading your dashboard...', - style: TextStyle( - color: Colors.grey[600], - fontSize: 16, - ), + style: TextStyle(color: Colors.grey[600], fontSize: 16), ), ], ), @@ -201,9 +197,17 @@ class _DoctorDashboardScreenState extends State { const SizedBox(height: 16), _buildInfoRow(Icons.person_outline, 'Full Name', name), const SizedBox(height: 12), - _buildInfoRow(Icons.sports_outlined, 'Specialization', sport.isEmpty ? 'Not specified' : sport), + _buildInfoRow( + Icons.sports_outlined, + 'Specialization', + sport.isEmpty ? 'Not specified' : sport, + ), const SizedBox(height: 12), - _buildInfoRow(Icons.cake_outlined, 'Date of Birth', dob.isEmpty ? 'Not specified' : dob), + _buildInfoRow( + Icons.cake_outlined, + 'Date of Birth', + dob.isEmpty ? 'Not specified' : dob, + ), ], ), ), @@ -222,10 +226,7 @@ class _DoctorDashboardScreenState extends State { const SizedBox(height: 4), Text( 'Access your tools and manage your practice', - style: TextStyle( - fontSize: 14, - color: Colors.grey[600], - ), + style: TextStyle(fontSize: 14, color: Colors.grey[600]), ), const SizedBox(height: 16), @@ -246,8 +247,7 @@ class _DoctorDashboardScreenState extends State { label: "Qualifications & License", color: Colors.orange[600]!, gradient: [Colors.orange[400]!, Colors.orange[600]!], - onTap: () { - }, + onTap: () {}, ), _buildActionCard( context, @@ -255,8 +255,7 @@ class _DoctorDashboardScreenState extends State { label: "Announcements", color: Colors.green[600]!, gradient: [Colors.green[400]!, Colors.green[600]!], - onTap: () { - }, + onTap: () {}, ), _buildActionCard( context, @@ -264,8 +263,7 @@ class _DoctorDashboardScreenState extends State { label: "Information Center", color: Colors.blue[600]!, gradient: [Colors.blue[400]!, Colors.blue[600]!], - onTap: () { - }, + onTap: () {}, ), ], ), @@ -280,11 +278,7 @@ class _DoctorDashboardScreenState extends State { Widget _buildInfoRow(IconData icon, String label, String value) { return Row( children: [ - Icon( - icon, - size: 18, - color: Colors.grey[600], - ), + Icon(icon, size: 18, color: Colors.grey[600]), const SizedBox(width: 12), Expanded( child: Column( @@ -320,13 +314,13 @@ class _DoctorDashboardScreenState extends State { } Widget _buildActionCard( - BuildContext context, { - required IconData icon, - required String label, - required Color color, - required List gradient, - required VoidCallback onTap, - }) { + BuildContext context, { + required IconData icon, + required String label, + required Color color, + required List gradient, + required VoidCallback onTap, + }) { return GestureDetector( onTap: onTap, child: Container( @@ -359,11 +353,7 @@ class _DoctorDashboardScreenState extends State { color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(16), ), - child: Icon( - icon, - size: 32, - color: Colors.white, - ), + child: Icon(icon, size: 32, color: Colors.white), ), const SizedBox(height: 12), Text( @@ -383,4 +373,4 @@ class _DoctorDashboardScreenState extends State { ), ); } -} \ No newline at end of file +} diff --git a/lib/views/screens/organization/add_tournament_screen.dart b/lib/views/screens/organization/add_tournament_screen.dart index ed224ef..9650f56 100644 --- a/lib/views/screens/organization/add_tournament_screen.dart +++ b/lib/views/screens/organization/add_tournament_screen.dart @@ -497,12 +497,14 @@ class _PurpleSaveButtonState extends State<_PurpleSaveButton> onTap: widget.onPressed, child: AnimatedBuilder( animation: _controller, - builder: (context, child) => Transform.scale( - scale: _scale.value, - child: child, - ), + builder: + (context, child) => + Transform.scale(scale: _scale.value, child: child), child: Container( - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 24), // Reduced size + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 24, + ), // Reduced size decoration: BoxDecoration( color: const Color(0xFF1976D2), // Blue shade borderRadius: BorderRadius.circular(20), @@ -535,4 +537,4 @@ class _PurpleSaveButtonState extends State<_PurpleSaveButton> ), ); } -} \ No newline at end of file +} diff --git a/lib/views/screens/privacy_terms_screen.dart b/lib/views/screens/privacy_terms_screen.dart index 4081128..188e500 100644 --- a/lib/views/screens/privacy_terms_screen.dart +++ b/lib/views/screens/privacy_terms_screen.dart @@ -31,7 +31,10 @@ class PrivacyTermsPage extends StatelessWidget { height: 1, decoration: BoxDecoration( gradient: LinearGradient( - colors: [Colors.blue.withValues(alpha: 0.3), Colors.transparent], + colors: [ + Colors.blue.withValues(alpha: 0.3), + Colors.transparent, + ], ), ), ), @@ -80,10 +83,7 @@ class PrivacyTermsPage extends StatelessWidget { gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [ - Colors.blue.shade600, - Colors.blue.shade700, - ], + colors: [Colors.blue.shade600, Colors.blue.shade700], ), borderRadius: BorderRadius.circular(16), boxShadow: [ @@ -102,11 +102,7 @@ class PrivacyTermsPage extends StatelessWidget { color: Colors.white.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(12), ), - child: const Icon( - Icons.security, - size: 32, - color: Colors.white, - ), + child: const Icon(Icons.security, size: 32, color: Colors.white), ), const SizedBox(height: 16), const Text( @@ -261,11 +257,7 @@ class PrivacyTermsPage extends StatelessWidget { color: iconColor.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(8), ), - child: Icon( - icon, - size: 24, - color: iconColor, - ), + child: Icon(icon, size: 24, color: iconColor), ), const SizedBox(width: 16), Text( @@ -280,10 +272,7 @@ class PrivacyTermsPage extends StatelessWidget { ), ), // Section Content - Padding( - padding: const EdgeInsets.all(24), - child: content, - ), + Padding(padding: const EdgeInsets.all(24), child: content), ], ), ); @@ -302,11 +291,7 @@ class PrivacyTermsPage extends StatelessWidget { color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), ), - child: Icon( - icon, - size: 16, - color: color, - ), + child: Icon(icon, size: 16, color: color), ), const SizedBox(width: 12), Expanded( @@ -371,21 +356,12 @@ class PrivacyTermsPage extends StatelessWidget { decoration: BoxDecoration( color: Colors.amber.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), - border: Border( - left: BorderSide( - color: Colors.amber, - width: 4, - ), - ), + border: Border(left: BorderSide(color: Colors.amber, width: 4)), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon( - Icons.info, - color: Colors.amber[700], - size: 20, - ), + Icon(Icons.info, color: Colors.amber[700], size: 20), const SizedBox(width: 12), Expanded( child: Text( @@ -435,18 +411,11 @@ class PrivacyTermsPage extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.email_outlined, - size: 16, - color: Colors.grey[600], - ), + Icon(Icons.email_outlined, size: 16, color: Colors.grey[600]), const SizedBox(width: 8), Text( 'Questions? Contact support@athletix.com', - style: TextStyle( - fontSize: 10, - color: Colors.grey[600], - ), + style: TextStyle(fontSize: 10, color: Colors.grey[600]), ), ], ), diff --git a/lib/views/screens/profile_screen.dart b/lib/views/screens/profile_screen.dart index c3bda90..eee81a7 100644 --- a/lib/views/screens/profile_screen.dart +++ b/lib/views/screens/profile_screen.dart @@ -4,10 +4,167 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:image_picker/image_picker.dart'; +import 'dart:io'; +import '../../services/auth_service.dart'; +import '../../services/cloudinary_service.dart'; -class ProfileScreen extends StatelessWidget { +class ProfileScreen extends StatefulWidget { const ProfileScreen({super.key}); + @override + State createState() => _ProfileScreenState(); +} + +class _ProfileScreenState extends State { + final AuthService _authService = AuthService(); + final CloudinaryService _cloudinaryService = CloudinaryService(); + final ImagePicker _imagePicker = ImagePicker(); + bool _isUploading = false; + + void _showImageSourceDialog() { + showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return SafeArea( + child: Wrap( + children: [ + ListTile( + leading: const Icon(Icons.photo_library), + title: const Text('Gallery'), + onTap: () { + Navigator.pop(context); + _pickImageFromSource(ImageSource.gallery); + }, + ), + ListTile( + leading: const Icon(Icons.photo_camera), + title: const Text('Camera'), + onTap: () { + Navigator.pop(context); + _pickImageFromSource(ImageSource.camera); + }, + ), + ListTile( + leading: const Icon(Icons.cancel), + title: const Text('Cancel'), + onTap: () => Navigator.pop(context), + ), + ], + ), + ); + }, + ); + } + + Future _pickImageFromSource(ImageSource source) async { + try { + final XFile? image = await _imagePicker.pickImage( + source: source, + maxWidth: 800, + maxHeight: 800, + imageQuality: 85, + ); + + if (image == null) return; + + setState(() { + _isUploading = true; + }); + + final imageUrl = await _cloudinaryService.uploadImage(File(image.path)); + + if (imageUrl != null) { + await _authService.updateProfileImage(imageUrl); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Profile picture updated successfully!'), + backgroundColor: Colors.green, + ), + ); + setState(() {}); + } + } else { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Failed to upload image. Please try again.'), + backgroundColor: Colors.red, + ), + ); + } + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error uploading image: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } finally { + if (mounted) { + setState(() { + _isUploading = false; + }); + } + } + } + + Widget _buildProfileAvatar(String? profileImageUrl) { + return GestureDetector( + onTap: _isUploading ? null : _showImageSourceDialog, + child: Stack( + alignment: Alignment.center, + children: [ + CircleAvatar( + radius: 50, + backgroundColor: Colors.blue.shade100, + backgroundImage: + profileImageUrl != null ? NetworkImage(profileImageUrl) : null, + child: + profileImageUrl == null + ? Icon(Icons.person, size: 60, color: Colors.blue.shade700) + : null, + ), + if (_isUploading) + Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: Colors.black.withAlpha(128), + shape: BoxShape.circle, + ), + child: const CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ), + if (!_isUploading) + Positioned( + bottom: 0, + right: 0, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.blue, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + ), + child: const Icon( + Icons.camera_alt, + color: Colors.white, + size: 16, + ), + ), + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { final uid = FirebaseAuth.instance.currentUser!.uid; @@ -20,7 +177,7 @@ class ProfileScreen extends StatelessWidget { title: const Text("Profile", style: TextStyle(color: Colors.black)), actions: [ IconButton( - icon: const Icon(Icons.logout, color: Colors.red), // 🔴 Red icon + icon: const Icon(Icons.logout, color: Colors.red), tooltip: 'Logout', onPressed: () async { await signoutConfirmation(context); @@ -41,6 +198,8 @@ class ProfileScreen extends StatelessWidget { final data = snapshot.data!.data() as Map; final role = (data['role'] ?? 'N/A').toString().toLowerCase(); + final profileImageUrl = data['profileImage'] as String?; + String? extraFieldLabel; String? extraFieldValue; @@ -81,15 +240,7 @@ class ProfileScreen extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - CircleAvatar( - radius: 50, - backgroundColor: Colors.blue.shade100, - child: Icon( - Icons.person, - size: 60, - color: Colors.blue.shade700, - ), - ), + _buildProfileAvatar(profileImageUrl), const SizedBox(height: 16), Text( data['name'] ?? 'N/A', @@ -147,7 +298,7 @@ class ProfileScreen extends StatelessWidget { } static String _formatDate(DateTime date) { - return DateFormat.yMMMMd().format(date); // e.g., July 21, 2025 + return DateFormat.yMMMMd().format(date); } Widget _buildInfoRow(String label, String value) { diff --git a/lib/views/widgets/custom_input_field.dart b/lib/views/widgets/custom_input_field.dart index f050d92..97d8b4e 100644 --- a/lib/views/widgets/custom_input_field.dart +++ b/lib/views/widgets/custom_input_field.dart @@ -34,6 +34,7 @@ class _CustomInputFieldState extends State { super.initState(); isobscure = true; } + Widget build(BuildContext context) { return Consumer( builder: (context, viewModel, child) { @@ -55,7 +56,8 @@ class _CustomInputFieldState extends State { ), child: TextField( controller: widget.controller, - obscureText: widget.suffixIcon != null ? isobscure : widget.obscureText, + obscureText: + widget.suffixIcon != null ? isobscure : widget.obscureText, onTap: widget.onTap, onChanged: widget.onChanged, style: TextStyle( @@ -84,15 +86,23 @@ class _CustomInputFieldState extends State { vertical: screenHeight * 0.001, ), suffixIcon: - widget.suffixIcon != null ? - IconButton(onPressed: (){ - setState(() { - isobscure = !isobscure; - }); - }, icon: isobscure ? Icon(Icons.visibility_off) : Icon(Icons.visibility)) : null, + widget.suffixIcon != null + ? IconButton( + onPressed: () { + setState(() { + isobscure = !isobscure; + }); + }, + icon: + isobscure + ? Icon(Icons.visibility_off) + : Icon(Icons.visibility), + ) + : null, errorText: (!viewModel.isLogin && - viewModel.formValidation.tappedFields[widget.fieldKey]!) + viewModel.formValidation.tappedFields[widget + .fieldKey]!) ? viewModel.formValidation.fieldErrors[widget.fieldKey] : null, errorStyle: TextStyle( @@ -105,5 +115,3 @@ class _CustomInputFieldState extends State { ); } } - - diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..64a0ece 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87..2db3c22 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 485a507..84d8eaa 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,6 +7,7 @@ import Foundation import cloud_firestore import file_picker +import file_selector_macos import firebase_auth import firebase_core import firebase_messaging @@ -16,6 +17,7 @@ import path_provider_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 0130a19..4374d3a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -97,6 +97,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.4.12" + cloudinary_public: + dependency: "direct main" + description: + name: cloudinary_public + sha256: "30c2aac5a31b468e5e955d2bbbac3eebcd1c66c9c7c2ffd75828606503bdda68" + url: "https://pub.dev" + source: hosted + version: "0.23.1" collection: dependency: transitive description: @@ -137,6 +145,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dio: + dependency: transitive + description: + name: dio + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" + url: "https://pub.dev" + source: hosted + version: "5.8.0+1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" equatable: dependency: transitive description: @@ -169,6 +193,38 @@ packages: url: "https://pub.dev" source: hosted version: "10.2.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" + url: "https://pub.dev" + source: hosted + version: "0.9.3+2" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "8c9250b2bd2d8d4268e39c82543bacbaca0fda7d29e0728c3c4bbb7c820fd711" + url: "https://pub.dev" + source: hosted + version: "0.9.4+3" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" + url: "https://pub.dev" + source: hosted + version: "0.9.3+4" firebase_auth: dependency: "direct main" description: @@ -488,6 +544,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.4" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "6fae381e6af2bbe0365a5e4ce1db3959462fa0c4d234facf070746024bb80c8d" + url: "https://pub.dev" + source: hosted + version: "0.8.12+24" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100" + url: "https://pub.dev" + source: hosted + version: "0.8.12+2" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9" + url: "https://pub.dev" + source: hosted + version: "0.2.1+2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1" + url: "https://pub.dev" + source: hosted + version: "0.2.1+2" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" + url: "https://pub.dev" + source: hosted + version: "2.10.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" intl: dependency: "direct main" description: @@ -568,6 +688,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" nested: dependency: transitive description: @@ -632,6 +760,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 + url: "https://pub.dev" + source: hosted + version: "12.0.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.dev" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" petitparser: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index be723ca..55d7098 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,6 +50,9 @@ dependencies: google_fonts: ^6.1.0 fluttertoast: ^8.2.12 provider: ^6.1.2 + image_picker: ^1.1.2 + cloudinary_public: ^0.23.1 + permission_handler: ^12.0.1 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index b6a8a84..0700cb5 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,17 +7,23 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { CloudFirestorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("CloudFirestorePluginCApi")); + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); FirebaseAuthPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); FirebaseCorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); GeolocatorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("GeolocatorWindows")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 609354a..ec3eabf 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,9 +4,11 @@ list(APPEND FLUTTER_PLUGIN_LIST cloud_firestore + file_selector_windows firebase_auth firebase_core geolocator_windows + permission_handler_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST