From e4a12c12d1dcfb636a75c625dc3416cec95bfb74 Mon Sep 17 00:00:00 2001 From: Samsularifin01 <158025491+Samsularifin01@users.noreply.github.com> Date: Mon, 20 Apr 2026 21:26:48 +0700 Subject: [PATCH] modify design forgot password, register account, otp code --- lib/screens/forgot_password_screen.dart | 168 ++++++++++++--- lib/screens/otp_screen.dart | 212 +++++++++++++++---- lib/screens/signup_screen.dart | 266 +++++++++++++++++++----- lib/widgets/custom_textfield.dart | 49 ++++- 4 files changed, 564 insertions(+), 131 deletions(-) diff --git a/lib/screens/forgot_password_screen.dart b/lib/screens/forgot_password_screen.dart index d7d9a32..6dcc5b8 100644 --- a/lib/screens/forgot_password_screen.dart +++ b/lib/screens/forgot_password_screen.dart @@ -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), + ], + ), ), ), ); diff --git a/lib/screens/otp_screen.dart b/lib/screens/otp_screen.dart index 6308512..049fd3e 100644 --- a/lib/screens/otp_screen.dart +++ b/lib/screens/otp_screen.dart @@ -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), + ], + ), ), ), ); diff --git a/lib/screens/signup_screen.dart b/lib/screens/signup_screen.dart index bf8fb6c..f07c013 100644 --- a/lib/screens/signup_screen.dart +++ b/lib/screens/signup_screen.dart @@ -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 { 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 { @override void dispose() { + nameController.dispose(); + phoneController.dispose(); emailController.dispose(); passwordController.dispose(); confirmPasswordController.dispose(); @@ -35,12 +40,43 @@ class _SignUpScreenState extends State { } 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 { return; } + // ============ VALIDASI JENIS KELAMIN ============ if (selectedGender == null) { tts.speak("Jenis kelamin wajib dipilih"); return; @@ -91,72 +128,187 @@ class _SignUpScreenState extends State { 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), + ], + ), ), ), ); diff --git a/lib/widgets/custom_textfield.dart b/lib/widgets/custom_textfield.dart index 0eec97f..5dc39f3 100644 --- a/lib/widgets/custom_textfield.dart +++ b/lib/widgets/custom_textfield.dart @@ -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? 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 createState() => _CustomTextFieldState(); +} + +class _CustomTextFieldState extends State { + 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, ), ), );