commit ab2856c82ce2572328cf2516bac6b24ac0d34f20 Author: PhongMacbook Date: Mon Dec 22 18:05:45 2025 +0700 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae9cd9b --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# OS / editor +.DS_Store +*.iml +.idea/ +.vscode/ + +# Local (do not commit machine-specific paths) +local.properties + +# Gradle +.gradle/ +build/ + +# Android/IntelliJ build outputs +**/build/ + +# APK/AAB artifacts +*.apk +*.aab + +# Logs +*.log + +# Keystore +*.jks +*.keystore + +# NDK +.externalNativeBuild/ +.cxx/ + +# Lint +lint-results*.xml +lint-results*.html +lint.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..5099cfc --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,60 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "com.example.cakeapp" + compileSdk = 34 + + defaultConfig { + applicationId = "com.example.cakeapp" + minSdk = 24 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + compose = true + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + implementation(libs.androidx.navigation.compose) + + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..b19b309 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,6 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d57718e --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + diff --git a/app/src/main/java/com/example/cakeapp/MainActivity.kt b/app/src/main/java/com/example/cakeapp/MainActivity.kt new file mode 100644 index 0000000..6197e5e --- /dev/null +++ b/app/src/main/java/com/example/cakeapp/MainActivity.kt @@ -0,0 +1,29 @@ +package com.example.cakeapp + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.ui.Modifier +import androidx.navigation.compose.rememberNavController +import com.example.cakeapp.navigation.CakeNavGraph +import com.example.cakeapp.ui.theme.CakeAppTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + CakeAppTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + val navController = rememberNavController() + CakeNavGraph(navController = navController) + } + } + } + } +} diff --git a/app/src/main/java/com/example/cakeapp/model/Cake.kt b/app/src/main/java/com/example/cakeapp/model/Cake.kt new file mode 100644 index 0000000..1937278 --- /dev/null +++ b/app/src/main/java/com/example/cakeapp/model/Cake.kt @@ -0,0 +1,19 @@ +package com.example.cakeapp.model + +data class Cake( + val id: Int, + val name: String, + val price: Double = 2.0, + val emoji: String +) + +object CakeData { + val cakes = listOf( + Cake(1, "Chocolate Cake", 2.0, "🍫"), + Cake(2, "Strawberry Cake", 2.0, "🍓"), + Cake(3, "Cheese Cake", 2.0, "🧀"), + Cake(4, "Matcha Cake", 2.0, "🍵") + ) + + fun getCakeById(id: Int): Cake? = cakes.find { it.id == id } +} diff --git a/app/src/main/java/com/example/cakeapp/navigation/CakeNavGraph.kt b/app/src/main/java/com/example/cakeapp/navigation/CakeNavGraph.kt new file mode 100644 index 0000000..60ec861 --- /dev/null +++ b/app/src/main/java/com/example/cakeapp/navigation/CakeNavGraph.kt @@ -0,0 +1,48 @@ +package com.example.cakeapp.navigation + +import androidx.compose.runtime.Composable +import androidx.navigation.NavHostController +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import com.example.cakeapp.screens.MenuScreen +import com.example.cakeapp.screens.OrderScreen + +object Routes { + const val MENU = "menu" + const val ORDER = "order/{cakeId}" + + fun orderRoute(cakeId: Int) = "order/$cakeId" +} + +@Composable +fun CakeNavGraph(navController: NavHostController) { + NavHost( + navController = navController, + startDestination = Routes.MENU + ) { + composable(Routes.MENU) { + MenuScreen( + onCakeSelected = { cakeId -> + navController.navigate(Routes.orderRoute(cakeId)) + } + ) + } + + composable( + route = Routes.ORDER, + arguments = listOf( + navArgument("cakeId") { type = NavType.IntType } + ) + ) { backStackEntry -> + val cakeId = backStackEntry.arguments?.getInt("cakeId") ?: 1 + OrderScreen( + cakeId = cakeId, + onBackToMenu = { + navController.popBackStack() + } + ) + } + } +} diff --git a/app/src/main/java/com/example/cakeapp/screens/MenuScreen.kt b/app/src/main/java/com/example/cakeapp/screens/MenuScreen.kt new file mode 100644 index 0000000..dedc17a --- /dev/null +++ b/app/src/main/java/com/example/cakeapp/screens/MenuScreen.kt @@ -0,0 +1,133 @@ +package com.example.cakeapp.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.cakeapp.model.Cake +import com.example.cakeapp.model.CakeData + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MenuScreen( + onCakeSelected: (Int) -> Unit +) { + Scaffold( + topBar = { + TopAppBar( + title = { + Text( + text = "🎂 Cake Menu", + fontWeight = FontWeight.Bold + ) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ) + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Chọn loại bánh yêu thích của bạn!", + fontSize = 18.sp, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(bottom = 24.dp) + ) + + LazyVerticalGrid( + columns = GridCells.Fixed(2), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + items(CakeData.cakes) { cake -> + CakeCard( + cake = cake, + onClick = { onCakeSelected(cake.id) } + ) + } + } + } + } +} + +@Composable +fun CakeCard( + cake: Cake, + onClick: () -> Unit +) { + Card( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clickable { onClick() }, + shape = RoundedCornerShape(16.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 8.dp), + colors = CardDefaults.cardColors( + containerColor = getCakeCardColor(cake.id) + ) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = cake.emoji, + fontSize = 48.sp + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = cake.name, + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + color = Color.White + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "$${cake.price.toInt()}", + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + } + } +} + +@Composable +fun getCakeCardColor(cakeId: Int): Color { + return when (cakeId) { + 1 -> Color(0xFF8B4513) // Chocolate - Brown + 2 -> Color(0xFFFF69B4) // Strawberry - Pink + 3 -> Color(0xFFFFA500) // Cheese - Orange + 4 -> Color(0xFF228B22) // Matcha - Green + else -> Color.Gray + } +} diff --git a/app/src/main/java/com/example/cakeapp/screens/OrderScreen.kt b/app/src/main/java/com/example/cakeapp/screens/OrderScreen.kt new file mode 100644 index 0000000..10f7d38 --- /dev/null +++ b/app/src/main/java/com/example/cakeapp/screens/OrderScreen.kt @@ -0,0 +1,211 @@ +package com.example.cakeapp.screens + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +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.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.cakeapp.model.CakeData + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun OrderScreen( + cakeId: Int, + onBackToMenu: () -> Unit +) { + val cake = CakeData.getCakeById(cakeId) + var quantity by remember { mutableStateOf("") } + var totalPrice by remember { mutableStateOf(0.0) } + + // Tính tổng giá khi quantity thay đổi + LaunchedEffect(quantity) { + val qty = quantity.toIntOrNull() ?: 0 + totalPrice = qty * (cake?.price ?: 0.0) + } + + Scaffold( + topBar = { + TopAppBar( + title = { + Text( + text = "🛒 Order", + fontWeight = FontWeight.Bold + ) + }, + navigationIcon = { + IconButton(onClick = onBackToMenu) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = "Back to Menu" + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ) + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + cake?.let { selectedCake -> + // Cake Info Card + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 8.dp), + colors = CardDefaults.cardColors( + containerColor = getCakeCardColor(selectedCake.id) + ) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = selectedCake.emoji, + fontSize = 80.sp + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = selectedCake.name, + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Giá: $${selectedCake.price.toInt()} / cái", + fontSize = 18.sp, + color = Color.White + ) + } + } + + Spacer(modifier = Modifier.height(32.dp)) + + // Quantity Input + Text( + text = "Nhập số lượng bánh:", + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(12.dp)) + + OutlinedTextField( + value = quantity, + onValueChange = { newValue -> + // Chỉ cho phép nhập số + if (newValue.isEmpty() || newValue.all { it.isDigit() }) { + quantity = newValue + } + }, + label = { Text("Số lượng") }, + placeholder = { Text("Nhập số lượng...") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + singleLine = true, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(12.dp) + ) + + Spacer(modifier = Modifier.height(32.dp)) + + // Total Price Display + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer + ) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Tổng tiền", + fontSize = 18.sp, + color = MaterialTheme.colorScheme.onSecondaryContainer + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "$${String.format("%.2f", totalPrice)}", + fontSize = 36.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary + ) + + if (quantity.isNotEmpty() && quantity.toIntOrNull() != null && quantity.toInt() > 0) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "${quantity} x $${selectedCake.price.toInt()} = $${String.format("%.2f", totalPrice)}", + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.7f) + ) + } + } + } + + Spacer(modifier = Modifier.weight(1f)) + + // Back to Menu Button + Button( + onClick = onBackToMenu, + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + shape = RoundedCornerShape(16.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary + ) + ) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Quay lại Menu", + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ) + } + } ?: run { + // Cake not found + Text( + text = "Không tìm thấy bánh!", + fontSize = 20.sp, + textAlign = TextAlign.Center + ) + } + } + } +} diff --git a/app/src/main/java/com/example/cakeapp/ui/theme/Color.kt b/app/src/main/java/com/example/cakeapp/ui/theme/Color.kt new file mode 100644 index 0000000..b544252 --- /dev/null +++ b/app/src/main/java/com/example/cakeapp/ui/theme/Color.kt @@ -0,0 +1,17 @@ +package com.example.cakeapp.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) + +// Custom Cake Colors +val ChocolateBrown = Color(0xFF8B4513) +val StrawberryPink = Color(0xFFFF69B4) +val CheeseOrange = Color(0xFFFFA500) +val MatchaGreen = Color(0xFF228B22) diff --git a/app/src/main/java/com/example/cakeapp/ui/theme/Theme.kt b/app/src/main/java/com/example/cakeapp/ui/theme/Theme.kt new file mode 100644 index 0000000..1a0757e --- /dev/null +++ b/app/src/main/java/com/example/cakeapp/ui/theme/Theme.kt @@ -0,0 +1,64 @@ +package com.example.cakeapp.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +private val DarkColorScheme = darkColorScheme( + primary = Pink80, + secondary = PurpleGrey80, + tertiary = Purple80, + primaryContainer = Color(0xFF4A3728) +) + +private val LightColorScheme = lightColorScheme( + primary = Color(0xFFE91E63), + secondary = PurpleGrey40, + tertiary = Pink40, + primaryContainer = Color(0xFFFFE4EC), + secondaryContainer = Color(0xFFFFF3E0), + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE) +) + +@Composable +fun CakeAppTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} diff --git a/app/src/main/java/com/example/cakeapp/ui/theme/Type.kt b/app/src/main/java/com/example/cakeapp/ui/theme/Type.kt new file mode 100644 index 0000000..068676e --- /dev/null +++ b/app/src/main/java/com/example/cakeapp/ui/theme/Type.kt @@ -0,0 +1,31 @@ +package com.example.cakeapp.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ), + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Bold, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) +) diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..1e5ffee --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..5ed0a2d --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..5ed0a2d --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..f11fb86 --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FF69B4 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..dc0d695 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Cake App + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..0891e36 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,4 @@ + + +