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..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 @@ -22,11 +26,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 +54,16 @@ fun ButtonPrimary( } Text(text = text, style = bodyMedium.copy(color = White)) } -} \ No newline at end of file +} + +@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 new file mode 100644 index 00000000..06d289f1 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/screen/ReportIssueScreen.kt @@ -0,0 +1,233 @@ +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.model.Sport +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) +} + +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: Sport, + 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 issueLabels = ReportIssueType.entries.associateWith { stringResource(it.label) } + 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, + optionLabel = { issueLabels.getValue(it) }, + onSelect = { issue = it } + ) + } + ReportField(stringResource(R.string.report_sport_question)) { + ReportDropdown( + label = stringResource(R.string.report_sport_question), + selected = sport, + options = reportSports, + optionLabel = { it.displayName }, + 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, + optionLabel: (T) -> String, + onSelect: (T) -> Unit +) { + var expanded by remember { mutableStateOf(false) } + val selectedLabel = selected?.let(optionLabel) + ?: 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( + optionLabel(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..2df40dff 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -28,4 +28,15 @@ 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 + Describe what’s wrong for us. + What happened? + SUBMIT +