diff --git a/frontend/core/auth/src/commonMain/kotlin/at/mocode/frontend/core/auth/presentation/LoginScreen.kt b/frontend/core/auth/src/commonMain/kotlin/at/mocode/frontend/core/auth/presentation/LoginScreen.kt index e3f6b2cb..f8c6e0c3 100644 --- a/frontend/core/auth/src/commonMain/kotlin/at/mocode/frontend/core/auth/presentation/LoginScreen.kt +++ b/frontend/core/auth/src/commonMain/kotlin/at/mocode/frontend/core/auth/presentation/LoginScreen.kt @@ -70,7 +70,8 @@ fun LoginScreen( focusManager = focusManager, onUsernameChange = viewModel::updateUsername, onPasswordChange = viewModel::updatePassword, - onLogin = { focusManager.clearFocus(); viewModel.login() } + onLogin = { focusManager.clearFocus(); viewModel.login() }, + onKeycloakLogin = viewModel::startOidcFlow ) } else { RegisterTabContent() @@ -91,7 +92,8 @@ private fun LoginTabContent( focusManager: androidx.compose.ui.focus.FocusManager, onUsernameChange: (String) -> Unit, onPasswordChange: (String) -> Unit, - onLogin: () -> Unit + onLogin: () -> Unit, + onKeycloakLogin: () -> Unit = {}, ) { var passwordVisible by remember { mutableStateOf(false) } @@ -169,6 +171,34 @@ private fun LoginTabContent( } } + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + HorizontalDivider(modifier = Modifier.weight(1f)) + Text( + text = " oder ", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + HorizontalDivider(modifier = Modifier.weight(1f)) + } + + OutlinedButton( + onClick = onKeycloakLogin, + enabled = !uiState.isLoading && !uiState.isOidcLoading, + modifier = Modifier.fillMaxWidth().height(48.dp) + ) { + if (uiState.isOidcLoading) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + strokeWidth = 2.dp + ) + } else { + Text("Mit Keycloak anmelden") + } + } + } } diff --git a/frontend/shells/meldestelle-portal/build.gradle.kts b/frontend/shells/meldestelle-portal/build.gradle.kts index cdac040b..9764fafd 100644 --- a/frontend/shells/meldestelle-portal/build.gradle.kts +++ b/frontend/shells/meldestelle-portal/build.gradle.kts @@ -14,6 +14,7 @@ plugins { alias(libs.plugins.composeCompiler) alias(libs.plugins.composeMultiplatform) alias(libs.plugins.kotlinSerialization) + alias(libs.plugins.composeHotReload) } kotlin { @@ -93,6 +94,7 @@ kotlin { jvmMain.dependencies { implementation(compose.desktop.currentOs) implementation(libs.kotlinx.coroutines.swing) + implementation(compose.uiTooling) implementation(libs.koin.core) implementation(project(":frontend:features:nennung-feature")) } diff --git a/frontend/shells/meldestelle-portal/src/commonMain/kotlin/MainApp.kt b/frontend/shells/meldestelle-portal/src/commonMain/kotlin/MainApp.kt index 756c0ecc..0710c141 100644 --- a/frontend/shells/meldestelle-portal/src/commonMain/kotlin/MainApp.kt +++ b/frontend/shells/meldestelle-portal/src/commonMain/kotlin/MainApp.kt @@ -14,7 +14,6 @@ import androidx.compose.ui.unit.dp import at.mocode.frontend.core.auth.data.AuthTokenManager import at.mocode.frontend.core.auth.presentation.LoginScreen import at.mocode.frontend.core.auth.presentation.LoginViewModel -import at.mocode.frontend.core.designsystem.components.AppFooter import at.mocode.frontend.core.designsystem.theme.AppTheme import at.mocode.frontend.core.domain.PlatformType import at.mocode.frontend.core.domain.currentPlatform @@ -168,134 +167,6 @@ fun MainApp() { } } -@Composable -private fun LandingScreen( - onPrimaryCta: () -> Unit, - onOpenPing: () -> Unit -) { - val scrollState = rememberScrollState() - - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(scrollState) - ) { - // Top Bar area (simple for landing) - Surface( - color = MaterialTheme.colorScheme.surface, - shadowElevation = 2.dp, - modifier = Modifier.fillMaxWidth() - ) { - Row( - modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = "mo-code.at", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary - ) - Button(onClick = onPrimaryCta) { - Text("Login Meldestelle") - } - } - } - - // Hero - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 60.dp), - verticalArrangement = Arrangement.spacedBy(24.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = "Die moderne Meldestelle", - style = MaterialTheme.typography.displayMedium, - fontWeight = FontWeight.Bold - ) - Text( - text = "Von Praktikern für Praktiker. Schneller Nennen, fehlerfrei Richten und stressfrei Auswerten – konform nach ÖTO & FEI.", - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - // Manifest / Intro - Surface(color = MaterialTheme.colorScheme.surfaceVariant) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 60.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Text("Unser Anspruch: Ein durchdachtes System.", style = MaterialTheme.typography.headlineMedium) - Text( - "Die Meldestelle ist das Herzstück jedes Turniers. Wenn sie stolpert, stockt der Sport. Wir verstehen den Balanceakt zwischen Veranstaltern, Reitern und den Verbänden.", - style = MaterialTheme.typography.bodyLarge - ) - Text( - "Deshalb entwickeln wir diese Plattform nicht am Reißbrett, sondern direkt am Turnierplatz – aus der Sicht der Meldestelle, der Richter, der Zeitnehmer und aller Funktionäre.", - style = MaterialTheme.typography.bodyLarge - ) - Text( - "Mit Fokus auf die Praxis: Tastaturbedienung für höchste Geschwindigkeit, Offline-Fähigkeit für das 'Plumpsklo' am Rand des Abreiteplatzes und eine integrierte Kassenführung.", - style = MaterialTheme.typography.bodyLarge - ) - } - } - - // Features - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 60.dp), - verticalArrangement = Arrangement.spacedBy(32.dp) - ) { - Text("Die Kern-Säulen", style = MaterialTheme.typography.headlineMedium) - Column(verticalArrangement = Arrangement.spacedBy(24.dp)) { - FeatureCard( - number = "01", - title = "Regelwerks-Intelligenz (ÖTO)", - body = "Wir nehmen Ihnen die Validierungs-Komplexität ab. Von der Lizenzprüfung der Reiter bis zur Kontrolle der Richterqualifikationen beim Anlegen der Bewerbe." - ) - FeatureCard( - number = "02", - title = "Offline-First & Resilient", - body = "Stabil auf dem Laptop. Dank Offline-Unterstützung und lokaler Datenbank arbeiten Sie nahtlos weiter, selbst wenn die Internetverbindung am Platz wieder einmal abreißt." - ) - FeatureCard( - number = "03", - title = "Speed-Workflow", - body = "Die Nennungsmaske und die Ergebniserfassung sind kompromisslos auf Geschwindigkeit und Tastaturbedienung (Enter & Tab) optimiert. Weil am Turniertag jede Sekunde zählt." - ) - FeatureCard( - number = "04", - title = "Smarte Kassenführung", - body = "Kontobasierte Abrechnung für Reiter und Besitzer. Nenngelder, Startgelder und Nachnenngebühren sauber getrennt – selbst ein Nennungstausch wird als einfacher Transfer verbucht." - ) - } - } - - // Ping Service Link - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 24.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - OutlinedButton(onClick = onOpenPing) { - Text("System Status (Ping-Service)") - } - } - - // Footer - AppFooter() - } -} - // Data class for dummy tournament private data class TournamentData( val id: String, @@ -384,31 +255,6 @@ private fun TournamentCard(data: TournamentData) { } } -@Composable -private fun FeatureCard(number: String, title: String, body: String) { - Card( - modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth().padding(16.dp), - verticalAlignment = Alignment.Top - ) { - Text( - text = number, - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.primary, - fontWeight = FontWeight.Black, - modifier = Modifier.width(64.dp) - ) - Column(modifier = Modifier.weight(1f)) { - Text(title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) - Spacer(Modifier.height(8.dp)) - Text(body, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant) - } - } - } -} @Composable private fun DashboardScreen( diff --git a/frontend/shells/meldestelle-portal/src/commonMain/kotlin/screens/LandingScreen.kt b/frontend/shells/meldestelle-portal/src/commonMain/kotlin/screens/LandingScreen.kt new file mode 100644 index 00000000..4353938a --- /dev/null +++ b/frontend/shells/meldestelle-portal/src/commonMain/kotlin/screens/LandingScreen.kt @@ -0,0 +1,163 @@ +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import at.mocode.frontend.core.designsystem.components.AppFooter + +@Composable +fun LandingScreen( + onPrimaryCta: () -> Unit, + onOpenPing: () -> Unit +) { + val scrollState = rememberScrollState() + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + ) { + // Top Bar + Surface( + color = MaterialTheme.colorScheme.surface, + shadowElevation = 2.dp, + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "mo-code.at", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary + ) + Button(onClick = onPrimaryCta) { + Text("Login Meldestelle") + } + } + } + + // Hero + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 60.dp), + verticalArrangement = Arrangement.spacedBy(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Die moderne Meldestelle", + style = MaterialTheme.typography.displayMedium, + fontWeight = FontWeight.Bold + ) + Text( + text = "Von Praktikern für Praktiker. Schneller Nennen, fehlerfrei Richten und stressfrei Auswerten – konform nach ÖTO & FEI.", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + // Manifest / Intro + Surface(color = MaterialTheme.colorScheme.surfaceVariant) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 60.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text("Unser Anspruch: Ein durchdachtes System.", style = MaterialTheme.typography.headlineMedium) + Text( + "Die Meldestelle ist das Herzstück jedes Turniers. Wenn sie stolpert, stockt der Sport. Wir verstehen den Balanceakt zwischen Veranstaltern, Reitern und den Verbänden.", + style = MaterialTheme.typography.bodyLarge + ) + Text( + "Deshalb entwickeln wir diese Plattform nicht am Reißbrett, sondern direkt am Turnierplatz – aus der Sicht der Meldestelle, der Richter, der Zeitnehmer und aller Funktionäre.", + style = MaterialTheme.typography.bodyLarge + ) + Text( + "Mit Fokus auf die Praxis: Tastaturbedienung für höchste Geschwindigkeit, Offline-Fähigkeit für das 'Plumpsklo' am Rand des Abreiteplatzes und eine integrierte Kassenführung.", + style = MaterialTheme.typography.bodyLarge + ) + } + } + + // Kern-Säulen + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 60.dp), + verticalArrangement = Arrangement.spacedBy(32.dp) + ) { + Text("Die Kern-Säulen", style = MaterialTheme.typography.headlineMedium) + Column(verticalArrangement = Arrangement.spacedBy(24.dp)) { + LandingFeatureCard( + number = "01", + title = "Regelwerks-Intelligenz (ÖTO)", + body = "Wir nehmen Ihnen die Validierungs-Komplexität ab. Von der Lizenzprüfung der Reiter bis zur Kontrolle der Richterqualifikationen beim Anlegen der Bewerbe." + ) + LandingFeatureCard( + number = "02", + title = "Offline-First & Resilient", + body = "Stabil auf dem Laptop. Dank Offline-Unterstützung und lokaler Datenbank arbeiten Sie nahtlos weiter, selbst wenn die Internetverbindung am Platz wieder einmal abreißt." + ) + LandingFeatureCard( + number = "03", + title = "Speed-Workflow", + body = "Die Nennungsmaske und die Ergebniserfassung sind kompromisslos auf Geschwindigkeit und Tastaturbedienung (Enter & Tab) optimiert. Weil am Turniertag jede Sekunde zählt." + ) + LandingFeatureCard( + number = "04", + title = "Smarte Kassenführung", + body = "Kontobasierte Abrechnung für Reiter und Besitzer. Nenngelder, Startgelder und Nachnenngebühren sauber getrennt – selbst ein Nennungstausch wird als einfacher Transfer verbucht." + ) + } + } + + // System Status Link + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + OutlinedButton(onClick = onOpenPing) { + Text("System Status (Ping-Service)") + } + } + + AppFooter() + } +} + +@Composable +private fun LandingFeatureCard(number: String, title: String, body: String) { + Card( + modifier = Modifier.fillMaxWidth(), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth().padding(16.dp), + verticalAlignment = Alignment.Top + ) { + Text( + text = number, + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Black, + modifier = Modifier.width(64.dp) + ) + Column(modifier = Modifier.weight(1f)) { + Text(title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) + Spacer(Modifier.height(8.dp)) + Text(body, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + } + } +}