From 4e133b4cb89f2719d33693196c250a9e18412fc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20B=C3=B6sche?= Date: Fri, 13 Feb 2026 18:05:33 +0100 Subject: [PATCH] Add AddFeaturesDialog widget and enhance error handling in login form and account settings dialog --- b0esche_cloud/lib/pages/home_page.dart | 50 ++- b0esche_cloud/lib/pages/login_form.dart | 28 +- .../lib/widgets/account_settings_dialog.dart | 145 ++++++++- .../lib/widgets/add_features_dialog.dart | 284 ++++++++++++++++++ b0esche_cloud/pubspec.lock | 20 +- 5 files changed, 487 insertions(+), 40 deletions(-) create mode 100644 b0esche_cloud/lib/widgets/add_features_dialog.dart diff --git a/b0esche_cloud/lib/pages/home_page.dart b/b0esche_cloud/lib/pages/home_page.dart index 8b0aa96..436dedd 100644 --- a/b0esche_cloud/lib/pages/home_page.dart +++ b/b0esche_cloud/lib/pages/home_page.dart @@ -21,6 +21,7 @@ import 'login_form.dart' show LoginForm; import 'file_explorer.dart'; import '../widgets/organization_settings_dialog.dart'; import '../widgets/account_settings_dialog.dart'; +import '../widgets/add_features_dialog.dart'; import '../widgets/audio_player_bar.dart'; import '../injection.dart'; @@ -231,6 +232,13 @@ class _HomePageState extends State with TickerProviderStateMixin { ); } + void _showAddFeaturesDialog(BuildContext context) { + showDialog( + context: context, + builder: (dialogContext) => const AddFeaturesDialog(), + ); + } + Widget _buildOrgRow(BuildContext context) { return BlocBuilder( builder: (context, state) { @@ -384,6 +392,7 @@ class _HomePageState extends State with TickerProviderStateMixin { IconData icon, { bool isAvatar = false, VoidCallback? onTap, + bool showSoonBadge = false, }) { final isSelected = _selectedTab == label; final highlightColor = const Color.fromARGB(255, 100, 200, 255); @@ -391,7 +400,7 @@ class _HomePageState extends State with TickerProviderStateMixin { ? AppTheme.primaryText : AppTheme.secondaryText; - return GestureDetector( + Widget button = GestureDetector( onTap: onTap ?? () { @@ -508,6 +517,36 @@ class _HomePageState extends State with TickerProviderStateMixin { ], ), ); + + if (showSoonBadge) { + return Stack( + clipBehavior: Clip.none, + children: [ + button, + Positioned( + top: -6, + right: -12, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), + decoration: BoxDecoration( + color: AppTheme.accentColor, + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + 'soon', + style: TextStyle( + color: Colors.black, + fontSize: 8, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ); + } + + return button; } @override @@ -599,12 +638,17 @@ class _HomePageState extends State with TickerProviderStateMixin { children: [ _buildNavButton('Drive', Icons.cloud), const SizedBox(width: 16), - _buildNavButton('Mail', Icons.mail), + _buildNavButton( + 'Mail', + Icons.mail, + showSoonBadge: true, + ), const SizedBox(width: 16), _buildNavButton( 'Add', Icons.add, - onTap: () {}, + onTap: () => + _showAddFeaturesDialog(context), ), const SizedBox(width: 16), if (hasSelectedOrg) diff --git a/b0esche_cloud/lib/pages/login_form.dart b/b0esche_cloud/lib/pages/login_form.dart index 68b2b74..8929d82 100644 --- a/b0esche_cloud/lib/pages/login_form.dart +++ b/b0esche_cloud/lib/pages/login_form.dart @@ -354,20 +354,18 @@ class _LoginFormState extends State { isLoading: state is AuthLoading, onPressed: () { if (_usernameController.text.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Username is required'), - ), - ); + setState(() { + _usernameHasError = true; + _usernameErrorText = 'username is required'; + }); return; } if (_isSignup) { if (_passwordController.text.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Password is required'), - ), - ); + setState(() { + _passwordHasError = true; + _passwordErrorText = 'password is required'; + }); return; } context.read().add( @@ -387,11 +385,11 @@ class _LoginFormState extends State { ); } else { if (_passwordController.text.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Password is required'), - ), - ); + setState(() { + _passwordHasError = true; + _passwordErrorText = + 'password is required'; + }); return; } context.read().add( diff --git a/b0esche_cloud/lib/widgets/account_settings_dialog.dart b/b0esche_cloud/lib/widgets/account_settings_dialog.dart index 9be04b2..c9c6f6b 100644 --- a/b0esche_cloud/lib/widgets/account_settings_dialog.dart +++ b/b0esche_cloud/lib/widgets/account_settings_dialog.dart @@ -35,6 +35,14 @@ class _AccountSettingsDialogState extends State { late TextEditingController _newPasswordController; late TextEditingController _confirmPasswordController; + // Password field error state + bool _currentPasswordHasError = false; + bool _newPasswordHasError = false; + bool _confirmPasswordHasError = false; + String? _currentPasswordErrorText; + String? _newPasswordErrorText; + String? _confirmPasswordErrorText; + User? _currentUser; @override @@ -236,12 +244,50 @@ class _AccountSettingsDialogState extends State { } Future _changePassword() async { + // Clear previous errors + setState(() { + _currentPasswordHasError = false; + _newPasswordHasError = false; + _confirmPasswordHasError = false; + _currentPasswordErrorText = null; + _newPasswordErrorText = null; + _confirmPasswordErrorText = null; + }); + + // Validate current password + if (_currentPasswordController.text.isEmpty) { + setState(() { + _currentPasswordHasError = true; + _currentPasswordErrorText = 'current password is required'; + }); + return; + } + + // Validate new password + if (_newPasswordController.text.isEmpty) { + setState(() { + _newPasswordHasError = true; + _newPasswordErrorText = 'new password is required'; + }); + return; + } + + // Validate confirm password + if (_confirmPasswordController.text.isEmpty) { + setState(() { + _confirmPasswordHasError = true; + _confirmPasswordErrorText = 'please confirm your password'; + }); + return; + } + + // Validate passwords match if (_newPasswordController.text != _confirmPasswordController.text) { - if (mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Passwords do not match'))); - } + setState(() { + _newPasswordHasError = true; + _confirmPasswordHasError = true; + _confirmPasswordErrorText = 'passwords do not match'; + }); return; } @@ -258,15 +304,34 @@ class _AccountSettingsDialogState extends State { ); } - // Clear fields + // Clear fields and errors _currentPasswordController.clear(); _newPasswordController.clear(); _confirmPasswordController.clear(); + setState(() { + _currentPasswordHasError = false; + _newPasswordHasError = false; + _confirmPasswordHasError = false; + _currentPasswordErrorText = null; + _newPasswordErrorText = null; + _confirmPasswordErrorText = null; + }); } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to change password: $e')), - ); + final errorMsg = e.toString().toLowerCase(); + if (errorMsg.contains('incorrect') || + errorMsg.contains('invalid') || + errorMsg.contains('wrong')) { + setState(() { + _currentPasswordHasError = true; + _currentPasswordErrorText = 'incorrect password'; + _currentPasswordController.clear(); + }); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to change password: $e')), + ); + } } } finally { setState(() => _isLoading = false); @@ -784,7 +849,9 @@ class _AccountSettingsDialogState extends State { color: AppTheme.primaryBackground.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(16), border: Border.all( - color: AppTheme.accentColor.withValues(alpha: 0.3), + color: _currentPasswordHasError + ? Colors.red + : AppTheme.accentColor.withValues(alpha: 0.3), ), ), child: TextFormField( @@ -792,6 +859,15 @@ class _AccountSettingsDialogState extends State { obscureText: true, cursorColor: AppTheme.accentColor, style: TextStyle(color: AppTheme.primaryText), + onChanged: (_) { + if (_currentPasswordHasError || + _currentPasswordErrorText != null) { + setState(() { + _currentPasswordHasError = false; + _currentPasswordErrorText = null; + }); + } + }, decoration: InputDecoration( hintText: 'Enter current password', hintStyle: TextStyle(color: AppTheme.secondaryText), @@ -800,6 +876,14 @@ class _AccountSettingsDialogState extends State { ), ), ), + if (_currentPasswordErrorText != null) + Padding( + padding: const EdgeInsets.only(top: 6, left: 8), + child: Text( + _currentPasswordErrorText!, + style: const TextStyle(color: Colors.red, fontSize: 12), + ), + ), const SizedBox(height: 16), // New Password @@ -816,7 +900,9 @@ class _AccountSettingsDialogState extends State { color: AppTheme.primaryBackground.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(16), border: Border.all( - color: AppTheme.accentColor.withValues(alpha: 0.3), + color: _newPasswordHasError + ? Colors.red + : AppTheme.accentColor.withValues(alpha: 0.3), ), ), child: TextFormField( @@ -824,6 +910,14 @@ class _AccountSettingsDialogState extends State { obscureText: true, cursorColor: AppTheme.accentColor, style: TextStyle(color: AppTheme.primaryText), + onChanged: (_) { + if (_newPasswordHasError || _newPasswordErrorText != null) { + setState(() { + _newPasswordHasError = false; + _newPasswordErrorText = null; + }); + } + }, decoration: InputDecoration( hintText: 'Enter new password', hintStyle: TextStyle(color: AppTheme.secondaryText), @@ -832,6 +926,14 @@ class _AccountSettingsDialogState extends State { ), ), ), + if (_newPasswordErrorText != null) + Padding( + padding: const EdgeInsets.only(top: 6, left: 8), + child: Text( + _newPasswordErrorText!, + style: const TextStyle(color: Colors.red, fontSize: 12), + ), + ), const SizedBox(height: 16), // Confirm Password @@ -848,7 +950,9 @@ class _AccountSettingsDialogState extends State { color: AppTheme.primaryBackground.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(16), border: Border.all( - color: AppTheme.accentColor.withValues(alpha: 0.3), + color: _confirmPasswordHasError + ? Colors.red + : AppTheme.accentColor.withValues(alpha: 0.3), ), ), child: TextFormField( @@ -856,6 +960,15 @@ class _AccountSettingsDialogState extends State { obscureText: true, cursorColor: AppTheme.accentColor, style: TextStyle(color: AppTheme.primaryText), + onChanged: (_) { + if (_confirmPasswordHasError || + _confirmPasswordErrorText != null) { + setState(() { + _confirmPasswordHasError = false; + _confirmPasswordErrorText = null; + }); + } + }, decoration: InputDecoration( hintText: 'Confirm new password', hintStyle: TextStyle(color: AppTheme.secondaryText), @@ -864,6 +977,14 @@ class _AccountSettingsDialogState extends State { ), ), ), + if (_confirmPasswordErrorText != null) + Padding( + padding: const EdgeInsets.only(top: 6, left: 8), + child: Text( + _confirmPasswordErrorText!, + style: const TextStyle(color: Colors.red, fontSize: 12), + ), + ), const SizedBox(height: 24), // Change Password Button diff --git a/b0esche_cloud/lib/widgets/add_features_dialog.dart b/b0esche_cloud/lib/widgets/add_features_dialog.dart new file mode 100644 index 0000000..f488003 --- /dev/null +++ b/b0esche_cloud/lib/widgets/add_features_dialog.dart @@ -0,0 +1,284 @@ +import 'package:flutter/material.dart'; +import '../theme/app_theme.dart'; + +class AddFeaturesDialog extends StatelessWidget { + const AddFeaturesDialog({super.key}); + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: AppTheme.primaryBackground, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Container( + width: 500, + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + children: [ + Icon( + Icons.add_circle_outline, + color: AppTheme.accentColor, + size: 28, + ), + const SizedBox(width: 12), + Text( + 'Add Features', + style: TextStyle( + color: AppTheme.primaryText, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const Spacer(), + IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: Icon(Icons.close, color: AppTheme.secondaryText), + splashRadius: 20, + ), + ], + ), + const SizedBox(height: 8), + Text( + 'Enhance your workspace with additional features', + style: TextStyle(color: AppTheme.secondaryText, fontSize: 14), + ), + const SizedBox(height: 24), + + // Features list + _FeatureCard( + icon: Icons.calendar_month, + title: 'Calendar', + description: + 'Schedule events, set reminders, and manage your time efficiently. Sync with your team and never miss important deadlines.', + onAdd: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Calendar feature coming soon!'), + ), + ); + }, + ), + const SizedBox(height: 16), + _FeatureCard( + icon: Icons.dashboard_customize, + title: 'Board', + description: + 'Organize tasks with kanban-style boards. Create columns, drag cards, and track progress visually across your projects.', + onAdd: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Board feature coming soon!')), + ); + }, + ), + const SizedBox(height: 24), + + // Footer hint + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppTheme.accentColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: AppTheme.accentColor.withValues(alpha: 0.3), + ), + ), + child: Row( + children: [ + Icon( + Icons.info_outline, + color: AppTheme.accentColor, + size: 20, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'More features will be available soon. Stay tuned!', + style: TextStyle( + color: AppTheme.accentColor, + fontSize: 13, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class _FeatureCard extends StatefulWidget { + final IconData icon; + final String title; + final String description; + final VoidCallback onAdd; + + const _FeatureCard({ + required this.icon, + required this.title, + required this.description, + required this.onAdd, + }); + + @override + State<_FeatureCard> createState() => _FeatureCardState(); +} + +class _FeatureCardState extends State<_FeatureCard> { + bool _isHovered = false; + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: _isHovered + ? Colors.white.withValues(alpha: 0.08) + : Colors.white.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: _isHovered + ? AppTheme.accentColor.withValues(alpha: 0.5) + : Colors.white.withValues(alpha: 0.1), + ), + ), + child: Row( + children: [ + // Feature icon with soon badge + Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppTheme.accentColor.withValues(alpha: 0.3), + AppTheme.accentColor.withValues(alpha: 0.1), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + widget.icon, + color: AppTheme.accentColor, + size: 28, + ), + ), + Positioned( + top: -6, + right: -8, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 5, + vertical: 2, + ), + decoration: BoxDecoration( + color: AppTheme.accentColor, + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + 'soon', + style: TextStyle( + color: Colors.black, + fontSize: 9, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + const SizedBox(width: 16), + + // Feature details + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.title, + style: TextStyle( + color: AppTheme.primaryText, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + widget.description, + style: TextStyle( + color: AppTheme.secondaryText, + fontSize: 13, + height: 1.4, + ), + ), + ], + ), + ), + const SizedBox(width: 12), + + // Add button + _AddButton(onPressed: widget.onAdd), + ], + ), + ), + ); + } +} + +class _AddButton extends StatefulWidget { + final VoidCallback onPressed; + + const _AddButton({required this.onPressed}); + + @override + State<_AddButton> createState() => _AddButtonState(); +} + +class _AddButtonState extends State<_AddButton> { + bool _isHovered = false; + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + child: GestureDetector( + onTap: widget.onPressed, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 40, + height: 40, + decoration: BoxDecoration( + color: _isHovered + ? AppTheme.accentColor + : AppTheme.accentColor.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: AppTheme.accentColor.withValues(alpha: 0.5), + ), + ), + child: Icon( + Icons.add, + color: _isHovered ? Colors.black : AppTheme.accentColor, + size: 24, + ), + ), + ), + ); + } +} diff --git a/b0esche_cloud/pubspec.lock b/b0esche_cloud/pubspec.lock index 4b57cd5..46dfd56 100644 --- a/b0esche_cloud/pubspec.lock +++ b/b0esche_cloud/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -205,10 +205,10 @@ packages: dependency: transitive description: name: ffi - sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" file: dependency: transitive description: @@ -535,10 +535,10 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -988,10 +988,10 @@ packages: dependency: transitive description: name: url_launcher_ios - sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" url: "https://pub.dev" source: hosted - version: "6.3.6" + version: "6.4.1" url_launcher_linux: dependency: transitive description: @@ -1092,10 +1092,10 @@ packages: dependency: transitive description: name: video_player_avfoundation - sha256: f46e9e20f1fe429760cf4dc118761336320d1bec0f50d255930c2355f2defb5b + sha256: f93b93a3baa12ca0ff7d00ca8bc60c1ecd96865568a01ff0c18a99853ee201a5 url: "https://pub.dev" source: hosted - version: "2.9.1" + version: "2.9.3" video_player_platform_interface: dependency: transitive description: