8bbc9dbff2
- Flutter + PocketBase dental lab management system - Clinic & lab dashboards, job tracking, patient management - Product catalog, finance tracking, multi-language support - AI assistant integration, realtime notifications - Windows installer (Inno Setup) included - Developed by kovakyazilim.com
889 lines
31 KiB
Dart
889 lines
31 KiB
Dart
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),
|
||
),
|
||
);
|
||
}
|
||
}
|