From 4735598e880951b9e41f0f9fb6f84c87b48d03d6 Mon Sep 17 00:00:00 2001 From: ali Date: Sun, 22 Oct 2023 13:21:43 +0200 Subject: [PATCH 1/4] implemented the prediction feature on iOS and Android --- .../fantasypremierleague/MainActivity.kt | 1 + .../FantasyPremierLeagueViewModel.kt | 10 +- .../FixtureDetails/FixtureDetailsView.kt | 241 +++++++++++++++++- .../presentation/fixtures/FixtureView.kt | 79 +++++- .../presentation/fixtures/FixturesListView.kt | 43 +++- .../FantasyPremierLeagueRepository.kt | 213 ++++++++++------ .../kotlin/dev/johnoreilly/common/di/Koin.kt | 31 ++- .../common/domain/entities/GameFixture.kt | 66 ++++- .../common/domain/entities/Prediction.kt | 9 + .../project.pbxproj | 14 +- .../FantasyPremierLeague/ContentView.swift | 37 +-- .../Fixtures/FixtureDetailView.swift | 95 +++++++ .../Fixtures/FixtureListView.swift | 49 +++- .../FantasyPremierLeague/ViewModel.swift | 12 + 14 files changed, 762 insertions(+), 138 deletions(-) create mode 100644 common/src/commonMain/kotlin/dev/johnoreilly/common/domain/entities/Prediction.kt diff --git a/app/src/main/java/dev/johnoreilly/fantasypremierleague/MainActivity.kt b/app/src/main/java/dev/johnoreilly/fantasypremierleague/MainActivity.kt index 85dbfb4..42e9541 100644 --- a/app/src/main/java/dev/johnoreilly/fantasypremierleague/MainActivity.kt +++ b/app/src/main/java/dev/johnoreilly/fantasypremierleague/MainActivity.kt @@ -114,6 +114,7 @@ fun MainLayout(viewModel: FantasyPremierLeagueViewModel) { fixture?.let { FixtureDetailsView( fixture, + onSubmitPredict = viewModel::onSubmitPredict, popBackStack = { navController.popBackStack() }) } } diff --git a/app/src/main/java/dev/johnoreilly/fantasypremierleague/presentation/FantasyPremierLeagueViewModel.kt b/app/src/main/java/dev/johnoreilly/fantasypremierleague/presentation/FantasyPremierLeagueViewModel.kt index bf7e58b..f444806 100644 --- a/app/src/main/java/dev/johnoreilly/fantasypremierleague/presentation/FantasyPremierLeagueViewModel.kt +++ b/app/src/main/java/dev/johnoreilly/fantasypremierleague/presentation/FantasyPremierLeagueViewModel.kt @@ -10,6 +10,7 @@ import dev.johnoreilly.common.data.repository.FantasyPremierLeagueRepository import dev.johnoreilly.common.domain.entities.GameFixture import dev.johnoreilly.common.domain.entities.Player import dev.johnoreilly.common.domain.entities.PlayerPastHistory +import dev.johnoreilly.common.domain.entities.Prediction import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableStateFlow @@ -28,6 +29,7 @@ class FantasyPremierLeagueViewModel( ) : ViewModel() { val searchQuery = MutableStateFlow("") + val currentGameweekToFixture = repository.currentGameweek val allPlayers = repository.playerList val visiblePlayerList: StateFlow> = searchQuery.debounce(250).flatMapLatest { searchQuery -> @@ -78,11 +80,17 @@ class FantasyPremierLeagueViewModel( } fun getFixture(fixtureId: Int?): GameFixture? { - return fixturesList.value.find { it.id == fixtureId} + return fixturesList.value.find { it.id == fixtureId } } fun updateLeagues(leagues: List) { repository.updateLeagues(leagues) } + fun onSubmitPredict(prediction: Prediction) { + viewModelScope.launch { + repository.submitPredict(prediction) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/dev/johnoreilly/fantasypremierleague/presentation/fixtures/FixtureDetails/FixtureDetailsView.kt b/app/src/main/java/dev/johnoreilly/fantasypremierleague/presentation/fixtures/FixtureDetails/FixtureDetailsView.kt index a554060..5297057 100644 --- a/app/src/main/java/dev/johnoreilly/fantasypremierleague/presentation/fixtures/FixtureDetails/FixtureDetailsView.kt +++ b/app/src/main/java/dev/johnoreilly/fantasypremierleague/presentation/fixtures/FixtureDetails/FixtureDetailsView.kt @@ -2,28 +2,63 @@ package dev.johnoreilly.fantasypremierleague.presentation.fixtures.FixtureDetails -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.background +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.OutlinedTextField import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.Button import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold 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.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +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 import dev.johnoreilly.common.domain.entities.GameFixture +import dev.johnoreilly.common.domain.entities.Prediction import dev.johnoreilly.fantasypremierleague.presentation.fixtures.ClubInFixtureView +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun FixtureDetailsView(fixture: GameFixture, popBackStack: () -> Unit) { +fun FixtureDetailsView( + fixture: GameFixture, + onSubmitPredict: (Prediction) -> Unit, + popBackStack: () -> Unit +) { Scaffold( topBar = { @@ -39,7 +74,9 @@ fun FixtureDetailsView(fixture: GameFixture, popBackStack: () -> Unit) { ) }) { Column( - modifier = Modifier.padding(it).fillMaxSize(), + modifier = Modifier + .padding(it) + .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally ) { Row( @@ -54,7 +91,7 @@ fun FixtureDetailsView(fixture: GameFixture, popBackStack: () -> Unit) { fixture.homeTeamPhotoUrl ) Text( - text = "(${fixture.homeTeamScore})", + text = fixture.homeScore, fontWeight = FontWeight.Bold, fontSize = 25.sp ) @@ -64,7 +101,7 @@ fun FixtureDetailsView(fixture: GameFixture, popBackStack: () -> Unit) { fontSize = 25.sp ) Text( - text = "(${fixture.awayTeamScore})", + text = fixture.awayScore, fontWeight = FontWeight.Bold, fontSize = 25.sp ) @@ -74,11 +111,201 @@ fun FixtureDetailsView(fixture: GameFixture, popBackStack: () -> Unit) { ) } - fixture.localKickoffTime?.let { localKickoffTime -> - val formattedTime = "%02d:%02d".format(localKickoffTime.hour, localKickoffTime.minute) + fixture.localKickoffTime.let { localKickoffTime -> + val formattedTime = + "%02d:%02d".format(localKickoffTime.hour, localKickoffTime.minute) PastFixtureStatView(statName = "Date", statValue = localKickoffTime.date.toString()) PastFixtureStatView(statName = "Kick Off Time", statValue = formattedTime) } + + if (fixture.isNotStartedYet || fixture.isPredicted) { + PredictionView( + gameFixture = fixture, + onSubmitPredict = onSubmitPredict, + ) + } + + } + } +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +private fun PredictionView( + gameFixture: GameFixture, + onSubmitPredict: (Prediction) -> Unit +) { + var homeTeamPrediction by remember { mutableStateOf(gameFixture.prediction?.homeScores.orEmpty()) } + var awayTeamPrediction by remember { mutableStateOf(gameFixture.prediction?.awayScores.orEmpty()) } + + val keyboardController = LocalSoftwareKeyboardController.current + val focusRequester = remember { FocusRequester() } + + Column( + modifier = Modifier + .focusRequester(focusRequester) + .padding(top = 24.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.SpaceBetween + ) { + + val isNotPredicted = remember(gameFixture.isPredicted) { gameFixture.isPredicted.not() } + + Box( + modifier = Modifier + .background( + brush = Brush.linearGradient( + listOf( + Color(0xFFC5A8FF), + Color(0xFF7E43A3), + ) + ) + ) + .fillMaxWidth() + .padding(8.dp) + ) { + Text( + text = if (isNotPredicted) "Make Your Predictions" else "Predictions Submitted!", + color = Color.White, + style = MaterialTheme.typography.titleMedium, // Customize the style + modifier = Modifier + .padding(12.dp) + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 24.dp, bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + + OutlinedTextField( + enabled = isNotPredicted, + value = homeTeamPrediction, + onValueChange = { homeTeamPrediction = it.take(2) }, + placeholder = { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = "?", + style = TextStyle( + fontSize = 40.sp, + color = Color.LightGray, + textAlign = TextAlign.Center, + ) + ) + } + }, + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onNext = { + // Move focus to the next field when "Next" is pressed + focusRequester.requestFocus() + }, + ), + textStyle = TextStyle( + fontSize = 34.sp, + color = Color.Black, + textAlign = TextAlign.Center + ), + singleLine = true, + modifier = Modifier + .size(120.dp) + .padding(horizontal = 16.dp), + ) + + Spacer(modifier = Modifier.width(16.dp)) + + OutlinedTextField( + enabled = isNotPredicted, + value = awayTeamPrediction, + onValueChange = { awayTeamPrediction = it.take(2) }, + placeholder = { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text( + text = "?", + style = TextStyle( + fontSize = 40.sp, + color = Color.LightGray, + textAlign = TextAlign.Center, + ) + ) + } + }, + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + keyboardController?.hide() + focusRequester.freeFocus() + } + ), + textStyle = TextStyle( + fontSize = 34.sp, + color = Color.Black, + textAlign = TextAlign.Center + ), + singleLine = true, + modifier = Modifier + .size(120.dp) + .padding(horizontal = 16.dp), + ) + + } + + + if (isNotPredicted) { + Button( + onClick = { + onSubmitPredict( + Prediction( + fixtureId = gameFixture.id, + homeScores = homeTeamPrediction, + awayScores = awayTeamPrediction, + ) + ) + }, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) { + Text(text = "Submit Predictions") + } + } else { + Text( + text = gameFixture.getPredictionMessage(), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier + .padding(top = 8.dp) + .align(Alignment.CenterHorizontally) + ) + } + + } +} + +@Preview +@Composable +fun FixtureDetailsViewPreview() { + MaterialTheme { + + Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) { + + FixtureDetailsView( + fixture = GameFixture.mockGameFixture, + onSubmitPredict = { _ -> }, + popBackStack = {}, + ) } } } diff --git a/app/src/main/java/dev/johnoreilly/fantasypremierleague/presentation/fixtures/FixtureView.kt b/app/src/main/java/dev/johnoreilly/fantasypremierleague/presentation/fixtures/FixtureView.kt index b2d24c7..16cd6fb 100644 --- a/app/src/main/java/dev/johnoreilly/fantasypremierleague/presentation/fixtures/FixtureView.kt +++ b/app/src/main/java/dev/johnoreilly/fantasypremierleague/presentation/fixtures/FixtureView.kt @@ -11,6 +11,8 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -21,6 +23,7 @@ import androidx.compose.ui.unit.sp import coil.compose.AsyncImage import com.google.accompanist.placeholder.placeholder import dev.johnoreilly.common.domain.entities.GameFixture +import dev.johnoreilly.common.domain.entities.Prediction import dev.johnoreilly.fantasypremierleague.presentation.global.lowfidelitygray import dev.johnoreilly.fantasypremierleague.presentation.global.maroon200 @@ -28,7 +31,7 @@ import dev.johnoreilly.fantasypremierleague.presentation.global.maroon200 fun FixtureView( fixture: GameFixture, onFixtureSelected: (fixtureId: Int) -> Unit, - isDataLoading: Boolean + isDataLoading: Boolean, ) { Surface( modifier = Modifier @@ -44,6 +47,66 @@ fun FixtureView( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceEvenly ) { + + if (fixture.isLive) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + Text( + text = if (fixture.isPredicted) { + "Prediction submitted! Watch the game live!" + } else { + "Live!" + }, + color = Color.White, + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .background( + shape = RoundedCornerShape(10.dp), + brush = Brush.linearGradient( + listOf( + Color(0xFFC5A8FF), + Color(0xFF7E43A3), + ) + ) + ) + .padding(8.dp) + ) + } + + } else if (fixture.isNotStartedYet) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + Text( + text = if (fixture.isPredicted) { + "Prediction submitted!" + } else { + "Join the Betting Action!" + }, + color = Color.White, + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .background( + shape = RoundedCornerShape(10.dp), + brush = Brush.linearGradient( + listOf( + Color(0xFFC5A8FF), + Color(0xFF7E43A3), + ) + ) + ) + .padding(8.dp) + ) + } + } + Row( modifier = Modifier .fillMaxWidth() @@ -56,7 +119,7 @@ fun FixtureView( fixture.homeTeamPhotoUrl ) Text( - text = "${fixture.homeTeamScore}", + text = fixture.homeScore, fontWeight = FontWeight.Bold, fontSize = 25.sp ) @@ -67,7 +130,7 @@ fun FixtureView( .background(color = maroon200) ) Text( - text = "${fixture.awayTeamScore}", + text = fixture.awayScore, fontWeight = FontWeight.Bold, fontSize = 25.sp ) @@ -84,7 +147,8 @@ fun FixtureView( ) fixture.localKickoffTime.let { localKickoffTime -> - val formattedTime = "%02d:%02d".format(localKickoffTime.hour, localKickoffTime.minute) + val formattedTime = + "%02d:%02d".format(localKickoffTime.hour, localKickoffTime.minute) Text( modifier = Modifier.padding(bottom = 16.dp), text = formattedTime, @@ -135,7 +199,12 @@ fun PreviewFixtureView() { "", 3, 0, - 5 + 5, + prediction = Prediction( + fixtureId = 1, + homeScores = "1", + awayScores = "0" + ) ), onFixtureSelected = {}, isDataLoading = false diff --git a/app/src/main/java/dev/johnoreilly/fantasypremierleague/presentation/fixtures/FixturesListView.kt b/app/src/main/java/dev/johnoreilly/fantasypremierleague/presentation/fixtures/FixturesListView.kt index ad12038..fa01686 100644 --- a/app/src/main/java/dev/johnoreilly/fantasypremierleague/presentation/fixtures/FixturesListView.kt +++ b/app/src/main/java/dev/johnoreilly/fantasypremierleague/presentation/fixtures/FixturesListView.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.placeholder.placeholder import dev.johnoreilly.common.domain.entities.GameFixture +import dev.johnoreilly.common.domain.entities.Prediction import dev.johnoreilly.fantasypremierleague.presentation.global.lowfidelitygray import dev.johnoreilly.fantasypremierleague.presentation.global.maroon200 import org.koin.androidx.compose.getViewModel @@ -38,7 +39,8 @@ fun FixturesListView( val fixturesViewModel: FixturesViewModel = getViewModel() val fixturesState = fixturesViewModel.gameweekToFixtures.collectAsStateWithLifecycle() - val currentGameweek: State = fixturesViewModel.currentGameweek.collectAsStateWithLifecycle() + val currentGameweek: State = + fixturesViewModel.currentGameweek.collectAsStateWithLifecycle() val selectedGameweek = remember { mutableIntStateOf(currentGameweek.value) } val isLoading = fixturesState.value[currentGameweek.value] == null Scaffold( @@ -53,8 +55,8 @@ fun FixturesListView( if (gameweekChange is GameweekChange.PastGameweek) selectedGameweek.intValue -= 1 else selectedGameweek.intValue += 1 }) LazyColumn { - val fixtureItems: List = if(isLoading) placeholderFixtureList - else fixturesState.value[selectedGameweek.intValue] ?: emptyList() + val fixtureItems: List = if (isLoading) placeholderFixtureList + else fixturesState.value[selectedGameweek.intValue] ?: emptyList() items( items = fixtureItems, itemContent = { fixture -> @@ -139,7 +141,12 @@ private val placeholderFixtureList = listOf( awayTeamPhotoUrl = "", homeTeamScore = null, awayTeamScore = null, - event = 0 + event = 0, + prediction = Prediction( + fixtureId = 1, + homeScores = "2", + awayScores = "1", + ), ), GameFixture( id = 1, @@ -150,7 +157,12 @@ private val placeholderFixtureList = listOf( awayTeamPhotoUrl = "", homeTeamScore = null, awayTeamScore = null, - event = 0 + event = 0, + prediction = Prediction( + fixtureId = 1, + homeScores = "2", + awayScores = "1", + ), ), GameFixture( id = 1, @@ -161,7 +173,12 @@ private val placeholderFixtureList = listOf( awayTeamPhotoUrl = "", homeTeamScore = null, awayTeamScore = null, - event = 0 + event = 0, + prediction = Prediction( + fixtureId = 1, + homeScores = "2", + awayScores = "1", + ), ), GameFixture( id = 1, @@ -172,7 +189,12 @@ private val placeholderFixtureList = listOf( awayTeamPhotoUrl = "", homeTeamScore = null, awayTeamScore = null, - event = 0 + event = 0, + prediction = Prediction( + fixtureId = 1, + homeScores = "2", + awayScores = "1", + ), ), GameFixture( id = 1, @@ -183,6 +205,11 @@ private val placeholderFixtureList = listOf( awayTeamPhotoUrl = "", homeTeamScore = null, awayTeamScore = null, - event = 0 + event = 0, + prediction = Prediction( + fixtureId = 1, + homeScores = "2", + awayScores = "1", + ), ) ) \ No newline at end of file diff --git a/common/src/commonMain/kotlin/dev/johnoreilly/common/data/repository/FantasyPremierLeagueRepository.kt b/common/src/commonMain/kotlin/dev/johnoreilly/common/data/repository/FantasyPremierLeagueRepository.kt index 09dbd91..1fa9b2c 100644 --- a/common/src/commonMain/kotlin/dev/johnoreilly/common/data/repository/FantasyPremierLeagueRepository.kt +++ b/common/src/commonMain/kotlin/dev/johnoreilly/common/data/repository/FantasyPremierLeagueRepository.kt @@ -11,6 +11,7 @@ import dev.johnoreilly.common.data.remote.FantasyPremierLeagueApi import dev.johnoreilly.common.domain.entities.GameFixture import dev.johnoreilly.common.domain.entities.Player import dev.johnoreilly.common.domain.entities.PlayerPastHistory +import dev.johnoreilly.common.domain.entities.Prediction import dev.johnoreilly.common.domain.entities.Team import io.realm.kotlin.Realm import io.realm.kotlin.UpdatePolicy @@ -25,14 +26,25 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import kotlinx.datetime.Clock import kotlinx.datetime.TimeZone import kotlinx.datetime.toInstant import kotlinx.datetime.toLocalDateTime import org.koin.core.component.KoinComponent import org.koin.core.component.inject +import kotlin.random.Random -class TeamDb: RealmObject { +class PredictionDb : RealmObject { + @PrimaryKey + var id: Int = 0 + var fixtureId: Int = 0 + var homeScores: String = "" + var awayScores: String = "" +} + + +class TeamDb : RealmObject { @PrimaryKey var id: Int = 0 var index: Int = 0 @@ -40,7 +52,7 @@ class TeamDb: RealmObject { var code: Int = 0 } -class PlayerDb: RealmObject { +class PlayerDb : RealmObject { @PrimaryKey var id: Int = 0 var firstName: String = "" @@ -54,7 +66,7 @@ class PlayerDb: RealmObject { var team: TeamDb? = null } -class FixtureDb: RealmObject { +class FixtureDb : RealmObject { @PrimaryKey var id: Int = 0 var kickoffTime: String? = "" @@ -63,6 +75,7 @@ class FixtureDb: RealmObject { var homeTeamScore: Int = 0 var awayTeamScore: Int = 0 var event: Int = 0 + var prediction: PredictionDb? = null } class FantasyPremierLeagueRepository : KoinComponent { @@ -95,90 +108,115 @@ class FantasyPremierLeagueRepository : KoinComponent { private var _currentGameweek: MutableStateFlow = MutableStateFlow(1) @NativeCoroutines - val currentGameweek = _currentGameweek.asStateFlow() + val currentGameweek: StateFlow = _currentGameweek.asStateFlow() init { coroutineScope.launch { loadData() + } + } - launch { - realm.query().asFlow() - .map { it.list } - .collect { it: RealmResults -> - _teamList.value = it.toList().map { - Team(it.id, it.index, it.name, it.code) - } - } - } - - launch { - realm.query().asFlow() - .map { it.list } - .collect { it: RealmResults -> - _playerList.value = it.toList().map { - val playerName = "${it.firstName} ${it.secondName}" - val playerImageUrl = "https://resources.premierleague.com/premierleague/photos/players/110x140/p${it.code}.png" - val teamName = it.team?.name ?: "" - val currentPrice = it.nowCost / 10.0 - - Player(it.id, playerName, teamName, playerImageUrl, it.totalPoints, currentPrice, it.goalsScored, it.assists) - } + private suspend fun loadData() { + coroutineScope.launch { + try { + val bootstrapStaticInfoDto = fantasyPremierLeagueApi.fetchBootstrapStaticInfo() + val fixtures = fantasyPremierLeagueApi.fetchFixtures() + writeDataToDb(bootstrapStaticInfoDto, fixtures) + + launch { + realm.query().asFlow() + .map { it.list } + .collect { it: RealmResults -> + _teamList.value = it.toList().map { + Team(it.id, it.index, it.name, it.code) + } + } } - } - launch { - realm.query().asFlow() - .map { it.list } - .collect { it: RealmResults -> - _fixtureList.value = it.toList().mapNotNull { - val homeTeamName = it.homeTeam?.name ?: "" - val homeTeamCode = it.homeTeam?.code ?: 0 - val homeTeamScore = it.homeTeamScore ?: 0 - val homeTeamPhotoUrl = - "https://resources.premierleague.com/premierleague/badges/t${homeTeamCode}.png" - - val awayTeamName = it.awayTeam?.name ?: "" - val awayTeamCode = it.awayTeam?.code ?: 0 - val awayTeamScore = it.awayTeamScore ?: 0 - val awayTeamPhotoUrl = - "https://resources.premierleague.com/premierleague/badges/t${awayTeamCode}.png" - - it.kickoffTime?.let { kickoffTime -> - val localKickoffTime = kickoffTime.toInstant() - .toLocalDateTime(TimeZone.currentSystemDefault()) - - val gf = GameFixture( - it.id, - localKickoffTime, - homeTeamName, - awayTeamName, - homeTeamPhotoUrl, - awayTeamPhotoUrl, - homeTeamScore, - awayTeamScore, - it.event + launch { + realm.query().asFlow() + .map { it.list } + .collect { it: RealmResults -> + _playerList.value = it.toList().map { + val playerName = "${it.firstName} ${it.secondName}" + val playerImageUrl = + "https://resources.premierleague.com/premierleague/photos/players/110x140/p${it.code}.png" + val teamName = it.team?.name ?: "" + val currentPrice = it.nowCost / 10.0 + + Player( + id = it.id, + name = playerName, + team = teamName, + photoUrl = playerImageUrl, + points = it.totalPoints, + currentPrice = currentPrice, + goalsScored = it.goalsScored, + assists = it.assists ) + } + } + } - return@let gf + launch { + realm.query().asFlow() + .map { it.list } + .collect { it: RealmResults -> + _fixtureList.value = it.toList().mapNotNull { + val homeTeamName = it.homeTeam?.name ?: "" + val homeTeamCode = it.homeTeam?.code ?: 0 + val homeTeamScore = it.homeTeamScore ?: 0 + val homeTeamPhotoUrl = + "https://resources.premierleague.com/premierleague/badges/t${homeTeamCode}.png" + + val awayTeamName = it.awayTeam?.name ?: "" + val awayTeamCode = it.awayTeam?.code ?: 0 + val awayTeamScore = it.awayTeamScore ?: 0 + val awayTeamPhotoUrl = + "https://resources.premierleague.com/premierleague/badges/t${awayTeamCode}.png" + + it.kickoffTime?.let { kickoffTime -> + val localKickoffTime = kickoffTime.toInstant() + .toLocalDateTime(TimeZone.currentSystemDefault()) + + val predictionDb = it.prediction + val prediction = + if (predictionDb != null && predictionDb.id != 0) { + Prediction( + id = predictionDb.id, + fixtureId = it.id, + homeScores = predictionDb.homeScores, + awayScores = predictionDb.awayScores, + ) + + } else { + null + } + val gf = GameFixture( + id = it.id, + localKickoffTime = localKickoffTime, + homeTeam = homeTeamName, + awayTeam = awayTeamName, + homeTeamPhotoUrl = homeTeamPhotoUrl, + awayTeamPhotoUrl = awayTeamPhotoUrl, + homeTeamScore = homeTeamScore, + awayTeamScore = awayTeamScore, + event = it.event, + prediction = prediction + ) + + return@let gf + } } + //Build gameweek to fixture map + _gameweekToFixtureMap.value = _fixtureList.value + .groupBy { it.event } } - //Build gameweek to fixture map - _gameweekToFixtureMap.value = _fixtureList.value - .groupBy { it.event } } + } catch (e: Exception) { + // TODO surface this to UI/option to retry etc ? + println("Exception reading data: $e") } - - } - } - - private suspend fun loadData() { - try { - val bootstrapStaticInfoDto = fantasyPremierLeagueApi.fetchBootstrapStaticInfo() - val fixtures = fantasyPremierLeagueApi.fetchFixtures() - writeDataToDb(bootstrapStaticInfoDto, fixtures) - } catch (e: Exception) { - // TODO surface this to UI/option to retry etc ? - println("Exception reading data: $e") } } @@ -186,7 +224,7 @@ class FantasyPremierLeagueRepository : KoinComponent { private suspend fun writeDataToDb( bootstrapStaticInfoDto: BootstrapStaticInfoDto, fixtures: List - ) { + ) { realm.write { // store teams @@ -217,10 +255,12 @@ class FantasyPremierLeagueRepository : KoinComponent { } //store current gameweek - _currentGameweek.value = bootstrapStaticInfoDto.events.firstOrNull { it.is_current }?.id ?: 1 + _currentGameweek.value = + bootstrapStaticInfoDto.events.firstOrNull { it.is_current }?.id ?: 1 // store fixtures val teams = query().find().toList() + val predictions = query().find().toList() fixtures.forEach { fixtureDto -> if (fixtureDto.kickoff_time != null) { copyToRealm(FixtureDb().apply { @@ -232,6 +272,7 @@ class FantasyPremierLeagueRepository : KoinComponent { homeTeam = teams.find { it.index == fixtureDto.team_h } awayTeam = teams.find { it.index == fixtureDto.team_a } + prediction = predictions.find { it.fixtureId == fixtureDto.id } }, updatePolicy = UpdatePolicy.ALL) } } @@ -256,8 +297,28 @@ class FantasyPremierLeagueRepository : KoinComponent { return fantasyPremierLeagueApi.fetchEventStatus() } + @NativeCoroutines + suspend fun submitPredict(prediction: Prediction) { + realm.write { + copyToRealm(PredictionDb().apply { + id = prediction.id ?: generateUniqueID() + fixtureId = prediction.fixtureId + homeScores = prediction.homeScores + awayScores = prediction.awayScores + }, updatePolicy = UpdatePolicy.ALL) + } + loadData() + } + fun updateLeagues(leagues: List) { appSettings.updatesLeagesSetting(leagues) } } + + +fun generateUniqueID(): Int { + val timestamp = Clock.System.now().toEpochMilliseconds().toInt() + val random = Random.nextInt(1000) + return timestamp + random +} \ No newline at end of file diff --git a/common/src/commonMain/kotlin/dev/johnoreilly/common/di/Koin.kt b/common/src/commonMain/kotlin/dev/johnoreilly/common/di/Koin.kt index cb31d01..8bb94fd 100644 --- a/common/src/commonMain/kotlin/dev/johnoreilly/common/di/Koin.kt +++ b/common/src/commonMain/kotlin/dev/johnoreilly/common/di/Koin.kt @@ -5,6 +5,7 @@ import dev.johnoreilly.common.data.remote.FantasyPremierLeagueApi import dev.johnoreilly.common.data.repository.FantasyPremierLeagueRepository import dev.johnoreilly.common.data.repository.FixtureDb import dev.johnoreilly.common.data.repository.PlayerDb +import dev.johnoreilly.common.data.repository.PredictionDb import dev.johnoreilly.common.data.repository.TeamDb import dev.johnoreilly.common.platformModule import io.ktor.client.* @@ -33,7 +34,16 @@ fun commonModule(enableNetworkLogs: Boolean) = module { single { createJson() } single { createHttpClient(get(), get(), enableNetworkLogs = enableNetworkLogs) } - single { RealmConfiguration.create(schema = setOf(PlayerDb::class, TeamDb::class, FixtureDb::class)) } + single { + RealmConfiguration.create( + schema = setOf( + PlayerDb::class, + TeamDb::class, + FixtureDb::class, + PredictionDb::class + ) + ) + } single { Realm.open(get()) } single { FantasyPremierLeagueRepository() } @@ -44,14 +54,15 @@ fun commonModule(enableNetworkLogs: Boolean) = module { fun createJson() = Json { isLenient = true; ignoreUnknownKeys = true } -fun createHttpClient(httpClientEngine: HttpClientEngine, json: Json, enableNetworkLogs: Boolean) = HttpClient(httpClientEngine) { - install(ContentNegotiation) { - json(json) - } - if (enableNetworkLogs) { - install(Logging) { - logger = Logger.DEFAULT - level = LogLevel.NONE +fun createHttpClient(httpClientEngine: HttpClientEngine, json: Json, enableNetworkLogs: Boolean) = + HttpClient(httpClientEngine) { + install(ContentNegotiation) { + json(json) + } + if (enableNetworkLogs) { + install(Logging) { + logger = Logger.DEFAULT + level = LogLevel.NONE + } } } -} diff --git a/common/src/commonMain/kotlin/dev/johnoreilly/common/domain/entities/GameFixture.kt b/common/src/commonMain/kotlin/dev/johnoreilly/common/domain/entities/GameFixture.kt index 8e3f703..a2f8c47 100644 --- a/common/src/commonMain/kotlin/dev/johnoreilly/common/domain/entities/GameFixture.kt +++ b/common/src/commonMain/kotlin/dev/johnoreilly/common/domain/entities/GameFixture.kt @@ -1,6 +1,10 @@ package dev.johnoreilly.common.domain.entities +import kotlinx.datetime.Clock import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toInstant +import kotlinx.datetime.toLocalDateTime data class GameFixture( val id: Int, @@ -11,5 +15,63 @@ data class GameFixture( val awayTeamPhotoUrl: String, val homeTeamScore: Int?, val awayTeamScore: Int?, - val event: Int -) \ No newline at end of file + val event: Int, + val prediction: Prediction?, +) { + + val isPredicted: Boolean = prediction != null + val isNotStartedYet = localKickoffTime.isPast() + val isLive = isFixtureLive() + + val homeScore = if (isNotStartedYet) RESULT_PLACE_HOLDER else homeTeamScore.toString() + val awayScore = if (isNotStartedYet) RESULT_PLACE_HOLDER else awayTeamScore.toString() + + companion object { + private const val RESULT_PLACE_HOLDER = "-" + val mockGameFixture = GameFixture( + id = 0, + localKickoffTime = LocalDateTime.parse("2023-12-14T11:30"), + homeTeam = "Brentford", + awayTeam = "Arsenal", + homeTeamPhotoUrl = "https://resources.premierleague.com/premierleague/badges/t8.svg", + awayTeamPhotoUrl = "https://resources.premierleague.com/premierleague/badges/t3.svg", + homeTeamScore = 2, + awayTeamScore = 0, + event = 1, + prediction = null, + + ) + } + + fun getPredictionMessage(): String { + if (isPredicted.not() || homeScore == RESULT_PLACE_HOLDER || awayScore == RESULT_PLACE_HOLDER) return "" + + val homeDifference = prediction!!.homeScores.toInt() - homeTeamScore!! + val awayDifference = prediction.awayScores.toInt() - awayTeamScore!! + + val totalDifference = homeDifference + awayDifference + + return when { + homeDifference == 0 && awayDifference == 0 -> "Wow! You predicted the exact score for both teams. You're a prediction genius!" + homeDifference in -2..2 && awayDifference in -2..2 -> "Impressive! You were very close for both teams. Keep up the good work!" + totalDifference in -5..5 -> "You were close on the total score. Keep predicting, and you'll get even better!" + homeDifference < -5 || awayDifference < -5 -> "It was a tough one, but don't give up. Predicting can be tricky sometimes!" + else -> "Your prediction was quite different from the result. No worries, better luck next time!" + } + } + +} + +fun GameFixture.isFixtureLive(): Boolean { + val currentDateTime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) + + val fixtureMillis = localKickoffTime.toInstant(TimeZone.UTC).toEpochMilliseconds() + val currentMillis = currentDateTime.toInstant(TimeZone.UTC).toEpochMilliseconds() + + val durationMinutes = (currentMillis - fixtureMillis) / (1000 * 60) // Milliseconds to minutes + + return durationMinutes in 1..95 +} + +fun LocalDateTime.isPast(): Boolean = + this > Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) \ No newline at end of file diff --git a/common/src/commonMain/kotlin/dev/johnoreilly/common/domain/entities/Prediction.kt b/common/src/commonMain/kotlin/dev/johnoreilly/common/domain/entities/Prediction.kt new file mode 100644 index 0000000..5c8f991 --- /dev/null +++ b/common/src/commonMain/kotlin/dev/johnoreilly/common/domain/entities/Prediction.kt @@ -0,0 +1,9 @@ +package dev.johnoreilly.common.domain.entities + + +data class Prediction( + val id: Int? = null, + val fixtureId: Int, + val homeScores: String, + val awayScores: String, +) diff --git a/ios/FantasyPremierLeague/FantasyPremierLeague.xcodeproj/project.pbxproj b/ios/FantasyPremierLeague/FantasyPremierLeague.xcodeproj/project.pbxproj index 750a80a..7bcb118 100644 --- a/ios/FantasyPremierLeague/FantasyPremierLeague.xcodeproj/project.pbxproj +++ b/ios/FantasyPremierLeague/FantasyPremierLeague.xcodeproj/project.pbxproj @@ -289,7 +289,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + JAVA_HOME = /opt/homebrew/opt/openjdk; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -344,7 +345,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + JAVA_HOME = /opt/homebrew/opt/openjdk; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -359,9 +361,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"FantasyPremierLeague/Preview Content\""; - DEVELOPMENT_TEAM = NT77748GS8; + DEVELOPMENT_TEAM = VFCFJC7Y5J; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../../common/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)"; INFOPLIST_FILE = FantasyPremierLeague/Info.plist; @@ -377,6 +380,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = dev.johnoreilly.FantasyPremierLeague; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -387,9 +391,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"FantasyPremierLeague/Preview Content\""; - DEVELOPMENT_TEAM = NT77748GS8; + DEVELOPMENT_TEAM = VFCFJC7Y5J; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../../common/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)"; INFOPLIST_FILE = FantasyPremierLeague/Info.plist; @@ -406,6 +411,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = dev.johnoreilly.FantasyPremierLeague; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; diff --git a/ios/FantasyPremierLeague/FantasyPremierLeague/ContentView.swift b/ios/FantasyPremierLeague/FantasyPremierLeague/ContentView.swift index 7bc79e2..b2cd303 100644 --- a/ios/FantasyPremierLeague/FantasyPremierLeague/ContentView.swift +++ b/ios/FantasyPremierLeague/FantasyPremierLeague/ContentView.swift @@ -6,22 +6,27 @@ import FantasyPremierLeagueKit struct ContentView: View { @StateObject var viewModel = FantasyPremierLeagueViewModel(repository: FantasyPremierLeagueRepository()) - var body: some View { - TabView { - PlayerListView(viewModel: viewModel) - .tabItem { - Label("Players", systemImage: "person") - } - FixtureListView(viewModel: viewModel) - .tabItem { - Label("Fixtues", systemImage: "clock") - } - LeagueListView(viewModel: viewModel) - .tabItem { - Label("League", systemImage: "list.number") - } - } - } + @State private var selectedTab = 1 + + var body: some View { + TabView(selection: $selectedTab) { + PlayerListView(viewModel: viewModel) + .tabItem { + Label("Players", systemImage: "person") + } + .tag(0) // First tab + FixtureListView(viewModel: viewModel) + .tabItem { + Label("Fixtures", systemImage: "clock") + } + .tag(1) // Second tab + LeagueListView(viewModel: viewModel) + .tabItem { + Label("League", systemImage: "list.number") + } + .tag(2) // Third tab + } + } } diff --git a/ios/FantasyPremierLeague/FantasyPremierLeague/Fixtures/FixtureDetailView.swift b/ios/FantasyPremierLeague/FantasyPremierLeague/Fixtures/FixtureDetailView.swift index 05cae66..59dfebf 100644 --- a/ios/FantasyPremierLeague/FantasyPremierLeague/Fixtures/FixtureDetailView.swift +++ b/ios/FantasyPremierLeague/FantasyPremierLeague/Fixtures/FixtureDetailView.swift @@ -3,6 +3,7 @@ import FantasyPremierLeagueKit struct FixtureDetailView: View { var fixture: GameFixture + var onSubmitPredict: (Prediction) -> Void var body: some View { @@ -18,6 +19,11 @@ struct FixtureDetailView: View { Spacer() ClubInFixtureView(teamName: fixture.awayTeam, teamPhotoUrl: fixture.awayTeamPhotoUrl) } + + if fixture.isNotStartedYet || fixture.isPredicted { + PredictionView(gameFixture: fixture, onSubmitPredict: onSubmitPredict) + } + } List { @@ -34,4 +40,93 @@ struct FixtureDetailView: View { +struct PredictionView: View { + @State private var homeTeamPrediction = "" + @State private var awayTeamPrediction = "" + + var gameFixture: GameFixture + var onSubmitPredict: (Prediction) -> Void + + var body: some View { + let isNotPredicted = !gameFixture.isPredicted + + VStack(spacing: 16) { + LinearGradientText(isNotPredicted: isNotPredicted) + + HStack { + ScoreTextField(value: $homeTeamPrediction) + Spacer().frame(width: 16) + ScoreTextField(value: $awayTeamPrediction) + } + + if isNotPredicted { + SubmitButton(onSubmit: { + onSubmitPredict(Prediction(id: nil,fixtureId: gameFixture.id, homeScores: homeTeamPrediction, awayScores: awayTeamPrediction)) + }) + } else { + Text(gameFixture.getPredictionMessage()) + .font(.body) + .padding(.top, 8) + } + } + .padding(8) + } +} + +struct LinearGradientText: View { + let isNotPredicted: Bool + + var body: some View { + let title = isNotPredicted ? "Make Your Predictions" : "Predictions Submitted" + return ZStack { + LinearGradient(gradient: Gradient(stops: [ + .init(color: Color(red: 0.77, green: 0.66, blue: 1.00), location: 0), + .init(color: Color(red: 0.49, green: 0.26, blue: 0.64), location: 1) + ]), startPoint: .top, endPoint: .bottom) + + Text(title) + .font(.title) + .fontWeight(.bold) + .foregroundColor(.white) + .padding(12) + } + .frame(maxWidth: .infinity) + } +} + +struct ScoreTextField: View { + @Binding var value: String + + var body: some View { + TextField("", text: $value) + .font(.system(size: 34)) + .foregroundColor(.black) + .multilineTextAlignment(.center) + .keyboardType(.numberPad) + .padding(8) + .background(Color.gray.opacity(0.3)) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .frame(width: 120, height: 60) + .padding(.horizontal, 16) + } +} + +struct SubmitButton: View { + var onSubmit: () -> Void + + var body: some View { + Button(action: onSubmit) { + Text("Submit Predictions") + .fontWeight(.bold) + .frame(width: 200, height: 40) + .background(Color.blue) + .foregroundColor(.white) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + } +} + + + + diff --git a/ios/FantasyPremierLeague/FantasyPremierLeague/Fixtures/FixtureListView.swift b/ios/FantasyPremierLeague/FantasyPremierLeague/Fixtures/FixtureListView.swift index a402866..71b81d0 100644 --- a/ios/FantasyPremierLeague/FantasyPremierLeague/Fixtures/FixtureListView.swift +++ b/ios/FantasyPremierLeague/FantasyPremierLeague/Fixtures/FixtureListView.swift @@ -8,8 +8,7 @@ extension GameFixture: Identifiable { } struct FixtureListView: View { @ObservedObject var viewModel: FantasyPremierLeagueViewModel - - @State var gameWeek = 1 + var body: some View { VStack { @@ -17,19 +16,19 @@ struct FixtureListView: View { VStack(spacing: 0) { HStack { Button(action: { - if (gameWeek > 1) { gameWeek = gameWeek - 1 } + if (viewModel.gameWeek > 1) { viewModel.gameWeek = viewModel.gameWeek - 1 } }) { Image(systemName: "arrow.left") } - Text("Gameweek \(gameWeek)") + Text("Gameweek \(viewModel.gameWeek)") Button(action: { - if (gameWeek < 38) { gameWeek = gameWeek + 1 } + if (viewModel.gameWeek < 38) { viewModel.gameWeek = viewModel.gameWeek + 1 } }) { Image(systemName: "arrow.right") } } - List(viewModel.gameWeekFixtures[gameWeek] ?? []) { fixture in - NavigationLink(destination: FixtureDetailView(fixture: fixture)) { + List(viewModel.gameWeekFixtures[viewModel.gameWeek] ?? []) { fixture in + NavigationLink(destination: FixtureDetailView(fixture: fixture, onSubmitPredict: { prediction in viewModel.onSubmitPredict(prediction: prediction) })) { FixtureView(fixture: fixture) } } @@ -56,14 +55,15 @@ struct FixtureView: View { var body: some View { VStack { + PredictView(fixture: fixture) HStack { ClubInFixtureView(teamName: fixture.homeTeam, teamPhotoUrl: fixture.homeTeamPhotoUrl) Spacer() - Text("(\(fixture.homeTeamScore ?? 0))").font(.system(size: 20)) + Text(fixture.homeScore).font(.system(size: 20)) Spacer() Text("vs").font(.system(size: 22)) Spacer() - Text("(\(fixture.awayTeamScore ?? 0))").font(.system(size: 20)) + Text(fixture.awayScore).font(.system(size: 20)) Spacer() ClubInFixtureView(teamName: fixture.awayTeam, teamPhotoUrl: fixture.awayTeamPhotoUrl) } @@ -74,6 +74,37 @@ struct FixtureView: View { } } +struct PredictView: View { + let fixture: GameFixture + + var body: some View { + if fixture.isLive { + VStack { + Text(fixture.isPredicted ? "Prediction submitted! Watch the game live!" : "Live!") + .font(.system(size: 14)) + .fontWeight(.bold) + .foregroundColor(.white) + .padding(8) + .background(LinearGradient(gradient: Gradient(colors: [Color(red: 0.77, green: 0.66, blue: 1.00), Color(red: 0.49, green: 0.26, blue: 0.64)]), startPoint: .top, endPoint: .bottom)) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + .frame(maxWidth: .infinity) + .padding(8) + } else if fixture.isNotStartedYet { + VStack { + Text(fixture.isPredicted ? "Prediction submitted!" : "Join the Betting Action!") + .font(.system(size: 14)) + .fontWeight(.bold) + .foregroundColor(.white) + .padding(8) + .background(LinearGradient(gradient: Gradient(colors: [Color(red: 0.77, green: 0.66, blue: 1.00), Color(red: 0.49, green: 0.26, blue: 0.64)]), startPoint: .top, endPoint: .bottom)) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + .frame(maxWidth: .infinity) + .padding(8) + } + } +} struct ClubInFixtureView: View { let teamName: String diff --git a/ios/FantasyPremierLeague/FantasyPremierLeague/ViewModel.swift b/ios/FantasyPremierLeague/FantasyPremierLeague/ViewModel.swift index 7c5b10e..232d21a 100644 --- a/ios/FantasyPremierLeague/FantasyPremierLeague/ViewModel.swift +++ b/ios/FantasyPremierLeague/FantasyPremierLeague/ViewModel.swift @@ -3,6 +3,7 @@ import FantasyPremierLeagueKit import KMPNativeCoroutinesAsync import AsyncAlgorithms import CollectionConcurrencyKit +import KMPNativeCoroutinesAsync extension PlayerPastHistory: Identifiable { } @@ -14,6 +15,7 @@ class FantasyPremierLeagueViewModel: ObservableObject { @Published var playerList = [Player]() @Published var fixtureList = [GameFixture]() @Published var gameWeekFixtures = [Int: [GameFixture]]() + @Published var gameWeek: Int = 1 @Published var playerHistory = [PlayerPastHistory]() @Published var leagueStandings = [LeagueStandingsDto]() @@ -30,6 +32,12 @@ class FantasyPremierLeagueViewModel: ObservableObject { Task { do { + //TODO improve this + let thisWeek = asyncSequence(for: repository.currentGameweek) + for try await weekNumber in thisWeek { + gameWeek = Int(weekNumber.int32Value) + } + let leagueStream = asyncSequence(for: repository.leagues) for try await data in leagueStream { leagues = data @@ -124,6 +132,10 @@ class FantasyPremierLeagueViewModel: ObservableObject { print("setLeagues, leagues = \(leagues)") repository.updateLeagues(leagues: leagues) } + + func onSubmitPredict(prediction: Prediction) { + repository.submitPredict(prediction: prediction) + } } From 5d97e2b49291784bf2ddbcdfd93bebf2aee75bac Mon Sep 17 00:00:00 2001 From: ali Date: Mon, 23 Oct 2023 21:04:56 +0200 Subject: [PATCH 2/4] updated project.pbxproj --- .../FantasyPremierLeague.xcodeproj/project.pbxproj | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/ios/FantasyPremierLeague/FantasyPremierLeague.xcodeproj/project.pbxproj b/ios/FantasyPremierLeague/FantasyPremierLeague.xcodeproj/project.pbxproj index 7bcb118..424e7c7 100644 --- a/ios/FantasyPremierLeague/FantasyPremierLeague.xcodeproj/project.pbxproj +++ b/ios/FantasyPremierLeague/FantasyPremierLeague.xcodeproj/project.pbxproj @@ -155,6 +155,7 @@ 1A0F7AD625C5C96F00EB34CF /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1230; LastUpgradeCheck = 1230; TargetAttributes = { @@ -290,7 +291,6 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.0; - JAVA_HOME = /opt/homebrew/opt/openjdk; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -337,6 +337,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -346,7 +347,6 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.0; - JAVA_HOME = /opt/homebrew/opt/openjdk; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -361,10 +361,9 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"FantasyPremierLeague/Preview Content\""; - DEVELOPMENT_TEAM = VFCFJC7Y5J; + DEVELOPMENT_TEAM = NT77748GS8; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../../common/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)"; INFOPLIST_FILE = FantasyPremierLeague/Info.plist; @@ -380,7 +379,6 @@ ); PRODUCT_BUNDLE_IDENTIFIER = dev.johnoreilly.FantasyPremierLeague; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -391,10 +389,9 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"FantasyPremierLeague/Preview Content\""; - DEVELOPMENT_TEAM = VFCFJC7Y5J; + DEVELOPMENT_TEAM = NT77748GS8; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../../common/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)"; INFOPLIST_FILE = FantasyPremierLeague/Info.plist; @@ -411,7 +408,6 @@ ); PRODUCT_BUNDLE_IDENTIFIER = dev.johnoreilly.FantasyPremierLeague; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; From 413387c7af71220a55c58c4861b088f6f591cbb9 Mon Sep 17 00:00:00 2001 From: ali Date: Mon, 23 Oct 2023 21:41:07 +0200 Subject: [PATCH 3/4] disabled prediction button after user predict --- .../Fixtures/FixtureDetailView.swift | 19 +++++++++++++++---- .../Fixtures/FixtureListView.swift | 6 +++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/ios/FantasyPremierLeague/FantasyPremierLeague/Fixtures/FixtureDetailView.swift b/ios/FantasyPremierLeague/FantasyPremierLeague/Fixtures/FixtureDetailView.swift index 59dfebf..4538364 100644 --- a/ios/FantasyPremierLeague/FantasyPremierLeague/Fixtures/FixtureDetailView.swift +++ b/ios/FantasyPremierLeague/FantasyPremierLeague/Fixtures/FixtureDetailView.swift @@ -43,25 +43,34 @@ struct FixtureDetailView: View { struct PredictionView: View { @State private var homeTeamPrediction = "" @State private var awayTeamPrediction = "" + @State private var isPredicted = false var gameFixture: GameFixture var onSubmitPredict: (Prediction) -> Void var body: some View { - let isNotPredicted = !gameFixture.isPredicted + let isNotPredicted = !isPredicted //TODO remove after testing VStack(spacing: 16) { LinearGradientText(isNotPredicted: isNotPredicted) HStack { - ScoreTextField(value: $homeTeamPrediction) + ScoreTextField(value: $homeTeamPrediction, isPredicted: $isPredicted) Spacer().frame(width: 16) - ScoreTextField(value: $awayTeamPrediction) + ScoreTextField(value: $awayTeamPrediction, isPredicted: $isPredicted) } if isNotPredicted { SubmitButton(onSubmit: { - onSubmitPredict(Prediction(id: nil,fixtureId: gameFixture.id, homeScores: homeTeamPrediction, awayScores: awayTeamPrediction)) + isPredicted = true + onSubmitPredict( + Prediction( + id: nil, + fixtureId: gameFixture.id, + homeScores: homeTeamPrediction, + awayScores: awayTeamPrediction + ) + ) }) } else { Text(gameFixture.getPredictionMessage()) @@ -96,6 +105,7 @@ struct LinearGradientText: View { struct ScoreTextField: View { @Binding var value: String + @Binding var isPredicted: Bool var body: some View { TextField("", text: $value) @@ -108,6 +118,7 @@ struct ScoreTextField: View { .clipShape(RoundedRectangle(cornerRadius: 10)) .frame(width: 120, height: 60) .padding(.horizontal, 16) + .disabled(isPredicted) } } diff --git a/ios/FantasyPremierLeague/FantasyPremierLeague/Fixtures/FixtureListView.swift b/ios/FantasyPremierLeague/FantasyPremierLeague/Fixtures/FixtureListView.swift index 71b81d0..bd4fb86 100644 --- a/ios/FantasyPremierLeague/FantasyPremierLeague/Fixtures/FixtureListView.swift +++ b/ios/FantasyPremierLeague/FantasyPremierLeague/Fixtures/FixtureListView.swift @@ -28,7 +28,11 @@ struct FixtureListView: View { } } List(viewModel.gameWeekFixtures[viewModel.gameWeek] ?? []) { fixture in - NavigationLink(destination: FixtureDetailView(fixture: fixture, onSubmitPredict: { prediction in viewModel.onSubmitPredict(prediction: prediction) })) { + NavigationLink( + destination: FixtureDetailView( + fixture: fixture, + onSubmitPredict: { prediction in viewModel.onSubmitPredict(prediction: prediction) } + )) { FixtureView(fixture: fixture) } } From b072dcf743f98372b99ccff8a397b583139d6e2d Mon Sep 17 00:00:00 2001 From: ali Date: Mon, 23 Oct 2023 22:46:22 +0200 Subject: [PATCH 4/4] fixed predict submission on iOS --- .../FixtureDetails/FixtureDetailsView.kt | 5 +++-- .../common/domain/entities/GameFixture.kt | 6 +++++- .../Fixtures/FixtureDetailView.swift | 21 ++++++++++++------- .../FantasyPremierLeague/ViewModel.swift | 4 +++- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/dev/johnoreilly/fantasypremierleague/presentation/fixtures/FixtureDetails/FixtureDetailsView.kt b/app/src/main/java/dev/johnoreilly/fantasypremierleague/presentation/fixtures/FixtureDetails/FixtureDetailsView.kt index 5297057..d3f0041 100644 --- a/app/src/main/java/dev/johnoreilly/fantasypremierleague/presentation/fixtures/FixtureDetails/FixtureDetailsView.kt +++ b/app/src/main/java/dev/johnoreilly/fantasypremierleague/presentation/fixtures/FixtureDetails/FixtureDetailsView.kt @@ -286,8 +286,9 @@ private fun PredictionView( text = gameFixture.getPredictionMessage(), style = MaterialTheme.typography.bodyMedium, modifier = Modifier - .padding(top = 8.dp) - .align(Alignment.CenterHorizontally) + .padding(top = 8.dp, start = 16.dp, end = 16.dp) + .align(Alignment.CenterHorizontally), + color = Color.Gray ) } diff --git a/common/src/commonMain/kotlin/dev/johnoreilly/common/domain/entities/GameFixture.kt b/common/src/commonMain/kotlin/dev/johnoreilly/common/domain/entities/GameFixture.kt index a2f8c47..8852db9 100644 --- a/common/src/commonMain/kotlin/dev/johnoreilly/common/domain/entities/GameFixture.kt +++ b/common/src/commonMain/kotlin/dev/johnoreilly/common/domain/entities/GameFixture.kt @@ -44,7 +44,11 @@ data class GameFixture( } fun getPredictionMessage(): String { - if (isPredicted.not() || homeScore == RESULT_PLACE_HOLDER || awayScore == RESULT_PLACE_HOLDER) return "" + if (isPredicted.not()) return "" + + if (isNotStartedYet) { + return "The game hasn't started yet. Come back later to see if your prediction was correct!" + } val homeDifference = prediction!!.homeScores.toInt() - homeTeamScore!! val awayDifference = prediction.awayScores.toInt() - awayTeamScore!! diff --git a/ios/FantasyPremierLeague/FantasyPremierLeague/Fixtures/FixtureDetailView.swift b/ios/FantasyPremierLeague/FantasyPremierLeague/Fixtures/FixtureDetailView.swift index 4538364..a1b4a32 100644 --- a/ios/FantasyPremierLeague/FantasyPremierLeague/Fixtures/FixtureDetailView.swift +++ b/ios/FantasyPremierLeague/FantasyPremierLeague/Fixtures/FixtureDetailView.swift @@ -41,28 +41,33 @@ struct FixtureDetailView: View { struct PredictionView: View { - @State private var homeTeamPrediction = "" - @State private var awayTeamPrediction = "" - @State private var isPredicted = false + @State private var homeTeamPrediction: String + @State private var awayTeamPrediction: String var gameFixture: GameFixture var onSubmitPredict: (Prediction) -> Void + init(gameFixture: GameFixture, onSubmitPredict: @escaping (Prediction) -> Void) { + self.gameFixture = gameFixture + self.onSubmitPredict = onSubmitPredict + _homeTeamPrediction = State(initialValue: gameFixture.prediction?.homeScores ?? "") + _awayTeamPrediction = State(initialValue: gameFixture.prediction?.awayScores ?? "") + } + var body: some View { - let isNotPredicted = !isPredicted //TODO remove after testing + let isNotPredicted = !gameFixture.isPredicted VStack(spacing: 16) { LinearGradientText(isNotPredicted: isNotPredicted) HStack { - ScoreTextField(value: $homeTeamPrediction, isPredicted: $isPredicted) + ScoreTextField(value: $homeTeamPrediction, isPredicted: !isNotPredicted) Spacer().frame(width: 16) - ScoreTextField(value: $awayTeamPrediction, isPredicted: $isPredicted) + ScoreTextField(value: $awayTeamPrediction, isPredicted: !isNotPredicted) } if isNotPredicted { SubmitButton(onSubmit: { - isPredicted = true onSubmitPredict( Prediction( id: nil, @@ -105,7 +110,7 @@ struct LinearGradientText: View { struct ScoreTextField: View { @Binding var value: String - @Binding var isPredicted: Bool + var isPredicted: Bool var body: some View { TextField("", text: $value) diff --git a/ios/FantasyPremierLeague/FantasyPremierLeague/ViewModel.swift b/ios/FantasyPremierLeague/FantasyPremierLeague/ViewModel.swift index 232d21a..7240617 100644 --- a/ios/FantasyPremierLeague/FantasyPremierLeague/ViewModel.swift +++ b/ios/FantasyPremierLeague/FantasyPremierLeague/ViewModel.swift @@ -134,7 +134,9 @@ class FantasyPremierLeagueViewModel: ObservableObject { } func onSubmitPredict(prediction: Prediction) { - repository.submitPredict(prediction: prediction) + Task { + try await asyncFunction(for: repository.submitPredict(prediction: prediction)) + } } }