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});
void handleReset(BuildContext context) {
String email = emailController.text;
// ============ HANDLE RESET PASSWORD ============
void handleReset(BuildContext context) {
String email = emailController.text.trim();
if (email.isEmpty) {
tts.speak("Email harus diisi");
return;
// ============ VALIDASI EMAIL KOSONG ============
if (email.isEmpty) {
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
Widget build(BuildContext context) {
@ -32,33 +41,128 @@ class ForgotPasswordScreen extends StatelessWidget {
return Scaffold(
backgroundColor: AppColors.background,
appBar: AppBar(
title: Text("Forgot Password"),
title: const Text("Lupa Password"),
centerTitle: true,
elevation: 0,
backgroundColor: AppColors.background,
),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
SizedBox(height: 20),
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(
"Masukkan email untuk reset password",
style: TextStyle(color: AppColors.text),
),
// 🔐 ICON
Container(
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(
label: "Email",
controller: emailController,
),
// 📝 TITLE
Text(
"Reset Password",
style: TextStyle(
color: AppColors.text,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 30),
SizedBox(height: 12),
CustomButton(
text: "Kirim OTP",
onPressed: () => handleReset(context),
),
],
// 📌 SUBTITLE
Text(
"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/services.dart';
import '../widgets/custom_textfield.dart';
import '../widgets/custom_button.dart';
import '../services/tts_service.dart';
import '../utils/colors.dart';
@ -10,64 +12,200 @@ class OTPScreen extends StatelessWidget {
OTPScreen({super.key});
// ============ VERIFY OTP CODE ============
void verifyOTP(BuildContext context) {
String otp = otpController.text;
String otp = otpController.text.trim();
// ============ VALIDASI OTP KOSONG ============
if (otp.isEmpty) {
tts.speak("Kode OTP harus diisi");
return;
}
// 🔥 simulasi OTP benar
if (otp == "1234") {
tts.speak("OTP benar");
Navigator.push(
context,
MaterialPageRoute(builder: (_) => ResetPasswordScreen()),
);
} else {
tts.speak("OTP salah");
// ============ VALIDASI OTP HARUS 6 DIGIT ============
if (otp.length < 6) {
tts.speak("Kode OTP harus 6 digit");
return;
}
// ============ 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
Widget build(BuildContext context) {
tts.speak("Masukkan kode OTP");
tts.speak("Halaman verifikasi kode OTP");
return Scaffold(
backgroundColor: AppColors.background,
appBar: AppBar(title: Text("Verifikasi OTP")),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
SizedBox(height: 30),
appBar: AppBar(
title: const Text("Verifikasi OTP"),
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: [
SizedBox(height: 40),
Text(
"Masukkan kode OTP",
style: TextStyle(color: AppColors.text),
),
// 📬 ICON
Container(
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(
controller: otpController,
keyboardType: TextInputType.number,
style: TextStyle(color: Colors.white),
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: "OTP",
labelStyle: TextStyle(color: Colors.white),
// 📝 TITLE
Text(
"Verifikasi Kode OTP",
style: TextStyle(
color: AppColors.text,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
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(
text: "Verifikasi",
onPressed: () => verifyOTP(context),
),
],
SizedBox(height: 8),
// 🔐 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/services.dart';
import '../widgets/custom_textfield.dart';
import '../widgets/custom_button.dart';
import '../services/tts_service.dart';
@ -15,6 +16,8 @@ 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();
String? selectedGender;
@ -28,6 +31,8 @@ class _SignUpScreenState extends State<SignUpScreen> {
@override
void dispose() {
nameController.dispose();
phoneController.dispose();
emailController.dispose();
passwordController.dispose();
confirmPasswordController.dispose();
@ -35,12 +40,43 @@ class _SignUpScreenState extends State<SignUpScreen> {
}
void handleSignUp(BuildContext context) {
String name = nameController.text;
String phone = phoneController.text;
String email = emailController.text;
String password = passwordController.text;
String confirm = confirmPasswordController.text;
if (email.isEmpty || password.isEmpty) {
tts.speak("Email dan password wajib diisi");
// ============ 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;
}
@ -49,6 +85,7 @@ class _SignUpScreenState extends State<SignUpScreen> {
return;
}
// ============ VALIDASI JENIS KELAMIN ============
if (selectedGender == null) {
tts.speak("Jenis kelamin wajib dipilih");
return;
@ -91,72 +128,187 @@ class _SignUpScreenState extends State<SignUpScreen> {
return Scaffold(
backgroundColor: AppColors.background,
appBar: AppBar(
title: const Text("Sign Up"),
title: const Text("Buat Akun Baru"),
centerTitle: true,
elevation: 0,
backgroundColor: AppColors.background,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 20),
CustomTextField(
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: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ==================== HEADER SECTION ====================
Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Jenis Kelamin",
style: TextStyle(
color: AppColors.text,
fontSize: 16,
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,
),
),
buildGenderOption("Laki-laki"),
const Divider(color: Colors.white24, height: 1),
buildGenderOption("Perempuan"),
],
),
),
),
const SizedBox(height: 30),
SizedBox(height: 32),
CustomButton(
text: "Daftar",
onPressed: () => handleSignUp(context),
),
],
// ==================== 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,
),
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/services.dart';
import '../utils/colors.dart';
class CustomTextField extends StatelessWidget {
class CustomTextField extends StatefulWidget {
final String label;
final TextEditingController controller;
final bool obscure;
final TextInputType? keyboardType;
final List<TextInputFormatter>? inputFormatters;
final int? maxLength;
final String? Function(String?)? validator;
const CustomTextField({
super.key,
required this.label,
required this.controller,
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
Widget build(BuildContext context) {
return Semantics(
label: "Input $label",
label: "Input ${widget.label}",
child: TextField(
controller: controller,
obscureText: obscure,
controller: widget.controller,
obscureText: _obscureText,
keyboardType: widget.keyboardType,
inputFormatters: widget.inputFormatters,
maxLength: widget.maxLength,
style: TextStyle(color: AppColors.text),
decoration: InputDecoration(
labelText: label,
labelText: widget.label,
labelStyle: TextStyle(color: AppColors.text),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: AppColors.text),
@ -30,6 +55,20 @@ class CustomTextField extends StatelessWidget {
focusedBorder: OutlineInputBorder(
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,
),
),
);