diff --git a/.gitignore b/.gitignore index 0a764a4..7ed4bc8 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -env +.env +google-service.json \ No newline at end of file diff --git a/.idea/bebras-pandai.iml b/.idea/bebras-pandai.iml index d6ebd48..f33560c 100644 --- a/.idea/bebras-pandai.iml +++ b/.idea/bebras-pandai.iml @@ -2,7 +2,11 @@ - + + + + + diff --git a/app/android/app/build.gradle b/app/android/app/build.gradle index 6548875..3e8e3c4 100644 --- a/app/android/app/build.gradle +++ b/app/android/app/build.gradle @@ -70,4 +70,5 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'com.google.android.gms:play-services-vision:20.1.3' } diff --git a/app/assets/images/bebras-mascot.png b/app/assets/images/bebras-mascot.png new file mode 100644 index 0000000..125eb4d Binary files /dev/null and b/app/assets/images/bebras-mascot.png differ diff --git a/app/lib/app.dart b/app/lib/app.dart index c712871..70e2bf0 100644 --- a/app/lib/app.dart +++ b/app/lib/app.dart @@ -3,8 +3,10 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'features/authentication/register/bloc/user_register_bloc.dart'; import 'features/onboarding/presentation/bloc/user_initialization_bloc.dart'; import 'features/quiz_exercise/presentation/bloc/quiz_exercise_bloc.dart'; +import 'features/quiz_registration/bloc/quiz_registration_cubit.dart'; import 'services/di.dart'; import 'services/router_service.dart'; @@ -19,11 +21,17 @@ class App extends StatelessWidget { ]); return MultiBlocProvider( providers: [ + BlocProvider( + create: (_) => get() + ..add( + InitEvent(), + )), BlocProvider( create: (_) => get() ..add( OnboardingAuthEvent(), ), + lazy: false, ), BlocProvider( create: (_) => get() @@ -31,10 +39,11 @@ class App extends StatelessWidget { GetQuizExercise(), ), ), + BlocProvider(create: (context) => QuizRegistrationCubit()), ], child: MaterialApp.router( theme: ThemeData( - textTheme: GoogleFonts.interTextTheme(), + textTheme: GoogleFonts.poppinsTextTheme(), ), routerConfig: router, debugShowCheckedModeBanner: false, diff --git a/app/lib/core/bases/widgets/atoms/button.dart b/app/lib/core/bases/widgets/atoms/button.dart index 2dcd802..383e1a1 100644 --- a/app/lib/core/bases/widgets/atoms/button.dart +++ b/app/lib/core/bases/widgets/atoms/button.dart @@ -13,6 +13,8 @@ class Button extends StatelessWidget { final double innerHorizontalPadding; final double innerVerticalPadding; final double fontSize; + final Color customButtonColor; + final Color customTextColor; const Button({ required this.text, @@ -23,6 +25,8 @@ class Button extends StatelessWidget { this.innerHorizontalPadding = 20, this.innerVerticalPadding = 16, this.fontSize = 16, + this.customButtonColor = Colors.transparent, + this.customTextColor = BaseColors.black, super.key, }); @@ -41,12 +45,12 @@ class Button extends StatelessWidget { buttonColor = BaseColors.white; break; case ButtonType.tertiary: - textColor = BaseColors.primarySwatch; - buttonColor = BaseColors.black; + textColor = BaseColors.white; + buttonColor = Colors.blueAccent; break; case null: - textColor = BaseColors.black; - buttonColor = Colors.transparent; + textColor = customTextColor; + buttonColor = customButtonColor; break; } diff --git a/app/lib/core/bases/widgets/layout/bebras_scaffold.dart b/app/lib/core/bases/widgets/layout/bebras_scaffold.dart index 93db499..eb267f3 100644 --- a/app/lib/core/bases/widgets/layout/bebras_scaffold.dart +++ b/app/lib/core/bases/widgets/layout/bebras_scaffold.dart @@ -2,11 +2,13 @@ import 'package:flutter/material.dart'; class BebrasScaffold extends StatelessWidget { final Widget body; - const BebrasScaffold({required this.body, super.key}); + final bool avoidBottomInset; + const BebrasScaffold({required this.body, super.key, this.avoidBottomInset = true}); @override Widget build(BuildContext context) { return Scaffold( + resizeToAvoidBottomInset: avoidBottomInset, body: SafeArea( child: body, ), diff --git a/app/lib/core/constants/assets.dart b/app/lib/core/constants/assets.dart index bfa8295..4915cfb 100644 --- a/app/lib/core/constants/assets.dart +++ b/app/lib/core/constants/assets.dart @@ -2,4 +2,5 @@ class Assets { static const logo = 'assets/images/logo.jpg'; static const bebrasPandaiText = 'assets/images/bebras-banner.png'; static const studyBackground = 'assets/images/study-background.jpg'; + static const bebrasMascot = 'assets/images/bebras-mascot.png'; } diff --git a/app/lib/core/constants/bebrasBiroDropdown.dart b/app/lib/core/constants/bebrasBiroDropdown.dart new file mode 100644 index 0000000..f73e86b --- /dev/null +++ b/app/lib/core/constants/bebrasBiroDropdown.dart @@ -0,0 +1,110 @@ +class BebrasBiro { + String bebrasBiroUniv; + String valueDropdown; + bool isActive; + + BebrasBiro({ + required this.bebrasBiroUniv, + required this.valueDropdown, + this.isActive = true, + }); + + String userAsString() { + return this.bebrasBiroUniv; + } +} + +List bebrasBiroList = [ + "Institut Teknologi Bandung", + "Institut Pertanian Bogor", + "Universitas Indonesia", + "Institut Teknologi Sepuluh November", + "Universitas Kristen Maranatha", + "Universitas Atmajaya Yogyakarta", + "Universitas Diponegoro", + "Universitas Islam Indonesia", + "Universitas Jendral Soedirman", + "Universitas Jember", + "Universitas Sriwijaya", + "Institut Teknologi Del", + "Sekolah Tinggi Teknologi Garut", + "Universitas Mulawarman", + "Unversitas Udayana", + "Universitas Negeri Malang", + "Universitas Dian Nuswantoro", + "Politeknik Caltex Riau", + "Universitas Katolik Parahyangan", + "Institut Teknologi Sumatera", + "Universitas Lambung Mangkurat", + "Universitas Tanjungpura", + "Universitas Sumatera Utara", + "Universitas Pembangunan Jaya", + "Universitas Kristen Satya Wacana", + "Universitas Maritim Raja Ali Haji", + "Universitas Paramadina", + "Universitas Katolik Widya Mandira", + "Politeknik Negeri Batam", + "Politeknik Negeri Malang", + "Universitas Katolik Soegijapranata", + "Universitas Muhammadiyah Surakarta", + "Politeknik Negeri Jakarta", + "Universitas Gadjah Mada", + "Universitas Negeri Yogyakarta", + "Politeknik Negeri Bandung", + "Universitas Hasanudin", + "UIN Alauddin Makassar", + "Universitas Pasundan", + "Universitas Telkom", + "Universitas Sanata Dharma", + "Universitas Andalas", + "Universitas Islam Nusantara", + "Universitas Sains Al-Qur’an Wonosobo", + "Universitas Bina Nusantara", + "Universitas Komputer Indonesia", + "Universitas Singaperbangsa Karawang", + "Universitas Pendidikan Indonesia Kampus Purwakarta", + "Universitas President", + "STIKOM PGRI Banyuwangi", + "Universitas Bumigora", + "Universitas Pendidikan Indonesia", + "Universitas Padjadjaran", + "Universitas Katolik De La Salle Manado", + "Universitas Ahmad Dahlan", + "Universitas Negeri Makassar", + "Universitas Pendidikan Muhammadiyah Sorong", + "Universitas Al-Azhar Indonesia", + "Politeknik Negeri Indramayu", + "Universitas Nahdlatul Ulama Sunan Giri Bojonegoro", + "Institut Bisnis dan Informatika Kwik Kian Gie", + "Universitas Internasional Semen Indonesia", + "Politeknik Negeri Madiun", + "Universitas Kristen Petra", + "Universitas Muhammadiyah Magelang", + "Universitas Surabaya", + "Universitas Katolik Indonesia Atma Jaya", + "Universitas Ciputra Surabaya", + "Universitas Katolik Widya Mandala Surabaya", + "Universitas Kristen Duta Wacana", + "Politeknik Manufaktur Negeri Bangka Belitung", + "Universitas Kristen Krida Wacana", + "Institut Agama Islam Negeri Salatiga", + "Universitas Muhammadiyah Purworejo", + "Institut Teknologi dan Bisnis Kalbis", + "Universitas Mercu Buana Jakarta", + "UPN “Veteran” Jakarta", + "Universitas Pertamina", + "Universitas Dr. Soetomo", + "Universitas Bakrie", + "Universitas Bengkulu", + "UIN Walisongo Semarang", + "Universitas Islam Sultan Agung Semarang", + "UIN Imam Bonjol Padang", + "Institut Pesantren Mathali’ul Falah Pati", + "UIN Sunan Gunung Djati Bandung", + "IAIN Pekalongan", + "UIN Sultan Aji Muhammad Idris Samarinda", + "Politeknik Jambi", + "Universitas Trisakti", + "Biro Institut Teknologi dan Bisnis Yadika Pasuruan", + "Biro Universitas Widyagama Malang", +]; diff --git a/app/lib/core/constants/colorConstant.dart b/app/lib/core/constants/colorConstant.dart new file mode 100644 index 0000000..f2c2fb0 --- /dev/null +++ b/app/lib/core/constants/colorConstant.dart @@ -0,0 +1,7 @@ +import 'dart:ui'; + +class ColorConstants { + static const Color primaryBlueColor = Color(0XFF6191CB); + static const Color darkPrimaryBlueColor = Color(0XFF2f609c); + static const Color whiteColor = Color(0XFFFFFFFF); +} \ No newline at end of file diff --git a/app/lib/core/constants/indonesiaProvince.dart b/app/lib/core/constants/indonesiaProvince.dart new file mode 100644 index 0000000..0106e04 --- /dev/null +++ b/app/lib/core/constants/indonesiaProvince.dart @@ -0,0 +1,57 @@ +class IndonesiaProvince { + String provinceName; + String valueProvince; + bool isActive; + + IndonesiaProvince({ + required this.provinceName, + required this.valueProvince, + this.isActive = true, + }); + + String userAsString() { + return this.provinceName; + } +} + +List provinceList = [ + "Bali", + "Banten", + "Nanggroe Aceh Darussalam", + "Sumatera Utara", + "Sumatera Selatan", + "Sumatera Barat", + "Bengkulu", + "Riau", + "Kepulauan Riau", + "Jambi", + "Lampung", + "Bangka Belitung", + "Kalimantan Barat", + "Kalimantan Timur", + "Kalimantan Selatan", + "Kalimantan Tengah", + "Kalimantan Utara", + "DKI Jakarta", + "Jawa Barat", + "Jawa Tengah", + "DI Yogyakarta", + "Jawa Timur", + "Nusa Tenggara Timur", + "Nusa Tenggara Barat", + "Gorontalo", + "Sulawesi Barat", + "Sulawesi Tengah", + "Sulawesi Utara", + "Sulawesi Tenggara", + "Sulawesi Selatan", + "Maluku Utara", + "Maluku", + "Papua Barat", + "Papua", + "Papua Tengah", + "Papua Pegunungan", + "Papua Selatan", + "Papua Barat Daya", +]; + diff --git a/app/lib/features/authentication/register/bloc/user_register_bloc.dart b/app/lib/features/authentication/register/bloc/user_register_bloc.dart new file mode 100644 index 0000000..69fcd41 --- /dev/null +++ b/app/lib/features/authentication/register/bloc/user_register_bloc.dart @@ -0,0 +1,157 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injectable/injectable.dart'; + +import '../../../authentication/register/repositories/register_user_repo.dart'; +import '../model/form_item.dart'; + +part 'user_register_event.dart'; + +part 'user_register_state.dart'; + +@injectable +@singleton +class UserRegisterBloc extends Bloc { + final RegisterUserRepository registerUserRepository; + UserRegisterBloc(this.registerUserRepository) : super(const RegisterFormState()) { + on(_initState); + on(_onEmailChanged); + on(_onNameChanged); + on(_onBirthDateChanged); + on(_onSchoolChanged); + on(_onProvinceChanged); + on(_onBebrasBiroChanged); + on(_onFormSubmitted); + on(_onFormReset); + } + + final formKey = GlobalKey(); + + Future _initState(InitEvent event, Emitter emit) async { + emit(state.copyWith(formKey: formKey)); + } + + Future _onNameChanged( + NameEvent event, Emitter emit) async { + emit( + state.copyWith( + name: BlocFormItem( + value: event.name.value, + error: event.name.value == '' ? 'Mohon mengisi nama terlebih dahulu.' : null, + ), + formKey: formKey, + ), + ); + } + + Future _onEmailChanged( + EmailEvent event, Emitter emit) async { + emit( + state.copyWith( + email: BlocFormItem( + value: event.email.value, + error: event.email.value == '' ? 'Mohon mengisi e-mail terlebih dahulu.' : null, + ), + formKey: formKey, + ), + ); + } + + Future _onBirthDateChanged( + BirthDateEvent event, Emitter emit) async { + emit( + state.copyWith( + birth_date: BlocFormItem( + value: event.birthDate.value, + error: event.birthDate.value == '' ? 'Mohon mengisi tanggal lahir terlebih dahulu.' : null, + ), + formKey: formKey, + ), + ); + } + + Future _onSchoolChanged( + SchoolEvent event, Emitter emit) async { + emit( + state.copyWith( + school: BlocFormItem( + value: event.school.value, + error: event.school.value == '' ? 'Mohon mengisi nama sekolah terlebih dahulu.' : null, + ), + formKey: formKey, + ), + ); + } + + Future _onProvinceChanged( + ProvinceEvent event, Emitter emit) async { + emit( + state.copyWith( + province: BlocFormItem( + value: event.province.value, + error: event.province.value == '' ? 'Mohon mengisi nama provinsi terlebih dahulu.' : null, + ), + formKey: formKey, + ), + ); + } + + Future _onBebrasBiroChanged( + BebrasBiroEvent event, Emitter emit) async { + emit( + state.copyWith( + bebras_biro: BlocFormItem( + value: event.bebras_biro.value, + error: event.bebras_biro.value == '' ? 'Mohon mengisi biro bebras terlebih dahulu.' : null, + ), + formKey: formKey, + ), + ); + } + + Future _onFormReset( + FormResetEvent event, + Emitter emit, + ) async { + state.formKey?.currentState?.reset(); + } + + Future _onFormSubmitted( + FormSubmitEvent event, + Emitter emit, + ) async { + final FirebaseAuth auth = FirebaseAuth.instance; + String userId = auth.currentUser?.uid as String; + + if (state.formKey!.currentState!.validate()) { + + String email = state.email.value.toString(); + String name = state.name.value.toString(); + String birthDate = state.birth_date.value.toString(); + String school = state.school.value.toString(); + String province = state.province.value.toString(); + String bebrasBiro = state.bebras_biro.value.toString(); + + emit(UserRegisterLoadingState()); + + try { + await registerUserRepository.create( + userId: userId, + email: email, + name: name, + birth_date: birthDate, + school: school, + province: province, + bebras_biro: bebrasBiro, + ); + emit(UserRegisterSuccessState()); + } catch (e) { + // emit(Us) + } + } + } +} diff --git a/app/lib/features/authentication/register/bloc/user_register_event.dart b/app/lib/features/authentication/register/bloc/user_register_event.dart new file mode 100644 index 0000000..9513ead --- /dev/null +++ b/app/lib/features/authentication/register/bloc/user_register_event.dart @@ -0,0 +1,62 @@ +part of 'user_register_bloc.dart'; + +abstract class UserRegisterEvent extends Equatable { + const UserRegisterEvent(); + + @override + List get props => []; +} + +class FormSubmitEvent extends UserRegisterEvent { + const FormSubmitEvent(); +} + +class FormResetEvent extends UserRegisterEvent { + const FormResetEvent(); +} + +class InitEvent extends UserRegisterEvent { + const InitEvent(); +} + +class EmailEvent extends UserRegisterEvent { + const EmailEvent({required this.email}); + final BlocFormItem email; + @override + List get props => [email]; +} + +class NameEvent extends UserRegisterEvent { + const NameEvent({required this.name}); + final BlocFormItem name; + @override + List get props => [name]; +} + +class BirthDateEvent extends UserRegisterEvent { + const BirthDateEvent({required this.birthDate}); + final BlocFormItem birthDate; + @override + List get props => [birthDate]; +} + +class SchoolEvent extends UserRegisterEvent { + const SchoolEvent({required this.school}); + final BlocFormItem school; + @override + List get props => [school]; +} + +class ProvinceEvent extends UserRegisterEvent { + const ProvinceEvent({required this.province}); + final BlocFormItem province; + @override + List get props => [province]; +} + +class BebrasBiroEvent extends UserRegisterEvent { + const BebrasBiroEvent({required this.bebras_biro}); + final BlocFormItem bebras_biro; + @override + List get props => [bebras_biro]; +} diff --git a/app/lib/features/authentication/register/bloc/user_register_state.dart b/app/lib/features/authentication/register/bloc/user_register_state.dart new file mode 100644 index 0000000..a905072 --- /dev/null +++ b/app/lib/features/authentication/register/bloc/user_register_state.dart @@ -0,0 +1,57 @@ +part of 'user_register_bloc.dart'; + +class RegisterFormState extends Equatable { + + const RegisterFormState({ + this.email = const BlocFormItem(error: 'Mohon mengisi e-mail terlebih dahulu.'), + this.name = const BlocFormItem(error: 'Mohon mengisi nama terlebih dahulu.'), + this.birth_date = const BlocFormItem(error: 'Mohon mengisi tanggal lahir terlebih dahulu.'), + this.school = const BlocFormItem(error: 'Mohon mengisi nama sekolah terlebih dahulu.'), + this.province = const BlocFormItem(error: 'Mohon mengisi nama provinsi terlebih dahulu.'), + this.bebras_biro = const BlocFormItem(error: 'Mohon mengisi biro bebras terlebih dahulu.'), + this.formKey, + }); + + final BlocFormItem email; + final BlocFormItem name; + final BlocFormItem birth_date; + final BlocFormItem school; + final BlocFormItem province; + final BlocFormItem bebras_biro; + final GlobalKey? formKey; + + RegisterFormState copyWith({ + BlocFormItem? email, + BlocFormItem? name, + BlocFormItem? birth_date, + BlocFormItem? school, + BlocFormItem? province, + BlocFormItem? bebras_biro, + GlobalKey? formKey, + }) { + return RegisterFormState( + email: email ?? this.email, + name: name ?? this.name, + birth_date: birth_date ?? this.birth_date, + school: school ?? this.school, + province: province ?? this.province, + bebras_biro: bebras_biro ?? this.bebras_biro, + formKey: formKey, + ); + } + + @override + List get props => [email, name, birth_date, school, province, bebras_biro]; +} + +enum FormStatus { none, inProgress, valid, invalid } + +class UserRegisterLoadingState extends RegisterFormState { + @override + List get props => []; +} + +class UserRegisterSuccessState extends RegisterFormState { + @override + List get props => []; +} \ No newline at end of file diff --git a/app/lib/features/authentication/register/model/form_item.dart b/app/lib/features/authentication/register/model/form_item.dart new file mode 100644 index 0000000..2982ce4 --- /dev/null +++ b/app/lib/features/authentication/register/model/form_item.dart @@ -0,0 +1,15 @@ +class BlocFormItem { + final String? error; + final String value; + const BlocFormItem({this.error, this.value = ''}); + + BlocFormItem copyWith({ + String? error, + String? value, + }) { + return BlocFormItem( + error: error ?? this.error, + value: value ?? this.value, + ); + } +} \ No newline at end of file diff --git a/app/lib/features/authentication/register/model/registered_user.dart b/app/lib/features/authentication/register/model/registered_user.dart new file mode 100644 index 0000000..ddf11be --- /dev/null +++ b/app/lib/features/authentication/register/model/registered_user.dart @@ -0,0 +1,28 @@ +class RegisteredUserModel { + final String email; + final String name; + final String birth_date; + final String school; + final String province; + final String bebras_biro; + + RegisteredUserModel({ + required this.email, + required this.name, + required this.birth_date, + required this.school, + required this.province, + required this.bebras_biro, + }); + + factory RegisteredUserModel.fromJson(dynamic json) { + return RegisteredUserModel( + email: json.data()['email'].toString(), + name: json.data()['name'].toString(), + birth_date: json.data()['birth_date'].toString(), + school: json.data()['school'].toString(), + province: json.data()['province'].toString(), + bebras_biro: json.data()['bebras_biro'].toString() + ); + } +} \ No newline at end of file diff --git a/app/lib/features/authentication/register/model/validation_form_model.dart b/app/lib/features/authentication/register/model/validation_form_model.dart new file mode 100644 index 0000000..9493f29 --- /dev/null +++ b/app/lib/features/authentication/register/model/validation_form_model.dart @@ -0,0 +1,5 @@ +class ValidationFormModel { + String? value; + String? error; + ValidationFormModel(this.value, this.error); +} \ No newline at end of file diff --git a/app/lib/features/authentication/register/presentation/pages/_pages.dart b/app/lib/features/authentication/register/presentation/pages/_pages.dart new file mode 100644 index 0000000..32f9bcd --- /dev/null +++ b/app/lib/features/authentication/register/presentation/pages/_pages.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; + +import '../../../../../core/bases/enum/button_type.dart'; +import '../../../../../core/bases/widgets/atoms/button.dart'; +import '../../../../../core/bases/widgets/layout/bebras_scaffold.dart'; +import '../../../../../core/constants/assets.dart'; +import '../../../../../core/constants/bebrasBiroDropdown.dart'; +import '../../../../../core/constants/colorConstant.dart'; +import '../../../../../core/constants/indonesiaProvince.dart'; +import '../../../../../services/di.dart'; +// import '../bloc/sign_in_bloc.dart'; +import 'package:dropdown_search/dropdown_search.dart'; + +import '../../bloc/user_register_bloc.dart'; +import '../../model/form_item.dart'; +import '../../repositories/register_user_repo.dart'; +import '../widgets/biro_bebras_dropdown.dart'; +import '../widgets/custom_date_picker.dart'; +import '../widgets/custom_text_field.dart'; +import '../widgets/province_dropdown.dart'; + +part 'register_page.dart'; diff --git a/app/lib/features/authentication/register/presentation/pages/register_page.dart b/app/lib/features/authentication/register/presentation/pages/register_page.dart new file mode 100644 index 0000000..13315fb --- /dev/null +++ b/app/lib/features/authentication/register/presentation/pages/register_page.dart @@ -0,0 +1,136 @@ +part of '_pages.dart'; + +class RegisterPage extends StatefulWidget { + const RegisterPage({super.key}); + + @override + State createState() => _RegisterPageState(); +} + +class _RegisterPageState extends State { + late final UserRegisterBloc _userRegisterBloc; + TextEditingController dateinput = TextEditingController(); + + String? selectedValue = null; + + @override + void initState() { + _userRegisterBloc = get(); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + return BlocListener( + listener: (context, state) { + if (state is UserRegisterSuccessState) { + context.go('/main'); + } + }, + child: BlocBuilder( + builder: (context, state) { + return BebrasScaffold( + avoidBottomInset: false, + body: Padding( + padding: const EdgeInsets.only( + left: 16.0, top: 30.0, right: 16.0), + child: Form( + key: state.formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Image.asset( + Assets.bebrasPandaiText, + height: 40, + ), + SizedBox( + height: 40, + ), + Text( + 'Detail Akun', + style: TextStyle( + fontSize: 22, fontWeight: FontWeight.w700), + ), + SizedBox( + height: 8, + ), + CustomTextField('Email', (value) { + BlocProvider.of(context) + .add(EmailEvent(email: BlocFormItem(value: value!))); + }, (val) { + return state.email.error; + },), + CustomTextField('Nama', (value) { + BlocProvider.of(context) + .add(NameEvent(name: BlocFormItem(value: value!))); + }, (val) { + return state.name.error; + },), + CustomDatePicker('Tanggal Lahir', (value) { + BlocProvider.of(context) + .add(BirthDateEvent(birthDate: BlocFormItem( + value: value!))); + }, (val) { + return state.birth_date.error; + }), + CustomTextField('Sekolah', (value) { + BlocProvider.of(context) + .add(SchoolEvent(school: BlocFormItem( + value: value!))); + }, (val) { + return state.school.error; + }), + ProvinceDropdown('Provinsi', (value) { + BlocProvider.of(context) + .add(ProvinceEvent(province: BlocFormItem( + value: value!))); + }, (val) { + return state.province.error; + }), + BiroBebrasDropdown('Bebras Biro', (value) { + BlocProvider.of(context) + .add(BebrasBiroEvent(bebras_biro: BlocFormItem( + value: value!))); + }, (val) { + return state.bebras_biro.error; + }), + SizedBox(height: 20.0), + BlocConsumer( + bloc: _userRegisterBloc, + listener: (context, state) { + if (state is UserRegisterSuccessState) { + context.go('/main'); + } + }, + builder: (context, state) { + return ElevatedButton( + style: ElevatedButton.styleFrom( + fixedSize: Size(size.width, 45), + backgroundColor: Colors.black, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + onPressed: () { + if (state is! UserRegisterLoadingState) { + BlocProvider.of(context) + .add(const FormSubmitEvent()); + // context.go('/main'); + } + }, + child: Text('Daftar', style: TextStyle(fontWeight: FontWeight.w600),), + ); + }), + ], + ), + ), + ), + ); + }, + ) + ); + } +} diff --git a/app/lib/features/authentication/register/presentation/widgets/biro_bebras_dropdown.dart b/app/lib/features/authentication/register/presentation/widgets/biro_bebras_dropdown.dart new file mode 100644 index 0000000..7d2740b --- /dev/null +++ b/app/lib/features/authentication/register/presentation/widgets/biro_bebras_dropdown.dart @@ -0,0 +1,50 @@ +import 'package:bebras_pandai/core/constants/bebrasBiroDropdown.dart'; +import 'package:dropdown_search/dropdown_search.dart'; +import 'package:flutter/material.dart'; + +class BiroBebrasDropdown extends StatelessWidget { + BiroBebrasDropdown(this.labelText, this.handleTextInput, this.validator); + + final String labelText; + final void Function (String value)? handleTextInput; + final String? Function(String?)? validator; + + @override + Widget build(BuildContext context) { + return Container( + height: 63.0, + child: DropdownSearch( + validator: validator, + popupProps: PopupProps.menu( + showSearchBox: true, + ), + items: bebrasBiroList, + dropdownDecoratorProps: DropDownDecoratorProps( + textAlignVertical: TextAlignVertical.center, + baseStyle: TextStyle(fontSize: 12.0), + dropdownSearchDecoration: InputDecoration( + helperText: '', + helperStyle: TextStyle(fontSize: 10,), + hintText: labelText, + filled: true, + fillColor: Colors.grey.shade200, + border: UnderlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + ), + enabledBorder: UnderlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + borderSide: BorderSide(color: Colors.grey.shade200), + ), + focusedBorder: UnderlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + borderSide: BorderSide(color: Colors.grey.shade400), + ), + ), + ), + onChanged: (String? item) => handleTextInput!(item!), + selectedItem: null, + ), + ); + } + +} \ No newline at end of file diff --git a/app/lib/features/authentication/register/presentation/widgets/custom_date_picker.dart b/app/lib/features/authentication/register/presentation/widgets/custom_date_picker.dart new file mode 100644 index 0000000..7238aa1 --- /dev/null +++ b/app/lib/features/authentication/register/presentation/widgets/custom_date_picker.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +class CustomDatePicker extends StatefulWidget { + CustomDatePicker(this.labelText, this.handleTextInput, this.validator); + + final String labelText; + final void Function (String value)? handleTextInput; + final String? Function(String?)? validator; + + @override + State createState() => _CustomDatePickerState(); +} + +class _CustomDatePickerState extends State { + final TextEditingController dateinput = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Container( + height: 60.0, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 0.0), + child: TextFormField( + controller: dateinput, + validator: widget.validator, + style: TextStyle(fontSize: 12.0, color: Colors.black), + decoration: InputDecoration( + helperText: '', + helperStyle: TextStyle(fontSize: 10), + labelText: widget.labelText, + filled: true, + fillColor: Colors.grey.shade200, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + borderSide: BorderSide(color: Colors.grey.shade200), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + borderSide: BorderSide(color: Colors.grey.shade400), + ), + ), + readOnly: true, + //set it true, so that user will not able to edit text + onTap: () async { + DateTime? pickedDate = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(1900), + lastDate: DateTime(2101)); + + if (pickedDate != null) { + String formattedDate = + DateFormat('yyyy-MM-dd').format(pickedDate); + widget.handleTextInput!(formattedDate); + setState(() { + dateinput.text = formattedDate; + }); + } else { + print("Date is not selected"); + } + }, + onChanged: (value) => widget.handleTextInput!(value), + ), + ), + ); + } +} \ No newline at end of file diff --git a/app/lib/features/authentication/register/presentation/widgets/custom_text_field.dart b/app/lib/features/authentication/register/presentation/widgets/custom_text_field.dart new file mode 100644 index 0000000..046edf1 --- /dev/null +++ b/app/lib/features/authentication/register/presentation/widgets/custom_text_field.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + +class CustomTextField extends StatelessWidget { + CustomTextField(this.labelText, this.handleTextInput, this.validator); + + final String labelText; + final void Function (String value)? handleTextInput; + final String? Function(String?)? validator; + + @override + Widget build(BuildContext context) { + return Container( + height: 60.0, + child: TextFormField( + onChanged: (value) => handleTextInput!(value), + validator: validator, + style: TextStyle(fontSize: 12.0), + decoration: InputDecoration( + helperText: '', + helperStyle: TextStyle(fontSize: 10), + labelText: labelText, + filled: true, + fillColor: Colors.grey.shade200, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + borderSide: BorderSide(color: Colors.grey.shade200), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + borderSide: BorderSide(color: Colors.grey.shade400), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/app/lib/features/authentication/register/presentation/widgets/province_dropdown.dart b/app/lib/features/authentication/register/presentation/widgets/province_dropdown.dart new file mode 100644 index 0000000..bb7d407 --- /dev/null +++ b/app/lib/features/authentication/register/presentation/widgets/province_dropdown.dart @@ -0,0 +1,50 @@ +import 'package:bebras_pandai/core/constants/indonesiaProvince.dart'; +import 'package:dropdown_search/dropdown_search.dart'; +import 'package:flutter/material.dart'; + +class ProvinceDropdown extends StatelessWidget { + ProvinceDropdown(this.labelText, this.handleTextInput, this.validator); + + final String labelText; + final void Function (String value)? handleTextInput; + final String? Function(String?)? validator; + + @override + Widget build(BuildContext context) { + return Container( + // margin: const EdgeInsets.only(top: 10), + height: 63.0, + child: DropdownSearch( + validator: validator, + popupProps: PopupProps.menu( + showSearchBox: true, + ), + items: provinceList, + dropdownDecoratorProps: DropDownDecoratorProps( + textAlignVertical: TextAlignVertical.center, + baseStyle: TextStyle(fontSize: 12.0), + dropdownSearchDecoration: InputDecoration( + helperText: '', + helperStyle: TextStyle(fontSize: 10), + hintText: labelText, + filled: true, + fillColor: Colors.grey.shade200, + border: UnderlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + ), + enabledBorder: UnderlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + borderSide: BorderSide(color: Colors.grey.shade200), + ), + focusedBorder: UnderlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + borderSide: BorderSide(color: Colors.grey.shade400), + ), + ), + ), + onChanged: (String? item) => handleTextInput!(item!), + selectedItem: null, + ), + ); + } +} \ No newline at end of file diff --git a/app/lib/features/authentication/register/provider/FormProvider.dart b/app/lib/features/authentication/register/provider/FormProvider.dart new file mode 100644 index 0000000..c353252 --- /dev/null +++ b/app/lib/features/authentication/register/provider/FormProvider.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; + +import '../model/validation_form_model.dart'; + +extension extString on String { + bool get isValidEmail { + final emailRegExp = RegExp(r"^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+"); + return emailRegExp.hasMatch(this); + } + + bool get isValidName{ + final nameRegExp = new RegExp(r"^\s*([A-Za-z]{1,}([\.,] |[-']| ))+[A-Za-z]+\.?\s*$"); + return nameRegExp.hasMatch(this); + } + + bool get isNotNull{ + return this!=null; + } + + bool get isValidPhone{ + final phoneRegExp = RegExp(r"^\+?0[0-9]{10}$"); + return phoneRegExp.hasMatch(this); + } + +} + +class FormProvider extends ChangeNotifier { + ValidationFormModel _email = ValidationFormModel(null, null); + ValidationFormModel _name = ValidationFormModel(null, null); + ValidationFormModel _birth_date = ValidationFormModel(null, null); + ValidationFormModel _school = ValidationFormModel(null, null); + ValidationFormModel _province = ValidationFormModel(null, null); + ValidationFormModel _bebras_biro = ValidationFormModel(null, null); + + ValidationFormModel get email => _email; + ValidationFormModel get name => _name; + ValidationFormModel get birthDate => _birth_date; + ValidationFormModel get school => _school; + ValidationFormModel get province => _province; + ValidationFormModel get bebrasBiro => _bebras_biro; + + void validateEmail(String? val) { + if (val != null && val.isValidEmail) { + _email = ValidationFormModel(val, null); + } else { + _email = ValidationFormModel(null, 'Mohon mengisi e-mail terlebih dahulu!'); + } + notifyListeners(); + } + + void validateName(String? val) { + if (val != null && val.isValidName) { + _name = ValidationFormModel(val, null); + } else { + _name = ValidationFormModel(null, 'Mohon mengisi nama terlebih dahulu!'); + } + notifyListeners(); + } + void validateSchool(String? val) { + if (val != null) { + _school = ValidationFormModel(val, null); + } else { + _school = ValidationFormModel(null, 'Mohon mengisi nama sekolah terlebih dahulu!'); + } + notifyListeners(); + } + + + void validateProvince(String? val) { + if (val != null) { + _province = ValidationFormModel(val, null); + } else { + _province = ValidationFormModel(null, 'Mohon mengisi nama provinsi terlebih dahulu!'); + } + notifyListeners(); + } + + void validateBebrasBiro(String? val) { + if (val != null) { + _bebras_biro = ValidationFormModel(val, null); + } else { + _bebras_biro = ValidationFormModel(null, 'Mohon mengisi bebras biro terlebih dahulu!'); + } + notifyListeners(); + } + + bool get validate { + return _email.value != null && + _name.value != null && + _birth_date.value != null && + _school.value != null && + _province.value != null && + _bebras_biro.value != null; + } +} diff --git a/app/lib/features/authentication/register/repositories/register_user_repo.dart b/app/lib/features/authentication/register/repositories/register_user_repo.dart new file mode 100644 index 0000000..c1868e5 --- /dev/null +++ b/app/lib/features/authentication/register/repositories/register_user_repo.dart @@ -0,0 +1,85 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/foundation.dart'; +import 'package:injectable/injectable.dart'; + +import '../model/registered_user.dart'; +@Injectable() +class RegisterUserRepository { + final _firecloud = FirebaseFirestore.instance.collection('registered_user'); + + Future create({ + required String userId, + required dynamic email, + required String name, + required String birth_date, + required String school, + required String province, + required String bebras_biro, + }) async { + try { + await _firecloud.doc(userId) + .set({ + "name": name, + "email": email, + "birth_date": birth_date, + "school": school, + "province": province, + "bebras_biro": bebras_biro, + }, + SetOptions(merge: true), + ); + } on FirebaseException catch (e) { + if (kDebugMode) { + print("Failed with error '${e.code}': '${e.message}'"); + } + } catch (e) { + throw Exception(e.toString()); + } + } + + Future> getAll() async { + List registeredUserList = []; + try { + final result = await FirebaseFirestore.instance.collection("registered_user").get(); + + result.docs.forEach((element) { + return registeredUserList.add(RegisteredUserModel.fromJson(element.data())); + }); + return registeredUserList; + } on FirebaseException catch (e) { + if (kDebugMode) { + print("Failed with error '${e.code}': '${e.message}'"); + } + return registeredUserList; + } catch (e) { + throw Exception(e.toString()); + } + } + + Future getById(String userId) async { + try { + final result = await FirebaseFirestore.instance + .collection("registered_user") + .doc(userId) + .get() + .then((DocumentSnapshot documentSnapshot) { + if (documentSnapshot.exists) { + return documentSnapshot; + } + }); + if(result != null) { + return RegisteredUserModel.fromJson(result); + } else { + return null; + } + + } on FirebaseException catch (e) { + if (kDebugMode) { + print("Failed with error '${e.code}': '${e.message}'"); + } + } catch (e) { + throw Exception(e.toString()); + } + return null; + } +} \ No newline at end of file diff --git a/app/lib/features/authentication/signin/presentation/bloc/sign_in_bloc.dart b/app/lib/features/authentication/signin/presentation/bloc/sign_in_bloc.dart index 2ec8dd5..2cda657 100644 --- a/app/lib/features/authentication/signin/presentation/bloc/sign_in_bloc.dart +++ b/app/lib/features/authentication/signin/presentation/bloc/sign_in_bloc.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:bebras_pandai/features/authentication/register/repositories/register_user_repo.dart'; import 'package:equatable/equatable.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -13,7 +14,8 @@ part 'sign_in_state.dart'; @injectable class SignInBloc extends Bloc { final SignInUseCase _signInUseCase; - SignInBloc(this._signInUseCase) : super(SignInInitialState()) { + final RegisterUserRepository registerUserRepository; + SignInBloc(this._signInUseCase, this.registerUserRepository) : super(SignInInitialState()) { on(_userSignIn); } @@ -35,6 +37,20 @@ class SignInBloc extends Bloc { }, (right) async { emit(SignInSuccessState(user: right)); + final creds = FirebaseAuth.instance.currentUser; + String userId = creds!.uid as String; + try { + final data = await registerUserRepository.getById(userId); + + print(data); + if (data == null) { + emit(UserUnregistered()); + } else { + emit(UserRegistered()); + } + } catch (e) { + emit(UserError(e.toString())); + } }, ); } diff --git a/app/lib/features/authentication/signin/presentation/bloc/sign_in_state.dart b/app/lib/features/authentication/signin/presentation/bloc/sign_in_state.dart index f2d580a..0984e9f 100644 --- a/app/lib/features/authentication/signin/presentation/bloc/sign_in_state.dart +++ b/app/lib/features/authentication/signin/presentation/bloc/sign_in_state.dart @@ -32,3 +32,21 @@ class SignInFailureState extends SignInState { @override List get props => [message]; } + +class UserUnregistered extends SignInState { + @override + List get props => []; +} + +class UserRegistered extends SignInState { + @override + List get props => []; +} + +class UserError extends SignInState { + final String error; + + UserError(this.error); + @override + List get props => [error]; +} diff --git a/app/lib/features/authentication/signin/presentation/pages/_pages.dart b/app/lib/features/authentication/signin/presentation/pages/_pages.dart index c3065f5..4706ef8 100644 --- a/app/lib/features/authentication/signin/presentation/pages/_pages.dart +++ b/app/lib/features/authentication/signin/presentation/pages/_pages.dart @@ -2,9 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import '../../../../../core/bases/enum/button_type.dart'; -import '../../../../../core/bases/widgets/atoms/button.dart'; import '../../../../../core/constants/assets.dart'; +import '../../../../../core/constants/colorConstant.dart'; import '../../../../../services/di.dart'; import '../bloc/sign_in_bloc.dart'; diff --git a/app/lib/features/authentication/signin/presentation/pages/sign_in_page.dart b/app/lib/features/authentication/signin/presentation/pages/sign_in_page.dart index 71ddd30..7af8086 100644 --- a/app/lib/features/authentication/signin/presentation/pages/sign_in_page.dart +++ b/app/lib/features/authentication/signin/presentation/pages/sign_in_page.dart @@ -67,7 +67,9 @@ class _LoginPageState extends State { BlocConsumer( bloc: _signInBloc, listener: (context, state) { - if (state is SignInSuccessState) { + if (state is UserUnregistered) { + context.go('/register'); + } else if (state is UserRegistered) { context.go('/main'); } }, @@ -75,10 +77,21 @@ class _LoginPageState extends State { return SizedBox( height: 50, width: 200, - child: Button( - text: 'Login with Google', - buttonType: ButtonType.primary, - onTap: () { + child: ElevatedButton( + child: Text( + 'Login with Google', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900,),), + style: ButtonStyle( + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), + ) + ), + backgroundColor: MaterialStateProperty.all( + ColorConstants.darkPrimaryBlueColor, + ), + ), + onPressed: () { if (state is! SignInLoadingState) { _signInBloc.add( TriggerSignInEvent(), @@ -97,5 +110,108 @@ class _LoginPageState extends State { ), ), ); + // Scaffold( + // backgroundColor: ColorConstants.primaryBlueColor, + // body: SafeArea( + // child: Stack( + // children: [ + // // Image.asset( + // // Assets.studyBackground, + // // fit: BoxFit.cover, + // // height: size.height * 0.45, + // // ), + // Align( + // alignment: Alignment.bottomCenter, + // child: Container( + // padding: const EdgeInsets.all(32), + // height: size.height, + // width: size.width, + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(32), + // color: Colors.transparent, + // ), + // child: Column( + // children: [ + // Image.asset( + // Assets.bebrasPandaiText, + // ), + // const SizedBox( + // height: 55, + // ), + // Image.asset( + // Assets.bebrasMascot, + // fit: BoxFit.cover, + // height: size.height * 0.4, + // ), + // const SizedBox( + // height: 30, + // ), + // const Text( + // 'Selamat Datang di Aplikasi Bebras Pandai!', + // textAlign: TextAlign.center, + // style: TextStyle( + // color: ColorConstants.whiteColor, + // fontWeight: FontWeight.bold, + // fontSize: 24, + // ), + // ), + // const SizedBox( + // height: 10, + // ), + // const Text( + // 'Yuk cari tahu seberapa tajam logikamu!', + // style: TextStyle( + // color: ColorConstants.whiteColor, + // ), + // ), + // const SizedBox( + // height: 24, + // ), + // BlocConsumer( + // bloc: _signInBloc, + // listener: (context, state) { + // if (state is UserUnregistered) { + // context.go('/register'); + // } else if (state is UserRegistered) { + // context.go('/main'); + // } + // }, + // builder: (context, state) { + // return SizedBox( + // height: 50, + // width: 200, + // child: ElevatedButton( + // child: Text( + // 'Login with Google', + // style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900,),), + // style: ButtonStyle( + // shape: MaterialStateProperty.all( + // RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(18.0), + // ) + // ), + // backgroundColor: MaterialStateProperty.all( + // ColorConstants.darkPrimaryBlueColor, + // ), + // ), + // onPressed: () { + // if (state is! SignInLoadingState) { + // _signInBloc.add( + // TriggerSignInEvent(), + // ); + // } + // }, + // ), + // ); + // }, + // ), + // ], + // ), + // ), + // ), + // ], + // ), + // ), + // ); } } diff --git a/app/lib/features/main/presentation/pages/_pages.dart b/app/lib/features/main/presentation/pages/_pages.dart index 1e1b68d..783f4be 100644 --- a/app/lib/features/main/presentation/pages/_pages.dart +++ b/app/lib/features/main/presentation/pages/_pages.dart @@ -1,3 +1,4 @@ +import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:google_sign_in/google_sign_in.dart'; diff --git a/app/lib/features/main/presentation/pages/main_page.dart b/app/lib/features/main/presentation/pages/main_page.dart index e99e45d..27de950 100644 --- a/app/lib/features/main/presentation/pages/main_page.dart +++ b/app/lib/features/main/presentation/pages/main_page.dart @@ -7,8 +7,20 @@ class MainPage extends StatefulWidget { State createState() => _MainPageState(); } +class Course { + String title; + String description; + Course(this.title, this.description); +} + class _MainPageState extends State { final nama = 'dummy'; + final List courses = [ + Course('SiKecil', 'abdcdafadf'), + Course('Siaga', 'abdcdafadf'), + Course('Penggalang', 'abdcdafadf'), + Course('Penegak', 'abdcdafadf'), + ]; @override Widget build(BuildContext context) { @@ -27,7 +39,7 @@ class _MainPageState extends State { Assets.bebrasPandaiText, ), const SizedBox( - height: 100, + height: 40, ), RichText( text: TextSpan( @@ -71,7 +83,7 @@ class _MainPageState extends State { Button( buttonType: ButtonType.primary, onTap: () async { - await context.push('/construction'); + await context.push('/quiz_registration'); }, text: 'Ikut Quiz', ), @@ -81,6 +93,7 @@ class _MainPageState extends State { Button( buttonType: ButtonType.primary, onTap: () async { + await FirebaseAuth.instance.signOut(); await GoogleSignIn().signOut(); context.go('/onboarding'); }, diff --git a/app/lib/features/onboarding/presentation/bloc/user_initialization_bloc.dart b/app/lib/features/onboarding/presentation/bloc/user_initialization_bloc.dart index 1c43fce..a6a0455 100644 --- a/app/lib/features/onboarding/presentation/bloc/user_initialization_bloc.dart +++ b/app/lib/features/onboarding/presentation/bloc/user_initialization_bloc.dart @@ -1,30 +1,47 @@ import 'dart:async'; import 'package:equatable/equatable.dart'; +import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:injectable/injectable.dart'; +import '../../../authentication/register/repositories/register_user_repo.dart'; + part 'user_initialization_event.dart'; + part 'user_initialization_state.dart'; @injectable @singleton class UserInitializationBloc extends Bloc { - final GoogleSignIn _googleSignIn = GoogleSignIn(); - UserInitializationBloc() : super(UserInitializationInitial()) { + final RegisterUserRepository registerUserRepository; + // final GoogleSignIn _googleSignIn = GoogleSignIn(); + + UserInitializationBloc(this.registerUserRepository) : super(UserInitializationInitial()) { on(_auth); } - FutureOr _auth( - OnboardingAuthEvent state, - Emitter emit, - ) async { - final creds = await _googleSignIn.signInSilently(); - + FutureOr _auth(OnboardingAuthEvent state, + Emitter emit,) async { + // final creds = await _googleSignIn.signInSilently(); + final creds = FirebaseAuth.instance.currentUser; if (creds != null) { emit(UserAuthenticated()); + String userId = creds.uid as String; + try { + final data = await registerUserRepository.getById(userId); + + print(data); + if (data == null) { + emit(UserUnregistered()); + } else { + emit(UserRegistered()); + } + } catch (e) { + emit(UserError(e.toString())); + } } else { emit(UserUnauthenticated()); } diff --git a/app/lib/features/onboarding/presentation/bloc/user_initialization_event.dart b/app/lib/features/onboarding/presentation/bloc/user_initialization_event.dart index 0e451ee..52fd027 100644 --- a/app/lib/features/onboarding/presentation/bloc/user_initialization_event.dart +++ b/app/lib/features/onboarding/presentation/bloc/user_initialization_event.dart @@ -8,3 +8,27 @@ abstract class UserInitializationEvent extends Equatable { } class OnboardingAuthEvent extends UserInitializationEvent {} + +class CheckRegisteredUser extends UserInitializationEvent {} + +class CreateUserData extends UserInitializationEvent { + final String email; + final String name; + final String birth_date; + final String school; + final String province; + final String bebras_biro; + + CreateUserData( + this.email, + this.name, + this.birth_date, + this.school, + this.province, + this.bebras_biro, + ); +} + +class GetUserData extends UserInitializationEvent { + GetUserData(); +} diff --git a/app/lib/features/onboarding/presentation/bloc/user_initialization_state.dart b/app/lib/features/onboarding/presentation/bloc/user_initialization_state.dart index 3490edc..2e81ff1 100644 --- a/app/lib/features/onboarding/presentation/bloc/user_initialization_state.dart +++ b/app/lib/features/onboarding/presentation/bloc/user_initialization_state.dart @@ -1,16 +1,58 @@ part of 'user_initialization_bloc.dart'; -abstract class UserInitializationState extends Equatable { - const UserInitializationState(); +abstract class UserInitializationState extends Equatable {} +class UserInitializationInitial extends UserInitializationState { @override List get props => []; } -class UserInitializationInitial extends UserInitializationState {} +class UserInitializationLoading extends UserInitializationState { + @override + List get props => []; +} + +class UserError extends UserInitializationState { + final String error; -class UserInitializationLoading extends UserInitializationState {} + UserError(this.error); + @override + List get props => [error]; +} -class UserAuthenticated extends UserInitializationState {} +class UserAuthenticated extends UserInitializationState { + @override + List get props => []; +} + +class UserUnauthenticated extends UserInitializationState { + @override + List get props => []; +} + +class UserUnregistered extends UserInitializationState { + @override + List get props => []; +} + +class UserRegistered extends UserInitializationState { + @override + List get props => []; +} + +class UserGetDataLoading extends UserInitializationState { + @override + List get props => []; +} + +class UserDataUploaded extends UserInitializationState { + @override + List get props => []; +} -class UserUnauthenticated extends UserInitializationState {} +// class UserDataLoaded extends UserInitializationState { +// final RegisteredUserModel userData; +// UserDataLoaded(this.userDataList); +// @override +// List get props => [userDataList]; +// } diff --git a/app/lib/features/onboarding/presentation/pages/splash_screen.dart b/app/lib/features/onboarding/presentation/pages/splash_screen.dart index d894df6..859159d 100644 --- a/app/lib/features/onboarding/presentation/pages/splash_screen.dart +++ b/app/lib/features/onboarding/presentation/pages/splash_screen.dart @@ -8,12 +8,14 @@ class SplashScreen extends StatelessWidget { final size = MediaQuery.of(context).size; return BlocListener( listener: (context, state) { - if (state is UserAuthenticated) { - context.go('/main'); - } else if (state is UserUnauthenticated) { + if (state is UserUnauthenticated) { context.go('/onboarding'); + } else if (state is UserUnregistered) { + context.go('/register'); + } else if (state is UserRegistered) { + context.go('/main'); } - }, + }, child: Scaffold( body: Column( children: [ diff --git a/app/lib/features/quiz_registration/bloc/quiz_registration_cubit.dart b/app/lib/features/quiz_registration/bloc/quiz_registration_cubit.dart new file mode 100644 index 0000000..405075f --- /dev/null +++ b/app/lib/features/quiz_registration/bloc/quiz_registration_cubit.dart @@ -0,0 +1,21 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +part 'quiz_registration_state.dart'; +part 'quiz_registration_event.dart'; + +class QuizRegistrationCubit extends Cubit { + QuizRegistrationCubit() : super(QuizRegistrationInitialState()); + + void selectWeek(String selectedWeek) { + emit(QuizRegistrationLoading()); + + emit(QuizRegistrationWeekSelected(selectedWeek)); + } + + void selectLevel(String selectedLevel) { + emit(QuizRegistrationLoading()); + + emit(QuizRegistrationLevelSelected(selectedLevel)); + } +} diff --git a/app/lib/features/quiz_registration/bloc/quiz_registration_event.dart b/app/lib/features/quiz_registration/bloc/quiz_registration_event.dart new file mode 100644 index 0000000..a048663 --- /dev/null +++ b/app/lib/features/quiz_registration/bloc/quiz_registration_event.dart @@ -0,0 +1,8 @@ +part of 'quiz_registration_cubit.dart'; + +abstract class QuizRegistrationEvent extends Equatable { + const QuizRegistrationEvent(); + + @override + List get props => []; +} diff --git a/app/lib/features/quiz_registration/bloc/quiz_registration_state.dart b/app/lib/features/quiz_registration/bloc/quiz_registration_state.dart new file mode 100644 index 0000000..35dce2a --- /dev/null +++ b/app/lib/features/quiz_registration/bloc/quiz_registration_state.dart @@ -0,0 +1,30 @@ +part of 'quiz_registration_cubit.dart'; + +abstract class QuizRegistrationState extends Equatable { + const QuizRegistrationState(); + + @override + List get props => []; +} + +class QuizRegistrationInitialState extends QuizRegistrationState {} + +class QuizRegistrationLoading extends QuizRegistrationState {} + +class QuizRegistrationWeekSelected extends QuizRegistrationState { + final String selectedWeek; + + const QuizRegistrationWeekSelected(this.selectedWeek); + + @override + List get props => [selectedWeek]; +} + +class QuizRegistrationLevelSelected extends QuizRegistrationState { + final String selectedLevel; + + const QuizRegistrationLevelSelected(this.selectedLevel); + + @override + List get props => [selectedLevel]; +} diff --git a/app/lib/features/quiz_registration/presentation/pages/_pages.dart b/app/lib/features/quiz_registration/presentation/pages/_pages.dart new file mode 100644 index 0000000..91946da --- /dev/null +++ b/app/lib/features/quiz_registration/presentation/pages/_pages.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../core/bases/enum/button_type.dart'; +import '../../../../core/bases/widgets/atoms/button.dart'; +import '../../../../core/bases/widgets/layout/bebras_scaffold.dart'; +import '../../../../core/constants/assets.dart'; +import '../../bloc/quiz_registration_cubit.dart'; + +part 'quiz_registration_page.dart'; diff --git a/app/lib/features/quiz_registration/presentation/pages/quiz_registration_page.dart b/app/lib/features/quiz_registration/presentation/pages/quiz_registration_page.dart new file mode 100644 index 0000000..c8e60a8 --- /dev/null +++ b/app/lib/features/quiz_registration/presentation/pages/quiz_registration_page.dart @@ -0,0 +1,272 @@ +// ignore_for_file: lines_longer_than_80_chars, require_trailing_commas + +part of '_pages.dart'; + +class QuizRegistrationPage extends StatefulWidget { + const QuizRegistrationPage({super.key}); + + @override + State createState() => _QuizRegistrationPageState(); +} + +class _QuizRegistrationPageState extends State { + final nama = 'dummy'; + + Widget quizCard() { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18), + decoration: BoxDecoration(border: Border.all()), + child: const Column(children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Quiz A', + style: TextStyle(fontSize: 18), + ), + Text( + 'Nilai: 100', + style: TextStyle(fontSize: 18), + ) + ], + ), + SizedBox( + height: 8, + ), + Row( + children: [ + Text( + 'dikerjakan: 2023-09-09 09:09', + style: TextStyle(fontSize: 12), + ) + ], + ) + ]), + ); + } + + Future showModal() async { + return showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return BlocConsumer( + listener: (context, state) { + // TODO: implement listener + }, + builder: (context, state) { + if (state is QuizRegistrationWeekSelected && + state.selectedWeek != '') { + return Container( + constraints: const BoxConstraints(minHeight: 30), + width: double.infinity, + color: Colors.white, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: 20, + ), + Container( + margin: const EdgeInsets.only(left: 20), + child: InkWell( + onTap: () => context + .read() + .selectWeek(''), + child: const Row( + children: [ + Icon(Icons.chevron_left), + Text('Pilih Minggu') + ], + ), + ), + ), + const SizedBox( + height: 20, + ), + Container( + margin: const EdgeInsets.only(left: 20), + child: Text( + 'Daftar Latihan Bebras ${state.selectedWeek == 'next_week' ? 'Minggu Depan' : 'Minggu Ini'}', + textAlign: TextAlign.left, + style: const TextStyle( + fontSize: 18, fontWeight: FontWeight.bold), + ), + ), + const SizedBox( + height: 25, + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 40), + width: double.infinity, + child: Button( + onTap: () => context + .read() + .selectLevel('sikecil'), + customButtonColor: Colors.blue.shade400, + customTextColor: Colors.white, + text: 'siKecil', + )), + const SizedBox( + height: 15, + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 40), + width: double.infinity, + child: Button( + onTap: () => context + .read() + .selectLevel('siaga'), + customButtonColor: Colors.green.shade400, + customTextColor: Colors.white, + text: 'Siaga', + )), + const SizedBox( + height: 15, + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 40), + width: double.infinity, + child: Button( + onTap: () => context + .read() + .selectLevel('penggalang'), + customButtonColor: Colors.red.shade400, + customTextColor: Colors.white, + text: 'Penggalang', + )), + const SizedBox( + height: 15, + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 40), + width: double.infinity, + child: Button( + onTap: () => context + .read() + .selectLevel('penegak'), + customButtonColor: Colors.orange.shade400, + customTextColor: Colors.white, + text: 'Penegak', + )), + const SizedBox( + height: 20, + ), + ], + ), + ); + } + return Container( + height: 260, + width: double.infinity, + color: Colors.white, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: 40, + ), + Container( + margin: const EdgeInsets.only(left: 20), + child: const Text( + 'Daftar Latihan Bebras', + textAlign: TextAlign.left, + style: + TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ), + const SizedBox( + height: 25, + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 40), + width: double.infinity, + child: Button( + onTap: () => context + .read() + .selectWeek('next_week'), + customButtonColor: Colors.green.shade400, + customTextColor: Colors.white, + text: 'Latihan Minggu Depan', + )), + const SizedBox( + height: 20, + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 40), + width: double.infinity, + child: Button( + onTap: () => context + .read() + .selectWeek('this_week'), + customButtonColor: Colors.brown.shade400, + customTextColor: Colors.white, + text: 'Latihan Minggu Ini', + )) + ], + ), + ); + }, + ); + }, + ).whenComplete(() => null); + } + + @override + Widget build(BuildContext context) { + return BebrasScaffold( + body: SingleChildScrollView( + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset( + Assets.bebrasPandaiText, + ), + const SizedBox( + height: 40, + ), + const Text('Latihan yang pernah diikuti'), + const SizedBox( + height: 10, + ), + Container( + height: MediaQuery.of(context).size.height - 300, + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration(border: Border.all()), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Silahkan klik Tombol `Daftar Latihan Bebras` dibawah untuk memulai', + ), + const SizedBox( + height: 20, + ), + quizCard() + ]), + ), + const SizedBox( + height: 10, + ), + Button( + buttonType: ButtonType.tertiary, + onTap: () async { + await showModal(); + }, + text: 'Daftar Latihan Bebras', + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/app/lib/services/di.dart b/app/lib/services/di.dart index d53fac3..e7abc66 100644 --- a/app/lib/services/di.dart +++ b/app/lib/services/di.dart @@ -2,7 +2,7 @@ import 'package:get_it/get_it.dart'; import 'package:injectable/injectable.dart'; import 'di.config.dart'; -final GetIt get = GetIt.I; +final GetIt get = GetIt.instance; @InjectableInit( initializerName: 'init', diff --git a/app/lib/services/router_service.dart b/app/lib/services/router_service.dart index 355d3ae..94fdc55 100644 --- a/app/lib/services/router_service.dart +++ b/app/lib/services/router_service.dart @@ -1,10 +1,12 @@ import 'package:go_router/go_router.dart'; +import '../features/authentication/register/presentation/pages/_pages.dart'; import '../features/authentication/signin/presentation/pages/_pages.dart'; import '../features/error/presentation/pages/_pages.dart'; import '../features/main/presentation/pages/_pages.dart'; import '../features/onboarding/presentation/pages/_pages.dart'; import '../features/quiz_exercise/presentation/pages/_pages.dart'; +import '../features/quiz_registration/presentation/pages/_pages.dart'; GoRouter router = GoRouter( routes: [ @@ -16,6 +18,10 @@ GoRouter router = GoRouter( path: '/onboarding', builder: (context, state) => const LoginPage(), ), + GoRoute( + path: '/register', + builder: (context, state) => const RegisterPage(), + ), GoRoute( path: '/main', builder: (context, state) => const MainPage(), @@ -28,5 +34,9 @@ GoRouter router = GoRouter( path: '/quiz_exercise', builder: (context, state) => const QuizExercisePage(), ), + GoRoute( + path: '/quiz_registration', + builder: (context, state) => const QuizRegistrationPage(), + ), ], ); diff --git a/app/pubspec.lock b/app/pubspec.lock index e6903cc..18b216b 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -257,6 +257,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.3.2" + dropdown_search: + dependency: "direct main" + description: + name: dropdown_search + sha256: "55106e8290acaa97ed15bea1fdad82c3cf0c248dd410e651f5a8ac6870f783ab" + url: "https://pub.dev" + source: hosted + version: "5.0.6" either_dart: dependency: "direct main" description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 5fe7483..118146d 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -56,6 +56,7 @@ dependencies: url_launcher: ^6.1.14 flutter_dotenv: ^5.1.0 cloud_firestore: ^4.9.3 + dropdown_search: ^5.0.6 dev_dependencies: build_runner: null