Initial commit: DLS - Dental Lab System
- 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
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
|
||||
/// Animated floating blob background used on auth screens.
|
||||
/// [bright] = true → white blobs (for dark/gradient backgrounds).
|
||||
/// [bright] = false → primary/accent blobs (for light backgrounds).
|
||||
class AnimatedAuthBg extends StatefulWidget {
|
||||
const AnimatedAuthBg({super.key, this.bright = false});
|
||||
final bool bright;
|
||||
|
||||
@override
|
||||
State<AnimatedAuthBg> createState() => _AnimatedAuthBgState();
|
||||
}
|
||||
|
||||
class _AnimatedAuthBgState extends State<AnimatedAuthBg>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _ctrl;
|
||||
late Animation<double> _anim;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_ctrl = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(seconds: 8),
|
||||
)..repeat(reverse: true);
|
||||
_anim = CurvedAnimation(parent: _ctrl, curve: Curves.easeInOut);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ctrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Color _blob(double alpha) => widget.bright
|
||||
? Colors.white.withValues(alpha: alpha * 1.5)
|
||||
: AppColors.primary.withValues(alpha: alpha);
|
||||
|
||||
Color _blobAccent(double alpha) => widget.bright
|
||||
? Colors.white.withValues(alpha: alpha * 1.2)
|
||||
: AppColors.accent.withValues(alpha: alpha);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _anim,
|
||||
builder: (_, __) {
|
||||
final t = _anim.value;
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: -80 + t * 30,
|
||||
left: -60 + t * 20,
|
||||
child: AuthBlob(size: 300, color: _blob(0.08)),
|
||||
),
|
||||
Positioned(
|
||||
top: 200 - t * 40,
|
||||
right: -100 + t * 25,
|
||||
child: AuthBlob(size: 250, color: _blobAccent(0.06)),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 100 + t * 30,
|
||||
left: 50 - t * 15,
|
||||
child: AuthBlob(size: 200, color: _blob(0.05)),
|
||||
),
|
||||
Positioned(
|
||||
bottom: -50 + t * 20,
|
||||
right: -50 + t * 10,
|
||||
child: AuthBlob(size: 280, color: _blobAccent(0.07)),
|
||||
),
|
||||
Positioned(
|
||||
top: 350 + t * 25,
|
||||
left: 80 + t * 20,
|
||||
child: AuthBlob(size: 160, color: _blob(0.04)),
|
||||
),
|
||||
Positioned(
|
||||
top: -40 - t * 10,
|
||||
left: 120 + t * 30,
|
||||
child: AuthBlob(size: 180, color: _blobAccent(0.05)),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple solid circle used as a background blob.
|
||||
class AuthBlob extends StatelessWidget {
|
||||
const AuthBlob({super.key, required this.size, required this.color});
|
||||
|
||||
final double size;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
import '../../core/api/pocketbase_client.dart';
|
||||
import '../../core/auth/auth_repository.dart';
|
||||
|
||||
class OnboardingRepository {
|
||||
OnboardingRepository._();
|
||||
static final instance = OnboardingRepository._();
|
||||
|
||||
PocketBase get _pb => PocketBaseClient.instance.pb;
|
||||
|
||||
Future<AuthResult> createTenantAndJoin({
|
||||
required String kind,
|
||||
required String companyName,
|
||||
}) async {
|
||||
final userId = _pb.authStore.record!.id;
|
||||
|
||||
final tenant = await _pb.collection('tenants').create(body: {
|
||||
'kind': kind,
|
||||
'company_name': companyName,
|
||||
'status': 'active',
|
||||
'default_currency': 'TRY',
|
||||
});
|
||||
|
||||
await _pb.collection('tenant_members').create(body: {
|
||||
'tenant_id': tenant.id,
|
||||
'user_id': userId,
|
||||
'role': 'owner',
|
||||
});
|
||||
|
||||
return AuthRepository.instance.refreshSession();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,461 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/providers/auth_provider.dart';
|
||||
import 'onboarding_repository.dart';
|
||||
|
||||
class OnboardingScreen extends ConsumerStatefulWidget {
|
||||
const OnboardingScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<OnboardingScreen> createState() => _OnboardingScreenState();
|
||||
}
|
||||
|
||||
class _OnboardingScreenState extends ConsumerState<OnboardingScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _nameCtrl = TextEditingController();
|
||||
String _selectedKind = 'clinic';
|
||||
bool _loading = false;
|
||||
String? _error;
|
||||
late AnimationController _animCtrl;
|
||||
late Animation<double> _fadeAnim;
|
||||
late Animation<Offset> _slideAnim;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animCtrl = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 600),
|
||||
);
|
||||
_fadeAnim = CurvedAnimation(parent: _animCtrl, curve: Curves.easeOut);
|
||||
_slideAnim = Tween<Offset>(
|
||||
begin: const Offset(0, 0.08),
|
||||
end: Offset.zero,
|
||||
).animate(CurvedAnimation(parent: _animCtrl, curve: Curves.easeOutCubic));
|
||||
_animCtrl.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animCtrl.dispose();
|
||||
_nameCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _create() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
final result = await OnboardingRepository.instance.createTenantAndJoin(
|
||||
kind: _selectedKind,
|
||||
companyName: _nameCtrl.text.trim(),
|
||||
);
|
||||
if (!mounted) return;
|
||||
ref.read(authProvider.notifier).setActiveTenant(result.tenants.first);
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error = 'Hesap oluşturulamadı. Lütfen tekrar deneyin.';
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFF4F46E5),
|
||||
body: Stack(
|
||||
children: [
|
||||
// ── Gradient background ──────────────────────────────────────────
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFF3730A3), Color(0xFF6366F1)],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ── Decorative circles ───────────────────────────────────────────
|
||||
Positioned(
|
||||
top: -40,
|
||||
right: -60,
|
||||
child: Container(
|
||||
width: 220,
|
||||
height: 220,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white.withValues(alpha: 0.06),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 80,
|
||||
left: -70,
|
||||
child: Container(
|
||||
width: 200,
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white.withValues(alpha: 0.04),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ── Content ──────────────────────────────────────────────────────
|
||||
Column(
|
||||
children: [
|
||||
// Header
|
||||
SafeArea(
|
||||
bottom: false,
|
||||
child: SizedBox(
|
||||
height: size.height * 0.26,
|
||||
child: Stack(
|
||||
children: [
|
||||
// Sign out
|
||||
Positioned(
|
||||
right: 8,
|
||||
top: 4,
|
||||
child: TextButton.icon(
|
||||
onPressed: () =>
|
||||
ref.read(authProvider.notifier).signOut(),
|
||||
icon: const Icon(Icons.logout_rounded,
|
||||
color: Colors.white70, size: 18),
|
||||
label: const Text(
|
||||
'Çıkış',
|
||||
style: TextStyle(color: Colors.white70),
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 68,
|
||||
height: 68,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.3),
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.domain_add_rounded,
|
||||
size: 32,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
const Text(
|
||||
'Kurumunuzu Oluşturun',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Klinik veya laboratuvar olarak kayıt olun',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.70),
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Form card
|
||||
Expanded(
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnim,
|
||||
child: SlideTransition(
|
||||
position: _slideAnim,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surface,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(32),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.15),
|
||||
blurRadius: 24,
|
||||
offset: const Offset(0, -4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(28, 32, 28, 24),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'Kurum Türünü Seçin',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// Kind cards
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _KindCard(
|
||||
icon: Icons.local_hospital_outlined,
|
||||
label: 'Klinik',
|
||||
description: 'Diş kliniği',
|
||||
value: 'clinic',
|
||||
selected: _selectedKind == 'clinic',
|
||||
onTap: () => setState(
|
||||
() => _selectedKind = 'clinic'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _KindCard(
|
||||
icon: Icons.science_outlined,
|
||||
label: 'Laboratuvar',
|
||||
description: 'Diş laboratuvarı',
|
||||
value: 'lab',
|
||||
selected: _selectedKind == 'lab',
|
||||
onTap: () =>
|
||||
setState(() => _selectedKind = 'lab'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Company name
|
||||
Text(
|
||||
'Kurum Adı',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextFormField(
|
||||
controller: _nameCtrl,
|
||||
textInputAction: TextInputAction.done,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
onFieldSubmitted: (_) => _create(),
|
||||
decoration: InputDecoration(
|
||||
labelText: _selectedKind == 'clinic'
|
||||
? 'Klinik Adı'
|
||||
: 'Laboratuvar Adı',
|
||||
prefixIcon: const Icon(
|
||||
Icons.business_outlined,
|
||||
size: 20),
|
||||
filled: true,
|
||||
fillColor: cs.surfaceContainerHighest,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFF4F46E5), width: 2),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderSide: BorderSide(
|
||||
color: cs.error, width: 1.5),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderSide:
|
||||
BorderSide(color: cs.error, width: 2),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 16),
|
||||
),
|
||||
validator: (v) {
|
||||
if (v == null || v.trim().isEmpty) {
|
||||
return 'Kurum adı gereklidir';
|
||||
}
|
||||
if (v.trim().length < 3) {
|
||||
return 'En az 3 karakter olmalıdır';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
|
||||
// Error banner
|
||||
if (_error != null) ...[
|
||||
const SizedBox(height: 14),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.errorContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.error_outline_rounded,
|
||||
color: cs.onErrorContainer, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_error!,
|
||||
style: TextStyle(
|
||||
color: cs.onErrorContainer,
|
||||
fontSize: 13),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 28),
|
||||
|
||||
FilledButton(
|
||||
onPressed: _loading ? null : _create,
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(52),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
backgroundColor: const Color(0xFF4F46E5),
|
||||
),
|
||||
child: _loading
|
||||
? const SizedBox(
|
||||
height: 22,
|
||||
width: 22,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.5,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'Devam Et',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _KindCard extends StatelessWidget {
|
||||
const _KindCard({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.description,
|
||||
required this.value,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String description;
|
||||
final String value;
|
||||
final bool selected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: selected ? const Color(0xFF4F46E5) : cs.outlineVariant,
|
||||
width: selected ? 2 : 1,
|
||||
),
|
||||
color: selected
|
||||
? const Color(0xFF4F46E5).withValues(alpha: 0.08)
|
||||
: cs.surfaceContainerLow,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: selected
|
||||
? const Color(0xFF4F46E5).withValues(alpha: 0.12)
|
||||
: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 26,
|
||||
color: selected
|
||||
? const Color(0xFF4F46E5)
|
||||
: cs.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
color: selected ? const Color(0xFF4F46E5) : cs.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
description,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: cs.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,888 @@
|
||||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,619 @@
|
||||
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/providers/auth_provider.dart';
|
||||
import '../../core/router/app_router.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../core/widgets/tooth_logo.dart';
|
||||
import 'auth_widgets.dart';
|
||||
|
||||
class SignUpScreen extends ConsumerStatefulWidget {
|
||||
const SignUpScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<SignUpScreen> createState() => _SignUpScreenState();
|
||||
}
|
||||
|
||||
class _SignUpScreenState extends ConsumerState<SignUpScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _firstNameCtrl = TextEditingController();
|
||||
final _lastNameCtrl = TextEditingController();
|
||||
final _emailCtrl = TextEditingController();
|
||||
final _passCtrl = TextEditingController();
|
||||
final _confirmPassCtrl = TextEditingController();
|
||||
bool _obscure = true;
|
||||
bool _obscureConfirm = true;
|
||||
bool _loading = false;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_firstNameCtrl.dispose();
|
||||
_lastNameCtrl.dispose();
|
||||
_emailCtrl.dispose();
|
||||
_passCtrl.dispose();
|
||||
_confirmPassCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
await ref.read(authProvider.notifier).register(
|
||||
email: _emailCtrl.text.trim(),
|
||||
password: _passCtrl.text,
|
||||
firstName: _firstNameCtrl.text.trim(),
|
||||
lastName: _lastNameCtrl.text.trim(),
|
||||
);
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error = _parseError(e.toString());
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
String _parseError(String msg) {
|
||||
if (msg.contains('already') || msg.contains('unique') || msg.contains('UNIQUE')) {
|
||||
return 'Bu e-posta adresi zaten kayıtlı.';
|
||||
}
|
||||
if (msg.contains('403') || msg.contains('Forbidden')) {
|
||||
return 'Kayıt şu anda kapalı. Lütfen yönetici ile iletişime geçin.';
|
||||
}
|
||||
return 'Kayıt olunamadı. Lütfen tekrar deneyin.';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDesktop = MediaQuery.sizeOf(context).width > 800;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
body: isDesktop ? _buildDesktop(context) : _buildMobile(context),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Mobile layout ──────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildMobile(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: _buildForm(context, isMobile: true),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Desktop layout ─────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildDesktop(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
// LEFT PANEL — solid gradient + white animated blobs on top
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [AppColors.primary, Color(0xFF1A5C8A)],
|
||||
),
|
||||
),
|
||||
),
|
||||
const AnimatedAuthBg(bright: true),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 56),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 72,
|
||||
height: 72,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.3),
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: const Center(child: ToothLogo(size: 38, color: Colors.white)),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
'DLS',
|
||||
style: TextStyle(
|
||||
fontSize: 48,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white,
|
||||
letterSpacing: 2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Dental Lab Sistemi',
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
color: Colors.white.withValues(alpha: 0.7),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
const _FeatureBullet(
|
||||
icon: Icons.dashboard_rounded,
|
||||
text: 'İş takibi tek ekranda',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const _FeatureBullet(
|
||||
icon: Icons.link_rounded,
|
||||
text: 'Klinik-lab bağlantısı',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const _FeatureBullet(
|
||||
icon: Icons.bolt_rounded,
|
||||
text: 'Gerçek zamanlı durum',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// RIGHT PANEL — light gray so white card stands out
|
||||
Container(
|
||||
width: 480,
|
||||
color: AppColors.background,
|
||||
child: SafeArea(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) => SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||
child: IntrinsicHeight(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 32),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [_buildForm(context, isMobile: false)],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ── Shared form content ────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildForm(BuildContext context, {required bool isMobile}) {
|
||||
return Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (isMobile) const SizedBox(height: 48),
|
||||
|
||||
// ── Back button + branding (mobile only) ───────────────────────
|
||||
if (isMobile) ...[
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () => context.go(routeSignIn),
|
||||
icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20),
|
||||
style: IconButton.styleFrom(
|
||||
foregroundColor: AppColors.textPrimary,
|
||||
backgroundColor: AppColors.surface,
|
||||
padding: const EdgeInsets.all(10),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: const BorderSide(color: AppColors.border),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
).animate().fadeIn(duration: 300.ms),
|
||||
|
||||
const SizedBox(height: 28),
|
||||
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 72,
|
||||
height: 72,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [AppColors.primary, AppColors.accent],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.accent.withValues(alpha: 0.3),
|
||||
blurRadius: 18,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.person_add_alt_1_rounded,
|
||||
size: 32,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Hesap Oluştur',
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'DLS ağına katılın',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
).animate().fadeIn(duration: 400.ms).slideY(begin: -0.08, end: 0),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
|
||||
// Desktop back button (outside card)
|
||||
if (!isMobile) ...[
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () => context.go(routeSignIn),
|
||||
icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 18),
|
||||
style: IconButton.styleFrom(
|
||||
foregroundColor: AppColors.textPrimary,
|
||||
backgroundColor: AppColors.surface,
|
||||
padding: const EdgeInsets.all(8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
side: const BorderSide(color: AppColors.border),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
).animate().fadeIn(duration: 300.ms),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
|
||||
// ── Form card ──────────────────────────────────────────────────
|
||||
Container(
|
||||
padding: EdgeInsets.all(isMobile ? 24 : 32),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: AppColors.border),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: isMobile ? 0.05 : 0.09),
|
||||
blurRadius: isMobile ? 16 : 28,
|
||||
spreadRadius: 0,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Heading inside card on desktop
|
||||
if (!isMobile) ...[
|
||||
const Text(
|
||||
'Hesap Oluştur',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
).animate().fadeIn(duration: 400.ms).slideY(begin: -0.08, end: 0),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'DLS ağına katılın',
|
||||
style: TextStyle(fontSize: 14, color: AppColors.textSecondary),
|
||||
).animate(delay: 40.ms).fadeIn(duration: 400.ms),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
// Ad / Soyad satırı
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _Field(
|
||||
controller: _firstNameCtrl,
|
||||
label: 'Ad',
|
||||
icon: Icons.badge_outlined,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (v) =>
|
||||
(v == null || v.trim().isEmpty) ? 'Gerekli' : null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _Field(
|
||||
controller: _lastNameCtrl,
|
||||
label: 'Soyad',
|
||||
icon: Icons.badge_outlined,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (v) =>
|
||||
(v == null || v.trim().isEmpty) ? 'Gerekli' : null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
_Field(
|
||||
controller: _emailCtrl,
|
||||
label: 'E-posta',
|
||||
icon: Icons.email_outlined,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (v) {
|
||||
if (v == null || v.trim().isEmpty) return 'E-posta gereklidir';
|
||||
if (!v.contains('@')) return 'Geçerli bir e-posta girin';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
_Field(
|
||||
controller: _passCtrl,
|
||||
label: 'Şifre',
|
||||
icon: Icons.lock_outline_rounded,
|
||||
obscureText: _obscure,
|
||||
textInputAction: TextInputAction.next,
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscure ? Icons.visibility_outlined : Icons.visibility_off_outlined,
|
||||
size: 20,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
onPressed: () => setState(() => _obscure = !_obscure),
|
||||
),
|
||||
validator: (v) {
|
||||
if (v == null || v.isEmpty) return 'Şifre gereklidir';
|
||||
if (v.length < 8) return 'En az 8 karakter olmalıdır';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
_Field(
|
||||
controller: _confirmPassCtrl,
|
||||
label: 'Şifre Tekrar',
|
||||
icon: Icons.lock_outline_rounded,
|
||||
obscureText: _obscureConfirm,
|
||||
textInputAction: TextInputAction.done,
|
||||
onFieldSubmitted: (_) => _submit(),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscureConfirm
|
||||
? Icons.visibility_outlined
|
||||
: Icons.visibility_off_outlined,
|
||||
size: 20,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
onPressed: () => setState(() => _obscureConfirm = !_obscureConfirm),
|
||||
),
|
||||
validator: (v) =>
|
||||
(v != _passCtrl.text) ? 'Şifreler eşleşmiyor' : null,
|
||||
),
|
||||
|
||||
if (_error != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cancelledBg,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: AppColors.cancelled.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.error_outline_rounded,
|
||||
color: AppColors.cancelled, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_error!,
|
||||
style: const TextStyle(
|
||||
color: AppColors.cancelled, fontSize: 13),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
FilledButton(
|
||||
onPressed: _loading ? null : _submit,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
minimumSize: const Size.fromHeight(52),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: _loading
|
||||
? const SizedBox(
|
||||
width: 22,
|
||||
height: 22,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.5, color: Colors.white),
|
||||
)
|
||||
: const Text(
|
||||
'Kayıt Ol',
|
||||
style:
|
||||
TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
).animate(delay: 100.ms).fadeIn(duration: 400.ms).slideY(begin: 0.1, end: 0),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
'Zaten hesabın var mı?',
|
||||
style: TextStyle(color: AppColors.textSecondary, fontSize: 14),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => context.go(routeSignIn),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: AppColors.accent,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
child: const Text(
|
||||
'Giriş Yap',
|
||||
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
).animate(delay: 200.ms).fadeIn(duration: 400.ms),
|
||||
|
||||
SizedBox(height: isMobile ? 32 : 16),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Feature bullet (desktop left panel) ──────────────────────────────────────
|
||||
|
||||
class _FeatureBullet extends StatelessWidget {
|
||||
const _FeatureBullet({required this.icon, required this.text});
|
||||
final IconData icon;
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(icon, size: 18, color: Colors.white),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Form field ────────────────────────────────────────────────────────────────
|
||||
|
||||
class _Field extends StatelessWidget {
|
||||
const _Field({
|
||||
required this.controller,
|
||||
required this.label,
|
||||
required this.icon,
|
||||
this.keyboardType,
|
||||
this.textCapitalization = TextCapitalization.none,
|
||||
this.textInputAction,
|
||||
this.obscureText = false,
|
||||
this.suffixIcon,
|
||||
this.onFieldSubmitted,
|
||||
this.validator,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final TextInputType? keyboardType;
|
||||
final TextCapitalization textCapitalization;
|
||||
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,
|
||||
textCapitalization: textCapitalization,
|
||||
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: AppColors.background,
|
||||
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: AppColors.accent, 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user