TKK_E32230254/lib/login_page.dart

453 lines
18 KiB
Dart

import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'dashboard_admin.dart';
import 'dashboard_guru.dart';
import 'dashboard_ortu.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final email = TextEditingController();
final password = TextEditingController();
bool isLoading = false;
bool obscurePassword = true;
@override
void dispose() {
email.dispose();
password.dispose();
super.dispose();
}
Future<void> login() async {
if (email.text.trim().isEmpty || password.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Email dan Password wajib diisi'),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
),
);
return;
}
setState(() => isLoading = true);
try {
final userCred = await FirebaseAuth.instance.signInWithEmailAndPassword(
email: email.text.trim(),
password: password.text.trim(),
);
final doc = await FirebaseFirestore.instance
.collection('users')
.doc(userCred.user!.uid)
.get();
final data = doc.data();
if (data == null) throw Exception("Data pengguna tidak ditemukan");
final role = data['role'];
if (!mounted) return;
if (role == 'admin') {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const DashboardAdmin()));
} else if (role == 'guru') {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const DashboardGuru()));
} else {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const DashboardOrtu()));
}
} on FirebaseAuthException catch (e) {
String pesan = "Login gagal";
if (e.code == "user-not-found") pesan = "Pengguna tidak ditemukan";
else if (e.code == "wrong-password") pesan = "Kata sandi salah";
else if (e.code == "invalid-email") pesan = "Format email tidak valid";
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(pesan),
behavior: SnackBarBehavior.floating,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Kesalahan: $e"),
behavior: SnackBarBehavior.floating,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
),
);
}
}
if (mounted) setState(() => isLoading = false);
}
// 📝 Diperkecil tinggi container field dari 54 menjadi 48
Widget inputField({
required TextEditingController controller,
required String hint,
required IconData icon,
bool obscure = false,
Widget? suffixIcon,
}) {
return Container(
height: 48,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: Color(0x0F7F56D9),
blurRadius: 8,
offset: Offset(0, 3),
)
],
border: Border.all(color: const Color(0xffE6E0FF), width: 1),
),
child: TextField(
controller: controller,
obscureText: obscure,
style: const TextStyle(fontSize: 14, color: Color(0xff2D2669)),
decoration: InputDecoration(
border: InputBorder.none,
hintText: hint,
hintStyle: const TextStyle(color: Color(0xffA098C7), fontSize: 13),
prefixIcon: Icon(icon, color: const Color(0xff7F56D9), size: 18),
suffixIcon: suffixIcon,
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 11),
),
),
);
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
final screenHeight = screenSize.height;
// ⚡ Penyesuaian proporsi tinggi komponen khusus layar Vivo Y02t agar lebih ringkas
final double illustrationHeight = screenHeight * 0.14; // Diturunkan ke ~14%
final double spacingTop = screenHeight * 0.015;
final double spacingMiddle = screenHeight * 0.018;
return Scaffold(
backgroundColor: const Color(0xffF5F2FF),
resizeToAvoidBottomInset: true,
body: SafeArea(
child: SingleChildScrollView(
physics: const ClampingScrollPhysics(),
child: Stack(
children: [
// DEKORASI LATAR BELAKANG
_buildBackgroundDecorations(screenHeight),
// KONTEN UTAMA
Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 400),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: spacingTop),
// Logo Sekolah (Diperkecil sedikit rasio layarnya)
Image.asset(
'lib/assets/sekolah.png',
height: screenHeight * 0.07,
fit: BoxFit.contain,
errorBuilder: (_, __, ___) => const Icon(
Icons.school,
size: 55,
color: Color(0xff7F56D9),
),
),
const SizedBox(height: 6),
// Ukuran font judul dikurangi agar tidak memakan tempat banyak
const Text(
"ABSENSI",
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Color(0xff2D2669),
letterSpacing: 1.2,
),
),
const Text(
"— SISTEM ABSENSI PGRI —",
style: TextStyle(
fontSize: 11,
color: Color(0xff7F56D9),
fontWeight: FontWeight.w500,
letterSpacing: 0.8,
),
),
const SizedBox(height: 2),
const Text(
"TK PGRI BHAKTI LESTARI",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Color(0xff2D2669),
),
),
SizedBox(height: spacingMiddle),
// ILUSTRASI ANAK (RESPONSIF & DIBATASI LEBIH KECIL)
_buildStudentIllustration(illustrationHeight),
SizedBox(height: spacingMiddle),
// KOTAK LOGIN (Padding dikurangi)
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: const [
BoxShadow(
color: Color(0x127F56D9),
blurRadius: 15,
offset: Offset(0, 4),
)
],
border: Border.all(color: const Color(0xffE6E0FF), width: 1.2),
),
child: Column(
children: [
const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("", style: TextStyle(fontSize: 16, color: Color(0xff7F56D9), fontWeight: FontWeight.bold)),
Text("Login", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Color(0xff2D2669))),
Text("", style: TextStyle(fontSize: 16, color: Color(0xff7F56D9), fontWeight: FontWeight.bold)),
],
),
const SizedBox(height: 14),
inputField(
controller: email,
hint: "Email",
icon: Icons.email_outlined,
),
const SizedBox(height: 10),
inputField(
controller: password,
hint: "Password",
icon: Icons.lock_outline,
obscure: obscurePassword,
suffixIcon: IconButton(
icon: Icon(
obscurePassword ? Icons.visibility_off_outlined : Icons.visibility_outlined,
color: const Color(0xffA098C7),
size: 18,
),
onPressed: () => setState(() => obscurePassword = !obscurePassword),
),
),
const SizedBox(height: 16),
// Tombol Login (Tinggi diturunkan dari 50 ke 44)
SizedBox(
width: double.infinity,
height: 44,
child: ElevatedButton(
onPressed: isLoading ? null : login,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xff7F56D9),
disabledBackgroundColor: const Color(0xffB7A3E8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 1.5,
shadowColor: const Color(0xff7F56D9).withOpacity(0.2),
),
child: isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2.2),
)
: const Text(
"Login",
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Colors.white),
),
),
),
const SizedBox(height: 10),
const Text(
"Gunakan akun yang telah terdaftar",
style: TextStyle(fontSize: 10, color: Color(0xff7F56D9)),
),
const SizedBox(height: 10),
// Garis pemisah
Row(
children: [
const Expanded(child: Divider(color: Color(0xffE0DBF5), thickness: 1, indent: 6, endIndent: 6)),
Container(
padding: const EdgeInsets.all(5),
decoration: const BoxDecoration(color: Color(0xffF5F2FF), shape: BoxShape.circle),
child: const Icon(Icons.shield_outlined, color: Color(0xff7F56D9), size: 12),
),
const Expanded(child: Divider(color: Color(0xffE0DBF5), thickness: 1, indent: 6, endIndent: 6)),
],
),
],
),
),
const SizedBox(height: 12),
// INFORMASI PENGEMBANG (Diperkecil padding dan ukuran teksnya)
Container(
width: double.infinity,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: const Color(0xffEDE9FF),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xffDCD4FF)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Dikembangkan oleh:", style: TextStyle(fontSize: 9, color: Color(0xff7F56D9), fontWeight: FontWeight.w500)),
SizedBox(height: 1),
Text("Quri Amaliatas Solehah", style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Color(0xff2D2669))),
SizedBox(height: 1),
Text("Politeknik Negeri Jember", style: TextStyle(fontSize: 10, color: Color(0xff4A4385))),
Text("Jurusan Teknologi Informasi | Teknik Komputer 2023", style: TextStyle(fontSize: 8, color: Color(0xff7F56D9))),
],
),
),
const SizedBox(width: 4),
Image.asset(
'lib/assets/jti.png',
height: 18,
fit: BoxFit.contain,
errorBuilder: (_, __, ___) => const Icon(Icons.school_outlined, size: 18, color: Color(0xff7F56D9)),
),
],
),
),
],
),
),
),
],
),
),
),
);
}
Widget _buildBackgroundDecorations(double height) {
return Positioned.fill(
child: Stack(
children: [
Positioned(
top: -50,
left: -50,
child: Container(width: 140, height: 140, decoration: BoxDecoration(color: const Color(0xff7F56D9).withOpacity(0.05), shape: BoxShape.circle)),
),
Positioned(
top: 40,
right: -25,
child: Container(width: 80, height: 80, decoration: BoxDecoration(color: const Color(0xff7F56D9).withOpacity(0.04), shape: BoxShape.circle)),
),
Positioned(
bottom: -30,
left: -15,
child: Container(width: 100, height: 100, decoration: BoxDecoration(color: const Color(0xff7F56D9).withOpacity(0.04), shape: BoxShape.circle)),
),
Positioned(top: 40, left: 20, child: _buildDotGrid()),
Positioned(top: 60, right: 30, child: _buildDotGrid()),
Positioned(bottom: 80, right: 20, child: _buildDotGrid()),
],
),
);
}
Widget _buildDotGrid() {
return Opacity(
opacity: 0.15,
child: Column(
children: List.generate(3, (_) => Row(
children: List.generate(3, (_) => Container(
width: 2.5,
height: 2.5,
margin: const EdgeInsets.all(2.0),
decoration: const BoxDecoration(color: Color(0xff7F56D9), shape: BoxShape.circle),
)),
)),
),
);
}
Widget _buildStudentIllustration(double dynamicHeight) {
return Container(
width: double.infinity,
height: dynamicHeight.clamp(110.0, 140.0), // Rentang clamp diturunkan agar pas di layar Vivo Y02t
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [const Color(0xffEDE9FF), const Color(0xffDCD4FF).withOpacity(0.5)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xffD1C8FF), width: 1),
),
child: Stack(
children: [
Positioned(top: 8, left: 12, child: Icon(Icons.cloud_outlined, color: const Color(0xff7F56D9).withOpacity(0.25), size: 18)),
Positioned(bottom: 12, right: 16, child: Icon(Icons.star_border, color: const Color(0xff7F56D9).withOpacity(0.25), size: 16)),
Center(
child: Image.asset(
'lib/assets/anak_rfid.png',
height: (dynamicHeight - 20).clamp(90.0, 120.0),
fit: BoxFit.contain,
errorBuilder: (_, __, ___) => const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.image_not_supported, size: 35, color: Color(0xff7F56D9)),
SizedBox(height: 2),
Text("Masukkan gambar ilustrasi_anak.png", style: TextStyle(color: Color(0xff7F56D9), fontSize: 10)),
],
),
),
),
],
),
);
}
}