Initial commit

This commit is contained in:
PhongMacbook
2025-12-22 18:05:45 +07:00
commit ab2856c82c
28 changed files with 1164 additions and 0 deletions

60
app/build.gradle.kts Normal file
View File

@@ -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)
}

6
app/proguard-rules.pro vendored Normal file
View File

@@ -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

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.CakeApp"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.CakeApp">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -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)
}
}
}
}
}

View File

@@ -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 }
}

View File

@@ -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()
}
)
}
}
}

View File

@@ -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
}
}

View File

@@ -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
)
}
}
}
}

View File

@@ -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)

View File

@@ -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
)
}

View File

@@ -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
)
)

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- Cake base -->
<path
android:fillColor="#8B4513"
android:pathData="M24,70 L84,70 L84,85 Q54,95 24,85 Z"/>
<!-- Cake middle layer -->
<path
android:fillColor="#FFC0CB"
android:pathData="M27,55 L81,55 L84,70 L24,70 Z"/>
<!-- Cake top layer -->
<path
android:fillColor="#FFFFFF"
android:pathData="M30,40 L78,40 L81,55 L27,55 Z"/>
<!-- Cherry on top -->
<path
android:fillColor="#FF0000"
android:pathData="M54,32 m-8,0 a8,8 0 1,1 16,0 a8,8 0 1,1 -16,0"/>
<!-- Candle -->
<path
android:fillColor="#FFD700"
android:pathData="M52,25 L56,25 L56,40 L52,40 Z"/>
<!-- Flame -->
<path
android:fillColor="#FF4500"
android:pathData="M54,18 Q58,22 54,25 Q50,22 54,18"/>
</vector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FF69B4</color>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Cake App</string>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.CakeApp" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
</full-backup-content>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
</cloud-backup>
<device-transfer>
</device-transfer>
</data-extraction-rules>