414 lines
12 KiB
Dart
414 lines
12 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import '../widgets/custom_textfield.dart';
|
|
import '../widgets/custom_button.dart';
|
|
import '../services/tts_service.dart';
|
|
import '../services/register_api_service.dart';
|
|
import '../models/register_request.dart';
|
|
import '../utils/colors.dart';
|
|
|
|
class SignUpScreen extends StatefulWidget {
|
|
const SignUpScreen({super.key});
|
|
|
|
@override
|
|
State<SignUpScreen> createState() => _SignUpScreenState();
|
|
}
|
|
|
|
class _SignUpScreenState extends State<SignUpScreen> {
|
|
final TextEditingController emailController = TextEditingController();
|
|
final TextEditingController passwordController = TextEditingController();
|
|
final TextEditingController confirmPasswordController = TextEditingController();
|
|
final TextEditingController nameController = TextEditingController();
|
|
final TextEditingController phoneController = TextEditingController();
|
|
final TTSService tts = TTSService();
|
|
final RegisterApiService registerApi = RegisterApiService();
|
|
String? selectedGender;
|
|
String passwordStrength = ""; // Track password strength
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
tts.speak("Halaman daftar akun");
|
|
});
|
|
|
|
// ============ LISTENER UNTUK PASSWORD REAL-TIME ============
|
|
passwordController.addListener(() {
|
|
setState(() {
|
|
passwordStrength = _getPasswordStrength(passwordController.text);
|
|
});
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
nameController.dispose();
|
|
phoneController.dispose();
|
|
emailController.dispose();
|
|
passwordController.dispose();
|
|
confirmPasswordController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> handleSignUp(BuildContext context) async {
|
|
String name = nameController.text;
|
|
String phone = phoneController.text;
|
|
String email = emailController.text;
|
|
String password = passwordController.text;
|
|
String confirm = confirmPasswordController.text;
|
|
|
|
// ============ VALIDASI NAMA ============
|
|
if (name.isEmpty) {
|
|
tts.speak("Nama lengkap wajib diisi");
|
|
return;
|
|
}
|
|
|
|
// ============ VALIDASI TELEPON ============
|
|
if (phone.isEmpty) {
|
|
tts.speak("Nomer telepon wajib diisi");
|
|
return;
|
|
}
|
|
|
|
if (phone.length < 10) {
|
|
tts.speak("Nomer telepon minimal 10 angka");
|
|
return;
|
|
}
|
|
|
|
// ============ VALIDASI EMAIL ============
|
|
if (email.isEmpty) {
|
|
tts.speak("Email wajib diisi");
|
|
return;
|
|
}
|
|
|
|
if (!email.contains("@")) {
|
|
tts.speak("Email harus mengandung simbol @");
|
|
return;
|
|
}
|
|
|
|
// ============ VALIDASI PASSWORD ============
|
|
if (password.isEmpty) {
|
|
tts.speak("Password wajib diisi");
|
|
return;
|
|
}
|
|
|
|
if (password != confirm) {
|
|
tts.speak("Password tidak sama");
|
|
return;
|
|
}
|
|
|
|
// ============ VALIDASI JENIS KELAMIN ============
|
|
if (selectedGender == null) {
|
|
tts.speak("Jenis kelamin wajib dipilih");
|
|
return;
|
|
}
|
|
|
|
final RegisterRequest request = RegisterRequest(
|
|
name: name,
|
|
email: email,
|
|
phone: phone,
|
|
gender: selectedGender!.toLowerCase(),
|
|
password: password,
|
|
);
|
|
|
|
final response = await registerApi.register(request);
|
|
|
|
if (!context.mounted) {
|
|
return;
|
|
}
|
|
|
|
if (response.isSuccess) {
|
|
tts.speak(response.message.isNotEmpty
|
|
? response.message
|
|
: "Pendaftaran berhasil");
|
|
Navigator.pop(context);
|
|
} else {
|
|
tts.speak(response.message.isNotEmpty
|
|
? response.message
|
|
: "Email sudah digunakan");
|
|
}
|
|
}
|
|
|
|
// ============ FUNGSI UNTUK MENGHITUNG PASSWORD STRENGTH ============
|
|
String _getPasswordStrength(String password) {
|
|
if (password.isEmpty) {
|
|
return "";
|
|
}
|
|
|
|
// ✅ Cek ada huruf besar
|
|
bool hasUpperCase = password.contains(RegExp(r'[A-Z]'));
|
|
|
|
// ✅ Cek ada angka
|
|
bool hasNumber = password.contains(RegExp(r'[0-9]'));
|
|
|
|
// ✅ Penentuan strength
|
|
if (password.length < 6) {
|
|
return "weak"; // Password lemah
|
|
} else if (hasUpperCase && hasNumber) {
|
|
return "strong"; // Password kuat
|
|
} else {
|
|
return "medium"; // Password sedang
|
|
}
|
|
}
|
|
|
|
// ============ FUNGSI UNTUK MENDAPATKAN WARNA PASSWORD STRENGTH ============
|
|
Color _getPasswordStrengthColor() {
|
|
switch (passwordStrength) {
|
|
case "weak":
|
|
return Colors.red; // Danger
|
|
case "strong":
|
|
return Colors.green; // Success
|
|
case "medium":
|
|
return Colors.orange; // Warning
|
|
default:
|
|
return Colors.transparent;
|
|
}
|
|
}
|
|
|
|
// ============ FUNGSI UNTUK MENDAPATKAN TEXT PASSWORD STRENGTH ============
|
|
String _getPasswordStrengthText() {
|
|
switch (passwordStrength) {
|
|
case "weak":
|
|
return "Password lemah";
|
|
case "strong":
|
|
return "Password kuat";
|
|
case "medium":
|
|
return "Password sedang";
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
|
|
Widget buildGenderOption(String gender) {
|
|
return RadioListTile<String>(
|
|
contentPadding: EdgeInsets.zero,
|
|
value: gender,
|
|
groupValue: selectedGender,
|
|
activeColor: AppColors.primary,
|
|
fillColor: MaterialStateProperty.resolveWith((states) {
|
|
if (states.contains(MaterialState.selected)) {
|
|
return AppColors.primary;
|
|
}
|
|
return AppColors.text;
|
|
}),
|
|
title: Text(
|
|
gender,
|
|
style: TextStyle(color: AppColors.text),
|
|
),
|
|
onChanged: (value) {
|
|
setState(() {
|
|
selectedGender = value;
|
|
});
|
|
|
|
if (value != null) {
|
|
tts.speak("Jenis kelamin $value dipilih");
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: AppColors.background,
|
|
appBar: AppBar(
|
|
title: const Text("Buat Akun Baru"),
|
|
centerTitle: true,
|
|
elevation: 0,
|
|
backgroundColor: AppColors.background,
|
|
),
|
|
body: SingleChildScrollView(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// ==================== HEADER SECTION ====================
|
|
Center(
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
width: 60,
|
|
height: 60,
|
|
decoration: BoxDecoration(
|
|
color: AppColors.primary,
|
|
borderRadius: BorderRadius.circular(15),
|
|
),
|
|
child: Icon(
|
|
Icons.person_add,
|
|
size: 32,
|
|
color: AppColors.background,
|
|
),
|
|
),
|
|
SizedBox(height: 16),
|
|
Text(
|
|
"Daftar Akun",
|
|
style: TextStyle(
|
|
color: AppColors.text,
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
SizedBox(height: 8),
|
|
Text(
|
|
"Isi data diri Anda untuk membuat akun",
|
|
style: TextStyle(
|
|
color: Colors.grey[400],
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
SizedBox(height: 32),
|
|
|
|
// ==================== PERSONAL INFO SECTION ====================
|
|
Text(
|
|
"Data Pribadi",
|
|
style: TextStyle(
|
|
color: AppColors.primary,
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
|
|
SizedBox(height: 16),
|
|
|
|
CustomTextField(
|
|
label: "Nama Lengkap",
|
|
controller: nameController,
|
|
),
|
|
|
|
SizedBox(height: 16),
|
|
|
|
CustomTextField(
|
|
label: "Nomer Telepon",
|
|
controller: phoneController,
|
|
keyboardType: TextInputType.number,
|
|
inputFormatters: [
|
|
FilteringTextInputFormatter.digitsOnly,
|
|
LengthLimitingTextInputFormatter(13),
|
|
],
|
|
maxLength: 13,
|
|
),
|
|
|
|
SizedBox(height: 20),
|
|
|
|
// ==================== GENDER SELECTION ====================
|
|
Text(
|
|
"Jenis Kelamin",
|
|
style: TextStyle(
|
|
color: AppColors.primary,
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
|
|
SizedBox(height: 12),
|
|
|
|
Semantics(
|
|
label: "Pilih jenis kelamin",
|
|
child: Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: AppColors.text, width: 1.5),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
buildGenderOption("laki-laki"),
|
|
const Divider(color: Colors.white24, height: 8),
|
|
buildGenderOption("perempuan"),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
SizedBox(height: 28),
|
|
|
|
// ==================== CREDENTIALS SECTION ====================
|
|
Text(
|
|
"Keamanan Akun",
|
|
style: TextStyle(
|
|
color: AppColors.primary,
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
|
|
SizedBox(height: 16),
|
|
|
|
CustomTextField(
|
|
label: "Email",
|
|
controller: emailController,
|
|
keyboardType: TextInputType.emailAddress,
|
|
),
|
|
|
|
SizedBox(height: 16),
|
|
|
|
CustomTextField(
|
|
label: "Password",
|
|
controller: passwordController,
|
|
obscure: true,
|
|
),
|
|
|
|
// ============ PASSWORD STRENGTH INDICATOR ============
|
|
if (passwordStrength.isNotEmpty)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 8),
|
|
child: Text(
|
|
_getPasswordStrengthText(),
|
|
style: TextStyle(
|
|
color: _getPasswordStrengthColor(),
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
|
|
SizedBox(height: 16),
|
|
|
|
CustomTextField(
|
|
label: "Konfirmasi Password",
|
|
controller: confirmPasswordController,
|
|
obscure: true,
|
|
),
|
|
|
|
SizedBox(height: 32),
|
|
|
|
// ==================== SUBMIT BUTTON ====================
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: CustomButton(
|
|
text: "Daftar Sekarang",
|
|
onPressed: () => handleSignUp(context),
|
|
),
|
|
),
|
|
|
|
SizedBox(height: 16),
|
|
|
|
// ==================== BACK TO LOGIN ====================
|
|
Center(
|
|
child: TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: Text(
|
|
"Kembali ke Login",
|
|
style: TextStyle(
|
|
color: AppColors.primary,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
SizedBox(height: 20),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|