feat: add game history screen, shared session state, updated navigation + PlayHub

This commit is contained in:
null 2026-06-19 01:43:06 -05:00
parent 3ba6c659dd
commit 70e7c66cd6
10 changed files with 457 additions and 11 deletions

View File

@ -51,8 +51,11 @@ import app.closer.ui.play.PlayHubScreen
import app.closer.ui.challenges.ConnectionChallengesScreen
import app.closer.ui.memorylane.MemoryLaneScreen
import app.closer.ui.desiresync.DesireSyncScreen
import app.closer.ui.desiresync.DSReplayScreen
import app.closer.ui.howwell.HowWellScreen
import app.closer.ui.howwell.HowWellReplayScreen
import app.closer.ui.thisorthat.ThisOrThatScreen
import app.closer.ui.thisorthat.ThisOrThatReplayScreen
import app.closer.ui.questions.DailyQuestionScreen
import app.closer.ui.questions.QuestionCategoryScreen
import app.closer.ui.questions.QuestionComposerScreen
@ -304,6 +307,27 @@ fun AppNavigation(
composable(route = AppRoute.WHEEL_HISTORY) {
WheelHistoryScreen(onNavigate = navigateRoute)
}
composable(route = AppRoute.GAME_HISTORY) {
WheelHistoryScreen(onNavigate = navigateRoute)
}
composable(
route = AppRoute.THIS_OR_THAT_REPLAY,
arguments = listOf(navArgument("sessionId") { type = NavType.StringType })
) {
ThisOrThatReplayScreen(onNavigate = navigateRoute)
}
composable(
route = AppRoute.DESIRE_SYNC_REPLAY,
arguments = listOf(navArgument("sessionId") { type = NavType.StringType })
) {
DSReplayScreen(onNavigate = navigateRoute)
}
composable(
route = AppRoute.HOW_WELL_REPLAY,
arguments = listOf(navArgument("sessionId") { type = NavType.StringType })
) {
HowWellReplayScreen(onNavigate = navigateRoute)
}
composable(route = AppRoute.THIS_OR_THAT) {
ThisOrThatScreen(onNavigate = navigateRoute)
}
@ -402,6 +426,11 @@ private val shellBackRoutes = setOf(
AppRoute.CONNECTION_CHALLENGES,
AppRoute.WAITING_FOR_PARTNER,
AppRoute.SUBSCRIPTION,
AppRoute.WHEEL_HISTORY,
AppRoute.GAME_HISTORY,
AppRoute.THIS_OR_THAT_REPLAY,
AppRoute.DESIRE_SYNC_REPLAY,
AppRoute.HOW_WELL_REPLAY,
)
@Composable

View File

@ -34,6 +34,10 @@ object AppRoute {
const val RELATIONSHIP_SETTINGS = "relationship_settings"
const val DELETE_ACCOUNT = "delete_account"
const val WHEEL_HISTORY = "wheel_history"
const val GAME_HISTORY = "game_history"
const val THIS_OR_THAT_REPLAY = "this_or_that_replay/{sessionId}"
const val DESIRE_SYNC_REPLAY = "desire_sync_replay/{sessionId}"
const val HOW_WELL_REPLAY = "how_well_replay/{sessionId}"
const val DATE_MATCH = "date_match"
const val DATE_MATCHES = "date_matches"
const val DATE_BUILDER = "date_builder"
@ -88,6 +92,10 @@ object AppRoute {
Definition(RELATIONSHIP_SETTINGS, "Relationship Settings", "settings"),
Definition(DELETE_ACCOUNT, "Delete Account", "settings"),
Definition(WHEEL_HISTORY, "Wheel History", "wheel"),
Definition(GAME_HISTORY, "Past Games", "play"),
Definition(THIS_OR_THAT_REPLAY, "This or That Results", "play"),
Definition(DESIRE_SYNC_REPLAY, "Desire Sync Results", "play"),
Definition(HOW_WELL_REPLAY, "How Well Results", "play"),
Definition(DATE_MATCH, "Date Match", "dates"),
Definition(DATE_MATCHES, "Matches", "dates"),
Definition(DATE_BUILDER, "Plan a Date", "dates"),
@ -135,6 +143,10 @@ object AppRoute {
WHEEL_SESSION,
WHEEL_COMPLETE,
WHEEL_HISTORY,
GAME_HISTORY,
THIS_OR_THAT_REPLAY,
DESIRE_SYNC_REPLAY,
HOW_WELL_REPLAY,
DATE_MATCH,
DATE_MATCHES,
DATE_BUILDER,
@ -165,6 +177,12 @@ object AppRoute {
fun wheelComplete(sessionId: String): String = "wheel_complete/${sessionId.asRouteArg()}"
fun thisOrThatReplay(sessionId: String): String = "this_or_that_replay/${sessionId.asRouteArg()}"
fun desireSyncReplay(sessionId: String): String = "desire_sync_replay/${sessionId.asRouteArg()}"
fun howWellReplay(sessionId: String): String = "how_well_replay/${sessionId.asRouteArg()}"
fun questionThread(
coupleId: String,
questionId: String,

View File

@ -47,6 +47,19 @@ class FirestoreThisOrThatDataSource @Inject constructor(
.await()
}
/** One-shot load of both partners' picks (for history replay). */
suspend fun getAnswers(coupleId: String, sessionId: String): ThisOrThatAnswers? =
runCatching {
val snap = doc(coupleId, sessionId).get().await()
if (!snap.exists()) return@runCatching null
@Suppress("UNCHECKED_CAST")
val raw = snap.get("answers") as? Map<String, *>
val byUser = raw.orEmpty().mapNotNull { (uid, value) ->
(value as? List<*>)?.filterIsInstance<String>()?.let { uid to it }
}.toMap()
ThisOrThatAnswers(byUser)
}.getOrNull()
/** Live view of both partners' picks; emits whenever either side submits. */
fun observeAnswers(coupleId: String, sessionId: String): Flow<ThisOrThatAnswers> =
callbackFlow {

View File

@ -189,4 +189,34 @@ class QuestionSessionRepositoryImpl @Inject constructor(
)
).getOrThrow()
}
override suspend fun getSessionById(coupleId: String, sessionId: String): QuestionSession? =
runCatching {
firestore.collection(FirestoreCollections.COUPLES)
.document(coupleId)
.collection(FirestoreCollections.Couples.SESSIONS)
.document(sessionId)
.get()
.await()
.takeIf { it.exists() }
?.let { doc ->
runCatching {
QuestionSession(
id = doc.getString("id") ?: doc.id,
coupleId = doc.getString("coupleId") ?: coupleId,
categoryId = doc.getString("categoryId") ?: "",
questionIds = (doc.get("questionIds") as? List<*>)
?.filterIsInstance<String>() ?: emptyList(),
startedByUserId = doc.getString("startedByUserId") ?: "",
startedAt = doc.getLong("startedAt") ?: 0L,
completedAt = doc.getLong("completedAt"),
isPremium = doc.getBoolean("isPremium") ?: false,
status = doc.getString("status") ?: "completed",
gameType = doc.getString("gameType") ?: GameType.WHEEL,
completedByUsers = (doc.get("completedByUsers") as? List<*>)
?.filterIsInstance<String>() ?: emptyList()
)
}.onFailure { crashReporter.recordException(it) }.getOrNull()
}
}.getOrNull()
}

View File

@ -17,4 +17,7 @@ interface QuestionSessionRepository {
// Force-complete the active session (escape hatch for stuck/abandoned games).
suspend fun abandonSession(coupleId: String): Result<Unit>
// Single-session lookup by ID (for history replay).
suspend fun getSessionById(coupleId: String, sessionId: String): QuestionSession?
}

View File

@ -50,6 +50,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.closer.core.navigation.AppRoute
@ -57,6 +58,7 @@ import app.closer.data.remote.FirestoreDesireSyncDataSource
import app.closer.domain.model.ChoiceAnswerConfigImpl
import app.closer.domain.model.Question
import app.closer.domain.repository.QuestionRepository
import app.closer.domain.repository.QuestionSessionRepository
import app.closer.domain.usecase.GameSessionManager
import app.closer.ui.components.StatusGlyph
import app.closer.ui.theme.CloserPalette
@ -847,3 +849,105 @@ private fun DesireMatchCard(match: DesireMatch) {
}
}
}
// ── History Replay ────────────────────────────────────────────────────────────
sealed interface DSReplayPhase {
object Loading : DSReplayPhase
data class Ready(val matches: List<DesireMatch>, val total: Int) : DSReplayPhase
data class Error(val message: String) : DSReplayPhase
}
data class DSReplayUiState(
val phase: DSReplayPhase = DSReplayPhase.Loading,
val partnerName: String = "Your partner"
)
@HiltViewModel
class DSReplayViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val gameSessionManager: GameSessionManager,
private val answerDataSource: FirestoreDesireSyncDataSource,
private val questionRepository: QuestionRepository,
private val sessionRepository: QuestionSessionRepository
) : ViewModel() {
private val sessionId: String = savedStateHandle["sessionId"] ?: ""
private val _uiState = MutableStateFlow(DSReplayUiState())
val uiState: StateFlow<DSReplayUiState> = _uiState.asStateFlow()
init {
viewModelScope.launch {
val uid = gameSessionManager.currentUserId ?: run {
_uiState.update { it.copy(phase = DSReplayPhase.Error("Not signed in")) }
return@launch
}
val couple = gameSessionManager.getCoupleForUser(uid) ?: run {
_uiState.update { it.copy(phase = DSReplayPhase.Error("No couple found")) }
return@launch
}
val partnerId = couple.userIds.firstOrNull { it != uid }
val partnerName = partnerId?.let { gameSessionManager.getUser(it) }?.displayName
?: "Your partner"
_uiState.update { it.copy(partnerName = partnerName) }
val session = sessionRepository.getSessionById(couple.id, sessionId) ?: run {
_uiState.update { it.copy(phase = DSReplayPhase.Error("Session not found")) }
return@launch
}
val answers = answerDataSource.getAnswers(couple.id, sessionId) ?: run {
_uiState.update { it.copy(phase = DSReplayPhase.Error("Answers not found")) }
return@launch
}
val questions = session.questionIds.mapNotNull { questionRepository.getQuestionById(it) }
val mine = answers.byUser[uid].orEmpty()
val theirs = partnerId?.let { answers.byUser[it] }.orEmpty()
val matches = questions.indices.mapNotNull { i ->
val a = mine.getOrNull(i)?.lowercase()
val b = theirs.getOrNull(i)?.lowercase()
if (a != null && b != null && a in POSITIVE_IDS && b in POSITIVE_IDS) {
DesireMatch(questions[i])
} else null
}
_uiState.update { it.copy(phase = DSReplayPhase.Ready(matches, questions.size)) }
}
}
}
@Composable
fun DSReplayScreen(
onNavigate: (String) -> Unit = {},
viewModel: DSReplayViewModel = hiltViewModel()
) {
val state by viewModel.uiState.collectAsState()
val phase = state.phase
Box(
modifier = Modifier
.fillMaxSize()
.background(closerBackgroundBrush())
) {
when (phase) {
is DSReplayPhase.Loading -> CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center),
color = CloserPalette.Romantic
)
is DSReplayPhase.Error -> Column(
modifier = Modifier
.align(Alignment.Center)
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(phase.message, textAlign = TextAlign.Center)
OutlinedButton(onClick = { onNavigate(AppRoute.GAME_HISTORY) }) { Text("Back") }
}
is DSReplayPhase.Ready -> DSRevealScreen(
matches = phase.matches,
total = phase.total,
partnerName = state.partnerName,
onPlayAgain = { onNavigate(AppRoute.DESIRE_SYNC) },
onHome = { onNavigate(AppRoute.PLAY) }
)
}
}
}

View File

@ -53,6 +53,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.closer.core.navigation.AppRoute
@ -63,6 +64,7 @@ import app.closer.domain.model.Question
import app.closer.domain.model.ScaleAnswerConfigImpl
import app.closer.domain.model.ThisOrThatAnswerConfigImpl
import app.closer.domain.repository.QuestionRepository
import app.closer.domain.repository.QuestionSessionRepository
import app.closer.data.remote.FirestoreHowWellDataSource
import app.closer.data.remote.HowWellAnswers
import app.closer.data.remote.HowWellRawAnswer
@ -1027,3 +1029,113 @@ private fun scoreLabel(score: Int, total: Int): String = when {
score >= total * 0.4 -> "Getting there!"
else -> "Room to grow"
}
// ── History Replay ────────────────────────────────────────────────────────────
sealed interface HWReplayPhase {
object Loading : HWReplayPhase
data class Ready(val score: Int, val total: Int, val results: List<HowWellResult>, val amSubject: Boolean) : HWReplayPhase
data class Error(val message: String) : HWReplayPhase
}
data class HWReplayUiState(
val phase: HWReplayPhase = HWReplayPhase.Loading,
val partnerName: String = "Your partner"
)
@HiltViewModel
class HowWellReplayViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val gameSessionManager: GameSessionManager,
private val answerDataSource: FirestoreHowWellDataSource,
private val questionRepository: QuestionRepository,
private val sessionRepository: QuestionSessionRepository
) : ViewModel() {
private val sessionId: String = savedStateHandle["sessionId"] ?: ""
private val _uiState = MutableStateFlow(HWReplayUiState())
val uiState: StateFlow<HWReplayUiState> = _uiState.asStateFlow()
init {
viewModelScope.launch {
val uid = gameSessionManager.currentUserId ?: run {
_uiState.update { it.copy(phase = HWReplayPhase.Error("Not signed in")) }
return@launch
}
val couple = gameSessionManager.getCoupleForUser(uid) ?: run {
_uiState.update { it.copy(phase = HWReplayPhase.Error("No couple found")) }
return@launch
}
val partnerId = couple.userIds.firstOrNull { it != uid }
val partnerName = partnerId?.let { gameSessionManager.getUser(it) }?.displayName
?: "Your partner"
_uiState.update { it.copy(partnerName = partnerName) }
val session = sessionRepository.getSessionById(couple.id, sessionId) ?: run {
_uiState.update { it.copy(phase = HWReplayPhase.Error("Session not found")) }
return@launch
}
val answers = answerDataSource.getAnswers(couple.id, sessionId) ?: run {
_uiState.update { it.copy(phase = HWReplayPhase.Error("Answers not found")) }
return@launch
}
val questions = session.questionIds.mapNotNull { questionRepository.getQuestionById(it) }
val subjectId = session.startedByUserId
val guesserId = if (subjectId == uid) partnerId else uid
val subjectAnswers = answers.byUser[subjectId].orEmpty()
val guesserAnswers = guesserId?.let { answers.byUser[it] }.orEmpty()
val results = questions.mapIndexed { i, q ->
val actual = subjectAnswers.getOrNull(i).toAnswer()
val prediction = guesserAnswers.getOrNull(i).toAnswer()
HowWellResult(q, actual, prediction, prediction.isMatch(actual), prediction.isClose(actual))
}
val score = results.count { it.isMatch }
val amSubject = subjectId == uid
_uiState.update { it.copy(phase = HWReplayPhase.Ready(score, questions.size, results, amSubject)) }
}
}
private fun HowWellRawAnswer?.toAnswer(): HowWellAnswer =
HowWellAnswer(this?.optionId, this?.scale)
}
@Composable
fun HowWellReplayScreen(
onNavigate: (String) -> Unit = {},
viewModel: HowWellReplayViewModel = hiltViewModel()
) {
val state by viewModel.uiState.collectAsState()
val phase = state.phase
Box(
modifier = Modifier
.fillMaxSize()
.background(closerBackgroundBrush())
) {
when (phase) {
is HWReplayPhase.Loading -> CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center),
color = CloserPalette.PurpleDeep
)
is HWReplayPhase.Error -> Column(
modifier = Modifier
.align(Alignment.Center)
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(phase.message, textAlign = TextAlign.Center)
OutlinedButton(onClick = { onNavigate(AppRoute.GAME_HISTORY) }) { Text("Back") }
}
is HWReplayPhase.Ready -> CompleteScreen(
score = phase.score,
total = phase.total,
results = phase.results,
amSubject = phase.amSubject,
partnerName = state.partnerName,
onPlayAgain = { onNavigate(AppRoute.HOW_WELL) },
onHome = { onNavigate(AppRoute.PLAY) }
)
}
}
}

View File

@ -166,12 +166,12 @@ private fun PlayHubContent(
onClick = { onNavigate(AppRoute.BUCKET_LIST) }
)
CompactPlayCard(
title = "Wheel History",
subtitle = "Past sessions",
title = "Past Games",
subtitle = "All results",
icon = Icons.Filled.Home,
tint = CloserPalette.PurpleDeep,
modifier = Modifier.weight(1f),
onClick = { onNavigate(AppRoute.WHEEL_HISTORY) }
onClick = { onNavigate(AppRoute.GAME_HISTORY) }
)
}
}

View File

@ -59,6 +59,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.closer.core.navigation.AppRoute
@ -68,6 +69,7 @@ import app.closer.domain.model.Question
import app.closer.domain.model.ThisOrThatAnswerConfig
import app.closer.domain.model.ThisOrThatAnswerConfigImpl
import app.closer.domain.repository.QuestionRepository
import app.closer.domain.repository.QuestionSessionRepository
import app.closer.domain.usecase.GameSessionManager
import app.closer.ui.theme.CloserPalette
import app.closer.ui.theme.closerBackgroundBrush
@ -992,6 +994,122 @@ private fun ErrorState(message: String, onBack: () -> Unit) {
}
}
// ── History Replay ────────────────────────────────────────────────────────────
sealed interface TotReplayPhase {
object Loading : TotReplayPhase
data class Ready(val matched: Int, val total: Int, val cards: List<RevealCard>) : TotReplayPhase
data class Error(val message: String) : TotReplayPhase
}
data class TotReplayUiState(
val phase: TotReplayPhase = TotReplayPhase.Loading,
val partnerName: String = "Your partner"
)
@HiltViewModel
class ThisOrThatReplayViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val gameSessionManager: GameSessionManager,
private val answerDataSource: FirestoreThisOrThatDataSource,
private val questionRepository: QuestionRepository,
private val sessionRepository: QuestionSessionRepository
) : ViewModel() {
private val sessionId: String = savedStateHandle["sessionId"] ?: ""
private val _uiState = MutableStateFlow(TotReplayUiState())
val uiState: StateFlow<TotReplayUiState> = _uiState.asStateFlow()
init {
viewModelScope.launch {
val uid = gameSessionManager.currentUserId ?: run {
_uiState.update { it.copy(phase = TotReplayPhase.Error("Not signed in")) }
return@launch
}
val couple = gameSessionManager.getCoupleForUser(uid) ?: run {
_uiState.update { it.copy(phase = TotReplayPhase.Error("No couple found")) }
return@launch
}
val partnerId = couple.userIds.firstOrNull { it != uid }
val partnerName = partnerId?.let { gameSessionManager.getUser(it) }?.displayName
?: "Your partner"
_uiState.update { it.copy(partnerName = partnerName) }
val session = sessionRepository.getSessionById(couple.id, sessionId) ?: run {
_uiState.update { it.copy(phase = TotReplayPhase.Error("Session not found")) }
return@launch
}
val answers = answerDataSource.getAnswers(couple.id, sessionId) ?: run {
_uiState.update { it.copy(phase = TotReplayPhase.Error("Answers not found")) }
return@launch
}
val questions = session.questionIds.mapNotNull { questionRepository.getQuestionById(it) }
val mine = answers.byUser[uid].orEmpty()
val theirs = partnerId?.let { answers.byUser[it] }.orEmpty()
val cards = questions.mapIndexed { i, q ->
val config = q.answerConfig as? ThisOrThatAnswerConfigImpl
val myOpt = mine.getOrNull(i)
val theirOpt = theirs.getOrNull(i)
RevealCard(
questionText = q.text,
myText = replayOptionText(config, myOpt),
partnerText = replayOptionText(config, theirOpt),
agreed = myOpt != null && myOpt == theirOpt
)
}
val matched = cards.count { it.agreed }
_uiState.update { it.copy(phase = TotReplayPhase.Ready(matched, cards.size, cards)) }
}
}
private fun replayOptionText(config: ThisOrThatAnswerConfigImpl?, optionId: String?): String =
when (optionId) {
null -> ""
config?.config?.optionA?.id -> config.config.optionA.text
config?.config?.optionB?.id -> config.config.optionB.text
else -> optionId
}
}
@Composable
fun ThisOrThatReplayScreen(
onNavigate: (String) -> Unit = {},
viewModel: ThisOrThatReplayViewModel = hiltViewModel()
) {
val state by viewModel.uiState.collectAsState()
val phase = state.phase
Box(
modifier = Modifier
.fillMaxSize()
.background(closerBackgroundBrush())
) {
when (phase) {
is TotReplayPhase.Loading -> CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center),
color = CloserPalette.PurpleDeep
)
is TotReplayPhase.Error -> Column(
modifier = Modifier
.align(Alignment.Center)
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(phase.message, textAlign = TextAlign.Center)
OutlinedButton(onClick = { onNavigate(AppRoute.GAME_HISTORY) }) { Text("Back") }
}
is TotReplayPhase.Ready -> ThisOrThatReveal(
matched = phase.matched,
total = phase.total,
partnerName = state.partnerName,
cards = phase.cards,
onPlayAgain = { onNavigate(AppRoute.THIS_OR_THAT) },
onHome = { onNavigate(AppRoute.PLAY) }
)
}
}
}
// ── Preview ───────────────────────────────────────────────────────────────────
@Preview

View File

@ -45,6 +45,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import app.closer.core.navigation.AppRoute
import app.closer.domain.model.GameType
import app.closer.domain.model.QuestionSession
import app.closer.ui.components.EmptyState
import app.closer.ui.components.ErrorState
@ -92,7 +93,7 @@ fun WheelHistoryScreen(
) {
item {
Text(
text = "Spin sessions",
text = "Past games",
style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.SemiBold),
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(top = 20.dp, bottom = 4.dp)
@ -113,13 +114,16 @@ fun WheelHistoryScreen(
state.sessions.isEmpty() -> item {
EmptyState(
title = "No sessions yet",
body = "Completed spin wheel sessions will appear here.",
actionLabel = "Spin now",
onAction = { onNavigate(AppRoute.CATEGORY_PICKER) }
body = "Completed games will appear here.",
actionLabel = "Play now",
onAction = { onNavigate(AppRoute.PLAY) }
)
}
else -> items(state.sessions, key = { it.id }) { session ->
WheelSessionCard(session = session)
WheelSessionCard(
session = session,
onClick = { onNavigate(sessionReplayRoute(session)) }
)
}
}
}
@ -128,8 +132,9 @@ fun WheelHistoryScreen(
}
@Composable
private fun WheelSessionCard(session: QuestionSession) {
private fun WheelSessionCard(session: QuestionSession, onClick: () -> Unit = {}) {
Card(
onClick = onClick,
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = closerCardColor(alpha = 0.88f)),
@ -142,7 +147,7 @@ private fun WheelSessionCard(session: QuestionSession) {
) {
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(
text = session.categoryId.displayCategoryName(),
text = sessionTitle(session),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurface
@ -164,6 +169,20 @@ private fun WheelSessionCard(session: QuestionSession) {
}
}
private fun sessionTitle(session: QuestionSession): String = when (session.gameType) {
GameType.THIS_OR_THAT -> "This or That"
GameType.HOW_WELL -> "How Well Do You Know Me"
GameType.DESIRE_SYNC -> "Desire Sync"
else -> session.categoryId.displayCategoryName().ifBlank { "Spin Wheel" }
}
private fun sessionReplayRoute(session: QuestionSession): String = when (session.gameType) {
GameType.THIS_OR_THAT -> AppRoute.thisOrThatReplay(session.id)
GameType.HOW_WELL -> AppRoute.howWellReplay(session.id)
GameType.DESIRE_SYNC -> AppRoute.desireSyncReplay(session.id)
else -> AppRoute.wheelComplete(session.id)
}
@Composable
private fun WheelHistoryLockedCard(onUnlock: () -> Unit) {
Card(
@ -197,7 +216,7 @@ private fun WheelHistoryLockedCard(onUnlock: () -> Unit) {
overflow = TextOverflow.Ellipsis
)
Text(
text = "Unlock to browse all your past spin wheel sessions together.",
text = "Unlock to browse all your past game results together.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 3,