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 af061fbe..b6383f1c 100644 --- a/app/src/main/java/app/closer/core/navigation/AppNavigation.kt +++ b/app/src/main/java/app/closer/core/navigation/AppNavigation.kt @@ -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 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 1f0a4b6c..4a4e2614 100644 --- a/app/src/main/java/app/closer/core/navigation/AppRoute.kt +++ b/app/src/main/java/app/closer/core/navigation/AppRoute.kt @@ -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, diff --git a/app/src/main/java/app/closer/data/remote/FirestoreThisOrThatDataSource.kt b/app/src/main/java/app/closer/data/remote/FirestoreThisOrThatDataSource.kt index 9e86c417..187e6dae 100644 --- a/app/src/main/java/app/closer/data/remote/FirestoreThisOrThatDataSource.kt +++ b/app/src/main/java/app/closer/data/remote/FirestoreThisOrThatDataSource.kt @@ -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 + val byUser = raw.orEmpty().mapNotNull { (uid, value) -> + (value as? List<*>)?.filterIsInstance()?.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 = callbackFlow { diff --git a/app/src/main/java/app/closer/data/repository/QuestionSessionRepositoryImpl.kt b/app/src/main/java/app/closer/data/repository/QuestionSessionRepositoryImpl.kt index d82e3716..ed312031 100644 --- a/app/src/main/java/app/closer/data/repository/QuestionSessionRepositoryImpl.kt +++ b/app/src/main/java/app/closer/data/repository/QuestionSessionRepositoryImpl.kt @@ -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() ?: 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() ?: emptyList() + ) + }.onFailure { crashReporter.recordException(it) }.getOrNull() + } + }.getOrNull() } diff --git a/app/src/main/java/app/closer/domain/repository/QuestionSessionRepository.kt b/app/src/main/java/app/closer/domain/repository/QuestionSessionRepository.kt index 42f0b8aa..240cc097 100644 --- a/app/src/main/java/app/closer/domain/repository/QuestionSessionRepository.kt +++ b/app/src/main/java/app/closer/domain/repository/QuestionSessionRepository.kt @@ -17,4 +17,7 @@ interface QuestionSessionRepository { // Force-complete the active session (escape hatch for stuck/abandoned games). suspend fun abandonSession(coupleId: String): Result + + // Single-session lookup by ID (for history replay). + suspend fun getSessionById(coupleId: String, sessionId: String): QuestionSession? } diff --git a/app/src/main/java/app/closer/ui/desiresync/DesireSyncScreen.kt b/app/src/main/java/app/closer/ui/desiresync/DesireSyncScreen.kt index 4272bc33..cb8ed1b9 100644 --- a/app/src/main/java/app/closer/ui/desiresync/DesireSyncScreen.kt +++ b/app/src/main/java/app/closer/ui/desiresync/DesireSyncScreen.kt @@ -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, 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 = _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) } + ) + } + } +} diff --git a/app/src/main/java/app/closer/ui/howwell/HowWellScreen.kt b/app/src/main/java/app/closer/ui/howwell/HowWellScreen.kt index 6f875d5d..1f52ef61 100644 --- a/app/src/main/java/app/closer/ui/howwell/HowWellScreen.kt +++ b/app/src/main/java/app/closer/ui/howwell/HowWellScreen.kt @@ -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, 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 = _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) } + ) + } + } +} diff --git a/app/src/main/java/app/closer/ui/play/PlayHubScreen.kt b/app/src/main/java/app/closer/ui/play/PlayHubScreen.kt index 7ff6d864..d9385882 100644 --- a/app/src/main/java/app/closer/ui/play/PlayHubScreen.kt +++ b/app/src/main/java/app/closer/ui/play/PlayHubScreen.kt @@ -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) } ) } } diff --git a/app/src/main/java/app/closer/ui/thisorthat/ThisOrThatScreen.kt b/app/src/main/java/app/closer/ui/thisorthat/ThisOrThatScreen.kt index 50063475..728215d4 100644 --- a/app/src/main/java/app/closer/ui/thisorthat/ThisOrThatScreen.kt +++ b/app/src/main/java/app/closer/ui/thisorthat/ThisOrThatScreen.kt @@ -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) : 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 = _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 diff --git a/app/src/main/java/app/closer/ui/wheel/WheelHistoryScreen.kt b/app/src/main/java/app/closer/ui/wheel/WheelHistoryScreen.kt index 4b2def6c..1b21393d 100644 --- a/app/src/main/java/app/closer/ui/wheel/WheelHistoryScreen.kt +++ b/app/src/main/java/app/closer/ui/wheel/WheelHistoryScreen.kt @@ -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,