Add registration tab to LoginScreen and implement OrganizerProfile screen
This commit is contained in:
@@ -326,7 +326,11 @@
|
|||||||
"USER",
|
"USER",
|
||||||
"ORGANIZER"
|
"ORGANIZER"
|
||||||
],
|
],
|
||||||
"clientRoles": {}
|
"clientRoles": {
|
||||||
|
"api-gateway": [
|
||||||
|
"ORGANIZER"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"groups": [],
|
"groups": [],
|
||||||
|
|||||||
+4
-12
@@ -46,19 +46,10 @@ class AuthApiClient(
|
|||||||
formParameters = Parameters.build {
|
formParameters = Parameters.build {
|
||||||
append("grant_type", "password")
|
append("grant_type", "password")
|
||||||
append("client_id", clientId)
|
append("client_id", clientId)
|
||||||
|
append("scope", "openid profile email")
|
||||||
// WICHTIG: Senden Sie client_secret nur, wenn es sich NICHT um einen öffentlichen Client (wie 'web-app') handelt.
|
if (!clientSecret.isNullOrBlank()) {
|
||||||
// Keycloak lehnt Anfragen von öffentlichen Clients ab, die client_secret enthalten.
|
|
||||||
// Wir prüfen, ob die Client-ID auf einen öffentlichen Client hindeutet oder ob ein Secret explizit angegeben wurde.
|
|
||||||
// Aktuell gehen wir davon aus, dass 'web-app' öffentlich ist und daher kein Secret gesendet werden sollte.
|
|
||||||
|
|
||||||
// Logik: Wenn clientId 'web-app' ist, ignorieren wir das Geheimnis oder verlassen uns darauf, dass der Aufrufer null übergibt.
|
|
||||||
// Da AppConstants möglicherweise noch das Geheimnis für 'postman-client' enthält, ist Vorsicht geboten.
|
|
||||||
|
|
||||||
if (!clientSecret.isNullOrBlank() && clientId != "web-app") {
|
|
||||||
append("client_secret", clientSecret)
|
append("client_secret", clientSecret)
|
||||||
}
|
}
|
||||||
|
|
||||||
append("username", username)
|
append("username", username)
|
||||||
append("password", password)
|
append("password", password)
|
||||||
}
|
}
|
||||||
@@ -102,7 +93,8 @@ class AuthApiClient(
|
|||||||
formParameters = Parameters.build {
|
formParameters = Parameters.build {
|
||||||
append("grant_type", "refresh_token")
|
append("grant_type", "refresh_token")
|
||||||
append("client_id", clientId)
|
append("client_id", clientId)
|
||||||
if (!clientSecret.isNullOrBlank() && clientId != "web-app") {
|
append("scope", "openid profile email")
|
||||||
|
if (!clientSecret.isNullOrBlank()) {
|
||||||
append("client_secret", clientSecret)
|
append("client_secret", clientSecret)
|
||||||
}
|
}
|
||||||
append("refresh_token", refreshToken)
|
append("refresh_token", refreshToken)
|
||||||
|
|||||||
+4
-2
@@ -45,7 +45,8 @@ enum class Permission {
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class JwtPayload(
|
data class JwtPayload(
|
||||||
val sub: String? = null, // User ID
|
val sub: String? = null, // User ID
|
||||||
val username: String? = null, // Username
|
@kotlinx.serialization.SerialName("preferred_username")
|
||||||
|
val username: String? = null, // Username (Keycloak: preferred_username)
|
||||||
val exp: Long? = null, // Expiration timestamp
|
val exp: Long? = null, // Expiration timestamp
|
||||||
val iat: Long? = null, // Issued at timestamp
|
val iat: Long? = null, // Issued at timestamp
|
||||||
val iss: String? = null, // Issuer
|
val iss: String? = null, // Issuer
|
||||||
@@ -272,7 +273,8 @@ class AuthTokenManager {
|
|||||||
if (parts.size != 3) return null
|
if (parts.size != 3) return null
|
||||||
|
|
||||||
// Decode the payload (second part)
|
// Decode the payload (second part)
|
||||||
val payloadJson = Base64.decode(parts[1]).decodeToString()
|
// JWT uses URL-safe Base64 without padding (RFC 4648 §5)
|
||||||
|
val payloadJson = Base64.UrlSafe.decode(parts[1]).decodeToString()
|
||||||
|
|
||||||
// First, try to parse with a standard approach
|
// First, try to parse with a standard approach
|
||||||
val basicPayload = try {
|
val basicPayload = try {
|
||||||
|
|||||||
+240
-141
@@ -1,8 +1,9 @@
|
|||||||
package at.mocode.frontend.core.auth.presentation
|
package at.mocode.frontend.core.auth.presentation
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Visibility
|
import androidx.compose.material.icons.filled.Visibility
|
||||||
@@ -25,16 +26,18 @@ import androidx.compose.ui.unit.dp
|
|||||||
fun LoginScreen(
|
fun LoginScreen(
|
||||||
viewModel: LoginViewModel,
|
viewModel: LoginViewModel,
|
||||||
onLoginSuccess: () -> Unit = {},
|
onLoginSuccess: () -> Unit = {},
|
||||||
onBack: () -> Unit = {} // New callback for back navigation
|
onBack: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
var passwordVisible by remember { mutableStateOf(false) }
|
|
||||||
|
// 0 = Anmelden, 1 = Registrieren
|
||||||
|
var selectedTab by remember { mutableStateOf(0) }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text("Anmelden") },
|
title = { Text(if (selectedTab == 0) "Anmelden" else "Registrieren") },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onBack) {
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
|
||||||
@@ -47,154 +50,250 @@ fun LoginScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.padding(24.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center
|
|
||||||
) {
|
) {
|
||||||
// Username field
|
PrimaryTabRow(selectedTabIndex = selectedTab) {
|
||||||
OutlinedTextField(
|
Tab(
|
||||||
value = uiState.username,
|
selected = selectedTab == 0,
|
||||||
onValueChange = viewModel::updateUsername,
|
onClick = { selectedTab = 0 },
|
||||||
label = { Text("Benutzername") },
|
text = { Text("Anmelden") }
|
||||||
enabled = !uiState.isLoading && !uiState.isOidcLoading,
|
)
|
||||||
isError = uiState.usernameError != null,
|
Tab(
|
||||||
supportingText = uiState.usernameError?.let { { Text(it) } },
|
selected = selectedTab == 1,
|
||||||
keyboardOptions = KeyboardOptions(
|
onClick = { selectedTab = 1 },
|
||||||
keyboardType = KeyboardType.Text,
|
text = { Text("Registrieren") }
|
||||||
imeAction = ImeAction.Next
|
|
||||||
),
|
|
||||||
keyboardActions = KeyboardActions(
|
|
||||||
onNext = { focusManager.moveFocus(FocusDirection.Next) }
|
|
||||||
),
|
|
||||||
singleLine = true,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(bottom = 16.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Password field
|
|
||||||
OutlinedTextField(
|
|
||||||
value = uiState.password,
|
|
||||||
onValueChange = viewModel::updatePassword,
|
|
||||||
label = { Text("Passwort") },
|
|
||||||
enabled = !uiState.isLoading && !uiState.isOidcLoading,
|
|
||||||
isError = uiState.passwordError != null,
|
|
||||||
supportingText = uiState.passwordError?.let { { Text(it) } },
|
|
||||||
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
|
||||||
trailingIcon = {
|
|
||||||
val image = if (passwordVisible)
|
|
||||||
Icons.Filled.Visibility
|
|
||||||
else
|
|
||||||
Icons.Filled.VisibilityOff
|
|
||||||
|
|
||||||
val description = if (passwordVisible) "Passwort verbergen" else "Passwort anzeigen"
|
|
||||||
|
|
||||||
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
|
||||||
Icon(imageVector = image, description)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
keyboardOptions = KeyboardOptions(
|
|
||||||
keyboardType = KeyboardType.Password,
|
|
||||||
imeAction = ImeAction.Done
|
|
||||||
),
|
|
||||||
keyboardActions = KeyboardActions(
|
|
||||||
onDone = {
|
|
||||||
focusManager.clearFocus()
|
|
||||||
if (uiState.canLogin) {
|
|
||||||
viewModel.login()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
singleLine = true,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(bottom = 24.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Error message
|
|
||||||
if (uiState.errorMessage != null) {
|
|
||||||
Card(
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.errorContainer
|
|
||||||
),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(bottom = 16.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = uiState.errorMessage!!,
|
|
||||||
color = MaterialTheme.colorScheme.onErrorContainer,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.padding(16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login button
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
focusManager.clearFocus()
|
|
||||||
viewModel.login()
|
|
||||||
},
|
|
||||||
enabled = uiState.canLogin && !uiState.isLoading,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(48.dp)
|
|
||||||
) {
|
|
||||||
if (uiState.isLoading) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
strokeWidth = 2.dp,
|
|
||||||
color = MaterialTheme.colorScheme.onPrimary
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Text("Anmelden")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Divider
|
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// OIDC / Keycloak button
|
if (selectedTab == 0) {
|
||||||
OutlinedButton(
|
LoginTabContent(
|
||||||
onClick = { viewModel.startOidcFlow() },
|
uiState = uiState,
|
||||||
enabled = !uiState.isLoading && !uiState.isOidcLoading,
|
focusManager = focusManager,
|
||||||
modifier = Modifier
|
onUsernameChange = viewModel::updateUsername,
|
||||||
.fillMaxWidth()
|
onPasswordChange = viewModel::updatePassword,
|
||||||
.height(48.dp)
|
onLogin = { focusManager.clearFocus(); viewModel.login() }
|
||||||
) {
|
)
|
||||||
if (uiState.isOidcLoading) {
|
} else {
|
||||||
CircularProgressIndicator(
|
RegisterTabContent()
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
strokeWidth = 2.dp
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text("Weiterleitung ...")
|
|
||||||
} else {
|
|
||||||
Text("Mit Keycloak anmelden")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle login success
|
|
||||||
LaunchedEffect(uiState.isAuthenticated) {
|
LaunchedEffect(uiState.isAuthenticated) {
|
||||||
if (uiState.isAuthenticated) {
|
if (uiState.isAuthenticated) {
|
||||||
onLoginSuccess()
|
onLoginSuccess()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LoginTabContent(
|
||||||
|
uiState: LoginUiState,
|
||||||
|
focusManager: androidx.compose.ui.focus.FocusManager,
|
||||||
|
onUsernameChange: (String) -> Unit,
|
||||||
|
onPasswordChange: (String) -> Unit,
|
||||||
|
onLogin: () -> Unit
|
||||||
|
) {
|
||||||
|
var passwordVisible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = uiState.username,
|
||||||
|
onValueChange = onUsernameChange,
|
||||||
|
label = { Text("Benutzername") },
|
||||||
|
enabled = !uiState.isLoading && !uiState.isOidcLoading,
|
||||||
|
isError = uiState.usernameError != null,
|
||||||
|
supportingText = uiState.usernameError?.let { { Text(it) } },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Next),
|
||||||
|
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) }),
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = uiState.password,
|
||||||
|
onValueChange = onPasswordChange,
|
||||||
|
label = { Text("Passwort") },
|
||||||
|
enabled = !uiState.isLoading && !uiState.isOidcLoading,
|
||||||
|
isError = uiState.passwordError != null,
|
||||||
|
supportingText = uiState.passwordError?.let { { Text(it) } },
|
||||||
|
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (passwordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff,
|
||||||
|
contentDescription = if (passwordVisible) "Passwort verbergen" else "Passwort anzeigen"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done),
|
||||||
|
keyboardActions = KeyboardActions(onDone = { if (uiState.canLogin) onLogin() }),
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(bottom = 24.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (uiState.errorMessage != null) {
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer),
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = uiState.errorMessage,
|
||||||
|
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = onLogin,
|
||||||
|
enabled = uiState.canLogin && !uiState.isLoading,
|
||||||
|
modifier = Modifier.fillMaxWidth().height(48.dp)
|
||||||
|
) {
|
||||||
|
if (uiState.isLoading) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimary
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text("Anmelden")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RegisterTabContent() {
|
||||||
|
var displayName by remember { mutableStateOf("") }
|
||||||
|
var email by remember { mutableStateOf("") }
|
||||||
|
var password by remember { mutableStateOf("") }
|
||||||
|
var passwordConfirm by remember { mutableStateOf("") }
|
||||||
|
var passwordVisible by remember { mutableStateOf(false) }
|
||||||
|
var passwordConfirmVisible by remember { mutableStateOf(false) }
|
||||||
|
var registerAttempted by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
|
val displayNameError = if (registerAttempted && displayName.isBlank()) "Name ist erforderlich" else null
|
||||||
|
val emailError = when {
|
||||||
|
registerAttempted && email.isBlank() -> "E-Mail ist erforderlich"
|
||||||
|
registerAttempted && !email.contains("@") -> "Ungültige E-Mail-Adresse"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
val passwordError = if (registerAttempted && password.length < 8) "Mindestens 8 Zeichen erforderlich" else null
|
||||||
|
val passwordConfirmError = if (registerAttempted && password != passwordConfirm) "Passwörter stimmen nicht überein" else null
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Top
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Neues Konto erstellen",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(bottom = 20.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = displayName,
|
||||||
|
onValueChange = { displayName = it },
|
||||||
|
label = { Text("Vollständiger Name") },
|
||||||
|
isError = displayNameError != null,
|
||||||
|
supportingText = displayNameError?.let { { Text(it) } },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Next),
|
||||||
|
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) }),
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = email,
|
||||||
|
onValueChange = { email = it },
|
||||||
|
label = { Text("E-Mail-Adresse") },
|
||||||
|
isError = emailError != null,
|
||||||
|
supportingText = emailError?.let { { Text(it) } },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email, imeAction = ImeAction.Next),
|
||||||
|
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) }),
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = password,
|
||||||
|
onValueChange = { password = it },
|
||||||
|
label = { Text("Passwort") },
|
||||||
|
isError = passwordError != null,
|
||||||
|
supportingText = passwordError?.let { { Text(it) } },
|
||||||
|
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (passwordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff,
|
||||||
|
contentDescription = if (passwordVisible) "Passwort verbergen" else "Passwort anzeigen"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Next),
|
||||||
|
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) }),
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = passwordConfirm,
|
||||||
|
onValueChange = { passwordConfirm = it },
|
||||||
|
label = { Text("Passwort bestätigen") },
|
||||||
|
isError = passwordConfirmError != null,
|
||||||
|
supportingText = passwordConfirmError?.let { { Text(it) } },
|
||||||
|
visualTransformation = if (passwordConfirmVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = { passwordConfirmVisible = !passwordConfirmVisible }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (passwordConfirmVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff,
|
||||||
|
contentDescription = if (passwordConfirmVisible) "Passwort verbergen" else "Passwort anzeigen"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done),
|
||||||
|
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus(); registerAttempted = true }),
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(bottom = 24.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
registerAttempted = true
|
||||||
|
// TODO: Backend-Anbindung für Registrierung (POST /api/auth/register)
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth().height(48.dp)
|
||||||
|
) {
|
||||||
|
Text("Konto erstellen")
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Mit der Registrierung stimmst du den Nutzungsbedingungen zu.",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+2
@@ -11,6 +11,7 @@ sealed class AppScreen(val route: String) {
|
|||||||
|
|
||||||
data object Ping : AppScreen("/ping")
|
data object Ping : AppScreen("/ping")
|
||||||
data object Profile : AppScreen("/profile")
|
data object Profile : AppScreen("/profile")
|
||||||
|
data object OrganizerProfile : AppScreen("/organizer/profile")
|
||||||
data object AuthCallback : AppScreen("/auth/callback")
|
data object AuthCallback : AppScreen("/auth/callback")
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -23,6 +24,7 @@ sealed class AppScreen(val route: String) {
|
|||||||
Routes.LOGIN, Routes.Auth.LOGIN -> Login()
|
Routes.LOGIN, Routes.Auth.LOGIN -> Login()
|
||||||
"/ping" -> Ping
|
"/ping" -> Ping
|
||||||
"/profile" -> Profile
|
"/profile" -> Profile
|
||||||
|
"/organizer/profile" -> OrganizerProfile
|
||||||
"/auth/callback" -> AuthCallback
|
"/auth/callback" -> AuthCallback
|
||||||
else -> Landing // Default fallback
|
else -> Landing // Default fallback
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
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
|
||||||
@@ -65,7 +67,7 @@ fun MainApp() {
|
|||||||
} else {
|
} else {
|
||||||
LandingScreen(
|
LandingScreen(
|
||||||
onPrimaryCta = { navigationPort.navigateToScreen(AppScreen.Login(returnTo = AppScreen.Dashboard)) }, // Takes you to Meldestelle login
|
onPrimaryCta = { navigationPort.navigateToScreen(AppScreen.Login(returnTo = AppScreen.Dashboard)) }, // Takes you to Meldestelle login
|
||||||
onOpenPing = { navigationPort.navigateToScreen(AppScreen.Profile) } // Open the Ping Overview / Status page
|
onOpenPing = { navigationPort.navigateToScreen(AppScreen.Login(returnTo = AppScreen.OrganizerProfile)) } // Nach Login zum OrganizerProfile
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,9 +126,25 @@ fun MainApp() {
|
|||||||
|
|
||||||
is AppScreen.Ping -> PingScreen(
|
is AppScreen.Ping -> PingScreen(
|
||||||
viewModel = pingViewModel,
|
viewModel = pingViewModel,
|
||||||
onBack = { navigationPort.navigateToScreen(AppScreen.Profile) } // Always go back to overview
|
onBack = { navigationPort.navigateToScreen(AppScreen.Profile) } // Zurück zum Profil/Übersicht
|
||||||
)
|
)
|
||||||
|
|
||||||
|
is AppScreen.OrganizerProfile -> OrganizerProfileScreen(
|
||||||
|
authTokenManager = authTokenManager,
|
||||||
|
onLogout = {
|
||||||
|
authTokenManager.clearToken()
|
||||||
|
navigationPort.navigateToScreen(AppScreen.Landing)
|
||||||
|
},
|
||||||
|
onNavigateToDashboard = { navigationPort.navigateToScreen(AppScreen.Dashboard) }
|
||||||
|
)
|
||||||
|
|
||||||
|
is AppScreen.AuthCallback -> {
|
||||||
|
// OIDC-Callback: Nach erfolgreichem OAuth-Redirect zum Dashboard navigieren
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
navigationPort.navigateToScreen(AppScreen.Dashboard)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
is AppScreen.Profile -> AuthStatusScreen(
|
is AppScreen.Profile -> AuthStatusScreen(
|
||||||
authTokenManager = authTokenManager,
|
authTokenManager = authTokenManager,
|
||||||
onNavigateToLogin = {
|
onNavigateToLogin = {
|
||||||
@@ -144,7 +162,7 @@ fun MainApp() {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> {}
|
else -> { navigationPort.navigateToScreen(AppScreen.Landing) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,7 +212,7 @@ private fun LandingScreen(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Die moderne Turniermeldestelle",
|
text = "Die moderne Meldestelle",
|
||||||
style = MaterialTheme.typography.displayMedium,
|
style = MaterialTheme.typography.displayMedium,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
@@ -1188,6 +1206,327 @@ fun TournamentStepBewerbe() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun OrganizerProfileScreen(
|
||||||
|
authTokenManager: AuthTokenManager,
|
||||||
|
onLogout: () -> Unit,
|
||||||
|
onNavigateToDashboard: () -> Unit
|
||||||
|
) {
|
||||||
|
val authState by authTokenManager.authState.collectAsState()
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
// Formular-Felder
|
||||||
|
var vereinsname by remember { mutableStateOf("URFV Neumarkt") }
|
||||||
|
var vereinskuerzel by remember { mutableStateOf("URFV") }
|
||||||
|
var adresse by remember { mutableStateOf("") }
|
||||||
|
var plz by remember { mutableStateOf("") }
|
||||||
|
var ort by remember { mutableStateOf("") }
|
||||||
|
var land by remember { mutableStateOf("Österreich") }
|
||||||
|
var mapsLink by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
// Ansprechpersonen
|
||||||
|
var kontakt1Name by remember { mutableStateOf("") }
|
||||||
|
var kontakt1Email by remember { mutableStateOf("") }
|
||||||
|
var kontakt1Telefon by remember { mutableStateOf("") }
|
||||||
|
var kontakt2Name by remember { mutableStateOf("") }
|
||||||
|
var kontakt2Email by remember { mutableStateOf("") }
|
||||||
|
var kontakt2Telefon by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
// Social / Links
|
||||||
|
var webseite by remember { mutableStateOf("") }
|
||||||
|
var facebook by remember { mutableStateOf("") }
|
||||||
|
var instagram by remember { mutableStateOf("") }
|
||||||
|
var youtube by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
// Weitere Infos
|
||||||
|
var vereinsbeschreibung by remember { mutableStateOf("") }
|
||||||
|
var bankverbindung by remember { mutableStateOf("") }
|
||||||
|
var uid by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
var saveSuccess by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("Veranstalter Profil") },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onNavigateToDashboard) {
|
||||||
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
Text(
|
||||||
|
text = authState.username ?: "",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = androidx.compose.ui.Modifier.padding(end = 8.dp)
|
||||||
|
)
|
||||||
|
TextButton(onClick = onLogout) { Text("Abmelden") }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = androidx.compose.ui.Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
.padding(24.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(24.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
// --- Logo & Vereinsname ---
|
||||||
|
Card(modifier = androidx.compose.ui.Modifier.fillMaxWidth()) {
|
||||||
|
Column(modifier = androidx.compose.ui.Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
Text("Verein / Veranstalter", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||||||
|
|
||||||
|
// Logo Placeholder
|
||||||
|
Surface(
|
||||||
|
modifier = androidx.compose.ui.Modifier.fillMaxWidth(),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = androidx.compose.ui.Modifier.padding(24.dp).fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text("🏆", style = MaterialTheme.typography.displayMedium)
|
||||||
|
Text("Vereins-/Veranstaltungslogo", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
OutlinedButton(onClick = { /* TODO: File Picker */ }) {
|
||||||
|
Text("Logo hochladen")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = vereinsname,
|
||||||
|
onValueChange = { vereinsname = it },
|
||||||
|
label = { Text("Vereinsname / Veranstalter") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = androidx.compose.ui.Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = vereinskuerzel,
|
||||||
|
onValueChange = { vereinskuerzel = it },
|
||||||
|
label = { Text("Kürzel") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = androidx.compose.ui.Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = vereinsbeschreibung,
|
||||||
|
onValueChange = { vereinsbeschreibung = it },
|
||||||
|
label = { Text("Kurzbeschreibung / Über uns") },
|
||||||
|
minLines = 3,
|
||||||
|
maxLines = 6,
|
||||||
|
modifier = androidx.compose.ui.Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Adresse ---
|
||||||
|
Card(modifier = androidx.compose.ui.Modifier.fillMaxWidth()) {
|
||||||
|
Column(modifier = androidx.compose.ui.Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
Text("Adresse", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = adresse,
|
||||||
|
onValueChange = { adresse = it },
|
||||||
|
label = { Text("Straße & Hausnummer") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = androidx.compose.ui.Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = plz,
|
||||||
|
onValueChange = { plz = it },
|
||||||
|
label = { Text("PLZ") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = androidx.compose.ui.Modifier.width(100.dp)
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = ort,
|
||||||
|
onValueChange = { ort = it },
|
||||||
|
label = { Text("Ort") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = androidx.compose.ui.Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
OutlinedTextField(
|
||||||
|
value = land,
|
||||||
|
onValueChange = { land = it },
|
||||||
|
label = { Text("Land") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = androidx.compose.ui.Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = mapsLink,
|
||||||
|
onValueChange = { mapsLink = it },
|
||||||
|
label = { Text("Google Maps / OpenStreetMap Link") },
|
||||||
|
placeholder = { Text("https://maps.google.com/...") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = androidx.compose.ui.Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Ansprechpersonen ---
|
||||||
|
Card(modifier = androidx.compose.ui.Modifier.fillMaxWidth()) {
|
||||||
|
Column(modifier = androidx.compose.ui.Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
Text("Ansprechpersonen", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||||||
|
|
||||||
|
Text("Hauptkontakt", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = kontakt1Name,
|
||||||
|
onValueChange = { kontakt1Name = it },
|
||||||
|
label = { Text("Name") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = androidx.compose.ui.Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = kontakt1Email,
|
||||||
|
onValueChange = { kontakt1Email = it },
|
||||||
|
label = { Text("E-Mail") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = androidx.compose.ui.Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = kontakt1Telefon,
|
||||||
|
onValueChange = { kontakt1Telefon = it },
|
||||||
|
label = { Text("Telefon / Mobil") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = androidx.compose.ui.Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider()
|
||||||
|
|
||||||
|
Text("Weiterer Kontakt (optional)", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.secondary)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = kontakt2Name,
|
||||||
|
onValueChange = { kontakt2Name = it },
|
||||||
|
label = { Text("Name") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = androidx.compose.ui.Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = kontakt2Email,
|
||||||
|
onValueChange = { kontakt2Email = it },
|
||||||
|
label = { Text("E-Mail") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = androidx.compose.ui.Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = kontakt2Telefon,
|
||||||
|
onValueChange = { kontakt2Telefon = it },
|
||||||
|
label = { Text("Telefon / Mobil") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = androidx.compose.ui.Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Social Media & Links ---
|
||||||
|
Card(modifier = androidx.compose.ui.Modifier.fillMaxWidth()) {
|
||||||
|
Column(modifier = androidx.compose.ui.Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
Text("Links & Social Media", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = webseite,
|
||||||
|
onValueChange = { webseite = it },
|
||||||
|
label = { Text("Webseite") },
|
||||||
|
placeholder = { Text("https://www.meinverein.at") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = androidx.compose.ui.Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = facebook,
|
||||||
|
onValueChange = { facebook = it },
|
||||||
|
label = { Text("Facebook") },
|
||||||
|
placeholder = { Text("https://facebook.com/...") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = androidx.compose.ui.Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = instagram,
|
||||||
|
onValueChange = { instagram = it },
|
||||||
|
label = { Text("Instagram") },
|
||||||
|
placeholder = { Text("https://instagram.com/...") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = androidx.compose.ui.Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = youtube,
|
||||||
|
onValueChange = { youtube = it },
|
||||||
|
label = { Text("YouTube") },
|
||||||
|
placeholder = { Text("https://youtube.com/...") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = androidx.compose.ui.Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Weitere Vereinsdaten ---
|
||||||
|
Card(modifier = androidx.compose.ui.Modifier.fillMaxWidth()) {
|
||||||
|
Column(modifier = androidx.compose.ui.Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
Text("Weitere Informationen", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = bankverbindung,
|
||||||
|
onValueChange = { bankverbindung = it },
|
||||||
|
label = { Text("IBAN / Bankverbindung") },
|
||||||
|
placeholder = { Text("AT12 3456 7890 1234 5678") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = androidx.compose.ui.Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = uid,
|
||||||
|
onValueChange = { uid = it },
|
||||||
|
label = { Text("UID-Nummer / ZVR-Zahl") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = androidx.compose.ui.Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Speichern ---
|
||||||
|
if (saveSuccess) {
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer),
|
||||||
|
modifier = androidx.compose.ui.Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"✓ Profil erfolgreich gespeichert!",
|
||||||
|
modifier = androidx.compose.ui.Modifier.padding(16.dp),
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
// TODO: Backend-Anbindung (PUT /api/organizer/profile)
|
||||||
|
saveSuccess = true
|
||||||
|
},
|
||||||
|
modifier = androidx.compose.ui.Modifier.fillMaxWidth().height(52.dp)
|
||||||
|
) {
|
||||||
|
Text("Profil speichern", style = MaterialTheme.typography.titleMedium)
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = onNavigateToDashboard,
|
||||||
|
modifier = androidx.compose.ui.Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text("Zum Dashboard")
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = androidx.compose.ui.Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AuthStatusScreen(
|
private fun AuthStatusScreen(
|
||||||
authTokenManager: AuthTokenManager,
|
authTokenManager: AuthTokenManager,
|
||||||
|
|||||||
Reference in New Issue
Block a user