feat: add LoginScreen, ScheduleScreen, and EventDetailScreen
Add 3 Compose Multiplatform screens consuming Task 6 ViewModels and Task 8 components: - LoginScreen: MyIce title, login button, loading/error states - ScheduleScreen: AppBar + filter card (4 dropdowns) + event list with loading/error/empty/refreshing states - EventDetailScreen: AppBar + recap card + players/staff sections with amber empty-state card Use TextButton for Refresh/Logout actions (material-icons-core not available in this Compose Multiplatform setup). Guard setAccount call against nullable onSelect callback.
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
package ch.parano.myice.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ch.parano.myice.ui.components.LoadingIndicator
|
||||
import ch.parano.myice.viewmodel.EventDetailViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun EventDetailScreen(
|
||||
gameId: String,
|
||||
account: String,
|
||||
eventTitle: String,
|
||||
viewModel: EventDetailViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LaunchedEffect(gameId, account) {
|
||||
viewModel.loadEventDetail(gameId, account)
|
||||
}
|
||||
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(title = { Text(eventTitle) })
|
||||
},
|
||||
modifier = modifier,
|
||||
) { padding ->
|
||||
when {
|
||||
state.isLoading -> {
|
||||
LoadingIndicator(modifier = Modifier.padding(padding))
|
||||
}
|
||||
state.error != null -> {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(padding),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text(
|
||||
text = state.error!!,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
TextButton(onClick = { viewModel.loadEventDetail(gameId, account) }) {
|
||||
Text("Reessayer")
|
||||
}
|
||||
}
|
||||
}
|
||||
state.eventDetail != null -> {
|
||||
val detail = state.eventDetail!!
|
||||
val hasPlayers = detail.convocation.available.isNotEmpty()
|
||||
val hasStaff = detail.convocation.staff.isNotEmpty()
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize().padding(padding),
|
||||
contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
item {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text("Type: ${detail.type}")
|
||||
Text("Lieu: ${detail.place}")
|
||||
Text("Heure: ${detail.timeStart} - ${detail.timeEnd}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasPlayers && !hasStaff) {
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0xFFFFC107),
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = "Aucun joueur ni personnel convoque",
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasPlayers) {
|
||||
item {
|
||||
Text(
|
||||
text = "Joueurs (${detail.convocation.available.size})",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
}
|
||||
items(detail.convocation.available) { player ->
|
||||
val position = player.position ?: "N/A"
|
||||
val number = player.number ?: "N/A"
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
) {
|
||||
Text("[$position] #$number - ${player.fname} ${player.lname}")
|
||||
Text(
|
||||
text = "DOB: ${player.dob}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasStaff) {
|
||||
item {
|
||||
Text(
|
||||
text = "Personnel",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
}
|
||||
items(detail.convocation.staff) { staff ->
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = "${staff.role}: ${staff.fname} ${staff.lname}",
|
||||
modifier = Modifier.padding(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(padding),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text("Aucune donnee disponible")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package ch.parano.myice.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ch.parano.myice.viewmodel.AuthViewModel
|
||||
import ch.parano.myice.viewmodel.AuthState
|
||||
|
||||
@Composable
|
||||
fun LoginScreen(
|
||||
viewModel: AuthViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
LaunchedEffect(state) {
|
||||
if (state is AuthState.Unauthenticated) {
|
||||
snackbarHostState.showSnackbar("Erreur de connexion")
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
modifier = modifier,
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(padding),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text(
|
||||
text = "MyIce",
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Text(
|
||||
text = "Games",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
if (state is AuthState.Loading) {
|
||||
CircularProgressIndicator()
|
||||
} else {
|
||||
Button(
|
||||
onClick = { viewModel.login() },
|
||||
) {
|
||||
Text("Se connecter avec Infomaniak")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package ch.parano.myice.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import ch.parano.myice.filter.TypeFilter
|
||||
import ch.parano.myice.ui.components.EventCard
|
||||
import ch.parano.myice.ui.components.FilterDropdown
|
||||
import ch.parano.myice.ui.components.LoadingIndicator
|
||||
import ch.parano.myice.viewmodel.AuthViewModel
|
||||
import ch.parano.myice.viewmodel.ScheduleViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ScheduleScreen(
|
||||
scheduleViewModel: ScheduleViewModel,
|
||||
authViewModel: AuthViewModel,
|
||||
onEventClick: (gameId: String, account: String, eventTitle: String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val scheduleState by scheduleViewModel.state.collectAsState()
|
||||
val authState by authViewModel.state.collectAsState()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("MyIce") },
|
||||
actions = {
|
||||
val email = (authState as? ch.parano.myice.viewmodel.AuthState.Authenticated)?.email
|
||||
if (email != null) {
|
||||
Text(
|
||||
text = email,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
)
|
||||
}
|
||||
TextButton(onClick = { scheduleViewModel.refreshSchedule() }) {
|
||||
Text("Refresh")
|
||||
}
|
||||
TextButton(onClick = {
|
||||
scheduleViewModel.clearCache()
|
||||
authViewModel.logout()
|
||||
}) {
|
||||
Text("Logout")
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(padding),
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
FilterDropdown(
|
||||
label = "Compte",
|
||||
selectedValue = scheduleState.selectedAccount,
|
||||
options = scheduleState.accounts.map { it.name },
|
||||
onSelect = { it?.let { acc -> scheduleViewModel.setAccount(acc) } },
|
||||
)
|
||||
FilterDropdown(
|
||||
label = "Age",
|
||||
selectedValue = scheduleState.selectedAgegroup,
|
||||
options = scheduleState.agegroups,
|
||||
onSelect = { scheduleViewModel.setAgegroup(it) },
|
||||
)
|
||||
FilterDropdown(
|
||||
label = "Sous-groupe",
|
||||
selectedValue = scheduleState.selectedSubgroup,
|
||||
options = scheduleState.subgroups,
|
||||
onSelect = { scheduleViewModel.setSubgroup(it) },
|
||||
)
|
||||
FilterDropdown(
|
||||
label = "Type",
|
||||
selectedValue = typeFilterLabel(scheduleState.typeFilter),
|
||||
options = listOf("Tous", "Matchs", "Entrainements"),
|
||||
onSelect = { label ->
|
||||
val filter = when (label) {
|
||||
"Tous" -> TypeFilter.ALL
|
||||
"Matchs" -> TypeFilter.GAMES
|
||||
"Entrainements" -> TypeFilter.PRACTICES
|
||||
else -> TypeFilter.DEFAULT
|
||||
}
|
||||
scheduleViewModel.setTypeFilter(filter)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
when {
|
||||
scheduleState.isLoading && scheduleState.events.isEmpty() -> {
|
||||
LoadingIndicator()
|
||||
}
|
||||
scheduleState.error != null && scheduleState.events.isEmpty() -> {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = scheduleState.error!!,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
TextButton(onClick = { scheduleViewModel.refreshSchedule() }) {
|
||||
Text("Reessayer")
|
||||
}
|
||||
}
|
||||
}
|
||||
scheduleState.filteredEvents.isEmpty() -> {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text("Aucun evenement disponible")
|
||||
if (scheduleState.events.isEmpty()) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Appuyez sur le bouton de rafraichissement pour charger les evenements",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.Gray,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
items(scheduleState.filteredEvents) { event ->
|
||||
EventCard(
|
||||
event = event,
|
||||
onClick = {
|
||||
onEventClick(
|
||||
event.idEvent,
|
||||
scheduleState.selectedAccount!!,
|
||||
event.title,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (scheduleState.isRefreshing) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Card {
|
||||
Column(
|
||||
modifier = Modifier.padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text("Chargement...", fontWeight = FontWeight.Medium)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun typeFilterLabel(filter: TypeFilter): String = when (filter) {
|
||||
TypeFilter.ALL -> "Tous"
|
||||
TypeFilter.GAMES -> "Matchs"
|
||||
TypeFilter.PRACTICES -> "Entrainements"
|
||||
}
|
||||
Reference in New Issue
Block a user