Files
eatvio_app/lib/features/registration/presentation/pages/registration_screen.dart
2025-09-08 14:14:41 +03:00

379 lines
15 KiB
Dart

import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_state.dart';
import 'package:eatvio_app/features/registration/presentation/pages/additional_information_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/allergies_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/blood_pressure_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/body_circumferences_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/city_of_living_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/credentials_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/date_of_birth_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/favorite_foods_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/favorite_sports_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/gender_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/height_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/medicine_intake_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/name_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/phone_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/weight_screen.dart';
import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:validators/validators.dart';
class RegistrationScreen extends StatefulWidget {
const RegistrationScreen({super.key});
@override
State<RegistrationScreen> createState() => _RegistrationScreenState();
}
class _RegistrationScreenState extends State<RegistrationScreen> {
double progressValue = 1 / 15;
int step = 1;
final pageController = PageController();
final RegistrationCubit registrationCubit = sl<RegistrationCubit>();
@override
void initState() {
super.initState();
RegistrationAnalyticsService.reportRegistrationStep(step);
}
@override
Widget build(BuildContext context) {
return BlocProvider<RegistrationCubit>(
create: (BuildContext context) => registrationCubit,
child: BlocListener<RegistrationCubit, RegistrationState>(
listener: (context, state) {
if (state.error != null && state.error!.isNotEmpty) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка при регистрации'),
content: Text(state.error!),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
}
},
child: BlocBuilder<RegistrationCubit, RegistrationState>(
builder: (BuildContext context, RegistrationState state) {
return Scaffold(
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: Row(
children: [
Container(
height: 48,
width: MediaQuery.of(context).size.width * 0.2,
padding: const EdgeInsets.only(left: 16, right: 4),
child: FloatingActionButton.extended(
heroTag: 'backButtonRegistration',
highlightElevation: 0,
focusElevation: 0,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
backgroundColor: const Color(0xFFEAF2FD),
onPressed: () async {
setState(() {
pageController.previousPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut);
});
},
label: Icon(Icons.arrow_back_ios_new_rounded, color: appTheme.blueA400),
),
),
Container(
height: 48,
width: MediaQuery.of(context).size.width * 0.8,
padding: const EdgeInsets.only(left: 4, right: 16),
child: FloatingActionButton.extended(
heroTag: 'nextButtonRegistration',
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
backgroundColor: appTheme.blueA400,
onPressed: () async {
if (await canGoNext(state, context)) {
setState(() {
pageController.nextPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut);
});
}
},
label: Text('Продолжить', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)),
),
),
],
),
appBar: AppBar(
automaticallyImplyLeading: false,
leading: Builder(
builder: (context) => IconButton(icon: const Icon(Icons.close_rounded), onPressed: () => GoRouter.of(context).pop()),
),
centerTitle: true,
title: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: 48, fit: BoxFit.contain),
actions: [Padding(padding: const EdgeInsets.only(right: 16), child: Text('шаг $step из 15'))],
bottom: PreferredSize(
preferredSize: Size(MediaQuery.of(context).size.width, 32),
child: Padding(
padding: const EdgeInsets.all(16),
child: TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
tween: Tween<double>(begin: 1 / 15, end: progressValue),
builder: (context, value, _) => LinearProgressIndicator(
value: value,
borderRadius: BorderRadius.circular(16),
minHeight: 8,
backgroundColor: Colors.grey.shade100,
valueColor: AlwaysStoppedAnimation<Color>(appTheme.blueA400),
),
),
),
),
),
body: PageView(
physics: const NeverScrollableScrollPhysics(),
controller: pageController,
onPageChanged: (index) {
setState(() {
step = index + 1;
progressValue = step / 15;
});
// Report analytics for step completion
RegistrationAnalyticsService.reportRegistrationStep(step);
},
children: [
// LanguageSelectScreen(sheet: state.sheet),
PhoneScreen(sheet: state.sheet),
CredentialsScreen(sheet: state.sheet),
NameScreen(sheet: state.sheet),
GenderScreen(sheet: state.sheet),
DateOfBirthScreen(sheet: state.sheet),
CityOfLivingScreen(sheet: state.sheet),
BodyCircumferencesScreen(sheet: state.sheet),
WeightScreen(sheet: state.sheet),
HeightScreen(sheet: state.sheet),
BloodPressureScreen(sheet: state.sheet),
AllergiesScreen(sheet: state.sheet),
MedicineIntakeScreen(sheet: state.sheet),
FavoriteSportsScreen(sheet: state.sheet),
FavoriteFoodsScreen(sheet: state.sheet),
AdditionalInformationScreen(sheet: state.sheet),
],
),
);
},
),
),
);
}
Future<bool> canGoNext(RegistrationState state, BuildContext context) async {
switch (step) {
case 1:
if (state.sheet.phone.length != 18) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: const Text('Введите корректный номер телефона'),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
return false;
} else {
return true;
}
case 2:
List<String> errors = validateCredentialsFields(state.sheet.email, state.sheet.password, state.sheet.passwordConfirmation, context);
if (errors.isEmpty) {
return true;
} else {
String errorMessage = '';
for (var error in errors) {
errorMessage += '$error\n\n';
}
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: Text(errorMessage),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
return false;
}
case 3:
List<String> errors = validateNameFields(state.sheet.firstName.toString(), state.sheet.lastName.toString(), context);
if (errors.isEmpty) {
return true;
} else {
String errorMessage = '';
for (var error in errors) {
errorMessage += '$error\n\n';
}
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: Text(errorMessage),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
return false;
}
case 4:
if (state.sheet.gender.isNotEmpty) {
return true;
} else {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: const Text('Выберете пол'),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
return false;
}
case 6:
if (state.sheet.location.isNotEmpty) {
return true;
} else {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: const Text('Заполните все поля'),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
return false;
}
case 7:
List<String> errors = validateCircumferencesFields(
state.sheet.chestCircumference.toString(),
state.sheet.waistCircumference.toString(),
state.sheet.hipCircumference.toString(),
context,
);
if (errors.isEmpty) {
return true;
} else {
String errorMessage = '';
for (var error in errors) {
errorMessage += '$error\n\n';
}
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: Text(errorMessage),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
return false;
}
case 15:
try {
showDialog(
barrierDismissible: false,
useSafeArea: false,
context: context,
builder: (context) => const PopScope(canPop: false, child: Loader()),
);
await registrationCubit.tryRegistration();
if (context.mounted) {
Navigator.pop(context);
}
} catch (e) {
if (context.mounted) {
Navigator.pop(context);
showDialog(
context: context,
builder: (context) => const AlertDialog(title: Text('Ошибка при попытке регистрации')),
);
}
}
return false;
default:
return true;
}
}
List<String> validateCircumferencesFields(String chestCircumference, String waistCircumference, String hipCircumference, BuildContext context) {
final s = S.of(context);
List<String> errors = [];
if (chestCircumference.isEmpty | waistCircumference.isEmpty | hipCircumference.isEmpty) {
errors.add(s.circumference_empty);
}
if (![chestCircumference, waistCircumference, hipCircumference].every((circumference) => int.tryParse(circumference) != null)) {
errors.add(s.circumference_is_not_a_number);
}
return errors;
}
List<String> validateCredentialsFields(String email, String password, String passwordConfirmation, BuildContext context) {
final s = S.of(context);
List<String> errors = [];
if (!isEmail(email)) {
errors.add(s.email_invalid);
}
if (email.isEmpty) {
errors.add(s.email_empty);
}
if (password.length < 6) {
errors.add(s.password_short);
}
if (password != passwordConfirmation) {
errors.add(s.password_confirmation_mismatch);
}
return errors;
}
List<String> validateNameFields(String firstName, String lastName, BuildContext context) {
List<String> errors = [];
if (firstName.isEmpty) {
errors.add('First name cannot be empty');
}
if (firstName.isEmpty) {
errors.add('First name is too short');
}
if (lastName.isEmpty) {
errors.add('Last name cannot be empty');
}
if (lastName.isEmpty) {
errors.add('Last name is too short');
}
return errors;
}
}