From 054f0016d40bbc9234a147f3ab2ddd6d48ab2f54 Mon Sep 17 00:00:00 2001 From: Emil Jiang Date: Fri, 19 Sep 2025 15:49:43 -0400 Subject: [PATCH 1/8] git issue fix --- app/build.gradle.kts | 1 + .../com/cornellappdev/score/screen/GameDetailsScreen.kt | 7 ++++++- .../cornellappdev/score/screen/GameScoreSummaryScreen.kt | 3 ++- gradle/libs.versions.toml | 2 ++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fca0fe5..d6c534f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -71,6 +71,7 @@ dependencies { implementation("androidx.navigation:navigation-compose:2.8.2") implementation(libs.material3) implementation("com.google.dagger:hilt-android:2.51.1") + implementation(libs.androidx.material3) kapt("com.google.dagger:hilt-android-compiler:2.51.1") implementation("androidx.hilt:hilt-navigation-compose:1.0.0") implementation("com.google.accompanist:accompanist-pager:0.24.0-alpha") diff --git a/app/src/main/java/com/cornellappdev/score/screen/GameDetailsScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/GameDetailsScreen.kt index d2f01bf..4062de6 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/GameDetailsScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/GameDetailsScreen.kt @@ -3,6 +3,7 @@ package com.cornellappdev.score.screen import ScoringSummary import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -106,6 +107,7 @@ fun GameDetailsContent( gameCard: DetailsCardData, navigateToGameScoreSummary: (List) -> Unit ) { + val scrollState = rememberScrollState() Column( modifier = Modifier .background(White) @@ -132,7 +134,10 @@ fun GameDetailsContent( ) Text( text = gameCard.title, - style = heading1.copy(color = GrayPrimary) + style = heading1.copy(color = GrayPrimary), + maxLines = 1, + modifier = Modifier + .horizontalScroll(scrollState) ) Spacer(modifier = Modifier.height(13.5.dp)) diff --git a/app/src/main/java/com/cornellappdev/score/screen/GameScoreSummaryScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/GameScoreSummaryScreen.kt index 0c94504..a9514f9 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/GameScoreSummaryScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/GameScoreSummaryScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -43,7 +44,7 @@ fun GameScoreSummaryScreenDetail(scoreEvents: List, onBackArrow: () ) { items(scoreEvents.size) { event -> ScoreEventItemDetailed(event = scoreEvents[event]) - Divider(color = Color.LightGray, thickness = 0.5.dp) + HorizontalDivider(thickness = 0.5.dp, color = Color.LightGray) } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0fa6263..b53ee5a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,6 +16,7 @@ media3CommonKtx = "1.5.1" # Using alpha version due to bug with pull to refresh in the latest stabel version # See https://stackoverflow.com/a/79126321 material3 = "1.4.0-alpha11" +material3Version = "1.3.2" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -31,6 +32,7 @@ androidx-constraintlayout = { group = "androidx.constraintlayout", name = "const androidx-runtime-android = { group = "androidx.compose.runtime", name = "runtime-android", version.ref = "runtimeAndroid" } apollo-runtime = { module = "com.apollographql.apollo:apollo-runtime" } androidx-media3-common-ktx = { group = "androidx.media3", name = "media3-common-ktx", version.ref = "media3CommonKtx" } +androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3Version" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } From d1e732c9c340a6e9f509f452bf7e032222fbece1 Mon Sep 17 00:00:00 2001 From: Emil Jiang Date: Tue, 30 Sep 2025 14:04:49 -0400 Subject: [PATCH 2/8] advanced fitler first iteration --- app/build.gradle.kts | 4 +- .../score/components/ButtonPrimary.kt | 11 +- .../score/components/ExpandableSection.kt | 66 ++++++++++++ .../cornellappdev/score/screen/HomeScreen.kt | 102 +++++++++++++++--- app/src/main/res/drawable/advanced_filter.xml | 13 +++ app/src/main/res/drawable/ic_round_minus.xml | 9 ++ app/src/main/res/drawable/ic_round_plus.xml | 9 ++ build.gradle.kts | 3 +- 8 files changed, 201 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/com/cornellappdev/score/components/ExpandableSection.kt create mode 100644 app/src/main/res/drawable/advanced_filter.xml create mode 100644 app/src/main/res/drawable/ic_round_minus.xml create mode 100644 app/src/main/res/drawable/ic_round_plus.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d6c534f..182d1ca 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -6,7 +6,7 @@ plugins { id("com.google.dagger.hilt.android") id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" // this version matches your Kotlin version id("org.jetbrains.kotlin.plugin.serialization") - + id("com.google.gms.google-services") } @@ -94,6 +94,8 @@ dependencies { implementation("io.coil-kt.coil3:coil-compose:3.1.0") implementation("io.coil-kt.coil3:coil-network-okhttp:3.1.0") lintChecks(libs.compose.lint.checks) + implementation(platform("com.google.firebase:firebase-bom:34.3.0")) + implementation("com.google.firebase:firebase-analytics") } apollo { 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 6c9b046..0e6e074 100644 --- a/app/src/main/java/com/cornellappdev/score/components/ButtonPrimary.kt +++ b/app/src/main/java/com/cornellappdev/score/components/ButtonPrimary.kt @@ -18,9 +18,16 @@ import com.cornellappdev.score.theme.Style.bodyMedium import com.cornellappdev.score.theme.White @Composable -fun ButtonPrimary(text: String, icon: Painter?, onClick: () -> Unit = {}) { - Button(onClick = onClick, +fun ButtonPrimary( + text: String, + icon: Painter?, + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + Button( + onClick = onClick, colors = ButtonDefaults.buttonColors(containerColor = CrimsonPrimary), + modifier = modifier, contentPadding = PaddingValues(12.dp) ) { if (icon != null) { diff --git a/app/src/main/java/com/cornellappdev/score/components/ExpandableSection.kt b/app/src/main/java/com/cornellappdev/score/components/ExpandableSection.kt new file mode 100644 index 0000000..7c1c671 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/components/ExpandableSection.kt @@ -0,0 +1,66 @@ +package com.cornellappdev.score.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.RadioButton +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.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.cornellappdev.score.R + +@Composable +fun ExpandableSection(title: String, options: List) { + var expanded by remember { mutableStateOf(false) } + var selectedOption by remember { mutableStateOf("Under $20") } + + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { expanded = !expanded }, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text(title, fontSize = 18.sp) + Icon( + painter = painterResource( + id = if (expanded) R.drawable.ic_round_minus else R.drawable.ic_round_plus + ), + contentDescription = if (expanded) "Collapse" else "Expand" + ) + } + + if (expanded) { + Column { + options.forEach { option -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { selectedOption = option } + .padding(vertical = 4.dp) + ) { + RadioButton( + selected = (selectedOption == option), + onClick = { selectedOption = option } + ) + Text(option) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt index 9e42a06..9894e2f 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt @@ -2,10 +2,12 @@ package com.cornellappdev.score.screen import androidx.compose.foundation.ExperimentalFoundationApi 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.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -15,15 +17,23 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +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.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel +import com.cornellappdev.score.components.ButtonPrimary import com.cornellappdev.score.components.EmptyStateBox import com.cornellappdev.score.components.ErrorState import com.cornellappdev.score.components.GameCard @@ -45,13 +55,15 @@ import com.cornellappdev.score.util.sportSelectionList import com.cornellappdev.score.viewmodel.HomeUiState import com.cornellappdev.score.viewmodel.HomeViewModel +@OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeScreen( homeViewModel: HomeViewModel = hiltViewModel(), navigateToGameDetails: (String) -> Unit = {} ) { val uiState = homeViewModel.collectUiStateValue() - + var showBottomSheet by remember { mutableStateOf(false) } + val sheetState = rememberModalBottomSheetState() Column( verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top), modifier = Modifier @@ -72,10 +84,56 @@ fun HomeScreen( onGenderSelected = { homeViewModel.onGenderSelected(it) }, onSportSelected = { homeViewModel.onSportSelected(it) }, navigateToGameDetails = navigateToGameDetails, - onRefresh = { homeViewModel.onRefresh() } + onRefresh = { homeViewModel.onRefresh() }, + onAdvFilterClick = { showBottomSheet = true } ) } } + if (showBottomSheet) { + ModalBottomSheet( + onDismissRequest = { showBottomSheet = false }, + sheetState = sheetState + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 32.dp, bottom = 12.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + ExpandableSection( + title = "Price", + options = listOf("Unticketed", "Under $20", "Under $50", "Over $50") + ) + ExpandableSection( + title = "Location", + options = listOf("On Campus", "1-2 Hours", "2-4 Hours", "Over 4 Hours") + ) + ExpandableSection( + title = "Date of Game", + options = listOf("Today", "Within 7 Days", "Within a Month", "Over a Month") + ) + ButtonPrimary( + text = "Apply", + icon = null, + modifier = Modifier.fillMaxWidth(), + onClick = { + // TODO: Apply filter logic via ViewModel + showBottomSheet = false + } + ) + Text( + "Reset", + fontSize = 14.sp, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .clickable { + // TODO: Reset filter logic + showBottomSheet = false + } + ) + } + } + } } } @@ -86,10 +144,17 @@ private fun HomeContent( onGenderSelected: (GenderDivision) -> Unit, onSportSelected: (SportSelection) -> Unit, onRefresh: () -> Unit, - navigateToGameDetails: (String) -> Unit = {} + navigateToGameDetails: (String) -> Unit = {}, + onAdvFilterClick: () -> Unit ) { ScorePullToRefreshBox(isRefreshing = uiState.loadedState == ApiResponse.Loading, onRefresh) { - HomeLazyColumn(uiState, onGenderSelected, onSportSelected, navigateToGameDetails) + HomeLazyColumn( + uiState, + onGenderSelected, + onSportSelected, + navigateToGameDetails, + onAdvFilterClick + ) } } @@ -99,7 +164,8 @@ private fun HomeLazyColumn( uiState: HomeUiState, onGenderSelected: (GenderDivision) -> Unit, onSportSelected: (SportSelection) -> Unit, - navigateToGameDetails: (String) -> Unit + navigateToGameDetails: (String) -> Unit, + onAdvFilterClick: () -> Unit ) { LazyColumn(contentPadding = PaddingValues(top = 24.dp)) { if (uiState.filteredGames.isNotEmpty()) { @@ -129,12 +195,22 @@ private fun HomeLazyColumn( .padding(horizontal = 24.dp) ) { Spacer(Modifier.height(24.dp)) - Text( - text = "Game Schedule", - style = title, - modifier = Modifier - .fillMaxWidth() - ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Game Schedule", + style = title, + ) + ButtonPrimary( + "", + painterResource(id = com.cornellappdev.score.R.drawable.advanced_filter) + ) { + onAdvFilterClick() + } + } Spacer(modifier = Modifier.height(8.dp)) SportSelectorHeader( sports = uiState.selectionList, @@ -199,6 +275,7 @@ private fun HomeScreenPreview() = ScorePreview { onGenderSelected = {}, onSportSelected = {}, onRefresh = {}, + onAdvFilterClick = {} ) } } @@ -215,7 +292,8 @@ private fun HomeScreenEmptyStatePreview() = ScorePreview { ), onGenderSelected = {}, onSportSelected = {}, - onRefresh = {} + onRefresh = {}, + onAdvFilterClick = {} ) } diff --git a/app/src/main/res/drawable/advanced_filter.xml b/app/src/main/res/drawable/advanced_filter.xml new file mode 100644 index 0000000..62010d4 --- /dev/null +++ b/app/src/main/res/drawable/advanced_filter.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_round_minus.xml b/app/src/main/res/drawable/ic_round_minus.xml new file mode 100644 index 0000000..cbca170 --- /dev/null +++ b/app/src/main/res/drawable/ic_round_minus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_round_plus.xml b/app/src/main/res/drawable/ic_round_plus.xml new file mode 100644 index 0000000..eb042ed --- /dev/null +++ b/app/src/main/res/drawable/ic_round_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/build.gradle.kts b/build.gradle.kts index 61d5852..648673c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,8 +1,9 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { alias(libs.plugins.androidApplication) apply false - alias(libs.plugins.jetbrainsKotlinAndroid)version "1.9.10" apply false + alias(libs.plugins.jetbrainsKotlinAndroid) version "1.9.10" apply false id("com.google.dagger.hilt.android") version "2.51.1" apply false kotlin("jvm") version "2.0.20" kotlin("plugin.serialization") version "2.0.20" + id("com.google.gms.google-services") version "4.4.3" apply false } From bf86fc9294b65dbdd9fddb6f083ef46ee3162a1f Mon Sep 17 00:00:00 2001 From: Emil Jiang Date: Wed, 1 Oct 2025 17:20:05 -0400 Subject: [PATCH 3/8] center icon --- .../java/com/cornellappdev/score/components/ButtonPrimary.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 0e6e074..4aa4108 100644 --- a/app/src/main/java/com/cornellappdev/score/components/ButtonPrimary.kt +++ b/app/src/main/java/com/cornellappdev/score/components/ButtonPrimary.kt @@ -39,7 +39,9 @@ fun ButtonPrimary( .height(24.dp), colorFilter = ColorFilter.tint(White) ) - Spacer(modifier = Modifier.width(8.dp)) + if (text.isNotEmpty()) { + Spacer(modifier = Modifier.width(8.dp)) + } } Text(text = text, style = bodyMedium.copy(color = White)) } From 2f2017ddc0727f58b8633856530750895bde480b Mon Sep 17 00:00:00 2001 From: Emil Jiang Date: Wed, 1 Oct 2025 17:49:14 -0400 Subject: [PATCH 4/8] google-services.json --- app/src/google-services.json | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 app/src/google-services.json diff --git a/app/src/google-services.json b/app/src/google-services.json new file mode 100644 index 0000000..6d64378 --- /dev/null +++ b/app/src/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "1041954613554", + "project_id": "score-2bbd4", + "storage_bucket": "score-2bbd4.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:1041954613554:android:afdaffd5c956622603cf2b", + "android_client_info": { + "package_name": "com.cornellappdev.score" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyCjJpkMnpwSVu270_k6_3UmaHXb_NiCc0I" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file From 3cc61b7b8a20b794ee114a683a736bea5ffc001b Mon Sep 17 00:00:00 2001 From: Emil Jiang Date: Thu, 2 Oct 2025 11:56:18 -0400 Subject: [PATCH 5/8] fix scrollable --- .../cornellappdev/score/screen/HomeScreen.kt | 84 ++++++++++++------- 1 file changed, 53 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt index 9894e2f..459abb3 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -36,6 +37,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.cornellappdev.score.components.ButtonPrimary import com.cornellappdev.score.components.EmptyStateBox import com.cornellappdev.score.components.ErrorState +import com.cornellappdev.score.components.ExpandableSection import com.cornellappdev.score.components.GameCard import com.cornellappdev.score.components.GamesCarousel import com.cornellappdev.score.components.LoadingScreen @@ -94,49 +96,69 @@ fun HomeScreen( onDismissRequest = { showBottomSheet = false }, sheetState = sheetState ) { - Column( + LazyColumn( modifier = Modifier .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, top = 32.dp, bottom = 12.dp), + .padding(horizontal = 16.dp), + contentPadding = PaddingValues( + top = 32.dp, + bottom = 24.dp + ), verticalArrangement = Arrangement.spacedBy(20.dp) ) { - ExpandableSection( - title = "Price", - options = listOf("Unticketed", "Under $20", "Under $50", "Over $50") - ) - ExpandableSection( - title = "Location", - options = listOf("On Campus", "1-2 Hours", "2-4 Hours", "Over 4 Hours") - ) - ExpandableSection( - title = "Date of Game", - options = listOf("Today", "Within 7 Days", "Within a Month", "Over a Month") - ) - ButtonPrimary( - text = "Apply", - icon = null, - modifier = Modifier.fillMaxWidth(), - onClick = { - // TODO: Apply filter logic via ViewModel - showBottomSheet = false - } - ) - Text( - "Reset", - fontSize = 14.sp, - modifier = Modifier - .align(Alignment.CenterHorizontally) - .clickable { - // TODO: Reset filter logic + item { + ExpandableSection( + title = "Price", + options = listOf("Unticketed", "Under $20", "Under $50", "Over $50") + ) + } + item { + ExpandableSection( + title = "Location", + options = listOf("On Campus", "1-2 Hours", "2-4 Hours", "Over 4 Hours") + ) + } + item { + ExpandableSection( + title = "Date of Game", + options = listOf( + "Today", + "Within 7 Days", + "Within a Month", + "Over a Month" + ) + ) + } + item { + ButtonPrimary( + text = "Apply", + icon = null, + modifier = Modifier.fillMaxWidth(), + onClick = { + // TODO: Apply filter logic via ViewModel showBottomSheet = false } - ) + ) + } + item { + Text( + "Reset", fontSize = 14.sp, + modifier = Modifier + .fillMaxWidth() + .clickable { + // TODO: Reset filter logic + showBottomSheet = false + }, + textAlign = TextAlign.Center + ) + } } } } } } + @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @Composable private fun HomeContent( From 6fef02b5bfb033702e53c58815ff9a718929190a Mon Sep 17 00:00:00 2001 From: Emil Jiang Date: Mon, 27 Oct 2025 10:51:08 -0400 Subject: [PATCH 6/8] pagination --- app/build.gradle.kts | 5 +- app/src/main/graphql/FragmentedGame.graphql | 33 ++++++ app/src/main/graphql/schema.graphqls | 55 ++++++++- .../score/model/ScoreRepository.kt | 106 ++++++++++++++++-- 4 files changed, 182 insertions(+), 17 deletions(-) create mode 100644 app/src/main/graphql/FragmentedGame.graphql diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 182d1ca..5b068a7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,7 +1,7 @@ plugins { alias(libs.plugins.androidApplication) alias(libs.plugins.jetbrainsKotlinAndroid) version "1.9.10" - alias(libs.plugins.apollo) + id("com.apollographql.apollo") version "4.0.0" id("kotlin-kapt") id("com.google.dagger.hilt.android") id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" // this version matches your Kotlin version @@ -88,9 +88,8 @@ dependencies { debugImplementation("androidx.compose.ui:ui-tooling") androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) - implementation(libs.apollo.runtime) implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") - implementation(libs.apollo.runtime) + implementation("com.apollographql.apollo:apollo-runtime:4.0.0") implementation("io.coil-kt.coil3:coil-compose:3.1.0") implementation("io.coil-kt.coil3:coil-network-okhttp:3.1.0") lintChecks(libs.compose.lint.checks) diff --git a/app/src/main/graphql/FragmentedGame.graphql b/app/src/main/graphql/FragmentedGame.graphql new file mode 100644 index 0000000..4f5d398 --- /dev/null +++ b/app/src/main/graphql/FragmentedGame.graphql @@ -0,0 +1,33 @@ +query PagedGames($limit: Int!, $offset: Int!) { + games(limit: $limit, offset: $offset) { + id + city + date + gender + location + opponentId + result + sport + state + time + scoreBreakdown + utcDate + team { + id + color + image + name + } + boxScore { + team + period + time + description + scorer + assist + scoreBy + corScore + oppScore + } + } +} diff --git a/app/src/main/graphql/schema.graphqls b/app/src/main/graphql/schema.graphqls index 0265edb..e1ebfad 100644 --- a/app/src/main/graphql/schema.graphqls +++ b/app/src/main/graphql/schema.graphqls @@ -1,13 +1,15 @@ type Query { + articles(sportsType: String): [ArticleType] + youtubeVideos: [YoutubeVideoType] youtubeVideo(id: String!): YoutubeVideoType - games: [GameType] + games("Number of games to return" limit: Int = 100, "Number of games to skip" offset: Int = 0): [GameType] game(id: String!): GameType - gameByData(city: String!, date: String!, gender: String!, location: String, opponentId: String!, sport: String!, state: String!, time: String!): GameType + gameByData(city: String!, date: String!, gender: String!, location: String, opponentId: String!, sport: String!, state: String!, time: String!, ticketLink: String): GameType gamesBySport(sport: String!): [GameType] @@ -22,6 +24,30 @@ type Query { teamByName(name: String!): TeamType } +""" +A GraphQL type representing a news article. + +Attributes: + - title: The title of the article + - image: The filename of the article's main image + - sports_type: The specific sport category + - published_at: The publication date + - url: The URL to the full article +""" +type ArticleType { + id: String + + title: String! + + image: String + + sportsType: String! + + publishedAt: String! + + url: String! +} + """ A GraphQL type representing a YouTube video. @@ -42,6 +68,8 @@ type YoutubeVideoType { thumbnail: String! + b64Thumbnail: String! + url: String! publishedAt: String! @@ -63,6 +91,7 @@ Attributes: - `time`: The time of the game. (optional) - `box_score`: The box score of the game. - `score_breakdown`: The score breakdown of the game. + - `ticket_link`: The ticket link of the game. (optional) """ type GameType { id: String @@ -90,6 +119,10 @@ type GameType { scoreBreakdown: [[String]] team: TeamType + + utcDate: String + + ticketLink: String } """ @@ -133,6 +166,7 @@ Attributes: - `id`: The ID of the team (optional). - `color`: The color of the team. - `image`: The image of the team (optional). + - `b64_image`: The base64 encoded image of the team (optional). - `name`: The name of the team. """ type TeamType { @@ -142,6 +176,8 @@ type TeamType { image: String + b64Image: String + name: String! } @@ -149,17 +185,22 @@ type Mutation { """ Creates a new game. """ - createGame(boxScore: String, city: String!, date: String!, gender: String!, location: String, opponentId: String!, result: String, scoreBreakdown: String, sport: String!, state: String!, time: String!): CreateGame + createGame(boxScore: String, city: String!, date: String!, gender: String!, location: String, opponentId: String!, result: String, scoreBreakdown: String, sport: String!, state: String!, ticketLink: String, time: String!, utcDate: String): CreateGame """ Creates a new team. """ - createTeam(color: String!, image: String, name: String!): CreateTeam + createTeam(b64Image: String, color: String!, image: String, name: String!): CreateTeam """ Creates a new youtube video. """ - createYoutubeVideo(description: String!, id: String!, publishedAt: String!, thumbnail: String!, title: String!, url: String!): CreateYoutubeVideo + createYoutubeVideo(b64Thumbnail: String!, description: String!, id: String!, publishedAt: String!, thumbnail: String!, title: String!, url: String!): CreateYoutubeVideo + + """ + Creates a new article. + """ + createArticle(image: String, publishedAt: String!, slug: String!, sportsType: String!, title: String!, url: String!): CreateArticle } type CreateGame { @@ -174,6 +215,10 @@ type CreateYoutubeVideo { youtubeVideo: YoutubeVideoType } +type CreateArticle { + article: ArticleType +} + """ A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation and subscription operations. """ diff --git a/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt b/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt index 1310cd4..5d283ef 100644 --- a/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt +++ b/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt @@ -2,10 +2,12 @@ package com.cornellappdev.score.model import android.util.Log import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.exception.ApolloException import com.cornellappdev.score.util.isValidSport import com.cornellappdev.score.util.parseColor import com.example.score.GameByIdQuery import com.example.score.GamesQuery +import com.example.score.PagedGamesQuery import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -36,11 +38,17 @@ class ScoreRepository @Inject constructor( MutableStateFlow>(ApiResponse.Loading) val currentGamesFlow = _currentGameFlow.asStateFlow() + companion object { + private const val PAGE_LIMIT = 100 + private const val MAX_RETRIES = 3 + private const val PAGE_TIMEOUT_MILLIS = 3000L + } + /** * Asynchronously fetches the list of games from the API. Once finished, will send down * `upcomingGamesFlow` to be observed. */ - fun fetchGames() = appScope.launch { + fun fetchGamesPrev() = appScope.launch { _upcomingGamesFlow.value = ApiResponse.Loading try { val result = @@ -92,6 +100,79 @@ class ScoreRepository @Inject constructor( } } + fun fetchGames() = appScope.launch { + _upcomingGamesFlow.value = ApiResponse.Loading + val allGames = mutableListOf() + var offset = 0 + var retries = 0 + + try { + while (true) { + val pageResult: List? = try { + withTimeout(PAGE_TIMEOUT_MILLIS) { + apolloClient.query(PagedGamesQuery(limit = PAGE_LIMIT, offset = offset)) + .execute() + .data + ?.games + } + } catch (e: Exception) { + null + } + + if (pageResult == null) { + if (retries < MAX_RETRIES) { + retries++ + continue + } else { + break + } + } + + if (pageResult.isEmpty()) { + break + } + + retries = 0 + + val pageGames: List = pageResult + .filterNotNull() + .filter { gql -> isValidSport(gql.sport ?: "") } + .mapNotNull { gql -> + val scores = gql.result?.split(",")?.getOrNull(1)?.split("-") + val cornellScore = scores?.getOrNull(0)?.toNumberOrNull() + val otherScore = scores?.getOrNull(1)?.toNumberOrNull() + gql.team?.image?.let { imageUrl -> + Game( + id = gql.id ?: "", + teamLogo = imageUrl, + teamName = gql.team.name, + teamColor = parseColor(gql.team.color).copy(alpha = 0.4f * 255), + gender = if (gql.gender == "Mens") "Men's" else "Women's", + sport = gql.sport, + date = gql.date, + city = gql.city, + cornellScore = cornellScore, + otherScore = otherScore + ) + } + } + + allGames.addAll(pageGames) + + if (pageResult.size < PAGE_LIMIT) break + offset += PAGE_LIMIT + } + + _upcomingGamesFlow.value = + if (allGames.isNotEmpty()) ApiResponse.Success(allGames) + else ApiResponse.Error + + } catch (e: Exception) { + Log.e("ScoreRepository", "Error fetching upcoming games", e) + _upcomingGamesFlow.value = ApiResponse.Error + } + } + /** * Asynchronously fetches game details for a particular game. Once finished, will update * `currentGamesFlow` to be observed. @@ -99,17 +180,26 @@ class ScoreRepository @Inject constructor( fun getGameById(id: String) = appScope.launch { _currentGameFlow.value = ApiResponse.Loading try { - val result = + val response = withTimeout(TIMEOUT_TIME_MILLIS) { - apolloClient.query(GameByIdQuery(id)).execute().toResult() + apolloClient.query(GameByIdQuery(id)).execute() } + if (response.hasErrors()) { + Log.e("ScoreRepository", "Error fetching game with id: $id: ${response.errors}") + _currentGameFlow.value = ApiResponse.Error + return@launch + } - result.getOrNull()?.game?.let { + response.data?.game?.let { _currentGameFlow.value = ApiResponse.Success(it.toGameDetails()) } ?: _currentGameFlow.update { ApiResponse.Error } + + } catch (e: ApolloException) { + Log.e("ScoreRepository", "Error fetching game with id: $id: ", e) + _currentGameFlow.value = ApiResponse.Error } catch (e: Exception) { - Log.e("ScoreRepository", "Error fetching game with id: ${id}: ", e) + Log.e("ScoreRepository", "A timeout or other error occurred for game id: $id", e) _currentGameFlow.value = ApiResponse.Error } } @@ -117,8 +207,6 @@ class ScoreRepository @Inject constructor( } fun String.toNumberOrNull(): Number? { - return when { - this.contains(".") -> this.toFloatOrNull() // Try converting to Float if there's a decimal - else -> this.toIntOrNull() // Otherwise, try converting to Int - } + return this.trim().toFloatOrNull() ?: this.trim().toIntOrNull() } + From 517656fde870449db5fe7fa69e26ba95ba1a4f15 Mon Sep 17 00:00:00 2001 From: Emil Jiang Date: Mon, 16 Feb 2026 22:56:37 -0500 Subject: [PATCH 7/8] changes for baseball scores --- app/src/main/graphql/Games.graphql | 1 + .../com/cornellappdev/score/model/Game.kt | 16 +++++++++----- .../score/model/ScoreRepository.kt | 10 ++++++++- .../cornellappdev/score/util/GameDataUtil.kt | 22 +++++++++++++------ 4 files changed, 36 insertions(+), 13 deletions(-) diff --git a/app/src/main/graphql/Games.graphql b/app/src/main/graphql/Games.graphql index 0af8c70..66fe3c6 100644 --- a/app/src/main/graphql/Games.graphql +++ b/app/src/main/graphql/Games.graphql @@ -2,6 +2,7 @@ query Games{ games{ id date + time city sport team{ diff --git a/app/src/main/java/com/cornellappdev/score/model/Game.kt b/app/src/main/java/com/cornellappdev/score/model/Game.kt index d6c93d7..6074d76 100644 --- a/app/src/main/java/com/cornellappdev/score/model/Game.kt +++ b/app/src/main/java/com/cornellappdev/score/model/Game.kt @@ -20,6 +20,7 @@ data class Game( val id: String, val teamName: String, val teamLogo: String, + val time: String?, val teamColor: Color, val gender: String, val sport: String, @@ -146,7 +147,7 @@ data class TeamScore( // Aggregated game data showing scores for both teams data class GameData( val teamScores: Pair -){ +) { val maxPeriods: Int get() = maxOf( @@ -250,7 +251,11 @@ fun Game.toGameCardData(): GameCardData { date = parseDateOrNull(date), dateString = parseDateOrNull(date)?.format(outputFormatter) ?: date, - isLive = (LocalDate.now() == parseDateOrNull(date)), + isLive = parseDateTimeOrNull(date, time ?: "")?.let { startTime -> + val now = LocalDateTime.now() + val endTime = startTime.plusHours(2) + now.isAfter(startTime) && now.isBefore(endTime) + } ?: false, isPast = isPast, location = city, gender = gender, @@ -290,14 +295,15 @@ fun GameDetailsGame.toGameCardData(): DetailsCardData { scoreBreakdown = scoreBreakdown, team1 = TeamBoxScore("Cornell"), team2 = TeamBoxScore(team?.name ?: ""), - sport = sport + sport = sport, + result = result ?: "" ), scoreEvent = boxScore?.toScoreEvents(team?.image ?: "") ?: emptyList(), daysUntilGame = daysUntil, hoursUntilGame = hoursUntil, - homeScore = convertScores(scoreBreakdown?.getOrNull(0), sport).second + homeScore = convertScores(scoreBreakdown?.getOrNull(0), sport, result ?: "").second ?: parsedScores?.first ?: 0, - oppScore = convertScores(scoreBreakdown?.getOrNull(1), sport).second + oppScore = convertScores(scoreBreakdown?.getOrNull(1), sport, result ?: "").second ?: parsedScores?.second ?: 0 ) } diff --git a/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt b/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt index 2b84524..1e9d91b 100644 --- a/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt +++ b/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt @@ -4,6 +4,7 @@ import android.util.Log import com.apollographql.apollo.ApolloClient import com.cornellappdev.score.util.isValidSport import com.cornellappdev.score.util.parseColor +import com.cornellappdev.score.util.parseResultScore import com.example.score.GameByIdQuery import com.example.score.GamesQuery import com.example.score.PagedGamesQuery @@ -75,6 +76,7 @@ class ScoreRepository @Inject constructor( id = game.id ?: "", // Should never be null teamLogo = it, teamName = game.team.name, + time = game.time, teamColor = parseColor(game.team.color).copy(alpha = 0.4f * 255), gender = if (game.gender == "Mens") "Men's" else "Women's", sport = game.sport, @@ -133,11 +135,15 @@ class ScoreRepository @Inject constructor( .mapNotNull { graphqlGame -> val scores = graphqlGame.result?.split(",")?.getOrNull(1)?.split("-") val cornellScore = scores?.getOrNull(0)?.toNumberOrNull() - val otherScore = scores?.getOrNull(1)?.toNumberOrNull() + ?: parseResultScore(graphqlGame.result)?.first + val otherScore = scores?.getOrNull(1)?.toNumberOrNull() ?: parseResultScore( + graphqlGame.result + )?.second graphqlGame.team?.image?.let { imageUrl -> Game( id = graphqlGame.id ?: "", teamLogo = imageUrl, + time = graphqlGame.time, teamName = graphqlGame.team.name, teamColor = parseColor(graphqlGame.team.color).copy(alpha = 0.4f * 255), gender = if (graphqlGame.gender == "Mens") "Men's" else "Women's", @@ -171,6 +177,7 @@ class ScoreRepository @Inject constructor( * `currentGamesFlow` to be observed. */ fun getGameById(id: String) = appScope.launch { + Log.d("ScoreRepository", "Fetching game with id: $id") _currentGameFlow.value = ApiResponse.Loading try { val result = @@ -181,6 +188,7 @@ class ScoreRepository @Inject constructor( result.getOrNull()?.game?.let { _currentGameFlow.value = ApiResponse.Success(it.toGameDetails()) + } ?: _currentGameFlow.update { ApiResponse.Error } } catch (e: Exception) { Log.e("ScoreRepository", "Error fetching game with id: ${id}: ", e) diff --git a/app/src/main/java/com/cornellappdev/score/util/GameDataUtil.kt b/app/src/main/java/com/cornellappdev/score/util/GameDataUtil.kt index a25e9ee..2da4dbb 100644 --- a/app/src/main/java/com/cornellappdev/score/util/GameDataUtil.kt +++ b/app/src/main/java/com/cornellappdev/score/util/GameDataUtil.kt @@ -1,5 +1,6 @@ package com.cornellappdev.score.util +import android.util.Log import com.cornellappdev.score.model.GameData import com.cornellappdev.score.model.TeamBoxScore import com.cornellappdev.score.model.TeamScore @@ -17,7 +18,7 @@ import com.cornellappdev.score.model.TeamScore * @return a pair where the first value is a list of parsed period scores and the second is the total score (or null if invalid) */ // TODO: ASK ABOUT OT. Other sports might be added. -fun convertScores(scoreList: List?, sport: String): Pair, Int?> { +fun convertScores(scoreList: List?, sport: String, result: String): Pair, Int?> { if (scoreList == null || scoreList.size < 2) return Pair(emptyList(), null) var scoresByPeriod = scoreList @@ -31,7 +32,12 @@ fun convertScores(scoreList: List?, sport: String): Pair, Int } if (sport.lowercase() == "baseball") { - scoresByPeriod = scoresByPeriod.take(9) + val scoreParsed = result.split("(") + scoresByPeriod = if (scoreParsed.size > 1) { + scoresByPeriod.take(6) + } else { + scoresByPeriod.take(9) + } val totalScore = scoresByPeriod.sum() return Pair(scoresByPeriod, totalScore) } @@ -56,14 +62,15 @@ fun toGameData( scoreBreakdown: List?>?, team1: TeamBoxScore, team2: TeamBoxScore, - sport: String + sport: String, + result: String, ): GameData { val (team1Scores, team1Total) = scoreBreakdown?.getOrNull(0)?.let { - convertScores(it, sport) + convertScores(it, sport, result) } ?: (emptyList() to null) val (team2Scores, team2Total) = scoreBreakdown?.getOrNull(1)?.let { - convertScores(it, sport) + convertScores(it, sport, result) } ?: (emptyList() to null) val team1Score = @@ -90,11 +97,12 @@ fun parseResultScore(result: String?): Pair? { if (parts.size != 2) return null val scorePart = parts[1].split("-") + val secondScorePartEdge = scorePart[1].split("(") if (scorePart.size != 2) return null val homeScore = scorePart[0].toIntOrNull() - val oppScore = scorePart[1].toIntOrNull() - + val oppScore = secondScorePartEdge[0].toIntOrNull() + Log.d("HIHI", oppScore.toString()) if (homeScore != null && oppScore != null) { return Pair(homeScore, oppScore) } else { From ec7266ca85a266b5aa9d65bdb429e9517ac85241 Mon Sep 17 00:00:00 2001 From: Emil Jiang <77295343+EmilJiang@users.noreply.github.com> Date: Wed, 18 Feb 2026 18:04:01 -0500 Subject: [PATCH 8/8] Delete app/src/google-services.json --- app/src/google-services.json | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 app/src/google-services.json diff --git a/app/src/google-services.json b/app/src/google-services.json deleted file mode 100644 index 6d64378..0000000 --- a/app/src/google-services.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "project_info": { - "project_number": "1041954613554", - "project_id": "score-2bbd4", - "storage_bucket": "score-2bbd4.firebasestorage.app" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:1041954613554:android:afdaffd5c956622603cf2b", - "android_client_info": { - "package_name": "com.cornellappdev.score" - } - }, - "oauth_client": [], - "api_key": [ - { - "current_key": "AIzaSyCjJpkMnpwSVu270_k6_3UmaHXb_NiCc0I" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [] - } - } - } - ], - "configuration_version": "1" -} \ No newline at end of file