From 0453bff9aa6a720e55a9f3dcbfc324a6c21fe552 Mon Sep 17 00:00:00 2001 From: Subhajit Roy Date: Thu, 7 Aug 2025 15:35:42 +0530 Subject: [PATCH 1/5] Fixed peofile image issue --- android/app/src/main/AndroidManifest.xml | 3 + ios/Runner/Info.plist | 4 + lib/models/user_model.dart | 6 + lib/services/auth_service.dart | 64 +++++ lib/services/cloudinary_service.dart | 42 +++ lib/views/screens/profile_screen.dart | 254 ++++++++++++++++-- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 176 ++++++++++++ pubspec.yaml | 3 + .../flutter/generated_plugin_registrant.cc | 6 + windows/flutter/generated_plugins.cmake | 2 + 13 files changed, 549 insertions(+), 18 deletions(-) create mode 100644 lib/services/cloudinary_service.dart 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/models/user_model.dart b/lib/models/user_model.dart index e929680..83b0789 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,7 @@ 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 +70,7 @@ class UserModel { bool? signupCompleted, DateTime? createdAt, String? fcmToken, + String? profileImage, // Added to copyWith }) { return UserModel( uid: uid ?? this.uid, @@ -78,6 +83,7 @@ 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..5211163 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -3,10 +3,12 @@ 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(); @@ -95,6 +97,68 @@ class AuthService { }); } + /// Updates the current user's profile image URL in Firestore + /// Note: Old images are not deleted from Cloudinary due to client-side limitations + Future updateProfileImage(String newImageUrl) async { + final uid = _auth.currentUser?.uid; + if (uid == null) { + throw Exception('No user logged in'); + } + + try { + // Update Firestore with new image URL + 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; + } + } + + /// Removes the current user's profile image URL from Firestore + /// Note: The actual image is not deleted from Cloudinary due to client-side limitations + Future removeProfileImage() async { + final uid = _auth.currentUser?.uid; + if (uid == null) { + throw Exception('No user logged in'); + } + + try { + // Remove profile image field from Firestore + 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; + } + } + + /// Gets the current user's profile image URL + 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..20ea03c --- /dev/null +++ b/lib/services/cloudinary_service.dart @@ -0,0 +1,42 @@ +import 'dart:io'; +import 'package:cloudinary_public/cloudinary_public.dart'; +import 'package:flutter/foundation.dart'; + +class CloudinaryService { + static const String _cloudName = 'YOUR_CLOUD_NAME'; // Replace with your Cloudinary cloud name + static const String _uploadPreset = 'YOUR_UPLOAD_PRESET'; // Replace with your upload preset + + final CloudinaryPublic _cloudinary = CloudinaryPublic( + _cloudName, + _uploadPreset, + cache: false, + ); + + /// Uploads an image file to Cloudinary and returns the secure URL + /// Returns null if upload fails + 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; + } + } + +/// Note: The cloudinary_public package doesn't support image deletion +/// from the client side. Image deletion should be handled from your backend +/// or through Cloudinary's admin API for security reasons. +/// +/// For now, we'll just keep the old image URLs in the database +/// and let Cloudinary handle unused image cleanup through their +/// auto-moderation features or manual cleanup. +} diff --git a/lib/views/screens/profile_screen.dart b/lib/views/screens/profile_screen.dart index c3bda90..3e10c9d 100644 --- a/lib/views/screens/profile_screen.dart +++ b/lib/views/screens/profile_screen.dart @@ -4,10 +4,234 @@ 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; + + Future _pickAndUploadImage() async { + try { + final XFile? image = await _imagePicker.pickImage( + source: ImageSource.gallery, + maxWidth: 800, + maxHeight: 800, + imageQuality: 85, + ); + + if (image == null) return; + + setState(() { + _isUploading = true; + }); + + // Upload to Cloudinary + final imageUrl = await _cloudinaryService.uploadImage(File(image.path)); + + if (imageUrl != null) { + // Update user profile with new image URL + await _authService.updateProfileImage(imageUrl); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Profile picture updated successfully!'), + backgroundColor: Colors.green, + ), + ); + // Refresh the screen to show new image + 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; + }); + } + } + } + + 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; + }); + + // Upload to Cloudinary + final imageUrl = await _cloudinaryService.uploadImage(File(image.path)); + + if (imageUrl != null) { + // Update user profile with new image URL + await _authService.updateProfileImage(imageUrl); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Profile picture updated successfully!'), + backgroundColor: Colors.green, + ), + ); + // Refresh the screen to show new image + 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.withOpacity(0.5), + 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 +244,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 +265,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; @@ -56,14 +282,14 @@ class ProfileScreen extends StatelessWidget { final createdAtRaw = data['createdAt']; final dobFormatted = - dobRaw != null - ? _formatDate(DateTime.tryParse(dobRaw) ?? DateTime.now()) - : 'N/A'; + dobRaw != null + ? _formatDate(DateTime.tryParse(dobRaw) ?? DateTime.now()) + : 'N/A'; final createdAtFormatted = - createdAtRaw != null - ? _formatDate((createdAtRaw as Timestamp).toDate()) - : 'N/A'; + createdAtRaw != null + ? _formatDate((createdAtRaw as Timestamp).toDate()) + : 'N/A'; return Column( children: [ @@ -81,15 +307,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 +365,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/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 From 261f8211fc4658ef6854b4d3a943026dccf71f56 Mon Sep 17 00:00:00 2001 From: Subhajit Roy Date: Thu, 7 Aug 2025 15:54:27 +0530 Subject: [PATCH 2/5] Removed unnecessary code --- lib/services/auth_service.dart | 7 --- lib/services/cloudinary_service.dart | 14 +----- lib/views/screens/profile_screen.dart | 65 +-------------------------- 3 files changed, 3 insertions(+), 83 deletions(-) diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 5211163..95f7c65 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -97,8 +97,6 @@ class AuthService { }); } - /// Updates the current user's profile image URL in Firestore - /// Note: Old images are not deleted from Cloudinary due to client-side limitations Future updateProfileImage(String newImageUrl) async { final uid = _auth.currentUser?.uid; if (uid == null) { @@ -106,7 +104,6 @@ class AuthService { } try { - // Update Firestore with new image URL await _firestore.collection('users').doc(uid).update({ 'profileImage': newImageUrl, 'updatedAt': FieldValue.serverTimestamp(), @@ -119,8 +116,6 @@ class AuthService { } } - /// Removes the current user's profile image URL from Firestore - /// Note: The actual image is not deleted from Cloudinary due to client-side limitations Future removeProfileImage() async { final uid = _auth.currentUser?.uid; if (uid == null) { @@ -128,7 +123,6 @@ class AuthService { } try { - // Remove profile image field from Firestore await _firestore.collection('users').doc(uid).update({ 'profileImage': FieldValue.delete(), 'updatedAt': FieldValue.serverTimestamp(), @@ -141,7 +135,6 @@ class AuthService { } } - /// Gets the current user's profile image URL Future getProfileImageUrl() async { final uid = _auth.currentUser?.uid; if (uid == null) return null; diff --git a/lib/services/cloudinary_service.dart b/lib/services/cloudinary_service.dart index 20ea03c..b510ea2 100644 --- a/lib/services/cloudinary_service.dart +++ b/lib/services/cloudinary_service.dart @@ -3,8 +3,8 @@ import 'package:cloudinary_public/cloudinary_public.dart'; import 'package:flutter/foundation.dart'; class CloudinaryService { - static const String _cloudName = 'YOUR_CLOUD_NAME'; // Replace with your Cloudinary cloud name - static const String _uploadPreset = 'YOUR_UPLOAD_PRESET'; // Replace with your upload preset + static const String _cloudName = 'YOUR_CLOUD_NAME'; + static const String _uploadPreset = 'YOUR_UPLOAD_PRESET'; final CloudinaryPublic _cloudinary = CloudinaryPublic( _cloudName, @@ -12,8 +12,6 @@ class CloudinaryService { cache: false, ); - /// Uploads an image file to Cloudinary and returns the secure URL - /// Returns null if upload fails Future uploadImage(File imageFile) async { try { // Upload the image with folder organization @@ -31,12 +29,4 @@ class CloudinaryService { return null; } } - -/// Note: The cloudinary_public package doesn't support image deletion -/// from the client side. Image deletion should be handled from your backend -/// or through Cloudinary's admin API for security reasons. -/// -/// For now, we'll just keep the old image URLs in the database -/// and let Cloudinary handle unused image cleanup through their -/// auto-moderation features or manual cleanup. } diff --git a/lib/views/screens/profile_screen.dart b/lib/views/screens/profile_screen.dart index 3e10c9d..7c6cf7e 100644 --- a/lib/views/screens/profile_screen.dart +++ b/lib/views/screens/profile_screen.dart @@ -22,66 +22,6 @@ class _ProfileScreenState extends State { final ImagePicker _imagePicker = ImagePicker(); bool _isUploading = false; - Future _pickAndUploadImage() async { - try { - final XFile? image = await _imagePicker.pickImage( - source: ImageSource.gallery, - maxWidth: 800, - maxHeight: 800, - imageQuality: 85, - ); - - if (image == null) return; - - setState(() { - _isUploading = true; - }); - - // Upload to Cloudinary - final imageUrl = await _cloudinaryService.uploadImage(File(image.path)); - - if (imageUrl != null) { - // Update user profile with new image URL - await _authService.updateProfileImage(imageUrl); - - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Profile picture updated successfully!'), - backgroundColor: Colors.green, - ), - ); - // Refresh the screen to show new image - 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; - }); - } - } - } - void _showImageSourceDialog() { showModalBottomSheet( context: context, @@ -132,11 +72,9 @@ class _ProfileScreenState extends State { _isUploading = true; }); - // Upload to Cloudinary final imageUrl = await _cloudinaryService.uploadImage(File(image.path)); if (imageUrl != null) { - // Update user profile with new image URL await _authService.updateProfileImage(imageUrl); if (mounted) { @@ -146,7 +84,6 @@ class _ProfileScreenState extends State { backgroundColor: Colors.green, ), ); - // Refresh the screen to show new image setState(() {}); } } else { @@ -202,7 +139,7 @@ class _ProfileScreenState extends State { width: 100, height: 100, decoration: BoxDecoration( - color: Colors.black.withOpacity(0.5), + color: Colors.black.withAlpha(128), shape: BoxShape.circle, ), child: const CircularProgressIndicator( From ba152904d4cc8c978a8691389c66da6b47385723 Mon Sep 17 00:00:00 2001 From: Subhajit Roy Date: Thu, 7 Aug 2025 16:01:11 +0530 Subject: [PATCH 3/5] Ran dart format . --- lib/components/bottom_nav_bar.dart | 32 +-- lib/components/financial_chart.dart | 138 ++++++----- lib/models/auth_state.dart | 3 +- lib/models/financial_entry_model.dart | 20 +- lib/models/user_model.dart | 6 +- lib/services/auth_service.dart | 28 +-- lib/services/firestore_service.dart | 9 +- lib/services/validation_service.dart | 52 ++-- lib/viewmodels/auth_viewmodel.dart | 35 ++- .../screens/athlete/athlete_dashboard.dart | 50 ++-- .../screens/athlete/calendar_screen.dart | 232 ++++++++++-------- .../athlete/injury_tracker_screen.dart | 106 ++++---- .../athlete/performance_logs_screen.dart | 107 ++++---- .../screens/athlete/tournaments_screen.dart | 227 ++++++++--------- lib/views/screens/auth_screen.dart | 13 +- lib/views/screens/coach/coach_dashboard.dart | 2 +- .../screens/doctor/doctor_dashboard.dart | 70 +++--- .../organization/add_tournament_screen.dart | 14 +- lib/views/screens/privacy_terms_screen.dart | 57 +---- lib/views/screens/profile_screen.dart | 28 +-- lib/views/screens/splash_screen.dart | 14 +- lib/views/widgets/custom_input_field.dart | 28 ++- 22 files changed, 650 insertions(+), 621 deletions(-) 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 83b0789..b94f2ce 100644 --- a/lib/models/user_model.dart +++ b/lib/models/user_model.dart @@ -55,7 +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 + if (profileImage != null) + 'profileImage': profileImage, // Added profile image to Firestore }; } @@ -83,7 +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 + profileImage: + profileImage ?? this.profileImage, // Added profile image to copyWith ); } } diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 95f7c65..25cb36e 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -14,9 +14,9 @@ class AuthService { 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, @@ -24,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, @@ -72,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); @@ -89,9 +89,9 @@ class AuthService { } Future updateEmailVerificationStatus( - String uid, - bool isVerified, - ) async { + String uid, + bool isVerified, + ) async { await _firestore.collection('users').doc(uid).update({ 'emailVerified': isVerified, }); 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 495f126..4b32237 100644 --- a/lib/views/screens/athlete/athlete_dashboard.dart +++ b/lib/views/screens/athlete/athlete_dashboard.dart @@ -117,10 +117,7 @@ class _DashboardScreenState extends State gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [ - Color(0xFF667EEA), - Color(0xFF764BA2), - ], + colors: [Color(0xFF667EEA), Color(0xFF764BA2)], ), ), ), @@ -204,10 +201,7 @@ class _DashboardScreenState extends State const SizedBox(height: 8), const Text( 'Please try refreshing the page', - style: TextStyle( - fontSize: 14, - color: Colors.grey, - ), + style: TextStyle(fontSize: 14, color: Colors.grey), ), const SizedBox(height: 24), ElevatedButton.icon( @@ -249,7 +243,12 @@ class _DashboardScreenState extends State Widget _buildWelcomeCard(String name, String sport, String dob) { final hour = DateTime.now().hour; - String greeting = hour < 12 ? 'Good Morning' : hour < 17 ? 'Good Afternoon' : 'Good Evening'; + String greeting = + hour < 12 + ? 'Good Morning' + : hour < 17 + ? 'Good Afternoon' + : 'Good Evening'; return Container( width: double.infinity, @@ -257,10 +256,7 @@ class _DashboardScreenState extends State gradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [ - Color(0xFFFFFFFF), - Color(0xFFF8FAFC), - ], + colors: [Color(0xFFFFFFFF), Color(0xFFF8FAFC)], ), borderRadius: BorderRadius.circular(20), boxShadow: [ @@ -350,11 +346,7 @@ class _DashboardScreenState extends State child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon( - icon, - size: 16, - color: const Color(0xFF667EEA), - ), + Icon(icon, size: 16, color: const Color(0xFF667EEA)), const SizedBox(width: 6), Text( text, @@ -493,11 +485,7 @@ class _DashboardScreenState 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( @@ -633,8 +621,18 @@ class _DashboardScreenState extends State try { final date = DateTime.parse(dateStr); final months = [ - 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', - 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', ]; return '${months[date.month - 1]} ${date.day}'; } catch (e) { @@ -646,4 +644,4 @@ class _DashboardScreenState extends State final uid = FirebaseAuth.instance.currentUser!.uid; return await FirebaseFirestore.instance.collection('users').doc(uid).get(); } -} \ No newline at end of file +} 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/injury_tracker_screen.dart b/lib/views/screens/athlete/injury_tracker_screen.dart index bcd4e24..ac4efe1 100644 --- a/lib/views/screens/athlete/injury_tracker_screen.dart +++ b/lib/views/screens/athlete/injury_tracker_screen.dart @@ -71,21 +71,29 @@ class _InjuryTrackerScreenState extends State { Future _deleteInjury(String docId) async { final confirm = await showDialog( context: context, - builder: (_) => AlertDialog( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - title: const Text("Delete Injury"), - content: const Text("Are you sure you want to delete this injury entry?"), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text("Cancel"), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - child: const Text("Delete", style: TextStyle(color: Colors.red)), + builder: + (_) => AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + title: const Text("Delete Injury"), + content: const Text( + "Are you sure you want to delete this injury entry?", + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text("Cancel"), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text( + "Delete", + style: TextStyle(color: Colors.red), + ), + ), + ], ), - ], - ), ); if (confirm == true) { @@ -106,9 +114,10 @@ class _InjuryTrackerScreenState extends State { DateTime? modalInjuryDate = _injuryDate; // Set initial text for date field for immediate display - _dateController.text = modalInjuryDate != null - ? DateFormat('yyyy-MM-dd').format(modalInjuryDate) - : ''; + _dateController.text = + modalInjuryDate != null + ? DateFormat('yyyy-MM-dd').format(modalInjuryDate) + : ''; return Padding( padding: EdgeInsets.only( @@ -129,14 +138,17 @@ class _InjuryTrackerScreenState extends State { if (picked != null) { setModalState(() { modalInjuryDate = picked; - _dateController.text = DateFormat('yyyy-MM-dd').format(picked); + _dateController.text = DateFormat( + 'yyyy-MM-dd', + ).format(picked); }); setState(() => _injuryDate = picked); } } final isFormValid = - _injuryController.text.trim().isNotEmpty && modalInjuryDate != null; + _injuryController.text.trim().isNotEmpty && + modalInjuryDate != null; return SingleChildScrollView( child: Column( @@ -144,9 +156,7 @@ class _InjuryTrackerScreenState extends State { children: [ Text( "Add Injury", - style: Theme.of(context) - .textTheme - .headlineSmall + style: Theme.of(context).textTheme.headlineSmall ?.copyWith(color: Colors.black87), ), const SizedBox(height: 18), @@ -192,8 +202,10 @@ class _InjuryTrackerScreenState extends State { _clearForm(); }, style: OutlinedButton.styleFrom( - side: - const BorderSide(color: Colors.grey, width: 1.3), + side: const BorderSide( + color: Colors.grey, + width: 1.3, + ), backgroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( @@ -213,16 +225,19 @@ class _InjuryTrackerScreenState extends State { const SizedBox(width: 16), Expanded( child: OutlinedButton( - onPressed: isFormValid - ? () async { - await _addInjury(); - } - : null, + onPressed: + isFormValid + ? () async { + await _addInjury(); + } + : null, style: OutlinedButton.styleFrom( backgroundColor: Colors.white, side: BorderSide( color: - isFormValid ? const Color(0xFF1565C0) : Colors.grey, + isFormValid + ? const Color(0xFF1565C0) + : Colors.grey, width: 1.6, ), padding: const EdgeInsets.symmetric(vertical: 14), @@ -233,9 +248,10 @@ class _InjuryTrackerScreenState extends State { child: Text( 'Add', style: TextStyle( - color: isFormValid - ? const Color(0xFF1565C0) - : Colors.grey, + color: + isFormValid + ? const Color(0xFF1565C0) + : Colors.grey, fontWeight: FontWeight.bold, fontSize: 16, ), @@ -298,9 +314,7 @@ class _InjuryTrackerScreenState extends State { final uid = _auth.currentUser?.uid; if (uid == null) { - return const Scaffold( - body: Center(child: Text("Not logged in")), - ); + return const Scaffold(body: Center(child: Text("Not logged in"))); } return Scaffold( @@ -318,11 +332,12 @@ class _InjuryTrackerScreenState extends State { iconTheme: const IconThemeData(color: Colors.black87), ), body: StreamBuilder( - stream: _firestore - .collection('injuries') - .where('uid', isEqualTo: uid) - .orderBy('createdAt', descending: true) - .snapshots(), + stream: + _firestore + .collection('injuries') + .where('uid', isEqualTo: uid) + .orderBy('createdAt', descending: true) + .snapshots(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); @@ -340,17 +355,14 @@ class _InjuryTrackerScreenState extends State { return ListView.builder( padding: const EdgeInsets.symmetric(vertical: 8), itemCount: docs.length, - itemBuilder: (context, index) => - _buildInjuryCard(context, docs[index]), + itemBuilder: + (context, index) => _buildInjuryCard(context, docs[index]), ); }, ), floatingActionButton: FloatingActionButton.extended( onPressed: () => _showAddInjurySheet(context), - icon: const Icon( - Icons.add, - color: Color(0xFF1565C0), - ), + icon: const Icon(Icons.add, color: Color(0xFF1565C0)), label: const Text( "Add Injury", style: TextStyle( diff --git a/lib/views/screens/athlete/performance_logs_screen.dart b/lib/views/screens/athlete/performance_logs_screen.dart index 4e4d2cd..f964cb6 100644 --- a/lib/views/screens/athlete/performance_logs_screen.dart +++ b/lib/views/screens/athlete/performance_logs_screen.dart @@ -86,22 +86,29 @@ class _PerformanceLogScreenState extends State Future _deleteLog(String docId) async { final confirm = await showDialog( context: context, - builder: (context) => AlertDialog( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - title: const Text("Delete Log"), - content: - const Text("Are you sure you want to delete this performance log entry?"), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text("Cancel"), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - child: const Text("Delete", style: TextStyle(color: Colors.red)), + builder: + (context) => AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + title: const Text("Delete Log"), + content: const Text( + "Are you sure you want to delete this performance log entry?", + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text("Cancel"), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text( + "Delete", + style: TextStyle(color: Colors.red), + ), + ), + ], ), - ], - ), ); if (confirm == true) { @@ -122,7 +129,9 @@ class _PerformanceLogScreenState extends State DateTime? modalLogDate = _logDate; _dateController.text = - modalLogDate != null ? DateFormat('yyyy-MM-dd').format(modalLogDate) : ''; + modalLogDate != null + ? DateFormat('yyyy-MM-dd').format(modalLogDate) + : ''; return Padding( padding: EdgeInsets.only( @@ -143,7 +152,9 @@ class _PerformanceLogScreenState extends State if (picked != null) { setModalState(() { modalLogDate = picked; - _dateController.text = DateFormat('yyyy-MM-dd').format(picked); + _dateController.text = DateFormat( + 'yyyy-MM-dd', + ).format(picked); }); setState(() { _logDate = picked; @@ -157,10 +168,12 @@ class _PerformanceLogScreenState extends State children: [ Text( "Add Performance Log", - style: Theme.of(context) - .textTheme - .headlineSmall - ?.copyWith(fontWeight: FontWeight.bold, color: Colors.black87), + style: Theme.of( + context, + ).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + color: Colors.black87, + ), ), const SizedBox(height: 20), TextField( @@ -198,24 +211,24 @@ class _PerformanceLogScreenState extends State _isLoading ? const CircularProgressIndicator() : ElevatedButton( - onPressed: _isFormValid ? _addLog : null, - style: ElevatedButton.styleFrom( - minimumSize: const Size.fromHeight(48), - backgroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + onPressed: _isFormValid ? _addLog : null, + style: ElevatedButton.styleFrom( + minimumSize: const Size.fromHeight(48), + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 2, + ), + child: Text( + "Submit", + style: TextStyle( + color: _isFormValid ? Colors.black : Colors.grey, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), ), - elevation: 2, - ), - child: Text( - "Submit", - style: TextStyle( - color: _isFormValid ? Colors.black : Colors.grey, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - ), const SizedBox(height: 12), ], ), @@ -264,9 +277,7 @@ class _PerformanceLogScreenState extends State final uid = _auth.currentUser?.uid; if (uid == null) { - return const Scaffold( - body: Center(child: Text("Not logged in")), - ); + return const Scaffold(body: Center(child: Text("Not logged in"))); } return Scaffold( @@ -281,11 +292,12 @@ class _PerformanceLogScreenState extends State iconTheme: const IconThemeData(color: Colors.black87), ), body: StreamBuilder( - stream: _firestore - .collection('performance_logs') - .where('uid', isEqualTo: uid) - .orderBy('createdAt', descending: true) - .snapshots(), + stream: + _firestore + .collection('performance_logs') + .where('uid', isEqualTo: uid) + .orderBy('createdAt', descending: true) + .snapshots(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); @@ -297,7 +309,8 @@ class _PerformanceLogScreenState extends State return ListView.builder( itemCount: docs.length, - itemBuilder: (context, index) => _buildLogCard(context, docs[index]), + itemBuilder: + (context, index) => _buildLogCard(context, docs[index]), ); }, ), 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 7c6cf7e..eee81a7 100644 --- a/lib/views/screens/profile_screen.dart +++ b/lib/views/screens/profile_screen.dart @@ -123,16 +123,12 @@ class _ProfileScreenState extends State { 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, + backgroundImage: + profileImageUrl != null ? NetworkImage(profileImageUrl) : null, + child: + profileImageUrl == null + ? Icon(Icons.person, size: 60, color: Colors.blue.shade700) + : null, ), if (_isUploading) Container( @@ -219,14 +215,14 @@ class _ProfileScreenState extends State { final createdAtRaw = data['createdAt']; final dobFormatted = - dobRaw != null - ? _formatDate(DateTime.tryParse(dobRaw) ?? DateTime.now()) - : 'N/A'; + dobRaw != null + ? _formatDate(DateTime.tryParse(dobRaw) ?? DateTime.now()) + : 'N/A'; final createdAtFormatted = - createdAtRaw != null - ? _formatDate((createdAtRaw as Timestamp).toDate()) - : 'N/A'; + createdAtRaw != null + ? _formatDate((createdAtRaw as Timestamp).toDate()) + : 'N/A'; return Column( children: [ diff --git a/lib/views/screens/splash_screen.dart b/lib/views/screens/splash_screen.dart index 23bc213..40c5726 100644 --- a/lib/views/screens/splash_screen.dart +++ b/lib/views/screens/splash_screen.dart @@ -18,7 +18,8 @@ class SplashScreen extends StatefulWidget { } /// State for [SplashScreen] that manages animation and navigation logic. -class _SplashScreenState extends State with TickerProviderStateMixin { +class _SplashScreenState extends State + with TickerProviderStateMixin { late final AnimationController _controller; @override @@ -39,7 +40,11 @@ class _SplashScreenState extends State with TickerProviderStateMix if (user != null) { try { - final doc = await FirebaseFirestore.instance.collection('users').doc(user.uid).get(); + final doc = + await FirebaseFirestore.instance + .collection('users') + .doc(user.uid) + .get(); final data = doc.data(); if (data == null || data['role'] == null) { throw Exception("User role not found"); @@ -125,10 +130,7 @@ class _SplashScreenState extends State with TickerProviderStateMix const Text( 'Your Sports Journey Starts Here', textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - color: Colors.black54, - ), + style: TextStyle(fontSize: 16, color: Colors.black54), ), ], ), 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 { ); } } - - From 28fa3de46cbc7b818bb691a828d468510fc7b1a2 Mon Sep 17 00:00:00 2001 From: Subhajit Roy Date: Mon, 11 Aug 2025 18:42:54 +0530 Subject: [PATCH 4/5] Modified Fetchquote and children[] --- lib/views/screens/athlete/athlete_dashboard.dart | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) 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), From da06a711108d9924a6089eb83d4e2f1afa2a3fe3 Mon Sep 17 00:00:00 2001 From: Subhajit Roy Date: Mon, 11 Aug 2025 18:44:43 +0530 Subject: [PATCH 5/5] Modified _cloudName and _uploadPreset --- lib/services/cloudinary_service.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/services/cloudinary_service.dart b/lib/services/cloudinary_service.dart index b510ea2..6ad8cd2 100644 --- a/lib/services/cloudinary_service.dart +++ b/lib/services/cloudinary_service.dart @@ -3,8 +3,8 @@ import 'package:cloudinary_public/cloudinary_public.dart'; import 'package:flutter/foundation.dart'; class CloudinaryService { - static const String _cloudName = 'YOUR_CLOUD_NAME'; - static const String _uploadPreset = 'YOUR_UPLOAD_PRESET'; + static const String _cloudName = 'dgiqmo1t1'; + static const String _uploadPreset = 'flutter_unsigned'; final CloudinaryPublic _cloudinary = CloudinaryPublic( _cloudName,