diff --git a/lib/features/auth/presentation/pages/login_page.dart b/lib/features/auth/presentation/pages/login_page.dart new file mode 100644 index 0000000..1c21534 --- /dev/null +++ b/lib/features/auth/presentation/pages/login_page.dart @@ -0,0 +1,295 @@ +import 'package:cb_prestige_qr/core/utils/MainShell.dart'; +import 'package:flutter/material.dart'; + +class LoginPage extends StatefulWidget { + const LoginPage({super.key}); + + @override + State createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + final _formKey = GlobalKey(); + final _usernameController = TextEditingController(); + final _passwordController = TextEditingController(); + + var _obscurePassword = true; + + @override + void dispose() { + _usernameController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + void _submit() { + final form = _formKey.currentState; + if (form == null || !form.validate()) return; + + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const MainShell()), + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + return Scaffold( + body: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xff17181c), + Color(0xff25262b), + Color(0xff2c2d33), + ], + ), + ), + child: SafeArea( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 440), + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: const Color(0xff1d1e23).withOpacity(0.92), + borderRadius: BorderRadius.circular(28), + border: Border.all(color: Colors.white.withOpacity(0.08)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.28), + blurRadius: 28, + offset: const Offset(0, 18), + ), + ], + ), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Container( + height: 56, + width: 56, + decoration: BoxDecoration( + color: colorScheme.primary.withOpacity(0.16), + borderRadius: BorderRadius.circular(18), + ), + child: const Icon( + Icons.lock_person_rounded, + color: Colors.white, + size: 30, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Secure Login', + style: theme.textTheme.headlineSmall + ?.copyWith( + fontWeight: FontWeight.w700, + color: Colors.white, + ), + ), + const SizedBox(height: 4), + Text( + 'Use your username and password to continue.', + style: theme.textTheme.bodyMedium?.copyWith( + color: Colors.white70, + height: 1.35, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 28), + Container( + width: double.infinity, + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.04), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Colors.white.withOpacity(0.06), + ), + ), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(14), + child: Image.asset( + 'assets/images/logo_white.png', + height: 48, + width: 48, + fit: BoxFit.cover, + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'CB Prestige Banking', + style: theme.textTheme.titleMedium + ?.copyWith( + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + 'Sign in to access your QR dashboard.', + style: theme.textTheme.bodySmall + ?.copyWith(color: Colors.white60), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 28), + _AuthTextField( + controller: _usernameController, + label: 'Username', + hintText: 'Enter your username', + keyboardType: TextInputType.text, + prefixIcon: Icons.person_rounded, + validator: (value) { + final username = value?.trim() ?? ''; + if (username.isEmpty) { + return 'Username is required'; + } + if (username.length < 3) { + return 'Username must be at least 3 characters'; + } + return null; + }, + ), + const SizedBox(height: 16), + _AuthTextField( + controller: _passwordController, + label: 'Password', + hintText: 'Enter your password', + prefixIcon: Icons.lock_rounded, + obscureText: _obscurePassword, + validator: (value) { + final password = value ?? ''; + if (password.isEmpty) { + return 'Password is required'; + } + if (password.length < 6) { + return 'Password must be at least 6 characters'; + } + return null; + }, + suffixIcon: IconButton( + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + icon: Icon( + _obscurePassword + ? Icons.visibility_off_rounded + : Icons.visibility_rounded, + color: Colors.white70, + ), + ), + ), + const SizedBox(height: 12), + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () {}, + child: const Text('Forgot password?'), + ), + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: FilledButton( + onPressed: _submit, + style: FilledButton.styleFrom( + backgroundColor: colorScheme.primary, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18), + ), + ), + child: const Text( + 'Sign In', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ), + ), + ), + ); + } +} + +class _AuthTextField extends StatelessWidget { + const _AuthTextField({ + required this.controller, + required this.label, + required this.hintText, + required this.prefixIcon, + required this.validator, + this.keyboardType, + this.obscureText = false, + this.suffixIcon, + }); + + final TextEditingController controller; + final String label; + final String hintText; + final IconData prefixIcon; + final String? Function(String?) validator; + final TextInputType? keyboardType; + final bool obscureText; + final Widget? suffixIcon; + + @override + Widget build(BuildContext context) { + return TextFormField( + controller: controller, + keyboardType: keyboardType, + obscureText: obscureText, + validator: validator, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: label, + hintText: hintText, + prefixIcon: Icon(prefixIcon), + suffixIcon: suffixIcon, + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 490cfe0..48aae98 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,4 @@ -import 'package:cb_prestige_qr/core/utils/MainShell.dart'; +import 'package:cb_prestige_qr/features/auth/presentation/pages/login_page.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -27,8 +27,35 @@ class MyApp extends StatelessWidget { foregroundColor: Colors.white, surfaceTintColor: Colors.transparent, ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: const Color(0xff2a2b31), + labelStyle: const TextStyle(color: Colors.white70), + hintStyle: const TextStyle(color: Colors.white38), + prefixIconColor: Colors.white70, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(18), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(18), + borderSide: BorderSide(color: Colors.white.withOpacity(0.06)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(18), + borderSide: const BorderSide(color: Color(0xff896e4b), width: 1.2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(18), + borderSide: const BorderSide(color: Colors.redAccent), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(18), + borderSide: const BorderSide(color: Colors.redAccent), + ), + ), ), - home: const MainShell(), + home: const LoginPage(), ); } } diff --git a/test/widget_test.dart b/test/widget_test.dart index 1e52a14..69efdcf 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -5,26 +5,18 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:cb_prestige_qr/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} +import 'package:flutter_test/flutter_test.dart'; + +import 'package:cb_prestige_qr/main.dart'; + +void main() { + testWidgets('shows login form fields', (WidgetTester tester) async { + await tester.pumpWidget(const MyApp()); + + expect(find.text('Login'), findsOneWidget); + expect(find.text('Phone Number'), findsOneWidget); + expect(find.text('User ID (Username)'), findsOneWidget); + expect(find.text('Password'), findsOneWidget); + expect(find.text('Sign In'), findsOneWidget); + }); +}