diff --git a/app/src/main/java/com/example/kalkulatorbmi/LoginScreen.kt b/app/src/main/java/com/example/kalkulatorbmi/LoginScreen.kt new file mode 100644 index 0000000..1a9879f --- /dev/null +++ b/app/src/main/java/com/example/kalkulatorbmi/LoginScreen.kt @@ -0,0 +1,223 @@ +package com.example.kalkulatorbmi + +// 1. TAMBAHKAN IMPORT INI untuk mengakses resource seperti gambar +import com.example.kalkulatorbmi.R + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.slideInVertically +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +// 2. Rapikan import yang duplikat +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@Composable +fun LoginScreen(navController: NavController) { + var username by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + var passwordVisible by remember { mutableStateOf(false) } + var isLoading by remember { mutableStateOf(false) } + + // State untuk validasi error + var usernameError by remember { mutableStateOf(null) } + var passwordError by remember { mutableStateOf(null) } + + // State untuk animasi + var isFormVisible by remember { mutableStateOf(false) } + val coroutineScope = rememberCoroutineScope() + var shakeState by remember { mutableStateOf(false) } + + // Animasi scale untuk efek "pop in" pada logo + val scale = animateFloatAsState( + targetValue = if (isFormVisible) 1f else 0.5f, + animationSpec = tween(durationMillis = 500), label = "logo_scale" + ).value + + LaunchedEffect(Unit) { + isFormVisible = true + } + + val focusManager = LocalFocusManager.current + + // 3. Latar Belakang Gradien + val gradientBrush = Brush.verticalGradient( + colors = listOf( + MaterialTheme.colorScheme.primary.copy(alpha = 0.2f), + MaterialTheme.colorScheme.background + ) + ) + + Box( + modifier = Modifier + .fillMaxSize() + .background(gradientBrush), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Animasi Logo dengan efek scale + Image( + // Kode ini sekarang akan berfungsi karena import R sudah ditambahkan + painter = painterResource(id = R.drawable.ic_launcher_foreground), + contentDescription = "Logo Aplikasi", + modifier = Modifier + .size(120.dp) + .scale(scale) + ) + Text(text = "Selamat Datang", style = MaterialTheme.typography.headlineMedium) + Spacer(modifier = Modifier.height(32.dp)) + + // 2. Animasi Goyang saat Error + val cardScale = animateFloatAsState( + targetValue = if (shakeState) 1.05f else 1f, + animationSpec = tween(100), label = "card_shake" + ).value + + AnimatedVisibility( + visible = isFormVisible, + enter = fadeIn(animationSpec = tween(1000, delayMillis = 500)) + + slideInVertically( + initialOffsetY = { it / 2 }, + animationSpec = tween(1000, delayMillis = 500) + ) + ) { + Card( + shape = RoundedCornerShape(16.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 8.dp), + modifier = Modifier + .fillMaxWidth() + .scale(cardScale) // Terapkan animasi goyang + ) { + Column(modifier = Modifier.padding(16.dp)) { + OutlinedTextField( + value = username, + onValueChange = { + username = it + usernameError = null // Hapus error saat user mulai mengetik + }, + label = { Text("Username") }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + isError = usernameError != null, // Tampilkan error + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) }) + ) + // 1. Tampilkan pesan error di bawah field + if (usernameError != null) { + Text( + text = usernameError!!, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(start = 16.dp, top = 4.dp) + ) + } + Spacer(modifier = Modifier.height(if (usernameError != null) 8.dp else 16.dp)) + + OutlinedTextField( + value = password, + onValueChange = { + password = it + passwordError = null // Hapus error saat user mulai mengetik + }, + label = { Text("Password") }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + isError = passwordError != null, // Tampilkan error + visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), + trailingIcon = { + val description = if (passwordVisible) "Sembunyikan password" else "Tampilkan password" + IconButton(onClick = { passwordVisible = !passwordVisible }) { + } + } + ) + // Tampilkan pesan error di bawah field + if (passwordError != null) { + Text( + text = passwordError!!, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(start = 16.dp, top = 4.dp) + ) + } + } + } + } + Spacer(modifier = Modifier.height(32.dp)) + + Button( + onClick = { + var hasError = false + if (username.isBlank()) { + usernameError = "Username tidak boleh kosong" + hasError = true + } + if (password.isBlank()) { + passwordError = "Password tidak boleh kosong" + hasError = true + } + if (hasError) { + // Jalankan animasi goyang + coroutineScope.launch { + shakeState = true + delay(100) + shakeState = false + } + } else { + isLoading = true + } + }, + modifier = Modifier + .fillMaxWidth() + .height(50.dp), + enabled = !isLoading + ) { + if (isLoading) { + CircularProgressIndicator(modifier = Modifier.size(24.dp), color = MaterialTheme.colorScheme.onPrimary) + } else { + Text("Login") + } + } + + if (isLoading) { + LaunchedEffect(Unit) { + delay(1500) + navController.navigate("main") { + popUpTo("login") { inclusive = true } + } + } + } + } + } +}