# Conflicts:
#	lib/presentation/navigation/app_router.dart
#	pubspec.yaml
This commit is contained in:
2026-08-11 00:14:00 +03:00
parent c9b8d143d1
commit 182be1049b
351 changed files with 24566 additions and 22898 deletions

View File

@@ -1,186 +1,186 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../core/app_colors.dart';
import '../../di/service_locator.dart';
import '../../domain/usecase/apply_promo_code_usecase.dart';
import '../components/custom_app_bar.dart';
import '../event/promo_code_event.dart';
import '../state/promo_code_state.dart';
import '../viewmodel/promo_code_bloc.dart';
class PromoCodeScreen extends StatelessWidget {
const PromoCodeScreen({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => PromoCodeBloc(getIt<ApplyPromoCodeUsecase>()),
child: const _PromoCodeScreenContent(),
);
}
}
class _PromoCodeScreenContent extends StatefulWidget {
const _PromoCodeScreenContent();
@override
State<_PromoCodeScreenContent> createState() => _PromoCodeScreenContentState();
}
class _PromoCodeScreenContentState extends State<_PromoCodeScreenContent> {
final TextEditingController promoController = TextEditingController();
@override
void dispose() {
promoController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
body: Container(
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
child: SafeArea(
child: Column(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
const SizedBox(height: 16),
CustomAppBar(title: 'Промокоды'),
const SizedBox(height: 32),
// 🔹 LISTENER: Только сайд-эффекты (SnackBar, навигация)
BlocListener<PromoCodeBloc, PromoCodeState>(
listener: (context, state) {
if (state.status == PromoCodeStatus.success) {
promoController.clear();
Future.delayed(const Duration(milliseconds: 800), () {
if (mounted) Navigator.pop(context);
});
}
},
child: BlocBuilder<PromoCodeBloc, PromoCodeState>(
builder: (context, state) {
final bool hasError = state.status == PromoCodeStatus.failure;
final bool isLoading = state.status == PromoCodeStatus.loading;
return Container(
padding: const EdgeInsets.all(25),
decoration: BoxDecoration(
color: const Color(0xFF141530),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'У вас есть промокод?',
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600),
),
const SizedBox(height: 24),
const Text(
'Введите промокод и получите скидку на поездку',
style: TextStyle(color: AppColors.white70, fontSize: 16),
),
const SizedBox(height: 20),
TextField(
controller: promoController,
enabled: !isLoading,
style: TextStyle(color: hasError ? Colors.red : Colors.white),
decoration: InputDecoration(
hintText: 'Введите промокод',
hintStyle: const TextStyle(color: AppColors.white70),
filled: true,
fillColor: Colors.white.withOpacity(0.1),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
borderSide: BorderSide(
color: hasError ? Colors.red : AppColors.smsDigit,
width: 1.5,
),
),
// ✅ Явно убираем ошибку при успехе
errorText: hasError ? 'Неверный промокод' : null,
),
onSubmitted: (_) => _submit(context),
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: isLoading ? null : () => Navigator.pop(context),
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
side: BorderSide(color: AppColors.white70.withOpacity(0.4)),
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: const Text('Отмена', style: TextStyle(color: AppColors.white70)),
),
),
const SizedBox(width: 22),
Expanded(
child: ElevatedButton(
onPressed: isLoading ? null : () => _submit(context),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
backgroundColor: AppColors.activeButtonGradient.colors[0],
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(color: AppColors.activeButtonText, strokeWidth: 2),
)
: const Text('Активировать', style: TextStyle(color: AppColors.activeButtonText)),
),
),
],
),
],
),
);
},
),
),
const Spacer(),
],
),
),
),
Image.asset(
'assets/promo_bottom.png',
width: double.infinity,
fit: BoxFit.contain,
alignment: Alignment.center,
),
const SizedBox(height: 80),
],
),
),
),
);
}
void _submit(BuildContext context) {
final code = promoController.text.trim();
if (code.isEmpty) return;
context.read<PromoCodeBloc>().add(PromoCodeApplyRequested(code));
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../core/app_colors.dart';
import '../../di/service_locator.dart';
import '../../domain/usecase/apply_promo_code_usecase.dart';
import '../components/custom_app_bar.dart';
import '../event/promo_code_event.dart';
import '../state/promo_code_state.dart';
import '../viewmodel/promo_code_bloc.dart';
class PromoCodeScreen extends StatelessWidget {
const PromoCodeScreen({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => PromoCodeBloc(getIt<ApplyPromoCodeUsecase>()),
child: const _PromoCodeScreenContent(),
);
}
}
class _PromoCodeScreenContent extends StatefulWidget {
const _PromoCodeScreenContent();
@override
State<_PromoCodeScreenContent> createState() => _PromoCodeScreenContentState();
}
class _PromoCodeScreenContentState extends State<_PromoCodeScreenContent> {
final TextEditingController promoController = TextEditingController();
@override
void dispose() {
promoController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
body: Container(
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
child: SafeArea(
child: Column(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
const SizedBox(height: 16),
CustomAppBar(title: 'Промокоды'),
const SizedBox(height: 32),
// 🔹 LISTENER: Только сайд-эффекты (SnackBar, навигация)
BlocListener<PromoCodeBloc, PromoCodeState>(
listener: (context, state) {
if (state.status == PromoCodeStatus.success) {
promoController.clear();
Future.delayed(const Duration(milliseconds: 800), () {
if (mounted) Navigator.pop(context);
});
}
},
child: BlocBuilder<PromoCodeBloc, PromoCodeState>(
builder: (context, state) {
final bool hasError = state.status == PromoCodeStatus.failure;
final bool isLoading = state.status == PromoCodeStatus.loading;
return Container(
padding: const EdgeInsets.all(25),
decoration: BoxDecoration(
color: const Color(0xFF141530),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'У вас есть промокод?',
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600),
),
const SizedBox(height: 24),
const Text(
'Введите промокод и получите скидку на поездку',
style: TextStyle(color: AppColors.white70, fontSize: 16),
),
const SizedBox(height: 20),
TextField(
controller: promoController,
enabled: !isLoading,
style: TextStyle(color: hasError ? Colors.red : Colors.white),
decoration: InputDecoration(
hintText: 'Введите промокод',
hintStyle: const TextStyle(color: AppColors.white70),
filled: true,
fillColor: Colors.white.withOpacity(0.1),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
borderSide: BorderSide(
color: hasError ? Colors.red : AppColors.smsDigit,
width: 1.5,
),
),
// ✅ Явно убираем ошибку при успехе
errorText: hasError ? 'Неверный промокод' : null,
),
onSubmitted: (_) => _submit(context),
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: isLoading ? null : () => Navigator.pop(context),
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
side: BorderSide(color: AppColors.white70.withOpacity(0.4)),
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: const Text('Отмена', style: TextStyle(color: AppColors.white70)),
),
),
const SizedBox(width: 22),
Expanded(
child: ElevatedButton(
onPressed: isLoading ? null : () => _submit(context),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
backgroundColor: AppColors.activeButtonGradient.colors[0],
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(color: AppColors.activeButtonText, strokeWidth: 2),
)
: const Text('Активировать', style: TextStyle(color: AppColors.activeButtonText)),
),
),
],
),
],
),
);
},
),
),
const Spacer(),
],
),
),
),
Image.asset(
'assets/promo_bottom.png',
width: double.infinity,
fit: BoxFit.contain,
alignment: Alignment.center,
),
const SizedBox(height: 80),
],
),
),
),
);
}
void _submit(BuildContext context) {
final code = promoController.text.trim();
if (code.isEmpty) return;
context.read<PromoCodeBloc>().add(PromoCodeApplyRequested(code));
}
}