diff --git a/lib/core/providers/supabase_provider.dart b/lib/core/providers/supabase_provider.dart index 03dba8f..d4e70c9 100644 --- a/lib/core/providers/supabase_provider.dart +++ b/lib/core/providers/supabase_provider.dart @@ -21,6 +21,7 @@ class SupabaseService extends StateHandler { if (user != null) { // Handle OAuth sign-in flow if (event == AuthChangeEvent.signedIn) { + _loginSnackShown = true; await _handleOAuthSignIn(user); } await _fetchCurrentUserDetails( @@ -39,10 +40,17 @@ class SupabaseService extends StateHandler { CurrentUser? _currentUser; // Holds the consolidated user data bool _isDark = false; + bool _loginSnackShown = false; bool get isDark => _isDark; CurrentUser? get currentUser => _currentUser; String get defaultPfpPath => _defaultPfpPath; + bool get loginSnackShown => _loginSnackShown; + + void setLoginSnackShown(bool value) { + _loginSnackShown = value; + notifyListeners(); + } // --- Theme Management --- Future _loadTheme() async { @@ -70,7 +78,9 @@ class SupabaseService extends StateHandler { // If it's an OAuth user and they don't have our custom metadata fields, // it's their first time. if (isOAuth && (currentAvatarUrl == null || currentName == null)) { - final String? name = user.userMetadata!['full_name'] as String? ?? user.userMetadata!['name'] as String?; + final String? name = + user.userMetadata!['full_name'] as String? ?? + user.userMetadata!['name'] as String?; final String? avatarUrl = user.userMetadata!['avatar_url'] as String?; final Map updatedData = {}; @@ -176,8 +186,10 @@ class SupabaseService extends StateHandler { // and added to your Supabase Auth Providers -> Google -> Redirect URIs // For desktop, usually 'http://localhost:port' or similar is used. final String? redirectUrl = - kIsWeb ? - kReleaseMode ? 'http://cookethflow.cookethcompany.xyz/dashboard' : 'http://localhost:3000/dashboard' + kIsWeb + ? kReleaseMode + ? 'http://cookethflow.cookethcompany.xyz/dashboard' + : 'http://localhost:3000/dashboard' : (Platform.isAndroid || Platform.isIOS ? 'myapp://login-callback/' : null); // For mobile/desktop @@ -201,8 +213,10 @@ class SupabaseService extends StateHandler { Future signInWithGithub() async { try { final String? redirectUrl = - kIsWeb ? - kReleaseMode ? 'http://cookethflow.cookethcompany.xyz/dashboard' : 'http://localhost:3000/dashboard' + kIsWeb + ? kReleaseMode + ? 'http://cookethflow.cookethcompany.xyz/dashboard' + : 'http://localhost:3000/dashboard' : (Platform.isAndroid || Platform.isIOS ? 'my.scheme://my-host' : null); // Replace with your actual scheme @@ -390,8 +404,7 @@ class SupabaseService extends StateHandler { } // --- Profile Picture Management --- - final String _profileBucketName = - 'profile'; // Renamed bucket for clarity + final String _profileBucketName = 'profile'; // Renamed bucket for clarity final String _defaultPfpPath = 'assets/images/pfp.png'; // Make sure this asset exists! @@ -505,4 +518,4 @@ class SupabaseService extends StateHandler { } return null; } -} \ No newline at end of file +} diff --git a/lib/core/utils/enums.dart b/lib/core/utils/enums.dart index 03405b3..93b201f 100644 --- a/lib/core/utils/enums.dart +++ b/lib/core/utils/enums.dart @@ -65,4 +65,6 @@ enum InteractionMode { drawingConnector, } -enum ConnectorAnchor { top, bottom, left, right } \ No newline at end of file +enum ConnectorAnchor { top, bottom, left, right } + +enum SnackbarType { success, error, info, warning } diff --git a/lib/features/auth/widgets/login_form.dart b/lib/features/auth/widgets/login_form.dart index 7c62365..b5a1208 100644 --- a/lib/features/auth/widgets/login_form.dart +++ b/lib/features/auth/widgets/login_form.dart @@ -5,6 +5,7 @@ import 'package:cookethflow/core/providers/supabase_provider.dart'; import 'package:cookethflow/core/router/app_route_const.dart'; import 'package:cookethflow/core/theme/colors.dart'; import 'package:cookethflow/features/auth/providers/auth_provider.dart'; +import 'package:cookethflow/features/dashboard/widgets/snackbar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:go_router/go_router.dart'; @@ -33,9 +34,7 @@ class LoginForm extends StatelessWidget { // Check if current route is not already dashboard to prevent loop context.go(RoutesPath.dashboard); // context.goNamed(RouteName.dashboard,pathParameters: {'username': supabaseService.currentUser!.name!}); - authProvider.setLoading( - false, - ); + authProvider.setLoading(false); }); } @@ -150,7 +149,7 @@ class LoginForm extends StatelessWidget { authProvider.obscurePassword ? PhosphorIconsRegular.eye : PhosphorIconsRegular.eyeSlash, - size: 24.sp, + size: isMobile ? 45.sp : 24.sp, ), onPressed: authProvider.toggleObscurePassword, style: ButtonStyle( @@ -193,19 +192,12 @@ class LoginForm extends StatelessWidget { password: authProvider.passwordController.text, ); if (res != "Logged in successfully") { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(res), - duration: const Duration(seconds: 5), - ), - ); + CustomSnackbar.showError(context, res); } } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(passwordValidationCheck), - duration: const Duration(seconds: 2), - ), + CustomSnackbar.showError( + context, + passwordValidationCheck, ); } // Navigation handled by the Consumer2's listener @@ -265,10 +257,9 @@ class LoginForm extends StatelessWidget { onPressed: () async { // Call void method, loading handled by provider await authProvider.googleAuth(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Initiating Google Sign-In..."), - ), + CustomSnackbar.showInfo( + context, + "Initiating Google Sign-In...", ); // Navigation handled by the Consumer2's listener }, @@ -316,10 +307,9 @@ class LoginForm extends StatelessWidget { onPressed: () async { // Call void method, loading handled by provider await authProvider.githubSignin(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Initiating GitHub Sign-In..."), - ), + CustomSnackbar.showInfo( + context, + "Initiating GitHub Sign-In...", ); // Navigation handled by the Consumer2's listener }, @@ -377,12 +367,9 @@ class LoginForm extends StatelessWidget { onPressed: () async { // Call void method, loading handled by provider await authProvider.googleAuth(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - "Initiating Google Sign-In...", - ), - ), + CustomSnackbar.showInfo( + context, + "Initiating Google Sign-In...", ); // Navigation handled by the Consumer2's listener }, @@ -436,12 +423,9 @@ class LoginForm extends StatelessWidget { onPressed: () async { // Call void method, loading handled by provider await authProvider.githubSignin(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - "Initiating GitHub Sign-In...", - ), - ), + CustomSnackbar.showInfo( + context, + "Initiating GitHub Sign-In...", ); // Navigation handled by the Consumer2's listener }, diff --git a/lib/features/dashboard/pages/desktop/dashboard_desktop.dart b/lib/features/dashboard/pages/desktop/dashboard_desktop.dart index 965bb02..4d20504 100644 --- a/lib/features/dashboard/pages/desktop/dashboard_desktop.dart +++ b/lib/features/dashboard/pages/desktop/dashboard_desktop.dart @@ -3,6 +3,7 @@ import 'package:cookethflow/features/dashboard/pages/desktop/short_cut_setting.d import 'package:cookethflow/features/dashboard/providers/dashboard_provider.dart'; import 'package:cookethflow/features/dashboard/widgets/dashboard_drawer.dart'; import 'package:cookethflow/features/dashboard/widgets/project_card.dart'; +import 'package:cookethflow/features/dashboard/widgets/snackbar.dart'; import 'package:cookethflow/features/dashboard/widgets/start_project.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -10,9 +11,32 @@ import 'package:provider/provider.dart'; import 'package:cookethflow/core/helpers/responsive_layout.helper.dart' as rh; import 'package:cookethflow/core/utils/enums.dart' as en; -class DashboardDesktop extends StatelessWidget { +class DashboardDesktop extends StatefulWidget { const DashboardDesktop({super.key}); + @override + State createState() => _DashboardDesktopState(); +} + +class _DashboardDesktopState extends State { + @override + void initState() { + super.initState(); + + // Show snackbar after the first frame is rendered + WidgetsBinding.instance.addPostFrameCallback((_) { + final su = Provider.of(context, listen: false); + if (su.loginSnackShown) { + CustomSnackbar.showSuccess(context, "Successfully Logged in"); + Future.delayed(Duration(seconds: 4), () { + if (mounted) { + context.read().setLoginSnackShown(false); + } + }); + } + }); + } + @override Widget build(BuildContext context) { en.DeviceType deviceType = rh.ResponsiveLayoutHelper.getDeviceType(context); @@ -48,9 +72,12 @@ class DashboardDesktop extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - Padding( - padding: EdgeInsets.only(bottom: 20.h), - child: const StartProject(), + Visibility( + visible: provider.tabIndex <= 1, + child: Padding( + padding: EdgeInsets.only(bottom: 20.h), + child: const StartProject(), + ), ), const SizedBox(height: 32), Expanded( diff --git a/lib/features/dashboard/pages/mobile/dashboard_mobile.dart b/lib/features/dashboard/pages/mobile/dashboard_mobile.dart index d0b7ec2..d6886da 100644 --- a/lib/features/dashboard/pages/mobile/dashboard_mobile.dart +++ b/lib/features/dashboard/pages/mobile/dashboard_mobile.dart @@ -3,6 +3,7 @@ import 'package:cookethflow/features/dashboard/pages/desktop/short_cut_setting.d import 'package:cookethflow/features/dashboard/pages/mobile/drawer_mobile.dart'; import 'package:cookethflow/features/dashboard/providers/dashboard_provider.dart'; import 'package:cookethflow/features/dashboard/widgets/project_card.dart'; +import 'package:cookethflow/features/dashboard/widgets/snackbar.dart'; import 'package:cookethflow/features/dashboard/widgets/start_project.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -20,6 +21,24 @@ class DashboardMobile extends StatefulWidget { class _DashboardMobileState extends State { bool isVisible = false; + @override + void initState() { + super.initState(); + + // Show snackbar after the first frame is rendered + WidgetsBinding.instance.addPostFrameCallback((_) { + final su = Provider.of(context, listen: false); + if (su.loginSnackShown) { + CustomSnackbar.showSuccess(context, "Successfully Logged in"); + Future.delayed(Duration(seconds: 4), () { + if (mounted) { + context.read().setLoginSnackShown(false); + } + }); + } + }); + } + @override Widget build(BuildContext context) { en.DeviceType deviceType = rh.ResponsiveLayoutHelper.getDeviceType(context); @@ -57,7 +76,13 @@ class _DashboardMobileState extends State { child: Icon(Icons.menu, size: 30), ), ), - const StartProject(), + Visibility( + visible: provider.tabIndex <= 1, + child: Padding( + padding: EdgeInsets.only(bottom: 20.h), + child: const StartProject(), + ), + ), ], ), const SizedBox(height: 34), diff --git a/lib/features/dashboard/pages/mobile/drawer_mobile.dart b/lib/features/dashboard/pages/mobile/drawer_mobile.dart index 464b671..d570570 100644 --- a/lib/features/dashboard/pages/mobile/drawer_mobile.dart +++ b/lib/features/dashboard/pages/mobile/drawer_mobile.dart @@ -1,6 +1,7 @@ import 'package:cookethflow/core/helpers/responsive_layout.helper.dart' as rh; import 'package:cookethflow/core/providers/supabase_provider.dart'; import 'package:cookethflow/core/theme/colors.dart'; +import 'package:cookethflow/features/dashboard/pages/mobile/edit_profile_mobile.dart'; import 'package:cookethflow/features/dashboard/providers/dashboard_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -124,7 +125,7 @@ class DashboardDrawerMob extends StatelessWidget { showDialog( context: context, builder: - (context) => const ProfileSettingsWidget(), + (context) => const ProfileSettingsWidgetMob(), ); }, icon: Icon( diff --git a/lib/features/dashboard/pages/mobile/edit_profile_mobile.dart b/lib/features/dashboard/pages/mobile/edit_profile_mobile.dart new file mode 100644 index 0000000..670cad0 --- /dev/null +++ b/lib/features/dashboard/pages/mobile/edit_profile_mobile.dart @@ -0,0 +1,494 @@ +import 'dart:io'; + +import 'package:cookethflow/core/providers/supabase_provider.dart'; +import 'package:cookethflow/core/theme/colors.dart'; // Ensure this is correctly imported +import 'package:cookethflow/features/dashboard/providers/dashboard_provider.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:cookethflow/features/dashboard/widgets/delete_account.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:provider/provider.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:image_picker/image_picker.dart'; + +import 'package:cookethflow/core/helpers/responsive_layout.helper.dart' as rh; +import 'package:cookethflow/core/utils/enums.dart' as en; + +class ProfileSettingsWidgetMob extends StatefulWidget { + const ProfileSettingsWidgetMob({super.key}); + + @override + State createState() => + _ProfileSettingsWidgetMobState(); +} + +class _ProfileSettingsWidgetMobState extends State { + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _emailController = TextEditingController(); + final TextEditingController _usernameController = TextEditingController(); + XFile? _selectedImage; // To hold the newly selected image for upload + + @override + void dispose() { + _nameController.dispose(); + _emailController.dispose(); + _usernameController.dispose(); + super.dispose(); + } + + Future _pickImage() async { + final ImagePicker picker = ImagePicker(); + final XFile? image = await picker.pickImage(source: ImageSource.gallery); + if (image != null) { + setState(() { + _selectedImage = image; + }); + } + } + + Future _saveChanges() async { + final supabaseService = Provider.of( + context, + listen: false, + ); + + // Update Name and Username + if (_nameController.text != supabaseService.currentUser!.name || + _usernameController.text != supabaseService.currentUser!.username) { + try { + final res = await supabaseService.updateUserName( + newName: _nameController.text, + newUsername: _usernameController.text, + ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(res))); + } catch (e) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(e.toString()))); + } + } + + // Update Email + if (_emailController.text != supabaseService.currentUser!.email) { + try { + final res = await supabaseService.updateUserEmail( + email: _emailController.text, + ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(res))); + } catch (e) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(e.toString()))); + } + } + + // Upload Profile Picture if a new one was selected + if (_selectedImage != null) { + try { + await supabaseService.uploadUserProfilePicture(_selectedImage!); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Profile picture updated!')), + ); + setState(() { + _selectedImage = null; // Clear selected image after upload + }); + } catch (e) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(e.toString()))); + } + } + + // Refresh user data from Supabase to ensure UI is in sync + await supabaseService.supabase.auth.refreshSession(); + } + + @override + Widget build(BuildContext context) { + en.DeviceType deviceType = rh.ResponsiveLayoutHelper.getDeviceType(context); + bool isDesk = deviceType == en.DeviceType.desktop ? true : false; + + return Consumer2( + builder: (context, dashboardProvider, supabaseService, child) { + final currentUser = supabaseService.currentUser; + + // Update controllers with the current state of the provider + _nameController.text = currentUser?.name ?? ''; + _emailController.text = currentUser?.email ?? ''; + _usernameController.text = currentUser?.username ?? ''; + + final String displayAvatarUrl = + _selectedImage != null + ? _selectedImage!.path + : currentUser?.avatarUrl ?? ''; + + return Dialog( + backgroundColor: Theme.of(context).cardColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Container( + width: 1.5.sw, + height: 1.3.sh, + padding: + deviceType == en.DeviceType.desktop + ? const EdgeInsets.all(35) + : const EdgeInsets.only(top: 16, left: 10, right: 10), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Align( + alignment: Alignment.topRight, + child: IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(Icons.close, size: 28), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ), + const SizedBox(height: 8), + // Profile section + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Stack( + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: Theme.of(context).dividerColor, + width: 2, + ), + ), + child: ClipOval( + child: + displayAvatarUrl.isNotEmpty + ? (_selectedImage != null + ? (kIsWeb + ? Image.network( + displayAvatarUrl, + fit: BoxFit.cover, + ) + : Image.file( + File(displayAvatarUrl), + fit: BoxFit.cover, + )) + : CachedNetworkImage( + imageUrl: displayAvatarUrl, + fit: BoxFit.cover, + placeholder: + (context, url) => + const CircularProgressIndicator(), + errorWidget: + (context, url, error) => + Image.asset( + supabaseService + .defaultPfpPath, + fit: BoxFit.cover, + ), + )) + : Image.asset( + supabaseService.defaultPfpPath, + fit: BoxFit.cover, + ), // Default fallback + ), + ), + Positioned( + bottom: 0, + right: 0, + child: InkWell( + onTap: _pickImage, + child: Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + shape: BoxShape.circle, + border: Border.all( + color: Theme.of(context).cardColor, + width: 2, + ), + ), + child: Icon( + Icons.edit, + size: 14, + color: Theme.of(context).cardColor, + ), + ), + ), + ), + ], + ), + const SizedBox(width: 18), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + currentUser?.name ?? 'Loading Name...', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: + Theme.of(context).textTheme.titleLarge?.color, + ), + ), + const SizedBox(height: 2), + Text( + currentUser?.username ?? 'Loading Username...', + style: TextStyle( + fontSize: 14, + color: Theme.of(context).textTheme.bodySmall?.color, + ), + ), + ], + ), + ], + ), + const SizedBox(height: 10), + _buildField( + context, + label: 'Name', + controller: _nameController, + ), + const SizedBox(height: 8), + // Form fields + _buildField( + context, + label: 'Username', + controller: _usernameController, + ), + const SizedBox(height: 8), + _buildField( + context, + label: 'Email', + controller: _emailController, + ), + const SizedBox(height: 16), + + // Theme toggle section + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Theme', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: + Theme.of(context).textTheme.titleLarge?.color, + ), + ), + deviceType == en.DeviceType.desktop + ? const SizedBox(height: 4) + : const SizedBox(height: 2), + Text( + 'Toggle between light and dark mode', + style: TextStyle( + fontSize: 12, + color: Theme.of(context).textTheme.bodySmall?.color, + ), + ), + ], + ), + Row( + children: [ + Icon( + supabaseService.isDark + ? Icons.dark_mode + : Icons.light_mode, + color: secondaryColors[7], + size: 10, + ), + const SizedBox(height: 4), + Switch( + value: supabaseService.isDark, + onChanged: (value) => supabaseService.toggleTheme(), + activeColor: Theme.of(context).primaryColor, + ), + ], + ), + ], + ), + + const SizedBox(height: 12), + + // Buttons + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 18, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide(color: secondaryColors[7]), + ), + ), + child: Text( + 'Cancel', + style: TextStyle( + color: secondaryColors[7], + fontWeight: FontWeight.w500, + ), + ), + ), + const SizedBox(width: 16), + ElevatedButton( + onPressed: _saveChanges, // Call the save changes function + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).primaryColor, + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 18, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + elevation: 0, + ), + child: Text( + 'Save all changes', + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimary, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + + const SizedBox(height: 12), + Divider(color: Theme.of(context).dividerColor), + const SizedBox(height: 10), + + // Delete account + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Delete Account', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context).textTheme.titleLarge?.color, + ), + ), + const SizedBox(height: 3), + Text( + 'Permanently delete your account', + style: TextStyle( + fontSize: 12, + color: Theme.of(context).textTheme.bodySmall?.color, + ), + ), + ], + ), + const SizedBox(height: 4), + Visibility( + visible: !isDesk, + child: Center( + child: ElevatedButton( + onPressed: () { + showDialog( + context: context, + builder: (context) => const DeleteAccountDialog(), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: secondaryColors[1], + padding: EdgeInsets.symmetric( + horizontal: 10, + vertical: 5, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + elevation: 0, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.delete_outline, + size: 16, + color: Colors.white, + ), + const SizedBox(width: 8), + Text( + 'Delete Account', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + } + + Widget _buildField( + BuildContext context, { + required String label, + required TextEditingController controller, + double? width, + }) { + return SizedBox( + width: width, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context).textTheme.titleLarge?.color, + ), + ), + const SizedBox(height: 8), + TextFormField( + controller: controller, // Use the provided controller + decoration: InputDecoration( + filled: true, + fillColor: Theme.of(context).cardColor, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Theme.of(context).dividerColor), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/dashboard/widgets/edit_profile.dart b/lib/features/dashboard/widgets/edit_profile.dart index 2303880..7db7c73 100644 --- a/lib/features/dashboard/widgets/edit_profile.dart +++ b/lib/features/dashboard/widgets/edit_profile.dart @@ -13,6 +13,7 @@ import 'package:image_picker/image_picker.dart'; import 'package:cookethflow/core/helpers/responsive_layout.helper.dart' as rh; import 'package:cookethflow/core/utils/enums.dart' as en; + class ProfileSettingsWidget extends StatefulWidget { const ProfileSettingsWidget({super.key}); @@ -130,10 +131,10 @@ class _ProfileSettingsWidgetState extends State { borderRadius: BorderRadius.circular(16), ), child: Container( - width: deviceType == en.DeviceType.desktop ? 500 : 1.6.sw, + width: deviceType == en.DeviceType.desktop ? 0.57.sw : 1.6.sw, padding: deviceType == en.DeviceType.desktop - ? const EdgeInsets.all(24) + ? const EdgeInsets.all(35) : const EdgeInsets.only(top: 16, left: 10, right: 10), child: Column( mainAxisSize: MainAxisSize.min, @@ -144,23 +145,24 @@ class _ProfileSettingsWidgetState extends State { alignment: Alignment.topRight, child: IconButton( onPressed: () => Navigator.of(context).pop(), - icon: const Icon(Icons.close, size: 24), + icon: const Icon(Icons.close, size: 32), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ), deviceType == en.DeviceType.desktop - ? const SizedBox(height: 16) + ? const SizedBox(height: 26) : const SizedBox(height: 8), // Profile section Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Stack( children: [ Container( - width: 80, - height: 80, + width: 100, + height: 100, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( @@ -228,8 +230,8 @@ class _ProfileSettingsWidgetState extends State { ], ), deviceType == en.DeviceType.desktop - ? const SizedBox(width: 16) - : const SizedBox(width: 8), + ? const SizedBox(width: 26) + : const SizedBox(width: 18), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -254,6 +256,15 @@ class _ProfileSettingsWidgetState extends State { ), ], ), + const SizedBox(width: 200), + Expanded( + child: _buildField( + context, + label: 'Name', + controller: _nameController, + ), + ), + const SizedBox(width: 16), ], ), @@ -263,17 +274,24 @@ class _ProfileSettingsWidgetState extends State { // Form fields Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - _buildField( - context, - label: 'Name', - controller: _nameController, + // Username field + Expanded( + child: _buildField( + context, + label: 'Username', + controller: _usernameController, + width: 250, + ), ), - const SizedBox(width: 16), - _buildField( - context, - label: 'Email', - controller: _emailController, + const SizedBox(width: 50), + Expanded( + child: _buildField( + context, + label: 'Email', + controller: _emailController, + ), ), ], ), @@ -282,14 +300,6 @@ class _ProfileSettingsWidgetState extends State { ? const SizedBox(height: 20) : const SizedBox(height: 10), - // Username field - _buildField( - context, - label: 'Username', - controller: _usernameController, - width: 250, - ), - deviceType == en.DeviceType.desktop ? const SizedBox(height: 32) : const SizedBox(height: 16), @@ -329,7 +339,7 @@ class _ProfileSettingsWidgetState extends State { supabaseService.isDark ? Icons.dark_mode : Icons.light_mode, - color: Theme.of(context).iconTheme.color, + color: secondaryColors[7], size: deviceType == en.DeviceType.desktop ? 20 : 10, ), deviceType == en.DeviceType.desktop @@ -351,37 +361,36 @@ class _ProfileSettingsWidgetState extends State { // Buttons Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: () => Navigator.of(context).pop(), style: TextButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: 24, - vertical: 12, + vertical: 18, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), - side: BorderSide( - color: Theme.of(context).dividerColor, - ), + side: BorderSide(color: secondaryColors[7]), ), ), child: Text( 'Cancel', style: TextStyle( - color: Theme.of(context).textTheme.titleLarge?.color, + color: secondaryColors[7], fontWeight: FontWeight.w500, ), ), ), + const SizedBox(width: 16), ElevatedButton( onPressed: _saveChanges, // Call the save changes function style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context).primaryColor, padding: const EdgeInsets.symmetric( horizontal: 24, - vertical: 12, + vertical: 18, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), @@ -449,9 +458,9 @@ class _ProfileSettingsWidgetState extends State { backgroundColor: secondaryColors[1], padding: EdgeInsets.symmetric( horizontal: - deviceType == en.DeviceType.desktop ? 20 : 10, + deviceType == en.DeviceType.desktop ? 24 : 10, vertical: - deviceType == en.DeviceType.desktop ? 10 : 5, + deviceType == en.DeviceType.desktop ? 18 : 5, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), @@ -541,39 +550,36 @@ class _ProfileSettingsWidgetState extends State { required TextEditingController controller, double? width, }) { - return Expanded( - flex: width != null ? 0 : 1, - child: SizedBox( - width: width, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Theme.of(context).textTheme.titleLarge?.color, - ), + return SizedBox( + width: width, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context).textTheme.titleLarge?.color, ), - const SizedBox(height: 8), - TextFormField( - controller: controller, // Use the provided controller - decoration: InputDecoration( - filled: true, - fillColor: Theme.of(context).cardColor, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: Theme.of(context).dividerColor), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), + ), + const SizedBox(height: 8), + TextFormField( + controller: controller, // Use the provided controller + decoration: InputDecoration( + filled: true, + fillColor: Theme.of(context).cardColor, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Theme.of(context).dividerColor), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, ), ), - ], - ), + ), + ], ), ); } diff --git a/lib/features/dashboard/widgets/snackbar.dart b/lib/features/dashboard/widgets/snackbar.dart new file mode 100644 index 0000000..535c164 --- /dev/null +++ b/lib/features/dashboard/widgets/snackbar.dart @@ -0,0 +1,265 @@ +import 'package:cookethflow/core/theme/colors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:cookethflow/core/helpers/responsive_layout.helper.dart' as rh; +import 'package:cookethflow/core/utils/enums.dart' as en; + +class CustomSnackbar { + CustomSnackbar._(); + static void showSuccess( + BuildContext context, + String message, { + int duration = 3, + }) { + _show( + context, + message: message, + type: en.SnackbarType.success, + duration: duration, + ); + } + + static void showError( + BuildContext context, + String message, { + int duration = 3, + }) { + _show( + context, + message: message, + type: en.SnackbarType.error, + duration: duration, + ); + } + + static void showInfo( + BuildContext context, + String message, { + int duration = 3, + }) { + _show( + context, + message: message, + type: en.SnackbarType.info, + duration: duration, + ); + } + + static void showWarning( + BuildContext context, + String message, { + int duration = 3, + }) { + _show( + context, + message: message, + type: en.SnackbarType.warning, + duration: duration, + ); + } + + static void _show( + BuildContext context, { + required String message, + required en.SnackbarType type, + required int duration, + }) { + final overlay = Overlay.of(context); + late OverlayEntry overlayEntry; + + overlayEntry = OverlayEntry( + builder: + (context) => Positioned( + bottom: 30.h, + left: 0, + right: 0, + child: _SnackbarWidget( + message: message, + type: type, + duration: duration, + onDismiss: () { + overlayEntry.remove(); + }, + ), + ), + ); + + overlay.insert(overlayEntry); + } +} + +class _SnackbarWidget extends StatefulWidget { + final String message; + final en.SnackbarType type; + final int duration; + final VoidCallback onDismiss; + + const _SnackbarWidget({ + required this.message, + required this.type, + required this.duration, + required this.onDismiss, + }); + + @override + State<_SnackbarWidget> createState() => _SnackbarWidgetState(); +} + +class _SnackbarWidgetState extends State<_SnackbarWidget> + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _slideAnimation; + late Animation _fadeAnimation; + + @override + void initState() { + super.initState(); + + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 600), + ); + + _slideAnimation = Tween( + begin: const Offset(0, 3), + end: Offset.zero, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut)); + + _fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn)); + + _controller.forward(); + + Future.delayed(Duration(seconds: widget.duration), () { + if (mounted) { + _controller.reverse().then((_) { + if (mounted) widget.onDismiss(); + }); + } + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + Color _getBackgroundColor() { + switch (widget.type) { + case en.SnackbarType.success: + return secondaryColors[0]; // Green for successfull events + case en.SnackbarType.error: + return secondaryColors[1]; // Red for error + case en.SnackbarType.info: + return secondaryColors[6]; // Blue for information + case en.SnackbarType.warning: + return secondaryColors[7]; // Orange for warning + } + } + + IconData _getIcon() { + switch (widget.type) { + case en.SnackbarType.success: + return Icons.check_circle_rounded; + case en.SnackbarType.error: + return Icons.error_rounded; + case en.SnackbarType.info: + return Icons.info_rounded; + case en.SnackbarType.warning: + return Icons.warning_rounded; + } + } + + @override + Widget build(BuildContext context) { + en.DeviceType deviceType = rh.ResponsiveLayoutHelper.getDeviceType(context); + final screenWidth = MediaQuery.of(context).size.width; + final isDesktop = deviceType == en.DeviceType.desktop; + final isTablet = deviceType == en.DeviceType.tab; + + return SlideTransition( + position: _slideAnimation, + child: FadeTransition( + opacity: _fadeAnimation, + child: Center( + child: GestureDetector( + onTap: () { + _controller.reverse().then((_) { + if (mounted) widget.onDismiss(); + }); + }, + child: Material( + color: Colors.transparent, + child: Container( + constraints: BoxConstraints( + maxWidth: + isDesktop + ? 400 + : isTablet + ? 350 + : screenWidth * 0.9, + minHeight: isDesktop ? 60 : 70, + ), + margin: EdgeInsets.symmetric(horizontal: 20.w), + padding: EdgeInsets.symmetric( + horizontal: isDesktop ? 20 : 16, + vertical: isDesktop ? 16 : 14, + ), + decoration: BoxDecoration( + color: _getBackgroundColor(), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 15, + offset: const Offset(0, 8), + spreadRadius: 2, + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + _getIcon(), + color: Colors.white, + size: isDesktop ? 28 : 32, + ), + SizedBox(width: isDesktop ? 12 : 14), + Expanded( + child: Text( + widget.message, + style: TextStyle( + fontSize: isDesktop ? 16 : 15, + color: Colors.white, + fontWeight: FontWeight.w600, + height: 1.4, + ), + ), + ), + SizedBox(width: 8), + GestureDetector( + onTap: () { + _controller.reverse().then((_) { + if (mounted) widget.onDismiss(); + }); + }, + child: Icon( + Icons.close, + color: Colors.white.withOpacity(0.9), + size: isDesktop ? 20 : 22, + ), + ), + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/dashboard/widgets/start_project.dart b/lib/features/dashboard/widgets/start_project.dart index 3d09ae3..368f5cb 100644 --- a/lib/features/dashboard/widgets/start_project.dart +++ b/lib/features/dashboard/widgets/start_project.dart @@ -27,7 +27,7 @@ class StartProject extends StatelessWidget { ? EdgeInsets.symmetric(vertical: 35.h, horizontal: 24.w) : deviceType == en.DeviceType.tab ? EdgeInsets.symmetric(vertical: 35.h, horizontal: 24.w) - : EdgeInsets.symmetric(vertical: 20.h, horizontal: 24.w), + : EdgeInsets.symmetric(vertical: 30.h, horizontal: 34.w), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12.r), ), @@ -44,13 +44,22 @@ class StartProject extends StatelessWidget { ? 18.sp : deviceType == en.DeviceType.tab ? 25.sp - : 35.sp, + : 65.sp, color: Colors.white, fontWeight: FontWeight.w500, ), ), SizedBox(width: 16.w), - Icon(PhosphorIconsRegular.plus, color: Colors.white, size: 24.sp), + Icon( + PhosphorIconsRegular.plus, + color: Colors.white, + size: + deviceType == en.DeviceType.desktop + ? 24.sp + : deviceType == en.DeviceType.tab + ? 24.sp + : 44.sp, + ), ], ), ); diff --git a/lib/features/workspace/widgets/node_editing_toolbox.dart b/lib/features/workspace/widgets/node_editing_toolbox.dart index db17afe..ca26f2f 100644 --- a/lib/features/workspace/widgets/node_editing_toolbox.dart +++ b/lib/features/workspace/widgets/node_editing_toolbox.dart @@ -1,5 +1,6 @@ +import 'package:cookethflow/core/helpers/responsive_layout.helper.dart' as rh; import 'package:cookethflow/core/providers/supabase_provider.dart'; -import 'package:cookethflow/core/utils/enums.dart'; +import 'package:cookethflow/core/utils/enums.dart' as en; import 'package:cookethflow/features/models/canvas_models/objects/connector_object.dart'; import 'package:cookethflow/features/models/canvas_models/objects/sticky_note_object.dart'; import 'package:cookethflow/features/models/canvas_models/objects/text_box_object.dart'; @@ -8,6 +9,7 @@ import 'package:cookethflow/features/workspace/widgets/connector_customizer.dart import 'package:cookethflow/features/workspace/widgets/node_colour.dart'; import 'package:cookethflow/features/workspace/widgets/node_picker.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; import 'package:provider/provider.dart'; @@ -61,8 +63,11 @@ class NodeEditingToolbox extends StatelessWidget { final isConnector = object is ConnectorObject; final isShape = - object is! TextBoxObject && object is! ConnectorObject && object is! StickyNoteObject; - final canChangeColor = object is! TextBoxObject && object is! ConnectorObject; + object is! TextBoxObject && + object is! ConnectorObject && + object is! StickyNoteObject; + final canChangeColor = + object is! TextBoxObject && object is! ConnectorObject; if (isConnector) { buttons.add( @@ -72,15 +77,16 @@ class NodeEditingToolbox extends StatelessWidget { onPressed: () { showDialog( context: context, - builder: (context) => ConnectorCustomizer( - initialColor: object.color, - initialType: object.connectionType, - initialThickness: object.thickness, - onStyleSelected: (color, type, thickness) { - provider.changeConnectorStyle(color, type, thickness); - }, - su: su, - ), + builder: + (context) => ConnectorCustomizer( + initialColor: object.color, + initialType: object.connectionType, + initialThickness: object.thickness, + onStyleSelected: (color, type, thickness) { + provider.changeConnectorStyle(color, type, thickness); + }, + su: su, + ), ); }, tooltip: 'Change Connector Style', @@ -96,17 +102,18 @@ class NodeEditingToolbox extends StatelessWidget { onPressed: () { showDialog( context: context, - builder: (context) => NodeColourPicker( - initialColor: object.color, - onColorSelected: (color) { - provider.changeConnectorStyle( - color, - object.connectionType, - object.thickness, - ); - Navigator.of(context).pop(); - }, - ), + builder: + (context) => NodeColourPicker( + initialColor: object.color, + onColorSelected: (color) { + provider.changeConnectorStyle( + color, + object.connectionType, + object.thickness, + ); + Navigator.of(context).pop(); + }, + ), ); }, tooltip: 'Change Color', @@ -114,73 +121,154 @@ class NodeEditingToolbox extends StatelessWidget { ), ); } - // } else if (isShape) { - // buttons.add( - // _buildIconButton( - // context, - // icon: PhosphorIcons.shapes(), - // onPressed: () { - // showDialog( - // context: context, - // builder: (context) => NodePicker( - // onShapeSelected: (shapeType) { - // provider.changeObjectShape(shapeType); - // Navigator.of(context).pop(); - // }, - // su: su, - // ), - // ); - // }, - // tooltip: 'Change Shape', - // su: su, - // ), - // ); - // if (canChangeColor) { - // buttons.add(_buildDivider()); - // buttons.add( - // _buildIconButton( - // context, - // icon: PhosphorIcons.paintBucket(), - // onPressed: () { - // showDialog( - // context: context, - // builder: (context) => NodeColourPicker( - // initialColor: object.color, - // onColorSelected: (color) { - // provider.changeObjectColor(color); - // Navigator.of(context).pop(); - // }, - // ), - // ); - // }, - // tooltip: 'Change Color', - // su: su, - // ), - // ); - // } + // } else if (isShape) { + // buttons.add( + // _buildIconButton( + // context, + // icon: PhosphorIcons.shapes(), + // onPressed: () { + // showDialog( + // context: context, + // builder: (context) => NodePicker( + // onShapeSelected: (shapeType) { + // provider.changeObjectShape(shapeType); + // Navigator.of(context).pop(); + // }, + // su: su, + // ), + // ); + // }, + // tooltip: 'Change Shape', + // su: su, + // ), + // ); + // if (canChangeColor) { + // buttons.add(_buildDivider()); + // buttons.add( + // _buildIconButton( + // context, + // icon: PhosphorIcons.paintBucket(), + // onPressed: () { + // showDialog( + // context: context, + // builder: (context) => NodeColourPicker( + // initialColor: object.color, + // onColorSelected: (color) { + // provider.changeObjectColor(color); + // Navigator.of(context).pop(); + // }, + // ), + // ); + // }, + // tooltip: 'Change Color', + // su: su, + // ), + // ); + // } } else if (canChangeColor) { buttons.add( _buildIconButton( context, - icon: PhosphorIcons.paintBucket(), + icon: PhosphorIconsRegular.circlesThreePlus, + onPressed: () { + final device = rh.ResponsiveLayoutHelper.getDeviceType(context); + _showNodePicker(context, device, su, provider); + }, + tooltip: "Node Types", + su: su, + ), + ); + buttons.add(_buildDivider()); + buttons.add( + _buildIconButton( + context, + icon: PhosphorIconsRegular.circle, onPressed: () { showDialog( context: context, - builder: (context) => NodeColourPicker( - initialColor: object.color, - onColorSelected: (color) { - provider.changeObjectColor(color); - Navigator.of(context).pop(); - }, - ), + builder: + (context) => NodeColourPicker( + initialColor: object.color, + onColorSelected: (color) { + provider.changeObjectColor(color); + Navigator.of(context).pop(); + }, + ), ); }, - tooltip: 'Change Color', + tooltip: "Customize node colours", + su: su, + color: Color.fromRGBO(195, 177, 225, 1), + ), + ); + buttons.add( + _buildIconButton( + context, + icon: PhosphorIconsRegular.textAlignCenter, + onPressed: () {}, + tooltip: "Customize node outline", + su: su, + ), + ); + buttons.add(_buildDivider()); + buttons.add( + _buildIconButton( + context, + icon: PhosphorIcons.textAa(), + onPressed: () {}, + tooltip: "Font style", + su: su, + ), + ); + buttons.add(_buildDivider()); + buttons.add( + _buildIconButton( + context, + icon: PhosphorIcons.textB(), + onPressed: () {}, + tooltip: "Font add-ons", + su: su, + ), + ); + buttons.add( + _buildIconButton( + context, + icon: PhosphorIcons.textItalic(), + onPressed: () {}, + tooltip: "Font add-ons", + su: su, + ), + ); + buttons.add( + _buildIconButton( + context, + icon: PhosphorIcons.textUnderline(), + onPressed: () {}, + tooltip: "Font add-ons", + su: su, + ), + ); + buttons.add( + _buildIconButton( + context, + icon: PhosphorIcons.textStrikethrough(), + onPressed: () {}, + tooltip: "Font add-ons", + su: su, + ), + ); + buttons.add(_buildDivider()); + buttons.add( + _buildIconButton( + context, + icon: PhosphorIcons.link(), + onPressed: () {}, + tooltip: "Add links", su: su, ), ); } - + // Always add the delete button if there's an object selected if (buttons.isNotEmpty) { buttons.add(_buildDivider()); @@ -219,4 +307,52 @@ class NodeEditingToolbox extends StatelessWidget { ), ); } -} \ No newline at end of file + + void _showNodePicker( + BuildContext context, + en.DeviceType device, + SupabaseService su, + WorkspaceProvider wp, + ) { + final RenderBox? renderBox = context.findRenderObject() as RenderBox?; + if (renderBox == null) return; + + final position = renderBox.localToGlobal(Offset.zero); + final nodePickerWidth = + device == en.DeviceType.desktop + ? 340 + : device == en.DeviceType.tab + ? 340 + : 300; + final padding = 20.w; + + showDialog( + context: context, + barrierColor: Colors.transparent, + builder: (context) { + double topPos; + double leftPos; + if (device == en.DeviceType.mobile) { + topPos = position.dy; + leftPos = position.dx; + } else { + topPos = position.dy + 10.h; + leftPos = position.dx; + } + return Stack( + children: [ + Positioned.fill(child: Container(color: Colors.transparent)), + Positioned( + top: topPos, + left: leftPos, + child: Material( + color: Colors.transparent, + child: NodePicker(su: su), + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/features/workspace/widgets/sticky_notes.dart b/lib/features/workspace/widgets/sticky_notes.dart index dd5bc1b..a737e00 100644 --- a/lib/features/workspace/widgets/sticky_notes.dart +++ b/lib/features/workspace/widgets/sticky_notes.dart @@ -31,7 +31,7 @@ class StickyNotesWidget extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, - children: [ + children: [ // Header Row( mainAxisAlignment: MainAxisAlignment.spaceBetween,