diff --git a/app/src/main/java/com/couplesconnect/app/ui/questions/QuestionCategoryScreen.kt b/app/src/main/java/com/couplesconnect/app/ui/questions/QuestionCategoryScreen.kt index c8b4a1bd..b5a433e2 100644 --- a/app/src/main/java/com/couplesconnect/app/ui/questions/QuestionCategoryScreen.kt +++ b/app/src/main/java/com/couplesconnect/app/ui/questions/QuestionCategoryScreen.kt @@ -1,6 +1,8 @@ package com.couplesconnect.app.ui.questions import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -10,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape @@ -22,6 +25,9 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState 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.geometry.Offset @@ -59,6 +65,13 @@ private fun QuestionCategoryContent( state: QuestionCategoryUiState, onQuestionSelected: (Question) -> Unit ) { + var selectedType by remember { mutableStateOf(null) } + val visibleQuestions = remember(state.questions, selectedType) { + state.questions.filter { question -> + selectedType == null || question.type == selectedType + } + } + Box( modifier = Modifier .fillMaxSize() @@ -81,22 +94,12 @@ private fun QuestionCategoryContent( item { val title = state.category?.displayName ?: categoryId.displayCategoryName() - Column( - modifier = Modifier.padding(top = 20.dp, bottom = 6.dp), - verticalArrangement = Arrangement.spacedBy(10.dp) - ) { - Text( - text = title, - style = MaterialTheme.typography.headlineLarge.copy(fontWeight = FontWeight.SemiBold), - color = Color(0xFF27211F) - ) - Text( - text = state.category?.description - ?: "Browse prompts for this kind of conversation.", - style = MaterialTheme.typography.bodyLarge, - color = Color(0xFF4E4642) - ) - } + CategoryHero( + title = title, + category = state.category, + questionCount = state.questions.size, + modifier = Modifier.padding(top = 20.dp, bottom = 6.dp) + ) } when { @@ -115,23 +118,163 @@ private fun QuestionCategoryContent( } else -> { item { - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - CategoryPill("${state.questions.size} prompts") - state.category?.access?.let { CategoryPill(it.displayCategoryName()) } - } - } - items(state.questions, key = { it.id }) { question -> - QuestionListCard( - question = question, - onClick = { onQuestionSelected(question) } + CategoryFilters( + questions = state.questions, + selectedType = selectedType, + onTypeSelected = { selectedType = it } ) } + if (visibleQuestions.isEmpty()) { + item { + CategoryMessageCard( + title = "No prompts match", + message = "Try another filter to keep browsing." + ) + } + } else { + visibleQuestions.groupBy { it.depthLevel }.toSortedMap().forEach { (depth, questions) -> + item(key = "depth-$depth") { + DepthHeader(depth = depth, count = questions.size) + } + items(questions, key = { it.id }) { question -> + QuestionListCard( + question = question, + onClick = { onQuestionSelected(question) } + ) + } + } + } } } } } } +@Composable +private fun CategoryHero( + title: String, + category: QuestionCategory?, + questionCount: Int, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = title, + style = MaterialTheme.typography.headlineLarge.copy(fontWeight = FontWeight.SemiBold), + color = Color(0xFF27211F), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + Text( + text = category?.description + ?: "Browse prompts for this kind of conversation.", + style = MaterialTheme.typography.bodyLarge, + color = Color(0xFF4E4642) + ) + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + CategoryPill("$questionCount ${if (questionCount == 1) "prompt" else "prompts"}", emphasis = true) + category?.access?.let { CategoryPill(it.displayCategoryName()) } + category?.iconName + ?.takeIf { it.isNotBlank() } + ?.let { it.displayCategoryName() } + ?.takeIf { it != "Question" } + ?.let { CategoryPill(it) } + } + } +} + +@Composable +private fun CategoryFilters( + questions: List, + selectedType: String?, + onTypeSelected: (String?) -> Unit +) { + val types = questions.map { it.type }.distinct().sorted() + + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = "Format", + style = MaterialTheme.typography.labelMedium, + color = Color(0xFF4E4642), + fontWeight = FontWeight.SemiBold + ) + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + (listOf(null) + types).forEach { option -> + FilterPill( + label = option?.displayQuestionFilterName() ?: "All", + selected = selectedType == option, + onClick = { onTypeSelected(option) } + ) + } + } + } +} + +private fun String.displayQuestionFilterName(): String { + return when (this) { + "single_choice" -> "Single" + "multi_choice" -> "Multi" + "this_or_that" -> "Either/or" + "scale" -> "Scale" + else -> "Written" + } +} + +@Composable +private fun FilterPill( + label: String, + selected: Boolean, + onClick: () -> Unit +) { + Surface( + modifier = Modifier.clickable(onClick = onClick), + shape = RoundedCornerShape(999.dp), + color = if (selected) Color(0xFFF3E8FF) else Color.White.copy(alpha = 0.74f), + shadowElevation = if (selected) 2.dp else 0.dp + ) { + Text( + text = label, + modifier = Modifier.padding(horizontal = 13.dp, vertical = 8.dp), + style = MaterialTheme.typography.labelMedium, + color = if (selected) Color(0xFF5F3A87) else Color(0xFF3E3734), + fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Medium, + maxLines = 1 + ) + } +} + +@Composable +private fun DepthHeader(depth: Int, count: Int) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 6.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Depth $depth", + style = MaterialTheme.typography.titleMedium, + color = Color(0xFF27211F), + fontWeight = FontWeight.SemiBold + ) + CategoryPill("$count ${if (count == 1) "prompt" else "prompts"}") + } +} + @Composable private fun QuestionListCard( question: Question, @@ -140,13 +283,13 @@ private fun QuestionListCard( Card( onClick = onClick, modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(22.dp), - colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.84f)), - elevation = CardDefaults.cardElevation(defaultElevation = 5.dp) + shape = RoundedCornerShape(20.dp), + colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.9f)), + elevation = CardDefaults.cardElevation(defaultElevation = 3.dp) ) { Column( - modifier = Modifier.padding(17.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) ) { Text( text = question.text, @@ -157,7 +300,6 @@ private fun QuestionListCard( overflow = TextOverflow.Ellipsis ) Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - CategoryPill("Depth ${question.depthLevel}") CategoryPill(question.type.displayQuestionType()) if (question.isPremium) { CategoryPill("Premium") @@ -170,16 +312,19 @@ private fun QuestionListCard( } @Composable -private fun CategoryPill(label: String) { +private fun CategoryPill( + label: String, + emphasis: Boolean = false +) { Surface( shape = RoundedCornerShape(999.dp), - color = Color(0xFFF8F4F1) + color = if (emphasis) Color(0xFFF3E8FF) else Color(0xFFF8F4F1) ) { Text( text = label, modifier = Modifier.padding(horizontal = 11.dp, vertical = 7.dp), style = MaterialTheme.typography.labelMedium, - color = Color(0xFF3E3734), + color = if (emphasis) Color(0xFF5F3A87) else Color(0xFF3E3734), maxLines = 1 ) } diff --git a/app/src/main/java/com/couplesconnect/app/ui/questions/QuestionPackLibraryScreen.kt b/app/src/main/java/com/couplesconnect/app/ui/questions/QuestionPackLibraryScreen.kt index b8a3b0d5..4067316c 100644 --- a/app/src/main/java/com/couplesconnect/app/ui/questions/QuestionPackLibraryScreen.kt +++ b/app/src/main/java/com/couplesconnect/app/ui/questions/QuestionPackLibraryScreen.kt @@ -1,33 +1,36 @@ package com.couplesconnect.app.ui.questions import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll 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.height 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.rememberScrollState import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Lock 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 -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.runtime.collectAsState 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.geometry.Offset @@ -41,6 +44,13 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.couplesconnect.app.core.navigation.AppRoute import com.couplesconnect.app.domain.model.QuestionCategory +private enum class PackFilter(val label: String) { + ALL("All"), + FREE("Free"), + MIXED("Mixed"), + PREMIUM("Premium") +} + @Composable fun QuestionPackLibraryScreen( onNavigate: (String) -> Unit = {}, @@ -61,6 +71,18 @@ private fun QuestionPackLibraryContent( onPackSelected: (QuestionPackItem) -> Unit, onPaywall: () -> Unit ) { + var selectedFilter by remember { mutableStateOf(PackFilter.ALL) } + val visiblePacks = remember(state.packs, selectedFilter) { + state.packs.filter { item -> + when (selectedFilter) { + PackFilter.ALL -> true + PackFilter.FREE -> item.category.access == "free" + PackFilter.MIXED -> item.category.access == "mixed" + PackFilter.PREMIUM -> item.category.access == "premium" + } + } + } + Box( modifier = Modifier .fillMaxSize() @@ -112,8 +134,28 @@ private fun QuestionPackLibraryContent( message = "Question packs are not available right now. Try again in a moment." ) } + visiblePacks.isEmpty() -> { + item { + PackFilterRow( + selected = selectedFilter, + onSelected = { selectedFilter = it } + ) + } + item { + PackMessageCard( + title = "Nothing in ${selectedFilter.label.lowercase()} yet", + message = "Try another filter to keep browsing." + ) + } + } else -> { - items(state.packs, key = { it.category.id }) { item -> + item { + PackFilterRow( + selected = selectedFilter, + onSelected = { selectedFilter = it } + ) + } + items(visiblePacks, key = { it.category.id }) { item -> QuestionPackCard( item = item, onClick = { @@ -148,82 +190,164 @@ private fun QuestionPackCard( onClick: () -> Unit ) { val containerColor = if (item.isLocked) - Color(0xFFF5F0EC).copy(alpha = 0.84f) + Color(0xFFFAF7F5).copy(alpha = 0.9f) else - Color.White.copy(alpha = 0.84f) + Color.White.copy(alpha = 0.9f) + val accent = packAccent(item.category.id) Card( onClick = onClick, modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(26.dp), + shape = RoundedCornerShape(22.dp), colors = CardDefaults.cardColors(containerColor = containerColor), - elevation = CardDefaults.cardElevation(defaultElevation = 8.dp) + elevation = CardDefaults.cardElevation(defaultElevation = 6.dp) ) { - Column( - modifier = Modifier.padding(18.dp), - verticalArrangement = Arrangement.spacedBy(14.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.Top + Column(modifier = Modifier.fillMaxWidth()) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(5.dp) + .background(if (item.isLocked) Color(0xFFD9D1CE) else accent) + ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(17.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) ) { - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(6.dp) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.Top ) { - Text( - text = item.category.displayName.ifBlank { item.category.id.displayCategoryName() }, - style = MaterialTheme.typography.titleLarge, - color = if (item.isLocked) Color(0xFF9E9693) else Color(0xFF27211F), - fontWeight = FontWeight.SemiBold, - maxLines = 2, - overflow = TextOverflow.Ellipsis - ) - Text( - text = item.category.description, - style = MaterialTheme.typography.bodyMedium, - color = Color(0xFF4E4642), - maxLines = 3, - overflow = TextOverflow.Ellipsis - ) + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = item.category.displayName.ifBlank { item.category.id.displayCategoryName() }, + style = MaterialTheme.typography.titleLarge, + color = Color(0xFF27211F), + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = item.category.description, + style = MaterialTheme.typography.bodyMedium, + color = Color(0xFF4E4642), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + PackPill(item.promptCountLabel(), emphasis = true) } - if (item.isLocked) { - Icon( - imageVector = Icons.Default.Lock, - contentDescription = "Premium", - tint = Color(0xFFB0A9A6), - modifier = Modifier.size(20.dp) - ) - } else { - PackPill("${item.questionCount} prompts") + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + item.metadataLabels().forEach { label -> + PackPill(label, emphasis = label == "Premium") + } } } - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - if (item.isPremium) PackPill("Premium") - PackPill(item.category.access.displayCategoryName()) - PackPill(item.category.iconName.ifBlank { "question" }.displayCategoryName()) - } } } } @Composable -private fun PackPill(label: String) { +private fun PackFilterRow( + selected: PackFilter, + onSelected: (PackFilter) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + PackFilter.entries.forEach { filter -> + FilterPill( + label = filter.label, + selected = selected == filter, + onClick = { onSelected(filter) } + ) + } + } +} + +@Composable +private fun FilterPill( + label: String, + selected: Boolean, + onClick: () -> Unit +) { + Surface( + modifier = Modifier.clickable(onClick = onClick), + shape = RoundedCornerShape(999.dp), + color = if (selected) Color(0xFFF3E8FF) else Color.White.copy(alpha = 0.74f), + tonalElevation = 0.dp, + shadowElevation = if (selected) 3.dp else 0.dp + ) { + Text( + text = label, + modifier = Modifier.padding(horizontal = 13.dp, vertical = 8.dp), + style = MaterialTheme.typography.labelMedium, + color = if (selected) Color(0xFF5F3A87) else Color(0xFF3E3734), + fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Medium, + maxLines = 1 + ) + } +} + +@Composable +private fun PackPill( + label: String, + emphasis: Boolean = false +) { Surface( shape = RoundedCornerShape(999.dp), - color = Color(0xFFF8F4F1) + color = if (emphasis) Color(0xFFF3E8FF) else Color(0xFFF8F4F1) ) { Text( text = label, modifier = Modifier.padding(horizontal = 11.dp, vertical = 7.dp), style = MaterialTheme.typography.labelMedium, - color = Color(0xFF3E3734), + color = if (emphasis) Color(0xFF5F3A87) else Color(0xFF3E3734), maxLines = 1 ) } } +private fun QuestionPackItem.promptCountLabel(): String = + "$questionCount ${if (questionCount == 1) "prompt" else "prompts"}" + +private fun QuestionPackItem.metadataLabels(): List { + val access = when (category.access) { + "premium" -> "Premium" + "mixed" -> "Mixed access" + "free" -> "Free" + else -> category.access.displayCategoryName() + } + return listOf( + access, + category.iconName.ifBlank { "question" }.displayCategoryName() + ).filterNot { it == "Question" }.distinct() +} + +private fun packAccent(categoryId: String): Color { + val palette = listOf( + Color(0xFF5F3A87), + Color(0xFF5C7C8A), + Color(0xFF6F7D4F), + Color(0xFF8A5A74), + Color(0xFF7A6A3A) + ) + return palette[kotlin.math.abs(categoryId.hashCode()) % palette.size] +} + @Composable private fun LoadingPackCard() { Card(