diff --git a/composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/components/EmptyState.kt b/composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/components/EmptyState.kt new file mode 100644 index 0000000..89c5a0c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/components/EmptyState.kt @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// MyIce Kotlin Multiplatform — schedule/convocation viewer +// Copyright (C) 2026 parano.ch +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package ch.parano.myicek.ui.components + +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.size +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +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.vector.ImageVector +import androidx.compose.ui.unit.dp +import ch.parano.myicek.ui.theme.Dimens + +@Composable +fun EmptyState( + icon: ImageVector, + title: String, + message: String, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(Modifier.height(Dimens.md)) + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + Spacer(Modifier.height(Dimens.sm)) + Text( + text = message, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } +} diff --git a/composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/components/ErrorState.kt b/composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/components/ErrorState.kt new file mode 100644 index 0000000..a67e9ed --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/components/ErrorState.kt @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// MyIce Kotlin Multiplatform — schedule/convocation viewer +// Copyright (C) 2026 parano.ch +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package ch.parano.myicek.ui.components + +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.size +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +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.vector.ImageVector +import androidx.compose.ui.unit.dp +import ch.parano.myicek.ui.theme.Dimens + +@Composable +fun ErrorState( + icon: ImageVector, + title: String, + message: String, + onRetry: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(Modifier.height(Dimens.md)) + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + Spacer(Modifier.height(Dimens.sm)) + Text( + text = message, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(Modifier.height(Dimens.lg)) + Button(onClick = onRetry) { + Text("Réessayer") + } + } +} diff --git a/composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/components/SkeletonCard.kt b/composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/components/SkeletonCard.kt new file mode 100644 index 0000000..db224a6 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ch/parano/myicek/ui/components/SkeletonCard.kt @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// MyIce Kotlin Multiplatform — schedule/convocation viewer +// Copyright (C) 2026 parano.ch +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package ch.parano.myicek.ui.components + +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import ch.parano.myicek.ui.theme.Dimens + +@Composable +fun SkeletonCard(modifier: Modifier = Modifier) { + val transition = rememberInfiniteTransition(label = "shimmer") + val alpha by transition.animateFloat( + initialValue = 0.3f, + targetValue = 0.6f, + animationSpec = infiniteRepeatable( + animation = tween(800), + repeatMode = RepeatMode.Reverse, + ), + label = "shimmerAlpha", + ) + + val skeletonColor = MaterialTheme.colorScheme.surfaceVariant + val skeletonModifier = Modifier + .alpha(alpha) + .background(skeletonColor, RoundedCornerShape(4.dp)) + + Box( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = Dimens.lg, vertical = Dimens.sm) + .clip(MaterialTheme.shapes.medium) + .background(MaterialTheme.colorScheme.surface) + .padding(Dimens.lg), + ) { + Column(verticalArrangement = Arrangement.spacedBy(Dimens.sm)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Box(modifier = Modifier + .height(20.dp) + .width(160.dp) + .then(skeletonModifier)) + Box(modifier = Modifier + .size(28.dp) + .clip(CircleShape) + .then(skeletonModifier)) + } + Box(modifier = Modifier + .height(16.dp) + .width(200.dp) + .then(skeletonModifier)) + Box(modifier = Modifier + .height(16.dp) + .width(180.dp) + .then(skeletonModifier)) + } + } +}