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,
|
focusManager = focusManager,
|
||||||
onUsernameChange = viewModel::updateUsername,
|
onUsernameChange = viewModel::updateUsername,
|
||||||
onPasswordChange = viewModel::updatePassword,
|
onPasswordChange = viewModel::updatePassword,
|
||||||
onLogin = { focusManager.clearFocus(); viewModel.login() }
|
onLogin = { focusManager.clearFocus(); viewModel.login() },
|
||||||
|
onKeycloakLogin = viewModel::startOidcFlow
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
RegisterTabContent()
|
RegisterTabContent()
|
||||||
|
|
@ -91,7 +92,8 @@ private fun LoginTabContent(
|
||||||
focusManager: androidx.compose.ui.focus.FocusManager,
|
focusManager: androidx.compose.ui.focus.FocusManager,
|
||||||
onUsernameChange: (String) -> Unit,
|
onUsernameChange: (String) -> Unit,
|
||||||
onPasswordChange: (String) -> Unit,
|
onPasswordChange: (String) -> Unit,
|
||||||
onLogin: () -> Unit
|
onLogin: () -> Unit,
|
||||||
|
onKeycloakLogin: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
var passwordVisible by remember { mutableStateOf(false) }
|
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.composeCompiler)
|
||||||
alias(libs.plugins.composeMultiplatform)
|
alias(libs.plugins.composeMultiplatform)
|
||||||
alias(libs.plugins.kotlinSerialization)
|
alias(libs.plugins.kotlinSerialization)
|
||||||
|
alias(libs.plugins.composeHotReload)
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
|
@ -93,6 +94,7 @@ kotlin {
|
||||||
jvmMain.dependencies {
|
jvmMain.dependencies {
|
||||||
implementation(compose.desktop.currentOs)
|
implementation(compose.desktop.currentOs)
|
||||||
implementation(libs.kotlinx.coroutines.swing)
|
implementation(libs.kotlinx.coroutines.swing)
|
||||||
|
implementation(compose.uiTooling)
|
||||||
implementation(libs.koin.core)
|
implementation(libs.koin.core)
|
||||||
implementation(project(":frontend:features:nennung-feature"))
|
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.data.AuthTokenManager
|
||||||
import at.mocode.frontend.core.auth.presentation.LoginScreen
|
import at.mocode.frontend.core.auth.presentation.LoginScreen
|
||||||
import at.mocode.frontend.core.auth.presentation.LoginViewModel
|
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.designsystem.theme.AppTheme
|
||||||
import at.mocode.frontend.core.domain.PlatformType
|
import at.mocode.frontend.core.domain.PlatformType
|
||||||
import at.mocode.frontend.core.domain.currentPlatform
|
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
|
// Data class for dummy tournament
|
||||||
private data class TournamentData(
|
private data class TournamentData(
|
||||||
val id: String,
|
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
|
@Composable
|
||||||
private fun DashboardScreen(
|
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