From 6120173402068071fdf5ff75821f7346cf894c69 Mon Sep 17 00:00:00 2001 From: Indris Alpasela <202310715200@mhs.ubharajaya.ac.id> Date: Fri, 7 Nov 2025 20:21:29 +0700 Subject: [PATCH] first commit --- README.md | 54 +- .../java/com/example/tiptime/MainActivity.kt | 820 ++++++++++++++---- 2 files changed, 720 insertions(+), 154 deletions(-) diff --git a/README.md b/README.md index 08d4aa4..12ae6a8 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,50 @@ -Kalkulator BMI -=============== +# Kalkulator BMI -Silahkan kembangkan aplikasi ini untuk melakukan perhitungan BMI +Aplikasi Android sederhana untuk menghitung Indeks Massa Tubuh (IMT) atau *Body Mass Index* (BMI). Aplikasi ini dibuat sebagai bagian dari tugas atau latihan pengembangan aplikasi Android. -Petunjuk lebih detil dapat dibaca di -https://docs.google.com/document/d/1iGiC0Bg3Bdcd2Maq45TYkCDUkZ5Ql51E/edit?rtpof=true +## Deskripsi -Starter dimodifikasi dan terinspirasi dari: -https://developer.android.com/codelabs/basic-android-compose-calculate-tip#0 \ No newline at end of file +*Body Mass Index* (BMI) atau Indeks Massa Tubuh (IMT) adalah angka yang menjadi standar penilaian untuk menentukan apakah berat badan Anda tergolong normal, kurang, berlebih, atau obesitas. [21] Perhitungan ini didasarkan pada perbandingan antara berat dan tinggi badan. [21] Aplikasi ini menyediakan antarmuka yang mudah digunakan untuk memasukkan data dan melihat hasilnya secara langsung. + +Aplikasi ini bertujuan untuk: +* Menyediakan alat praktis bagi pengguna untuk memantau berat badan. +* Membantu pengguna memahami kategori berat badan mereka (kurang, normal, berlebih). +* Menjadi proyek latihan untuk pengembangan aplikasi Android menggunakan teknologi modern. + +## Fitur + +* **Input Data Pengguna:** Memasukkan berat badan (kg) dan tinggi badan (cm). +* **Perhitungan BMI:** Menghitung skor BMI secara otomatis berdasarkan data yang dimasukkan. +* **Tampilan Hasil:** Menampilkan skor BMI beserta kategori berat badan (contoh: Kurus, Normal, Gemuk). +* **Antarmuka Intuitif:** Desain yang simpel dan mudah digunakan. + +## Teknologi + +Proyek ini dibangun menggunakan: +* **Kotlin:** Bahasa pemrograman yang direkomendasikan Google untuk pengembangan Android. [8] +* **Jetpack Compose:** *Toolkit* modern dari Google untuk membangun UI Android secara deklaratif, yang memungkinkan pembuatan antarmuka pengguna dengan kode yang lebih ringkas dan efisien. [2, 6, 8] +* **Android Studio:** Lingkungan pengembangan terintegrasi (IDE) resmi untuk pengembangan aplikasi Android. [6] +* **Material Design 3:** Implementasi sistem desain Google untuk memberikan tampilan dan nuansa yang konsisten pada aplikasi. [6] + +## Petunjuk Pengembangan + +Petunjuk lebih detail mengenai pengembangan dan fitur yang harus diimplementasikan dapat dibaca di dokumen berikut: +[Petunjuk Pengerjaan Proyek](https://docs.google.com/document/d/1iGiC0Bg3Bdcd2Maq45TYkCDUkZ5Ql51E/edit?rtpof=true) + +## Cara Berkontribusi + +Kontribusi dari Anda sangat diharapkan! Jika Anda ingin berkontribusi pada proyek ini, silakan ikuti langkah-langkah berikut: +1. **Fork** repositori ini ke akun GitHub Anda. [1] +2. **Clone** repositori yang sudah di-fork ke mesin lokal Anda. [1] +3. Buat **branch** baru untuk setiap fitur atau perbaikan yang akan Anda kerjakan (`git checkout -b nama-fitur-anda`). [4] +4. Lakukan perubahan pada kode. +5. **Commit** perubahan Anda dengan pesan yang jelas dan deskriptif (`git commit -m 'Menambahkan fitur X'`). [4] +6. **Push** perubahan ke branch Anda di repositori fork (`git push origin nama-fitur-anda`). [4] +7. Buat **Pull Request** dari branch Anda ke branch `main` repositori ini. [3] + +Pastikan untuk membaca aturan kontribusi (jika ada) sebelum memulai. [1] + +## Inspirasi + +Proyek starter ini dimodifikasi dan terinspirasi dari codelab resmi Android: +[Basic Android Compose - Calculate Tip](https://developer.android.com/codelabs/basic-android-compose-calculate-tip#0) diff --git a/app/src/main/java/com/example/tiptime/MainActivity.kt b/app/src/main/java/com/example/tiptime/MainActivity.kt index d0fdd80..bdc2b90 100644 --- a/app/src/main/java/com/example/tiptime/MainActivity.kt +++ b/app/src/main/java/com/example/tiptime/MainActivity.kt @@ -1,18 +1,15 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * 🌃 Cyberpunk BMI Calculator App + * Developed with ❤ by INDRIS ALPASELA + * NPM: [202310715200] + * Nama: INDRIS ALPASELA * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * ⚡ Modern BMI calculator with futuristic Cyberpunk Neon theme! */ + package com.example.tiptime import android.os.Bundle @@ -20,195 +17,724 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawingPadding -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.* +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.* import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.example.tiptime.ui.theme.TipTimeTheme -import java.text.NumberFormat +import kotlin.math.pow +import kotlin.math.sin +// ---------------- CYBERPUNK NEON COLORS ---------------- +val NeonPink = Color(0xFFFF006E) +val NeonCyan = Color(0xFF00F5FF) +val NeonPurple = Color(0xFF8B00FF) +val NeonYellow = Color(0xFFFFFB00) +val CyberBlack = Color(0xFF0A0E27) +val CyberDarkBlue = Color(0xFF1A1F3A) +val NeonGreen = Color(0xFF39FF14) +val NeonOrange = Color(0xFFFF3C00) + +// ---------------- MAIN ACTIVITY ---------------- class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) setContent { TipTimeTheme { - Surface( - modifier = Modifier.fillMaxSize(), - ) { - TipTimeLayout() + Surface(modifier = Modifier.fillMaxSize()) { + BMIApp() } } } } } +// ---------------- APP NAVIGATION ---------------- @Composable -fun TipTimeLayout() { - var amountInput by remember { mutableStateOf("") } - var tipInput by remember { mutableStateOf("") } - var roundUp by remember { mutableStateOf(false) } +fun BMIApp() { + var currentScreen by remember { mutableStateOf("home") } - val BmiHeight = amountInput.toDoubleOrNull() ?: 0.0 - val BmiWeight = tipInput.toDoubleOrNull() ?: 0.0 - val bmi = calculateBMI(BmiHeight, BmiWeight, roundUp) - val category = calculateBMICategory(BmiHeight, BmiWeight, roundUp) + Crossfade(targetState = currentScreen, label = "screen_transition") { screen -> + when (screen) { + "home" -> HomeScreen(onNavigateToCalculator = { currentScreen = "calculator" }) + "calculator" -> BMICalculatorScreen(onNavigateBack = { currentScreen = "home" }) + } + } +} - Column( +// ---------------- HOME SCREEN WITH ANIMATED NEON EFFECT ---------------- +@Composable +fun HomeScreen(onNavigateToCalculator: () -> Unit) { + // Animasi untuk efek neon berkedip + val infiniteTransition = rememberInfiniteTransition(label = "neon_pulse") + val glowAlpha by infiniteTransition.animateFloat( + initialValue = 0.3f, + targetValue = 0.8f, + animationSpec = infiniteRepeatable( + animation = tween(1500, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse + ), + label = "glow_animation" + ) + + Box( modifier = Modifier - .statusBarsPadding() - .padding(horizontal = 40.dp) - .verticalScroll(rememberScrollState()) - .safeDrawingPadding(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + .fillMaxSize() + .background( + brush = Brush.verticalGradient( + colors = listOf(CyberBlack, CyberDarkBlue, Color(0xFF0D1B2A)) + ) + ) ) { - Text( - text = stringResource(R.string.calculate_tip), - modifier = Modifier - .padding(bottom = 16.dp, top = 40.dp) - .align(alignment = Alignment.Start) - ) - EditNumberField( - label = R.string.height, - leadingIcon = R.drawable.number, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Number, - imeAction = ImeAction.Next - ), - value = amountInput, - onValueChanged = { amountInput = it }, - modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth(), - ) - EditNumberField( - label = R.string.weight, - leadingIcon = R.drawable.number, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Number, - imeAction = ImeAction.Done - ), - value = tipInput, - onValueChanged = { tipInput = it }, - modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth(), - ) - RoundTheTipRow( - roundUp = roundUp, - onRoundUpChanged = { roundUp = it }, - modifier = Modifier.padding(bottom = 32.dp) - ) - Text( - text = stringResource(R.string.bmi_calculation, bmi), - style = MaterialTheme.typography.displaySmall - ) - Text( - text = stringResource(R.string.bmi_category, category), - style = MaterialTheme.typography.displaySmall - ) + // Grid lines untuk efek cyberpunk + Canvas(modifier = Modifier.fillMaxSize()) { + val gridSize = 60f + // Vertical lines + var x = 0f + while (x < size.width) { + drawLine( + color = NeonCyan.copy(alpha = 0.1f), + start = Offset(x, 0f), + end = Offset(x, size.height), + strokeWidth = 1f + ) + x += gridSize + } + // Horizontal lines + var y = 0f + while (y < size.height) { + drawLine( + color = NeonPink.copy(alpha = 0.1f), + start = Offset(0f, y), + end = Offset(size.width, y), + strokeWidth = 1f + ) + y += gridSize + } - Spacer(modifier = Modifier.height(150.dp)) + // Neon circles dengan efek glow + drawCircle( + color = NeonPink.copy(alpha = glowAlpha * 0.2f), + radius = 200f, + center = Offset(x = size.width * 0.2f, y = size.height * 0.3f) + ) + drawCircle( + color = NeonCyan.copy(alpha = glowAlpha * 0.15f), + radius = 250f, + center = Offset(x = size.width * 0.8f, y = size.height * 0.6f) + ) + drawCircle( + color = NeonPurple.copy(alpha = glowAlpha * 0.18f), + radius = 180f, + center = Offset(x = size.width * 0.5f, y = size.height * 0.8f) + ) + } + + Column( + modifier = Modifier + .fillMaxSize() + .statusBarsPadding() + .padding(32.dp) + .safeDrawingPadding(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + // Logo/Icon section dengan border neon + Box( + modifier = Modifier + .size(80.dp) + .background( + brush = Brush.linearGradient( + colors = listOf(NeonPink, NeonCyan) + ), + shape = RoundedCornerShape(16.dp) + ) + .padding(2.dp) + .background(CyberBlack, RoundedCornerShape(14.dp)), + contentAlignment = Alignment.Center + ) { + Text( + text = "⚡", + fontSize = 40.sp, + color = NeonCyan + ) + } + + Spacer(Modifier.height(24.dp)) + + Text( + text = "CYBER BMI", + style = MaterialTheme.typography.displayLarge.copy( + fontWeight = FontWeight.Black, + brush = Brush.linearGradient( + colors = listOf(NeonPink, NeonCyan, NeonPurple) + ) + ), + textAlign = TextAlign.Center + ) + + Text( + text = "// CALCULATOR_V2.0", + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.Light, + letterSpacing = 4.sp + ), + color = NeonCyan.copy(alpha = 0.7f), + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 8.dp) + ) + + Text( + text = "< Developed by Faris Naufal />", + style = MaterialTheme.typography.bodySmall.copy( + fontWeight = FontWeight.Light + ), + color = Color.White.copy(alpha = 0.5f), + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 4.dp) + ) + + Spacer(Modifier.height(16.dp)) + + // Deskripsi dengan border cyberpunk + Card( + colors = CardDefaults.cardColors( + containerColor = CyberDarkBlue.copy(alpha = 0.6f) + ), + shape = RoundedCornerShape(12.dp), + modifier = Modifier.padding(horizontal = 16.dp) + ) { + Text( + text = "Hitung Body Mass Index Anda dengan teknologi futuristik", + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + color = Color.White.copy(alpha = 0.9f), + modifier = Modifier.padding(16.dp) + ) + } + + Spacer(Modifier.height(48.dp)) + + // Button dengan efek neon gradient + Button( + onClick = onNavigateToCalculator, + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.Transparent + ), + contentPadding = PaddingValues(0.dp), + shape = RoundedCornerShape(12.dp) + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background( + brush = Brush.horizontalGradient( + colors = listOf(NeonPink, NeonPurple, NeonCyan) + ) + ), + contentAlignment = Alignment.Center + ) { + Text( + "⚡ MULAI SCANNING", + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.Bold, + letterSpacing = 2.sp + ), + color = Color.White + ) + } + } + } + } +} + +// ---------------- BMI CALCULATOR SCREEN ---------------- +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BMICalculatorScreen(onNavigateBack: () -> Unit) { + var heightInput by remember { mutableStateOf("") } + var weightInput by remember { mutableStateOf("") } + var useUSUnits by remember { mutableStateOf(false) } + + // Validasi input untuk mencegah nilai yang tidak wajar + val height = heightInput.toDoubleOrNull() ?: 0.0 + val weight = weightInput.toDoubleOrNull() ?: 0.0 + + // Validasi range yang wajar + val isValidHeight = if (useUSUnits) height in 20.0..96.0 else height in 50.0..250.0 + val isValidWeight = if (useUSUnits) weight in 20.0..1000.0 else weight in 10.0..500.0 + + val bmi = calculateBMI(height, weight, useUSUnits) + val category = calculateBMICategory(height, weight, useUSUnits) + + Scaffold( + topBar = { + TopAppBar( + title = { + Text( + "⚡ CYBER BMI SCANNER", + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.Bold, + letterSpacing = 1.sp + ) + ) + }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon( + Icons.Default.ArrowBack, + contentDescription = "Kembali", + tint = NeonCyan + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = CyberBlack, + titleContentColor = NeonPink + ) + ) + } + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .background( + brush = Brush.verticalGradient( + colors = listOf(CyberBlack, CyberDarkBlue) + ) + ) + .padding(innerPadding) + .padding(horizontal = 24.dp) + .verticalScroll(rememberScrollState()) + .safeDrawingPadding(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Spacer(modifier = Modifier.height(16.dp)) + + // Unit Switch dengan desain cyberpunk + UnitSwitchRow( + useUSUnits = useUSUnits, + onUnitChanged = { useUSUnits = it }, + modifier = Modifier.padding(bottom = 24.dp) + ) + + // Input fields dengan neon border + CyberTextField( + label = if (useUSUnits) "HEIGHT (inches) ⚡" else "TINGGI (cm) ⚡", + leadingIcon = R.drawable.number, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Next + ), + value = heightInput, + onValueChanged = { heightInput = it }, + isError = height > 0 && !isValidHeight, + errorMessage = if (useUSUnits) "Range: 20-96 inches" else "Range: 50-250 cm", + modifier = Modifier.fillMaxWidth().padding(bottom = 20.dp) + ) + + CyberTextField( + label = if (useUSUnits) "WEIGHT (lbs) ⚡" else "BERAT (kg) ⚡", + leadingIcon = R.drawable.number, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done + ), + value = weightInput, + onValueChanged = { weightInput = it }, + isError = weight > 0 && !isValidWeight, + errorMessage = if (useUSUnits) "Range: 20-1000 lbs" else "Range: 10-500 kg", + modifier = Modifier.fillMaxWidth().padding(bottom = 32.dp) + ) + + // Tampilkan hasil hanya jika input valid + if (height > 0 && weight > 0) { + if (isValidHeight && isValidWeight) { + // Card hasil BMI dengan efek neon + Card( + colors = CardDefaults.cardColors( + containerColor = CyberDarkBlue.copy(alpha = 0.8f) + ), + shape = RoundedCornerShape(20.dp), + modifier = Modifier + .fillMaxWidth() + .background( + brush = Brush.linearGradient( + colors = listOf( + NeonPink.copy(alpha = 0.3f), + NeonCyan.copy(alpha = 0.3f) + ) + ), + shape = RoundedCornerShape(20.dp) + ) + ) { + Column( + modifier = Modifier.padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "// SCAN RESULT", + style = MaterialTheme.typography.titleSmall.copy( + letterSpacing = 3.sp + ), + color = NeonCyan.copy(alpha = 0.7f) + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = bmi, + style = MaterialTheme.typography.displayLarge.copy( + fontWeight = FontWeight.Black, + brush = Brush.linearGradient( + colors = listOf( + getBMICategoryColor(category), + getBMICategoryColor(category).copy(alpha = 0.7f) + ) + ) + ) + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = category.uppercase(), + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.Bold, + letterSpacing = 2.sp + ), + color = getBMICategoryColor(category) + ) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + BMICategoryTable() + } else { + // Peringatan untuk input tidak valid + Card( + colors = CardDefaults.cardColors( + containerColor = NeonOrange.copy(alpha = 0.2f) + ), + shape = RoundedCornerShape(12.dp), + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "⚠ INPUT TIDAK VALID\nMasukkan nilai yang wajar", + style = MaterialTheme.typography.bodyLarge.copy( + fontWeight = FontWeight.SemiBold + ), + color = NeonOrange, + textAlign = TextAlign.Center, + modifier = Modifier.padding(16.dp) + ) + } + } + } + + Spacer(modifier = Modifier.height(24.dp)) + } + } +} + +// ---------------- BMI CATEGORY TABLE WITH CYBERPUNK STYLE ---------------- +@Composable +fun BMICategoryTable() { + Card( + colors = CardDefaults.cardColors( + containerColor = CyberDarkBlue.copy(alpha = 0.7f) + ), + shape = RoundedCornerShape(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier.padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "// BMI CATEGORIES", + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.Bold, + letterSpacing = 2.sp + ), + color = NeonCyan, + modifier = Modifier.padding(bottom = 12.dp) + ) + + Divider( + color = NeonPink.copy(alpha = 0.5f), + thickness = 2.dp, + modifier = Modifier.padding(bottom = 12.dp) + ) + + BMICategoryRow("UNDERWEIGHT", "< 18.5", Color(0xFF00D9FF)) + Spacer(modifier = Modifier.height(8.dp)) + BMICategoryRow("NORMAL", "18.5 – 24.9", NeonGreen) + Spacer(modifier = Modifier.height(8.dp)) + BMICategoryRow("OVERWEIGHT", "25 – 29.9", NeonYellow) + Spacer(modifier = Modifier.height(8.dp)) + BMICategoryRow("OBESE", "≥ 30", NeonOrange) + } } } @Composable -fun EditNumberField( - @StringRes label: Int, +fun BMICategoryRow(label: String, range: String, color: Color) { + Row( + modifier = Modifier + .fillMaxWidth() + .background( + brush = Brush.horizontalGradient( + colors = listOf( + color.copy(alpha = 0.3f), + color.copy(alpha = 0.1f) + ) + ), + shape = RoundedCornerShape(8.dp) + ) + .padding(12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = label, + color = color, + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + letterSpacing = 1.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = range, + color = color, + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold + ) + } +} + +// ---------------- CUSTOM CYBERPUNK TEXT FIELD ---------------- +@Composable +fun CyberTextField( + label: String, @DrawableRes leadingIcon: Int, keyboardOptions: KeyboardOptions, value: String, onValueChanged: (String) -> Unit, + isError: Boolean = false, + errorMessage: String = "", modifier: Modifier = Modifier ) { - TextField( - value = value, - singleLine = true, - leadingIcon = { Icon(painter = painterResource(id = leadingIcon), null) }, - modifier = modifier, - onValueChange = onValueChanged, - label = { Text(stringResource(label)) }, - keyboardOptions = keyboardOptions - ) + Column(modifier = modifier) { + TextField( + value = value, + onValueChange = onValueChanged, + singleLine = true, + leadingIcon = { + Icon( + painterResource(id = leadingIcon), + contentDescription = null, + tint = if (isError) NeonOrange else NeonCyan + ) + }, + label = { + Text( + label, + color = if (isError) NeonOrange else NeonPink, + style = MaterialTheme.typography.bodyMedium.copy( + letterSpacing = 1.sp + ) + ) + }, + placeholder = { + Text( + "Enter value...", + color = Color.Gray.copy(alpha = 0.5f) + ) + }, + keyboardOptions = keyboardOptions, + isError = isError, + colors = TextFieldDefaults.colors( + focusedContainerColor = CyberDarkBlue.copy(alpha = 0.6f), + unfocusedContainerColor = CyberDarkBlue.copy(alpha = 0.4f), + focusedTextColor = Color.White, + unfocusedTextColor = Color.White, + focusedIndicatorColor = if (isError) NeonOrange else NeonCyan, + unfocusedIndicatorColor = if (isError) NeonOrange.copy(alpha = 0.5f) else NeonPurple.copy(alpha = 0.5f), + errorIndicatorColor = NeonOrange, + errorContainerColor = CyberDarkBlue.copy(alpha = 0.6f), + cursorColor = NeonCyan + ), + shape = RoundedCornerShape(12.dp), + modifier = Modifier.fillMaxWidth() + ) + + if (isError && errorMessage.isNotEmpty()) { + Text( + text = errorMessage, + color = NeonOrange, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(start = 16.dp, top = 4.dp) + ) + } + } } +// ---------------- UNIT SWITCH WITH CYBERPUNK STYLE ---------------- @Composable -fun RoundTheTipRow( - roundUp: Boolean, - onRoundUpChanged: (Boolean) -> Unit, +fun UnitSwitchRow( + useUSUnits: Boolean, + onUnitChanged: (Boolean) -> Unit, modifier: Modifier = Modifier ) { - Row( - modifier = modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically + Card( + colors = CardDefaults.cardColors( + containerColor = CyberDarkBlue.copy(alpha = 0.6f) + ), + shape = RoundedCornerShape(12.dp), + modifier = modifier.fillMaxWidth() ) { - Text(text = stringResource(R.string.use_usc)) - Switch( + Row( modifier = Modifier .fillMaxWidth() - .wrapContentWidth(Alignment.End), - checked = roundUp, - onCheckedChange = onRoundUpChanged - ) + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column { + Text( + text = if (useUSUnits) "US UNITS" else "METRIC UNITS", + color = NeonCyan, + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.Bold, + letterSpacing = 1.sp + ) + ) + Text( + text = if (useUSUnits) "inches, lbs" else "cm, kg", + color = Color.White.copy(alpha = 0.6f), + style = MaterialTheme.typography.bodySmall + ) + } + Switch( + checked = useUSUnits, + onCheckedChange = onUnitChanged, + colors = SwitchDefaults.colors( + checkedThumbColor = NeonCyan, + checkedTrackColor = NeonPink.copy(alpha = 0.5f), + uncheckedThumbColor = Color.Gray, + uncheckedTrackColor = CyberBlack + ) + ) + } } } -/** - * Calculates the BMI - * - * Catatan: tambahkan unit test untuk kalkulasi BMI ini - */ -private fun calculateBMI(BmiHeight: Double, BmiWeight: Double = 15.0, roundUp: Boolean): String { - var bmi = BmiWeight / 100 * BmiHeight - if (roundUp) { - bmi = kotlin.math.ceil(bmi) - } - return NumberFormat.getNumberInstance().format(bmi) -} -/** - * Calculates the BMI Category - * - * Catatan: tambahkan unit test untuk kalkulasi BMI ini - */ +// ---------------- LOGIC & UTILITIES ---------------- -private fun calculateBMICategory(BmiHeight: Double, BmiWeight: Double = 15.0, roundUp: Boolean): String { - var bmi = BmiWeight / 100 * BmiHeight - if (roundUp) { - bmi = kotlin.math.ceil(bmi) - } - return NumberFormat.getNumberInstance().format(bmi) +/** + * Menentukan warna kategori BMI berdasarkan status kesehatan + * @param category String kategori BMI + * @return Color warna yang sesuai dengan kategori + */ +@Composable +fun getBMICategoryColor(category: String): Color = when { + category.contains("Underweight", true) || category.contains("Kurus", true) -> Color(0xFF00D9FF) + category.contains("Normal", true) -> NeonGreen + category.contains("Overweight", true) || category.contains("Gemuk", true) -> NeonYellow + category.contains("Obese", true) || category.contains("Obesitas", true) -> NeonOrange + else -> Color.White } + +/** + * Menghitung nilai BMI berdasarkan tinggi dan berat + * Formula Metrik: BMI = weight (kg) / (height (m))^2 + * Formula US: BMI = (weight (lbs) / (height (inches))^2) * 703 + * + * @param height Tinggi badan (cm untuk metrik, inches untuk US) + * @param weight Berat badan (kg untuk metrik, lbs untuk US) + * @param useUSUnits Boolean untuk menentukan sistem satuan + * @return String nilai BMI dengan 1 desimal + */ +private fun calculateBMI(height: Double, weight: Double, useUSUnits: Boolean): String { + // Validasi input tidak boleh nol atau negatif + if (height <= 0 || weight <= 0) return "-" + + val bmi = if (useUSUnits) { + // Formula US: (weight / height^2) * 703 + (weight / height.pow(2)) * 703 + } else { + // Formula Metrik: weight / (height in meters)^2 + weight / (height / 100).pow(2) + } + + // Format dengan 1 angka desimal + return String.format("%.1f", bmi) +} + +/** + * Menentukan kategori BMI berdasarkan standar WHO + * - Underweight: BMI < 18.5 + * - Normal: BMI 18.5-24.9 + * - Overweight: BMI 25-29.9 + * - Obese: BMI >= 30 + * + * @param height Tinggi badan + * @param weight Berat badan + * @param useUSUnits Boolean sistem satuan + * @return String kategori BMI + */ +private fun calculateBMICategory(height: Double, weight: Double, useUSUnits: Boolean): String { + if (height <= 0 || weight <= 0) return "-" + + val bmi = if (useUSUnits) { + (weight / height.pow(2)) * 703 + } else { + weight / (height / 100).pow(2) + } + + return when { + bmi < 18.5 -> "Underweight (Kurus)" + bmi < 25 -> "Normal" + bmi < 30 -> "Overweight (Gemuk)" + else -> "Obese (Obesitas)" + } +} + +// ---------------- PREVIEWS ---------------- @Preview(showBackground = true) @Composable -fun TipTimeLayoutPreview() { - TipTimeTheme { - TipTimeLayout() - } +fun HomeScreenPreview() { + TipTimeTheme { HomeScreen(onNavigateToCalculator = {}) } +} + +@Preview(showBackground = true) +@Composable +fun BMICalculatorScreenPreview() { + TipTimeTheme { BMICalculatorScreen(onNavigateBack = {}) } } \ No newline at end of file