This commit is contained in:
Phuoc Nguyen
2025-10-10 17:36:10 +07:00
parent 04f7042b8d
commit bdaf0b96c5
82 changed files with 4753 additions and 329 deletions

View File

@@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
/// Custom elevated button for authentication actions
class AuthButton extends StatelessWidget {
const AuthButton({
super.key,
required this.onPressed,
required this.text,
this.isLoading = false,
this.enabled = true,
});
final VoidCallback? onPressed;
final String text;
final bool isLoading;
final bool enabled;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: (enabled && !isLoading) ? onPressed : null,
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
foregroundColor: theme.colorScheme.onPrimary,
disabledBackgroundColor:
theme.colorScheme.onSurface.withOpacity(0.12),
disabledForegroundColor:
theme.colorScheme.onSurface.withOpacity(0.38),
elevation: 2,
shadowColor: theme.colorScheme.primary.withOpacity(0.3),
),
child: isLoading
? SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
theme.colorScheme.onPrimary,
),
),
)
: Text(
text,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onPrimary,
),
),
),
);
}
}

View File

@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
/// Auth header widget displaying app logo and welcome text
class AuthHeader extends StatelessWidget {
const AuthHeader({
super.key,
required this.title,
required this.subtitle,
});
final String title;
final String subtitle;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// App logo/icon
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(20),
),
child: Icon(
Icons.store,
size: 60,
color: theme.colorScheme.primary,
),
),
const SizedBox(height: 24),
// Title
Text(
title,
style: theme.textTheme.displaySmall?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
// Subtitle
Text(
subtitle,
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
textAlign: TextAlign.center,
),
],
);
}
}

View File

@@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// Custom text field for authentication forms
class AuthTextField extends StatelessWidget {
const AuthTextField({
super.key,
required this.controller,
required this.label,
this.hint,
this.validator,
this.keyboardType,
this.textInputAction,
this.onFieldSubmitted,
this.prefixIcon,
this.enabled = true,
this.autofocus = false,
this.inputFormatters,
});
final TextEditingController controller;
final String label;
final String? hint;
final String? Function(String?)? validator;
final TextInputType? keyboardType;
final TextInputAction? textInputAction;
final void Function(String)? onFieldSubmitted;
final IconData? prefixIcon;
final bool enabled;
final bool autofocus;
final List<TextInputFormatter>? inputFormatters;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return TextFormField(
controller: controller,
validator: validator,
keyboardType: keyboardType,
textInputAction: textInputAction,
onFieldSubmitted: onFieldSubmitted,
enabled: enabled,
autofocus: autofocus,
inputFormatters: inputFormatters,
style: theme.textTheme.bodyLarge,
decoration: InputDecoration(
labelText: label,
hintText: hint,
prefixIcon: prefixIcon != null
? Icon(prefixIcon, color: theme.colorScheme.primary)
: null,
labelStyle: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
),
hintStyle: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.4),
),
errorStyle: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.error,
),
),
);
}
}

View File

@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/auth_provider.dart';
import '../pages/login_page.dart';
/// Wrapper widget that checks authentication status
/// Shows login page if not authenticated, otherwise shows child widget
class AuthWrapper extends ConsumerWidget {
const AuthWrapper({
super.key,
required this.child,
});
final Widget child;
@override
Widget build(BuildContext context, WidgetRef ref) {
final authState = ref.watch(authProvider);
// Show loading indicator while checking auth status
if (authState.isLoading && authState.user == null) {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
// Show child widget if authenticated, otherwise show login page
if (authState.isAuthenticated) {
return child;
} else {
return const LoginPage();
}
}
}

View File

@@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
/// Password field with show/hide toggle
class PasswordField extends StatefulWidget {
const PasswordField({
super.key,
required this.controller,
required this.label,
this.hint,
this.validator,
this.textInputAction,
this.onFieldSubmitted,
this.enabled = true,
this.autofocus = false,
});
final TextEditingController controller;
final String label;
final String? hint;
final String? Function(String?)? validator;
final TextInputAction? textInputAction;
final void Function(String)? onFieldSubmitted;
final bool enabled;
final bool autofocus;
@override
State<PasswordField> createState() => _PasswordFieldState();
}
class _PasswordFieldState extends State<PasswordField> {
bool _obscureText = true;
void _toggleVisibility() {
setState(() {
_obscureText = !_obscureText;
});
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return TextFormField(
controller: widget.controller,
validator: widget.validator,
obscureText: _obscureText,
textInputAction: widget.textInputAction,
onFieldSubmitted: widget.onFieldSubmitted,
enabled: widget.enabled,
autofocus: widget.autofocus,
style: theme.textTheme.bodyLarge,
decoration: InputDecoration(
labelText: widget.label,
hintText: widget.hint,
prefixIcon: Icon(
Icons.lock_outline,
color: theme.colorScheme.primary,
),
suffixIcon: IconButton(
icon: Icon(
_obscureText ? Icons.visibility : Icons.visibility_off,
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
onPressed: _toggleVisibility,
),
labelStyle: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
),
hintStyle: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.4),
),
errorStyle: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.error,
),
),
);
}
}

View File

@@ -0,0 +1,6 @@
/// Export file for all auth widgets
export 'auth_button.dart';
export 'auth_header.dart';
export 'auth_text_field.dart';
export 'auth_wrapper.dart';
export 'password_field.dart';