From dd29d2f90fc16fed02089741526bc0da0572f297 Mon Sep 17 00:00:00 2001 From: Marc Rejohn Castillano Date: Sun, 22 Feb 2026 14:46:59 +0800 Subject: [PATCH] Improved sign up screen --- lib/screens/auth/signup_screen.dart | 393 +++++++++++++++++----------- 1 file changed, 240 insertions(+), 153 deletions(-) diff --git a/lib/screens/auth/signup_screen.dart b/lib/screens/auth/signup_screen.dart index b1469362..e7507bbd 100644 --- a/lib/screens/auth/signup_screen.dart +++ b/lib/screens/auth/signup_screen.dart @@ -85,166 +85,242 @@ class _SignUpScreenState extends ConsumerState { body: ResponsiveBody( maxWidth: 480, padding: const EdgeInsets.symmetric(vertical: 24), - child: Form( - key: _formKey, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center( - child: Column( + child: SingleChildScrollView( + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Column( + children: [ + Image.asset('assets/tasq_ico.png', height: 72, width: 72), + const SizedBox(height: 12), + Text( + 'TasQ', + style: Theme.of(context).textTheme.headlineSmall, + ), + ], + ), + ), + const SizedBox(height: 24), + TextFormField( + controller: _fullNameController, + decoration: const InputDecoration(labelText: 'Full name'), + textInputAction: TextInputAction.next, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Full name is required.'; + } + return null; + }, + ), + const SizedBox(height: 12), + TextFormField( + controller: _emailController, + decoration: const InputDecoration(labelText: 'Email'), + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.next, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Email is required.'; + } + return null; + }, + ), + const SizedBox(height: 12), + TextFormField( + controller: _passwordController, + decoration: const InputDecoration(labelText: 'Password'), + obscureText: true, + textInputAction: TextInputAction.next, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Password is required.'; + } + if (value.length < 6) { + return 'Use at least 6 characters.'; + } + return null; + }, + ), + const SizedBox(height: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Image.asset('assets/tasq_ico.png', height: 72, width: 72), - const SizedBox(height: 12), Text( - 'TasQ', - style: Theme.of(context).textTheme.headlineSmall, + 'Password strength: $_passwordStrengthLabel', + style: Theme.of(context).textTheme.labelMedium, + ), + const SizedBox(height: 6), + LinearProgressIndicator( + value: _passwordStrength, + minHeight: 8, + borderRadius: BorderRadius.circular(8), + color: _passwordStrengthColor, + backgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, ), ], ), - ), - const SizedBox(height: 24), - TextFormField( - controller: _fullNameController, - decoration: const InputDecoration(labelText: 'Full name'), - textInputAction: TextInputAction.next, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Full name is required.'; - } - return null; - }, - ), - const SizedBox(height: 12), - TextFormField( - controller: _emailController, - decoration: const InputDecoration(labelText: 'Email'), - keyboardType: TextInputType.emailAddress, - textInputAction: TextInputAction.next, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Email is required.'; - } - return null; - }, - ), - const SizedBox(height: 12), - TextFormField( - controller: _passwordController, - decoration: const InputDecoration(labelText: 'Password'), - obscureText: true, - textInputAction: TextInputAction.next, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Password is required.'; - } - if (value.length < 6) { - return 'Use at least 6 characters.'; - } - return null; - }, - ), - const SizedBox(height: 8), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Password strength: $_passwordStrengthLabel', - style: Theme.of(context).textTheme.labelMedium, + const SizedBox(height: 12), + TextFormField( + controller: _confirmPasswordController, + decoration: const InputDecoration( + labelText: 'Confirm password', ), - const SizedBox(height: 6), - LinearProgressIndicator( - value: _passwordStrength, - minHeight: 8, - borderRadius: BorderRadius.circular(8), - color: _passwordStrengthColor, - backgroundColor: Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - ), - ], - ), - const SizedBox(height: 12), - TextFormField( - controller: _confirmPasswordController, - decoration: const InputDecoration( - labelText: 'Confirm password', + obscureText: true, + textInputAction: TextInputAction.done, + onFieldSubmitted: (_) { + if (!_isLoading) { + _handleSignUp(); + } + }, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Confirm your password.'; + } + if (value != _passwordController.text) { + return 'Passwords do not match.'; + } + return null; + }, ), - obscureText: true, - textInputAction: TextInputAction.done, - onFieldSubmitted: (_) { - if (!_isLoading) { - _handleSignUp(); - } - }, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Confirm your password.'; - } - if (value != _passwordController.text) { - return 'Passwords do not match.'; - } - return null; - }, - ), - const SizedBox(height: 16), - Text('Offices', style: Theme.of(context).textTheme.titleSmall), - const SizedBox(height: 8), - officesAsync.when( - data: (offices) { - if (offices.isEmpty) { - return const Text('No offices available.'); - } - return Column( - children: offices - .map( - (office) => CheckboxListTile( - value: _selectedOfficeIds.contains(office.id), - onChanged: _isLoading - ? null - : (selected) { - setState(() { - if (selected == true) { - _selectedOfficeIds.add(office.id); - } else { - _selectedOfficeIds.remove(office.id); - } - }); - }, - title: Text(office.name), - controlAffinity: ListTileControlAffinity.leading, - contentPadding: EdgeInsets.zero, + const SizedBox(height: 12), + Text('Offices', style: Theme.of(context).textTheme.titleSmall), + const SizedBox(height: 8), + officesAsync.when( + data: (offices) { + if (offices.isEmpty) { + return const Text('No offices available.'); + } + + final officeNameById = { + for (final o in offices) o.id: o.name, + }; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ElevatedButton.icon( + onPressed: _isLoading + ? null + : () => _showOfficeSelectionDialog(offices), + icon: const Icon(Icons.place), + label: const Text('Select Offices'), + ), + const SizedBox(height: 8), + if (_selectedOfficeIds.isEmpty) + const Text('No office selected.') + else + Wrap( + spacing: 8, + runSpacing: 8, + children: _selectedOfficeIds.map((id) { + final name = officeNameById[id] ?? id; + return Chip( + label: Text(name), + onDeleted: _isLoading + ? null + : () { + setState( + () => _selectedOfficeIds.remove(id), + ); + }, + ); + }).toList(), ), + ], + ); + }, + loading: () => const LinearProgressIndicator(), + error: (error, _) => Text('Failed to load offices: $error'), + ), + const SizedBox(height: 24), + FilledButton( + onPressed: _isLoading ? null : _handleSignUp, + child: _isLoading + ? const SizedBox( + height: 18, + width: 18, + child: CircularProgressIndicator(strokeWidth: 2), ) - .toList(), - ); - }, - loading: () => const LinearProgressIndicator(), - error: (error, _) => Text('Failed to load offices: $error'), - ), - const SizedBox(height: 24), - FilledButton( - onPressed: _isLoading ? null : _handleSignUp, - child: _isLoading - ? const SizedBox( - height: 18, - width: 18, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Text('Create Account'), - ), - const SizedBox(height: 12), - TextButton( - onPressed: _isLoading ? null : () => context.go('/login'), - child: const Text('Back to sign in'), - ), - ], + : const Text('Create Account'), + ), + const SizedBox(height: 12), + TextButton( + onPressed: _isLoading ? null : () => context.go('/login'), + child: const Text('Back to sign in'), + ), + ], + ), ), ), ), ); } + Future _showOfficeSelectionDialog(List offices) async { + final tempSelected = Set.from(_selectedOfficeIds); + + await showDialog( + context: context, + builder: (dialogCtx) => StatefulBuilder( + builder: (ctx2, setStateDialog) { + return AlertDialog( + title: const Text('Select Offices'), + content: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 480), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: offices.map((office) { + final id = office.id; + final name = office.name; + return CheckboxListTile( + value: tempSelected.contains(id), + onChanged: (v) { + setStateDialog(() { + if (v == true) { + tempSelected.add(id); + } else { + tempSelected.remove(id); + } + }); + }, + title: Text(name), + controlAffinity: ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + ); + }).toList(), + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogCtx).pop(), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () { + setState(() { + _selectedOfficeIds + ..clear() + ..addAll(tempSelected); + }); + Navigator.of(dialogCtx).pop(); + }, + child: const Text('Save'), + ), + ], + ); + }, + ), + ); + } + void _updatePasswordStrength() { final text = _passwordController.text; var score = 0; @@ -253,18 +329,29 @@ class _SignUpScreenState extends ConsumerState { if (RegExp(r'[A-Z]').hasMatch(text)) score++; if (RegExp(r'[a-z]').hasMatch(text)) score++; if (RegExp(r'\d').hasMatch(text)) score++; - if (RegExp(r'[!@#$%^&*(),.?":{}|<>\[\]\\/+=;_-]').hasMatch(text)) { + if (RegExp(r'[!@#\$%\^&\*(),.?":{}|<>\[\]\\/+=;_-]').hasMatch(text)) { score++; } final normalized = (score / 6).clamp(0.0, 1.0); - final (label, color) = switch (normalized) { - <= 0.2 => ('Very weak', Colors.red), - <= 0.4 => ('Weak', Colors.deepOrange), - <= 0.6 => ('Fair', Colors.orange), - <= 0.8 => ('Strong', Colors.green), - _ => ('Excellent', Colors.teal), - }; + String label; + Color color; + if (normalized <= 0.2) { + label = 'Very weak'; + color = Colors.red; + } else if (normalized <= 0.4) { + label = 'Weak'; + color = Colors.deepOrange; + } else if (normalized <= 0.6) { + label = 'Fair'; + color = Colors.orange; + } else if (normalized <= 0.8) { + label = 'Strong'; + color = Colors.green; + } else { + label = 'Excellent'; + color = Colors.teal; + } setState(() { _passwordStrength = normalized;