Created
November 28, 2025 06:47
-
-
Save webianks/10ddbafef0b7e9310ff0b0d0bac52abe to your computer and use it in GitHub Desktop.
PL MiniApp - StockTracker Progress Indicator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package com.webianks.miniapps | |
| import android.graphics.Color | |
| import android.os.Bundle | |
| import androidx.activity.ComponentActivity | |
| import androidx.activity.SystemBarStyle | |
| import androidx.activity.compose.setContent | |
| import androidx.activity.enableEdgeToEdge | |
| import androidx.compose.animation.core.Animatable | |
| import androidx.compose.animation.core.FastOutSlowInEasing | |
| import androidx.compose.animation.core.animateFloatAsState | |
| import androidx.compose.animation.core.tween | |
| import androidx.compose.foundation.Image | |
| import androidx.compose.foundation.background | |
| import androidx.compose.foundation.border | |
| import androidx.compose.foundation.layout.Arrangement | |
| import androidx.compose.foundation.layout.Box | |
| 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.size | |
| import androidx.compose.foundation.layout.width | |
| import androidx.compose.foundation.shape.CircleShape | |
| import androidx.compose.foundation.shape.RoundedCornerShape | |
| import androidx.compose.material3.Button | |
| import androidx.compose.material3.ButtonDefaults | |
| import androidx.compose.material3.CircularProgressIndicator | |
| import androidx.compose.material3.HorizontalDivider | |
| import androidx.compose.material3.MaterialTheme | |
| import androidx.compose.material3.Scaffold | |
| import androidx.compose.material3.Text | |
| import androidx.compose.material3.Typography | |
| import androidx.compose.material3.lightColorScheme | |
| import androidx.compose.runtime.Composable | |
| import androidx.compose.runtime.LaunchedEffect | |
| import androidx.compose.runtime.getValue | |
| import androidx.compose.runtime.mutableIntStateOf | |
| import androidx.compose.runtime.remember | |
| import androidx.compose.runtime.setValue | |
| import androidx.compose.ui.Alignment | |
| import androidx.compose.ui.Modifier | |
| import androidx.compose.ui.draw.clip | |
| import androidx.compose.ui.draw.scale | |
| import androidx.compose.ui.graphics.Brush | |
| import androidx.compose.ui.graphics.StrokeCap | |
| import androidx.compose.ui.res.painterResource | |
| import androidx.compose.ui.text.TextStyle | |
| import androidx.compose.ui.text.font.Font | |
| import androidx.compose.ui.text.font.FontFamily | |
| import androidx.compose.ui.text.font.FontWeight | |
| import androidx.compose.ui.text.style.TextDecoration | |
| import androidx.compose.ui.text.style.TextOverflow | |
| import androidx.compose.ui.tooling.preview.Preview | |
| import androidx.compose.ui.graphics.Color as ComposeColor | |
| import androidx.compose.ui.unit.dp | |
| import androidx.compose.ui.unit.sp | |
| // Color section | |
| val Background = ComposeColor(0xFFF6F2ED) | |
| val Surface = ComposeColor(0xFFFFFFFF) | |
| val Outline = ComposeColor(0xFFDFDDDB) | |
| val TextPrimary = ComposeColor(0xFF211304) | |
| val TextSecondary = ComposeColor(0xFF6B5D4F) | |
| val TextDisabled = ComposeColor(0xFF9A9795) | |
| val TextAlt = ComposeColor(0xFFFFFFFF) | |
| val Discount = ComposeColor(0xFF7C1414) | |
| val NikeRed = ComposeColor(0xFFD32F2F) | |
| // Type section | |
| val HostGrotesk = FontFamily( | |
| Font(R.font.host_grotesk_medium, FontWeight.Normal), | |
| Font(R.font.host_grotesk_medium, FontWeight.Medium), | |
| Font(R.font.host_grotesk_semibold, FontWeight.SemiBold), | |
| Font(R.font.host_grotesk_semibold, FontWeight.Bold) | |
| ) | |
| val Typography = Typography( | |
| displayLarge = TextStyle( | |
| fontFamily = HostGrotesk, | |
| fontWeight = FontWeight.Bold, | |
| fontSize = 24.sp, | |
| lineHeight = 24.sp, | |
| letterSpacing = 0.sp | |
| ), | |
| bodyLarge = TextStyle( | |
| fontFamily = HostGrotesk, | |
| fontWeight = FontWeight.Medium, | |
| fontSize = 16.sp, | |
| lineHeight = 20.sp, | |
| letterSpacing = 0.sp | |
| ), | |
| bodyMedium = TextStyle( | |
| fontFamily = HostGrotesk, | |
| fontWeight = FontWeight.Normal, | |
| fontSize = 12.sp, | |
| lineHeight = 14.sp, | |
| letterSpacing = 0.sp | |
| ), | |
| labelMedium = TextStyle( | |
| fontFamily = HostGrotesk, | |
| fontWeight = FontWeight.Medium, | |
| fontSize = 13.sp, | |
| lineHeight = 16.sp, | |
| letterSpacing = 0.sp | |
| ), | |
| labelSmall = TextStyle( | |
| fontFamily = HostGrotesk, | |
| fontWeight = FontWeight.Medium, | |
| fontSize = 14.sp, | |
| lineHeight = 16.sp, | |
| letterSpacing = 0.sp | |
| ) | |
| ) | |
| // Theme section | |
| private val LightColorScheme = lightColorScheme( | |
| primary = TextPrimary, | |
| secondary = TextSecondary, | |
| background = Background, | |
| surface = Surface, | |
| onPrimary = TextAlt, | |
| onSecondary = TextAlt, | |
| onBackground = TextPrimary, | |
| onSurface = TextPrimary, | |
| ) | |
| @Composable | |
| fun MiniAppsTheme( | |
| content: @Composable () -> Unit | |
| ) { | |
| MaterialTheme( | |
| colorScheme = LightColorScheme, | |
| typography = Typography, | |
| content = content | |
| ) | |
| } | |
| class MainActivity : ComponentActivity() { | |
| override fun onCreate(savedInstanceState: Bundle?) { | |
| super.onCreate(savedInstanceState) | |
| enableEdgeToEdge( | |
| statusBarStyle = SystemBarStyle.light( | |
| Color.TRANSPARENT, Color.TRANSPARENT | |
| ), | |
| navigationBarStyle = SystemBarStyle.light( | |
| Color.TRANSPARENT, Color.TRANSPARENT | |
| ) | |
| ) | |
| setContent { | |
| MiniAppsTheme { | |
| CircularStockTrackerScreen() | |
| } | |
| } | |
| } | |
| } | |
| @Composable | |
| fun CircularStockTrackerScreen() { | |
| val maxStock = 12 | |
| var currentStock by remember { mutableIntStateOf(maxStock) } | |
| val isOutOfStock = currentStock == 0 | |
| Scaffold( | |
| modifier = Modifier.fillMaxSize(), | |
| containerColor = MaterialTheme.colorScheme.background | |
| ) { innerPadding -> | |
| Column( | |
| modifier = Modifier | |
| .padding(innerPadding) | |
| .fillMaxSize() | |
| .padding(16.dp), | |
| verticalArrangement = Arrangement.Center | |
| ) { | |
| Box { | |
| ProductImageHeader(isOutOfStock) | |
| Column( | |
| modifier = Modifier | |
| .padding(top = 270.dp) | |
| .clip(RoundedCornerShape(24.dp)) | |
| .background(MaterialTheme.colorScheme.surface) | |
| .padding(24.dp) | |
| ) { | |
| Row( | |
| modifier = Modifier.fillMaxWidth(), | |
| horizontalArrangement = Arrangement.SpaceBetween, | |
| verticalAlignment = Alignment.Top | |
| ) { | |
| Column(modifier = Modifier.weight(1f)) { | |
| Text( | |
| text = "Nike Air Zoom Pegasus 41", | |
| color = TextPrimary, | |
| style = MaterialTheme.typography.displayLarge.copy( | |
| fontSize = 20.sp | |
| ) | |
| ) | |
| Spacer(modifier = Modifier.height(2.dp)) | |
| Text( | |
| text = "Legendary running shoes with Air Zoom technology and ReactX cushioning for daily training and marathons.", | |
| maxLines = 3, | |
| overflow = TextOverflow.Ellipsis, | |
| style = MaterialTheme.typography.bodyMedium, | |
| color = TextDisabled | |
| ) | |
| } | |
| PricingRow() | |
| } | |
| Spacer(modifier = Modifier.height(12.dp)) | |
| Text( | |
| text = "Choose your size", | |
| color = TextDisabled, | |
| style = MaterialTheme.typography.bodyMedium | |
| ) | |
| Spacer(modifier = Modifier.height(4.dp)) | |
| SizeSelectionRow() | |
| Spacer(modifier = Modifier.height(16.dp)) | |
| HorizontalDivider(color = Outline) | |
| Spacer(modifier = Modifier.height(16.dp)) | |
| StockIndicatorSection( | |
| currentStock = currentStock, | |
| maxStock = maxStock | |
| ) | |
| Spacer(modifier = Modifier.height(12.dp)) | |
| BuyButton( | |
| isOutOfStock = isOutOfStock, | |
| onClick = { | |
| if (currentStock > 0) { | |
| currentStock-- | |
| } | |
| } | |
| ) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| @Composable | |
| fun ProductImageHeader(isOutOfStock: Boolean) { | |
| val gradientBrush = Brush.horizontalGradient( | |
| colors = if (isOutOfStock) listOf(Outline, Outline) | |
| else listOf(ComposeColor(0xFFD33F3F), ComposeColor(0xFF7C1414)) | |
| ) | |
| Box( | |
| modifier = Modifier | |
| .fillMaxWidth() | |
| .height(310.dp) | |
| .clip(RoundedCornerShape(24.dp)) | |
| .background(gradientBrush), | |
| contentAlignment = Alignment.Center | |
| ) { | |
| Image( | |
| painterResource(R.drawable.img), | |
| modifier = Modifier.fillMaxSize(), | |
| contentDescription = "Product Image", | |
| ) | |
| } | |
| } | |
| @Composable | |
| fun PricingRow() { | |
| Column(horizontalAlignment = Alignment.End) { | |
| Text( | |
| text = "$100", | |
| color = Discount, | |
| style = MaterialTheme.typography.displayLarge | |
| ) | |
| Text( | |
| text = "$160", | |
| color = TextSecondary, | |
| style = MaterialTheme.typography.labelSmall.copy( | |
| textDecoration = TextDecoration.LineThrough | |
| ) | |
| ) | |
| } | |
| } | |
| @Composable | |
| fun SizeSelectionRow() { | |
| val sizes = listOf("39", "40", "41", "42", "43", "44") | |
| val selectedSize = "42" | |
| Row( | |
| modifier = Modifier.fillMaxWidth(), | |
| horizontalArrangement = Arrangement.spacedBy(8.dp) | |
| ) { | |
| sizes.forEach { size -> | |
| val isSelected = size == selectedSize | |
| Box( | |
| modifier = Modifier | |
| .weight(1f) | |
| .height(32.dp) | |
| .clip(RoundedCornerShape(4.dp)) | |
| .background(if (isSelected) TextSecondary else ComposeColor.Transparent) | |
| .border( | |
| width = 1.dp, | |
| color = if (isSelected) TextSecondary else Outline, | |
| shape = RoundedCornerShape(4.dp) | |
| ), | |
| contentAlignment = Alignment.Center | |
| ) { | |
| Text( | |
| text = size, | |
| color = if (isSelected) ComposeColor.White else TextSecondary, | |
| style = MaterialTheme.typography.bodyLarge | |
| ) | |
| } | |
| } | |
| } | |
| } | |
| @Composable | |
| fun StockIndicatorSection(currentStock: Int, maxStock: Int) { | |
| val progressTarget = if (maxStock > 0) currentStock.toFloat() / maxStock.toFloat() else 0f | |
| val animatedProgress by animateFloatAsState( | |
| targetValue = progressTarget, | |
| animationSpec = tween(durationMillis = 600, easing = FastOutSlowInEasing), | |
| label = "Progress" | |
| ) | |
| val scale = remember { Animatable(1f) } | |
| LaunchedEffect(currentStock) { | |
| if (currentStock != maxStock) { | |
| scale.animateTo( | |
| targetValue = 1.3f, | |
| animationSpec = tween(durationMillis = 150) | |
| ) | |
| scale.animateTo( | |
| targetValue = 1f, | |
| animationSpec = tween(durationMillis = 150) | |
| ) | |
| } | |
| } | |
| Row( | |
| verticalAlignment = Alignment.CenterVertically, | |
| modifier = Modifier.fillMaxWidth() | |
| ) { | |
| Box( | |
| contentAlignment = Alignment.Center, | |
| modifier = Modifier | |
| .size(36.dp) | |
| .border(1.dp, Outline, CircleShape) | |
| ) { | |
| CircularProgressIndicator( | |
| progress = { 1f }, | |
| modifier = Modifier.fillMaxSize(), | |
| color = ComposeColor.Transparent, | |
| strokeWidth = 2.dp, | |
| trackColor = ComposeColor.Transparent, | |
| ) | |
| CircularProgressIndicator( | |
| progress = { animatedProgress }, | |
| modifier = Modifier.fillMaxSize(), | |
| color = Discount, | |
| strokeWidth = 4.dp, | |
| strokeCap = StrokeCap.Round, | |
| trackColor = ComposeColor.Transparent, | |
| ) | |
| Text( | |
| text = currentStock.toString(), | |
| modifier = Modifier.scale(scale.value), | |
| color = TextSecondary, | |
| style = MaterialTheme.typography.labelMedium | |
| ) | |
| } | |
| Spacer(modifier = Modifier.width(16.dp)) | |
| Text( | |
| text = "Remaining at discounted price", | |
| color = TextSecondary, | |
| style = MaterialTheme.typography.bodyMedium | |
| ) | |
| } | |
| } | |
| @Composable | |
| fun BuyButton(isOutOfStock: Boolean, onClick: () -> Unit) { | |
| Button( | |
| onClick = onClick, | |
| enabled = !isOutOfStock, | |
| modifier = Modifier | |
| .fillMaxWidth() | |
| .height(56.dp), | |
| colors = ButtonDefaults.buttonColors( | |
| containerColor = TextPrimary, | |
| disabledContainerColor = Outline, | |
| contentColor = ComposeColor.White, | |
| disabledContentColor = ComposeColor.White | |
| ), | |
| shape = RoundedCornerShape(12.dp) | |
| ) { | |
| Text( | |
| text = if (isOutOfStock) "Out of stock" else "Buy", | |
| style = MaterialTheme.typography.bodyLarge | |
| ) | |
| } | |
| } | |
| @Preview(showBackground = true, backgroundColor = 0xFFF6F2ED) | |
| @Composable | |
| fun PreviewApp() { | |
| MiniAppsTheme { | |
| CircularStockTrackerScreen() | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment