This commit is contained in:
Trey t
2025-11-05 20:52:51 -06:00
parent bc289d6c88
commit a8083380aa
12 changed files with 720 additions and 78 deletions

View File

@@ -0,0 +1,256 @@
package com.mycrib.android.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.mycrib.android.ui.components.common.ErrorCard
import com.mycrib.android.viewmodel.AuthViewModel
import com.mycrib.shared.network.ApiResult
import com.mycrib.storage.TokenStorage
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ProfileScreen(
onNavigateBack: () -> Unit,
onLogout: () -> Unit,
viewModel: AuthViewModel = viewModel { AuthViewModel() }
) {
var firstName by remember { mutableStateOf("") }
var lastName by remember { mutableStateOf("") }
var email by remember { mutableStateOf("") }
var errorMessage by remember { mutableStateOf("") }
var isLoading by remember { mutableStateOf(false) }
var successMessage by remember { mutableStateOf("") }
var isLoadingUser by remember { mutableStateOf(true) }
val updateState by viewModel.updateProfileState.collectAsState()
// Load current user data
LaunchedEffect(Unit) {
val token = TokenStorage.getToken()
if (token != null) {
val authApi = com.mycrib.shared.network.AuthApi()
when (val result = authApi.getCurrentUser(token)) {
is ApiResult.Success -> {
firstName = result.data.firstName ?: ""
lastName = result.data.lastName ?: ""
email = result.data.email
isLoadingUser = false
}
else -> {
errorMessage = "Failed to load user data"
isLoadingUser = false
}
}
} else {
errorMessage = "Not authenticated"
isLoadingUser = false
}
}
LaunchedEffect(updateState) {
when (updateState) {
is ApiResult.Success -> {
successMessage = "Profile updated successfully"
isLoading = false
errorMessage = ""
viewModel.resetUpdateProfileState()
}
is ApiResult.Error -> {
errorMessage = (updateState as ApiResult.Error).message
isLoading = false
successMessage = ""
}
is ApiResult.Loading -> {
isLoading = true
errorMessage = ""
successMessage = ""
}
else -> {}
}
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Profile", fontWeight = FontWeight.SemiBold) },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
},
actions = {
IconButton(onClick = onLogout) {
Icon(Icons.Default.Logout, contentDescription = "Logout")
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface
)
)
}
) { paddingValues ->
if (isLoadingUser) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
} else {
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState())
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp)
) {
Spacer(modifier = Modifier.height(8.dp))
// Profile Icon
Icon(
Icons.Default.AccountCircle,
contentDescription = null,
modifier = Modifier.size(80.dp),
tint = MaterialTheme.colorScheme.primary
)
Text(
"Update Your Profile",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = firstName,
onValueChange = { firstName = it },
label = { Text("First Name") },
leadingIcon = {
Icon(Icons.Default.Person, contentDescription = null)
},
modifier = Modifier.fillMaxWidth(),
singleLine = true,
shape = RoundedCornerShape(12.dp)
)
OutlinedTextField(
value = lastName,
onValueChange = { lastName = it },
label = { Text("Last Name") },
leadingIcon = {
Icon(Icons.Default.Person, contentDescription = null)
},
modifier = Modifier.fillMaxWidth(),
singleLine = true,
shape = RoundedCornerShape(12.dp)
)
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("Email") },
leadingIcon = {
Icon(Icons.Default.Email, contentDescription = null)
},
modifier = Modifier.fillMaxWidth(),
singleLine = true,
shape = RoundedCornerShape(12.dp)
)
if (errorMessage.isNotEmpty()) {
ErrorCard(message = errorMessage)
}
if (successMessage.isNotEmpty()) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
),
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Default.CheckCircle,
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimaryContainer
)
Text(
successMessage,
color = MaterialTheme.colorScheme.onPrimaryContainer,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.SemiBold
)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = {
if (email.isNotEmpty()) {
viewModel.updateProfile(
firstName = firstName.ifBlank { null },
lastName = lastName.ifBlank { null },
email = email
)
} else {
errorMessage = "Email is required"
}
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
enabled = email.isNotEmpty() && !isLoading,
shape = RoundedCornerShape(12.dp)
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 2.dp
)
} else {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Default.Save, contentDescription = null)
Text(
"Save Changes",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
}
}
}
}

View File

@@ -25,6 +25,7 @@ fun ResidencesScreen(
onResidenceClick: (Int) -> Unit,
onAddResidence: () -> Unit,
onLogout: () -> Unit,
onNavigateToProfile: () -> Unit = {},
viewModel: ResidenceViewModel = viewModel { ResidenceViewModel() }
) {
val myResidencesState by viewModel.myResidencesState.collectAsState()
@@ -43,6 +44,9 @@ fun ResidencesScreen(
)
},
actions = {
IconButton(onClick = onNavigateToProfile) {
Icon(Icons.Default.AccountCircle, contentDescription = "Profile")
}
IconButton(onClick = onLogout) {
Icon(Icons.Default.ExitToApp, contentDescription = "Logout")
}