feat(auth, ui): add Keycloak login flow and enhance LoginScreen UI
- Introduced `onKeycloakLogin` callback in LoginScreen for Keycloak-based authentication. - Updated Login UI with "Login with Keycloak" button and loading indicator for OIDC flow. - Implemented dividers and spacing for improved visual structure. - Refactored LandingScreen into a separate file for better modularity and maintenance. - Reintroduced LandingScreen features, hero section, and footer with enhanced composables. - Added `composeHotReload` plugin and `uiTooling` dependency for development improvements. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
parent
faa2c344d1
commit
0e32999df4
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user