From 8fb1385f52ec1da09370e4869ce6cc0ee0f6b359 Mon Sep 17 00:00:00 2001 From: Emil Jiang Date: Tue, 10 Nov 2026 13:58:46 -0500 Subject: [PATCH 1/2] Add report issue screen --- .../score/components/ButtonPrimary.kt | 11 +- .../score/screen/ReportIssueScreen.kt | 231 ++++++++++++++++++ .../score/screen/SettingsScreen.kt | 109 +++++---- app/src/main/res/drawable/report_chevron.xml | 3 + app/src/main/res/values/strings.xml | 20 +- 5 files changed, 328 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/com/cornellappdev/score/screen/ReportIssueScreen.kt create mode 100644 app/src/main/res/drawable/report_chevron.xml diff --git a/app/src/main/java/com/cornellappdev/score/components/ButtonPrimary.kt b/app/src/main/java/com/cornellappdev/score/components/ButtonPrimary.kt index 4aa4108e..8e05c0b5 100644 --- a/app/src/main/java/com/cornellappdev/score/components/ButtonPrimary.kt +++ b/app/src/main/java/com/cornellappdev/score/components/ButtonPrimary.kt @@ -22,11 +22,16 @@ fun ButtonPrimary( text: String, icon: Painter?, modifier: Modifier = Modifier, - onClick: () -> Unit = {} + onClick: () -> Unit = {}, + enabled: Boolean = true ) { Button( onClick = onClick, - colors = ButtonDefaults.buttonColors(containerColor = CrimsonPrimary), + enabled = enabled, + colors = ButtonDefaults.buttonColors( + containerColor = CrimsonPrimary, + disabledContainerColor = CrimsonPrimary.copy(alpha = 0.38f) + ), modifier = modifier, contentPadding = PaddingValues(12.dp) ) { @@ -45,4 +50,4 @@ fun ButtonPrimary( } Text(text = text, style = bodyMedium.copy(color = White)) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/cornellappdev/score/screen/ReportIssueScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/ReportIssueScreen.kt new file mode 100644 index 00000000..4f2bd9ac --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/screen/ReportIssueScreen.kt @@ -0,0 +1,231 @@ +package com.cornellappdev.score.screen + +import androidx.annotation.StringRes +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.cornellappdev.score.R +import com.cornellappdev.score.components.ButtonPrimary +import com.cornellappdev.score.components.NavigationHeader +import com.cornellappdev.score.theme.CrimsonPrimary +import com.cornellappdev.score.theme.GrayLight +import com.cornellappdev.score.theme.GrayMedium +import com.cornellappdev.score.theme.GrayPrimary +import com.cornellappdev.score.theme.GrayStroke +import com.cornellappdev.score.theme.Style +import com.cornellappdev.score.theme.White + +enum class ReportIssueType(@StringRes val label: Int) { + INACCURATE_SCORES(R.string.report_inaccurate_scores), + WRONG_GAME_TIME(R.string.report_wrong_game_time), + MISSING_GAME(R.string.report_missing_game), + OTHER(R.string.report_other) +} + +enum class ReportSport(@StringRes val label: Int) { + BASKETBALL(R.string.report_basketball), + ICE_HOCKEY(R.string.report_ice_hockey), + LACROSSE(R.string.report_lacrosse), + SOCCER(R.string.report_soccer), + FIELD_HOCKEY(R.string.report_field_hockey), + BASEBALL(R.string.report_baseball), + FOOTBALL(R.string.report_football) +} + +data class IssueReport( + val issue: ReportIssueType, + val sport: ReportSport, + val description: String +) + +/** Sending stays disabled until the caller supplies a reporting integration. */ +@Composable +fun ReportIssueScreen( + onBack: () -> Unit, + modifier: Modifier = Modifier, + onSubmit: ((IssueReport) -> Unit)? = null +) { + var issue by rememberSaveable { mutableStateOf(null) } + var sport by rememberSaveable { mutableStateOf(null) } + var description by rememberSaveable { mutableStateOf("") } + val descriptionLabel = stringResource(R.string.report_description) + + Column(modifier.fillMaxSize().background(White).imePadding()) { + NavigationHeader(stringResource(R.string.report_title), onBack) + Column( + Modifier.weight(1f).verticalScroll(rememberScrollState()).padding(16.dp, 24.dp), + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + ReportField(stringResource(R.string.report_issue_question)) { + ReportDropdown( + label = stringResource(R.string.report_issue_question), + selected = issue, + options = ReportIssueType.entries, + optionLabelRes = { it.label }, + onSelect = { issue = it } + ) + } + ReportField(stringResource(R.string.report_sport_question)) { + ReportDropdown( + label = stringResource(R.string.report_sport_question), + selected = sport, + options = ReportSport.entries, + optionLabelRes = { it.label }, + onSelect = { sport = it } + ) + } + ReportField(descriptionLabel) { + BasicTextField( + value = description, + onValueChange = { description = it }, + modifier = Modifier.fillMaxWidth().heightIn(min = 194.dp) + .background(GrayStroke, RoundedCornerShape(8.dp)).padding(12.dp) + .semantics { contentDescription = descriptionLabel }, + textStyle = Style.bodyMedium.copy(color = GrayPrimary, lineHeight = 20.sp), + cursorBrush = SolidColor(CrimsonPrimary), + decorationBox = { innerTextField -> + Box { + if (description.isEmpty()) { + Text( + stringResource(R.string.report_description_hint), + style = Style.bodyMedium, + color = GrayMedium + ) + } + innerTextField() + } + } + ) + } + } + ButtonPrimary( + text = stringResource(R.string.report_submit), + icon = null, + modifier = Modifier.align(Alignment.CenterHorizontally) + .padding(top = 16.dp, bottom = 76.dp), + enabled = onSubmit != null && issue != null && sport != null && description.isNotBlank(), + onClick = { + val selectedIssue = issue + val selectedSport = sport + if (selectedIssue != null && selectedSport != null && description.isNotBlank()) { + onSubmit?.invoke(IssueReport(selectedIssue, selectedSport, description.trim())) + } + } + ) + } +} + +@Composable +private fun ReportField(label: String, content: @Composable () -> Unit) { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + Text( + label, + style = Style.bodySemibold.copy(fontSize = 16.sp, fontWeight = FontWeight.Bold), + color = GrayPrimary + ) + content() + } +} + +@Composable +private fun ReportDropdown( + label: String, + selected: T?, + options: List, + optionLabelRes: (T) -> Int, + onSelect: (T) -> Unit +) { + var expanded by remember { mutableStateOf(false) } + val selectedLabel = selected?.let { stringResource(optionLabelRes(it)) } + ?: stringResource(R.string.report_choose_option) + Box { + Row( + Modifier.fillMaxWidth().background(GrayStroke, RoundedCornerShape(8.dp)) + .clickable(role = Role.Button, onClick = { expanded = !expanded }) + .semantics { + contentDescription = label + stateDescription = selectedLabel + } + .heightIn(min = 48.dp).padding(horizontal = 10.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + selectedLabel, + style = Style.bodyMedium, + color = if (selected == null) GrayMedium else GrayPrimary + ) + Spacer(Modifier.weight(1f)) + Icon( + painterResource(R.drawable.report_chevron), + contentDescription = null, + modifier = Modifier.size(20.dp).rotate(if (expanded) -90f else 90f), + tint = GrayLight + ) + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + containerColor = White + ) { + options.forEach { option -> + DropdownMenuItem( + text = { + Text( + stringResource(optionLabelRes(option)), style = Style.bodyMedium, + color = if (selected == option) CrimsonPrimary else GrayPrimary + ) + }, + onClick = { + onSelect(option) + expanded = false + } + ) + } + } + } +} + +@Preview(showBackground = true, widthDp = 393, heightDp = 805) +@Composable +private fun ReportIssueScreenPreview() { + ReportIssueScreen(onBack = {}) +} diff --git a/app/src/main/java/com/cornellappdev/score/screen/SettingsScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/SettingsScreen.kt index d91df284..28bd28f4 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/SettingsScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/SettingsScreen.kt @@ -1,10 +1,12 @@ package com.cornellappdev.score.screen import androidx.annotation.DrawableRes +import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable 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.Spacer @@ -20,6 +22,10 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -39,45 +45,64 @@ import com.cornellappdev.score.theme.GrayStroke import com.cornellappdev.score.theme.Style @Composable -fun SettingsScreen(onBack: () -> Unit, modifier: Modifier = Modifier) { - Column(modifier.fillMaxSize().background(Color.White)) { - NavigationHeader(stringResource(R.string.profile_settings), onBack) - Column( - Modifier.fillMaxWidth().verticalScroll(rememberScrollState()).padding(24.dp, 32.dp), - verticalArrangement = Arrangement.spacedBy(28.dp) - ) { - SettingsSection(stringResource(R.string.settings_preferences)) { - SettingsRow( - stringResource(R.string.profile_favorite_sports), - R.drawable.settings_star - ) - HorizontalDivider(color = GrayStroke) - SettingsRow( - stringResource(R.string.profile_notifications), - R.drawable.settings_notifications, - iconSize = 28.dp - ) - } - SettingsSection(stringResource(R.string.settings_help)) { - SettingsRow( - stringResource(R.string.settings_about), - R.drawable.settings_info, - iconSize = 33.dp - ) - HorizontalDivider(color = GrayStroke) - SettingsRow(stringResource(R.string.settings_report), R.drawable.settings_flag) - HorizontalDivider(color = GrayStroke) - SettingsRow( - stringResource(R.string.settings_logout), - R.drawable.settings_logout, - destructive = true - ) - HorizontalDivider(color = GrayStroke) - SettingsRow( - stringResource(R.string.settings_delete), - R.drawable.settings_delete, - destructive = true - ) +fun SettingsScreen( + onBack: () -> Unit, + modifier: Modifier = Modifier, + onSubmitIssue: ((IssueReport) -> Unit)? = null +) { + var reportingIssue by rememberSaveable { mutableStateOf(false) } + BackHandler(enabled = reportingIssue) { reportingIssue = false } + Box(modifier) { + if (reportingIssue) { + ReportIssueScreen( + onBack = { reportingIssue = false }, + onSubmit = onSubmitIssue + ) + return@Box + } + Column(Modifier.fillMaxSize().background(Color.White)) { + NavigationHeader(stringResource(R.string.profile_settings), onBack) + Column( + Modifier.fillMaxWidth().verticalScroll(rememberScrollState()).padding(24.dp, 32.dp), + verticalArrangement = Arrangement.spacedBy(28.dp) + ) { + SettingsSection(stringResource(R.string.settings_preferences)) { + SettingsRow( + stringResource(R.string.profile_favorite_sports), + R.drawable.settings_star + ) + HorizontalDivider(color = GrayStroke) + SettingsRow( + stringResource(R.string.profile_notifications), + R.drawable.settings_notifications, + iconSize = 28.dp + ) + } + SettingsSection(stringResource(R.string.settings_help)) { + SettingsRow( + stringResource(R.string.settings_about), + R.drawable.settings_info, + iconSize = 33.dp + ) + HorizontalDivider(color = GrayStroke) + SettingsRow( + stringResource(R.string.settings_report), + R.drawable.settings_flag, + onClick = { reportingIssue = true } + ) + HorizontalDivider(color = GrayStroke) + SettingsRow( + stringResource(R.string.settings_logout), + R.drawable.settings_logout, + destructive = true + ) + HorizontalDivider(color = GrayStroke) + SettingsRow( + stringResource(R.string.settings_delete), + R.drawable.settings_delete, + destructive = true + ) + } } } } @@ -100,11 +125,11 @@ private fun SettingsRow( title: String, @DrawableRes icon: Int, iconSize: Dp = 30.dp, - destructive: Boolean = false + destructive: Boolean = false, + onClick: () -> Unit = {} ) { - // Placeholder actions intentionally do nothing until the settings flows are implemented. Row( - Modifier.fillMaxWidth().clickable(role = Role.Button, onClick = {}) + Modifier.fillMaxWidth().clickable(role = Role.Button, onClick = onClick) .padding(vertical = 16.dp), verticalAlignment = Alignment.CenterVertically ) { diff --git a/app/src/main/res/drawable/report_chevron.xml b/app/src/main/res/drawable/report_chevron.xml new file mode 100644 index 00000000..f0bd969b --- /dev/null +++ b/app/src/main/res/drawable/report_chevron.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 26545d4d..85a7ae88 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -28,4 +28,22 @@ Report an Issue Log Out Delete Account - \ No newline at end of file + Report an issue + What was the issue? + Which sport does it concern? + Choose an option ... + Inaccurate scores + Wrong game time + Missing game + Other + Basketball + Ice Hockey + Lacrosse + Soccer + Field Hockey + Baseball + Football + Describe what’s wrong for us. + What happened? + SUBMIT + From 22f48a6b24a7af49afb399af2e74bdf85873a945 Mon Sep 17 00:00:00 2001 From: Emil Jiang Date: Tue, 10 Nov 2026 14:05:07 -0500 Subject: [PATCH 2/2] Address report issue review feedback --- .../score/components/ButtonPrimary.kt | 16 +++++++++ .../score/screen/ReportIssueScreen.kt | 36 ++++++++++--------- app/src/main/res/values/strings.xml | 7 ---- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/score/components/ButtonPrimary.kt b/app/src/main/java/com/cornellappdev/score/components/ButtonPrimary.kt index 8e05c0b5..2338553c 100644 --- a/app/src/main/java/com/cornellappdev/score/components/ButtonPrimary.kt +++ b/app/src/main/java/com/cornellappdev/score/components/ButtonPrimary.kt @@ -1,9 +1,12 @@ package com.cornellappdev.score.components import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -12,6 +15,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.cornellappdev.score.theme.CrimsonPrimary import com.cornellappdev.score.theme.Style.bodyMedium @@ -51,3 +55,15 @@ fun ButtonPrimary( Text(text = text, style = bodyMedium.copy(color = White)) } } + +@Preview(showBackground = true) +@Composable +private fun ButtonPrimaryPreview() { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + ButtonPrimary(text = "Submit", icon = null) + ButtonPrimary(text = "Submit", icon = null, enabled = false) + } +} diff --git a/app/src/main/java/com/cornellappdev/score/screen/ReportIssueScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/ReportIssueScreen.kt index 4f2bd9ac..06d289f1 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/ReportIssueScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/ReportIssueScreen.kt @@ -46,6 +46,7 @@ import androidx.compose.ui.unit.sp import com.cornellappdev.score.R import com.cornellappdev.score.components.ButtonPrimary import com.cornellappdev.score.components.NavigationHeader +import com.cornellappdev.score.model.Sport import com.cornellappdev.score.theme.CrimsonPrimary import com.cornellappdev.score.theme.GrayLight import com.cornellappdev.score.theme.GrayMedium @@ -61,19 +62,19 @@ enum class ReportIssueType(@StringRes val label: Int) { OTHER(R.string.report_other) } -enum class ReportSport(@StringRes val label: Int) { - BASKETBALL(R.string.report_basketball), - ICE_HOCKEY(R.string.report_ice_hockey), - LACROSSE(R.string.report_lacrosse), - SOCCER(R.string.report_soccer), - FIELD_HOCKEY(R.string.report_field_hockey), - BASEBALL(R.string.report_baseball), - FOOTBALL(R.string.report_football) -} +private val reportSports = listOf( + Sport.BASKETBALL, + Sport.ICE_HOCKEY, + Sport.LACROSSE, + Sport.SOCCER, + Sport.FIELD_HOCKEY, + Sport.BASEBALL, + Sport.FOOTBALL +) data class IssueReport( val issue: ReportIssueType, - val sport: ReportSport, + val sport: Sport, val description: String ) @@ -85,8 +86,9 @@ fun ReportIssueScreen( onSubmit: ((IssueReport) -> Unit)? = null ) { var issue by rememberSaveable { mutableStateOf(null) } - var sport by rememberSaveable { mutableStateOf(null) } + var sport by rememberSaveable { mutableStateOf(null) } var description by rememberSaveable { mutableStateOf("") } + val issueLabels = ReportIssueType.entries.associateWith { stringResource(it.label) } val descriptionLabel = stringResource(R.string.report_description) Column(modifier.fillMaxSize().background(White).imePadding()) { @@ -100,7 +102,7 @@ fun ReportIssueScreen( label = stringResource(R.string.report_issue_question), selected = issue, options = ReportIssueType.entries, - optionLabelRes = { it.label }, + optionLabel = { issueLabels.getValue(it) }, onSelect = { issue = it } ) } @@ -108,8 +110,8 @@ fun ReportIssueScreen( ReportDropdown( label = stringResource(R.string.report_sport_question), selected = sport, - options = ReportSport.entries, - optionLabelRes = { it.label }, + options = reportSports, + optionLabel = { it.displayName }, onSelect = { sport = it } ) } @@ -171,11 +173,11 @@ private fun ReportDropdown( label: String, selected: T?, options: List, - optionLabelRes: (T) -> Int, + optionLabel: (T) -> String, onSelect: (T) -> Unit ) { var expanded by remember { mutableStateOf(false) } - val selectedLabel = selected?.let { stringResource(optionLabelRes(it)) } + val selectedLabel = selected?.let(optionLabel) ?: stringResource(R.string.report_choose_option) Box { Row( @@ -210,7 +212,7 @@ private fun ReportDropdown( DropdownMenuItem( text = { Text( - stringResource(optionLabelRes(option)), style = Style.bodyMedium, + optionLabel(option), style = Style.bodyMedium, color = if (selected == option) CrimsonPrimary else GrayPrimary ) }, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 85a7ae88..2df40dff 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -36,13 +36,6 @@ Wrong game time Missing game Other - Basketball - Ice Hockey - Lacrosse - Soccer - Field Hockey - Baseball - Football Describe what’s wrong for us. What happened? SUBMIT