Skip to content

Instantly share code, notes, and snippets.

@webianks
Created November 28, 2025 06:47
Show Gist options
  • Select an option

  • Save webianks/10ddbafef0b7e9310ff0b0d0bac52abe to your computer and use it in GitHub Desktop.

Select an option

Save webianks/10ddbafef0b7e9310ff0b0d0bac52abe to your computer and use it in GitHub Desktop.
PL MiniApp - StockTracker Progress Indicator
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