From a0fd1d56f64bed0ed608a43070a2c80190d4aaf2 Mon Sep 17 00:00:00 2001 From: null Date: Wed, 17 Jun 2026 20:38:06 -0500 Subject: [PATCH] feat: category picker redesign, bucket list wiring, settings + nav updates --- .../closer/core/navigation/AppNavigation.kt | 13 +- .../app/closer/core/navigation/AppRoute.kt | 4 +- .../repository/BucketListRepositoryImpl.kt | 14 +- .../domain/repository/BucketListRepository.kt | 6 +- .../closer/ui/dates/BucketListViewModel.kt | 9 +- .../java/app/closer/ui/play/PlayHubScreen.kt | 356 ++++++++++++++++++ .../app/closer/ui/settings/SettingsScreen.kt | 7 + .../closer/ui/wheel/CategoryPickerScreen.kt | 173 +++++++-- .../app/closer/ui/wheel/SpinWheelScreen.kt | 106 +++++- 9 files changed, 622 insertions(+), 66 deletions(-) create mode 100644 app/src/main/java/app/closer/ui/play/PlayHubScreen.kt diff --git a/app/src/main/java/app/closer/core/navigation/AppNavigation.kt b/app/src/main/java/app/closer/core/navigation/AppNavigation.kt index cbc16a0a..6f65e504 100644 --- a/app/src/main/java/app/closer/core/navigation/AppNavigation.kt +++ b/app/src/main/java/app/closer/core/navigation/AppNavigation.kt @@ -3,9 +3,9 @@ package app.closer.core.navigation import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Done import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Star import androidx.compose.material3.ExperimentalMaterial3Api @@ -47,6 +47,7 @@ import app.closer.ui.dates.DateMatchesScreen import app.closer.ui.dates.DateBuilderScreen import app.closer.ui.dates.BucketListScreen import app.closer.ui.paywall.PaywallScreen +import app.closer.ui.play.PlayHubScreen import app.closer.ui.questions.DailyQuestionScreen import app.closer.ui.questions.QuestionCategoryScreen import app.closer.ui.questions.QuestionComposerScreen @@ -170,6 +171,9 @@ fun AppNavigation( composable(route = AppRoute.PARTNER_HOME) { PartnerHomeScreen(onNavigate = navigateRoute) } + composable(route = AppRoute.PLAY) { + PlayHubScreen(onNavigate = navigateRoute) + } // Daily Question composable(route = AppRoute.DAILY_QUESTION) { @@ -349,8 +353,8 @@ private data class TopLevelRoute( private val topLevelRoutes = listOf( TopLevelRoute(AppRoute.HOME, "Home", Icons.Filled.Home), TopLevelRoute(AppRoute.DAILY_QUESTION, "Today", Icons.Filled.Favorite), + TopLevelRoute(AppRoute.PLAY, "Play", Icons.Filled.PlayArrow), TopLevelRoute(AppRoute.QUESTION_PACKS, "Packs", Icons.Filled.Star), - TopLevelRoute(AppRoute.ANSWER_HISTORY, "Answers", Icons.Filled.Done), TopLevelRoute(AppRoute.SETTINGS, "Settings", Icons.Filled.Settings) ) @@ -364,6 +368,11 @@ private val shellBackRoutes = setOf( AppRoute.SPIN_WHEEL, AppRoute.WHEEL_SESSION, AppRoute.WHEEL_COMPLETE, + AppRoute.WHEEL_HISTORY, + AppRoute.DATE_MATCH, + AppRoute.DATE_MATCHES, + AppRoute.DATE_BUILDER, + AppRoute.BUCKET_LIST, AppRoute.ACCOUNT, AppRoute.SUBSCRIPTION, AppRoute.PAYWALL diff --git a/app/src/main/java/app/closer/core/navigation/AppRoute.kt b/app/src/main/java/app/closer/core/navigation/AppRoute.kt index b9bacf79..c4fd4854 100644 --- a/app/src/main/java/app/closer/core/navigation/AppRoute.kt +++ b/app/src/main/java/app/closer/core/navigation/AppRoute.kt @@ -10,6 +10,7 @@ object AppRoute { const val CREATE_PROFILE = "create_profile" const val HOME = "home" const val PARTNER_HOME = "partner_home" + const val PLAY = "play" const val DAILY_QUESTION = "daily_question" const val QUESTION_PACKS = "question_packs" const val QUESTION_CATEGORY = "question_category/{categoryId}" @@ -56,6 +57,7 @@ object AppRoute { Definition(FORGOT_PASSWORD, "Forgot Password", "auth"), Definition(HOME, "Home", "home"), Definition(PARTNER_HOME, "Partner", "home"), + Definition(PLAY, "Play", "play"), Definition(DAILY_QUESTION, "Daily Question", "questions"), Definition(QUESTION_PACKS, "Question Packs", "questions"), Definition(QUESTION_CATEGORY, "Question Pack", "questions"), @@ -89,8 +91,8 @@ object AppRoute { val topLevelRoutes = setOf( HOME, DAILY_QUESTION, + PLAY, QUESTION_PACKS, - ANSWER_HISTORY, SETTINGS ) diff --git a/app/src/main/java/app/closer/data/repository/BucketListRepositoryImpl.kt b/app/src/main/java/app/closer/data/repository/BucketListRepositoryImpl.kt index bb86b67b..262ddca7 100644 --- a/app/src/main/java/app/closer/data/repository/BucketListRepositoryImpl.kt +++ b/app/src/main/java/app/closer/data/repository/BucketListRepositoryImpl.kt @@ -28,24 +28,22 @@ class BucketListRepositoryImpl @Inject constructor( dataSource.updateItem(item.coupleId, item) } - override suspend fun getItem(itemId: String): BucketListItem? { - // Need coupleId to fetch - in practice, this would be called from a context - // where the coupleId is known. For now, return null. - return null + override suspend fun getItem(coupleId: String, itemId: String): BucketListItem? { + return dataSource.getItem(coupleId, itemId) } override suspend fun getItems(coupleId: String): List { return dataSource.getItems(coupleId) } - override suspend fun deleteItem(itemId: String) { - // Need coupleId to delete - same limitation as getItem + override suspend fun deleteItem(coupleId: String, itemId: String) { + dataSource.deleteItem(coupleId, itemId) } // ─── Completion methods ────────────────────────────────────────────────── - override suspend fun completeItem(itemId: String, completedBy: String) { - // Need coupleId - same limitation + override suspend fun completeItem(coupleId: String, itemId: String, completedBy: String) { + dataSource.completeItem(coupleId, itemId, completedBy) } // ─── Category methods ──────────────────────────────────────────────────── diff --git a/app/src/main/java/app/closer/domain/repository/BucketListRepository.kt b/app/src/main/java/app/closer/domain/repository/BucketListRepository.kt index fc765c02..2286a260 100644 --- a/app/src/main/java/app/closer/domain/repository/BucketListRepository.kt +++ b/app/src/main/java/app/closer/domain/repository/BucketListRepository.kt @@ -22,18 +22,18 @@ interface BucketListRepository { suspend fun updateItem(item: BucketListItem) /** Get a bucket list item by ID. */ - suspend fun getItem(itemId: String): BucketListItem? + suspend fun getItem(coupleId: String, itemId: String): BucketListItem? /** Get all bucket list items for a couple. */ suspend fun getItems(coupleId: String): List /** Delete a bucket list item. */ - suspend fun deleteItem(itemId: String) + suspend fun deleteItem(coupleId: String, itemId: String) // ─── Completion methods ────────────────────────────────────────────────── /** Mark an item as completed. */ - suspend fun completeItem(itemId: String, completedBy: String) + suspend fun completeItem(coupleId: String, itemId: String, completedBy: String) // ─── Category methods ──────────────────────────────────────────────────── diff --git a/app/src/main/java/app/closer/ui/dates/BucketListViewModel.kt b/app/src/main/java/app/closer/ui/dates/BucketListViewModel.kt index b84801a1..79d7cd42 100644 --- a/app/src/main/java/app/closer/ui/dates/BucketListViewModel.kt +++ b/app/src/main/java/app/closer/ui/dates/BucketListViewModel.kt @@ -62,6 +62,8 @@ class BucketListViewModel @Inject constructor( fun toggleComplete(itemId: String) { val item = _uiState.value.items.find { it.id == itemId } ?: return + val coupleId = _uiState.value.coupleId + if (coupleId.isEmpty()) return viewModelScope.launch { if (item.isCompleted) { @@ -72,7 +74,7 @@ class BucketListViewModel @Inject constructor( ) } } else { - repository.completeItem(itemId, FirebaseAuth.getInstance().currentUser?.uid ?: "") + repository.completeItem(coupleId, itemId, FirebaseAuth.getInstance().currentUser?.uid ?: "") _uiState.update { it.copy( items = it.items.map { @@ -86,8 +88,11 @@ class BucketListViewModel @Inject constructor( } fun deleteItem(itemId: String) { + val coupleId = _uiState.value.coupleId + if (coupleId.isEmpty()) return + viewModelScope.launch { - repository.deleteItem(itemId) + repository.deleteItem(coupleId, itemId) _uiState.update { it.copy(items = it.items.filter { it.id != itemId }) } diff --git a/app/src/main/java/app/closer/ui/play/PlayHubScreen.kt b/app/src/main/java/app/closer/ui/play/PlayHubScreen.kt new file mode 100644 index 00000000..b70fa2ae --- /dev/null +++ b/app/src/main/java/app/closer/ui/play/PlayHubScreen.kt @@ -0,0 +1,356 @@ +package app.closer.ui.play + +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.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForward +import androidx.compose.material.icons.filled.Done +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import app.closer.core.navigation.AppRoute + +@Composable +fun PlayHubScreen( + onNavigate: (String) -> Unit = {} +) { + PlayHubContent(onNavigate = onNavigate) +} + +@Composable +private fun PlayHubContent( + onNavigate: (String) -> Unit +) { + Box( + modifier = Modifier + .fillMaxSize() + .background( + Brush.linearGradient( + listOf(Color(0xFFFFFBFE), Color(0xFFF8F1FF), Color(0xFFFFEEF7)), + start = Offset.Zero, + end = Offset.Infinite + ) + ) + ) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .safeDrawingPadding() + .navigationBarsPadding() + .padding(horizontal = 20.dp), + verticalArrangement = Arrangement.spacedBy(14.dp) + ) { + item { + Column( + modifier = Modifier.padding(top = 22.dp, bottom = 4.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "Play", + style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.SemiBold), + color = Color(0xFF261D2E), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = "Pick something light, useful, or a little surprising to do together.", + style = MaterialTheme.typography.bodyLarge, + color = Color(0xFF5A5060), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + } + + item { + FeaturedPlayCard( + onClick = { onNavigate(AppRoute.CATEGORY_PICKER) } + ) + } + + item { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + CompactPlayCard( + title = "Date Match", + subtitle = "Swipe ideas", + icon = Icons.Filled.Favorite, + tint = Color(0xFF9B1B5A), + modifier = Modifier.weight(1f), + onClick = { onNavigate(AppRoute.DATE_MATCH) } + ) + CompactPlayCard( + title = "Plan Date", + subtitle = "Set the shape", + icon = Icons.Filled.Star, + tint = Color(0xFF6A4A00), + modifier = Modifier.weight(1f), + onClick = { onNavigate(AppRoute.DATE_BUILDER) } + ) + } + } + + item { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + CompactPlayCard( + title = "Bucket List", + subtitle = "Save ideas", + icon = Icons.Filled.Done, + tint = Color(0xFF2F5B4F), + modifier = Modifier.weight(1f), + onClick = { onNavigate(AppRoute.BUCKET_LIST) } + ) + CompactPlayCard( + title = "Wheel History", + subtitle = "Past sessions", + icon = Icons.Filled.Home, + tint = Color(0xFF56306F), + modifier = Modifier.weight(1f), + onClick = { onNavigate(AppRoute.WHEEL_HISTORY) } + ) + } + } + } + } +} + +@Composable +private fun FeaturedPlayCard( + onClick: () -> Unit +) { + Card( + onClick = onClick, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(30.dp), + colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.9f)), + elevation = CardDefaults.cardElevation(defaultElevation = 12.dp) + ) { + Column( + modifier = Modifier + .background( + Brush.linearGradient( + listOf(Color.White, Color(0xFFF3E8FF), Color(0xFFFFF7FB)), + start = Offset.Zero, + end = Offset.Infinite + ) + ) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(18.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Surface( + shape = RoundedCornerShape(999.dp), + color = Color.White.copy(alpha = 0.72f) + ) { + Text( + text = "Wheel", + modifier = Modifier.padding(horizontal = 12.dp, vertical = 7.dp), + style = MaterialTheme.typography.labelMedium, + color = Color(0xFF56306F), + fontWeight = FontWeight.SemiBold + ) + } + Surface( + shape = RoundedCornerShape(999.dp), + color = Color(0xFFFFE8F4) + ) { + Text( + text = "10 prompts", + modifier = Modifier.padding(horizontal = 12.dp, vertical = 7.dp), + style = MaterialTheme.typography.labelMedium, + color = Color(0xFF6D2B55), + fontWeight = FontWeight.SemiBold + ) + } + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(14.dp), + verticalAlignment = Alignment.CenterVertically + ) { + WheelGlyph( + modifier = Modifier.size(74.dp) + ) + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = "Spin the Wheel", + style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.SemiBold), + color = Color(0xFF261D2E), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + Text( + text = "Choose a mood, spin once, and move through a short session together.", + style = MaterialTheme.typography.bodyMedium, + color = Color(0xFF5A5060), + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) + } + } + + Button( + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 54.dp), + shape = RoundedCornerShape(18.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFF56306F), + contentColor = Color.White + ) + ) { + Text("Choose category") + } + } + } +} + +@Composable +private fun CompactPlayCard( + title: String, + subtitle: String, + icon: ImageVector, + tint: Color, + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + Card( + onClick = onClick, + modifier = modifier.heightIn(min = 154.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 6.dp) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + verticalArrangement = Arrangement.SpaceBetween + ) { + Surface( + shape = RoundedCornerShape(18.dp), + color = tint.copy(alpha = 0.14f), + modifier = Modifier.size(46.dp) + ) { + Box(contentAlignment = Alignment.Center) { + Icon( + imageVector = icon, + contentDescription = null, + tint = tint, + modifier = Modifier.size(24.dp) + ) + } + } + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold), + color = Color(0xFF261D2E), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = subtitle, + style = MaterialTheme.typography.bodySmall, + color = Color(0xFF5A5060), + modifier = Modifier.weight(1f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowForward, + contentDescription = null, + tint = tint, + modifier = Modifier.size(18.dp) + ) + } + } + } + } +} + +@Composable +private fun WheelGlyph( + modifier: Modifier = Modifier +) { + Surface( + modifier = modifier, + shape = RoundedCornerShape(26.dp), + color = Color(0xFF56306F) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.background( + Brush.linearGradient( + listOf(Color(0xFFB98AF4), Color(0xFFE7A2D1), Color(0xFF56306F)), + start = Offset.Zero, + end = Offset.Infinite + ) + ) + ) { + Icon( + imageVector = Icons.Filled.PlayArrow, + contentDescription = null, + tint = Color.White, + modifier = Modifier.size(34.dp) + ) + } + } +} + +@Preview +@Composable +fun PlayHubScreenPreview() { + PlayHubContent(onNavigate = {}) +} diff --git a/app/src/main/java/app/closer/ui/settings/SettingsScreen.kt b/app/src/main/java/app/closer/ui/settings/SettingsScreen.kt index 094ec35c..87b2642d 100644 --- a/app/src/main/java/app/closer/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/app/closer/ui/settings/SettingsScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowForwardIos +import androidx.compose.material.icons.filled.Done import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.FavoriteBorder import androidx.compose.material.icons.filled.Lock @@ -330,6 +331,12 @@ fun SettingsScreen( colors = CardDefaults.cardColors(containerColor = SettingsCard) ) { Column { + SettingsRow( + icon = Icons.Filled.Done, + label = "Answer History", + onClick = { onNavigate(AppRoute.ANSWER_HISTORY) } + ) + Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp) SettingsRow( icon = Icons.Filled.Notifications, label = "Notifications", diff --git a/app/src/main/java/app/closer/ui/wheel/CategoryPickerScreen.kt b/app/src/main/java/app/closer/ui/wheel/CategoryPickerScreen.kt index 6e433777..b1feb772 100644 --- a/app/src/main/java/app/closer/ui/wheel/CategoryPickerScreen.kt +++ b/app/src/main/java/app/closer/ui/wheel/CategoryPickerScreen.kt @@ -15,8 +15,13 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator @@ -54,6 +59,7 @@ fun CategoryPickerScreen( if (item.isLocked) onNavigate(AppRoute.PAYWALL) else onNavigate(AppRoute.spinWheel(item.category.id)) }, + onHistory = { onNavigate(AppRoute.WHEEL_HISTORY) }, onRetry = viewModel::load ) } @@ -62,6 +68,7 @@ fun CategoryPickerScreen( private fun CategoryPickerContent( state: CategoryPickerUiState, onCategorySelected: (CategoryPickerItem) -> Unit, + onHistory: () -> Unit, onRetry: () -> Unit ) { Box( @@ -84,25 +91,10 @@ private fun CategoryPickerContent( verticalArrangement = Arrangement.spacedBy(14.dp) ) { item { - Column( - modifier = Modifier.padding(top = 20.dp, bottom = 4.dp), - verticalArrangement = Arrangement.spacedBy(10.dp) - ) { - Text( - text = "Choose the weather", - style = MaterialTheme.typography.headlineLarge.copy(fontWeight = FontWeight.SemiBold), - color = Color(0xFF261D2E), - maxLines = 2, - overflow = TextOverflow.Ellipsis - ) - Text( - text = "Pick a category that matches where you are tonight. The wheel picks the question.", - style = MaterialTheme.typography.bodyLarge, - color = Color(0xFF5A5060), - maxLines = 3, - overflow = TextOverflow.Ellipsis - ) - } + WheelPickerHeader( + onHistory = onHistory, + modifier = Modifier.padding(top = 20.dp, bottom = 4.dp) + ) } when { @@ -125,6 +117,13 @@ private fun CategoryPickerContent( Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { Text("Categories unavailable", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold, color = Color(0xFF261D2E)) Text(state.error, style = MaterialTheme.typography.bodyMedium, color = Color(0xFF5A5060)) + Button( + onClick = onRetry, + shape = RoundedCornerShape(16.dp), + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF56306F)) + ) { + Text("Retry", color = Color.White) + } } } } @@ -138,27 +137,138 @@ private fun CategoryPickerContent( } } +@Composable +private fun WheelPickerHeader( + onHistory: () -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(14.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top + ) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "Spin the Wheel", + style = MaterialTheme.typography.headlineLarge.copy(fontWeight = FontWeight.SemiBold), + color = Color(0xFF261D2E), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + Text( + text = "Choose a mood and let chance pick the next conversation.", + style = MaterialTheme.typography.bodyLarge, + color = Color(0xFF5A5060), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + Button( + onClick = onHistory, + modifier = Modifier.heightIn(min = 48.dp), + shape = RoundedCornerShape(999.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.White.copy(alpha = 0.82f), + contentColor = Color(0xFF56306F) + ) + ) { + Text("History") + } + } + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(22.dp), + color = Color(0xFFFFF8FC) + ) { + Row( + modifier = Modifier.padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Surface( + shape = RoundedCornerShape(18.dp), + color = Color(0xFFF0DFFF), + modifier = Modifier.size(46.dp) + ) { + Box(contentAlignment = Alignment.Center) { + Icon( + imageVector = Icons.Filled.PlayArrow, + contentDescription = null, + tint = Color(0xFF56306F), + modifier = Modifier.size(24.dp) + ) + } + } + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(3.dp) + ) { + Text( + text = "Ten prompts per spin", + style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.SemiBold), + color = Color(0xFF261D2E), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = "You can skip, continue, or end whenever the moment feels complete.", + style = MaterialTheme.typography.bodySmall, + color = Color(0xFF5A5060), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + } + } + } +} + @Composable private fun CategoryCard( item: CategoryPickerItem, onClick: () -> Unit ) { - val containerColor = if (item.isLocked) Color(0xFFFFF8FC).copy(alpha = 0.84f) else Color.White.copy(alpha = 0.86f) + val containerColor = if (item.isLocked) Color(0xFFFFF8FC) else Color.White Card( onClick = onClick, modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(24.dp), + shape = RoundedCornerShape(22.dp), colors = CardDefaults.cardColors(containerColor = containerColor), - elevation = CardDefaults.cardElevation(defaultElevation = 6.dp) + elevation = CardDefaults.cardElevation(defaultElevation = if (item.isLocked) 2.dp else 6.dp) ) { Row( - modifier = Modifier.padding(18.dp), + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { + Surface( + shape = RoundedCornerShape(18.dp), + color = if (item.isLocked) Color(0xFFF0EDF9) else Color(0xFFFFE8F4), + modifier = Modifier.size(48.dp) + ) { + Box(contentAlignment = Alignment.Center) { + Icon( + imageVector = if (item.isLocked) Icons.Default.Lock else Icons.Filled.Star, + contentDescription = null, + tint = if (item.isLocked) Color(0xFF9B8AA6) else Color(0xFF6D2B55), + modifier = Modifier.size(22.dp) + ) + } + } Column( - modifier = Modifier.weight(1f), + modifier = Modifier + .weight(1f) + .padding(horizontal = 12.dp), verticalArrangement = Arrangement.spacedBy(6.dp) ) { Text( @@ -174,14 +284,12 @@ private fun CategoryCard( if (item.isLocked) CategoryPill("Premium") } } - if (item.isLocked) { - Icon( - imageVector = Icons.Default.Lock, - contentDescription = "Locked", - tint = Color(0xFF9B8AA6), - modifier = Modifier.size(20.dp) - ) - } + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowForward, + contentDescription = if (item.isLocked) "Unlock" else "Spin", + tint = if (item.isLocked) Color(0xFF9B8AA6) else Color(0xFF56306F), + modifier = Modifier.size(20.dp) + ) } } } @@ -224,6 +332,7 @@ fun CategoryPickerScreenPreview() { ) ), onCategorySelected = {}, + onHistory = {}, onRetry = {} ) } diff --git a/app/src/main/java/app/closer/ui/wheel/SpinWheelScreen.kt b/app/src/main/java/app/closer/ui/wheel/SpinWheelScreen.kt index 9953ea03..49828428 100644 --- a/app/src/main/java/app/closer/ui/wheel/SpinWheelScreen.kt +++ b/app/src/main/java/app/closer/ui/wheel/SpinWheelScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -36,12 +37,13 @@ import androidx.compose.ui.draw.rotate import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel @Composable @@ -137,22 +139,11 @@ private fun SpinWheelContent( contentAlignment = Alignment.Center, modifier = Modifier.weight(1f) ) { - Surface( - modifier = Modifier - .size(220.dp) - .rotate(if (state.isSpinning) rotation else 0f), - shape = CircleShape, - color = Color(0xFFF0EDF9), - shadowElevation = 12.dp - ) { - Box(contentAlignment = Alignment.Center) { - Text( - text = if (state.spunAndReady) "✓" else "◎", - fontSize = 64.sp, - color = if (state.spunAndReady) Color(0xFFB98AF4) else Color(0xFF56306F) - ) - } - } + WheelSpinner( + isSpinning = state.isSpinning, + spunAndReady = state.spunAndReady, + rotation = rotation + ) } Column( @@ -212,7 +203,7 @@ private fun SpinWheelContent( shape = RoundedCornerShape(18.dp), colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF56306F)) ) { - Text("Spin", color = Color.White) + Text("Spin wheel", color = Color.White) } } } @@ -221,6 +212,85 @@ private fun SpinWheelContent( } } +@Composable +private fun WheelSpinner( + isSpinning: Boolean, + spunAndReady: Boolean, + rotation: Float +) { + val segmentColors = listOf( + Color(0xFFB98AF4), + Color(0xFFFFC2DD), + Color(0xFFE7A2D1), + Color(0xFFF0DFFF), + Color(0xFF8F67C5), + Color(0xFFFFE8F4), + Color(0xFF56306F), + Color(0xFFF7C8E4) + ) + + Box( + modifier = Modifier.size(270.dp), + contentAlignment = Alignment.Center + ) { + Canvas( + modifier = Modifier + .size(236.dp) + .rotate(if (isSpinning) rotation else 0f) + ) { + val sweep = 360f / segmentColors.size + segmentColors.forEachIndexed { index, color -> + drawArc( + color = color, + startAngle = -90f + (index * sweep), + sweepAngle = sweep, + useCenter = true + ) + } + drawCircle( + color = Color.White.copy(alpha = 0.78f), + radius = size.minDimension * 0.24f + ) + drawCircle( + color = Color.White.copy(alpha = 0.84f), + style = Stroke(width = 7.dp.toPx()) + ) + } + + Canvas( + modifier = Modifier + .align(Alignment.TopCenter) + .size(width = 34.dp, height = 28.dp) + ) { + val path = Path().apply { + moveTo(size.width / 2f, size.height) + lineTo(0f, 0f) + lineTo(size.width, 0f) + close() + } + drawPath(path = path, color = Color(0xFF261D2E)) + } + + Surface( + modifier = Modifier.size(94.dp), + shape = CircleShape, + color = Color.White, + shadowElevation = 10.dp + ) { + Box(contentAlignment = Alignment.Center) { + Text( + text = if (spunAndReady) "Ready" else "Spin", + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold), + color = Color(0xFF56306F), + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } +} + @Preview @Composable fun SpinWheelScreenPreview() {