From 3942ba80668350b950d5ab7a709d0ac6c590213e Mon Sep 17 00:00:00 2001 From: Salman Ashraf Date: Wed, 18 Feb 2026 14:28:05 +0800 Subject: [PATCH 1/2] feat: US-0.9: Handled empty states and error design states --- .../sa/feature/cart/ui/CartScreenMockup.kt | 46 +- .../sa/feature/product/ui/HomeScreenMockup.kt | 46 +- .../feature/search/ui/SearchScreenMockup.kt | 31 +- .../java/com/sa/core/ui/component/States.kt | 396 ++++++++++++++++++ 4 files changed, 441 insertions(+), 78 deletions(-) create mode 100644 core/ui/src/main/java/com/sa/core/ui/component/States.kt diff --git a/core/feature/feature-cart/src/main/java/com/sa/feature/cart/ui/CartScreenMockup.kt b/core/feature/feature-cart/src/main/java/com/sa/feature/cart/ui/CartScreenMockup.kt index c0c18d1..79174b3 100644 --- a/core/feature/feature-cart/src/main/java/com/sa/feature/cart/ui/CartScreenMockup.kt +++ b/core/feature/feature-cart/src/main/java/com/sa/feature/cart/ui/CartScreenMockup.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.sa.core.ui.component.CartItemCard +import com.sa.core.ui.component.EmptyCartState import com.sa.core.ui.component.OrderSummary import com.sa.core.ui.component.PrimaryButton import com.sa.core.ui.component.SimpleTopAppBar @@ -134,50 +135,10 @@ fun CartScreenEmptyMockup() { ) { Column( modifier = Modifier - .fillMaxSize() - .padding(horizontal = Spacing.lg), - horizontalAlignment = Alignment.CenterHorizontally, + .fillMaxSize(), verticalArrangement = Arrangement.Center ) { - Box( - modifier = Modifier - .size(120.dp) - .clip(CircleShape) - .background(SurfaceVariant), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = Icons.Filled.ShoppingCart, - contentDescription = "Empty cart", - tint = TextSecondaryColor, - modifier = Modifier.size(48.dp) - ) - } - - Spacer(modifier = Modifier.height(Spacing.lg)) - - Text( - text = "Your cart is empty", - style = MaterialTheme.typography.titleLarge, - color = TextPrimaryColor, - fontWeight = FontWeight.Bold - ) - - Spacer(modifier = Modifier.height(Spacing.sm)) - - Text( - text = "Looks like you have not added anything yet.", - style = MaterialTheme.typography.bodyMedium, - color = TextSecondaryColor, - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(Spacing.lg)) - - PrimaryButton( - text = "Start Shopping", - onClick = { /* Navigate to home */ } - ) + EmptyCartState(onStartShopping = { /* Navigate to home */ }) } } } @@ -215,4 +176,3 @@ private data class CartMockItem( val price: Double, val quantity: Int ) - diff --git a/core/feature/feature-product/src/main/java/com/sa/feature/product/ui/HomeScreenMockup.kt b/core/feature/feature-product/src/main/java/com/sa/feature/product/ui/HomeScreenMockup.kt index 625a893..47bfafb 100644 --- a/core/feature/feature-product/src/main/java/com/sa/feature/product/ui/HomeScreenMockup.kt +++ b/core/feature/feature-product/src/main/java/com/sa/feature/product/ui/HomeScreenMockup.kt @@ -43,12 +43,32 @@ fun HomeScreenLightPreview() { } } +@Preview( + name = "Home Screen - Loading State", + showBackground = true, + widthDp = 448, + heightDp = 900 +) +@Composable +fun HomeScreenLoadingPreview() { + CommerceXTheme { + HomeScreenMockup( + showLoadingState = true, + showPullToRefreshIndicator = true, + pullToRefreshProgress = 0.85f + ) + } +} + @Composable fun HomeScreenMockup( onProductClick: () -> Unit = {}, onCartClick: () -> Unit = {}, onSearchClick: () -> Unit = {}, - onProfileClick: () -> Unit = {} + onProfileClick: () -> Unit = {}, + showLoadingState: Boolean = false, + showPullToRefreshIndicator: Boolean = false, + pullToRefreshProgress: Float = 0f ) { var selectedCategory by remember { mutableStateOf("All") } var cartItemCount by remember { mutableStateOf(3) } @@ -99,12 +119,24 @@ fun HomeScreenMockup( Spacer(modifier = Modifier.height(Spacing.lg)) - // Product Grid with staggered animation - ProductGridWithAnimation( - products = products, - modifier = Modifier.weight(1f), - onProductClick = onProductClick - ) + if (showPullToRefreshIndicator) { + PullToRefreshIndicator( + isRefreshing = false, + progress = pullToRefreshProgress, + modifier = Modifier.padding(horizontal = Spacing.lg) + ) + } + + if (showLoadingState) { + LoadingProductGridState(modifier = Modifier.weight(1f)) + } else { + // Product Grid with staggered animation + ProductGridWithAnimation( + products = products, + modifier = Modifier.weight(1f), + onProductClick = onProductClick + ) + } } // Fixed Bottom Navigation diff --git a/core/feature/feature-search/src/main/java/com/sa/feature/search/ui/SearchScreenMockup.kt b/core/feature/feature-search/src/main/java/com/sa/feature/search/ui/SearchScreenMockup.kt index bc50f19..529d30b 100644 --- a/core/feature/feature-search/src/main/java/com/sa/feature/search/ui/SearchScreenMockup.kt +++ b/core/feature/feature-search/src/main/java/com/sa/feature/search/ui/SearchScreenMockup.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.sa.core.ui.component.CategoryChip +import com.sa.core.ui.component.NoSearchResultsState import com.sa.core.ui.component.ProductCard import com.sa.core.ui.component.SearchBar import com.sa.core.ui.theme.* @@ -177,35 +178,10 @@ fun SearchScreenNoResultsMockup() { ) { Column( modifier = Modifier - .fillMaxSize() - .padding(horizontal = Spacing.lg), - horizontalAlignment = Alignment.CenterHorizontally, + .fillMaxSize(), verticalArrangement = Arrangement.Center ) { - Icon( - imageVector = Icons.Filled.Search, - contentDescription = "No results", - tint = TextSecondaryColor, - modifier = Modifier.size(64.dp) - ) - - Spacer(modifier = Modifier.height(Spacing.lg)) - - Text( - text = "No results found", - style = MaterialTheme.typography.titleLarge, - color = TextPrimaryColor, - fontWeight = FontWeight.Bold - ) - - Spacer(modifier = Modifier.height(Spacing.sm)) - - Text( - text = "Try a different keyword or check your spelling.", - style = MaterialTheme.typography.bodyMedium, - color = TextSecondaryColor, - textAlign = TextAlign.Center - ) + NoSearchResultsState(query = "iphone") } } } @@ -276,4 +252,3 @@ private data class MockSearchProduct( val rating: Double, val reviewCount: Int ) - diff --git a/core/ui/src/main/java/com/sa/core/ui/component/States.kt b/core/ui/src/main/java/com/sa/core/ui/component/States.kt new file mode 100644 index 0000000..78f130d --- /dev/null +++ b/core/ui/src/main/java/com/sa/core/ui/component/States.kt @@ -0,0 +1,396 @@ +package com.sa.core.ui.component + +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +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.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CloudOff +import androidx.compose.material.icons.filled.ErrorOutline +import androidx.compose.material.icons.filled.FavoriteBorder +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.ShoppingCart +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +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.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.sa.core.ui.theme.BackgroundColor +import com.sa.core.ui.theme.BorderColor +import com.sa.core.ui.theme.CommerceXTheme +import com.sa.core.ui.theme.ErrorColor +import com.sa.core.ui.theme.PrimaryColor +import com.sa.core.ui.theme.Spacing +import com.sa.core.ui.theme.SurfaceColor +import com.sa.core.ui.theme.SurfaceVariant +import com.sa.core.ui.theme.TextPrimaryColor +import com.sa.core.ui.theme.TextSecondaryColor + +@Composable +fun EmptyCartState( + onStartShopping: () -> Unit = {} +) { + StateContent( + icon = Icons.Filled.ShoppingCart, + iconTint = TextSecondaryColor, + iconContainerColor = SurfaceVariant, + title = "Your cart is empty", + message = "Looks like you have not added anything yet.", + primaryAction = "Start Shopping", + onPrimaryAction = onStartShopping + ) +} + +@Composable +fun NoSearchResultsState( + query: String = "headphones" +) { + StateContent( + icon = Icons.Filled.Search, + iconTint = TextSecondaryColor, + iconContainerColor = SurfaceVariant, + title = "No results found", + message = "No products matched \"$query\". Try a different keyword." + ) +} + +@Composable +fun OfflineState( + onRetry: () -> Unit = {} +) { + StateContent( + icon = Icons.Filled.CloudOff, + iconTint = PrimaryColor, + iconContainerColor = PrimaryColor.copy(alpha = 0.12f), + title = "You're offline", + message = "Check your connection and try again.", + primaryAction = "Retry", + onPrimaryAction = onRetry + ) +} + +@Composable +fun EmptyWishlistState( + onDiscover: () -> Unit = {} +) { + StateContent( + icon = Icons.Filled.FavoriteBorder, + iconTint = PrimaryColor, + iconContainerColor = PrimaryColor.copy(alpha = 0.12f), + title = "Your wishlist is empty", + message = "Save products you love and find them here instantly.", + primaryAction = "Discover Products", + onPrimaryAction = onDiscover + ) +} + +@Composable +fun GenericErrorState( + onRetry: () -> Unit = {} +) { + StateContent( + icon = Icons.Filled.ErrorOutline, + iconTint = ErrorColor, + iconContainerColor = ErrorColor.copy(alpha = 0.12f), + title = "Something went wrong", + message = "We couldn't load this content. Please try again.", + primaryAction = "Try Again", + onPrimaryAction = onRetry + ) +} + +@Composable +fun PullToRefreshIndicator( + isRefreshing: Boolean, + progress: Float, + modifier: Modifier = Modifier +) { + val normalizedProgress = progress.coerceIn(0f, 1f) + val arrowScale by animateFloatAsState( + targetValue = if (isRefreshing) 0f else (0.8f + normalizedProgress * 0.4f), + animationSpec = spring(), + label = "pull_scale" + ) + + Row( + modifier = modifier + .fillMaxWidth() + .padding(vertical = Spacing.sm), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + if (isRefreshing) { + CircularProgressIndicator( + modifier = Modifier.size(18.dp), + strokeWidth = 2.dp + ) + Text( + text = "Refreshing...", + modifier = Modifier.padding(start = Spacing.sm), + color = TextSecondaryColor, + style = MaterialTheme.typography.labelLarge + ) + } else { + Icon( + imageVector = if (normalizedProgress > 0.8f) Icons.Filled.Refresh else Icons.Filled.KeyboardArrowDown, + contentDescription = "Pull to refresh", + modifier = Modifier.scale(arrowScale), + tint = PrimaryColor + ) + Text( + text = if (normalizedProgress > 0.8f) "Release to refresh" else "Pull to refresh", + modifier = Modifier.padding(start = Spacing.xs), + color = TextSecondaryColor, + style = MaterialTheme.typography.labelLarge + ) + } + } +} + +@Composable +fun LoadingProductGridState( + modifier: Modifier = Modifier, + itemCount: Int = 6 +) { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(Spacing.md), + verticalArrangement = Arrangement.spacedBy(Spacing.lg), + contentPadding = PaddingValues(horizontal = Spacing.lg, vertical = Spacing.sm) + ) { + items((1..itemCount).toList()) { + LoadingProductCard() + } + } +} + +@Composable +private fun LoadingProductCard() { + val shimmer = rememberShimmerBrush() + + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(SurfaceColor) + .padding(Spacing.md), + verticalArrangement = Arrangement.spacedBy(Spacing.sm) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(120.dp) + .clip(RoundedCornerShape(12.dp)) + .background(shimmer) + ) + Box( + modifier = Modifier + .fillMaxWidth(0.85f) + .height(14.dp) + .clip(RoundedCornerShape(8.dp)) + .background(shimmer) + ) + Box( + modifier = Modifier + .fillMaxWidth(0.5f) + .height(14.dp) + .clip(RoundedCornerShape(8.dp)) + .background(shimmer) + ) + } +} + +@Composable +private fun rememberShimmerBrush(): Brush { + val transition = rememberInfiniteTransition(label = "state_shimmer") + val xProgress by transition.animateFloat( + initialValue = 0f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 1200, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Restart + ), + label = "shimmer_progress" + ) + + return Brush.linearGradient( + colors = listOf( + SurfaceVariant, + Color.White, + SurfaceVariant + ), + start = Offset(x = xProgress * 900f - 450f, y = 0f), + end = Offset(x = xProgress * 900f, y = 280f) + ) +} + +@Composable +private fun StateContent( + icon: ImageVector, + iconTint: Color, + iconContainerColor: Color, + title: String, + message: String, + primaryAction: String? = null, + onPrimaryAction: () -> Unit = {} +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = Spacing.lg), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Box( + modifier = Modifier + .size(120.dp) + .clip(CircleShape) + .background(iconContainerColor), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = icon, + contentDescription = title, + tint = iconTint, + modifier = Modifier.size(52.dp) + ) + } + + Text( + text = title, + modifier = Modifier.padding(top = Spacing.lg), + style = MaterialTheme.typography.titleLarge, + color = TextPrimaryColor, + fontWeight = FontWeight.Bold + ) + + Text( + text = message, + modifier = Modifier.padding(top = Spacing.sm), + style = MaterialTheme.typography.bodyMedium, + color = TextSecondaryColor, + textAlign = TextAlign.Center + ) + + if (primaryAction != null) { + PrimaryButton( + text = primaryAction, + onClick = onPrimaryAction, + modifier = Modifier + .fillMaxWidth() + .padding(top = Spacing.lg) + ) + } + } +} + +@Preview(name = "State - Empty Cart", showBackground = true, widthDp = 448, heightDp = 900) +@Composable +private fun EmptyCartStatePreview() { + CommerceXTheme { + Box(modifier = Modifier.background(BackgroundColor).padding(top = 180.dp)) { + EmptyCartState() + } + } +} + +@Preview(name = "State - Search Empty", showBackground = true, widthDp = 448, heightDp = 900) +@Composable +private fun SearchEmptyStatePreview() { + CommerceXTheme { + Box(modifier = Modifier.background(BackgroundColor).padding(top = 180.dp)) { + NoSearchResultsState() + } + } +} + +@Preview(name = "State - Offline", showBackground = true, widthDp = 448, heightDp = 900) +@Composable +private fun OfflineStatePreview() { + CommerceXTheme { + Box(modifier = Modifier.background(BackgroundColor).padding(top = 180.dp)) { + OfflineState() + } + } +} + +@Preview(name = "State - Generic Error", showBackground = true, widthDp = 448, heightDp = 900) +@Composable +private fun GenericErrorPreview() { + CommerceXTheme { + Box(modifier = Modifier.background(BackgroundColor).padding(top = 180.dp)) { + GenericErrorState() + } + } +} + +@Preview(name = "State - Wishlist Empty", showBackground = true, widthDp = 448, heightDp = 900) +@Composable +private fun WishlistEmptyPreview() { + CommerceXTheme { + Box(modifier = Modifier.background(BackgroundColor).padding(top = 180.dp)) { + EmptyWishlistState() + } + } +} + +@Preview(name = "State - Loading Products", showBackground = true, widthDp = 448, heightDp = 900) +@Composable +private fun LoadingStatePreview() { + CommerceXTheme { + Box(modifier = Modifier.background(BackgroundColor)) { + LoadingProductGridState() + } + } +} + +@Preview(name = "State - Pull To Refresh", showBackground = true, widthDp = 448, heightDp = 160) +@Composable +private fun PullToRefreshPreview() { + CommerceXTheme { + Column(modifier = Modifier.background(BackgroundColor)) { + PullToRefreshIndicator(isRefreshing = false, progress = 0.35f) + PullToRefreshIndicator(isRefreshing = false, progress = 0.9f) + PullToRefreshIndicator(isRefreshing = true, progress = 1f) + } + } +} From 42f82c61e7ac7f2a4ac81c49ef0f95bad737cd02 Mon Sep 17 00:00:00 2001 From: Salman Ashraf Date: Wed, 18 Feb 2026 14:47:10 +0800 Subject: [PATCH 2/2] feat: US-0.9: fixed issue --- .../sa/feature/product/ui/HomeScreenMockup.kt | 257 +++--------------- 1 file changed, 45 insertions(+), 212 deletions(-) diff --git a/core/feature/feature-product/src/main/java/com/sa/feature/product/ui/HomeScreenMockup.kt b/core/feature/feature-product/src/main/java/com/sa/feature/product/ui/HomeScreenMockup.kt index fabdd92..d5afaab 100644 --- a/core/feature/feature-product/src/main/java/com/sa/feature/product/ui/HomeScreenMockup.kt +++ b/core/feature/feature-product/src/main/java/com/sa/feature/product/ui/HomeScreenMockup.kt @@ -1,7 +1,5 @@ package com.sa.feature.product.ui -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.AnimatedContent import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.Spring @@ -12,30 +10,53 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.with import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectVerticalDragGestures 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.itemsIndexed -import androidx.compose.foundation.gestures.detectVerticalDragGestures import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.ShoppingCart -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +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.alpha +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.sa.core.ui.component.* -import com.sa.core.ui.theme.* +import com.sa.core.ui.component.BottomNavItem +import com.sa.core.ui.component.BottomNavigationBar +import com.sa.core.ui.component.Badge +import com.sa.core.ui.component.CategoryChipsRow +import com.sa.core.ui.component.ProductCard +import com.sa.core.ui.component.ShimmerProductGrid +import com.sa.core.ui.theme.BackgroundColor +import com.sa.core.ui.theme.CommerceXGradients +import com.sa.core.ui.theme.CommerceXTheme +import com.sa.core.ui.theme.DividerColor +import com.sa.core.ui.theme.PrimaryColor +import com.sa.core.ui.theme.Spacing +import com.sa.core.ui.theme.TextPrimaryColor +import com.sa.core.ui.theme.TextSecondaryColor import kotlinx.coroutines.delay /** @@ -70,7 +91,6 @@ fun HomeScreenMockup( var isRefreshing by remember { mutableStateOf(false) } var isLoading by remember { mutableStateOf(true) } - // Sample product data for mockup val products = remember { listOf( MockProduct("iPhone 9", 549.0, 699.0, 12, 4.69, 94, "Electronics"), @@ -85,9 +105,6 @@ fun HomeScreenMockup( } val categories = listOf("All", "Electronics", "Jewelry", "Men's Clothing", "Women's Clothing") - val filteredProducts = remember(selectedCategory, products) { - if (selectedCategory == "All") products else products.filter { it.category == selectedCategory } - } LaunchedEffect(Unit) { delay(900) @@ -95,9 +112,7 @@ fun HomeScreenMockup( } val refreshProducts: () -> Unit = { - if (!isRefreshing) { - isRefreshing = true - } + if (!isRefreshing) isRefreshing = true } LaunchedEffect(isRefreshing) { @@ -114,20 +129,15 @@ fun HomeScreenMockup( .background(BackgroundColor) .windowInsetsPadding(WindowInsets.systemBars) ) { - Column( - modifier = Modifier.fillMaxSize() - ) { - // Glassmorphic Header + Column(modifier = Modifier.fillMaxSize()) { GlassmorphicHeader( cartItemCount = cartItemCount, onSearchClick = onSearchClick, onCartClick = onCartClick ) - // Hero Banner HeroBanner() - // Category Chips Spacer(modifier = Modifier.height(Spacing.lg)) CategoryChipsRow( categories = categories, @@ -150,9 +160,13 @@ fun HomeScreenMockup( targetState = selectedCategory, transitionSpec = { fadeIn(tween(180)) with fadeOut(tween(180)) }, label = "category_layout_transition" - ) { _ -> + ) { targetCategory -> ProductGridWithAnimation( - products = filteredProducts, + products = if (targetCategory == "All") { + products + } else { + products.filter { it.category == targetCategory } + }, modifier = Modifier.fillMaxSize(), onProductClick = onProductClick ) @@ -161,7 +175,6 @@ fun HomeScreenMockup( } } - // Fixed Bottom Navigation BottomNavigationBar( selectedItem = selectedNavItem, cartItemCount = cartItemCount, @@ -206,9 +219,7 @@ private fun PullToRefreshStrip( } }, onDragEnd = { - if (!isRefreshing && progress > 0.85f) { - onRefresh() - } + if (!isRefreshing && progress > 0.85f) onRefresh() dragDistance = 0f }, onDragCancel = { dragDistance = 0f } @@ -252,9 +263,6 @@ private fun PullToRefreshStrip( } } -/** - * Glassmorphic Header Component - */ @Composable fun GlassmorphicHeader( cartItemCount: Int, @@ -268,9 +276,7 @@ fun GlassmorphicHeader( color = Color.White.copy(alpha = 0.8f), shadowElevation = 0.dp ) { - Box( - modifier = Modifier.fillMaxSize() - ) { + Box(modifier = Modifier.fillMaxSize()) { Row( modifier = Modifier .fillMaxSize() @@ -285,9 +291,7 @@ fun GlassmorphicHeader( color = PrimaryColor ) - Row( - horizontalArrangement = Arrangement.spacedBy(Spacing.sm) - ) { + Row(horizontalArrangement = Arrangement.spacedBy(Spacing.sm)) { IconButton(onClick = onSearchClick) { Icon( imageVector = Icons.Filled.Search, @@ -325,37 +329,26 @@ fun GlassmorphicHeader( } } -/** - * Hero Banner Component - */ @Composable fun HeroBanner() { Box( modifier = Modifier .fillMaxWidth() .height(180.dp) - .background( - brush = CommerceXGradients.heroGradient - ) + .background(brush = CommerceXGradients.heroGradient) ) { Box( modifier = Modifier .size(120.dp) .offset(x = (-20).dp, y = 20.dp) - .background( - color = Color.White.copy(alpha = 0.1f), - shape = CircleShape - ) + .background(color = Color.White.copy(alpha = 0.1f), shape = CircleShape) ) Box( modifier = Modifier .size(80.dp) .offset(x = 320.dp, y = 100.dp) - .background( - color = Color.White.copy(alpha = 0.15f), - shape = CircleShape - ) + .background(color = Color.White.copy(alpha = 0.15f), shape = CircleShape) ) Column( @@ -383,9 +376,6 @@ fun HeroBanner() { } } -/** - * Product Grid with Staggered Fade-Up Animation - */ @Composable fun ProductGridWithAnimation( products: List, @@ -411,9 +401,6 @@ fun ProductGridWithAnimation( } } -/** - * Animated Product Card with staggered entrance - */ @Composable fun AnimatedProductCard( product: MockProduct, @@ -423,7 +410,7 @@ fun AnimatedProductCard( var isVisible by remember { mutableStateOf(false) } LaunchedEffect(Unit) { - kotlinx.coroutines.delay(index * 50L) + delay(index * 50L) isVisible = true } @@ -456,172 +443,18 @@ fun AnimatedProductCard( rating = product.rating, reviewCount = product.reviewCount, isWishlisted = false, - onWishlistClick = { /* Wishlist action */ }, + onWishlistClick = { }, onClick = onClick ) } } -/** - * Mock Product Data Class - */ data class MockProduct( val title: String, val price: Double, val originalPrice: Double?, val discountPercent: Int?, val rating: Double, - val reviewCount: Int, - val category: String -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.with -import androidx.compose.foundation.gestures.detectVerticalDragGestures -import androidx.compose.material.icons.filled.KeyboardArrowDown -import androidx.compose.material.icons.filled.Refresh -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.pointer.pointerInput -import kotlinx.coroutines.delay -@Preview( - name = "Home Screen - Loading State", - showBackground = true, - widthDp = 448, - heightDp = 900 -) -@Composable -fun HomeScreenLoadingPreview() { - CommerceXTheme { - HomeScreenMockup( - showLoadingState = true, - showPullToRefreshIndicator = true, - pullToRefreshProgress = 0.85f - ) - } -} - -@OptIn(ExperimentalAnimationApi::class) - onProfileClick: () -> Unit = {}, - showLoadingState: Boolean = false, - showPullToRefreshIndicator: Boolean = false, - pullToRefreshProgress: Float = 0f - var isRefreshing by remember { mutableStateOf(false) } - var isLoading by remember { mutableStateOf(true) } - val filteredProducts = remember(selectedCategory, products) { - if (selectedCategory == "All") products else products.filter { it.category == selectedCategory } - } - - LaunchedEffect(Unit) { - delay(900) - isLoading = false - } - - val refreshProducts: () -> Unit = { - if (!isRefreshing) { - isRefreshing = true - } - } - - LaunchedEffect(isRefreshing) { - if (isRefreshing) { - delay(950) - cartItemCount += 1 - isRefreshing = false - } - } - PullToRefreshStrip( - isRefreshing = isRefreshing, - onRefresh = refreshProducts - ) - - if (showPullToRefreshIndicator) { - PullToRefreshIndicator( - isRefreshing = false, - progress = pullToRefreshProgress, - modifier = Modifier.padding(horizontal = Spacing.lg) - ) - } - - if (showLoadingState) { - LoadingProductGridState(modifier = Modifier.weight(1f)) - } else { - // Product Grid with staggered animation - ProductGridWithAnimation( - products = products, - modifier = Modifier.weight(1f), - onProductClick = onProductClick - ) - } -@Composable -private fun PullToRefreshStrip( - isRefreshing: Boolean, - onRefresh: () -> Unit -) { - var dragDistance by remember { mutableStateOf(0f) } - val progress = (dragDistance / 150f).coerceIn(0f, 1f) - val arrowRotation by animateFloatAsState( - targetValue = progress * 180f, - animationSpec = tween(durationMillis = 120), - label = "pull_arrow_rotation" - ) - - Box( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = Spacing.lg, vertical = Spacing.sm) - .pointerInput(isRefreshing) { - detectVerticalDragGestures( - onVerticalDrag = { _, dragAmount -> - if (!isRefreshing) { - dragDistance = (dragDistance + dragAmount).coerceIn(0f, 180f) - } - }, - onDragEnd = { - if (!isRefreshing && progress > 0.85f) { - onRefresh() - } - dragDistance = 0f - }, - onDragCancel = { dragDistance = 0f } - ) - }, - contentAlignment = Alignment.Center - ) { - if (isRefreshing) { - Row( - horizontalArrangement = Arrangement.spacedBy(Spacing.sm), - verticalAlignment = Alignment.CenterVertically - ) { - CircularProgressIndicator( - modifier = Modifier.size(16.dp), - strokeWidth = 2.dp - ) - Text( - text = "Refreshing products...", - style = MaterialTheme.typography.labelLarge, - color = TextSecondaryColor - ) - } - } else { - Row( - horizontalArrangement = Arrangement.spacedBy(Spacing.xs), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = if (progress > 0.5f) Icons.Filled.Refresh else Icons.Filled.KeyboardArrowDown, - contentDescription = "Pull to refresh", - tint = PrimaryColor, - modifier = Modifier.graphicsLayer(rotationZ = arrowRotation) - ) - Text( - text = if (progress > 0.85f) "Release to refresh" else "Pull down to refresh", - style = MaterialTheme.typography.labelLarge, - color = TextSecondaryColor - ) - } - } - } -} - val reviewCount: Int, val category: String )