diff --git a/lib/common/extension/date_extension.dart b/lib/common/extension/date_extension.dart index 0c552fd..9486843 100644 --- a/lib/common/extension/date_extension.dart +++ b/lib/common/extension/date_extension.dart @@ -23,7 +23,7 @@ extension DateTimeIndonesia on DateTime { /// Format: 13-08-2025 String get toServerDate { - return DateFormat('dd-MM-yyyy', 'id_ID').format(this); + return DateFormat('yyyy-MM-dd', 'id_ID').format(this); } /// Format jam: 14:30 diff --git a/lib/common/theme/theme.dart b/lib/common/theme/theme.dart index fbedba4..9f00497 100644 --- a/lib/common/theme/theme.dart +++ b/lib/common/theme/theme.dart @@ -16,6 +16,28 @@ class ThemeApp { fontFamily: FontFamily.quicksand, primaryColor: AppColor.primary, scaffoldBackgroundColor: AppColor.white, + datePickerTheme: DatePickerThemeData( + backgroundColor: AppColor.white, + todayBackgroundColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.selected)) { + return AppColor.primary; // warna background tanggal terpilih + } + return null; // default + }), + todayBorder: BorderSide(color: AppColor.primary, width: 1), + dayBackgroundColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.selected)) { + return AppColor.primary; // warna background tanggal terpilih + } + return null; // default + }), + dayForegroundColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.selected)) { + return AppColor.white; // warna text tanggal terpilih + } + return null; // default + }), + ), appBarTheme: AppBarTheme( backgroundColor: AppColor.white, foregroundColor: AppColor.textPrimary, diff --git a/lib/main.dart b/lib/main.dart index b9daa12..6c40893 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,12 +2,14 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:injectable/injectable.dart'; +import 'package:intl/date_symbol_data_local.dart'; import 'injection.dart'; import 'presentation/app_widget.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + await initializeDateFormatting('id_ID', null); SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle( diff --git a/lib/presentation/components/field/date_text_form_field.dart b/lib/presentation/components/field/date_text_form_field.dart new file mode 100644 index 0000000..f244751 --- /dev/null +++ b/lib/presentation/components/field/date_text_form_field.dart @@ -0,0 +1,59 @@ +part of 'field.dart'; + +class DatePickerField extends StatefulWidget { + final String label; + final DateTime? selectedDate; + final Function(DateTime) onDateSelected; + + const DatePickerField({ + super.key, + required this.label, + required this.onDateSelected, + this.selectedDate, + }); + + @override + State createState() => _DatePickerFieldState(); +} + +class _DatePickerFieldState extends State { + DateTime? _selectedDate; + + @override + void initState() { + super.initState(); + _selectedDate = widget.selectedDate; + } + + Future _selectDate() async { + final picked = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(2000), + lastDate: DateTime(2050), + ); + + if (picked != null && picked != _selectedDate) { + setState(() { + _selectedDate = picked; + }); + widget.onDateSelected(picked); + } + } + + @override + Widget build(BuildContext context) { + return AppTextFormField( + title: widget.label, + hintText: 'Pilih tanggal', + readOnly: true, + controller: TextEditingController(text: _selectedDate?.toServerDate), + onChanged: (value) { + widget.onDateSelected(_selectedDate!); + }, + onTap: () { + _selectDate(); + }, + ); + } +} diff --git a/lib/presentation/components/field/field.dart b/lib/presentation/components/field/field.dart index c76a3ea..b4e8984 100644 --- a/lib/presentation/components/field/field.dart +++ b/lib/presentation/components/field/field.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import '../../../common/extension/extension.dart'; import '../../../common/theme/theme.dart'; part 'text_form_field.dart'; part 'search_text_form_field.dart'; part 'password_text_form_page.dart'; +part 'date_text_form_field.dart'; diff --git a/lib/presentation/components/field/text_form_field.dart b/lib/presentation/components/field/text_form_field.dart index dafa5e2..ce5cab3 100644 --- a/lib/presentation/components/field/text_form_field.dart +++ b/lib/presentation/components/field/text_form_field.dart @@ -12,6 +12,8 @@ class AppTextFormField extends StatelessWidget { this.keyboardType, this.onChanged, this.validator, + this.readOnly = false, + this.onTap, }); final String? hintText; @@ -23,6 +25,8 @@ class AppTextFormField extends StatelessWidget { final TextInputType? keyboardType; final ValueChanged? onChanged; final String? Function(String?)? validator; + final bool readOnly; + final Function()? onTap; @override Widget build(BuildContext context) { @@ -42,6 +46,8 @@ class AppTextFormField extends StatelessWidget { fontWeight: FontWeight.w500, ), validator: validator, + onTap: onTap, + readOnly: readOnly, decoration: InputDecoration( hintText: hintText, prefixIcon: prefixIcon, diff --git a/lib/presentation/pages/auth/login/login_page.dart b/lib/presentation/pages/auth/login/login_page.dart index ba71dd9..5d2aabe 100644 --- a/lib/presentation/pages/auth/login/login_page.dart +++ b/lib/presentation/pages/auth/login/login_page.dart @@ -33,7 +33,9 @@ class LoginPage extends StatelessWidget implements AutoRouteWrapper { Future.delayed(Duration(milliseconds: 1000), () { log(data.toString()); if (data.status.isNotRegistered) { - context.router.push(RegisterRoute()); + context.router.push( + RegisterRoute(phoneNumber: data.phoneNumber), + ); } else if (data.status.isPasswordRequired) { context.router.push( PasswordRoute( diff --git a/lib/presentation/pages/auth/otp/otp_page.dart b/lib/presentation/pages/auth/otp/otp_page.dart index e83da2d..ca5496e 100644 --- a/lib/presentation/pages/auth/otp/otp_page.dart +++ b/lib/presentation/pages/auth/otp/otp_page.dart @@ -8,7 +8,8 @@ import '../../../router/app_router.gr.dart'; @RoutePage() class OtpPage extends StatefulWidget { - const OtpPage({super.key}); + final String registrationToken; + const OtpPage({super.key, required this.registrationToken}); @override State createState() => _OtpPageState(); diff --git a/lib/presentation/pages/auth/register/register_page.dart b/lib/presentation/pages/auth/register/register_page.dart index 2df773d..11cd157 100644 --- a/lib/presentation/pages/auth/register/register_page.dart +++ b/lib/presentation/pages/auth/register/register_page.dart @@ -1,45 +1,91 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../../application/auth/register_form/register_form_bloc.dart'; +import '../../../../injection.dart'; import '../../../components/button/button.dart'; +import '../../../components/toast/flushbar.dart'; import '../../../router/app_router.gr.dart'; +import 'widgets/birth_date_field.dart'; import 'widgets/name_field.dart'; -import 'widgets/phone_field.dart'; @RoutePage() -class RegisterPage extends StatelessWidget { - const RegisterPage({super.key}); +class RegisterPage extends StatelessWidget implements AutoRouteWrapper { + final String phoneNumber; + const RegisterPage({super.key, required this.phoneNumber}); @override Widget build(BuildContext context) { - return Scaffold( - 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), + return BlocListener( + listenWhen: (p, c) => + p.failureOrRegisterOption != c.failureOrRegisterOption, + listener: (context, state) { + state.failureOrRegisterOption.fold( + () {}, + (either) => either.fold( + (f) => AppFlushbar.showAuthFailureToast(context, f), + (data) { + AppFlushbar.showSuccess(context, data.message); + Future.delayed(Duration(milliseconds: 1000), () { + context.router.push( + OtpRoute(registrationToken: data.registrationToken), + ); + }); + }, + ), + ); + }, + child: Scaffold( + appBar: AppBar(title: const Text('Daftar')), + body: BlocBuilder( + builder: (context, state) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Form( + autovalidateMode: state.showErrorMessages + ? AutovalidateMode.always + : AutovalidateMode.disabled, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 40), - // Title - RegisterPhoneField(), - SizedBox(height: 24), - RegisterNameField(), + RegisterNameField(), + SizedBox(height: 24), + RegisterBirthDateField(), - const SizedBox(height: 50), + const SizedBox(height: 50), - Spacer(), + Spacer(), - // Continue Button - AppElevatedButton( - onPressed: () => context.router.push(const OtpRoute()), - title: 'Daftar & Lanjutkan', - ), + // Continue Button + AppElevatedButton( + onPressed: state.isSubmitting + ? null + : () => context.read().add( + RegisterFormEvent.submitted(), + ), + title: 'Daftar & Lanjutkan', + isLoading: state.isSubmitting, + ), - const SizedBox(height: 24), - ], + const SizedBox(height: 24), + ], + ), + ), + ); + }, ), ), ); } + + @override + Widget wrappedRoute(BuildContext context) => BlocProvider( + create: (context) => + getIt() + ..add(RegisterFormEvent.phoneNumberChanged(phoneNumber)), + child: this, + ); } diff --git a/lib/presentation/pages/auth/register/widgets/birth_date_field.dart b/lib/presentation/pages/auth/register/widgets/birth_date_field.dart new file mode 100644 index 0000000..87bc3c1 --- /dev/null +++ b/lib/presentation/pages/auth/register/widgets/birth_date_field.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../../application/auth/register_form/register_form_bloc.dart'; +import '../../../../components/field/field.dart'; + +class RegisterBirthDateField extends StatelessWidget { + const RegisterBirthDateField({super.key}); + + @override + Widget build(BuildContext context) { + return DatePickerField( + label: 'Masukkan tanggal lahir', + onDateSelected: (value) { + context.read().add( + RegisterFormEvent.birthDateChanged(value), + ); + }, + ); + } +} diff --git a/lib/presentation/pages/auth/register/widgets/name_field.dart b/lib/presentation/pages/auth/register/widgets/name_field.dart index 19462bf..ed512b3 100644 --- a/lib/presentation/pages/auth/register/widgets/name_field.dart +++ b/lib/presentation/pages/auth/register/widgets/name_field.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../../../application/auth/register_form/register_form_bloc.dart'; import '../../../../components/field/field.dart'; class RegisterNameField extends StatelessWidget { @@ -7,6 +9,21 @@ class RegisterNameField extends StatelessWidget { @override Widget build(BuildContext context) { - return AppTextFormField(title: 'Masukkan nama', hintText: 'John Doe'); + return AppTextFormField( + title: 'Masukkan nama', + hintText: 'John Doe', + onChanged: (value) { + context.read().add( + RegisterFormEvent.nameChanged(value), + ); + }, + validator: (value) { + if (context.read().state.name.isEmpty) { + return 'Masukkan nama'; + } + + return null; + }, + ); } } diff --git a/lib/presentation/pages/auth/register/widgets/phone_field.dart b/lib/presentation/pages/auth/register/widgets/phone_field.dart deleted file mode 100644 index 74e45e4..0000000 --- a/lib/presentation/pages/auth/register/widgets/phone_field.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../../../../common/theme/theme.dart'; -import '../../../../components/field/field.dart'; - -class RegisterPhoneField extends StatefulWidget { - const RegisterPhoneField({super.key}); - - @override - State createState() => _RegisterPhoneFieldState(); -} - -class _RegisterPhoneFieldState 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/router/app_router.gr.dart b/lib/presentation/router/app_router.gr.dart index 24083cd..e2cee0a 100644 --- a/lib/presentation/router/app_router.gr.dart +++ b/lib/presentation/router/app_router.gr.dart @@ -416,20 +416,44 @@ class OrderRoute extends _i33.PageRouteInfo { /// generated route for /// [_i20.OtpPage] -class OtpRoute extends _i33.PageRouteInfo { - const OtpRoute({List<_i33.PageRouteInfo>? children}) - : super(OtpRoute.name, initialChildren: children); +class OtpRoute extends _i33.PageRouteInfo { + OtpRoute({ + _i34.Key? key, + required String registrationToken, + List<_i33.PageRouteInfo>? children, + }) : super( + OtpRoute.name, + args: OtpRouteArgs(key: key, registrationToken: registrationToken), + initialChildren: children, + ); static const String name = 'OtpRoute'; static _i33.PageInfo page = _i33.PageInfo( name, builder: (data) { - return const _i20.OtpPage(); + final args = data.argsAs(); + return _i20.OtpPage( + key: args.key, + registrationToken: args.registrationToken, + ); }, ); } +class OtpRouteArgs { + const OtpRouteArgs({this.key, required this.registrationToken}); + + final _i34.Key? key; + + final String registrationToken; + + @override + String toString() { + return 'OtpRouteArgs{key: $key, registrationToken: $registrationToken}'; + } +} + /// generated route for /// [_i21.PasswordPage] class PasswordRoute extends _i33.PageRouteInfo { @@ -449,7 +473,9 @@ class PasswordRoute extends _i33.PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return _i21.PasswordPage(key: args.key, phoneNumber: args.phoneNumber); + return _i33.WrappedRoute( + child: _i21.PasswordPage(key: args.key, phoneNumber: args.phoneNumber), + ); }, ); } @@ -631,20 +657,43 @@ class ProfileRoute extends _i33.PageRouteInfo { /// generated route for /// [_i28.RegisterPage] -class RegisterRoute extends _i33.PageRouteInfo { - const RegisterRoute({List<_i33.PageRouteInfo>? children}) - : super(RegisterRoute.name, initialChildren: children); +class RegisterRoute extends _i33.PageRouteInfo { + RegisterRoute({ + _i34.Key? key, + required String phoneNumber, + List<_i33.PageRouteInfo>? children, + }) : super( + RegisterRoute.name, + args: RegisterRouteArgs(key: key, phoneNumber: phoneNumber), + initialChildren: children, + ); static const String name = 'RegisterRoute'; static _i33.PageInfo page = _i33.PageInfo( name, builder: (data) { - return const _i28.RegisterPage(); + final args = data.argsAs(); + return _i33.WrappedRoute( + child: _i28.RegisterPage(key: args.key, phoneNumber: args.phoneNumber), + ); }, ); } +class RegisterRouteArgs { + const RegisterRouteArgs({this.key, required this.phoneNumber}); + + final _i34.Key? key; + + final String phoneNumber; + + @override + String toString() { + return 'RegisterRouteArgs{key: $key, phoneNumber: $phoneNumber}'; + } +} + /// generated route for /// [_i29.RewardPage] class RewardRoute extends _i33.PageRouteInfo {