diff --git a/lib/common/theme/app_color.dart b/lib/common/theme/app_color.dart index f7d376f..1187b9d 100644 --- a/lib/common/theme/app_color.dart +++ b/lib/common/theme/app_color.dart @@ -2,7 +2,7 @@ part of 'theme.dart'; class AppColor { // Primary Colors (Merah) - static const Color primary = Color(0xFFD90000); // #d90000 + static const Color primary = Color.fromARGB(255, 196, 2, 2); // #d90000 static const Color primaryLight = Color(0xFFFF4D4D); // merah terang static const Color primaryDark = Color(0xFF990000); // merah gelap diff --git a/lib/common/theme/theme.dart b/lib/common/theme/theme.dart index 1f4cef9..17b4352 100644 --- a/lib/common/theme/theme.dart +++ b/lib/common/theme/theme.dart @@ -6,12 +6,27 @@ part 'app_color.dart'; part 'app_style.dart'; part 'app_value.dart'; +UnderlineInputBorder _inputBorder = UnderlineInputBorder( + borderSide: BorderSide(color: AppColor.borderDark, width: 1), +); + class ThemeApp { static ThemeData get theme => ThemeData( useMaterial3: true, fontFamily: FontFamily.quicksand, primaryColor: AppColor.primary, scaffoldBackgroundColor: AppColor.white, + appBarTheme: AppBarTheme( + backgroundColor: AppColor.white, + foregroundColor: AppColor.textPrimary, + elevation: 0, + titleTextStyle: AppStyle.xl.copyWith( + color: AppColor.primary, + fontWeight: FontWeight.w600, + ), + centerTitle: true, + iconTheme: IconThemeData(color: AppColor.primary), + ), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( backgroundColor: AppColor.primary, @@ -20,5 +35,23 @@ class ThemeApp { shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), ), ), + inputDecorationTheme: InputDecorationTheme( + border: _inputBorder, + focusedBorder: _inputBorder.copyWith( + borderSide: BorderSide(color: AppColor.primary, width: 2), + ), + enabledBorder: _inputBorder, + disabledBorder: _inputBorder.copyWith( + borderSide: BorderSide(color: AppColor.border), + ), + errorBorder: _inputBorder.copyWith( + borderSide: BorderSide(color: AppColor.error), + ), + hintStyle: AppStyle.md.copyWith( + color: AppColor.textLight, + fontWeight: FontWeight.w500, + ), + contentPadding: const EdgeInsets.symmetric(vertical: 12), + ), ); } diff --git a/lib/presentation/components/field/field.dart b/lib/presentation/components/field/field.dart new file mode 100644 index 0000000..c9b3e7d --- /dev/null +++ b/lib/presentation/components/field/field.dart @@ -0,0 +1,5 @@ +import 'package:flutter/material.dart'; + +import '../../../common/theme/theme.dart'; + +part 'text_form_field.dart'; diff --git a/lib/presentation/components/field/text_form_field.dart b/lib/presentation/components/field/text_form_field.dart new file mode 100644 index 0000000..17328b4 --- /dev/null +++ b/lib/presentation/components/field/text_form_field.dart @@ -0,0 +1,59 @@ +part of 'field.dart'; + +class AppTextFormField extends StatelessWidget { + const AppTextFormField({ + super.key, + this.hintText, + required this.title, + this.controller, + this.focusNode, + this.prefixIcon, + this.suffixIcon, + this.keyboardType, + this.onChanged, + }); + + final String? hintText; + final String title; + final TextEditingController? controller; + final FocusNode? focusNode; + final Widget? prefixIcon; + final Widget? suffixIcon; + final TextInputType? keyboardType; + final ValueChanged? onChanged; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: AppStyle.lg.copyWith(fontWeight: FontWeight.w600)), + const SizedBox(height: 8), + TextFormField( + controller: controller, + focusNode: focusNode, + keyboardType: keyboardType, + onChanged: onChanged, + cursorColor: AppColor.primary, + style: AppStyle.md.copyWith( + color: AppColor.textPrimary, + fontWeight: FontWeight.w500, + ), + decoration: InputDecoration( + hintText: hintText, + prefixIcon: prefixIcon, + suffixIcon: suffixIcon, + prefixIconConstraints: const BoxConstraints( + minWidth: 0, + minHeight: 0, + ), + suffixIconConstraints: const BoxConstraints( + minWidth: 0, + minHeight: 0, + ), + ), + ), + ], + ); + } +} diff --git a/lib/presentation/pages/auth/login/login_page.dart b/lib/presentation/pages/auth/login/login_page.dart new file mode 100644 index 0000000..781207e --- /dev/null +++ b/lib/presentation/pages/auth/login/login_page.dart @@ -0,0 +1,73 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; + +import '../../../../common/theme/theme.dart'; +import '../../../components/button/button.dart'; +import 'widgets/phone_field.dart'; + +@RoutePage() +class LoginPage extends StatelessWidget { + const LoginPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColor.backgroundLight, + appBar: AppBar(title: const Text('Masuk')), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 40), + + // Title + LoginPhoneField(), + + const SizedBox(height: 50), + + // Continue Button + AppElevatedButton(onPressed: null, title: 'Lanjutkan'), + + const SizedBox(height: 24), + + // Terms and Conditions + Center( + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: AppStyle.md.copyWith( + color: AppColor.textSecondary, + height: 1.4, + ), + children: [ + const TextSpan( + text: 'Dengan masuk Enaklo, kamu telah\nmenyetujui ', + ), + TextSpan( + text: 'Syarat & Ketentuan', + style: AppStyle.md.copyWith( + color: AppColor.primary, + fontWeight: FontWeight.w600, + ), + ), + const TextSpan(text: ' dan\n'), + TextSpan( + text: 'Kebijakan Privasi', + style: AppStyle.md.copyWith( + color: AppColor.primary, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + + const SizedBox(height: 40), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/pages/auth/login/widgets/phone_field.dart b/lib/presentation/pages/auth/login/widgets/phone_field.dart new file mode 100644 index 0000000..b649983 --- /dev/null +++ b/lib/presentation/pages/auth/login/widgets/phone_field.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; + +import '../../../../../common/theme/theme.dart'; +import '../../../../components/field/field.dart'; + +class LoginPhoneField extends StatefulWidget { + const LoginPhoneField({super.key}); + + @override + State createState() => _LoginPhoneFieldState(); +} + +class _LoginPhoneFieldState extends State { + final TextEditingController _controller = TextEditingController(); + final FocusNode _focusNode = FocusNode(); + bool _hasFocus = false; + + @override + void initState() { + super.initState(); + _focusNode.addListener(() { + setState(() { + _hasFocus = _focusNode.hasFocus; + }); + }); + } + + @override + void dispose() { + _controller.dispose(); + _focusNode.dispose(); + super.dispose(); + } + + void _clearText() { + _controller.clear(); + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return AppTextFormField( + title: 'Masukkan no telepon', + hintText: '8712671212', + controller: _controller, + focusNode: _focusNode, + keyboardType: TextInputType.phone, + prefixIcon: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Text( + '+62', + style: AppStyle.md.copyWith( + color: AppColor.textPrimary, + fontWeight: FontWeight.w500, + ), + ), + ), + suffixIcon: (_hasFocus && _controller.text.isNotEmpty) + ? IconButton( + onPressed: _clearText, + icon: Icon(Icons.close, color: AppColor.primary, size: 20), + constraints: const BoxConstraints(), + padding: const EdgeInsets.all(8), + ) + : null, + onChanged: (value) { + setState(() {}); + }, + ); + } +} diff --git a/lib/presentation/pages/onboarding/onboarding_page.dart b/lib/presentation/pages/onboarding/onboarding_page.dart index 5c34dfa..784b745 100644 --- a/lib/presentation/pages/onboarding/onboarding_page.dart +++ b/lib/presentation/pages/onboarding/onboarding_page.dart @@ -6,6 +6,7 @@ import '../../../common/theme/theme.dart'; import '../../components/assets/assets.gen.dart'; import '../../components/button/button.dart'; import '../../components/image/image.dart'; +import '../../router/app_router.gr.dart'; @RoutePage() class OnboardingPage extends StatefulWidget { @@ -171,7 +172,10 @@ class _OnboardingPageState extends State { const Spacer(), - AppElevatedButton(onPressed: () {}, title: 'Masuk'), + AppElevatedButton( + onPressed: () => context.router.push(const LoginRoute()), + title: 'Masuk', + ), const SizedBox(height: 12), diff --git a/lib/presentation/router/app_router.dart b/lib/presentation/router/app_router.dart index c878605..d32fd3e 100644 --- a/lib/presentation/router/app_router.dart +++ b/lib/presentation/router/app_router.dart @@ -10,5 +10,8 @@ class AppRouter extends RootStackRouter { // Onboarding AutoRoute(page: OnboardingRoute.page), + + // Auth + AutoRoute(page: LoginRoute.page), ]; } diff --git a/lib/presentation/router/app_router.gr.dart b/lib/presentation/router/app_router.gr.dart index ba8a93c..ec111a7 100644 --- a/lib/presentation/router/app_router.gr.dart +++ b/lib/presentation/router/app_router.gr.dart @@ -9,39 +9,56 @@ // coverage:ignore-file // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:auto_route/auto_route.dart' as _i3; +import 'package:auto_route/auto_route.dart' as _i4; +import 'package:enaklo/presentation/pages/auth/login/login_page.dart' as _i1; import 'package:enaklo/presentation/pages/onboarding/onboarding_page.dart' - as _i1; -import 'package:enaklo/presentation/pages/splash/splash_page.dart' as _i2; + as _i2; +import 'package:enaklo/presentation/pages/splash/splash_page.dart' as _i3; /// generated route for -/// [_i1.OnboardingPage] -class OnboardingRoute extends _i3.PageRouteInfo { - const OnboardingRoute({List<_i3.PageRouteInfo>? children}) +/// [_i1.LoginPage] +class LoginRoute extends _i4.PageRouteInfo { + const LoginRoute({List<_i4.PageRouteInfo>? children}) + : super(LoginRoute.name, initialChildren: children); + + static const String name = 'LoginRoute'; + + static _i4.PageInfo page = _i4.PageInfo( + name, + builder: (data) { + return const _i1.LoginPage(); + }, + ); +} + +/// generated route for +/// [_i2.OnboardingPage] +class OnboardingRoute extends _i4.PageRouteInfo { + const OnboardingRoute({List<_i4.PageRouteInfo>? children}) : super(OnboardingRoute.name, initialChildren: children); static const String name = 'OnboardingRoute'; - static _i3.PageInfo page = _i3.PageInfo( + static _i4.PageInfo page = _i4.PageInfo( name, builder: (data) { - return const _i1.OnboardingPage(); + return const _i2.OnboardingPage(); }, ); } /// generated route for -/// [_i2.SplashPage] -class SplashRoute extends _i3.PageRouteInfo { - const SplashRoute({List<_i3.PageRouteInfo>? children}) +/// [_i3.SplashPage] +class SplashRoute extends _i4.PageRouteInfo { + const SplashRoute({List<_i4.PageRouteInfo>? children}) : super(SplashRoute.name, initialChildren: children); static const String name = 'SplashRoute'; - static _i3.PageInfo page = _i3.PageInfo( + static _i4.PageInfo page = _i4.PageInfo( name, builder: (data) { - return const _i2.SplashPage(); + return const _i3.SplashPage(); }, ); }