777 lines
38 KiB
Dart
777 lines
38 KiB
Dart
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
|
import 'package:tugas_akhir_supabase/widgets/legal_content_dialog.dart';
|
|
|
|
// Define theme colors
|
|
const Color primaryColor = Color(0xFF056839);
|
|
const Color secondaryColor = Color(0xFF4CAF50);
|
|
const Color backgroundColor = Color(0xFFF5F5F5);
|
|
const Color surfaceColor = Colors.white;
|
|
const Color textColor = Color(0xFF2D3748);
|
|
const Color subtextColor = Color(0xFF718096);
|
|
|
|
class RegisterScreen extends StatefulWidget {
|
|
const RegisterScreen({super.key});
|
|
|
|
@override
|
|
_RegisterScreenState createState() => _RegisterScreenState();
|
|
}
|
|
|
|
class _RegisterScreenState extends State<RegisterScreen> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
final _emailController = TextEditingController();
|
|
final _passwordController = TextEditingController();
|
|
final _confirmPasswordController = TextEditingController();
|
|
final _usernameController = TextEditingController();
|
|
bool _isLoading = false;
|
|
bool _obscurePassword = true;
|
|
bool _obscureConfirmPassword = true;
|
|
bool _agreedToTerms = false;
|
|
|
|
Future<void> _register() async {
|
|
if (!_formKey.currentState!.validate()) return;
|
|
if (!_agreedToTerms) {
|
|
_showErrorSnackbar('Harap setujui Syarat & Ketentuan untuk melanjutkan');
|
|
return;
|
|
}
|
|
|
|
setState(() => _isLoading = true);
|
|
|
|
try {
|
|
final email = _emailController.text.trim();
|
|
final password = _passwordController.text.trim();
|
|
final username = _usernameController.text.trim();
|
|
|
|
final AuthResponse res = await Supabase.instance.client.auth.signUp(
|
|
email: email,
|
|
password: password,
|
|
);
|
|
|
|
final user = res.user;
|
|
if (user == null)
|
|
throw Exception(
|
|
'Otentikasi pengguna berhasil dibuat tetapi data tidak tersimpan',
|
|
);
|
|
|
|
if (res.session != null) {
|
|
await Supabase.instance.client.auth.setSession(
|
|
res.session!.accessToken,
|
|
);
|
|
}
|
|
|
|
try {
|
|
await _createProfileDirect(user.id, email, username);
|
|
} catch (e) {
|
|
debugPrint('Pembuatan profil langsung gagal: $e');
|
|
await _createProfileViaRpc(user.id, email, username);
|
|
}
|
|
|
|
if (!mounted) return;
|
|
Navigator.pushReplacementNamed(
|
|
context,
|
|
'/otp',
|
|
arguments: {'email': email, 'userId': user.id},
|
|
);
|
|
} catch (e) {
|
|
debugPrint('Kesalahan registrasi: $e');
|
|
String errorMessage = e.toString();
|
|
if (e is AuthException && e.message.isNotEmpty) {
|
|
errorMessage = e.message;
|
|
} else if (e is PostgrestException && e.message.isNotEmpty) {
|
|
errorMessage = e.message;
|
|
}
|
|
debugPrint('Error detail: $errorMessage');
|
|
_showErrorSnackbar(errorMessage);
|
|
} finally {
|
|
if (mounted) setState(() => _isLoading = false);
|
|
}
|
|
}
|
|
|
|
Future<void> _createProfileDirect(
|
|
String userId,
|
|
String email,
|
|
String username,
|
|
) async {
|
|
await Supabase.instance.client.from('profiles').insert({
|
|
'user_id': userId,
|
|
'username': username,
|
|
'email': email,
|
|
'created_at': DateTime.now().toUtc().toIso8601String(),
|
|
});
|
|
}
|
|
|
|
Future<void> _createProfileViaRpc(
|
|
String userId,
|
|
String email,
|
|
String username,
|
|
) async {
|
|
await Supabase.instance.client.rpc(
|
|
'create_profile',
|
|
params: {'p_user_id': userId, 'p_email': email, 'p_username': username},
|
|
);
|
|
}
|
|
|
|
void _showErrorSnackbar(String message) {
|
|
if (!mounted) return;
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(message),
|
|
backgroundColor: Colors.redAccent,
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
duration: const Duration(seconds: 3),
|
|
action: SnackBarAction(
|
|
label: 'Tutup',
|
|
textColor: Colors.white,
|
|
onPressed: () {
|
|
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_emailController.dispose();
|
|
_passwordController.dispose();
|
|
_confirmPasswordController.dispose();
|
|
_usernameController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: backgroundColor,
|
|
body: SafeArea(
|
|
child: SingleChildScrollView(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(24.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
const SizedBox(height: 20),
|
|
|
|
// Logo and welcome text
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
height: 100,
|
|
width: 100,
|
|
decoration: BoxDecoration(
|
|
color: primaryColor.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(25),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: primaryColor.withOpacity(0.1),
|
|
blurRadius: 15,
|
|
offset: const Offset(0, 5),
|
|
),
|
|
],
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Image.asset(
|
|
'assets/images/farm_logo.png',
|
|
errorBuilder:
|
|
(context, error, stackTrace) => Icon(
|
|
Icons.eco_rounded,
|
|
color: primaryColor,
|
|
size: 50,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 32),
|
|
Text(
|
|
'Bergabung dengan TaniSM4RT',
|
|
textAlign: TextAlign.center,
|
|
style: Theme.of(
|
|
context,
|
|
).textTheme.headlineSmall?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
color: primaryColor,
|
|
fontSize: 24,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
'Solusi pertanian cerdas untuk petani modern',
|
|
textAlign: TextAlign.center,
|
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
|
color: subtextColor,
|
|
fontSize: 16,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 48),
|
|
|
|
// Registration form
|
|
Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
_buildTextField(
|
|
controller: _usernameController,
|
|
label: 'Nama Pengguna',
|
|
hintText: 'Masukkan nama pengguna Anda',
|
|
prefixIcon: Icons.person_outline,
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'Nama pengguna wajib diisi';
|
|
}
|
|
if (value.length < 3) {
|
|
return 'Minimal 3 karakter';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
_buildTextField(
|
|
controller: _emailController,
|
|
label: 'Email',
|
|
hintText: 'Masukkan alamat email Anda',
|
|
prefixIcon: Icons.email_outlined,
|
|
keyboardType: TextInputType.emailAddress,
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'Email wajib diisi';
|
|
}
|
|
final bool emailValid = RegExp(
|
|
r'^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+',
|
|
).hasMatch(value);
|
|
if (!emailValid) {
|
|
return 'Masukkan alamat email yang valid';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
_buildTextField(
|
|
controller: _passwordController,
|
|
label: 'Kata Sandi',
|
|
hintText: 'Buat kata sandi yang kuat',
|
|
prefixIcon: Icons.lock_outline,
|
|
obscureText: _obscurePassword,
|
|
suffixIcon: IconButton(
|
|
icon: Icon(
|
|
_obscurePassword
|
|
? Icons.visibility_off
|
|
: Icons.visibility,
|
|
color: subtextColor,
|
|
),
|
|
onPressed: () {
|
|
setState(() {
|
|
_obscurePassword = !_obscurePassword;
|
|
});
|
|
},
|
|
),
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'Kata sandi wajib diisi';
|
|
}
|
|
if (value.length < 8) {
|
|
return 'Minimal 8 karakter';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
_buildTextField(
|
|
controller: _confirmPasswordController,
|
|
label: 'Konfirmasi Kata Sandi',
|
|
hintText: 'Masukkan ulang kata sandi Anda',
|
|
prefixIcon: Icons.lock_outline,
|
|
obscureText: _obscureConfirmPassword,
|
|
suffixIcon: IconButton(
|
|
icon: Icon(
|
|
_obscureConfirmPassword
|
|
? Icons.visibility_off
|
|
: Icons.visibility,
|
|
color: subtextColor,
|
|
),
|
|
onPressed: () {
|
|
setState(() {
|
|
_obscureConfirmPassword =
|
|
!_obscureConfirmPassword;
|
|
});
|
|
},
|
|
),
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'Konfirmasi kata sandi wajib diisi';
|
|
}
|
|
if (value != _passwordController.text) {
|
|
return 'Kata sandi tidak cocok';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// Terms and conditions
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Transform.scale(
|
|
scale: 0.9,
|
|
child: Checkbox(
|
|
value: _agreedToTerms,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_agreedToTerms = value ?? false;
|
|
});
|
|
},
|
|
activeColor: primaryColor,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(top: 3),
|
|
child: RichText(
|
|
text: TextSpan(
|
|
style: TextStyle(
|
|
color: textColor,
|
|
fontSize: 12,
|
|
),
|
|
children: [
|
|
const TextSpan(text: 'Saya setuju dengan '),
|
|
TextSpan(
|
|
text: 'Syarat & Ketentuan',
|
|
style: TextStyle(
|
|
color: primaryColor,
|
|
fontWeight: FontWeight.bold,
|
|
decoration: TextDecoration.underline,
|
|
),
|
|
recognizer:
|
|
TapGestureRecognizer()
|
|
..onTap = () {
|
|
showDialog(
|
|
context: context,
|
|
builder:
|
|
(
|
|
context,
|
|
) => const LegalContentDialog(
|
|
title:
|
|
'Syarat & Ketentuan',
|
|
contentWidgets: [
|
|
ParagraphText(
|
|
'Selamat datang di TaniSM4RT (Platform Pertanian Cerdas)\n\n'
|
|
'Dengan menggunakan aplikasi kami, Anda menyetujui ketentuan-ketentuan ini yang mengatur akses Anda ke layanan pertanian cerdas kami.',
|
|
),
|
|
SectionTitle(
|
|
'1. Gambaran Layanan',
|
|
),
|
|
ParagraphText(
|
|
'TaniSM4RT menyediakan wawasan pertanian bertenaga AI, pemantauan tanaman, prakiraan cuaca, dan alat manajemen lahan untuk mengoptimalkan operasi pertanian Anda.',
|
|
),
|
|
SectionTitle(
|
|
'2. Tanggung Jawab Pengguna',
|
|
),
|
|
ListItem(
|
|
'Memberikan data lahan dan tanaman yang akurat untuk rekomendasi optimal',
|
|
),
|
|
ListItem(
|
|
'Menggunakan layanan hanya untuk tujuan pertanian yang sah',
|
|
),
|
|
ListItem(
|
|
'Menjaga keamanan dan kerahasiaan akun',
|
|
),
|
|
ListItem(
|
|
'Mematuhi peraturan dan hukum pertanian setempat',
|
|
),
|
|
SectionTitle(
|
|
'3. Data & Privasi',
|
|
),
|
|
ParagraphText(
|
|
'Data pertanian Anda membantu meningkatkan model AI kami. Kami melindungi informasi Anda sesuai dengan Kebijakan Privasi kami dan tidak pernah membagikan data lahan sensitif tanpa persetujuan.',
|
|
),
|
|
SectionTitle(
|
|
'4. Ketersediaan Layanan',
|
|
),
|
|
ParagraphText(
|
|
'Kami berusaha mencapai waktu aktif 99,9% tetapi tidak dapat menjamin layanan tanpa gangguan. Data cuaca dan rekomendasi disediakan sebagai panduan - keputusan pertanian akhir tetap menjadi tanggung jawab Anda.',
|
|
),
|
|
SectionTitle(
|
|
'5. Kekayaan Intelektual',
|
|
),
|
|
ParagraphText(
|
|
'Platform TaniSM4RT, algoritma, dan konten dilindungi oleh hukum kekayaan intelektual. Anda tetap memiliki kepemilikan atas data lahan Anda.',
|
|
),
|
|
SectionTitle(
|
|
'6. Pembatasan Tanggung Jawab',
|
|
),
|
|
ParagraphText(
|
|
'Rekomendasi kami bersifat konsultatif. Kami tidak bertanggung jawab atas kerugian tanaman, kerusakan terkait cuaca, atau keputusan pertanian berdasarkan wawasan kami.',
|
|
),
|
|
SectionTitle(
|
|
'7. Pembaruan & Perubahan',
|
|
),
|
|
ParagraphText(
|
|
'Kami dapat memperbarui ketentuan ini secara berkala. Penggunaan berkelanjutan merupakan penerimaan terhadap ketentuan yang direvisi.',
|
|
),
|
|
ParagraphText(
|
|
'Hubungi kami: support@tanismart.com\nTanggal Berlaku: Januari 2025',
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
const TextSpan(text: ' dan '),
|
|
TextSpan(
|
|
text: 'Kebijakan Privasi',
|
|
style: TextStyle(
|
|
color: primaryColor,
|
|
fontWeight: FontWeight.bold,
|
|
decoration: TextDecoration.underline,
|
|
),
|
|
recognizer:
|
|
TapGestureRecognizer()
|
|
..onTap = () {
|
|
showDialog(
|
|
context: context,
|
|
builder:
|
|
(
|
|
context,
|
|
) => const LegalContentDialog(
|
|
title:
|
|
'Kebijakan Privasi',
|
|
contentWidgets: [
|
|
ParagraphText(
|
|
'Kebijakan Privasi TaniSM4RT\n\n'
|
|
'Kami menghargai privasi Anda dan berkomitmen untuk melindungi data pribadi dan pertanian Anda. Kebijakan ini menjelaskan bagaimana kami mengumpulkan, menggunakan, dan melindungi informasi Anda.',
|
|
),
|
|
SectionTitle(
|
|
'1. Informasi yang Kami Kumpulkan',
|
|
),
|
|
ParagraphText(
|
|
'Informasi Akun:',
|
|
),
|
|
ListItem(
|
|
'Nama, email, nomor telepon, dan detail profil',
|
|
),
|
|
ListItem(
|
|
'Nama pengguna dan kata sandi terenkripsi',
|
|
),
|
|
ParagraphText(
|
|
'Data Pertanian:',
|
|
),
|
|
ListItem(
|
|
'Lokasi lahan, ukuran, dan jenis tanah',
|
|
),
|
|
ListItem(
|
|
'Jenis tanaman, jadwal tanam, dan data panen',
|
|
),
|
|
ListItem(
|
|
'Data cuaca dan sensor (jika terhubung)',
|
|
),
|
|
ListItem(
|
|
'Aktivitas pertanian dan penggunaan input',
|
|
),
|
|
ParagraphText(
|
|
'Data Penggunaan:',
|
|
),
|
|
ListItem(
|
|
'Interaksi aplikasi dan penggunaan fitur',
|
|
),
|
|
ListItem(
|
|
'Informasi perangkat dan alamat IP',
|
|
),
|
|
ListItem(
|
|
'Data lokasi (dengan izin)',
|
|
),
|
|
SectionTitle(
|
|
'2. Bagaimana Kami Menggunakan Data Anda',
|
|
),
|
|
ListItem(
|
|
'Memberikan rekomendasi pertanian yang dipersonalisasi',
|
|
),
|
|
ListItem(
|
|
'Menghasilkan prakiraan cuaca dan peringatan',
|
|
),
|
|
ListItem(
|
|
'Meningkatkan model AI dan fitur platform',
|
|
),
|
|
ListItem(
|
|
'Mengirim pembaruan dan notifikasi penting',
|
|
),
|
|
ListItem(
|
|
'Memastikan keamanan platform dan mencegah penipuan',
|
|
),
|
|
SectionTitle(
|
|
'3. Pembagian Data',
|
|
),
|
|
ParagraphText(
|
|
'Kami tidak menjual data pribadi Anda. Kami dapat membagikan data teragregasi dan anonim untuk:',
|
|
),
|
|
ListItem(
|
|
'Penelitian pertanian dan wawasan industri',
|
|
),
|
|
ListItem(
|
|
'Peningkatan pemodelan cuaca dan tanaman',
|
|
),
|
|
ListItem(
|
|
'Kemitraan akademis (dengan persetujuan)',
|
|
),
|
|
ParagraphText(
|
|
'Kami membagikan data pribadi hanya ketika:',
|
|
),
|
|
ListItem(
|
|
'Diwajibkan oleh hukum atau peraturan',
|
|
),
|
|
ListItem(
|
|
'Diperlukan untuk penyedia layanan (hosting cloud, analitik)',
|
|
),
|
|
ListItem(
|
|
'Anda memberikan persetujuan eksplisit',
|
|
),
|
|
SectionTitle(
|
|
'4. Keamanan Data',
|
|
),
|
|
ParagraphText(
|
|
'Kami menerapkan langkah-langkah keamanan standar industri termasuk enkripsi, server aman, kontrol akses, dan audit keamanan rutin untuk melindungi informasi Anda.',
|
|
),
|
|
SectionTitle(
|
|
'5. Hak Anda',
|
|
),
|
|
ListItem(
|
|
'Mengakses dan mengunduh data Anda',
|
|
),
|
|
ListItem(
|
|
'Memperbaiki informasi yang tidak akurat',
|
|
),
|
|
ListItem(
|
|
'Menghapus akun dan data Anda',
|
|
),
|
|
ListItem(
|
|
'Berhenti berlangganan komunikasi pemasaran',
|
|
),
|
|
ListItem(
|
|
'Mengontrol berbagi lokasi',
|
|
),
|
|
SectionTitle(
|
|
'6. Penyimpanan Data',
|
|
),
|
|
ParagraphText(
|
|
'Kami menyimpan data Anda selama akun Anda aktif dan untuk periode yang wajar setelah penghapusan untuk mematuhi persyaratan hukum.',
|
|
),
|
|
SectionTitle(
|
|
'7. Privasi Anak-anak',
|
|
),
|
|
ParagraphText(
|
|
'Layanan kami tidak ditujukan untuk pengguna di bawah 16 tahun. Kami tidak secara sengaja mengumpulkan data dari anak-anak.',
|
|
),
|
|
SectionTitle(
|
|
'8. Hubungi Kami',
|
|
),
|
|
ParagraphText(
|
|
'Ada pertanyaan tentang privasi? Hubungi kami di:\nprivacy@tanismart.com\n\nTerakhir Diperbarui: Mei 2025',
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
// Register button
|
|
ElevatedButton(
|
|
onPressed:
|
|
(_isLoading || !_agreedToTerms) ? null : _register,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: primaryColor,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
elevation: 0,
|
|
shadowColor: primaryColor.withOpacity(0.5),
|
|
),
|
|
child:
|
|
_isLoading
|
|
? const SizedBox(
|
|
height: 20,
|
|
width: 20,
|
|
child: CircularProgressIndicator(
|
|
color: Colors.white,
|
|
strokeWidth: 2,
|
|
),
|
|
)
|
|
: const Text(
|
|
'Buat Akun',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
// Sign in option
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
'Sudah punya akun?',
|
|
style: TextStyle(color: subtextColor),
|
|
),
|
|
TextButton(
|
|
onPressed: () {
|
|
Navigator.pushReplacementNamed(context, '/login');
|
|
},
|
|
style: TextButton.styleFrom(
|
|
minimumSize: Size.zero,
|
|
padding: const EdgeInsets.only(left: 8),
|
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
foregroundColor: primaryColor,
|
|
),
|
|
child: const Text(
|
|
'Masuk',
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTextField({
|
|
required TextEditingController controller,
|
|
required String label,
|
|
required String hintText,
|
|
required IconData prefixIcon,
|
|
bool obscureText = false,
|
|
Widget? suffixIcon,
|
|
String? Function(String?)? validator,
|
|
TextInputType keyboardType = TextInputType.text,
|
|
}) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(left: 4),
|
|
child: Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w600,
|
|
color: textColor,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: surfaceColor,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.04),
|
|
blurRadius: 12,
|
|
offset: const Offset(0, 3),
|
|
),
|
|
],
|
|
),
|
|
child: TextFormField(
|
|
controller: controller,
|
|
obscureText: obscureText,
|
|
keyboardType: keyboardType,
|
|
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
|
|
decoration: InputDecoration(
|
|
hintText: hintText,
|
|
hintStyle: TextStyle(
|
|
color: subtextColor.withOpacity(0.7),
|
|
fontSize: 15,
|
|
),
|
|
prefixIcon: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
child: Icon(
|
|
prefixIcon,
|
|
color: primaryColor.withOpacity(0.8),
|
|
size: 22,
|
|
),
|
|
),
|
|
prefixIconConstraints: const BoxConstraints(
|
|
minWidth: 50,
|
|
minHeight: 50,
|
|
),
|
|
suffixIcon:
|
|
suffixIcon != null
|
|
? Padding(
|
|
padding: const EdgeInsets.only(right: 8),
|
|
child: suffixIcon,
|
|
)
|
|
: null,
|
|
filled: true,
|
|
fillColor: surfaceColor,
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
vertical: 16,
|
|
horizontal: 16,
|
|
),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
borderSide: BorderSide(
|
|
color: primaryColor.withOpacity(0.6),
|
|
width: 1.5,
|
|
),
|
|
),
|
|
errorBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
borderSide: BorderSide(color: Colors.red.shade300, width: 1.5),
|
|
),
|
|
focusedErrorBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
borderSide: BorderSide(color: Colors.red.shade300, width: 1.5),
|
|
),
|
|
),
|
|
validator: validator,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|