feat(01-05): create reusable password reset form widget
- Complete password reset form with email validation - Real-time validation error clearing on user input - Consistent styling with existing AuthForm components - Responsive layout for mobile and tablet - Accessibility features with proper labels and tooltips - Optional helper text for user guidance - Clear email field functionality for better UX - Proper error handling and validation states - Reusable component with configurable properties
This commit is contained in:
@@ -0,0 +1,197 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'auth_button.dart';
|
||||||
|
|
||||||
|
/// Reusable password reset form with email validation and submission
|
||||||
|
class PasswordResetForm extends StatefulWidget {
|
||||||
|
final GlobalKey<FormState> formKey;
|
||||||
|
final TextEditingController emailController;
|
||||||
|
final bool isLoading;
|
||||||
|
final String? errorMessage;
|
||||||
|
final ValueChanged<String>? onSubmit;
|
||||||
|
final String emailLabel;
|
||||||
|
final String submitButtonText;
|
||||||
|
final bool autofocusEmail;
|
||||||
|
final String? hintText;
|
||||||
|
final String? helperText;
|
||||||
|
|
||||||
|
const PasswordResetForm({
|
||||||
|
super.key,
|
||||||
|
required this.formKey,
|
||||||
|
required this.emailController,
|
||||||
|
this.isLoading = false,
|
||||||
|
this.errorMessage,
|
||||||
|
this.onSubmit,
|
||||||
|
this.emailLabel = 'Email',
|
||||||
|
this.submitButtonText = 'Send Reset Email',
|
||||||
|
this.autofocusEmail = true,
|
||||||
|
this.hintText,
|
||||||
|
this.helperText,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PasswordResetForm> createState() => _PasswordResetFormState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PasswordResetFormState extends State<PasswordResetForm> {
|
||||||
|
String? _emailError;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// Set initial error from widget
|
||||||
|
_emailError = widget.errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(PasswordResetForm oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
// Update error when widget error changes
|
||||||
|
if (widget.errorMessage != oldWidget.errorMessage) {
|
||||||
|
_emailError = widget.errorMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleSubmit() async {
|
||||||
|
if (!widget.formKey.currentState!.validate()) return;
|
||||||
|
|
||||||
|
final email = widget.emailController.text.trim();
|
||||||
|
if (widget.onSubmit != null) {
|
||||||
|
await widget.onSubmit!(email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Form(
|
||||||
|
key: widget.formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
_buildEmailField(),
|
||||||
|
if (widget.helperText != null) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildHelperText(),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildSubmitButton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEmailField() {
|
||||||
|
return TextFormField(
|
||||||
|
controller: widget.emailController,
|
||||||
|
autofocus: widget.autofocusEmail,
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
onFieldSubmitted: (_) => _handleSubmit(),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: widget.emailLabel,
|
||||||
|
hintText: widget.hintText ?? 'Enter your email address',
|
||||||
|
prefixIcon: const Icon(Icons.email_outlined),
|
||||||
|
suffixIcon: widget.emailController.text.isNotEmpty
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
onPressed: () {
|
||||||
|
widget.emailController.clear();
|
||||||
|
setState(() {
|
||||||
|
_emailError = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip: 'Clear email',
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
errorText: _emailError,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: _emailError != null
|
||||||
|
? Theme.of(context).colorScheme.error
|
||||||
|
: Theme.of(context).colorScheme.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: _emailError != null
|
||||||
|
? Theme.of(context).colorScheme.error
|
||||||
|
: Theme.of(context).colorScheme.primary,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return 'Email is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
final email = value.trim();
|
||||||
|
|
||||||
|
// Basic email validation regex
|
||||||
|
final emailRegex = RegExp(
|
||||||
|
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!emailRegex.hasMatch(email)) {
|
||||||
|
return 'Please enter a valid email address';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
onChanged: (value) {
|
||||||
|
// Clear error when user starts typing
|
||||||
|
if (_emailError != null) {
|
||||||
|
setState(() {
|
||||||
|
_emailError = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
inputFormatters: [
|
||||||
|
FilteringTextInputFormatter.deny(RegExp(r'\s')), // Prevent spaces
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHelperText() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.info_outline,
|
||||||
|
size: 16,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
widget.helperText!,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSubmitButton() {
|
||||||
|
return AuthButton(
|
||||||
|
text: widget.submitButtonText,
|
||||||
|
isLoading: widget.isLoading,
|
||||||
|
onPressed: _handleSubmit,
|
||||||
|
icon: const Icon(Icons.send_outlined),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user