refactoring:
Ein neues Kotlin-Multiplattform-Mitgliedermodul wurde bereitgestellt, das clientseitige API-Aufrufe, Benutzeroberfläche und Navigation in die Host-Anwendung integriert. Der Build wurde durch die Entkopplung von Backend-Mitgliedermodulen stabilisiert, und alle Tests wurden erfolgreich abgeschlossen. Die Client-Funktionen erfolgen über REST-Aufrufe an das Gateway; die Backend-Integration wird in einer späteren Phase implementiert.
This commit is contained in:
@@ -77,6 +77,7 @@ kotlin {
|
||||
implementation(project(":clients:shared:common-ui"))
|
||||
implementation(project(":clients:shared:navigation"))
|
||||
implementation(project(":clients:ping-feature"))
|
||||
implementation(project(":clients:members-feature"))
|
||||
|
||||
// Compose Multiplatform
|
||||
implementation(compose.runtime)
|
||||
|
||||
@@ -3,6 +3,9 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.clients.membersfeature.ProfileScreen
|
||||
import at.mocode.clients.membersfeature.ProfileViewModel
|
||||
import at.mocode.clients.shared.navigation.AppScreen
|
||||
|
||||
@Composable
|
||||
fun MainApp() {
|
||||
@@ -11,13 +14,20 @@ fun MainApp() {
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
DevelopmentScreen()
|
||||
var currentScreen by remember { mutableStateOf<AppScreen>(AppScreen.Home) }
|
||||
|
||||
when (currentScreen) {
|
||||
is AppScreen.Home -> DevelopmentScreen(onOpenProfile = { currentScreen = AppScreen.Profile })
|
||||
is AppScreen.Login -> DevelopmentScreen(onOpenProfile = { currentScreen = AppScreen.Profile })
|
||||
is AppScreen.Ping -> DevelopmentScreen(onOpenProfile = { currentScreen = AppScreen.Profile })
|
||||
is AppScreen.Profile -> ProfileScreen(viewModel = remember { ProfileViewModel() })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DevelopmentScreen() {
|
||||
fun DevelopmentScreen(onOpenProfile: () -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -47,6 +57,9 @@ fun DevelopmentScreen() {
|
||||
Button(onClick = { testStatus = "Testing Ping Service..." }) {
|
||||
Text("Test Ping Service")
|
||||
}
|
||||
Button(onClick = onOpenProfile) {
|
||||
Text("Open Profile")
|
||||
}
|
||||
}
|
||||
|
||||
Text("Status: $testStatus")
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.composeMultiplatform)
|
||||
alias(libs.plugins.composeCompiler)
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
}
|
||||
|
||||
group = "at.mocode.clients"
|
||||
version = "1.0.0"
|
||||
|
||||
kotlin {
|
||||
val enableWasm = providers.gradleProperty("enableWasm").orNull == "true"
|
||||
|
||||
jvmToolchain(21)
|
||||
|
||||
jvm()
|
||||
|
||||
js {
|
||||
browser {
|
||||
testTask { enabled = false }
|
||||
}
|
||||
}
|
||||
|
||||
if (enableWasm) {
|
||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||
wasmJs { browser() }
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
// UI Kit
|
||||
implementation(project(":clients:shared:common-ui"))
|
||||
// Shared config/utilities
|
||||
implementation(project(":clients:shared"))
|
||||
// Authentication helpers (token + authenticated client)
|
||||
implementation(project(":clients:auth-feature"))
|
||||
|
||||
// Compose
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.material3)
|
||||
implementation(compose.ui)
|
||||
implementation(compose.components.resources)
|
||||
implementation(compose.materialIconsExtended)
|
||||
|
||||
// ViewModel lifecycle + compose helpers
|
||||
implementation(libs.bundles.compose.common)
|
||||
|
||||
// HTTP + Kotlinx
|
||||
implementation(libs.bundles.ktor.client.common)
|
||||
implementation(libs.bundles.kotlinx.core)
|
||||
}
|
||||
|
||||
commonTest.dependencies {
|
||||
implementation(libs.kotlin.test)
|
||||
implementation(libs.kotlinx.coroutines.test)
|
||||
implementation(libs.ktor.client.mock)
|
||||
}
|
||||
|
||||
jvmMain.dependencies {
|
||||
implementation(libs.ktor.client.cio)
|
||||
}
|
||||
|
||||
jsMain.dependencies {
|
||||
implementation(libs.ktor.client.js)
|
||||
}
|
||||
|
||||
if (enableWasm) {
|
||||
val wasmJsMain = getByName("wasmJsMain")
|
||||
wasmJsMain.dependencies {
|
||||
implementation(libs.ktor.client.js)
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.material3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_21)
|
||||
freeCompilerArgs.addAll("-opt-in=kotlin.RequiresOptIn")
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package at.mocode.clients.membersfeature
|
||||
|
||||
import at.mocode.clients.authfeature.AuthenticatedHttpClient
|
||||
import at.mocode.clients.shared.AppConfig
|
||||
import at.mocode.clients.membersfeature.model.MemberProfile
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
import at.mocode.clients.authfeature.AuthenticatedHttpClient.addAuthHeader
|
||||
|
||||
class MembersApiClient(
|
||||
private val baseUrl: String = AppConfig.GATEWAY_URL
|
||||
) {
|
||||
private val client = AuthenticatedHttpClient.create()
|
||||
|
||||
suspend fun getMyProfile(): MemberProfile {
|
||||
// Erwarteter Endpoint: GET /api/members/me
|
||||
return client.get("$baseUrl/api/members/me") {
|
||||
addAuthHeader()
|
||||
}.body()
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package at.mocode.clients.membersfeature
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun ProfileScreen(viewModel: ProfileViewModel) {
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.loadProfile()
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(text = "👤 Mein Profil", style = MaterialTheme.typography.headlineSmall)
|
||||
|
||||
if (state.isLoading) {
|
||||
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
state.errorMessage?.let { error ->
|
||||
Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer)) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text("Fehler", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(error)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Button(onClick = { viewModel.clearError() }) { Text("Schließen") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.profile?.let { profile ->
|
||||
Card {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(text = profile.fullName.ifBlank { "Unbekannt" }, style = MaterialTheme.typography.titleLarge)
|
||||
profile.username?.let { Text("Benutzername: $it") }
|
||||
profile.email?.let { Text("E-Mail: $it") }
|
||||
if (profile.roles.isNotEmpty()) {
|
||||
Text("Rollen: ${profile.roles.joinToString(", ")}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(onClick = { viewModel.loadProfile() }, enabled = !state.isLoading) {
|
||||
Text("Neu laden")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package at.mocode.clients.membersfeature
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import at.mocode.clients.membersfeature.model.MemberProfile
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class ProfileUiState(
|
||||
val isLoading: Boolean = false,
|
||||
val profile: MemberProfile? = null,
|
||||
val errorMessage: String? = null
|
||||
)
|
||||
|
||||
class ProfileViewModel(
|
||||
private val api: MembersApiClient = MembersApiClient()
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(ProfileUiState())
|
||||
val uiState: StateFlow<ProfileUiState> = _uiState.asStateFlow()
|
||||
|
||||
fun loadProfile() {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val profile = api.getMyProfile()
|
||||
_uiState.value = ProfileUiState(isLoading = false, profile = profile)
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = ProfileUiState(
|
||||
isLoading = false,
|
||||
errorMessage = e.message ?: "Profil konnte nicht geladen werden"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearError() {
|
||||
_uiState.value = _uiState.value.copy(errorMessage = null)
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package at.mocode.clients.membersfeature.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class MemberProfile(
|
||||
val id: String? = null,
|
||||
val username: String? = null,
|
||||
val email: String? = null,
|
||||
val firstName: String? = null,
|
||||
val lastName: String? = null,
|
||||
val roles: List<String> = emptyList()
|
||||
) {
|
||||
val fullName: String
|
||||
get() = listOfNotNull(firstName, lastName).joinToString(" ").ifBlank { username ?: "" }
|
||||
}
|
||||
+1
@@ -4,4 +4,5 @@ sealed class AppScreen {
|
||||
data object Home : AppScreen()
|
||||
data object Login : AppScreen()
|
||||
data object Ping : AppScreen()
|
||||
data object Profile : AppScreen()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user