modify design forgot password, register account, otp code

This commit is contained in:
Samsularifin01 2026-04-20 21:26:48 +07:00
parent 7f3b71a40b
commit e4a12c12d1
4 changed files with 564 additions and 131 deletions

View File

@ -11,19 +11,28 @@ class ForgotPasswordScreen extends StatelessWidget {
ForgotPasswordScreen({super.key}); ForgotPasswordScreen({super.key});
void handleReset(BuildContext context) { // ============ HANDLE RESET PASSWORD ============
String email = emailController.text; void handleReset(BuildContext context) {
String email = emailController.text.trim();
if (email.isEmpty) { // ============ VALIDASI EMAIL KOSONG ============
tts.speak("Email harus diisi"); if (email.isEmpty) {
return; tts.speak("Email harus diisi");
return;
}
// ============ VALIDASI EMAIL WAJIB "@" ============
if (!email.contains("@")) {
tts.speak("Email harus mengandung simbol @");
return;
}
tts.speak("OTP telah dikirim ke email Anda");
Navigator.push(
context,
MaterialPageRoute(builder: (_) => OTPScreen()),
);
} }
tts.speak("OTP telah dikirim ke email ");
Navigator.push(
context,
MaterialPageRoute(builder: (_) => OTPScreen()),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -32,33 +41,128 @@ class ForgotPasswordScreen extends StatelessWidget {
return Scaffold( return Scaffold(
backgroundColor: AppColors.background, backgroundColor: AppColors.background,
appBar: AppBar( appBar: AppBar(
title: Text("Forgot Password"), title: const Text("Lupa Password"),
centerTitle: true,
elevation: 0,
backgroundColor: AppColors.background,
), ),
body: Padding( body: SingleChildScrollView(
padding: const EdgeInsets.all(20), child: Padding(
child: Column( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
children: [ child: Column(
SizedBox(height: 20), crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ==================== HEADER SECTION ====================
Center(
child: Column(
children: [
SizedBox(height: 40),
Text( // 🔐 ICON
"Masukkan email untuk reset password", Container(
style: TextStyle(color: AppColors.text), width: 70,
), height: 70,
decoration: BoxDecoration(
color: AppColors.primary,
borderRadius: BorderRadius.circular(15),
),
child: Icon(
Icons.lock_reset,
size: 36,
color: AppColors.background,
),
),
SizedBox(height: 20), SizedBox(height: 24),
CustomTextField( // 📝 TITLE
label: "Email", Text(
controller: emailController, "Reset Password",
), style: TextStyle(
color: AppColors.text,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 30), SizedBox(height: 12),
CustomButton( // 📌 SUBTITLE
text: "Kirim OTP", Text(
onPressed: () => handleReset(context), "Masukkan email Anda untuk menerima kode OTP",
), textAlign: TextAlign.center,
], style: TextStyle(
color: Colors.grey[400],
fontSize: 14,
),
),
SizedBox(height: 40),
],
),
),
// ==================== FORM SECTION ====================
// 📧 EMAIL LABEL
Text(
"Email Address",
style: TextStyle(
color: AppColors.primary,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
// 📧 EMAIL FIELD
CustomTextField(
label: "Masukkan email Anda",
controller: emailController,
keyboardType: TextInputType.emailAddress,
),
SizedBox(height: 12),
// 📌 INFO TEXT
Text(
"Kami akan mengirimkan kode OTP ke email ini untuk verifikasi",
style: TextStyle(
color: Colors.grey[500],
fontSize: 12,
),
),
SizedBox(height: 40),
// ==================== SUBMIT BUTTON ====================
SizedBox(
width: double.infinity,
child: CustomButton(
text: "Kirim Kode OTP",
onPressed: () => handleReset(context),
),
),
SizedBox(height: 20),
// ==================== BACK BUTTON ====================
Center(
child: TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
"Kembali ke Login",
style: TextStyle(
color: AppColors.primary,
fontSize: 14,
),
),
),
),
SizedBox(height: 20),
],
),
), ),
), ),
); );

View File

@ -1,4 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../widgets/custom_textfield.dart';
import '../widgets/custom_button.dart'; import '../widgets/custom_button.dart';
import '../services/tts_service.dart'; import '../services/tts_service.dart';
import '../utils/colors.dart'; import '../utils/colors.dart';
@ -10,64 +12,200 @@ class OTPScreen extends StatelessWidget {
OTPScreen({super.key}); OTPScreen({super.key});
// ============ VERIFY OTP CODE ============
void verifyOTP(BuildContext context) { void verifyOTP(BuildContext context) {
String otp = otpController.text; String otp = otpController.text.trim();
// ============ VALIDASI OTP KOSONG ============
if (otp.isEmpty) { if (otp.isEmpty) {
tts.speak("Kode OTP harus diisi"); tts.speak("Kode OTP harus diisi");
return; return;
} }
// 🔥 simulasi OTP benar // ============ VALIDASI OTP HARUS 6 DIGIT ============
if (otp == "1234") { if (otp.length < 6) {
tts.speak("OTP benar"); tts.speak("Kode OTP harus 6 digit");
Navigator.push( return;
context,
MaterialPageRoute(builder: (_) => ResetPasswordScreen()),
);
} else {
tts.speak("OTP salah");
} }
// ============ LANGSUNG KE RESET PASSWORD (TANPA DATABASE) ============
// NOTE: Fitur ini bypass validasi karena belum ada database
tts.speak("Kode OTP valid, lanjut ke reset password");
Navigator.push(
context,
MaterialPageRoute(builder: (_) => ResetPasswordScreen()),
);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
tts.speak("Masukkan kode OTP"); tts.speak("Halaman verifikasi kode OTP");
return Scaffold( return Scaffold(
backgroundColor: AppColors.background, backgroundColor: AppColors.background,
appBar: AppBar(title: Text("Verifikasi OTP")), appBar: AppBar(
body: Padding( title: const Text("Verifikasi OTP"),
padding: const EdgeInsets.all(20), centerTitle: true,
child: Column( elevation: 0,
children: [ backgroundColor: AppColors.background,
SizedBox(height: 30), ),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ==================== HEADER SECTION ====================
Center(
child: Column(
children: [
SizedBox(height: 40),
Text( // 📬 ICON
"Masukkan kode OTP", Container(
style: TextStyle(color: AppColors.text), width: 70,
), height: 70,
decoration: BoxDecoration(
color: AppColors.primary,
borderRadius: BorderRadius.circular(15),
),
child: Icon(
Icons.mail_outline,
size: 36,
color: AppColors.background,
),
),
SizedBox(height: 20), SizedBox(height: 24),
TextField( // 📝 TITLE
controller: otpController, Text(
keyboardType: TextInputType.number, "Verifikasi Kode OTP",
style: TextStyle(color: Colors.white), style: TextStyle(
decoration: InputDecoration( color: AppColors.text,
border: OutlineInputBorder(), fontSize: 24,
labelText: "OTP", fontWeight: FontWeight.bold,
labelStyle: TextStyle(color: Colors.white), ),
),
SizedBox(height: 12),
// 📌 SUBTITLE
Text(
"Masukkan kode 6 digit yang telah dikirim ke email Anda",
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.grey[400],
fontSize: 14,
),
),
SizedBox(height: 40),
],
),
), ),
),
SizedBox(height: 30), // ==================== FORM SECTION ====================
// 🔐 OTP LABEL
Text(
"Kode OTP",
style: TextStyle(
color: AppColors.primary,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
CustomButton( SizedBox(height: 8),
text: "Verifikasi",
onPressed: () => verifyOTP(context), // 🔐 OTP FIELD - MAKSIMAL 6 DIGIT
), CustomTextField(
], label: "Masukkan 6 digit kode",
controller: otpController,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(6),
],
maxLength: 6,
),
SizedBox(height: 12),
// 📌 INFO TEXT
Text(
"Kode ini hanya berlaku selama 1 menit setelah dikirim",
style: TextStyle(
color: Colors.grey[500],
fontSize: 12,
),
),
SizedBox(height: 40),
// ==================== VERIFY BUTTON ====================
SizedBox(
width: double.infinity,
child: CustomButton(
text: "Verifikasi Kode",
onPressed: () => verifyOTP(context),
),
),
SizedBox(height: 20),
// ==================== RESEND CODE SECTION ====================
Center(
child: Column(
children: [
Text(
"Tidak menerima kode?",
style: TextStyle(color: Colors.grey[400], fontSize: 13),
),
SizedBox(height: 8),
TextButton(
onPressed: () {
tts.speak("Kode OTP telah dikirim ulang");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Kode OTP telah dikirim ulang ke email Anda"),
backgroundColor: Colors.green,
duration: Duration(seconds: 2),
),
);
},
child: Text(
"Kirim Ulang Kode",
style: TextStyle(
color: AppColors.primary,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
),
],
),
),
SizedBox(height: 20),
// ==================== BACK BUTTON ====================
Center(
child: TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
"Kembali",
style: TextStyle(
color: AppColors.primary,
fontSize: 14,
),
),
),
),
SizedBox(height: 20),
],
),
), ),
), ),
); );

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../widgets/custom_textfield.dart'; import '../widgets/custom_textfield.dart';
import '../widgets/custom_button.dart'; import '../widgets/custom_button.dart';
import '../services/tts_service.dart'; import '../services/tts_service.dart';
@ -15,6 +16,8 @@ class _SignUpScreenState extends State<SignUpScreen> {
final TextEditingController emailController = TextEditingController(); final TextEditingController emailController = TextEditingController();
final TextEditingController passwordController = TextEditingController(); final TextEditingController passwordController = TextEditingController();
final TextEditingController confirmPasswordController = TextEditingController(); final TextEditingController confirmPasswordController = TextEditingController();
final TextEditingController nameController = TextEditingController();
final TextEditingController phoneController = TextEditingController();
final TTSService tts = TTSService(); final TTSService tts = TTSService();
String? selectedGender; String? selectedGender;
@ -28,6 +31,8 @@ class _SignUpScreenState extends State<SignUpScreen> {
@override @override
void dispose() { void dispose() {
nameController.dispose();
phoneController.dispose();
emailController.dispose(); emailController.dispose();
passwordController.dispose(); passwordController.dispose();
confirmPasswordController.dispose(); confirmPasswordController.dispose();
@ -35,12 +40,43 @@ class _SignUpScreenState extends State<SignUpScreen> {
} }
void handleSignUp(BuildContext context) { void handleSignUp(BuildContext context) {
String name = nameController.text;
String phone = phoneController.text;
String email = emailController.text; String email = emailController.text;
String password = passwordController.text; String password = passwordController.text;
String confirm = confirmPasswordController.text; String confirm = confirmPasswordController.text;
if (email.isEmpty || password.isEmpty) { // ============ VALIDASI NAMA ============
tts.speak("Email dan password wajib diisi"); 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; return;
} }
@ -49,6 +85,7 @@ class _SignUpScreenState extends State<SignUpScreen> {
return; return;
} }
// ============ VALIDASI JENIS KELAMIN ============
if (selectedGender == null) { if (selectedGender == null) {
tts.speak("Jenis kelamin wajib dipilih"); tts.speak("Jenis kelamin wajib dipilih");
return; return;
@ -91,72 +128,187 @@ class _SignUpScreenState extends State<SignUpScreen> {
return Scaffold( return Scaffold(
backgroundColor: AppColors.background, backgroundColor: AppColors.background,
appBar: AppBar( appBar: AppBar(
title: const Text("Sign Up"), title: const Text("Buat Akun Baru"),
centerTitle: true,
elevation: 0,
backgroundColor: AppColors.background,
), ),
body: SingleChildScrollView( body: SingleChildScrollView(
padding: const EdgeInsets.all(20), child: Padding(
child: Column( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
const SizedBox(height: 20), children: [
// ==================== HEADER SECTION ====================
CustomTextField( Center(
label: "Email",
controller: emailController,
),
const SizedBox(height: 20),
CustomTextField(
label: "Password",
controller: passwordController,
obscure: true,
),
const SizedBox(height: 20),
CustomTextField(
label: "Konfirmasi Password",
controller: confirmPasswordController,
obscure: true,
),
const SizedBox(height: 20),
Semantics(
label: "Pilih jenis kelamin",
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
border: Border.all(color: AppColors.text),
borderRadius: BorderRadius.circular(4),
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text( Container(
"Jenis Kelamin", width: 60,
style: TextStyle( height: 60,
color: AppColors.text, decoration: BoxDecoration(
fontSize: 16, 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,
), ),
), ),
buildGenderOption("Laki-laki"),
const Divider(color: Colors.white24, height: 1),
buildGenderOption("Perempuan"),
], ],
), ),
), ),
),
const SizedBox(height: 30), SizedBox(height: 32),
CustomButton( // ==================== PERSONAL INFO SECTION ====================
text: "Daftar", Text(
onPressed: () => handleSignUp(context), "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,
),
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),
],
),
), ),
), ),
); );

View File

@ -1,28 +1,53 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../utils/colors.dart'; import '../utils/colors.dart';
class CustomTextField extends StatelessWidget { class CustomTextField extends StatefulWidget {
final String label; final String label;
final TextEditingController controller; final TextEditingController controller;
final bool obscure; final bool obscure;
final TextInputType? keyboardType;
final List<TextInputFormatter>? inputFormatters;
final int? maxLength;
final String? Function(String?)? validator;
const CustomTextField({ const CustomTextField({
super.key, super.key,
required this.label, required this.label,
required this.controller, required this.controller,
this.obscure = false, this.obscure = false,
this.keyboardType,
this.inputFormatters,
this.maxLength,
this.validator,
}); });
@override
State<CustomTextField> createState() => _CustomTextFieldState();
}
class _CustomTextFieldState extends State<CustomTextField> {
late bool _obscureText;
@override
void initState() {
super.initState();
_obscureText = widget.obscure;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Semantics( return Semantics(
label: "Input $label", label: "Input ${widget.label}",
child: TextField( child: TextField(
controller: controller, controller: widget.controller,
obscureText: obscure, obscureText: _obscureText,
keyboardType: widget.keyboardType,
inputFormatters: widget.inputFormatters,
maxLength: widget.maxLength,
style: TextStyle(color: AppColors.text), style: TextStyle(color: AppColors.text),
decoration: InputDecoration( decoration: InputDecoration(
labelText: label, labelText: widget.label,
labelStyle: TextStyle(color: AppColors.text), labelStyle: TextStyle(color: AppColors.text),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: AppColors.text), borderSide: BorderSide(color: AppColors.text),
@ -30,6 +55,20 @@ class CustomTextField extends StatelessWidget {
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: AppColors.primary), borderSide: BorderSide(color: AppColors.primary),
), ),
// ============ TOGGLE PASSWORD VISIBILITY ============
suffixIcon: widget.obscure
? IconButton(
onPressed: () {
setState(() {
_obscureText = !_obscureText;
});
},
icon: Icon(
_obscureText ? Icons.visibility_off : Icons.visibility,
color: AppColors.text,
),
)
: null,
), ),
), ),
); );