Files
lab-app/lib/features/auth/sign_in_screen.dart
T
2026-06-10 23:22:15 +03:00

889 lines
31 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/l10n/app_strings.dart';
import '../../core/providers/auth_provider.dart';
import '../../core/providers/locale_provider.dart';
import '../../core/router/app_router.dart';
import '../../core/theme/app_theme.dart';
import '../../core/widgets/tooth_logo.dart';
class SignInScreen extends ConsumerStatefulWidget {
const SignInScreen({super.key});
@override
ConsumerState<SignInScreen> createState() => _SignInScreenState();
}
class _SignInScreenState extends ConsumerState<SignInScreen> {
final _formKey = GlobalKey<FormState>();
final _emailCtrl = TextEditingController();
final _passCtrl = TextEditingController();
bool _obscure = true;
@override
void dispose() {
_emailCtrl.dispose();
_passCtrl.dispose();
super.dispose();
}
Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
await ref
.read(authProvider.notifier)
.signIn(_emailCtrl.text.trim(), _passCtrl.text);
}
@override
Widget build(BuildContext context) {
final auth = ref.watch(authProvider);
final s = ref.watch(stringsProvider);
final locale = ref.watch(localeProvider);
final isDesktop = MediaQuery.sizeOf(context).width > 800;
return Scaffold(
backgroundColor: AppColors.background,
body: isDesktop
? _buildDesktop(context, auth, s, locale)
: _buildMobile(context, auth, s, locale),
);
}
// ── Mobile ─────────────────────────────────────────────────────────────────
Widget _buildMobile(
BuildContext context, dynamic auth, AppStrings s, Locale locale) {
return Stack(
children: [
SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 56),
// Logo mark
Center(
child: Container(
width: 68,
height: 68,
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF0B1D35), Color(0xFF1A5C8A)],
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: const Color(0xFF0B1D35).withValues(alpha: 0.3),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child:
const Center(child: ToothLogo(size: 34, color: Colors.white)),
),
).animate().fadeIn(duration: 400.ms).scale(begin: const Offset(0.8, 0.8)),
const SizedBox(height: 24),
Center(
child: Text(
s.signInWelcome,
style: const TextStyle(
fontSize: 26,
fontWeight: FontWeight.w800,
color: AppColors.textPrimary,
letterSpacing: -0.5,
),
),
).animate(delay: 60.ms).fadeIn(duration: 400.ms).slideY(begin: 0.1),
const SizedBox(height: 6),
Center(
child: Text(
s.signInSubtitle,
style: const TextStyle(
fontSize: 14, color: AppColors.textSecondary),
),
).animate(delay: 100.ms).fadeIn(duration: 400.ms),
const SizedBox(height: 36),
_buildFormFields(auth, s),
const SizedBox(height: 24),
_buildSignUpLink(context, s),
const SizedBox(height: 32),
],
),
),
),
Positioned(
top: MediaQuery.paddingOf(context).top + 12,
right: 12,
child: _LanguageButton(locale: locale, s: s, ref: ref),
),
],
);
}
// ── Desktop ────────────────────────────────────────────────────────────────
Widget _buildDesktop(
BuildContext context, dynamic auth, AppStrings s, Locale locale) {
return Row(
children: [
// LEFT PANEL
Expanded(
flex: 55,
child: Stack(
fit: StackFit.expand,
children: [
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
stops: [0.0, 0.55, 1.0],
colors: [
Color(0xFF080F1E),
Color(0xFF0D2D58),
Color(0xFF0E4A82),
],
),
),
),
const Positioned(top: -140, left: -140, child: _Ring(size: 520, opacity: 0.06)),
const Positioned(bottom: -100, right: -100, child: _Ring(size: 400, opacity: 0.05)),
const Positioned(top: 160, right: 60, child: _Ring(size: 100, opacity: 0.09)),
const Positioned(bottom: 220, left: 60, child: _Ring(size: 70, opacity: 0.07)),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 64, vertical: 52),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 38,
height: 38,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: Colors.white.withValues(alpha: 0.2),
),
),
child: const Center(
child: ToothLogo(size: 20, color: Colors.white)),
),
const SizedBox(width: 12),
const Text(
'DLS',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w800,
letterSpacing: 1.5,
),
),
],
).animate().fadeIn(duration: 500.ms),
const Spacer(),
Text(
s.signInHeadline,
style: const TextStyle(
color: Colors.white,
fontSize: 46,
fontWeight: FontWeight.w800,
height: 1.1,
letterSpacing: -1.0,
),
)
.animate(delay: 100.ms)
.fadeIn(duration: 500.ms)
.slideY(begin: 0.1, end: 0),
const SizedBox(height: 18),
Text(
s.signInTagline,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.6),
fontSize: 16,
height: 1.6,
),
).animate(delay: 160.ms).fadeIn(duration: 500.ms),
const SizedBox(height: 44),
const _DashboardPreviewCard()
.animate(delay: 220.ms)
.fadeIn(duration: 600.ms)
.slideY(begin: 0.12, end: 0),
const Spacer(),
Text(
s.footerCopyright,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.3),
fontSize: 12,
),
).animate(delay: 300.ms).fadeIn(duration: 500.ms),
],
),
),
],
),
),
// RIGHT PANEL
Stack(
children: [
Container(
width: 460,
color: Colors.white,
child: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) => SingleChildScrollView(
child: ConstrainedBox(
constraints:
BoxConstraints(minHeight: constraints.maxHeight),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 52, vertical: 40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFF0B1D35),
Color(0xFF1A5C8A)
],
),
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: ToothLogo(
size: 24, color: Colors.white)),
).animate().fadeIn(duration: 400.ms),
const SizedBox(height: 32),
Text(
s.signInWelcome,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.w800,
color: AppColors.textPrimary,
letterSpacing: -0.5,
),
)
.animate(delay: 60.ms)
.fadeIn(duration: 400.ms)
.slideY(begin: 0.08, end: 0),
const SizedBox(height: 6),
Text(
s.signInSubtitle,
style: const TextStyle(
fontSize: 15,
color: AppColors.textSecondary,
),
).animate(delay: 100.ms).fadeIn(duration: 400.ms),
const SizedBox(height: 40),
_buildFormFields(auth, s)
.animate(delay: 140.ms)
.fadeIn(duration: 400.ms)
.slideY(begin: 0.08, end: 0),
const SizedBox(height: 28),
_buildSignUpLink(context, s)
.animate(delay: 200.ms)
.fadeIn(duration: 400.ms),
],
),
),
),
),
),
),
),
),
Positioned(
top: MediaQuery.paddingOf(context).top + 16,
right: 16,
child: _LanguageButton(locale: locale, s: s, ref: ref),
),
],
),
],
);
}
// ── Form fields (shared) ────────────────────────────────────────────────────
Widget _buildFormFields(dynamic auth, AppStrings s) {
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_Field(
controller: _emailCtrl,
label: s.emailAddress,
icon: Icons.email_outlined,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
validator: (v) =>
(v == null || v.trim().isEmpty) ? s.emailRequired : null,
),
const SizedBox(height: 14),
_Field(
controller: _passCtrl,
label: s.password,
icon: Icons.lock_outline_rounded,
obscureText: _obscure,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _submit(),
suffixIcon: IconButton(
icon: Icon(
_obscure
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
size: 20,
color: AppColors.textSecondary,
),
onPressed: () => setState(() => _obscure = !_obscure),
),
validator: (v) =>
(v == null || v.isEmpty) ? s.passwordRequired : null,
),
if (auth.error != null) ...[
const SizedBox(height: 14),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFFFEF2F2),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: AppColors.cancelled.withValues(alpha: 0.25)),
),
child: Row(
children: [
const Icon(Icons.error_outline_rounded,
color: AppColors.cancelled, size: 16),
const SizedBox(width: 8),
Expanded(
child: Text(
auth.error!,
style: const TextStyle(
color: AppColors.cancelled, fontSize: 13),
),
),
],
),
),
],
const SizedBox(height: 24),
DecoratedBox(
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF0B1D35), Color(0xFF1A5C8A)],
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: const Color(0xFF0B1D35).withValues(alpha: 0.35),
blurRadius: 16,
offset: const Offset(0, 6),
),
],
),
child: FilledButton(
onPressed: auth.isLoading ? null : _submit,
style: FilledButton.styleFrom(
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
disabledForegroundColor: Colors.white.withValues(alpha: 0.5),
disabledBackgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
minimumSize: const Size.fromHeight(52),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child: auth.isLoading
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2.5, color: Colors.white),
)
: Text(
s.signIn,
style: const TextStyle(
fontSize: 15, fontWeight: FontWeight.w600),
),
),
),
],
),
);
}
// ── Sign-up link ───────────────────────────────────────────────────────────
Widget _buildSignUpLink(BuildContext context, AppStrings s) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
s.noAccount,
style:
const TextStyle(color: AppColors.textSecondary, fontSize: 14),
),
TextButton(
onPressed: () => context.go(routeSignUp),
style: TextButton.styleFrom(
foregroundColor: const Color(0xFF0D4C85),
padding: const EdgeInsets.symmetric(horizontal: 8),
),
child: Text(
s.signUp,
style:
const TextStyle(fontWeight: FontWeight.w700, fontSize: 14),
),
),
],
);
}
}
// ── Language button ───────────────────────────────────────────────────────────
class _LanguageButton extends StatelessWidget {
const _LanguageButton(
{required this.locale, required this.s, required this.ref});
final Locale locale;
final AppStrings s;
final WidgetRef ref;
static const _flags = {
'tr': '🇹🇷',
'en': '🇬🇧',
'ru': '🇷🇺',
'ar': '🇸🇦',
'de': '🇩🇪',
};
@override
Widget build(BuildContext context) {
final flag = _flags[locale.languageCode] ?? '🌐';
return GestureDetector(
onTap: () => _showPicker(context),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.border),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.06),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(flag, style: const TextStyle(fontSize: 15)),
const SizedBox(width: 4),
const Icon(Icons.expand_more_rounded,
size: 14, color: AppColors.textSecondary),
],
),
),
);
}
void _showPicker(BuildContext context) {
final options = [
('tr', '🇹🇷', s.languageTurkish),
('en', '🇬🇧', s.languageEnglish),
('ru', '🇷🇺', s.languageRussian),
('ar', '🇸🇦', s.languageArabic),
('de', '🇩🇪', s.languageGerman),
];
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (_) => Container(
decoration: const BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: AppColors.border,
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: 16),
Text(
s.languageSelection,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 12),
for (final (code, flag, label) in options)
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 4),
leading: Text(flag, style: const TextStyle(fontSize: 24)),
title: Text(
label,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: AppColors.textPrimary,
),
),
trailing: locale.languageCode == code
? const Icon(Icons.check_circle_rounded,
color: AppColors.accent)
: null,
onTap: () {
ref.read(localeProvider.notifier).setLocale(Locale(code));
Navigator.pop(context);
},
),
SizedBox(height: MediaQuery.paddingOf(context).bottom + 4),
],
),
),
);
}
}
// ── Decorative ring ───────────────────────────────────────────────────────────
class _Ring extends StatelessWidget {
const _Ring({required this.size, required this.opacity});
final double size;
final double opacity;
@override
Widget build(BuildContext context) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: Colors.white.withValues(alpha: opacity),
width: 1.5,
),
),
);
}
}
// ── Dashboard preview card (glassmorphism) ────────────────────────────────────
class _DashboardPreviewCard extends StatelessWidget {
const _DashboardPreviewCard();
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(20),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16),
child: Container(
width: 340,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withValues(alpha: 0.12),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.bar_chart_rounded,
color: Colors.white,
size: 15,
),
),
const SizedBox(width: 10),
Text(
'Bugünkü Durum',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.9),
fontWeight: FontWeight.w600,
fontSize: 13,
),
),
const Spacer(),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
'Canlı',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.7),
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
),
],
),
const SizedBox(height: 18),
const Row(
children: [
_StatChip(value: '24', label: 'Aktif', color: Color(0xFF60A5FA)),
SizedBox(width: 8),
_StatChip(
value: '8', label: 'Bekliyor', color: Color(0xFFFBBF24)),
SizedBox(width: 8),
_StatChip(
value: '142', label: 'Bu ay', color: Color(0xFF34D399)),
],
),
const SizedBox(height: 18),
const _PreviewBar(
label: 'Zirkon', value: 0.76, color: Color(0xFF60A5FA)),
const SizedBox(height: 10),
const _PreviewBar(
label: 'Metal alt.', value: 0.48, color: Color(0xFFFBBF24)),
const SizedBox(height: 10),
const _PreviewBar(
label: 'Porselen', value: 0.62, color: Color(0xFF34D399)),
],
),
),
),
);
}
}
class _StatChip extends StatelessWidget {
const _StatChip({
required this.value,
required this.label,
required this.color,
});
final String value;
final String label;
final Color color;
@override
Widget build(BuildContext context) {
return Expanded(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: color.withValues(alpha: 0.2)),
),
child: Column(
children: [
Text(
value,
style: TextStyle(
color: color,
fontSize: 18,
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 2),
Text(
label,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.55),
fontSize: 11,
),
),
],
),
),
);
}
}
class _PreviewBar extends StatelessWidget {
const _PreviewBar({
required this.label,
required this.value,
required this.color,
});
final String label;
final double value;
final Color color;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.65),
fontSize: 12,
),
),
Text(
'${(value * 100).toInt()}%',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.65),
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 5),
LayoutBuilder(
builder: (_, constraints) => Stack(
children: [
Container(
height: 5,
width: constraints.maxWidth,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(10),
),
),
Container(
height: 5,
width: constraints.maxWidth * value,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(10),
),
),
],
),
),
],
);
}
}
// ── Form field ────────────────────────────────────────────────────────────────
class _Field extends StatelessWidget {
const _Field({
required this.controller,
required this.label,
required this.icon,
this.keyboardType,
this.textInputAction,
this.obscureText = false,
this.suffixIcon,
this.onFieldSubmitted,
this.validator,
});
final TextEditingController controller;
final String label;
final IconData icon;
final TextInputType? keyboardType;
final TextInputAction? textInputAction;
final bool obscureText;
final Widget? suffixIcon;
final ValueChanged<String>? onFieldSubmitted;
final FormFieldValidator<String>? validator;
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
keyboardType: keyboardType,
textInputAction: textInputAction,
obscureText: obscureText,
onFieldSubmitted: onFieldSubmitted,
validator: validator,
style: const TextStyle(fontSize: 15, color: AppColors.textPrimary),
decoration: InputDecoration(
labelText: label,
prefixIcon: Icon(icon, size: 20, color: AppColors.textSecondary),
suffixIcon: suffixIcon,
filled: true,
fillColor: const Color(0xFFF8FAFC),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.border),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.border),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFF0D4C85), width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide:
const BorderSide(color: AppColors.cancelled, width: 1.5),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.cancelled, width: 2),
),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
labelStyle: const TextStyle(
color: AppColors.textSecondary, fontSize: 14),
),
);
}
}