diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/AllTasksScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/AllTasksScreen.kt index ab92131..9565523 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/AllTasksScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/AllTasksScreen.kt @@ -23,6 +23,7 @@ import com.example.casera.viewmodel.TaskCompletionViewModel import com.example.casera.viewmodel.TaskViewModel import com.example.casera.models.TaskDetail import com.example.casera.network.ApiResult +import com.example.casera.ui.theme.* @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -105,164 +106,156 @@ fun AllTasksScreen( } } - Scaffold( - topBar = { - TopAppBar( - title = { - Text( - "All Tasks", - fontWeight = FontWeight.Bold - ) - }, - actions = { - IconButton( - onClick = { viewModel.loadTasks(forceRefresh = true) } - ) { - Icon( - Icons.Default.Refresh, - contentDescription = "Refresh" + WarmGradientBackground { + Scaffold( + containerColor = androidx.compose.ui.graphics.Color.Transparent, + topBar = { + TopAppBar( + title = { + Text( + "All Tasks", + fontWeight = FontWeight.Bold ) - } - IconButton( - onClick = { showNewTaskDialog = true }, - enabled = myResidencesState is ApiResult.Success && - (myResidencesState as ApiResult.Success).data.residences.isNotEmpty() - ) { - Icon( - Icons.Default.Add, - contentDescription = "Add Task" - ) - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface - ) - ) - } - ) { paddingValues -> - ApiResultHandler( - state = tasksState, - onRetry = { viewModel.loadTasks(forceRefresh = true) }, - modifier = Modifier.padding(paddingValues), - errorTitle = "Failed to Load Tasks" - ) { taskData -> - val hasNoTasks = taskData.columns.all { it.tasks.isEmpty() } - - if (hasNoTasks) { - Box( - modifier = Modifier - .fillMaxSize(), - contentAlignment = androidx.compose.ui.Alignment.Center - ) { - Column( - horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier.padding(24.dp) + }, + actions = { + IconButton( + onClick = { viewModel.loadTasks(forceRefresh = true) } ) { Icon( - Icons.Default.Assignment, - contentDescription = null, - modifier = Modifier.size(80.dp), - tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f) + Icons.Default.Refresh, + contentDescription = "Refresh" ) - Text( - "No tasks yet", - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.SemiBold + } + IconButton( + onClick = { showNewTaskDialog = true }, + enabled = myResidencesState is ApiResult.Success && + (myResidencesState as ApiResult.Success).data.residences.isNotEmpty() + ) { + Icon( + Icons.Default.Add, + contentDescription = "Add Task" ) - Text( - "Create your first task to get started", - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.height(8.dp)) - Button( - onClick = { showNewTaskDialog = true }, - modifier = Modifier - .fillMaxWidth(0.7f) - .height(56.dp), - enabled = myResidencesState is ApiResult.Success && - (myResidencesState as ApiResult.Success).data.residences.isNotEmpty() + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = androidx.compose.ui.graphics.Color.Transparent + ) + ) + } + ) { paddingValues -> + ApiResultHandler( + state = tasksState, + onRetry = { viewModel.loadTasks(forceRefresh = true) }, + modifier = Modifier.padding(paddingValues), + errorTitle = "Failed to Load Tasks" + ) { taskData -> + val hasNoTasks = taskData.columns.all { it.tasks.isEmpty() } + + if (hasNoTasks) { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = androidx.compose.ui.Alignment.Center + ) { + Column( + horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy), + modifier = Modifier.padding(OrganicSpacing.comfortable) ) { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = androidx.compose.ui.Alignment.CenterVertically - ) { - Icon(Icons.Default.Add, contentDescription = null) + OrganicIconContainer( + icon = Icons.Default.Assignment, + size = 80.dp, + iconScale = 0.6f, + backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f), + iconColor = MaterialTheme.colorScheme.primary + ) + Text( + "No tasks yet", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.textPrimary + ) + Text( + "Create your first task to get started", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.textSecondary + ) + Spacer(modifier = Modifier.height(OrganicSpacing.compact)) + OrganicPrimaryButton( + text = "Add Task", + onClick = { showNewTaskDialog = true }, + modifier = Modifier.fillMaxWidth(0.7f), + enabled = myResidencesState is ApiResult.Success && + (myResidencesState as ApiResult.Success).data.residences.isNotEmpty() + ) + if (myResidencesState is ApiResult.Success && + (myResidencesState as ApiResult.Success).data.residences.isEmpty()) { Text( - "Add Task", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold + "Add a property first from the Residences tab", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error ) } } - if (myResidencesState is ApiResult.Success && - (myResidencesState as ApiResult.Success).data.residences.isEmpty()) { - Text( - "Add a property first from the Residences tab", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.error - ) - } } - } - } else { - DynamicTaskKanbanView( - columns = taskData.columns, - onCompleteTask = { task -> - if (onNavigateToCompleteTask != null) { - // Use full-screen navigation - val residenceName = (myResidencesState as? ApiResult.Success) - ?.data?.residences?.find { it.id == task.residenceId }?.name ?: "" - onNavigateToCompleteTask(task, residenceName) - } else { - // Fall back to dialog - selectedTask = task - showCompleteDialog = true - } - }, - onEditTask = { task -> - onNavigateToEditTask(task) - }, - onCancelTask = { task -> + } else { + DynamicTaskKanbanView( + columns = taskData.columns, + onCompleteTask = { task -> + if (onNavigateToCompleteTask != null) { + // Use full-screen navigation + val residenceName = (myResidencesState as? ApiResult.Success) + ?.data?.residences?.find { it.id == task.residenceId }?.name ?: "" + onNavigateToCompleteTask(task, residenceName) + } else { + // Fall back to dialog + selectedTask = task + showCompleteDialog = true + } + }, + onEditTask = { task -> + onNavigateToEditTask(task) + }, + onCancelTask = { task -> // viewModel.cancelTask(task.id) { _ -> // viewModel.loadTasks() // } - }, - onUncancelTask = { task -> + }, + onUncancelTask = { task -> // viewModel.uncancelTask(task.id) { _ -> // viewModel.loadTasks() // } - }, - onMarkInProgress = { task -> - viewModel.markInProgress(task.id) { success -> - if (success) { - viewModel.loadTasks() + }, + onMarkInProgress = { task -> + viewModel.markInProgress(task.id) { success -> + if (success) { + viewModel.loadTasks() + } } - } - }, - onArchiveTask = { task -> - viewModel.archiveTask(task.id) { success -> - if (success) { - viewModel.loadTasks() + }, + onArchiveTask = { task -> + viewModel.archiveTask(task.id) { success -> + if (success) { + viewModel.loadTasks() + } } - } - }, - onUnarchiveTask = { task -> - viewModel.unarchiveTask(task.id) { success -> - if (success) { - viewModel.loadTasks() + }, + onUnarchiveTask = { task -> + viewModel.unarchiveTask(task.id) { success -> + if (success) { + viewModel.loadTasks() + } } + }, + modifier = Modifier, + bottomPadding = bottomNavBarPadding, + scrollToColumnIndex = scrollToColumnIndex, + onScrollComplete = { + scrollToColumnIndex = null + onClearNavigateToTask() } - }, - modifier = Modifier, - bottomPadding = bottomNavBarPadding, - scrollToColumnIndex = scrollToColumnIndex, - onScrollComplete = { - scrollToColumnIndex = null - onClearNavigateToTask() - } - ) + ) + } } } } diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/CompleteTaskScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/CompleteTaskScreen.kt index 6748f39..0014f33 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/CompleteTaskScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/CompleteTaskScreen.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -30,8 +29,7 @@ import com.example.casera.models.TaskCompletionCreateRequest import com.example.casera.models.ContractorSummary import com.example.casera.network.ApiResult import com.example.casera.platform.* -import com.example.casera.ui.theme.AppRadius -import com.example.casera.ui.theme.AppSpacing +import com.example.casera.ui.theme.* import com.example.casera.viewmodel.ContractorViewModel import org.jetbrains.compose.resources.stringResource @@ -74,378 +72,354 @@ fun CompleteTaskScreen( } } - Scaffold( - topBar = { - TopAppBar( - title = { - Text( - stringResource(Res.string.completions_complete_task_title, taskTitle), - fontWeight = FontWeight.SemiBold, - maxLines = 1 - ) - }, - navigationIcon = { - IconButton(onClick = onNavigateBack) { - Icon(Icons.Default.Close, contentDescription = stringResource(Res.string.common_cancel)) - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface - ) - ) - } - ) { paddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - .verticalScroll(rememberScrollState()) - ) { - // Task Info Section - Card( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = AppSpacing.lg, vertical = AppSpacing.md), - shape = RoundedCornerShape(AppRadius.md), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) { - Column( - modifier = Modifier.padding(AppSpacing.lg) - ) { - Text( - text = taskTitle, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold - ) - - Spacer(modifier = Modifier.height(AppSpacing.sm)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - if (residenceName.isNotEmpty()) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(AppSpacing.xs) - ) { - Icon( - Icons.Default.Home, - contentDescription = null, - modifier = Modifier.size(16.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = residenceName, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - } - } - - // Contractor Section - SectionHeader( - title = stringResource(Res.string.completions_select_contractor), - subtitle = stringResource(Res.string.completions_contractor_helper) - ) - - Card( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = AppSpacing.lg) - .clickable { showContractorPicker = true }, - shape = RoundedCornerShape(AppRadius.md), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(AppSpacing.lg), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(AppSpacing.md) - ) { - Icon( - Icons.Default.Build, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary + WarmGradientBackground { + Scaffold( + containerColor = androidx.compose.ui.graphics.Color.Transparent, + topBar = { + TopAppBar( + title = { + Text( + stringResource(Res.string.completions_complete_task_title, taskTitle), + fontWeight = FontWeight.SemiBold, + maxLines = 1 ) - Column { - Text( - text = selectedContractor?.name - ?: stringResource(Res.string.completions_none_manual), - style = MaterialTheme.typography.bodyLarge - ) - selectedContractor?.company?.let { company -> - Text( - text = company, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } + }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon(Icons.Default.Close, contentDescription = stringResource(Res.string.common_cancel)) } - } - Icon( - Icons.Default.ChevronRight, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = androidx.compose.ui.graphics.Color.Transparent ) - } + ) } - - Spacer(modifier = Modifier.height(AppSpacing.lg)) - - // Completion Details Section - SectionHeader( - title = stringResource(Res.string.completions_details_section), - subtitle = stringResource(Res.string.completions_optional_info) - ) - + ) { paddingValues -> Column( - modifier = Modifier.padding(horizontal = AppSpacing.lg), - verticalArrangement = Arrangement.spacedBy(AppSpacing.md) - ) { - // Completed By Name - OutlinedTextField( - value = completedByName, - onValueChange = { completedByName = it }, - label = { Text(stringResource(Res.string.completions_completed_by_name)) }, - placeholder = { Text(stringResource(Res.string.completions_completed_by_placeholder)) }, - leadingIcon = { Icon(Icons.Default.Person, null) }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - enabled = selectedContractor == null, - shape = RoundedCornerShape(AppRadius.md) - ) - - // Actual Cost - OutlinedTextField( - value = actualCost, - onValueChange = { actualCost = it }, - label = { Text(stringResource(Res.string.completions_actual_cost_optional)) }, - leadingIcon = { Icon(Icons.Default.AttachMoney, null) }, - prefix = { Text("$") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), - shape = RoundedCornerShape(AppRadius.md) - ) - } - - Spacer(modifier = Modifier.height(AppSpacing.lg)) - - // Notes Section - SectionHeader( - title = stringResource(Res.string.completions_notes_optional), - subtitle = stringResource(Res.string.completions_notes_helper) - ) - - OutlinedTextField( - value = notes, - onValueChange = { notes = it }, - placeholder = { Text(stringResource(Res.string.completions_notes_placeholder)) }, modifier = Modifier - .fillMaxWidth() - .padding(horizontal = AppSpacing.lg) - .height(120.dp), - shape = RoundedCornerShape(AppRadius.md) - ) - - Spacer(modifier = Modifier.height(AppSpacing.lg)) - - // Rating Section - SectionHeader( - title = stringResource(Res.string.completions_quality_rating), - subtitle = stringResource(Res.string.completions_rate_quality) - ) - - Card( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = AppSpacing.lg), - shape = RoundedCornerShape(AppRadius.md), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) ) { - Column( + // Task Info Section + OrganicCard( modifier = Modifier .fillMaxWidth() - .padding(AppSpacing.lg), - horizontalAlignment = Alignment.CenterHorizontally + .padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.md) ) { - Text( - text = "$rating / 5", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Spacer(modifier = Modifier.height(AppSpacing.md)) - - Row( - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically + Column( + modifier = Modifier.padding(OrganicSpacing.lg) ) { - (1..5).forEach { star -> - val isSelected = star <= rating - val starColor by animateColorAsState( - targetValue = if (isSelected) Color(0xFFFFD700) - else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f), - animationSpec = tween(durationMillis = 150), - label = "starColor" - ) + Text( + text = taskTitle, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) - IconButton( - onClick = { - hapticFeedback.perform(HapticFeedbackType.Selection) - rating = star - }, - modifier = Modifier.size(56.dp) - ) { - Icon( - imageVector = if (isSelected) Icons.Default.Star else Icons.Default.StarOutline, - contentDescription = "$star stars", - tint = starColor, - modifier = Modifier.size(40.dp) - ) + Spacer(modifier = Modifier.height(OrganicSpacing.sm)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + if (residenceName.isNotEmpty()) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.xs) + ) { + Icon( + Icons.Default.Home, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = residenceName, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } } } } } - } - Spacer(modifier = Modifier.height(AppSpacing.lg)) + // Contractor Section + SectionHeader( + title = stringResource(Res.string.completions_select_contractor), + subtitle = stringResource(Res.string.completions_contractor_helper) + ) - // Photos Section - SectionHeader( - title = stringResource(Res.string.completions_photos_count, selectedImages.size, MAX_IMAGES), - subtitle = stringResource(Res.string.completions_add_photos_helper) - ) - - Column( - modifier = Modifier.padding(horizontal = AppSpacing.lg), - verticalArrangement = Arrangement.spacedBy(AppSpacing.md) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(AppSpacing.md) + OrganicCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = OrganicSpacing.lg) + .clickable { showContractorPicker = true } ) { - OutlinedButton( - onClick = { - hapticFeedback.perform(HapticFeedbackType.Light) - cameraPicker() - }, - modifier = Modifier.weight(1f), - enabled = selectedImages.size < MAX_IMAGES - ) { - Icon(Icons.Default.CameraAlt, null, modifier = Modifier.size(20.dp)) - Spacer(modifier = Modifier.width(AppSpacing.sm)) - Text(stringResource(Res.string.completions_camera)) - } - - OutlinedButton( - onClick = { - hapticFeedback.perform(HapticFeedbackType.Light) - imagePicker() - }, - modifier = Modifier.weight(1f), - enabled = selectedImages.size < MAX_IMAGES - ) { - Icon(Icons.Default.PhotoLibrary, null, modifier = Modifier.size(20.dp)) - Spacer(modifier = Modifier.width(AppSpacing.sm)) - Text(stringResource(Res.string.completions_library)) - } - } - - if (selectedImages.isNotEmpty()) { Row( modifier = Modifier .fillMaxWidth() - .horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.spacedBy(AppSpacing.md) + .padding(OrganicSpacing.lg), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { - selectedImages.forEachIndexed { index, imageData -> - ImageThumbnailCard( - imageData = imageData, - onRemove = { - hapticFeedback.perform(HapticFeedbackType.Light) - selectedImages = selectedImages.toMutableList().also { - it.removeAt(index) - } - } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md) + ) { + OrganicIconContainer( + icon = Icons.Default.Build, + size = 24.dp ) + Column { + Text( + text = selectedContractor?.name + ?: stringResource(Res.string.completions_none_manual), + style = MaterialTheme.typography.bodyLarge + ) + selectedContractor?.company?.let { company -> + Text( + text = company, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + Icon( + Icons.Default.ChevronRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + Spacer(modifier = Modifier.height(OrganicSpacing.lg)) + + // Completion Details Section + SectionHeader( + title = stringResource(Res.string.completions_details_section), + subtitle = stringResource(Res.string.completions_optional_info) + ) + + Column( + modifier = Modifier.padding(horizontal = OrganicSpacing.lg), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md) + ) { + // Completed By Name + OutlinedTextField( + value = completedByName, + onValueChange = { completedByName = it }, + label = { Text(stringResource(Res.string.completions_completed_by_name)) }, + placeholder = { Text(stringResource(Res.string.completions_completed_by_placeholder)) }, + leadingIcon = { Icon(Icons.Default.Person, null) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + enabled = selectedContractor == null, + shape = OrganicShapes.medium + ) + + // Actual Cost + OutlinedTextField( + value = actualCost, + onValueChange = { actualCost = it }, + label = { Text(stringResource(Res.string.completions_actual_cost_optional)) }, + leadingIcon = { Icon(Icons.Default.AttachMoney, null) }, + prefix = { Text("$") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + shape = OrganicShapes.medium + ) + } + + Spacer(modifier = Modifier.height(OrganicSpacing.lg)) + + // Notes Section + SectionHeader( + title = stringResource(Res.string.completions_notes_optional), + subtitle = stringResource(Res.string.completions_notes_helper) + ) + + OutlinedTextField( + value = notes, + onValueChange = { notes = it }, + placeholder = { Text(stringResource(Res.string.completions_notes_placeholder)) }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = OrganicSpacing.lg) + .height(120.dp), + shape = OrganicShapes.medium + ) + + Spacer(modifier = Modifier.height(OrganicSpacing.lg)) + + // Rating Section + SectionHeader( + title = stringResource(Res.string.completions_quality_rating), + subtitle = stringResource(Res.string.completions_rate_quality) + ) + + OrganicCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = OrganicSpacing.lg) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(OrganicSpacing.lg), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "$rating / 5", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.height(OrganicSpacing.md)) + + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + (1..5).forEach { star -> + val isSelected = star <= rating + val starColor by animateColorAsState( + targetValue = if (isSelected) Color(0xFFFFD700) + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f), + animationSpec = tween(durationMillis = 150), + label = "starColor" + ) + + IconButton( + onClick = { + hapticFeedback.perform(HapticFeedbackType.Selection) + rating = star + }, + modifier = Modifier.size(56.dp) + ) { + Icon( + imageVector = if (isSelected) Icons.Default.Star else Icons.Default.StarOutline, + contentDescription = "$star stars", + tint = starColor, + modifier = Modifier.size(40.dp) + ) + } + } } } } - } - Spacer(modifier = Modifier.height(AppSpacing.xl)) + Spacer(modifier = Modifier.height(OrganicSpacing.lg)) - // Complete Button - Button( - onClick = { - isSubmitting = true - val notesWithContractor = buildString { - selectedContractor?.let { - append("Contractor: ${it.name}") - it.company?.let { company -> append(" ($company)") } - append("\n") - } - if (completedByName.isNotBlank()) { - append("Completed by: $completedByName\n") - } - if (notes.isNotBlank()) { - append(notes) - } - }.ifBlank { null } + // Photos Section + SectionHeader( + title = stringResource(Res.string.completions_photos_count, selectedImages.size, MAX_IMAGES), + subtitle = stringResource(Res.string.completions_add_photos_helper) + ) - onComplete( - TaskCompletionCreateRequest( - taskId = taskId, - completedAt = null, - actualCost = actualCost.ifBlank { null }?.toDoubleOrNull(), - notes = notesWithContractor, - rating = rating, - imageUrls = null - ), - selectedImages - ) - }, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = AppSpacing.lg) - .height(56.dp), - enabled = !isSubmitting, - shape = RoundedCornerShape(AppRadius.md) - ) { - if (isSubmitting) { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - color = MaterialTheme.colorScheme.onPrimary, - strokeWidth = 2.dp - ) - } else { - Icon(Icons.Default.CheckCircle, null) - Spacer(modifier = Modifier.width(AppSpacing.sm)) - Text( - stringResource(Res.string.completions_complete_button), - fontWeight = FontWeight.SemiBold - ) + Column( + modifier = Modifier.padding(horizontal = OrganicSpacing.lg), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md) + ) { + OutlinedButton( + onClick = { + hapticFeedback.perform(HapticFeedbackType.Light) + cameraPicker() + }, + modifier = Modifier.weight(1f), + enabled = selectedImages.size < MAX_IMAGES + ) { + Icon(Icons.Default.CameraAlt, null, modifier = Modifier.size(20.dp)) + Spacer(modifier = Modifier.width(OrganicSpacing.sm)) + Text(stringResource(Res.string.completions_camera)) + } + + OutlinedButton( + onClick = { + hapticFeedback.perform(HapticFeedbackType.Light) + imagePicker() + }, + modifier = Modifier.weight(1f), + enabled = selectedImages.size < MAX_IMAGES + ) { + Icon(Icons.Default.PhotoLibrary, null, modifier = Modifier.size(20.dp)) + Spacer(modifier = Modifier.width(OrganicSpacing.sm)) + Text(stringResource(Res.string.completions_library)) + } + } + + if (selectedImages.isNotEmpty()) { + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md) + ) { + selectedImages.forEachIndexed { index, imageData -> + ImageThumbnailCard( + imageData = imageData, + onRemove = { + hapticFeedback.perform(HapticFeedbackType.Light) + selectedImages = selectedImages.toMutableList().also { + it.removeAt(index) + } + } + ) + } + } + } } - } - Spacer(modifier = Modifier.height(AppSpacing.xl)) + Spacer(modifier = Modifier.height(OrganicSpacing.xl)) + + // Complete Button + OrganicPrimaryButton( + text = stringResource(Res.string.completions_complete_button), + onClick = { + isSubmitting = true + val notesWithContractor = buildString { + selectedContractor?.let { + append("Contractor: ${it.name}") + it.company?.let { company -> append(" ($company)") } + append("\n") + } + if (completedByName.isNotBlank()) { + append("Completed by: $completedByName\n") + } + if (notes.isNotBlank()) { + append(notes) + } + }.ifBlank { null } + + onComplete( + TaskCompletionCreateRequest( + taskId = taskId, + completedAt = null, + actualCost = actualCost.ifBlank { null }?.toDoubleOrNull(), + notes = notesWithContractor, + rating = rating, + imageUrls = null + ), + selectedImages + ) + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = OrganicSpacing.lg), + enabled = !isSubmitting, + isLoading = isSubmitting, + icon = Icons.Default.CheckCircle + ) + + Spacer(modifier = Modifier.height(OrganicSpacing.xl)) + } } } @@ -475,7 +449,7 @@ private fun SectionHeader( Column( modifier = Modifier .fillMaxWidth() - .padding(horizontal = AppSpacing.lg, vertical = AppSpacing.sm) + .padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.sm) ) { Text( text = title, @@ -503,7 +477,7 @@ private fun ImageThumbnailCard( Box( modifier = Modifier .size(100.dp) - .clip(RoundedCornerShape(AppRadius.md)) + .clip(OrganicShapes.medium) .background(MaterialTheme.colorScheme.surfaceVariant) ) { if (imageBitmap != null) { @@ -530,7 +504,7 @@ private fun ImageThumbnailCard( Box( modifier = Modifier .align(Alignment.TopEnd) - .padding(AppSpacing.xs) + .padding(OrganicSpacing.xs) .size(24.dp) .clip(CircleShape) .background(MaterialTheme.colorScheme.error) @@ -565,16 +539,16 @@ private fun ContractorPickerSheet( Column( modifier = Modifier .fillMaxWidth() - .padding(bottom = AppSpacing.xl) + .padding(bottom = OrganicSpacing.xl) ) { Text( text = stringResource(Res.string.completions_select_contractor), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, - modifier = Modifier.padding(horizontal = AppSpacing.lg, vertical = AppSpacing.md) + modifier = Modifier.padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.md) ) - HorizontalDivider() + OrganicDivider() // None option ListItem( @@ -592,13 +566,13 @@ private fun ContractorPickerSheet( modifier = Modifier.clickable { onSelect(null) } ) - HorizontalDivider() + OrganicDivider() if (isLoading) { Box( modifier = Modifier .fillMaxWidth() - .padding(AppSpacing.xl), + .padding(OrganicSpacing.xl), contentAlignment = Alignment.Center ) { CircularProgressIndicator() diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ContractorDetailScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ContractorDetailScreen.kt index 05983e0..8c9997a 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ContractorDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ContractorDetailScreen.kt @@ -1,19 +1,15 @@ package com.example.casera.ui.screens -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape 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.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalUriHandler @@ -31,6 +27,7 @@ import com.example.casera.network.ApiResult import com.example.casera.platform.rememberShareContractor import com.example.casera.utils.SubscriptionHelper import com.example.casera.ui.subscription.UpgradePromptDialog +import com.example.casera.ui.theme.* import casera.composeapp.generated.resources.* import org.jetbrains.compose.resources.stringResource @@ -123,64 +120,52 @@ fun ContractorDetailScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = Color(0xFFF9FAFB) + containerColor = MaterialTheme.colorScheme.surface ) ) } ) { padding -> - Box( - modifier = Modifier - .fillMaxSize() - .padding(padding) - .background(Color(0xFFF9FAFB)) - ) { - val uriHandler = LocalUriHandler.current - val residences = DataManager.residences.value + WarmGradientBackground { + Box( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + val uriHandler = LocalUriHandler.current + val residences = DataManager.residences.value - ApiResultHandler( - state = contractorState, - onRetry = { viewModel.loadContractorDetail(contractorId) }, - errorTitle = stringResource(Res.string.contractors_failed_to_load), - loadingContent = { - CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) - } - ) { contractor -> + ApiResultHandler( + state = contractorState, + onRetry = { viewModel.loadContractorDetail(contractorId) }, + errorTitle = stringResource(Res.string.contractors_failed_to_load), + loadingContent = { + CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) + } + ) { contractor -> LazyColumn( modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) + contentPadding = PaddingValues(OrganicSpacing.medium), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.medium) ) { // Header Card item { - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), - elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) - ) { + OrganicCard(modifier = Modifier.fillMaxWidth()) { Column( modifier = Modifier .fillMaxWidth() - .padding(24.dp), + .padding(OrganicSpacing.large), horizontalAlignment = Alignment.CenterHorizontally ) { // Avatar - Box( - modifier = Modifier - .size(80.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.primaryContainer), - contentAlignment = Alignment.Center - ) { - Icon( - Icons.Default.Person, - contentDescription = null, - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.primary - ) - } + OrganicIconContainer( + icon = Icons.Default.Person, + size = 80.dp, + iconSize = 48.dp, + containerColor = MaterialTheme.colorScheme.primaryContainer, + iconTint = MaterialTheme.colorScheme.primary + ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(OrganicSpacing.medium)) Text( text = contractor.name, @@ -198,18 +183,18 @@ fun ContractorDetailScreen( } if (contractor.specialties.isNotEmpty()) { - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(OrganicSpacing.medium)) FlowRow( - horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), - verticalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.small, Alignment.CenterHorizontally), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.small) ) { contractor.specialties.forEach { specialty -> Surface( - shape = RoundedCornerShape(20.dp), + shape = OrganicShapes.large, color = MaterialTheme.colorScheme.primaryContainer ) { Row( - modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), + modifier = Modifier.padding(horizontal = OrganicSpacing.medium, vertical = OrganicSpacing.extraSmall), verticalAlignment = Alignment.CenterVertically ) { Icon( @@ -218,7 +203,7 @@ fun ContractorDetailScreen( modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.primary ) - Spacer(modifier = Modifier.width(4.dp)) + Spacer(modifier = Modifier.width(OrganicSpacing.extraSmall)) Text( text = specialty.name, style = MaterialTheme.typography.bodyMedium, @@ -232,7 +217,7 @@ fun ContractorDetailScreen( } if (contractor.rating != null && contractor.rating > 0) { - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(OrganicSpacing.medium)) Row(verticalAlignment = Alignment.CenterVertically) { repeat(5) { index -> Icon( @@ -242,7 +227,7 @@ fun ContractorDetailScreen( tint = Color(0xFFF59E0B) ) } - Spacer(modifier = Modifier.width(8.dp)) + Spacer(modifier = Modifier.width(OrganicSpacing.small)) Text( text = ((contractor.rating * 10).toInt() / 10.0).toString(), style = MaterialTheme.typography.titleMedium, @@ -253,7 +238,7 @@ fun ContractorDetailScreen( } if (contractor.taskCount > 0) { - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(OrganicSpacing.extraSmall)) Text( text = stringResource(Res.string.contractors_completed_tasks, contractor.taskCount), style = MaterialTheme.typography.bodySmall, @@ -269,7 +254,7 @@ fun ContractorDetailScreen( item { Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp) + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.medium) ) { contractor.phone?.let { phone -> QuickActionButton( @@ -388,7 +373,7 @@ fun ContractorDetailScreen( text = stringResource(Res.string.contractors_no_contact_info), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(16.dp) + modifier = Modifier.padding(OrganicSpacing.medium) ) } } @@ -459,8 +444,8 @@ fun ContractorDetailScreen( item { DetailSection(title = stringResource(Res.string.contractors_notes)) { Row( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp) + modifier = Modifier.padding(horizontal = OrganicSpacing.medium, vertical = OrganicSpacing.medium), + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.medium) ) { Icon( Icons.Default.Notes, @@ -484,7 +469,7 @@ fun ContractorDetailScreen( Row( modifier = Modifier .fillMaxWidth() - .padding(16.dp), + .padding(OrganicSpacing.medium), horizontalArrangement = Arrangement.SpaceEvenly ) { StatCard( @@ -516,7 +501,7 @@ fun ContractorDetailScreen( value = createdBy.username, iconTint = MaterialTheme.colorScheme.onSurfaceVariant ) - HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp)) + OrganicDivider(modifier = Modifier.padding(horizontal = OrganicSpacing.medium)) } DetailRow( @@ -531,6 +516,7 @@ fun ContractorDetailScreen( } } } + } if (showEditDialog) { AddContractorDialog( @@ -565,8 +551,8 @@ fun ContractorDetailScreen( Text(stringResource(Res.string.common_cancel)) } }, - containerColor = Color.White, - shape = RoundedCornerShape(16.dp) + containerColor = MaterialTheme.colorScheme.surface, + shape = OrganicShapes.large ) } @@ -591,19 +577,14 @@ fun DetailSection( title: String, content: @Composable ColumnScope.() -> Unit ) { - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), - elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) - ) { + OrganicCard(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.fillMaxWidth()) { Text( text = title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(16.dp).padding(bottom = 0.dp) + modifier = Modifier.padding(OrganicSpacing.medium).padding(bottom = 0.dp) ) content() } @@ -620,7 +601,7 @@ fun DetailRow( Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp), + .padding(horizontal = OrganicSpacing.medium, vertical = OrganicSpacing.medium), verticalAlignment = Alignment.Top ) { Icon( @@ -629,7 +610,7 @@ fun DetailRow( modifier = Modifier.size(20.dp), tint = iconTint ) - Spacer(modifier = Modifier.width(12.dp)) + Spacer(modifier = Modifier.width(OrganicSpacing.medium)) Column(modifier = Modifier.weight(1f)) { Text( text = label, @@ -658,7 +639,7 @@ fun ClickableDetailRow( modifier = Modifier .fillMaxWidth() .clickable(onClick = onClick) - .padding(horizontal = 16.dp, vertical = 12.dp), + .padding(horizontal = OrganicSpacing.medium, vertical = OrganicSpacing.medium), verticalAlignment = Alignment.Top ) { Icon( @@ -667,7 +648,7 @@ fun ClickableDetailRow( modifier = Modifier.size(20.dp), tint = iconTint ) - Spacer(modifier = Modifier.width(12.dp)) + Spacer(modifier = Modifier.width(OrganicSpacing.medium)) Column(modifier = Modifier.weight(1f)) { Text( text = label, @@ -698,33 +679,23 @@ fun QuickActionButton( modifier: Modifier = Modifier, onClick: () -> Unit ) { - Card( - modifier = modifier.clickable(onClick = onClick), - shape = RoundedCornerShape(12.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), - elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) + OrganicCard( + modifier = modifier.clickable(onClick = onClick) ) { Column( modifier = Modifier .fillMaxWidth() - .padding(vertical = 12.dp), + .padding(vertical = OrganicSpacing.medium), horizontalAlignment = Alignment.CenterHorizontally ) { - Box( - modifier = Modifier - .size(44.dp) - .clip(CircleShape) - .background(color.copy(alpha = 0.1f)), - contentAlignment = Alignment.Center - ) { - Icon( - icon, - contentDescription = null, - modifier = Modifier.size(22.dp), - tint = color - ) - } - Spacer(modifier = Modifier.height(8.dp)) + OrganicIconContainer( + icon = icon, + size = 44.dp, + iconSize = 22.dp, + containerColor = color.copy(alpha = 0.1f), + iconTint = color + ) + Spacer(modifier = Modifier.height(OrganicSpacing.small)) Text( text = label, style = MaterialTheme.typography.labelMedium, @@ -745,21 +716,14 @@ fun StatCard( Column( horizontalAlignment = Alignment.CenterHorizontally ) { - Box( - modifier = Modifier - .size(44.dp) - .clip(CircleShape) - .background(color.copy(alpha = 0.1f)), - contentAlignment = Alignment.Center - ) { - Icon( - icon, - contentDescription = null, - modifier = Modifier.size(22.dp), - tint = color - ) - } - Spacer(modifier = Modifier.height(8.dp)) + OrganicIconContainer( + icon = icon, + size = 44.dp, + iconSize = 22.dp, + containerColor = color.copy(alpha = 0.1f), + iconTint = color + ) + Spacer(modifier = Modifier.height(OrganicSpacing.small)) Text( text = value, style = MaterialTheme.typography.titleLarge, diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ContractorsScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ContractorsScreen.kt index 8bcffac..1ee906b 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ContractorsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ContractorsScreen.kt @@ -1,12 +1,9 @@ package com.example.casera.ui.screens -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material3.* @@ -14,8 +11,6 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -31,6 +26,7 @@ import com.example.casera.ui.subscription.UpgradeFeatureScreen import com.example.casera.utils.SubscriptionHelper import com.example.casera.analytics.PostHogAnalytics import com.example.casera.analytics.AnalyticsEvents +import com.example.casera.ui.theme.* import casera.composeapp.generated.resources.* import org.jetbrains.compose.resources.stringResource @@ -215,130 +211,135 @@ fun ContractorsScreen( ) } } else { - Column( - modifier = Modifier - .fillMaxSize() - .padding(padding) - .background(MaterialTheme.colorScheme.background) - ) { - // Search bar - OutlinedTextField( - value = searchQuery, - onValueChange = { searchQuery = it }, + WarmGradientBackground { + Column( modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - placeholder = { Text(stringResource(Res.string.contractors_search)) }, - leadingIcon = { Icon(Icons.Default.Search, stringResource(Res.string.common_search)) }, - trailingIcon = { - if (searchQuery.isNotEmpty()) { - IconButton(onClick = { searchQuery = "" }) { - Icon(Icons.Default.Close, stringResource(Res.string.contractors_clear_search)) - } - } - }, - singleLine = true, - shape = RoundedCornerShape(12.dp), - colors = OutlinedTextFieldDefaults.colors( - focusedContainerColor = MaterialTheme.colorScheme.surface, - unfocusedContainerColor = MaterialTheme.colorScheme.surface, - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.outline - ) - ) - - // Active filters display - if (selectedFilter != null || showFavoritesOnly) { - Row( + .fillMaxSize() + .padding(padding) + ) { + // Search bar + OutlinedTextField( + value = searchQuery, + onValueChange = { searchQuery = it }, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 4.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - if (showFavoritesOnly) { - FilterChip( - selected = true, - onClick = { showFavoritesOnly = false }, - label = { Text(stringResource(Res.string.contractors_favorites)) }, - leadingIcon = { Icon(Icons.Default.Star, null, modifier = Modifier.size(16.dp)) } - ) - } - if (selectedFilter != null) { - FilterChip( - selected = true, - onClick = { selectedFilter = null }, - label = { Text(selectedFilter!!) }, - trailingIcon = { Icon(Icons.Default.Close, null, modifier = Modifier.size(16.dp)) } - ) - } - } - } - - ApiResultHandler( - state = contractorsState, - onRetry = { - viewModel.loadContractors() - }, - errorTitle = stringResource(Res.string.contractors_failed_to_load), - loadingContent = { - if (!isRefreshing) { - CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) - } - } - ) { _ -> - // Use filteredContractors for client-side filtering - if (filteredContractors.isEmpty()) { - // Empty state - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - Icons.Default.PersonAdd, - contentDescription = null, - modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - if (searchQuery.isNotEmpty() || selectedFilter != null || showFavoritesOnly) - stringResource(Res.string.contractors_no_results) - else - stringResource(Res.string.contractors_empty_title), - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - if (searchQuery.isEmpty() && selectedFilter == null && !showFavoritesOnly) { - Text( - stringResource(Res.string.contractors_empty_subtitle_first), - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodySmall - ) + .padding(horizontal = OrganicSpacing.medium, vertical = OrganicSpacing.small), + placeholder = { Text(stringResource(Res.string.contractors_search)) }, + leadingIcon = { Icon(Icons.Default.Search, stringResource(Res.string.common_search)) }, + trailingIcon = { + if (searchQuery.isNotEmpty()) { + IconButton(onClick = { searchQuery = "" }) { + Icon(Icons.Default.Close, stringResource(Res.string.contractors_clear_search)) } } - } - } else { - PullToRefreshBox( - isRefreshing = isRefreshing, - onRefresh = { - isRefreshing = true - viewModel.loadContractors() - }, - modifier = Modifier.fillMaxSize() + }, + singleLine = true, + shape = OrganicShapes.medium, + colors = OutlinedTextFieldDefaults.colors( + focusedContainerColor = MaterialTheme.colorScheme.surface, + unfocusedContainerColor = MaterialTheme.colorScheme.surface, + focusedBorderColor = MaterialTheme.colorScheme.primary, + unfocusedBorderColor = MaterialTheme.colorScheme.outline + ) + ) + + // Active filters display + if (selectedFilter != null || showFavoritesOnly) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = OrganicSpacing.medium, vertical = OrganicSpacing.extraSmall), + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.small) ) { - LazyColumn( + if (showFavoritesOnly) { + FilterChip( + selected = true, + onClick = { showFavoritesOnly = false }, + label = { Text(stringResource(Res.string.contractors_favorites)) }, + leadingIcon = { Icon(Icons.Default.Star, null, modifier = Modifier.size(16.dp)) } + ) + } + if (selectedFilter != null) { + FilterChip( + selected = true, + onClick = { selectedFilter = null }, + label = { Text(selectedFilter!!) }, + trailingIcon = { Icon(Icons.Default.Close, null, modifier = Modifier.size(16.dp)) } + ) + } + } + } + + ApiResultHandler( + state = contractorsState, + onRetry = { + viewModel.loadContractors() + }, + errorTitle = stringResource(Res.string.contractors_failed_to_load), + loadingContent = { + if (!isRefreshing) { + CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) + } + } + ) { _ -> + // Use filteredContractors for client-side filtering + if (filteredContractors.isEmpty()) { + // Empty state with organic styling + Box( modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) + contentAlignment = Alignment.Center ) { - items(filteredContractors, key = { it.id }) { contractor -> - ContractorCard( - contractor = contractor, - onToggleFavorite = { viewModel.toggleFavorite(it) }, - onClick = { onNavigateToContractorDetail(it) } + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.small) + ) { + OrganicIconContainer( + icon = Icons.Default.PersonAdd, + size = 80.dp, + iconSize = 40.dp, + containerColor = MaterialTheme.colorScheme.primaryContainer, + iconTint = MaterialTheme.colorScheme.onPrimaryContainer ) + Spacer(modifier = Modifier.height(OrganicSpacing.small)) + Text( + if (searchQuery.isNotEmpty() || selectedFilter != null || showFavoritesOnly) + stringResource(Res.string.contractors_no_results) + else + stringResource(Res.string.contractors_empty_title), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface + ) + if (searchQuery.isEmpty() && selectedFilter == null && !showFavoritesOnly) { + Text( + stringResource(Res.string.contractors_empty_subtitle_first), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium + ) + } + } + } + } else { + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = { + isRefreshing = true + viewModel.loadContractors() + }, + modifier = Modifier.fillMaxSize() + ) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(OrganicSpacing.medium), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.medium) + ) { + items(filteredContractors, key = { it.id }) { contractor -> + ContractorCard( + contractor = contractor, + onToggleFavorite = { viewModel.toggleFavorite(it) }, + onClick = { onNavigateToContractorDetail(it) } + ) + } } } } @@ -381,41 +382,27 @@ fun ContractorCard( onToggleFavorite: (Int) -> Unit, onClick: (Int) -> Unit ) { - Card( + OrganicCard( modifier = Modifier .fillMaxWidth() - .clickable { onClick(contractor.id) }, - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ), - elevation = CardDefaults.cardElevation( - defaultElevation = 1.dp - ) + .clickable { onClick(contractor.id) } ) { Row( modifier = Modifier .fillMaxWidth() - .padding(16.dp), + .padding(OrganicSpacing.medium), verticalAlignment = Alignment.CenterVertically ) { // Avatar/Icon - Box( - modifier = Modifier - .size(56.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.primaryContainer), - contentAlignment = Alignment.Center - ) { - Icon( - Icons.Default.Person, - contentDescription = null, - modifier = Modifier.size(32.dp), - tint = MaterialTheme.colorScheme.onPrimaryContainer - ) - } + OrganicIconContainer( + icon = Icons.Default.Person, + size = 56.dp, + iconSize = 32.dp, + containerColor = MaterialTheme.colorScheme.primaryContainer, + iconTint = MaterialTheme.colorScheme.onPrimaryContainer + ) - Spacer(modifier = Modifier.width(16.dp)) + Spacer(modifier = Modifier.width(OrganicSpacing.medium)) Column(modifier = Modifier.weight(1f)) { Row(verticalAlignment = Alignment.CenterVertically) { @@ -428,7 +415,7 @@ fun ContractorCard( overflow = TextOverflow.Ellipsis ) if (contractor.isFavorite) { - Spacer(modifier = Modifier.width(4.dp)) + Spacer(modifier = Modifier.width(OrganicSpacing.extraSmall)) Icon( Icons.Default.Star, contentDescription = stringResource(Res.string.contractors_favorite), @@ -448,10 +435,10 @@ fun ContractorCard( ) } - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(OrganicSpacing.small)) Row( - horizontalArrangement = Arrangement.spacedBy(12.dp), + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.medium), verticalAlignment = Alignment.CenterVertically ) { if (contractor.specialties.isNotEmpty()) { @@ -462,7 +449,7 @@ fun ContractorCard( modifier = Modifier.size(14.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant ) - Spacer(modifier = Modifier.width(4.dp)) + Spacer(modifier = Modifier.width(OrganicSpacing.extraSmall)) Text( text = contractor.specialties.first().name, style = MaterialTheme.typography.bodySmall, @@ -479,7 +466,7 @@ fun ContractorCard( modifier = Modifier.size(14.dp), tint = MaterialTheme.colorScheme.tertiary ) - Spacer(modifier = Modifier.width(4.dp)) + Spacer(modifier = Modifier.width(OrganicSpacing.extraSmall)) Text( text = "${(contractor.rating * 10).toInt() / 10.0}", style = MaterialTheme.typography.bodySmall, @@ -497,7 +484,7 @@ fun ContractorCard( modifier = Modifier.size(14.dp), tint = MaterialTheme.colorScheme.secondary ) - Spacer(modifier = Modifier.width(4.dp)) + Spacer(modifier = Modifier.width(OrganicSpacing.extraSmall)) Text( text = stringResource(Res.string.contractors_tasks_count, contractor.taskCount), style = MaterialTheme.typography.bodySmall, @@ -528,4 +515,3 @@ fun ContractorCard( } } } - diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/DocumentDetailScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/DocumentDetailScreen.kt index 2388b17..406e04e 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/DocumentDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/DocumentDetailScreen.kt @@ -36,6 +36,7 @@ import coil3.compose.SubcomposeAsyncImageContent import coil3.compose.AsyncImagePainter import com.example.casera.ui.components.AuthenticatedImage import com.example.casera.util.DateUtils +import com.example.casera.ui.theme.* import casera.composeapp.generated.resources.* import org.jetbrains.compose.resources.stringResource @@ -96,22 +97,23 @@ fun DocumentDetailScreen( ) } ) { padding -> - Box( - modifier = Modifier - .fillMaxSize() - .padding(padding) - ) { - ApiResultHandler( - state = documentState, - onRetry = { documentViewModel.loadDocumentDetail(documentId) }, - errorTitle = stringResource(Res.string.documents_failed_to_load) - ) { document -> + WarmGradientBackground { + Box( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + ApiResultHandler( + state = documentState, + onRetry = { documentViewModel.loadDocumentDetail(documentId) }, + errorTitle = stringResource(Res.string.documents_failed_to_load) + ) { document -> Column( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) - .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 96.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) + .padding(start = OrganicSpacing.lg, end = OrganicSpacing.lg, top = OrganicSpacing.lg, bottom = 96.dp), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg) ) { // Status badge (for warranties) if (document.documentType == "warranty") { @@ -124,16 +126,14 @@ fun DocumentDetailScreen( else -> Color(0xFF10B981) } - Card( + OrganicCard( modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = statusColor.copy(alpha = 0.1f) - ) + accentColor = statusColor.copy(alpha = 0.1f) ) { Row( modifier = Modifier .fillMaxWidth() - .padding(16.dp), + .padding(OrganicSpacing.lg), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { @@ -175,17 +175,17 @@ fun DocumentDetailScreen( } // Basic Information - Card(modifier = Modifier.fillMaxWidth()) { + OrganicCard(modifier = Modifier.fillMaxWidth()) { Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) + modifier = Modifier.padding(OrganicSpacing.lg), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md) ) { Text( stringResource(Res.string.documents_basic_info), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) - Divider() + OrganicDivider() DetailRow(stringResource(Res.string.documents_title_label), document.title) DetailRow(stringResource(Res.string.documents_type_label), DocumentType.fromValue(document.documentType).displayName) @@ -202,17 +202,17 @@ fun DocumentDetailScreen( if (document.documentType == "warranty" && (document.itemName != null || document.modelNumber != null || document.serialNumber != null || document.provider != null)) { - Card(modifier = Modifier.fillMaxWidth()) { + OrganicCard(modifier = Modifier.fillMaxWidth()) { Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) + modifier = Modifier.padding(OrganicSpacing.lg), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md) ) { Text( stringResource(Res.string.documents_item_details), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) - Divider() + OrganicDivider() document.itemName?.let { DetailRow(stringResource(Res.string.documents_item_name), it) } document.modelNumber?.let { DetailRow(stringResource(Res.string.documents_model_number), it) } @@ -227,17 +227,17 @@ fun DocumentDetailScreen( if (document.documentType == "warranty" && (document.claimPhone != null || document.claimEmail != null || document.claimWebsite != null)) { - Card(modifier = Modifier.fillMaxWidth()) { + OrganicCard(modifier = Modifier.fillMaxWidth()) { Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) + modifier = Modifier.padding(OrganicSpacing.lg), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md) ) { Text( stringResource(Res.string.documents_claim_info), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) - Divider() + OrganicDivider() document.claimPhone?.let { DetailRow(stringResource(Res.string.documents_claim_phone), it) } document.claimEmail?.let { DetailRow(stringResource(Res.string.documents_claim_email), it) } @@ -249,17 +249,17 @@ fun DocumentDetailScreen( // Dates if (document.purchaseDate != null || document.startDate != null || document.endDate != null) { - Card(modifier = Modifier.fillMaxWidth()) { + OrganicCard(modifier = Modifier.fillMaxWidth()) { Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) + modifier = Modifier.padding(OrganicSpacing.lg), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md) ) { Text( stringResource(Res.string.documents_important_dates), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) - Divider() + OrganicDivider() document.purchaseDate?.let { DetailRow(stringResource(Res.string.documents_purchase_date), DateUtils.formatDateMedium(it)) } document.startDate?.let { DetailRow(stringResource(Res.string.documents_start_date), DateUtils.formatDateMedium(it)) } @@ -269,17 +269,17 @@ fun DocumentDetailScreen( } // Residence & Contractor - Card(modifier = Modifier.fillMaxWidth()) { + OrganicCard(modifier = Modifier.fillMaxWidth()) { Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) + modifier = Modifier.padding(OrganicSpacing.lg), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md) ) { Text( stringResource(Res.string.documents_associations), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) - Divider() + OrganicDivider() document.residenceAddress?.let { DetailRow(stringResource(Res.string.documents_residence), it) } document.contractorName?.let { DetailRow(stringResource(Res.string.documents_contractor), it) } @@ -289,17 +289,17 @@ fun DocumentDetailScreen( // Additional Information if (document.tags != null || document.notes != null) { - Card(modifier = Modifier.fillMaxWidth()) { + OrganicCard(modifier = Modifier.fillMaxWidth()) { Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) + modifier = Modifier.padding(OrganicSpacing.lg), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md) ) { Text( stringResource(Res.string.documents_additional_info), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) - Divider() + OrganicDivider() document.tags?.let { DetailRow(stringResource(Res.string.documents_tags), it) } document.notes?.let { DetailRow(stringResource(Res.string.documents_notes), it) } @@ -309,22 +309,22 @@ fun DocumentDetailScreen( // Images if (document.images.isNotEmpty()) { - Card(modifier = Modifier.fillMaxWidth()) { + OrganicCard(modifier = Modifier.fillMaxWidth()) { Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) + modifier = Modifier.padding(OrganicSpacing.lg), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md) ) { Text( stringResource(Res.string.documents_images, document.images.size), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) - Divider() + OrganicDivider() // Image grid Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm) ) { document.images.take(4).forEachIndexed { index, image -> Box( @@ -366,17 +366,17 @@ fun DocumentDetailScreen( // File Information if (document.fileUrl != null) { - Card(modifier = Modifier.fillMaxWidth()) { + OrganicCard(modifier = Modifier.fillMaxWidth()) { Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) + modifier = Modifier.padding(OrganicSpacing.lg), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md) ) { Text( stringResource(Res.string.documents_attached_file), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) - Divider() + OrganicDivider() document.fileType?.let { DetailRow(stringResource(Res.string.documents_file_type), it) } document.fileSize?.let { @@ -388,7 +388,7 @@ fun DocumentDetailScreen( modifier = Modifier.fillMaxWidth() ) { Icon(Icons.Default.Download, null) - Spacer(modifier = Modifier.width(8.dp)) + Spacer(modifier = Modifier.width(OrganicSpacing.sm)) Text(stringResource(Res.string.documents_download_file)) } } @@ -396,17 +396,17 @@ fun DocumentDetailScreen( } // Metadata - Card(modifier = Modifier.fillMaxWidth()) { + OrganicCard(modifier = Modifier.fillMaxWidth()) { Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) + modifier = Modifier.padding(OrganicSpacing.lg), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md) ) { Text( stringResource(Res.string.documents_metadata), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) - Divider() + OrganicDivider() document.uploadedByUsername?.let { DetailRow(stringResource(Res.string.documents_uploaded_by), it) } document.createdAt?.let { DetailRow(stringResource(Res.string.documents_created), DateUtils.formatDateMedium(it)) } @@ -417,6 +417,7 @@ fun DocumentDetailScreen( } } } + } // Delete confirmation dialog if (showDeleteDialog) { @@ -463,7 +464,7 @@ fun DetailRow(label: String, value: String) { style = MaterialTheme.typography.labelMedium, color = Color.Gray ) - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(OrganicSpacing.xs)) Text( value, style = MaterialTheme.typography.bodyLarge @@ -498,7 +499,7 @@ fun DocumentImageViewer( modifier = Modifier .fillMaxWidth(0.95f) .fillMaxHeight(0.9f), - shape = RoundedCornerShape(16.dp), + shape = RoundedCornerShape(OrganicSpacing.lg), color = MaterialTheme.colorScheme.background ) { Column( @@ -508,7 +509,7 @@ fun DocumentImageViewer( Row( modifier = Modifier .fillMaxWidth() - .padding(16.dp), + .padding(OrganicSpacing.lg), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { @@ -531,7 +532,7 @@ fun DocumentImageViewer( } } - HorizontalDivider() + OrganicDivider() // Content if (showFullImage) { @@ -539,7 +540,7 @@ fun DocumentImageViewer( Column( modifier = Modifier .fillMaxSize() - .padding(16.dp), + .padding(OrganicSpacing.lg), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { @@ -553,16 +554,13 @@ fun DocumentImageViewer( ) images[selectedIndex].caption?.let { caption -> - Spacer(modifier = Modifier.height(16.dp)) - Card( - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ), + Spacer(modifier = Modifier.height(OrganicSpacing.lg)) + OrganicCard( modifier = Modifier.fillMaxWidth() ) { Text( text = caption, - modifier = Modifier.padding(16.dp), + modifier = Modifier.padding(OrganicSpacing.lg), style = MaterialTheme.typography.bodyMedium ) } @@ -570,7 +568,7 @@ fun DocumentImageViewer( // Navigation buttons if (images.size > 1) { - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(OrganicSpacing.lg)) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween @@ -580,7 +578,7 @@ fun DocumentImageViewer( enabled = selectedIndex > 0 ) { Icon(Icons.Default.ArrowBack, "Previous") - Spacer(modifier = Modifier.width(8.dp)) + Spacer(modifier = Modifier.width(OrganicSpacing.sm)) Text("Previous") } Button( @@ -588,7 +586,7 @@ fun DocumentImageViewer( enabled = selectedIndex < images.size - 1 ) { Text("Next") - Spacer(modifier = Modifier.width(8.dp)) + Spacer(modifier = Modifier.width(OrganicSpacing.sm)) Icon(Icons.Default.ArrowForward, "Next") } } @@ -600,19 +598,19 @@ fun DocumentImageViewer( columns = GridCells.Fixed(2), modifier = Modifier .fillMaxSize() - .padding(16.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) + .padding(OrganicSpacing.lg), + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md) ) { items(images.size) { index -> val image = images[index] - Card( - onClick = { - selectedIndex = index - showFullImage = true - }, - shape = RoundedCornerShape(12.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + OrganicCard( + modifier = Modifier + .fillMaxWidth() + .clickable { + selectedIndex = index + showFullImage = true + } ) { Column { AuthenticatedImage( @@ -627,7 +625,7 @@ fun DocumentImageViewer( image.caption?.let { caption -> Text( text = caption, - modifier = Modifier.padding(8.dp), + modifier = Modifier.padding(OrganicSpacing.sm), style = MaterialTheme.typography.bodySmall, maxLines = 2 ) diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/DocumentFormScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/DocumentFormScreen.kt index d81b8c9..2a874f3 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/DocumentFormScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/DocumentFormScreen.kt @@ -28,6 +28,7 @@ import com.example.casera.platform.rememberImagePicker import com.example.casera.platform.rememberCameraPicker import com.example.casera.analytics.PostHogAnalytics import com.example.casera.analytics.AnalyticsEvents +import com.example.casera.ui.theme.* import casera.composeapp.generated.resources.* import org.jetbrains.compose.resources.stringResource @@ -201,546 +202,532 @@ fun DocumentFormScreen( ) } ) { padding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(padding) - .verticalScroll(rememberScrollState()) - .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 96.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - // Loading state for edit mode - if (isEditMode && documentDetailState is ApiResult.Loading) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } - return@Column - } - - // Residence Dropdown (if needed) - if (needsResidenceSelection) { - when (residencesState) { - is ApiResult.Loading -> { - CircularProgressIndicator(modifier = Modifier.size(40.dp)) + WarmGradientBackground { + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .verticalScroll(rememberScrollState()) + .padding(start = OrganicSpacing.cozy, end = OrganicSpacing.cozy, top = OrganicSpacing.cozy, bottom = 96.dp), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy) + ) { + // Loading state for edit mode + if (isEditMode && documentDetailState is ApiResult.Loading) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() } - is ApiResult.Success -> { - val residences = (residencesState as ApiResult.Success>).data - ExposedDropdownMenuBox( - expanded = residenceExpanded, - onExpandedChange = { residenceExpanded = it } - ) { - OutlinedTextField( - value = selectedResidence?.name ?: stringResource(Res.string.documents_form_select_residence), - onValueChange = {}, - readOnly = true, - label = { Text(stringResource(Res.string.documents_form_residence_required)) }, - isError = residenceError.isNotEmpty(), - supportingText = if (residenceError.isNotEmpty()) { - { Text(residenceError) } - } else null, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = residenceExpanded) }, - modifier = Modifier.fillMaxWidth().menuAnchor() - ) - ExposedDropdownMenu( + return@Column + } + + // Residence Dropdown (if needed) + if (needsResidenceSelection) { + when (residencesState) { + is ApiResult.Loading -> { + CircularProgressIndicator(modifier = Modifier.size(40.dp)) + } + is ApiResult.Success -> { + val residences = (residencesState as ApiResult.Success>).data + ExposedDropdownMenuBox( expanded = residenceExpanded, - onDismissRequest = { residenceExpanded = false } + onExpandedChange = { residenceExpanded = it } ) { - residences.forEach { residence -> - DropdownMenuItem( - text = { Text(residence.name) }, - onClick = { - selectedResidence = residence - residenceError = "" - residenceExpanded = false - } - ) + OutlinedTextField( + value = selectedResidence?.name ?: stringResource(Res.string.documents_form_select_residence), + onValueChange = {}, + readOnly = true, + label = { Text(stringResource(Res.string.documents_form_residence_required)) }, + isError = residenceError.isNotEmpty(), + supportingText = if (residenceError.isNotEmpty()) { + { Text(residenceError) } + } else null, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = residenceExpanded) }, + modifier = Modifier.fillMaxWidth().menuAnchor() + ) + ExposedDropdownMenu( + expanded = residenceExpanded, + onDismissRequest = { residenceExpanded = false } + ) { + residences.forEach { residence -> + DropdownMenuItem( + text = { Text(residence.name) }, + onClick = { + selectedResidence = residence + residenceError = "" + residenceExpanded = false + } + ) + } } } } - } - is ApiResult.Error -> { - Text( - stringResource(Res.string.documents_form_failed_to_load_residences, com.example.casera.util.ErrorMessageParser.parse((residencesState as ApiResult.Error).message)), - color = MaterialTheme.colorScheme.error - ) - } - else -> {} - } - } - - // Document Type Dropdown - ExposedDropdownMenuBox( - expanded = documentTypeExpanded, - onExpandedChange = { documentTypeExpanded = it } - ) { - OutlinedTextField( - value = DocumentType.fromValue(selectedDocumentType).displayName, - onValueChange = {}, - readOnly = true, - label = { Text(stringResource(Res.string.documents_form_document_type_required)) }, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = documentTypeExpanded) }, - modifier = Modifier.fillMaxWidth().menuAnchor() - ) - ExposedDropdownMenu( - expanded = documentTypeExpanded, - onDismissRequest = { documentTypeExpanded = false } - ) { - DocumentType.values().forEach { type -> - DropdownMenuItem( - text = { Text(type.displayName) }, - onClick = { - selectedDocumentType = type.value - documentTypeExpanded = false - } - ) + is ApiResult.Error -> { + Text( + stringResource(Res.string.documents_form_failed_to_load_residences, com.example.casera.util.ErrorMessageParser.parse((residencesState as ApiResult.Error).message)), + color = MaterialTheme.colorScheme.error + ) + } + else -> {} } } - } - // Title - OutlinedTextField( - value = title, - onValueChange = { - title = it - titleError = "" - }, - label = { Text(stringResource(Res.string.documents_form_title_required)) }, - isError = titleError.isNotEmpty(), - supportingText = if (titleError.isNotEmpty()) { - { Text(titleError) } - } else null, - modifier = Modifier.fillMaxWidth() - ) - - // Warranty-specific fields - if (isWarranty) { - OutlinedTextField( - value = itemName, - onValueChange = { - itemName = it - itemNameError = "" - }, - label = { Text(stringResource(Res.string.documents_form_item_name_required)) }, - isError = itemNameError.isNotEmpty(), - supportingText = if (itemNameError.isNotEmpty()) { - { Text(itemNameError) } - } else null, - modifier = Modifier.fillMaxWidth() - ) - - OutlinedTextField( - value = modelNumber, - onValueChange = { modelNumber = it }, - label = { Text(stringResource(Res.string.documents_form_model_number)) }, - modifier = Modifier.fillMaxWidth() - ) - - OutlinedTextField( - value = serialNumber, - onValueChange = { serialNumber = it }, - label = { Text(stringResource(Res.string.documents_form_serial_number)) }, - modifier = Modifier.fillMaxWidth() - ) - - OutlinedTextField( - value = provider, - onValueChange = { - provider = it - providerError = "" - }, - label = { Text(stringResource(Res.string.documents_form_provider_required)) }, - isError = providerError.isNotEmpty(), - supportingText = if (providerError.isNotEmpty()) { - { Text(providerError) } - } else null, - modifier = Modifier.fillMaxWidth() - ) - - OutlinedTextField( - value = providerContact, - onValueChange = { providerContact = it }, - label = { Text(stringResource(Res.string.documents_form_provider_contact)) }, - modifier = Modifier.fillMaxWidth() - ) - - OutlinedTextField( - value = claimPhone, - onValueChange = { claimPhone = it }, - label = { Text(stringResource(Res.string.documents_form_claim_phone)) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone), - modifier = Modifier.fillMaxWidth() - ) - - OutlinedTextField( - value = claimEmail, - onValueChange = { claimEmail = it }, - label = { Text(stringResource(Res.string.documents_form_claim_email)) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email), - modifier = Modifier.fillMaxWidth() - ) - - OutlinedTextField( - value = claimWebsite, - onValueChange = { claimWebsite = it }, - label = { Text(stringResource(Res.string.documents_form_claim_website)) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri), - modifier = Modifier.fillMaxWidth() - ) - - OutlinedTextField( - value = purchaseDate, - onValueChange = { purchaseDate = it }, - label = { Text(stringResource(Res.string.documents_form_purchase_date)) }, - placeholder = { Text(stringResource(Res.string.documents_form_date_placeholder)) }, - modifier = Modifier.fillMaxWidth() - ) - - OutlinedTextField( - value = startDate, - onValueChange = { startDate = it }, - label = { Text(stringResource(Res.string.documents_form_warranty_start)) }, - placeholder = { Text(stringResource(Res.string.documents_form_date_placeholder)) }, - modifier = Modifier.fillMaxWidth() - ) - - OutlinedTextField( - value = endDate, - onValueChange = { endDate = it }, - label = { Text(stringResource(Res.string.documents_form_warranty_end_required)) }, - placeholder = { Text(stringResource(Res.string.documents_form_date_placeholder_end)) }, - modifier = Modifier.fillMaxWidth() - ) - } - - // Description - OutlinedTextField( - value = description, - onValueChange = { description = it }, - label = { Text(stringResource(Res.string.documents_form_description)) }, - minLines = 3, - modifier = Modifier.fillMaxWidth() - ) - - // Category Dropdown (for warranties and some documents) - if (isWarranty || selectedDocumentType in listOf("inspection", "manual", "receipt")) { + // Document Type Dropdown ExposedDropdownMenuBox( - expanded = categoryExpanded, - onExpandedChange = { categoryExpanded = it } + expanded = documentTypeExpanded, + onExpandedChange = { documentTypeExpanded = it } ) { OutlinedTextField( - value = selectedCategory?.let { DocumentCategory.fromValue(it).displayName } ?: stringResource(Res.string.documents_form_select_category), + value = DocumentType.fromValue(selectedDocumentType).displayName, onValueChange = {}, readOnly = true, - label = { Text(stringResource(Res.string.documents_form_category)) }, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = categoryExpanded) }, + label = { Text(stringResource(Res.string.documents_form_document_type_required)) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = documentTypeExpanded) }, modifier = Modifier.fillMaxWidth().menuAnchor() ) ExposedDropdownMenu( - expanded = categoryExpanded, - onDismissRequest = { categoryExpanded = false } + expanded = documentTypeExpanded, + onDismissRequest = { documentTypeExpanded = false } ) { - DropdownMenuItem( - text = { Text(stringResource(Res.string.documents_form_category_none)) }, - onClick = { - selectedCategory = null - categoryExpanded = false - } - ) - DocumentCategory.values().forEach { category -> + DocumentType.values().forEach { type -> DropdownMenuItem( - text = { Text(category.displayName) }, + text = { Text(type.displayName) }, onClick = { - selectedCategory = category.value + selectedDocumentType = type.value + documentTypeExpanded = false + } + ) + } + } + } + + // Title + OutlinedTextField( + value = title, + onValueChange = { + title = it + titleError = "" + }, + label = { Text(stringResource(Res.string.documents_form_title_required)) }, + isError = titleError.isNotEmpty(), + supportingText = if (titleError.isNotEmpty()) { + { Text(titleError) } + } else null, + modifier = Modifier.fillMaxWidth() + ) + + // Warranty-specific fields + if (isWarranty) { + OutlinedTextField( + value = itemName, + onValueChange = { + itemName = it + itemNameError = "" + }, + label = { Text(stringResource(Res.string.documents_form_item_name_required)) }, + isError = itemNameError.isNotEmpty(), + supportingText = if (itemNameError.isNotEmpty()) { + { Text(itemNameError) } + } else null, + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = modelNumber, + onValueChange = { modelNumber = it }, + label = { Text(stringResource(Res.string.documents_form_model_number)) }, + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = serialNumber, + onValueChange = { serialNumber = it }, + label = { Text(stringResource(Res.string.documents_form_serial_number)) }, + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = provider, + onValueChange = { + provider = it + providerError = "" + }, + label = { Text(stringResource(Res.string.documents_form_provider_required)) }, + isError = providerError.isNotEmpty(), + supportingText = if (providerError.isNotEmpty()) { + { Text(providerError) } + } else null, + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = providerContact, + onValueChange = { providerContact = it }, + label = { Text(stringResource(Res.string.documents_form_provider_contact)) }, + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = claimPhone, + onValueChange = { claimPhone = it }, + label = { Text(stringResource(Res.string.documents_form_claim_phone)) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone), + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = claimEmail, + onValueChange = { claimEmail = it }, + label = { Text(stringResource(Res.string.documents_form_claim_email)) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email), + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = claimWebsite, + onValueChange = { claimWebsite = it }, + label = { Text(stringResource(Res.string.documents_form_claim_website)) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri), + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = purchaseDate, + onValueChange = { purchaseDate = it }, + label = { Text(stringResource(Res.string.documents_form_purchase_date)) }, + placeholder = { Text(stringResource(Res.string.documents_form_date_placeholder)) }, + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = startDate, + onValueChange = { startDate = it }, + label = { Text(stringResource(Res.string.documents_form_warranty_start)) }, + placeholder = { Text(stringResource(Res.string.documents_form_date_placeholder)) }, + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = endDate, + onValueChange = { endDate = it }, + label = { Text(stringResource(Res.string.documents_form_warranty_end_required)) }, + placeholder = { Text(stringResource(Res.string.documents_form_date_placeholder_end)) }, + modifier = Modifier.fillMaxWidth() + ) + } + + // Description + OutlinedTextField( + value = description, + onValueChange = { description = it }, + label = { Text(stringResource(Res.string.documents_form_description)) }, + minLines = 3, + modifier = Modifier.fillMaxWidth() + ) + + // Category Dropdown (for warranties and some documents) + if (isWarranty || selectedDocumentType in listOf("inspection", "manual", "receipt")) { + ExposedDropdownMenuBox( + expanded = categoryExpanded, + onExpandedChange = { categoryExpanded = it } + ) { + OutlinedTextField( + value = selectedCategory?.let { DocumentCategory.fromValue(it).displayName } ?: stringResource(Res.string.documents_form_select_category), + onValueChange = {}, + readOnly = true, + label = { Text(stringResource(Res.string.documents_form_category)) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = categoryExpanded) }, + modifier = Modifier.fillMaxWidth().menuAnchor() + ) + ExposedDropdownMenu( + expanded = categoryExpanded, + onDismissRequest = { categoryExpanded = false } + ) { + DropdownMenuItem( + text = { Text(stringResource(Res.string.documents_form_category_none)) }, + onClick = { + selectedCategory = null categoryExpanded = false } ) + DocumentCategory.values().forEach { category -> + DropdownMenuItem( + text = { Text(category.displayName) }, + onClick = { + selectedCategory = category.value + categoryExpanded = false + } + ) + } } } } - } - // Tags - OutlinedTextField( - value = tags, - onValueChange = { tags = it }, - label = { Text(stringResource(Res.string.documents_form_tags)) }, - placeholder = { Text(stringResource(Res.string.documents_form_tags_placeholder)) }, - modifier = Modifier.fillMaxWidth() - ) + // Tags + OutlinedTextField( + value = tags, + onValueChange = { tags = it }, + label = { Text(stringResource(Res.string.documents_form_tags)) }, + placeholder = { Text(stringResource(Res.string.documents_form_tags_placeholder)) }, + modifier = Modifier.fillMaxWidth() + ) - // Notes - OutlinedTextField( - value = notes, - onValueChange = { notes = it }, - label = { Text(stringResource(Res.string.documents_form_notes)) }, - minLines = 3, - modifier = Modifier.fillMaxWidth() - ) + // Notes + OutlinedTextField( + value = notes, + onValueChange = { notes = it }, + label = { Text(stringResource(Res.string.documents_form_notes)) }, + minLines = 3, + modifier = Modifier.fillMaxWidth() + ) - // Active toggle (edit mode only) - if (isEditMode) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text(stringResource(Res.string.documents_form_active)) - Switch( - checked = isActive, - onCheckedChange = { isActive = it } - ) + // Active toggle (edit mode only) + if (isEditMode) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text(stringResource(Res.string.documents_form_active)) + Switch( + checked = isActive, + onCheckedChange = { isActive = it } + ) + } } - } - // Existing images (edit mode only) - if (isEditMode && existingImages.isNotEmpty()) { - Card( + // Existing images (edit mode only) + if (isEditMode && existingImages.isNotEmpty()) { + OrganicCard( + modifier = Modifier.fillMaxWidth(), + showBlob = false + ) { + Column( + modifier = Modifier.padding(OrganicSpacing.cozy), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + stringResource(Res.string.documents_form_existing_photos, existingImages.size), + style = MaterialTheme.typography.titleSmall + ) + + existingImages.forEach { image -> + AuthenticatedImage( + mediaUrl = image.mediaUrl, + contentDescription = image.caption, + modifier = Modifier + .fillMaxWidth() + .height(200.dp) + .clip(RoundedCornerShape(8.dp)) + .border(1.dp, MaterialTheme.colorScheme.outline, RoundedCornerShape(8.dp)), + contentScale = ContentScale.Crop + ) + } + } + } + } + + // Image upload section + OrganicCard( modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) + showBlob = false ) { Column( - modifier = Modifier.padding(16.dp), + modifier = Modifier.padding(OrganicSpacing.cozy), verticalArrangement = Arrangement.spacedBy(12.dp) ) { Text( - stringResource(Res.string.documents_form_existing_photos, existingImages.size), + if (isEditMode) { + stringResource(Res.string.documents_form_new_photos, selectedImages.size, maxImages) + } else { + stringResource(Res.string.documents_form_photos, selectedImages.size, maxImages) + }, style = MaterialTheme.typography.titleSmall ) - existingImages.forEach { image -> - AuthenticatedImage( - mediaUrl = image.mediaUrl, - contentDescription = image.caption, - modifier = Modifier - .fillMaxWidth() - .height(200.dp) - .clip(RoundedCornerShape(8.dp)) - .border(1.dp, MaterialTheme.colorScheme.outline, RoundedCornerShape(8.dp)), - contentScale = ContentScale.Crop - ) - } - } - } - } - - // Image upload section - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - Text( - if (isEditMode) { - stringResource(Res.string.documents_form_new_photos, selectedImages.size, maxImages) - } else { - stringResource(Res.string.documents_form_photos, selectedImages.size, maxImages) - }, - style = MaterialTheme.typography.titleSmall - ) - - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Button( - onClick = { cameraPicker() }, - modifier = Modifier.weight(1f), - enabled = selectedImages.size < maxImages + Row( + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact) ) { - Icon(Icons.Default.CameraAlt, null, modifier = Modifier.size(18.dp)) - Spacer(modifier = Modifier.width(4.dp)) - Text(stringResource(Res.string.documents_form_camera)) + Button( + onClick = { cameraPicker() }, + modifier = Modifier.weight(1f), + enabled = selectedImages.size < maxImages + ) { + Icon(Icons.Default.CameraAlt, null, modifier = Modifier.size(18.dp)) + Spacer(modifier = Modifier.width(4.dp)) + Text(stringResource(Res.string.documents_form_camera)) + } + + Button( + onClick = { imagePicker() }, + modifier = Modifier.weight(1f), + enabled = selectedImages.size < maxImages + ) { + Icon(Icons.Default.Photo, null, modifier = Modifier.size(18.dp)) + Spacer(modifier = Modifier.width(4.dp)) + Text(stringResource(Res.string.documents_form_gallery)) + } } - Button( - onClick = { imagePicker() }, - modifier = Modifier.weight(1f), - enabled = selectedImages.size < maxImages - ) { - Icon(Icons.Default.Photo, null, modifier = Modifier.size(18.dp)) - Spacer(modifier = Modifier.width(4.dp)) - Text(stringResource(Res.string.documents_form_gallery)) - } - } - - // Display selected images - if (selectedImages.isNotEmpty()) { - Column( - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - selectedImages.forEachIndexed { index, image -> - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { + // Display selected images + if (selectedImages.isNotEmpty()) { + Column( + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact) + ) { + selectedImages.forEachIndexed { index, image -> Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Icon( - Icons.Default.Image, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - Text( - stringResource(Res.string.documents_form_image_number, index + 1), - style = MaterialTheme.typography.bodyMedium - ) - } - IconButton( - onClick = { - selectedImages = selectedImages.filter { it != image } + Row( + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Default.Image, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + Text( + stringResource(Res.string.documents_form_image_number, index + 1), + style = MaterialTheme.typography.bodyMedium + ) + } + IconButton( + onClick = { + selectedImages = selectedImages.filter { it != image } + } + ) { + Icon( + Icons.Default.Close, + contentDescription = stringResource(Res.string.documents_form_remove_image), + tint = MaterialTheme.colorScheme.error + ) } - ) { - Icon( - Icons.Default.Close, - contentDescription = stringResource(Res.string.documents_form_remove_image), - tint = MaterialTheme.colorScheme.error - ) } } } } } } - } - // Error message - if (operationState is ApiResult.Error) { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.errorContainer - ) - ) { - Text( - com.example.casera.util.ErrorMessageParser.parse((operationState as ApiResult.Error).message), - modifier = Modifier.padding(12.dp), - color = MaterialTheme.colorScheme.onErrorContainer - ) + // Error message + if (operationState is ApiResult.Error) { + OrganicCard( + modifier = Modifier.fillMaxWidth(), + showBlob = false + ) { + Text( + com.example.casera.util.ErrorMessageParser.parse((operationState as ApiResult.Error).message), + modifier = Modifier.padding(12.dp), + color = MaterialTheme.colorScheme.error + ) + } } - } - // Error messages (need to be defined outside onClick) - val selectResidenceError = stringResource(Res.string.documents_form_select_residence_error) - val titleRequiredError = stringResource(Res.string.documents_form_title_error) - val itemRequiredError = stringResource(Res.string.documents_form_item_name_error) - val providerRequiredError = stringResource(Res.string.documents_form_provider_error) + // Error messages (need to be defined outside onClick) + val selectResidenceError = stringResource(Res.string.documents_form_select_residence_error) + val titleRequiredError = stringResource(Res.string.documents_form_title_error) + val itemRequiredError = stringResource(Res.string.documents_form_item_name_error) + val providerRequiredError = stringResource(Res.string.documents_form_provider_error) - // Save Button - Button( - onClick = { - // Validate - var hasError = false + // Save Button + OrganicPrimaryButton( + text = when { + isEditMode && isWarranty -> stringResource(Res.string.documents_form_update_warranty) + isEditMode -> stringResource(Res.string.documents_form_update_document) + isWarranty -> stringResource(Res.string.documents_form_add_warranty) + else -> stringResource(Res.string.documents_form_add_document) + }, + onClick = { + // Validate + var hasError = false - // Determine the actual residenceId to use - val actualResidenceId = if (needsResidenceSelection) { - if (selectedResidence == null) { - residenceError = selectResidenceError - hasError = true - -1 + // Determine the actual residenceId to use + val actualResidenceId = if (needsResidenceSelection) { + if (selectedResidence == null) { + residenceError = selectResidenceError + hasError = true + -1 + } else { + selectedResidence!!.id + } } else { - selectedResidence!!.id + residenceId ?: -1 } - } else { - residenceId ?: -1 - } - if (title.isBlank()) { - titleError = titleRequiredError - hasError = true - } - - if (isWarranty) { - if (itemName.isBlank()) { - itemNameError = itemRequiredError + if (title.isBlank()) { + titleError = titleRequiredError hasError = true } - if (provider.isBlank()) { - providerError = providerRequiredError - hasError = true - } - } - if (!hasError) { - if (isEditMode && existingDocumentId != null) { - documentViewModel.updateDocument( - id = existingDocumentId, - title = title, - documentType = selectedDocumentType, - description = description.ifBlank { null }, - category = selectedCategory, - tags = tags.ifBlank { null }, - notes = notes.ifBlank { null }, - isActive = isActive, - itemName = if (isWarranty) itemName else null, - modelNumber = modelNumber.ifBlank { null }, - serialNumber = serialNumber.ifBlank { null }, - provider = if (isWarranty) provider else null, - providerContact = providerContact.ifBlank { null }, - claimPhone = claimPhone.ifBlank { null }, - claimEmail = claimEmail.ifBlank { null }, - claimWebsite = claimWebsite.ifBlank { null }, - purchaseDate = purchaseDate.ifBlank { null }, - startDate = startDate.ifBlank { null }, - endDate = endDate.ifBlank { null }, - images = selectedImages - ) - } else { - documentViewModel.createDocument( - title = title, - documentType = selectedDocumentType, - residenceId = actualResidenceId, - description = description.ifBlank { null }, - category = selectedCategory, - tags = tags.ifBlank { null }, - notes = notes.ifBlank { null }, - contractorId = null, - isActive = true, - itemName = if (isWarranty) itemName else null, - modelNumber = modelNumber.ifBlank { null }, - serialNumber = serialNumber.ifBlank { null }, - provider = if (isWarranty) provider else null, - providerContact = providerContact.ifBlank { null }, - claimPhone = claimPhone.ifBlank { null }, - claimEmail = claimEmail.ifBlank { null }, - claimWebsite = claimWebsite.ifBlank { null }, - purchaseDate = purchaseDate.ifBlank { null }, - startDate = startDate.ifBlank { null }, - endDate = endDate.ifBlank { null }, - images = selectedImages - ) + if (isWarranty) { + if (itemName.isBlank()) { + itemNameError = itemRequiredError + hasError = true + } + if (provider.isBlank()) { + providerError = providerRequiredError + hasError = true + } } - } - }, - modifier = Modifier.fillMaxWidth(), - enabled = operationState !is ApiResult.Loading - ) { - if (operationState is ApiResult.Loading) { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - color = MaterialTheme.colorScheme.onPrimary - ) - } else { - Text( - when { - isEditMode && isWarranty -> stringResource(Res.string.documents_form_update_warranty) - isEditMode -> stringResource(Res.string.documents_form_update_document) - isWarranty -> stringResource(Res.string.documents_form_add_warranty) - else -> stringResource(Res.string.documents_form_add_document) + + if (!hasError) { + if (isEditMode && existingDocumentId != null) { + documentViewModel.updateDocument( + id = existingDocumentId, + title = title, + documentType = selectedDocumentType, + description = description.ifBlank { null }, + category = selectedCategory, + tags = tags.ifBlank { null }, + notes = notes.ifBlank { null }, + isActive = isActive, + itemName = if (isWarranty) itemName else null, + modelNumber = modelNumber.ifBlank { null }, + serialNumber = serialNumber.ifBlank { null }, + provider = if (isWarranty) provider else null, + providerContact = providerContact.ifBlank { null }, + claimPhone = claimPhone.ifBlank { null }, + claimEmail = claimEmail.ifBlank { null }, + claimWebsite = claimWebsite.ifBlank { null }, + purchaseDate = purchaseDate.ifBlank { null }, + startDate = startDate.ifBlank { null }, + endDate = endDate.ifBlank { null }, + images = selectedImages + ) + } else { + documentViewModel.createDocument( + title = title, + documentType = selectedDocumentType, + residenceId = actualResidenceId, + description = description.ifBlank { null }, + category = selectedCategory, + tags = tags.ifBlank { null }, + notes = notes.ifBlank { null }, + contractorId = null, + isActive = true, + itemName = if (isWarranty) itemName else null, + modelNumber = modelNumber.ifBlank { null }, + serialNumber = serialNumber.ifBlank { null }, + provider = if (isWarranty) provider else null, + providerContact = providerContact.ifBlank { null }, + claimPhone = claimPhone.ifBlank { null }, + claimEmail = claimEmail.ifBlank { null }, + claimWebsite = claimWebsite.ifBlank { null }, + purchaseDate = purchaseDate.ifBlank { null }, + startDate = startDate.ifBlank { null }, + endDate = endDate.ifBlank { null }, + images = selectedImages + ) + } } - ) - } + }, + enabled = operationState !is ApiResult.Loading, + isLoading = operationState is ApiResult.Loading + ) } } } diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/DocumentsScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/DocumentsScreen.kt index 9274c0d..4c04ced 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/DocumentsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/DocumentsScreen.kt @@ -18,6 +18,7 @@ import com.example.casera.viewmodel.DocumentViewModel import com.example.casera.models.* import com.example.casera.analytics.PostHogAnalytics import com.example.casera.analytics.AnalyticsEvents +import com.example.casera.ui.theme.* import casera.composeapp.generated.resources.* import org.jetbrains.compose.resources.stringResource @@ -120,7 +121,7 @@ fun DocumentsScreen( showFiltersMenu = false } ) - Divider() + OrganicDivider() DocumentCategory.values().forEach { category -> DropdownMenuItem( text = { Text(category.displayName) }, @@ -138,7 +139,7 @@ fun DocumentsScreen( showFiltersMenu = false } ) - Divider() + OrganicDivider() DocumentType.values().forEach { type -> DropdownMenuItem( text = { Text(type.displayName) }, @@ -199,31 +200,33 @@ fun DocumentsScreen( } } ) { padding -> - Box( - modifier = Modifier - .fillMaxSize() - .padding(padding) - ) { - if (isBlocked.allowed) { - // Screen is blocked (limit=0) - show upgrade prompt - UpgradeFeatureScreen( - triggerKey = isBlocked.triggerKey ?: "view_documents", - icon = Icons.Default.Description, - onNavigateBack = onNavigateBack - ) - } else { - // Pro users see normal content - use client-side filtered documents - DocumentsTabContent( - state = documentsState, - filteredDocuments = filteredDocuments, - isWarrantyTab = selectedTab == DocumentTab.WARRANTIES, - onDocumentClick = onNavigateToDocumentDetail, - onRetry = { - // Reload all documents on pull-to-refresh - documentViewModel.loadAllDocuments(residenceId = residenceId) - }, - onNavigateBack = onNavigateBack - ) + WarmGradientBackground { + Box( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + if (isBlocked.allowed) { + // Screen is blocked (limit=0) - show upgrade prompt + UpgradeFeatureScreen( + triggerKey = isBlocked.triggerKey ?: "view_documents", + icon = Icons.Default.Description, + onNavigateBack = onNavigateBack + ) + } else { + // Pro users see normal content - use client-side filtered documents + DocumentsTabContent( + state = documentsState, + filteredDocuments = filteredDocuments, + isWarrantyTab = selectedTab == DocumentTab.WARRANTIES, + onDocumentClick = onNavigateToDocumentDetail, + onRetry = { + // Reload all documents on pull-to-refresh + documentViewModel.loadAllDocuments(residenceId = residenceId) + }, + onNavigateBack = onNavigateBack + ) + } } } } diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/EditTaskScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/EditTaskScreen.kt index 3cec129..8691e8c 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/EditTaskScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/EditTaskScreen.kt @@ -17,6 +17,7 @@ import com.example.casera.viewmodel.ResidenceViewModel import com.example.casera.repository.LookupsRepository import com.example.casera.models.* import com.example.casera.network.ApiResult +import com.example.casera.ui.theme.* import casera.composeapp.generated.resources.* import org.jetbrains.compose.resources.stringResource @@ -103,239 +104,233 @@ fun EditTaskScreen( ) } ) { paddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - .padding(16.dp) - .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - // Required fields section - Text( - text = stringResource(Res.string.tasks_details), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary - ) - - OutlinedTextField( - value = title, - onValueChange = { title = it }, - label = { Text(stringResource(Res.string.tasks_title_required)) }, - modifier = Modifier.fillMaxWidth(), - isError = titleError.isNotEmpty(), - supportingText = if (titleError.isNotEmpty()) { - { Text(titleError) } - } else null - ) - - OutlinedTextField( - value = description, - onValueChange = { description = it }, - label = { Text(stringResource(Res.string.tasks_description_label)) }, - modifier = Modifier.fillMaxWidth(), - minLines = 3, - maxLines = 5 - ) - - // Category dropdown - ExposedDropdownMenuBox( - expanded = categoryExpanded, - onExpandedChange = { categoryExpanded = it } + WarmGradientBackground { + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(OrganicSpacing.cozy) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy) ) { - OutlinedTextField( - value = selectedCategory?.name?.replaceFirstChar { it.uppercase() } ?: "", - onValueChange = {}, - readOnly = true, - label = { Text(stringResource(Res.string.tasks_category_required)) }, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = categoryExpanded) }, - modifier = Modifier - .fillMaxWidth() - .menuAnchor(), - enabled = categories.isNotEmpty() + // Required fields section + Text( + text = stringResource(Res.string.tasks_details), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary ) - ExposedDropdownMenu( - expanded = categoryExpanded, - onDismissRequest = { categoryExpanded = false } - ) { - categories.forEach { category -> - DropdownMenuItem( - text = { Text(category.name.replaceFirstChar { it.uppercase() }) }, - onClick = { - selectedCategory = category - categoryExpanded = false - } - ) - } - } - } - // Frequency dropdown - ExposedDropdownMenuBox( - expanded = frequencyExpanded, - onExpandedChange = { frequencyExpanded = it } - ) { OutlinedTextField( - value = selectedFrequency?.name?.replaceFirstChar { it.uppercase() } ?: "", - onValueChange = {}, - readOnly = true, - label = { Text(stringResource(Res.string.tasks_frequency_required)) }, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = frequencyExpanded) }, - modifier = Modifier - .fillMaxWidth() - .menuAnchor(), - enabled = frequencies.isNotEmpty() - ) - ExposedDropdownMenu( - expanded = frequencyExpanded, - onDismissRequest = { frequencyExpanded = false } - ) { - frequencies.forEach { frequency -> - DropdownMenuItem( - text = { Text(frequency.name.replaceFirstChar { it.uppercase() }) }, - onClick = { - selectedFrequency = frequency - frequencyExpanded = false - // Clear custom interval if not Custom frequency - if (!frequency.name.equals("Custom", ignoreCase = true)) { - customIntervalDays = "" - } - } - ) - } - } - } - - // Custom Interval Days (only for "Custom" frequency) - if (selectedFrequency?.name?.equals("Custom", ignoreCase = true) == true) { - OutlinedTextField( - value = customIntervalDays, - onValueChange = { customIntervalDays = it.filter { char -> char.isDigit() } }, - label = { Text(stringResource(Res.string.tasks_interval_days)) }, + value = title, + onValueChange = { title = it }, + label = { Text(stringResource(Res.string.tasks_title_required)) }, modifier = Modifier.fillMaxWidth(), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - supportingText = { Text(stringResource(Res.string.tasks_custom_interval_help)) }, - singleLine = true + isError = titleError.isNotEmpty(), + supportingText = if (titleError.isNotEmpty()) { + { Text(titleError) } + } else null ) - } - // Priority dropdown - ExposedDropdownMenuBox( - expanded = priorityExpanded, - onExpandedChange = { priorityExpanded = it } - ) { OutlinedTextField( - value = selectedPriority?.name?.replaceFirstChar { it.uppercase() } ?: "", - onValueChange = {}, - readOnly = true, - label = { Text(stringResource(Res.string.tasks_priority_required)) }, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = priorityExpanded) }, - modifier = Modifier - .fillMaxWidth() - .menuAnchor(), - enabled = priorities.isNotEmpty() + value = description, + onValueChange = { description = it }, + label = { Text(stringResource(Res.string.tasks_description_label)) }, + modifier = Modifier.fillMaxWidth(), + minLines = 3, + maxLines = 5 ) - ExposedDropdownMenu( - expanded = priorityExpanded, - onDismissRequest = { priorityExpanded = false } + + // Category dropdown + ExposedDropdownMenuBox( + expanded = categoryExpanded, + onExpandedChange = { categoryExpanded = it } ) { - priorities.forEach { priority -> - DropdownMenuItem( - text = { Text(priority.name.replaceFirstChar { it.uppercase() }) }, - onClick = { - selectedPriority = priority - priorityExpanded = false - } - ) - } - } - } - - // In Progress toggle - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = androidx.compose.ui.Alignment.CenterVertically - ) { - Text( - text = stringResource(Res.string.tasks_in_progress_label), - style = MaterialTheme.typography.bodyLarge - ) - Switch( - checked = inProgress, - onCheckedChange = { inProgress = it } - ) - } - - OutlinedTextField( - value = dueDate, - onValueChange = { dueDate = it }, - label = { Text(stringResource(Res.string.tasks_due_date_required)) }, - modifier = Modifier.fillMaxWidth(), - isError = dueDateError.isNotEmpty(), - supportingText = if (dueDateError.isNotEmpty()) { - { Text(dueDateError) } - } else null, - placeholder = { Text(stringResource(Res.string.tasks_due_date_placeholder)) } - ) - - OutlinedTextField( - value = estimatedCost, - onValueChange = { estimatedCost = it }, - label = { Text(stringResource(Res.string.tasks_estimated_cost_label)) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), - modifier = Modifier.fillMaxWidth(), - prefix = { Text("$") } - ) - - // Error message - if (updateTaskState is ApiResult.Error) { - Text( - text = com.example.casera.util.ErrorMessageParser.parse((updateTaskState as ApiResult.Error).message), - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall - ) - } - - // Submit button - Button( - onClick = { - if (validateForm() && selectedCategory != null && - selectedFrequency != null && selectedPriority != null) { - viewModel.updateTask( - taskId = task.id, - request = TaskCreateRequest( - residenceId = task.residenceId, - title = title, - description = description.ifBlank { null }, - categoryId = selectedCategory!!.id, - frequencyId = selectedFrequency!!.id, - customIntervalDays = if (selectedFrequency?.name?.equals("Custom", ignoreCase = true) == true && customIntervalDays.isNotBlank()) { - customIntervalDays.toIntOrNull() - } else null, - priorityId = selectedPriority!!.id, - inProgress = inProgress, - dueDate = dueDate, - estimatedCost = estimatedCost.ifBlank { null }?.toDoubleOrNull() - ) - ) - } - }, - modifier = Modifier.fillMaxWidth(), - enabled = validateForm() && selectedCategory != null && - selectedFrequency != null && selectedPriority != null - ) { - if (updateTaskState is ApiResult.Loading) { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - color = MaterialTheme.colorScheme.onPrimary + OutlinedTextField( + value = selectedCategory?.name?.replaceFirstChar { it.uppercase() } ?: "", + onValueChange = {}, + readOnly = true, + label = { Text(stringResource(Res.string.tasks_category_required)) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = categoryExpanded) }, + modifier = Modifier + .fillMaxWidth() + .menuAnchor(), + enabled = categories.isNotEmpty() ) - } else { - Text(stringResource(Res.string.tasks_update)) + ExposedDropdownMenu( + expanded = categoryExpanded, + onDismissRequest = { categoryExpanded = false } + ) { + categories.forEach { category -> + DropdownMenuItem( + text = { Text(category.name.replaceFirstChar { it.uppercase() }) }, + onClick = { + selectedCategory = category + categoryExpanded = false + } + ) + } + } } - } - Spacer(modifier = Modifier.height(16.dp)) + // Frequency dropdown + ExposedDropdownMenuBox( + expanded = frequencyExpanded, + onExpandedChange = { frequencyExpanded = it } + ) { + OutlinedTextField( + value = selectedFrequency?.name?.replaceFirstChar { it.uppercase() } ?: "", + onValueChange = {}, + readOnly = true, + label = { Text(stringResource(Res.string.tasks_frequency_required)) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = frequencyExpanded) }, + modifier = Modifier + .fillMaxWidth() + .menuAnchor(), + enabled = frequencies.isNotEmpty() + ) + ExposedDropdownMenu( + expanded = frequencyExpanded, + onDismissRequest = { frequencyExpanded = false } + ) { + frequencies.forEach { frequency -> + DropdownMenuItem( + text = { Text(frequency.name.replaceFirstChar { it.uppercase() }) }, + onClick = { + selectedFrequency = frequency + frequencyExpanded = false + // Clear custom interval if not Custom frequency + if (!frequency.name.equals("Custom", ignoreCase = true)) { + customIntervalDays = "" + } + } + ) + } + } + } + + // Custom Interval Days (only for "Custom" frequency) + if (selectedFrequency?.name?.equals("Custom", ignoreCase = true) == true) { + OutlinedTextField( + value = customIntervalDays, + onValueChange = { customIntervalDays = it.filter { char -> char.isDigit() } }, + label = { Text(stringResource(Res.string.tasks_interval_days)) }, + modifier = Modifier.fillMaxWidth(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + supportingText = { Text(stringResource(Res.string.tasks_custom_interval_help)) }, + singleLine = true + ) + } + + // Priority dropdown + ExposedDropdownMenuBox( + expanded = priorityExpanded, + onExpandedChange = { priorityExpanded = it } + ) { + OutlinedTextField( + value = selectedPriority?.name?.replaceFirstChar { it.uppercase() } ?: "", + onValueChange = {}, + readOnly = true, + label = { Text(stringResource(Res.string.tasks_priority_required)) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = priorityExpanded) }, + modifier = Modifier + .fillMaxWidth() + .menuAnchor(), + enabled = priorities.isNotEmpty() + ) + ExposedDropdownMenu( + expanded = priorityExpanded, + onDismissRequest = { priorityExpanded = false } + ) { + priorities.forEach { priority -> + DropdownMenuItem( + text = { Text(priority.name.replaceFirstChar { it.uppercase() }) }, + onClick = { + selectedPriority = priority + priorityExpanded = false + } + ) + } + } + } + + // In Progress toggle + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = androidx.compose.ui.Alignment.CenterVertically + ) { + Text( + text = stringResource(Res.string.tasks_in_progress_label), + style = MaterialTheme.typography.bodyLarge + ) + Switch( + checked = inProgress, + onCheckedChange = { inProgress = it } + ) + } + + OutlinedTextField( + value = dueDate, + onValueChange = { dueDate = it }, + label = { Text(stringResource(Res.string.tasks_due_date_required)) }, + modifier = Modifier.fillMaxWidth(), + isError = dueDateError.isNotEmpty(), + supportingText = if (dueDateError.isNotEmpty()) { + { Text(dueDateError) } + } else null, + placeholder = { Text(stringResource(Res.string.tasks_due_date_placeholder)) } + ) + + OutlinedTextField( + value = estimatedCost, + onValueChange = { estimatedCost = it }, + label = { Text(stringResource(Res.string.tasks_estimated_cost_label)) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + modifier = Modifier.fillMaxWidth(), + prefix = { Text("$") } + ) + + // Error message + if (updateTaskState is ApiResult.Error) { + Text( + text = com.example.casera.util.ErrorMessageParser.parse((updateTaskState as ApiResult.Error).message), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall + ) + } + + // Submit button + OrganicPrimaryButton( + text = stringResource(Res.string.tasks_update), + onClick = { + if (validateForm() && selectedCategory != null && + selectedFrequency != null && selectedPriority != null) { + viewModel.updateTask( + taskId = task.id, + request = TaskCreateRequest( + residenceId = task.residenceId, + title = title, + description = description.ifBlank { null }, + categoryId = selectedCategory!!.id, + frequencyId = selectedFrequency!!.id, + customIntervalDays = if (selectedFrequency?.name?.equals("Custom", ignoreCase = true) == true && customIntervalDays.isNotBlank()) { + customIntervalDays.toIntOrNull() + } else null, + priorityId = selectedPriority!!.id, + inProgress = inProgress, + dueDate = dueDate, + estimatedCost = estimatedCost.ifBlank { null }?.toDoubleOrNull() + ) + ) + } + }, + enabled = validateForm() && selectedCategory != null && + selectedFrequency != null && selectedPriority != null, + isLoading = updateTaskState is ApiResult.Loading + ) + + Spacer(modifier = Modifier.height(OrganicSpacing.cozy)) + } } } } diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ForgotPasswordScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ForgotPasswordScreen.kt index d3a7d1a..dacacd7 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ForgotPasswordScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ForgotPasswordScreen.kt @@ -1,8 +1,6 @@ package com.example.casera.ui.screens -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material3.* @@ -18,6 +16,7 @@ import com.example.casera.ui.components.auth.AuthHeader import com.example.casera.ui.components.common.ErrorCard import com.example.casera.viewmodel.PasswordResetViewModel import com.example.casera.network.ApiResult +import com.example.casera.ui.theme.* import casera.composeapp.generated.resources.* import org.jetbrains.compose.resources.stringResource @@ -74,128 +73,127 @@ fun ForgotPasswordScreen( ) } ) { paddingValues -> - Box( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .padding(paddingValues), - contentAlignment = Alignment.Center - ) { - Card( + WarmGradientBackground { + Box( modifier = Modifier - .fillMaxWidth(0.9f) - .wrapContentHeight(), - shape = RoundedCornerShape(24.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ), - elevation = CardDefaults.cardElevation(defaultElevation = 8.dp) + .fillMaxSize() + .padding(paddingValues), + contentAlignment = Alignment.Center ) { - Column( + OrganicCard( modifier = Modifier - .fillMaxWidth() - .padding(32.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(20.dp) + .fillMaxWidth(0.9f) + .wrapContentHeight(), + showBlob = true, + blobVariation = 0 ) { - AuthHeader( - icon = Icons.Default.Key, - title = stringResource(Res.string.auth_forgot_title), - subtitle = stringResource(Res.string.auth_forgot_subtitle) - ) - - Spacer(modifier = Modifier.height(8.dp)) - - OutlinedTextField( - value = email, - onValueChange = { - email = it - viewModel.resetForgotPasswordState() - }, - label = { Text(stringResource(Res.string.auth_forgot_email_label)) }, - leadingIcon = { - Icon(Icons.Default.Email, contentDescription = null) - }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - shape = RoundedCornerShape(12.dp), - enabled = !isLoading - ) - - Text( - "We'll send a 6-digit verification code to this address", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) - - ErrorCard(message = errorMessage) - - if (isSuccess) { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer - ) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - Icons.Default.CheckCircle, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(12.dp)) - Text( - "Check your email for a 6-digit verification code", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onPrimaryContainer - ) - } - } - } - - Button( - onClick = { - viewModel.setEmail(email) - viewModel.requestPasswordReset(email) - }, + Column( modifier = Modifier .fillMaxWidth() - .height(56.dp), - enabled = email.isNotEmpty() && !isLoading, - shape = RoundedCornerShape(12.dp) + .padding(OrganicSpacing.spacious), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy) ) { - if (isLoading) { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - color = MaterialTheme.colorScheme.onPrimary, - strokeWidth = 2.dp - ) - } else { - Icon(Icons.Default.Send, contentDescription = null) - Spacer(modifier = Modifier.width(8.dp)) + OrganicIconContainer( + icon = Icons.Default.Key, + size = 80.dp, + iconScale = 0.5f, + backgroundColor = MaterialTheme.colorScheme.primary, + iconColor = MaterialTheme.colorScheme.onPrimary + ) + + Text( + text = stringResource(Res.string.auth_forgot_title), + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.textPrimary, + textAlign = TextAlign.Center + ) + + Text( + text = stringResource(Res.string.auth_forgot_subtitle), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.textSecondary, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(OrganicSpacing.compact)) + + OutlinedTextField( + value = email, + onValueChange = { + email = it + viewModel.resetForgotPasswordState() + }, + label = { Text(stringResource(Res.string.auth_forgot_email_label)) }, + leadingIcon = { + Icon(Icons.Default.Email, contentDescription = null) + }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + enabled = !isLoading + ) + + Text( + "We'll send a 6-digit verification code to this address", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.textSecondary, + textAlign = TextAlign.Center + ) + + ErrorCard(message = errorMessage) + + if (isSuccess) { + OrganicCard( + modifier = Modifier.fillMaxWidth(), + accentColor = MaterialTheme.colorScheme.primary, + showBlob = false + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(OrganicSpacing.cozy), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact) + ) { + Icon( + Icons.Default.CheckCircle, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + Text( + "Check your email for a 6-digit verification code", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.textPrimary + ) + } + } + } + + OrganicDivider( + modifier = Modifier.fillMaxWidth() + ) + + OrganicPrimaryButton( + text = stringResource(Res.string.auth_forgot_button), + onClick = { + viewModel.setEmail(email) + viewModel.requestPasswordReset(email) + }, + enabled = email.isNotEmpty() && !isLoading, + isLoading = isLoading + ) + + TextButton( + onClick = onNavigateBack, + modifier = Modifier.fillMaxWidth() + ) { Text( - stringResource(Res.string.auth_forgot_button), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold + "Remember your password? Back to Login", + style = MaterialTheme.typography.bodyMedium ) } } - - TextButton( - onClick = onNavigateBack, - modifier = Modifier.fillMaxWidth() - ) { - Text( - "Remember your password? Back to Login", - style = MaterialTheme.typography.bodyMedium - ) - } } } } diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/HomeScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/HomeScreen.kt index 7ad767e..9f9fe41 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/HomeScreen.kt @@ -1,22 +1,18 @@ package com.example.casera.ui.screens -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape 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.draw.clip -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.example.casera.ui.components.HandleErrors -import com.example.casera.ui.theme.AppRadius +import com.example.casera.ui.theme.* import com.example.casera.viewmodel.ResidenceViewModel import com.example.casera.viewmodel.TaskViewModel import com.example.casera.network.ApiResult @@ -67,194 +63,145 @@ fun HomeScreen( ) } ) { paddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .padding(paddingValues) - .padding(horizontal = 16.dp, vertical = 12.dp), - verticalArrangement = Arrangement.spacedBy(20.dp) - ) { - // Personalized Greeting + WarmGradientBackground { Column( - modifier = Modifier.padding(vertical = 8.dp) + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(horizontal = OrganicSpacing.comfortable, vertical = OrganicSpacing.cozy), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.generous) ) { - Text( - text = stringResource(Res.string.home_welcome), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = stringResource(Res.string.home_manage_properties), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - // Summary Card - when (summaryState) { - is ApiResult.Success -> { - val summary = (summaryState as ApiResult.Success).data - Card( - modifier = Modifier.fillMaxWidth(), - shape = MaterialTheme.shapes.large, - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ), - elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(20.dp) + // Personalized Greeting + Column( + modifier = Modifier.padding(vertical = OrganicSpacing.cozy) + ) { + Text( + text = stringResource(Res.string.home_welcome), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = stringResource(Res.string.home_manage_properties), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + // Summary Card + when (summaryState) { + is ApiResult.Success -> { + val summary = (summaryState as ApiResult.Success).data + OrganicCard( + modifier = Modifier.fillMaxWidth(), + showBlob = true, + blobVariation = 0 ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) + Column( + modifier = Modifier.fillMaxWidth() ) { - // Gradient circular icon - Box( - modifier = Modifier - .size(44.dp) - .clip(CircleShape) - .background( - Brush.linearGradient( - listOf( - Color(0xFF2563EB), - Color(0xFF8B5CF6) - ) - ) - ), - contentAlignment = Alignment.Center + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy) ) { - Icon( - Icons.Default.Home, - contentDescription = null, - tint = Color.White, - modifier = Modifier.size(24.dp) + // Gradient circular icon + OrganicIconContainer( + icon = Icons.Default.Home, + size = 44.dp ) + Column { + Text( + text = stringResource(Res.string.home_overview), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource(Res.string.home_property_stats), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } } - Column { - Text( - text = stringResource(Res.string.home_overview), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface + + Spacer(modifier = Modifier.height(OrganicSpacing.generous)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + OrganicStatPill( + icon = Icons.Default.Home, + value = "${summary.residences.size}", + label = stringResource(Res.string.home_properties), + color = Color(0xFF3B82F6) ) - Text( - text = stringResource(Res.string.home_property_stats), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + OrganicDivider( + modifier = Modifier + .height(48.dp) + .width(1.dp), + vertical = true + ) + OrganicStatPill( + icon = Icons.Default.Task, + value = "${totalSummary?.totalTasks ?: 0}", + label = stringResource(Res.string.home_total_tasks), + color = Color(0xFF8B5CF6) + ) + OrganicDivider( + modifier = Modifier + .height(48.dp) + .width(1.dp), + vertical = true + ) + OrganicStatPill( + icon = Icons.Default.Schedule, + value = "${totalSummary?.totalPending ?: 0}", + label = stringResource(Res.string.home_pending), + color = Color(0xFFF59E0B) ) } } - - Spacer(modifier = Modifier.height(20.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly + } + } + is ApiResult.Idle, is ApiResult.Loading -> { + OrganicCard(modifier = Modifier.fillMaxWidth()) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(120.dp), + contentAlignment = Alignment.Center ) { - StatItem( - value = "${summary.residences.size}", - label = stringResource(Res.string.home_properties), - color = Color(0xFF3B82F6) - ) - Divider( - modifier = Modifier - .height(48.dp) - .width(1.dp), - color = MaterialTheme.colorScheme.outlineVariant - ) - StatItem( - value = "${totalSummary?.totalTasks ?: 0}", - label = stringResource(Res.string.home_total_tasks), - color = Color(0xFF8B5CF6) - ) - Divider( - modifier = Modifier - .height(48.dp) - .width(1.dp), - color = MaterialTheme.colorScheme.outlineVariant - ) - StatItem( - value = "${totalSummary?.totalPending ?: 0}", - label = stringResource(Res.string.home_pending), - color = Color(0xFFF59E0B) - ) + CircularProgressIndicator() } } } - } - is ApiResult.Idle, is ApiResult.Loading -> { - Card(modifier = Modifier.fillMaxWidth()) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(120.dp), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } + is ApiResult.Error -> { + // Don't show error card, just let navigation cards show } - } - is ApiResult.Error -> { - // Don't show error card, just let navigation cards show + + else -> {} } - else -> {} + // Residences Card + NavigationCard( + title = stringResource(Res.string.home_properties), + subtitle = stringResource(Res.string.home_manage_residences), + icon = Icons.Default.Home, + iconColor = Color(0xFF3B82F6), + onClick = onNavigateToResidences + ) + + // Tasks Card + NavigationCard( + title = stringResource(Res.string.home_tasks), + subtitle = stringResource(Res.string.home_view_manage_tasks), + icon = Icons.Default.CheckCircle, + iconColor = Color(0xFF10B981), + onClick = onNavigateToTasks + ) } - - // Residences Card - NavigationCard( - title = stringResource(Res.string.home_properties), - subtitle = stringResource(Res.string.home_manage_residences), - icon = Icons.Default.Home, - iconColor = Color(0xFF3B82F6), - onClick = onNavigateToResidences - ) - - // Tasks Card - NavigationCard( - title = stringResource(Res.string.home_tasks), - subtitle = stringResource(Res.string.home_view_manage_tasks), - icon = Icons.Default.CheckCircle, - iconColor = Color(0xFF10B981), - onClick = onNavigateToTasks - ) } } } -@Composable -private fun StatItem( - value: String, - label: String, - color: Color -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - Box( - modifier = Modifier - .size(36.dp) - .clip(CircleShape) - .background(color.copy(alpha = 0.1f)), - contentAlignment = Alignment.Center - ) { - Text( - text = value, - style = MaterialTheme.typography.titleLarge, - color = color - ) - } - Text( - text = label, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } -} - @Composable private fun NavigationCard( title: String, @@ -263,45 +210,24 @@ private fun NavigationCard( iconColor: Color, onClick: () -> Unit ) { - Card( + OrganicCard( modifier = Modifier .fillMaxWidth() .clickable { onClick() }, - shape = MaterialTheme.shapes.large, - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ), - elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) + showBlob = true, + blobVariation = 1 ) { Row( - modifier = Modifier - .fillMaxWidth() - .padding(20.dp), + modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(16.dp) + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.comfortable) ) { // Gradient circular icon - Box( - modifier = Modifier - .size(56.dp) - .clip(CircleShape) - .background( - Brush.linearGradient( - listOf( - iconColor, - iconColor.copy(alpha = 0.7f) - ) - ) - ), - contentAlignment = Alignment.Center - ) { - Icon( - icon, - contentDescription = null, - tint = Color.White, - modifier = Modifier.size(28.dp) - ) - } + OrganicIconContainer( + icon = icon, + size = 56.dp, + iconColor = iconColor + ) Column( modifier = Modifier.weight(1f) @@ -311,7 +237,7 @@ private fun NavigationCard( style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface ) - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(OrganicSpacing.minimal)) Text( text = subtitle, style = MaterialTheme.typography.bodyMedium, diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/LoginScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/LoginScreen.kt index 2f5f61b..2f84880 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/LoginScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/LoginScreen.kt @@ -29,6 +29,7 @@ import com.example.casera.viewmodel.AuthViewModel import com.example.casera.network.ApiResult import com.example.casera.analytics.PostHogAnalytics import com.example.casera.analytics.AnalyticsEvents +import com.example.casera.ui.theme.* import casera.composeapp.generated.resources.* import org.jetbrains.compose.resources.stringResource @@ -93,186 +94,142 @@ fun LoginScreen( val isLoading = loginState is ApiResult.Loading || googleSignInState is ApiResult.Loading - Box( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background), - contentAlignment = Alignment.Center - ) { - Card( + WarmGradientBackground { + Box( modifier = Modifier - .fillMaxWidth(0.9f) - .wrapContentHeight(), - shape = MaterialTheme.shapes.extraLarge, - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ), - elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) + .fillMaxSize(), + contentAlignment = Alignment.Center ) { - Column( + OrganicCard( modifier = Modifier - .fillMaxWidth() - .padding(32.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(20.dp) + .fillMaxWidth(0.9f) + .wrapContentHeight(), + showBlob = true, + blobVariation = 0 ) { - AuthHeader( - icon = Icons.Default.Home, - title = stringResource(Res.string.app_name), - subtitle = stringResource(Res.string.app_tagline) - ) - - Spacer(modifier = Modifier.height(8.dp)) - - OutlinedTextField( - value = username, - onValueChange = { username = it }, - label = { Text(stringResource(Res.string.auth_login_username_label)) }, - leadingIcon = { - Icon(Icons.Default.Person, contentDescription = null) - }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - shape = RoundedCornerShape(12.dp), - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Email, - imeAction = ImeAction.Next - ) - ) - - OutlinedTextField( - value = password, - onValueChange = { password = it }, - label = { Text(stringResource(Res.string.auth_login_password_label)) }, - leadingIcon = { - Icon(Icons.Default.Lock, contentDescription = null) - }, - trailingIcon = { - IconButton(onClick = { passwordVisible = !passwordVisible }) { - Icon( - imageVector = if (passwordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff, - contentDescription = if (passwordVisible) stringResource(Res.string.auth_hide_password) else stringResource(Res.string.auth_show_password) - ) - } - }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), - shape = RoundedCornerShape(12.dp) - ) - - ErrorCard(message = errorMessage) - - // Clear Google error when user starts typing - LaunchedEffect(username, password) { - googleSignInError = null - } - - // Gradient button - Box( + Column( modifier = Modifier .fillMaxWidth() - .height(56.dp) - .clip(MaterialTheme.shapes.medium) - .then( - if (username.isNotEmpty() && password.isNotEmpty() && !isLoading) { - Modifier.background( - Brush.linearGradient( - listOf( - Color(0xFF2563EB), - Color(0xFF8B5CF6) - ) - ) - ) - } else { - Modifier.background(MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f)) - } - ), - contentAlignment = Alignment.Center + .padding(OrganicSpacing.xxl), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg) ) { - Button( + AuthHeader( + icon = Icons.Default.Home, + title = stringResource(Res.string.app_name), + subtitle = stringResource(Res.string.app_tagline) + ) + + Spacer(modifier = Modifier.height(OrganicSpacing.sm)) + + OutlinedTextField( + value = username, + onValueChange = { username = it }, + label = { Text(stringResource(Res.string.auth_login_username_label)) }, + leadingIcon = { + Icon(Icons.Default.Person, contentDescription = null) + }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + shape = RoundedCornerShape(OrganicRadius.md), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Email, + imeAction = ImeAction.Next + ) + ) + + OutlinedTextField( + value = password, + onValueChange = { password = it }, + label = { Text(stringResource(Res.string.auth_login_password_label)) }, + leadingIcon = { + Icon(Icons.Default.Lock, contentDescription = null) + }, + trailingIcon = { + IconButton(onClick = { passwordVisible = !passwordVisible }) { + Icon( + imageVector = if (passwordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff, + contentDescription = if (passwordVisible) stringResource(Res.string.auth_hide_password) else stringResource(Res.string.auth_show_password) + ) + } + }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), + shape = RoundedCornerShape(OrganicRadius.md) + ) + + ErrorCard(message = errorMessage) + + // Clear Google error when user starts typing + LaunchedEffect(username, password) { + googleSignInError = null + } + + // Organic Primary Button + OrganicPrimaryButton( + text = stringResource(Res.string.auth_login_button), onClick = { viewModel.login(username, password) }, - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxWidth(), enabled = username.isNotEmpty() && password.isNotEmpty(), - shape = MaterialTheme.shapes.medium, - colors = ButtonDefaults.buttonColors( - containerColor = Color.Transparent, - disabledContainerColor = Color.Transparent - ) + isLoading = isLoading + ) + + // Divider with "or" + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically ) { - if (isLoading) { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - color = Color.White, - strokeWidth = 2.dp - ) - } else { - Text( - stringResource(Res.string.auth_login_button), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - color = Color.White - ) - } + OrganicDivider( + modifier = Modifier.weight(1f) + ) + Text( + text = "or", + modifier = Modifier.padding(horizontal = OrganicSpacing.lg), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + OrganicDivider( + modifier = Modifier.weight(1f) + ) } - } - // Divider with "or" - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - HorizontalDivider( - modifier = Modifier.weight(1f), - color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) + // Google Sign In button (only shows on Android) + GoogleSignInButton( + onSignInStarted = { + googleSignInError = null + }, + onSignInSuccess = { idToken -> + viewModel.googleSignIn(idToken) + }, + onSignInError = { error -> + googleSignInError = error + }, + enabled = !isLoading ) - Text( - text = "or", - modifier = Modifier.padding(horizontal = 16.dp), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - HorizontalDivider( - modifier = Modifier.weight(1f), - color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) - ) - } - // Google Sign In button (only shows on Android) - GoogleSignInButton( - onSignInStarted = { - googleSignInError = null - }, - onSignInSuccess = { idToken -> - viewModel.googleSignIn(idToken) - }, - onSignInError = { error -> - googleSignInError = error - }, - enabled = !isLoading - ) + TextButton( + onClick = onNavigateToForgotPassword, + modifier = Modifier.fillMaxWidth() + ) { + Text( + stringResource(Res.string.auth_forgot_password), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold + ) + } - TextButton( - onClick = onNavigateToForgotPassword, - modifier = Modifier.fillMaxWidth() - ) { - Text( - stringResource(Res.string.auth_forgot_password), - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.SemiBold - ) - } - - TextButton( - onClick = onNavigateToRegister, - modifier = Modifier.fillMaxWidth() - ) { - Text( - stringResource(Res.string.auth_no_account), - style = MaterialTheme.typography.bodyMedium - ) + TextButton( + onClick = onNavigateToRegister, + modifier = Modifier.fillMaxWidth() + ) { + Text( + stringResource(Res.string.auth_no_account), + style = MaterialTheme.typography.bodyMedium + ) + } } } } diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/MainScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/MainScreen.kt index c2a488a..ccce286 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/MainScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/MainScreen.kt @@ -17,6 +17,7 @@ import com.example.casera.models.Residence import com.example.casera.models.TaskDetail import com.example.casera.storage.TokenStorage import com.example.casera.ui.subscription.UpgradeScreen +import com.example.casera.ui.theme.* import casera.composeapp.generated.resources.* import kotlinx.serialization.json.Json import org.jetbrains.compose.resources.stringResource @@ -45,304 +46,289 @@ fun MainScreen( } } - Scaffold( - bottomBar = { - NavigationBar( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - tonalElevation = 3.dp + WarmGradientBackground { + Scaffold( + containerColor = androidx.compose.ui.graphics.Color.Transparent, + bottomBar = { + NavigationBar( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + tonalElevation = 3.dp + ) { + NavigationBarItem( + icon = { Icon(Icons.Default.Home, contentDescription = stringResource(Res.string.properties_title)) }, + label = { Text(stringResource(Res.string.properties_title)) }, + selected = selectedTab == 0, + onClick = { + selectedTab = 0 + navController.navigate(MainTabResidencesRoute) { + popUpTo(MainTabResidencesRoute) { inclusive = true } + } + }, + colors = NavigationBarItemDefaults.colors( + selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer, + selectedTextColor = MaterialTheme.colorScheme.onSecondaryContainer, + indicatorColor = MaterialTheme.colorScheme.secondaryContainer, + unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, + unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) + NavigationBarItem( + icon = { Icon(Icons.Default.CheckCircle, contentDescription = stringResource(Res.string.tasks_title)) }, + label = { Text(stringResource(Res.string.tasks_title)) }, + selected = selectedTab == 1, + onClick = { + selectedTab = 1 + navController.navigate(MainTabTasksRoute) { + popUpTo(MainTabResidencesRoute) { inclusive = false } + } + }, + colors = NavigationBarItemDefaults.colors( + selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer, + selectedTextColor = MaterialTheme.colorScheme.onSecondaryContainer, + indicatorColor = MaterialTheme.colorScheme.secondaryContainer, + unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, + unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) + NavigationBarItem( + icon = { Icon(Icons.Default.Build, contentDescription = stringResource(Res.string.contractors_title)) }, + label = { Text(stringResource(Res.string.contractors_title)) }, + selected = selectedTab == 2, + onClick = { + selectedTab = 2 + navController.navigate(MainTabContractorsRoute) { + popUpTo(MainTabResidencesRoute) { inclusive = false } + } + }, + colors = NavigationBarItemDefaults.colors( + selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer, + selectedTextColor = MaterialTheme.colorScheme.onSecondaryContainer, + indicatorColor = MaterialTheme.colorScheme.secondaryContainer, + unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, + unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) + NavigationBarItem( + icon = { Icon(Icons.Default.Description, contentDescription = stringResource(Res.string.documents_title)) }, + label = { Text(stringResource(Res.string.documents_title)) }, + selected = selectedTab == 3, + onClick = { + selectedTab = 3 + navController.navigate(MainTabDocumentsRoute) { + popUpTo(MainTabResidencesRoute) { inclusive = false } + } + }, + colors = NavigationBarItemDefaults.colors( + selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer, + selectedTextColor = MaterialTheme.colorScheme.onSecondaryContainer, + indicatorColor = MaterialTheme.colorScheme.secondaryContainer, + unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, + unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) + } + } + ) { paddingValues -> + NavHost( + navController = navController, + startDestination = MainTabResidencesRoute, + modifier = Modifier.fillMaxSize() ) { - NavigationBarItem( - icon = { Icon(Icons.Default.Home, contentDescription = stringResource(Res.string.properties_title)) }, - label = { Text(stringResource(Res.string.properties_title)) }, - selected = selectedTab == 0, - onClick = { - selectedTab = 0 - navController.navigate(MainTabResidencesRoute) { - popUpTo(MainTabResidencesRoute) { inclusive = true } + composable { + Box(modifier = Modifier.fillMaxSize()) { + ResidencesScreen( + onResidenceClick = onResidenceClick, + onAddResidence = onAddResidence, + onLogout = onLogout, + onNavigateToProfile = { + // Don't change selectedTab since Profile isn't in the bottom nav + navController.navigate(MainTabProfileRoute) } - }, - colors = NavigationBarItemDefaults.colors( - selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer, - selectedTextColor = MaterialTheme.colorScheme.onSecondaryContainer, - indicatorColor = MaterialTheme.colorScheme.secondaryContainer, - unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, - unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant ) - ) - NavigationBarItem( - icon = { Icon(Icons.Default.CheckCircle, contentDescription = stringResource(Res.string.tasks_title)) }, - label = { Text(stringResource(Res.string.tasks_title)) }, - selected = selectedTab == 1, - onClick = { - selectedTab = 1 - navController.navigate(MainTabTasksRoute) { - popUpTo(MainTabResidencesRoute) { inclusive = false } - } - }, - colors = NavigationBarItemDefaults.colors( - selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer, - selectedTextColor = MaterialTheme.colorScheme.onSecondaryContainer, - indicatorColor = MaterialTheme.colorScheme.secondaryContainer, - unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, - unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant - ) - ) - NavigationBarItem( - icon = { Icon(Icons.Default.Build, contentDescription = stringResource(Res.string.contractors_title)) }, - label = { Text(stringResource(Res.string.contractors_title)) }, - selected = selectedTab == 2, - onClick = { - selectedTab = 2 - navController.navigate(MainTabContractorsRoute) { - popUpTo(MainTabResidencesRoute) { inclusive = false } - } - }, - colors = NavigationBarItemDefaults.colors( - selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer, - selectedTextColor = MaterialTheme.colorScheme.onSecondaryContainer, - indicatorColor = MaterialTheme.colorScheme.secondaryContainer, - unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, - unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant - ) - ) - NavigationBarItem( - icon = { Icon(Icons.Default.Description, contentDescription = stringResource(Res.string.documents_title)) }, - label = { Text(stringResource(Res.string.documents_title)) }, - selected = selectedTab == 3, - onClick = { - selectedTab = 3 - navController.navigate(MainTabDocumentsRoute) { - popUpTo(MainTabResidencesRoute) { inclusive = false } - } - }, - colors = NavigationBarItemDefaults.colors( - selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer, - selectedTextColor = MaterialTheme.colorScheme.onSecondaryContainer, - indicatorColor = MaterialTheme.colorScheme.secondaryContainer, - unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, - unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant - ) - ) -// NavigationBarItem( -// icon = { Icon(Icons.Default.Person, contentDescription = "Profile") }, -// label = { Text("Profile") }, -// selected = selectedTab == 4, -// onClick = { -// selectedTab = 4 -// navController.navigate(MainTabProfileRoute) { -// popUpTo(MainTabResidencesRoute) { inclusive = false } -// } -// }, -// colors = NavigationBarItemDefaults.colors( -// selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer, -// selectedTextColor = MaterialTheme.colorScheme.onSecondaryContainer, -// indicatorColor = MaterialTheme.colorScheme.secondaryContainer, -// unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, -// unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant -// ) -// ) - } - } - ) { paddingValues -> - NavHost( - navController = navController, - startDestination = MainTabResidencesRoute, - modifier = Modifier.fillMaxSize() - ) { - composable { - Box(modifier = Modifier.fillMaxSize()) { - ResidencesScreen( - onResidenceClick = onResidenceClick, - onAddResidence = onAddResidence, - onLogout = onLogout, - onNavigateToProfile = { - // Don't change selectedTab since Profile isn't in the bottom nav - navController.navigate(MainTabProfileRoute) - } - ) - } - } - - composable { - Box(modifier = Modifier.fillMaxSize()) { - AllTasksScreen( - onNavigateToEditTask = onNavigateToEditTask, - onAddTask = onAddTask, - bottomNavBarPadding = paddingValues.calculateBottomPadding(), - navigateToTaskId = navigateToTaskId, - onClearNavigateToTask = onClearNavigateToTask, - onNavigateToCompleteTask = { task, residenceName -> - navController.navigate( - CompleteTaskRoute( - taskId = task.id, - taskTitle = task.title, - residenceName = residenceName - ) - ) - } - ) - } - } - - composable { - Box(modifier = Modifier.fillMaxSize()) { - ContractorsScreen( - onNavigateBack = { - selectedTab = 0 - navController.navigate(MainTabResidencesRoute) - }, - onNavigateToContractorDetail = { contractorId -> - navController.navigate(ContractorDetailRoute(contractorId)) - } - ) - } - } - - composable { backStackEntry -> - val route = backStackEntry.toRoute() - Box(modifier = Modifier.fillMaxSize()) { - ContractorDetailScreen( - contractorId = route.contractorId, - onNavigateBack = { - navController.popBackStack() - } - ) - } - } - - composable { - Box(modifier = Modifier.fillMaxSize()) { - DocumentsScreen( - onNavigateBack = { - selectedTab = 0 - navController.navigate(MainTabResidencesRoute) - }, - residenceId = null, - onNavigateToAddDocument = { residenceId, documentType -> - navController.navigate( - AddDocumentRoute( - residenceId = residenceId, - initialDocumentType = documentType - ) - ) - }, - onNavigateToDocumentDetail = { documentId -> - navController.navigate(DocumentDetailRoute(documentId)) - } - ) - } - } - - composable { backStackEntry -> - val route = backStackEntry.toRoute() - AddDocumentScreen( - residenceId = route.residenceId, - initialDocumentType = route.initialDocumentType, - onNavigateBack = { navController.popBackStack() }, - onDocumentCreated = { - navController.popBackStack() } - ) - } + } - composable { backStackEntry -> - val route = backStackEntry.toRoute() - DocumentDetailScreen( - documentId = route.documentId, - onNavigateBack = { navController.popBackStack() }, - onNavigateToEdit = { documentId -> - navController.navigate(EditDocumentRoute(documentId)) + composable { + Box(modifier = Modifier.fillMaxSize()) { + AllTasksScreen( + onNavigateToEditTask = onNavigateToEditTask, + onAddTask = onAddTask, + bottomNavBarPadding = paddingValues.calculateBottomPadding(), + navigateToTaskId = navigateToTaskId, + onClearNavigateToTask = onClearNavigateToTask, + onNavigateToCompleteTask = { task, residenceName -> + navController.navigate( + CompleteTaskRoute( + taskId = task.id, + taskTitle = task.title, + residenceName = residenceName + ) + ) + } + ) } - ) - } - - composable { backStackEntry -> - val route = backStackEntry.toRoute() - EditDocumentScreen( - documentId = route.documentId, - onNavigateBack = { navController.popBackStack() } - ) - } - - composable { - Box(modifier = Modifier.fillMaxSize()) { - ProfileScreen( - onNavigateBack = { - selectedTab = 0 - navController.navigate(MainTabResidencesRoute) - }, - onLogout = onLogout, - onNavigateToNotificationPreferences = { - navController.navigate(NotificationPreferencesRoute) - }, - onNavigateToUpgrade = { - navController.navigate(UpgradeRoute) - } - ) } - } - composable { - Box(modifier = Modifier.fillMaxSize()) { - NotificationPreferencesScreen( - onNavigateBack = { - navController.popBackStack() - } - ) + composable { + Box(modifier = Modifier.fillMaxSize()) { + ContractorsScreen( + onNavigateBack = { + selectedTab = 0 + navController.navigate(MainTabResidencesRoute) + }, + onNavigateToContractorDetail = { contractorId -> + navController.navigate(ContractorDetailRoute(contractorId)) + } + ) + } } - } - composable { backStackEntry -> - val route = backStackEntry.toRoute() - Box(modifier = Modifier.fillMaxSize()) { - CompleteTaskScreen( - taskId = route.taskId, - taskTitle = route.taskTitle, - residenceName = route.residenceName, - onNavigateBack = { - navController.popBackStack() - }, - onComplete = { request, images -> - // Navigation back happens in the screen after successful completion - navController.popBackStack() - } - ) + composable { backStackEntry -> + val route = backStackEntry.toRoute() + Box(modifier = Modifier.fillMaxSize()) { + ContractorDetailScreen( + contractorId = route.contractorId, + onNavigateBack = { + navController.popBackStack() + } + ) + } } - } - composable { backStackEntry -> - val route = backStackEntry.toRoute() - Box(modifier = Modifier.fillMaxSize()) { - ManageUsersScreen( + composable { + Box(modifier = Modifier.fillMaxSize()) { + DocumentsScreen( + onNavigateBack = { + selectedTab = 0 + navController.navigate(MainTabResidencesRoute) + }, + residenceId = null, + onNavigateToAddDocument = { residenceId, documentType -> + navController.navigate( + AddDocumentRoute( + residenceId = residenceId, + initialDocumentType = documentType + ) + ) + }, + onNavigateToDocumentDetail = { documentId -> + navController.navigate(DocumentDetailRoute(documentId)) + } + ) + } + } + + composable { backStackEntry -> + val route = backStackEntry.toRoute() + AddDocumentScreen( residenceId = route.residenceId, - residenceName = route.residenceName, - isPrimaryOwner = route.isPrimaryOwner, - residenceOwnerId = route.residenceOwnerId, - onNavigateBack = { + initialDocumentType = route.initialDocumentType, + onNavigateBack = { navController.popBackStack() }, + onDocumentCreated = { navController.popBackStack() - }, - onUserRemoved = { - // Could trigger a refresh if needed } ) } - } - composable { - Box(modifier = Modifier.fillMaxSize()) { - UpgradeScreen( - onNavigateBack = { - navController.popBackStack() - }, - onPurchase = { planId -> - // Handle purchase - integrate with billing system - navController.popBackStack() - }, - onRestorePurchases = { - // Handle restore - integrate with billing system + composable { backStackEntry -> + val route = backStackEntry.toRoute() + DocumentDetailScreen( + documentId = route.documentId, + onNavigateBack = { navController.popBackStack() }, + onNavigateToEdit = { documentId -> + navController.navigate(EditDocumentRoute(documentId)) } ) } + + composable { backStackEntry -> + val route = backStackEntry.toRoute() + EditDocumentScreen( + documentId = route.documentId, + onNavigateBack = { navController.popBackStack() } + ) + } + + composable { + Box(modifier = Modifier.fillMaxSize()) { + ProfileScreen( + onNavigateBack = { + selectedTab = 0 + navController.navigate(MainTabResidencesRoute) + }, + onLogout = onLogout, + onNavigateToNotificationPreferences = { + navController.navigate(NotificationPreferencesRoute) + }, + onNavigateToUpgrade = { + navController.navigate(UpgradeRoute) + } + ) + } + } + + composable { + Box(modifier = Modifier.fillMaxSize()) { + NotificationPreferencesScreen( + onNavigateBack = { + navController.popBackStack() + } + ) + } + } + + composable { backStackEntry -> + val route = backStackEntry.toRoute() + Box(modifier = Modifier.fillMaxSize()) { + CompleteTaskScreen( + taskId = route.taskId, + taskTitle = route.taskTitle, + residenceName = route.residenceName, + onNavigateBack = { + navController.popBackStack() + }, + onComplete = { request, images -> + // Navigation back happens in the screen after successful completion + navController.popBackStack() + } + ) + } + } + + composable { backStackEntry -> + val route = backStackEntry.toRoute() + Box(modifier = Modifier.fillMaxSize()) { + ManageUsersScreen( + residenceId = route.residenceId, + residenceName = route.residenceName, + isPrimaryOwner = route.isPrimaryOwner, + residenceOwnerId = route.residenceOwnerId, + onNavigateBack = { + navController.popBackStack() + }, + onUserRemoved = { + // Could trigger a refresh if needed + } + ) + } + } + + composable { + Box(modifier = Modifier.fillMaxSize()) { + UpgradeScreen( + onNavigateBack = { + navController.popBackStack() + }, + onPurchase = { planId -> + // Handle purchase - integrate with billing system + navController.popBackStack() + }, + onRestorePurchases = { + // Handle restore - integrate with billing system + } + ) + } + } } } } diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ManageUsersScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ManageUsersScreen.kt index b0111cb..78216f1 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ManageUsersScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ManageUsersScreen.kt @@ -3,7 +3,6 @@ package com.example.casera.ui.screens import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material3.* @@ -22,8 +21,7 @@ import com.example.casera.models.ResidenceShareCode import com.example.casera.network.ApiResult import com.example.casera.network.ResidenceApi import com.example.casera.storage.TokenStorage -import com.example.casera.ui.theme.AppRadius -import com.example.casera.ui.theme.AppSpacing +import com.example.casera.ui.theme.* import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource @@ -68,308 +66,281 @@ fun ManageUsersScreen( } } - Scaffold( - topBar = { - TopAppBar( - title = { - Column { - Text( - stringResource(Res.string.manage_users_invite_title), - fontWeight = FontWeight.SemiBold - ) - Text( - residenceName, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - }, - navigationIcon = { - IconButton(onClick = onNavigateBack) { - Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back)) - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface - ) - ) - }, - snackbarHost = { SnackbarHost(snackbarHostState) } - ) { paddingValues -> - if (isLoading) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } - } else if (error != null) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), - contentAlignment = Alignment.Center - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(AppSpacing.md) - ) { - Icon( - Icons.Default.Error, - contentDescription = null, - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.error - ) - Text( - text = error ?: "Unknown error", - color = MaterialTheme.colorScheme.error - ) - } - } - } else { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), - contentPadding = PaddingValues(AppSpacing.lg), - verticalArrangement = Arrangement.spacedBy(AppSpacing.lg) - ) { - // Share sections (primary owner only) - if (isPrimaryOwner) { - // Easy Share Section - item { - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(AppRadius.lg), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer - ) - ) { - Column( - modifier = Modifier.padding(AppSpacing.lg) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm) - ) { - Icon( - Icons.Default.Share, - contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimaryContainer - ) - Text( - text = stringResource(Res.string.manage_users_easy_share), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onPrimaryContainer - ) - } - - Spacer(modifier = Modifier.height(AppSpacing.md)) - - Button( - onClick = onSharePackage, - modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary - ) - ) { - Icon(Icons.Default.Send, null, modifier = Modifier.size(20.dp)) - Spacer(modifier = Modifier.width(AppSpacing.sm)) - Text( - stringResource(Res.string.manage_users_send_invite), - fontWeight = FontWeight.SemiBold - ) - } - - Spacer(modifier = Modifier.height(AppSpacing.sm)) - - Text( - text = stringResource(Res.string.manage_users_easy_share_desc), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f) - ) - } - } - } - - // Divider with "or" - item { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - HorizontalDivider(modifier = Modifier.weight(1f)) + WarmGradientBackground { + Scaffold( + containerColor = androidx.compose.ui.graphics.Color.Transparent, + topBar = { + TopAppBar( + title = { + Column { Text( - text = stringResource(Res.string.manage_users_or), + stringResource(Res.string.manage_users_invite_title), + fontWeight = FontWeight.SemiBold + ) + Text( + residenceName, style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(horizontal = AppSpacing.lg) + color = MaterialTheme.colorScheme.onSurfaceVariant ) - HorizontalDivider(modifier = Modifier.weight(1f)) } + }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back)) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = androidx.compose.ui.graphics.Color.Transparent + ) + ) + }, + snackbarHost = { SnackbarHost(snackbarHostState) } + ) { paddingValues -> + if (isLoading) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } else if (error != null) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md) + ) { + Icon( + Icons.Default.Error, + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.error + ) + Text( + text = error ?: "Unknown error", + color = MaterialTheme.colorScheme.error + ) } - - // Share Code Section - item { - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(AppRadius.lg), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) { - Column( - modifier = Modifier.padding(AppSpacing.lg) + } + } else { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + contentPadding = PaddingValues(OrganicSpacing.lg), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg) + ) { + // Share sections (primary owner only) + if (isPrimaryOwner) { + // Easy Share Section + item { + OrganicCard( + modifier = Modifier.fillMaxWidth(), + accentColor = MaterialTheme.colorScheme.primaryContainer ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm) - ) { - Icon( - Icons.Default.Key, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - Text( - text = stringResource(Res.string.manage_users_share_code), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold - ) - } - - Spacer(modifier = Modifier.height(AppSpacing.lg)) - - // Share code display - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(AppRadius.md), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ) + Column( + modifier = Modifier.padding(OrganicSpacing.lg) ) { Row( - modifier = Modifier - .fillMaxWidth() - .padding(AppSpacing.lg), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm) ) { - if (shareCode != null) { - Text( - text = shareCode!!.code, - style = MaterialTheme.typography.headlineMedium.copy( - fontFamily = FontFamily.Monospace, - letterSpacing = 4.sp - ), - color = MaterialTheme.colorScheme.primary, - fontWeight = FontWeight.Bold - ) - IconButton( - onClick = { - clipboardManager.setText(AnnotatedString(shareCode!!.code)) - scope.launch { - snackbarHostState.showSnackbar("Code copied to clipboard") - } - } - ) { - Icon( - Icons.Default.ContentCopy, - contentDescription = "Copy code", - tint = MaterialTheme.colorScheme.primary - ) - } - } else { - Text( - text = stringResource(Res.string.manage_users_no_code), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - - Spacer(modifier = Modifier.height(AppSpacing.md)) - - Button( - onClick = { - scope.launch { - isGeneratingCode = true - val token = TokenStorage.getToken() - if (token != null) { - when (val result = residenceApi.generateShareCode(token, residenceId)) { - is ApiResult.Success -> { - shareCode = result.data.shareCode - } - is ApiResult.Error -> { - error = result.message - } - else -> {} - } - } - isGeneratingCode = false - } - }, - enabled = !isGeneratingCode, - modifier = Modifier.fillMaxWidth() - ) { - if (isGeneratingCode) { - CircularProgressIndicator( - modifier = Modifier.size(20.dp), - color = MaterialTheme.colorScheme.onPrimary, - strokeWidth = 2.dp + Icon( + Icons.Default.Share, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + Text( + text = stringResource(Res.string.manage_users_easy_share), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onPrimaryContainer ) - } else { - Icon(Icons.Default.Refresh, null, modifier = Modifier.size(20.dp)) } - Spacer(modifier = Modifier.width(AppSpacing.sm)) - Text( - if (shareCode != null) stringResource(Res.string.manage_users_generate_new) - else stringResource(Res.string.manage_users_generate) - ) - } - if (shareCode != null) { - Spacer(modifier = Modifier.height(AppSpacing.sm)) + Spacer(modifier = Modifier.height(OrganicSpacing.md)) + + OrganicPrimaryButton( + text = stringResource(Res.string.manage_users_send_invite), + onClick = onSharePackage, + modifier = Modifier.fillMaxWidth(), + icon = Icons.Default.Send + ) + + Spacer(modifier = Modifier.height(OrganicSpacing.sm)) + Text( - text = stringResource(Res.string.manage_users_code_desc), + text = stringResource(Res.string.manage_users_easy_share_desc), style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f) ) } } } + + // Divider with "or" + item { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + OrganicDivider(modifier = Modifier.weight(1f)) + Text( + text = stringResource(Res.string.manage_users_or), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = OrganicSpacing.lg) + ) + OrganicDivider(modifier = Modifier.weight(1f)) + } + } + + // Share Code Section + item { + OrganicCard( + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier.padding(OrganicSpacing.lg) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm) + ) { + OrganicIconContainer( + icon = Icons.Default.Key, + size = 24.dp + ) + Text( + text = stringResource(Res.string.manage_users_share_code), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + } + + Spacer(modifier = Modifier.height(OrganicSpacing.lg)) + + // Share code display + OrganicCard( + modifier = Modifier.fillMaxWidth(), + accentColor = MaterialTheme.colorScheme.surface + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(OrganicSpacing.lg), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + if (shareCode != null) { + Text( + text = shareCode!!.code, + style = MaterialTheme.typography.headlineMedium.copy( + fontFamily = FontFamily.Monospace, + letterSpacing = 4.sp + ), + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold + ) + IconButton( + onClick = { + clipboardManager.setText(AnnotatedString(shareCode!!.code)) + scope.launch { + snackbarHostState.showSnackbar("Code copied to clipboard") + } + } + ) { + Icon( + Icons.Default.ContentCopy, + contentDescription = "Copy code", + tint = MaterialTheme.colorScheme.primary + ) + } + } else { + Text( + text = stringResource(Res.string.manage_users_no_code), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + + Spacer(modifier = Modifier.height(OrganicSpacing.md)) + + OrganicPrimaryButton( + text = if (shareCode != null) stringResource(Res.string.manage_users_generate_new) + else stringResource(Res.string.manage_users_generate), + onClick = { + scope.launch { + isGeneratingCode = true + val token = TokenStorage.getToken() + if (token != null) { + when (val result = residenceApi.generateShareCode(token, residenceId)) { + is ApiResult.Success -> { + shareCode = result.data.shareCode + } + is ApiResult.Error -> { + error = result.message + } + else -> {} + } + } + isGeneratingCode = false + } + }, + modifier = Modifier.fillMaxWidth(), + enabled = !isGeneratingCode, + isLoading = isGeneratingCode, + icon = Icons.Default.Refresh + ) + + if (shareCode != null) { + Spacer(modifier = Modifier.height(OrganicSpacing.sm)) + Text( + text = stringResource(Res.string.manage_users_code_desc), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } + + item { + OrganicDivider() + } } + // Users Header item { - HorizontalDivider() + Text( + text = stringResource(Res.string.manage_users_users_count, users.size), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) } - } - // Users Header - item { - Text( - text = stringResource(Res.string.manage_users_users_count, users.size), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold - ) - } + // Users List + items(users) { user -> + UserCard( + user = user, + isOwner = user.id == residenceOwnerId, + canRemove = isPrimaryOwner && user.id != residenceOwnerId, + onRemove = { showRemoveConfirmation = user } + ) + } - // Users List - items(users) { user -> - UserCard( - user = user, - isOwner = user.id == residenceOwnerId, - canRemove = isPrimaryOwner && user.id != residenceOwnerId, - onRemove = { showRemoveConfirmation = user } - ) - } - - // Bottom spacing - item { - Spacer(modifier = Modifier.height(AppSpacing.xl)) + // Bottom spacing + item { + Spacer(modifier = Modifier.height(OrganicSpacing.xl)) + } } } } @@ -424,27 +395,23 @@ private fun UserCard( canRemove: Boolean, onRemove: () -> Unit ) { - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(AppRadius.md), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ) + OrganicCard( + modifier = Modifier.fillMaxWidth() ) { Row( modifier = Modifier .fillMaxWidth() - .padding(AppSpacing.lg), + .padding(OrganicSpacing.lg), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(AppSpacing.md) + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md) ) { // Avatar Surface( - shape = RoundedCornerShape(AppRadius.md), + shape = OrganicShapes.medium, color = MaterialTheme.colorScheme.primaryContainer, modifier = Modifier.size(48.dp) ) { @@ -461,7 +428,7 @@ private fun UserCard( Column { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm) + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm) ) { Text( text = user.username, @@ -471,13 +438,13 @@ private fun UserCard( if (isOwner) { Surface( color = MaterialTheme.colorScheme.primaryContainer, - shape = RoundedCornerShape(AppRadius.xs) + shape = OrganicShapes.extraSmall ) { Text( text = stringResource(Res.string.manage_users_owner_badge), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onPrimaryContainer, - modifier = Modifier.padding(horizontal = AppSpacing.sm, vertical = 2.dp) + modifier = Modifier.padding(horizontal = OrganicSpacing.sm, vertical = 2.dp) ) } } diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/NotificationPreferencesScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/NotificationPreferencesScreen.kt index db1585d..215e28d 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/NotificationPreferencesScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/NotificationPreferencesScreen.kt @@ -3,7 +3,6 @@ package com.example.casera.ui.screens import androidx.compose.foundation.clickable 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.* @@ -15,8 +14,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.example.casera.network.ApiResult -import com.example.casera.ui.theme.AppRadius -import com.example.casera.ui.theme.AppSpacing +import com.example.casera.ui.theme.* import com.example.casera.util.DateUtils import com.example.casera.viewmodel.NotificationPreferencesViewModel import com.example.casera.analytics.PostHogAnalytics @@ -93,377 +91,353 @@ fun NotificationPreferencesScreen( } } - Scaffold( - topBar = { - TopAppBar( - title = { Text(stringResource(Res.string.notifications_title), fontWeight = FontWeight.SemiBold) }, - navigationIcon = { - IconButton(onClick = onNavigateBack) { - Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back)) - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + WarmGradientBackground { + Scaffold( + containerColor = androidx.compose.ui.graphics.Color.Transparent, + topBar = { + TopAppBar( + title = { Text(stringResource(Res.string.notifications_title), fontWeight = FontWeight.SemiBold) }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back)) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = androidx.compose.ui.graphics.Color.Transparent + ) ) - ) - } - ) { paddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - .verticalScroll(rememberScrollState()) - .padding(horizontal = AppSpacing.lg, vertical = AppSpacing.md), - verticalArrangement = Arrangement.spacedBy(AppSpacing.md) - ) { - // Header - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(AppRadius.lg), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(AppSpacing.xl), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(AppSpacing.md) - ) { - Icon( - imageVector = Icons.Default.Notifications, - contentDescription = null, - modifier = Modifier.size(60.dp), - tint = MaterialTheme.colorScheme.primary - ) - - Text( - stringResource(Res.string.notifications_preferences), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold - ) - - Text( - stringResource(Res.string.notifications_choose), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } } - - when (preferencesState) { - is ApiResult.Loading -> { - Box( + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + .padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.md), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md) + ) { + // Header + OrganicCard( + modifier = Modifier.fillMaxWidth() + ) { + Column( modifier = Modifier .fillMaxWidth() - .padding(AppSpacing.xl), - contentAlignment = Alignment.Center + .padding(OrganicSpacing.xl), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md) ) { - CircularProgressIndicator() + OrganicIconContainer( + icon = Icons.Default.Notifications, + size = 60.dp + ) + + Text( + stringResource(Res.string.notifications_preferences), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + + Text( + stringResource(Res.string.notifications_choose), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) } } - is ApiResult.Error -> { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.errorContainer - ), - shape = RoundedCornerShape(AppRadius.md) - ) { - Column( + when (preferencesState) { + is ApiResult.Loading -> { + Box( modifier = Modifier .fillMaxWidth() - .padding(AppSpacing.lg), - verticalArrangement = Arrangement.spacedBy(AppSpacing.md) + .padding(OrganicSpacing.xl), + contentAlignment = Alignment.Center ) { - Row( - horizontalArrangement = Arrangement.spacedBy(AppSpacing.md), - verticalAlignment = Alignment.CenterVertically + CircularProgressIndicator() + } + } + + is ApiResult.Error -> { + OrganicCard( + modifier = Modifier.fillMaxWidth(), + accentColor = MaterialTheme.colorScheme.errorContainer + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(OrganicSpacing.lg), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md) ) { - Icon( - Icons.Default.Error, - contentDescription = null, - tint = MaterialTheme.colorScheme.error - ) - Text( - (preferencesState as ApiResult.Error).message, - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.SemiBold - ) - } - Button( - onClick = { viewModel.loadPreferences() }, - modifier = Modifier.fillMaxWidth() - ) { - Text(stringResource(Res.string.common_retry)) - } - } - } - } - - is ApiResult.Success, is ApiResult.Idle -> { - // Task Notifications Section - Text( - stringResource(Res.string.notifications_task_section), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - modifier = Modifier.padding(top = AppSpacing.md) - ) - - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(AppRadius.md), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) { - Column { - NotificationToggle( - title = stringResource(Res.string.notifications_task_due_soon), - description = stringResource(Res.string.notifications_task_due_soon_desc), - icon = Icons.Default.Schedule, - iconTint = MaterialTheme.colorScheme.tertiary, - checked = taskDueSoon, - onCheckedChange = { - taskDueSoon = it - viewModel.updatePreference(taskDueSoon = it) + Row( + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Default.Error, + contentDescription = null, + tint = MaterialTheme.colorScheme.error + ) + Text( + (preferencesState as ApiResult.Error).message, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold + ) } - ) - - // Time picker for Task Due Soon - if (taskDueSoon) { - NotificationTimePickerRow( - currentHour = taskDueSoonHour, - onSetCustomTime = { - val localHour = defaultTaskDueSoonLocalHour - taskDueSoonHour = localHour - val utcHour = DateUtils.localHourToUtc(localHour) - viewModel.updatePreference(taskDueSoonHour = utcHour) - }, - onChangeTime = { showTaskDueSoonTimePicker = true } - ) - } - - HorizontalDivider( - modifier = Modifier.padding(horizontal = AppSpacing.lg), - color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) - ) - - NotificationToggle( - title = stringResource(Res.string.notifications_task_overdue), - description = stringResource(Res.string.notifications_task_overdue_desc), - icon = Icons.Default.Warning, - iconTint = MaterialTheme.colorScheme.error, - checked = taskOverdue, - onCheckedChange = { - taskOverdue = it - viewModel.updatePreference(taskOverdue = it) - } - ) - - // Time picker for Task Overdue - if (taskOverdue) { - NotificationTimePickerRow( - currentHour = taskOverdueHour, - onSetCustomTime = { - val localHour = defaultTaskOverdueLocalHour - taskOverdueHour = localHour - val utcHour = DateUtils.localHourToUtc(localHour) - viewModel.updatePreference(taskOverdueHour = utcHour) - }, - onChangeTime = { showTaskOverdueTimePicker = true } - ) - } - - HorizontalDivider( - modifier = Modifier.padding(horizontal = AppSpacing.lg), - color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) - ) - - NotificationToggle( - title = stringResource(Res.string.notifications_task_completed), - description = stringResource(Res.string.notifications_task_completed_desc), - icon = Icons.Default.CheckCircle, - iconTint = MaterialTheme.colorScheme.primary, - checked = taskCompleted, - onCheckedChange = { - taskCompleted = it - viewModel.updatePreference(taskCompleted = it) - } - ) - - HorizontalDivider( - modifier = Modifier.padding(horizontal = AppSpacing.lg), - color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) - ) - - NotificationToggle( - title = stringResource(Res.string.notifications_task_assigned), - description = stringResource(Res.string.notifications_task_assigned_desc), - icon = Icons.Default.PersonAdd, - iconTint = MaterialTheme.colorScheme.secondary, - checked = taskAssigned, - onCheckedChange = { - taskAssigned = it - viewModel.updatePreference(taskAssigned = it) - } - ) - } - } - - // Time picker dialogs - if (showTaskDueSoonTimePicker) { - HourPickerDialog( - currentHour = taskDueSoonHour ?: defaultTaskDueSoonLocalHour, - onHourSelected = { hour -> - taskDueSoonHour = hour - val utcHour = DateUtils.localHourToUtc(hour) - viewModel.updatePreference(taskDueSoonHour = utcHour) - showTaskDueSoonTimePicker = false - }, - onDismiss = { showTaskDueSoonTimePicker = false } - ) - } - - if (showTaskOverdueTimePicker) { - HourPickerDialog( - currentHour = taskOverdueHour ?: defaultTaskOverdueLocalHour, - onHourSelected = { hour -> - taskOverdueHour = hour - val utcHour = DateUtils.localHourToUtc(hour) - viewModel.updatePreference(taskOverdueHour = utcHour) - showTaskOverdueTimePicker = false - }, - onDismiss = { showTaskOverdueTimePicker = false } - ) - } - - // Other Notifications Section - Text( - stringResource(Res.string.notifications_other_section), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - modifier = Modifier.padding(top = AppSpacing.md) - ) - - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(AppRadius.md), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) { - Column { - NotificationToggle( - title = stringResource(Res.string.notifications_property_shared), - description = stringResource(Res.string.notifications_property_shared_desc), - icon = Icons.Default.Home, - iconTint = MaterialTheme.colorScheme.primary, - checked = residenceShared, - onCheckedChange = { - residenceShared = it - viewModel.updatePreference(residenceShared = it) - } - ) - - HorizontalDivider( - modifier = Modifier.padding(horizontal = AppSpacing.lg), - color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) - ) - - NotificationToggle( - title = stringResource(Res.string.notifications_warranty_expiring), - description = stringResource(Res.string.notifications_warranty_expiring_desc), - icon = Icons.Default.Description, - iconTint = MaterialTheme.colorScheme.tertiary, - checked = warrantyExpiring, - onCheckedChange = { - warrantyExpiring = it - viewModel.updatePreference(warrantyExpiring = it) - } - ) - - HorizontalDivider( - modifier = Modifier.padding(horizontal = AppSpacing.lg), - color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) - ) - - NotificationToggle( - title = stringResource(Res.string.notifications_daily_digest), - description = stringResource(Res.string.notifications_daily_digest_desc), - icon = Icons.Default.Summarize, - iconTint = MaterialTheme.colorScheme.secondary, - checked = dailyDigest, - onCheckedChange = { - dailyDigest = it - viewModel.updatePreference(dailyDigest = it) - } - ) - - // Time picker for Daily Digest - if (dailyDigest) { - NotificationTimePickerRow( - currentHour = dailyDigestHour, - onSetCustomTime = { - val localHour = defaultDailyDigestLocalHour - dailyDigestHour = localHour - val utcHour = DateUtils.localHourToUtc(localHour) - viewModel.updatePreference(dailyDigestHour = utcHour) - }, - onChangeTime = { showDailyDigestTimePicker = true } + OrganicPrimaryButton( + text = stringResource(Res.string.common_retry), + onClick = { viewModel.loadPreferences() }, + modifier = Modifier.fillMaxWidth() ) } } } - // Daily Digest time picker dialog - if (showDailyDigestTimePicker) { - HourPickerDialog( - currentHour = dailyDigestHour ?: defaultDailyDigestLocalHour, - onHourSelected = { hour -> - dailyDigestHour = hour - val utcHour = DateUtils.localHourToUtc(hour) - viewModel.updatePreference(dailyDigestHour = utcHour) - showDailyDigestTimePicker = false - }, - onDismiss = { showDailyDigestTimePicker = false } + is ApiResult.Success, is ApiResult.Idle -> { + // Task Notifications Section + Text( + stringResource(Res.string.notifications_task_section), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(top = OrganicSpacing.md) ) - } - // Email Notifications Section - Text( - stringResource(Res.string.notifications_email_section), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - modifier = Modifier.padding(top = AppSpacing.md) - ) + OrganicCard( + modifier = Modifier.fillMaxWidth() + ) { + Column { + NotificationToggle( + title = stringResource(Res.string.notifications_task_due_soon), + description = stringResource(Res.string.notifications_task_due_soon_desc), + icon = Icons.Default.Schedule, + iconTint = MaterialTheme.colorScheme.tertiary, + checked = taskDueSoon, + onCheckedChange = { + taskDueSoon = it + viewModel.updatePreference(taskDueSoon = it) + } + ) - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(AppRadius.md), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) { - Column { - NotificationToggle( - title = stringResource(Res.string.notifications_email_task_completed), - description = stringResource(Res.string.notifications_email_task_completed_desc), - icon = Icons.Default.Email, - iconTint = MaterialTheme.colorScheme.primary, - checked = emailTaskCompleted, - onCheckedChange = { - emailTaskCompleted = it - viewModel.updatePreference(emailTaskCompleted = it) + // Time picker for Task Due Soon + if (taskDueSoon) { + NotificationTimePickerRow( + currentHour = taskDueSoonHour, + onSetCustomTime = { + val localHour = defaultTaskDueSoonLocalHour + taskDueSoonHour = localHour + val utcHour = DateUtils.localHourToUtc(localHour) + viewModel.updatePreference(taskDueSoonHour = utcHour) + }, + onChangeTime = { showTaskDueSoonTimePicker = true } + ) } + + OrganicDivider( + modifier = Modifier.padding(horizontal = OrganicSpacing.lg) + ) + + NotificationToggle( + title = stringResource(Res.string.notifications_task_overdue), + description = stringResource(Res.string.notifications_task_overdue_desc), + icon = Icons.Default.Warning, + iconTint = MaterialTheme.colorScheme.error, + checked = taskOverdue, + onCheckedChange = { + taskOverdue = it + viewModel.updatePreference(taskOverdue = it) + } + ) + + // Time picker for Task Overdue + if (taskOverdue) { + NotificationTimePickerRow( + currentHour = taskOverdueHour, + onSetCustomTime = { + val localHour = defaultTaskOverdueLocalHour + taskOverdueHour = localHour + val utcHour = DateUtils.localHourToUtc(localHour) + viewModel.updatePreference(taskOverdueHour = utcHour) + }, + onChangeTime = { showTaskOverdueTimePicker = true } + ) + } + + OrganicDivider( + modifier = Modifier.padding(horizontal = OrganicSpacing.lg) + ) + + NotificationToggle( + title = stringResource(Res.string.notifications_task_completed), + description = stringResource(Res.string.notifications_task_completed_desc), + icon = Icons.Default.CheckCircle, + iconTint = MaterialTheme.colorScheme.primary, + checked = taskCompleted, + onCheckedChange = { + taskCompleted = it + viewModel.updatePreference(taskCompleted = it) + } + ) + + OrganicDivider( + modifier = Modifier.padding(horizontal = OrganicSpacing.lg) + ) + + NotificationToggle( + title = stringResource(Res.string.notifications_task_assigned), + description = stringResource(Res.string.notifications_task_assigned_desc), + icon = Icons.Default.PersonAdd, + iconTint = MaterialTheme.colorScheme.secondary, + checked = taskAssigned, + onCheckedChange = { + taskAssigned = it + viewModel.updatePreference(taskAssigned = it) + } + ) + } + } + + // Time picker dialogs + if (showTaskDueSoonTimePicker) { + HourPickerDialog( + currentHour = taskDueSoonHour ?: defaultTaskDueSoonLocalHour, + onHourSelected = { hour -> + taskDueSoonHour = hour + val utcHour = DateUtils.localHourToUtc(hour) + viewModel.updatePreference(taskDueSoonHour = utcHour) + showTaskDueSoonTimePicker = false + }, + onDismiss = { showTaskDueSoonTimePicker = false } ) } - } - Spacer(modifier = Modifier.height(AppSpacing.xl)) + if (showTaskOverdueTimePicker) { + HourPickerDialog( + currentHour = taskOverdueHour ?: defaultTaskOverdueLocalHour, + onHourSelected = { hour -> + taskOverdueHour = hour + val utcHour = DateUtils.localHourToUtc(hour) + viewModel.updatePreference(taskOverdueHour = utcHour) + showTaskOverdueTimePicker = false + }, + onDismiss = { showTaskOverdueTimePicker = false } + ) + } + + // Other Notifications Section + Text( + stringResource(Res.string.notifications_other_section), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(top = OrganicSpacing.md) + ) + + OrganicCard( + modifier = Modifier.fillMaxWidth() + ) { + Column { + NotificationToggle( + title = stringResource(Res.string.notifications_property_shared), + description = stringResource(Res.string.notifications_property_shared_desc), + icon = Icons.Default.Home, + iconTint = MaterialTheme.colorScheme.primary, + checked = residenceShared, + onCheckedChange = { + residenceShared = it + viewModel.updatePreference(residenceShared = it) + } + ) + + OrganicDivider( + modifier = Modifier.padding(horizontal = OrganicSpacing.lg) + ) + + NotificationToggle( + title = stringResource(Res.string.notifications_warranty_expiring), + description = stringResource(Res.string.notifications_warranty_expiring_desc), + icon = Icons.Default.Description, + iconTint = MaterialTheme.colorScheme.tertiary, + checked = warrantyExpiring, + onCheckedChange = { + warrantyExpiring = it + viewModel.updatePreference(warrantyExpiring = it) + } + ) + + OrganicDivider( + modifier = Modifier.padding(horizontal = OrganicSpacing.lg) + ) + + NotificationToggle( + title = stringResource(Res.string.notifications_daily_digest), + description = stringResource(Res.string.notifications_daily_digest_desc), + icon = Icons.Default.Summarize, + iconTint = MaterialTheme.colorScheme.secondary, + checked = dailyDigest, + onCheckedChange = { + dailyDigest = it + viewModel.updatePreference(dailyDigest = it) + } + ) + + // Time picker for Daily Digest + if (dailyDigest) { + NotificationTimePickerRow( + currentHour = dailyDigestHour, + onSetCustomTime = { + val localHour = defaultDailyDigestLocalHour + dailyDigestHour = localHour + val utcHour = DateUtils.localHourToUtc(localHour) + viewModel.updatePreference(dailyDigestHour = utcHour) + }, + onChangeTime = { showDailyDigestTimePicker = true } + ) + } + } + } + + // Daily Digest time picker dialog + if (showDailyDigestTimePicker) { + HourPickerDialog( + currentHour = dailyDigestHour ?: defaultDailyDigestLocalHour, + onHourSelected = { hour -> + dailyDigestHour = hour + val utcHour = DateUtils.localHourToUtc(hour) + viewModel.updatePreference(dailyDigestHour = utcHour) + showDailyDigestTimePicker = false + }, + onDismiss = { showDailyDigestTimePicker = false } + ) + } + + // Email Notifications Section + Text( + stringResource(Res.string.notifications_email_section), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(top = OrganicSpacing.md) + ) + + OrganicCard( + modifier = Modifier.fillMaxWidth() + ) { + Column { + NotificationToggle( + title = stringResource(Res.string.notifications_email_task_completed), + description = stringResource(Res.string.notifications_email_task_completed_desc), + icon = Icons.Default.Email, + iconTint = MaterialTheme.colorScheme.primary, + checked = emailTaskCompleted, + onCheckedChange = { + emailTaskCompleted = it + viewModel.updatePreference(emailTaskCompleted = it) + } + ) + } + } + + Spacer(modifier = Modifier.height(OrganicSpacing.xl)) + } } } } @@ -482,8 +456,8 @@ private fun NotificationToggle( Row( modifier = Modifier .fillMaxWidth() - .padding(AppSpacing.lg), - horizontalArrangement = Arrangement.spacedBy(AppSpacing.md), + .padding(OrganicSpacing.lg), + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md), verticalAlignment = Alignment.CenterVertically ) { Icon( @@ -528,8 +502,8 @@ private fun NotificationTimePickerRow( Row( modifier = Modifier .fillMaxWidth() - .padding(start = AppSpacing.lg + 24.dp + AppSpacing.md, end = AppSpacing.lg, bottom = AppSpacing.md), - horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm), + .padding(start = OrganicSpacing.lg + 24.dp + OrganicSpacing.md, end = OrganicSpacing.lg, bottom = OrganicSpacing.md), + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm), verticalAlignment = Alignment.CenterVertically ) { Icon( @@ -584,7 +558,7 @@ private fun HourPickerDialog( Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(AppSpacing.md) + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md) ) { Text( text = DateUtils.formatHour(selectedHour), @@ -601,7 +575,7 @@ private fun HourPickerDialog( // AM hours (6 AM - 11 AM) Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(AppSpacing.xs) + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs) ) { Text( "AM", @@ -620,7 +594,7 @@ private fun HourPickerDialog( // PM hours (12 PM - 5 PM) Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(AppSpacing.xs) + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs) ) { Text( "PM", @@ -639,7 +613,7 @@ private fun HourPickerDialog( // Evening hours (6 PM - 11 PM) Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(AppSpacing.xs) + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs) ) { Text( "EVE", @@ -687,7 +661,7 @@ private fun HourChip( modifier = Modifier .width(56.dp) .clickable { onClick() }, - shape = RoundedCornerShape(AppRadius.sm), + shape = OrganicShapes.small, color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant ) { Text( @@ -695,7 +669,7 @@ private fun HourChip( style = MaterialTheme.typography.bodySmall, fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal, color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(horizontal = AppSpacing.sm, vertical = AppSpacing.xs), + modifier = Modifier.padding(horizontal = OrganicSpacing.sm, vertical = OrganicSpacing.xs), textAlign = androidx.compose.ui.text.style.TextAlign.Center ) } diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ProfileScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ProfileScreen.kt index ec41a8d..965f3df 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ProfileScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ProfileScreen.kt @@ -23,6 +23,7 @@ import com.example.casera.utils.SubscriptionHelper import com.example.casera.ui.theme.AppRadius import com.example.casera.ui.theme.AppSpacing import com.example.casera.ui.theme.ThemeManager +import com.example.casera.ui.theme.* import com.example.casera.viewmodel.AuthViewModel import com.example.casera.network.ApiResult import com.example.casera.storage.TokenStorage @@ -140,537 +141,521 @@ fun ProfileScreen( ) } ) { paddingValues -> - if (isLoadingUser) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } - } else { - Column( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - .verticalScroll(rememberScrollState()) - .padding(start = 24.dp, end = 24.dp, top = 24.dp, bottom = 96.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( - stringResource(Res.string.profile_update_title), - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold - ) - - Spacer(modifier = Modifier.height(8.dp)) - - // Edit Profile Section (scrolls down to profile fields) - Card( + WarmGradientBackground { + if (isLoadingUser) { + Box( modifier = Modifier - .fillMaxWidth() - .clickable { /* Profile fields are below - could add scroll behavior */ }, - shape = RoundedCornerShape(AppRadius.md), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) + .fillMaxSize() + .padding(paddingValues), + contentAlignment = Alignment.Center ) { - Row( + CircularProgressIndicator() + } + } else { + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + .padding(start = 24.dp, end = 24.dp, top = 24.dp, bottom = 96.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md) + ) { + Spacer(modifier = Modifier.height(OrganicSpacing.sm)) + + // Profile Icon + OrganicIconContainer( + icon = Icons.Default.AccountCircle, + size = 80.dp, + iconSize = 48.dp + ) + + Text( + stringResource(Res.string.profile_update_title), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(OrganicSpacing.sm)) + + // Edit Profile Section + OrganicCard( modifier = Modifier .fillMaxWidth() - .padding(AppSpacing.lg), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + .clickable { /* Profile fields are below - could add scroll behavior */ } + .naturalShadow() ) { - Column( - verticalArrangement = Arrangement.spacedBy(AppSpacing.xs) - ) { - Text( - text = stringResource(Res.string.profile_edit_profile), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold - ) - Text( - text = if (firstName.isNotBlank() || lastName.isNotBlank()) - "$firstName $lastName".trim() - else email, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - Icon( - imageVector = Icons.Default.Person, - contentDescription = stringResource(Res.string.profile_edit_profile), - tint = MaterialTheme.colorScheme.primary - ) - } - } - - // Theme Selector Section - Card( - modifier = Modifier - .fillMaxWidth() - .clickable { showThemePicker = true }, - shape = RoundedCornerShape(AppRadius.md), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(AppSpacing.lg), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column( - verticalArrangement = Arrangement.spacedBy(AppSpacing.xs) - ) { - Text( - text = stringResource(Res.string.profile_appearance), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold - ) - Text( - text = currentTheme.displayName, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary - ) - } - Icon( - imageVector = Icons.Default.Palette, - contentDescription = stringResource(Res.string.profile_change_theme), - tint = MaterialTheme.colorScheme.primary - ) - } - } - - // Notification Preferences Section - Card( - modifier = Modifier - .fillMaxWidth() - .clickable { onNavigateToNotificationPreferences() }, - shape = RoundedCornerShape(AppRadius.md), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(AppSpacing.lg), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column( - verticalArrangement = Arrangement.spacedBy(AppSpacing.xs) - ) { - Text( - text = stringResource(Res.string.profile_notifications), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold - ) - Text( - text = stringResource(Res.string.profile_notifications_subtitle), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - Icon( - imageVector = Icons.Default.Notifications, - contentDescription = stringResource(Res.string.profile_notifications), - tint = MaterialTheme.colorScheme.primary - ) - } - } - - // Contact Support Section - val uriHandler = LocalUriHandler.current - Card( - modifier = Modifier - .fillMaxWidth() - .clickable { - uriHandler.openUri("mailto:caseraSupport@treymail.com?subject=Casera%20Support%20Request") - }, - shape = RoundedCornerShape(AppRadius.md), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(AppSpacing.lg), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column( - verticalArrangement = Arrangement.spacedBy(AppSpacing.xs) - ) { - Text( - text = stringResource(Res.string.profile_contact_support), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold - ) - Text( - text = stringResource(Res.string.profile_contact_support_subtitle), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - Icon( - imageVector = Icons.Default.Email, - contentDescription = stringResource(Res.string.profile_contact_support), - tint = MaterialTheme.colorScheme.primary - ) - } - } - - // Privacy Policy Section - Card( - modifier = Modifier - .fillMaxWidth() - .clickable { - uriHandler.openUri("https://mycrib.treytartt.com/privacy") - }, - shape = RoundedCornerShape(AppRadius.md), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(AppSpacing.lg), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column( - verticalArrangement = Arrangement.spacedBy(AppSpacing.xs) - ) { - Text( - text = stringResource(Res.string.profile_privacy), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold - ) - Text( - text = stringResource(Res.string.profile_privacy_subtitle), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - Icon( - imageVector = Icons.Default.Lock, - contentDescription = stringResource(Res.string.profile_privacy), - tint = MaterialTheme.colorScheme.primary - ) - } - } - - // Subscription Section - Only show if limitations are enabled - if (currentSubscription?.limitationsEnabled == true) { - Divider(modifier = Modifier.padding(vertical = AppSpacing.sm)) - - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(AppRadius.md), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) { - Column( + Row( modifier = Modifier .fillMaxWidth() - .padding(AppSpacing.lg), - verticalArrangement = Arrangement.spacedBy(AppSpacing.md) + .padding(OrganicSpacing.lg), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(AppSpacing.md) + Column( + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs) ) { - Icon( - imageVector = Icons.Default.Star, - contentDescription = "Subscription", - tint = if (SubscriptionHelper.currentTier == "pro") MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.onSurfaceVariant + Text( + text = stringResource(Res.string.profile_edit_profile), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Text( + text = if (firstName.isNotBlank() || lastName.isNotBlank()) + "$firstName $lastName".trim() + else email, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant ) - - Column( - verticalArrangement = Arrangement.spacedBy(AppSpacing.xs) - ) { - Text( - text = if (SubscriptionHelper.currentTier == "pro") stringResource(Res.string.profile_pro_plan) else stringResource(Res.string.profile_free_plan), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold - ) - - Text( - text = if (SubscriptionHelper.currentTier == "pro" && currentSubscription?.expiresAt != null) { - stringResource(Res.string.profile_active_until, currentSubscription?.expiresAt ?: "") - } else { - stringResource(Res.string.profile_limited_features) - }, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } } + Icon( + imageVector = Icons.Default.Person, + contentDescription = stringResource(Res.string.profile_edit_profile), + tint = MaterialTheme.colorScheme.primary + ) + } + } - if (SubscriptionHelper.currentTier != "pro") { - // Upgrade Benefits List - Column( - modifier = Modifier.padding(vertical = AppSpacing.sm), - verticalArrangement = Arrangement.spacedBy(AppSpacing.sm) - ) { - Text( - text = stringResource(Res.string.profile_upgrade_benefits_title), - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.primary - ) + // Theme Selector Section + OrganicCard( + modifier = Modifier + .fillMaxWidth() + .clickable { showThemePicker = true } + .naturalShadow() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(OrganicSpacing.lg), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs) + ) { + Text( + text = stringResource(Res.string.profile_appearance), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Text( + text = currentTheme.displayName, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary + ) + } + Icon( + imageVector = Icons.Default.Palette, + contentDescription = stringResource(Res.string.profile_change_theme), + tint = MaterialTheme.colorScheme.primary + ) + } + } - UpgradeBenefitRow( - icon = Icons.Default.Home, - text = stringResource(Res.string.profile_benefit_unlimited_properties) - ) - UpgradeBenefitRow( - icon = Icons.Default.Folder, - text = stringResource(Res.string.profile_benefit_document_vault) - ) - UpgradeBenefitRow( - icon = Icons.Default.People, - text = stringResource(Res.string.profile_benefit_residence_sharing) - ) - UpgradeBenefitRow( - icon = Icons.Default.Share, - text = stringResource(Res.string.profile_benefit_contractor_sharing) - ) - UpgradeBenefitRow( - icon = Icons.Default.Notifications, - text = stringResource(Res.string.profile_benefit_actionable_notifications) - ) - UpgradeBenefitRow( - icon = Icons.Default.Widgets, - text = stringResource(Res.string.profile_benefit_widgets) - ) - } + // Notification Preferences Section + OrganicCard( + modifier = Modifier + .fillMaxWidth() + .clickable { onNavigateToNotificationPreferences() } + .naturalShadow() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(OrganicSpacing.lg), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs) + ) { + Text( + text = stringResource(Res.string.profile_notifications), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Text( + text = stringResource(Res.string.profile_notifications_subtitle), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Icon( + imageVector = Icons.Default.Notifications, + contentDescription = stringResource(Res.string.profile_notifications), + tint = MaterialTheme.colorScheme.primary + ) + } + } - Button( - onClick = { - if (onNavigateToUpgrade != null) { - onNavigateToUpgrade() - } else { - showUpgradePrompt = true - } - }, - modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary - ) + // Contact Support Section + val uriHandler = LocalUriHandler.current + OrganicCard( + modifier = Modifier + .fillMaxWidth() + .clickable { + uriHandler.openUri("mailto:caseraSupport@treymail.com?subject=Casera%20Support%20Request") + } + .naturalShadow() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(OrganicSpacing.lg), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs) + ) { + Text( + text = stringResource(Res.string.profile_contact_support), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Text( + text = stringResource(Res.string.profile_contact_support_subtitle), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Icon( + imageVector = Icons.Default.Email, + contentDescription = stringResource(Res.string.profile_contact_support), + tint = MaterialTheme.colorScheme.primary + ) + } + } + + // Privacy Policy Section + OrganicCard( + modifier = Modifier + .fillMaxWidth() + .clickable { + uriHandler.openUri("https://mycrib.treytartt.com/privacy") + } + .naturalShadow() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(OrganicSpacing.lg), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs) + ) { + Text( + text = stringResource(Res.string.profile_privacy), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Text( + text = stringResource(Res.string.profile_privacy_subtitle), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Icon( + imageVector = Icons.Default.Lock, + contentDescription = stringResource(Res.string.profile_privacy), + tint = MaterialTheme.colorScheme.primary + ) + } + } + + // Subscription Section - Only show if limitations are enabled + if (currentSubscription?.limitationsEnabled == true) { + OrganicDivider(modifier = Modifier.padding(vertical = OrganicSpacing.sm)) + + OrganicCard( + modifier = Modifier + .fillMaxWidth() + .naturalShadow() + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(OrganicSpacing.lg), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md) ) { Icon( - imageVector = Icons.Default.KeyboardArrowUp, - contentDescription = null + imageVector = Icons.Default.Star, + contentDescription = "Subscription", + tint = if (SubscriptionHelper.currentTier == "pro") MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.onSurfaceVariant ) - Spacer(modifier = Modifier.width(AppSpacing.sm)) - Text(stringResource(Res.string.profile_upgrade_to_pro), fontWeight = FontWeight.SemiBold) + + Column( + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs) + ) { + Text( + text = if (SubscriptionHelper.currentTier == "pro") stringResource(Res.string.profile_pro_plan) else stringResource(Res.string.profile_free_plan), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + + Text( + text = if (SubscriptionHelper.currentTier == "pro" && currentSubscription?.expiresAt != null) { + stringResource(Res.string.profile_active_until, currentSubscription?.expiresAt ?: "") + } else { + stringResource(Res.string.profile_limited_features) + }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } } - } else { + + if (SubscriptionHelper.currentTier != "pro") { + // Upgrade Benefits List + Column( + modifier = Modifier.padding(vertical = OrganicSpacing.sm), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm) + ) { + Text( + text = stringResource(Res.string.profile_upgrade_benefits_title), + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.primary + ) + + UpgradeBenefitRow( + icon = Icons.Default.Home, + text = stringResource(Res.string.profile_benefit_unlimited_properties) + ) + UpgradeBenefitRow( + icon = Icons.Default.Folder, + text = stringResource(Res.string.profile_benefit_document_vault) + ) + UpgradeBenefitRow( + icon = Icons.Default.People, + text = stringResource(Res.string.profile_benefit_residence_sharing) + ) + UpgradeBenefitRow( + icon = Icons.Default.Share, + text = stringResource(Res.string.profile_benefit_contractor_sharing) + ) + UpgradeBenefitRow( + icon = Icons.Default.Notifications, + text = stringResource(Res.string.profile_benefit_actionable_notifications) + ) + UpgradeBenefitRow( + icon = Icons.Default.Widgets, + text = stringResource(Res.string.profile_benefit_widgets) + ) + } + + Button( + onClick = { + if (onNavigateToUpgrade != null) { + onNavigateToUpgrade() + } else { + showUpgradePrompt = true + } + }, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary + ) + ) { + Icon( + imageVector = Icons.Default.KeyboardArrowUp, + contentDescription = null + ) + Spacer(modifier = Modifier.width(OrganicSpacing.sm)) + Text(stringResource(Res.string.profile_upgrade_to_pro), fontWeight = FontWeight.SemiBold) + } + } else { + Text( + text = stringResource(Res.string.profile_manage_subscription), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = OrganicSpacing.xs) + ) + } + } + } + + OrganicDivider(modifier = Modifier.padding(vertical = OrganicSpacing.sm)) + } + + Text( + stringResource(Res.string.profile_information), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.align(Alignment.Start) + ) + + OutlinedTextField( + value = firstName, + onValueChange = { firstName = it }, + label = { Text(stringResource(Res.string.profile_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(stringResource(Res.string.profile_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(stringResource(Res.string.profile_email)) }, + leadingIcon = { + Icon(Icons.Default.Email, contentDescription = null) + }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + shape = RoundedCornerShape(12.dp) + ) + + if (errorMessage.isNotEmpty()) { + val displayError = when (errorMessage) { + "profile_load_failed" -> stringResource(Res.string.profile_load_failed) + "profile_not_authenticated" -> stringResource(Res.string.profile_not_authenticated) + "profile_update_coming_soon" -> stringResource(Res.string.profile_update_coming_soon) + "profile_email_required" -> stringResource(Res.string.profile_email_required) + else -> errorMessage + } + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer + ), + shape = RoundedCornerShape(AppRadius.md) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(AppSpacing.lg), + horizontalArrangement = Arrangement.spacedBy(AppSpacing.md), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Default.Error, + contentDescription = null, + tint = MaterialTheme.colorScheme.error + ) Text( - text = stringResource(Res.string.profile_manage_subscription), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(top = AppSpacing.xs) + displayError, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold ) } } } - Divider(modifier = Modifier.padding(vertical = AppSpacing.sm)) - } - - Text( - stringResource(Res.string.profile_information), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - modifier = Modifier.align(Alignment.Start) - ) - - OutlinedTextField( - value = firstName, - onValueChange = { firstName = it }, - label = { Text(stringResource(Res.string.profile_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(stringResource(Res.string.profile_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(stringResource(Res.string.profile_email)) }, - leadingIcon = { - Icon(Icons.Default.Email, contentDescription = null) - }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - shape = RoundedCornerShape(12.dp) - ) - - if (errorMessage.isNotEmpty()) { - val displayError = when (errorMessage) { - "profile_load_failed" -> stringResource(Res.string.profile_load_failed) - "profile_not_authenticated" -> stringResource(Res.string.profile_not_authenticated) - "profile_update_coming_soon" -> stringResource(Res.string.profile_update_coming_soon) - "profile_email_required" -> stringResource(Res.string.profile_email_required) - else -> errorMessage - } - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.errorContainer - ), - shape = RoundedCornerShape(AppRadius.md) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(AppSpacing.lg), - horizontalArrangement = Arrangement.spacedBy(AppSpacing.md), - verticalAlignment = Alignment.CenterVertically + if (successMessage.isNotEmpty()) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ), + shape = RoundedCornerShape(12.dp) ) { - Icon( - Icons.Default.Error, - contentDescription = null, - tint = MaterialTheme.colorScheme.error - ) - Text( - displayError, - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.SemiBold - ) + 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 + ) + } } } - } - if (successMessage.isNotEmpty()) { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer - ), + Spacer(modifier = Modifier.height(OrganicSpacing.sm)) + + Button( + onClick = { + if (email.isNotEmpty()) { + // viewModel.updateProfile not available yet + errorMessage = "profile_update_coming_soon" + } else { + errorMessage = "profile_email_required" + } + }, + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + enabled = email.isNotEmpty() && !isLoading, 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 + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = MaterialTheme.colorScheme.onPrimary, + strokeWidth = 2.dp ) - 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 not available yet - errorMessage = "profile_update_coming_soon" } else { - errorMessage = "profile_email_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( - stringResource(Res.string.profile_save), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold - ) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon(Icons.Default.Save, contentDescription = null) + Text( + stringResource(Res.string.profile_save), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + } } } + + Spacer(modifier = Modifier.height(OrganicSpacing.lg)) + + // App Version Section + OrganicDivider(modifier = Modifier.padding(vertical = OrganicSpacing.md)) + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs) + ) { + Text( + text = stringResource(Res.string.profile_app_name), + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = stringResource(Res.string.profile_app_version, getAppVersion()), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + ) + } + + Spacer(modifier = Modifier.height(OrganicSpacing.lg)) } - - Spacer(modifier = Modifier.height(16.dp)) - - // App Version Section - HorizontalDivider(modifier = Modifier.padding(vertical = AppSpacing.md)) - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(AppSpacing.xs) - ) { - Text( - text = stringResource(Res.string.profile_app_name), - style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = stringResource(Res.string.profile_app_version, getAppVersion()), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) - ) - } - - Spacer(modifier = Modifier.height(16.dp)) } } @@ -713,7 +698,7 @@ private fun UpgradeBenefitRow( ) { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm) + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm) ) { Icon( imageVector = icon, diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/RegisterScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/RegisterScreen.kt index 9943d66..e9b87e3 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/RegisterScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/RegisterScreen.kt @@ -17,6 +17,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.example.casera.ui.components.HandleErrors import com.example.casera.ui.components.auth.AuthHeader import com.example.casera.ui.components.common.ErrorCard +import com.example.casera.ui.theme.* import com.example.casera.viewmodel.AuthViewModel import com.example.casera.network.ApiResult import com.example.casera.analytics.PostHogAnalytics @@ -65,131 +66,129 @@ fun RegisterScreen( } } - Scaffold( - topBar = { - TopAppBar( - title = { Text(stringResource(Res.string.auth_register_title), fontWeight = FontWeight.SemiBold) }, - navigationIcon = { - IconButton(onClick = onNavigateBack) { - Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back)) - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + WarmGradientBackground { + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(Res.string.auth_register_title), fontWeight = FontWeight.SemiBold) }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back)) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f) + ) ) - ) - } - ) { paddingValues -> - 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)) - - AuthHeader( - icon = Icons.Default.PersonAdd, - title = stringResource(Res.string.auth_register_title), - subtitle = stringResource(Res.string.auth_register_subtitle) - ) - - Spacer(modifier = Modifier.height(16.dp)) - - OutlinedTextField( - value = username, - onValueChange = { username = it }, - label = { Text(stringResource(Res.string.auth_register_username)) }, - leadingIcon = { - Icon(Icons.Default.Person, contentDescription = null) - }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - shape = RoundedCornerShape(12.dp) - ) - - OutlinedTextField( - value = email, - onValueChange = { email = it }, - label = { Text(stringResource(Res.string.auth_register_email)) }, - leadingIcon = { - Icon(Icons.Default.Email, contentDescription = null) - }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - shape = RoundedCornerShape(12.dp) - ) - - OutlinedTextField( - value = password, - onValueChange = { password = it }, - label = { Text(stringResource(Res.string.auth_register_password)) }, - leadingIcon = { - Icon(Icons.Default.Lock, contentDescription = null) - }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - visualTransformation = PasswordVisualTransformation(), - shape = RoundedCornerShape(12.dp) - ) - - OutlinedTextField( - value = confirmPassword, - onValueChange = { confirmPassword = it }, - label = { Text(stringResource(Res.string.auth_register_confirm_password)) }, - leadingIcon = { - Icon(Icons.Default.Lock, contentDescription = null) - }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - visualTransformation = PasswordVisualTransformation(), - shape = RoundedCornerShape(12.dp) - ) - - ErrorCard(message = errorMessage) - - Spacer(modifier = Modifier.height(8.dp)) - - val passwordsDontMatchMessage = stringResource(Res.string.auth_passwords_dont_match) - Button( - onClick = { - when { - password != confirmPassword -> { - errorMessage = passwordsDontMatchMessage - } - else -> { - isLoading = true - errorMessage = "" - viewModel.register(username, email, password) - } - } - }, + }, + containerColor = androidx.compose.ui.graphics.Color.Transparent + ) { paddingValues -> + Column( modifier = Modifier - .fillMaxWidth() - .height(56.dp), - enabled = username.isNotEmpty() && email.isNotEmpty() && - password.isNotEmpty() && !isLoading, - shape = RoundedCornerShape(12.dp) + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + .padding(OrganicSpacing.xl), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg) ) { - if (isLoading) { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - color = MaterialTheme.colorScheme.onPrimary, - strokeWidth = 2.dp - ) - } else { - Text( - stringResource(Res.string.auth_register_button), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold - ) - } - } + Spacer(modifier = Modifier.height(OrganicSpacing.sm)) - Spacer(modifier = Modifier.height(16.dp)) + AuthHeader( + icon = Icons.Default.PersonAdd, + title = stringResource(Res.string.auth_register_title), + subtitle = stringResource(Res.string.auth_register_subtitle) + ) + + Spacer(modifier = Modifier.height(OrganicSpacing.md)) + + OrganicCard( + modifier = Modifier.fillMaxWidth() + ) { + Column( + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md) + ) { + OutlinedTextField( + value = username, + onValueChange = { username = it }, + label = { Text(stringResource(Res.string.auth_register_username)) }, + leadingIcon = { + Icon(Icons.Default.Person, contentDescription = null) + }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + shape = RoundedCornerShape(12.dp) + ) + + OutlinedTextField( + value = email, + onValueChange = { email = it }, + label = { Text(stringResource(Res.string.auth_register_email)) }, + leadingIcon = { + Icon(Icons.Default.Email, contentDescription = null) + }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + shape = RoundedCornerShape(12.dp) + ) + + OrganicDivider() + + OutlinedTextField( + value = password, + onValueChange = { password = it }, + label = { Text(stringResource(Res.string.auth_register_password)) }, + leadingIcon = { + Icon(Icons.Default.Lock, contentDescription = null) + }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + visualTransformation = PasswordVisualTransformation(), + shape = RoundedCornerShape(12.dp) + ) + + OutlinedTextField( + value = confirmPassword, + onValueChange = { confirmPassword = it }, + label = { Text(stringResource(Res.string.auth_register_confirm_password)) }, + leadingIcon = { + Icon(Icons.Default.Lock, contentDescription = null) + }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + visualTransformation = PasswordVisualTransformation(), + shape = RoundedCornerShape(12.dp) + ) + } + } + + ErrorCard(message = errorMessage) + + Spacer(modifier = Modifier.height(OrganicSpacing.sm)) + + val passwordsDontMatchMessage = stringResource(Res.string.auth_passwords_dont_match) + OrganicPrimaryButton( + text = stringResource(Res.string.auth_register_button), + onClick = { + when { + password != confirmPassword -> { + errorMessage = passwordsDontMatchMessage + } + else -> { + isLoading = true + errorMessage = "" + viewModel.register(username, email, password) + } + } + }, + modifier = Modifier.fillMaxWidth(), + enabled = username.isNotEmpty() && email.isNotEmpty() && + password.isNotEmpty() && !isLoading, + isLoading = isLoading + ) + + Spacer(modifier = Modifier.height(OrganicSpacing.md)) + } } } } diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ResetPasswordScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ResetPasswordScreen.kt index 0a1c517..83ba150 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ResetPasswordScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ResetPasswordScreen.kt @@ -1,8 +1,6 @@ package com.example.casera.ui.screens -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material3.* @@ -20,6 +18,7 @@ import com.example.casera.ui.components.auth.RequirementItem import com.example.casera.ui.components.common.ErrorCard import com.example.casera.viewmodel.PasswordResetViewModel import com.example.casera.network.ApiResult +import com.example.casera.ui.theme.* import casera.composeapp.generated.resources.* import org.jetbrains.compose.resources.stringResource @@ -80,194 +79,199 @@ fun ResetPasswordScreen( ) } ) { paddingValues -> - Box( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .padding(paddingValues), - contentAlignment = Alignment.Center - ) { - Card( + WarmGradientBackground { + Box( modifier = Modifier - .fillMaxWidth(0.9f) - .wrapContentHeight(), - shape = RoundedCornerShape(24.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ), - elevation = CardDefaults.cardElevation(defaultElevation = 8.dp) + .fillMaxSize() + .padding(paddingValues), + contentAlignment = Alignment.Center ) { - Column( + OrganicCard( modifier = Modifier - .fillMaxWidth() - .padding(32.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(20.dp) + .fillMaxWidth(0.9f) + .wrapContentHeight(), + showBlob = true, + blobVariation = 2 ) { - if (isSuccess) { - // Success State - AuthHeader( - icon = Icons.Default.CheckCircle, - title = "Success!", - subtitle = "Your password has been reset successfully" - ) - - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer + Column( + modifier = Modifier + .fillMaxWidth() + .padding(OrganicSpacing.spacious), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy) + ) { + if (isSuccess) { + // Success State + OrganicIconContainer( + icon = Icons.Default.CheckCircle, + size = 80.dp, + iconScale = 0.5f, + backgroundColor = MaterialTheme.colorScheme.primary, + iconColor = MaterialTheme.colorScheme.onPrimary ) - ) { + Text( - "You can now log in with your new password", - modifier = Modifier.padding(16.dp), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onPrimaryContainer, + text = "Success!", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.textPrimary, textAlign = TextAlign.Center ) - } - Button( - onClick = onPasswordResetSuccess, - modifier = Modifier - .fillMaxWidth() - .height(56.dp), - shape = RoundedCornerShape(12.dp) - ) { Text( - "Return to Login", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold + text = "Your password has been reset successfully", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.textSecondary, + textAlign = TextAlign.Center ) - } - } else { - // Reset Password Form - AuthHeader( - icon = Icons.Default.LockReset, - title = "Set New Password", - subtitle = "Create a strong password to secure your account" - ) - // Password Requirements - Card( - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer - ) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) + OrganicCard( + modifier = Modifier.fillMaxWidth(), + accentColor = MaterialTheme.colorScheme.primary, + showBlob = false ) { Text( - "Password Requirements", + "You can now log in with your new password", + modifier = Modifier.padding(OrganicSpacing.cozy), style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.SemiBold - ) - - RequirementItem( - "At least 8 characters", - newPassword.length >= 8 - ) - RequirementItem( - "Contains letters", - hasLetter - ) - RequirementItem( - "Contains numbers", - hasNumber - ) - RequirementItem( - "Passwords match", - passwordsMatch + color = MaterialTheme.colorScheme.textPrimary, + textAlign = TextAlign.Center ) } - } - OutlinedTextField( - value = newPassword, - onValueChange = { - newPassword = it - viewModel.resetResetPasswordState() - }, - label = { Text(stringResource(Res.string.auth_reset_new_password)) }, - leadingIcon = { - Icon(Icons.Default.Lock, contentDescription = null) - }, - trailingIcon = { - IconButton(onClick = { newPasswordVisible = !newPasswordVisible }) { - Icon( - if (newPasswordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff, - contentDescription = if (newPasswordVisible) "Hide password" else "Show password" + OrganicDivider( + modifier = Modifier.fillMaxWidth() + ) + + OrganicPrimaryButton( + text = "Return to Login", + onClick = onPasswordResetSuccess + ) + } else { + // Reset Password Form + OrganicIconContainer( + icon = Icons.Default.LockReset, + size = 80.dp, + iconScale = 0.5f, + backgroundColor = MaterialTheme.colorScheme.primary, + iconColor = MaterialTheme.colorScheme.onPrimary + ) + + Text( + text = "Set New Password", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.textPrimary, + textAlign = TextAlign.Center + ) + + Text( + text = "Create a strong password to secure your account", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.textSecondary, + textAlign = TextAlign.Center + ) + + // Password Requirements + OrganicCard( + modifier = Modifier.fillMaxWidth(), + accentColor = MaterialTheme.colorScheme.secondary, + showBlob = false + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(OrganicSpacing.cozy), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact) + ) { + Text( + "Password Requirements", + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.textPrimary + ) + + RequirementItem( + "At least 8 characters", + newPassword.length >= 8 + ) + RequirementItem( + "Contains letters", + hasLetter + ) + RequirementItem( + "Contains numbers", + hasNumber + ) + RequirementItem( + "Passwords match", + passwordsMatch ) } - }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - visualTransformation = if (newPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(), - shape = RoundedCornerShape(12.dp), - enabled = !isLoading - ) - - OutlinedTextField( - value = confirmPassword, - onValueChange = { - confirmPassword = it - viewModel.resetResetPasswordState() - }, - label = { Text(stringResource(Res.string.auth_reset_confirm_password)) }, - leadingIcon = { - Icon(Icons.Default.Lock, contentDescription = null) - }, - trailingIcon = { - IconButton(onClick = { confirmPasswordVisible = !confirmPasswordVisible }) { - Icon( - if (confirmPasswordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff, - contentDescription = if (confirmPasswordVisible) "Hide password" else "Show password" - ) - } - }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - visualTransformation = if (confirmPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(), - shape = RoundedCornerShape(12.dp), - enabled = !isLoading - ) - - ErrorCard(message = errorMessage) - - Button( - onClick = { - viewModel.resetPassword(newPassword, confirmPassword) - }, - modifier = Modifier - .fillMaxWidth() - .height(56.dp), - enabled = isFormValid && !isLoading && !isLoggingIn, - shape = RoundedCornerShape(12.dp) - ) { - if (isLoading || isLoggingIn) { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - color = MaterialTheme.colorScheme.onPrimary, - strokeWidth = 2.dp - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - if (isLoggingIn) "Logging in..." else "Resetting...", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold - ) - } else { - Icon(Icons.Default.LockReset, contentDescription = null) - Spacer(modifier = Modifier.width(8.dp)) - Text( - stringResource(Res.string.auth_reset_button), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold - ) } + + OutlinedTextField( + value = newPassword, + onValueChange = { + newPassword = it + viewModel.resetResetPasswordState() + }, + label = { Text(stringResource(Res.string.auth_reset_new_password)) }, + leadingIcon = { + Icon(Icons.Default.Lock, contentDescription = null) + }, + trailingIcon = { + IconButton(onClick = { newPasswordVisible = !newPasswordVisible }) { + Icon( + if (newPasswordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff, + contentDescription = if (newPasswordVisible) "Hide password" else "Show password" + ) + } + }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + visualTransformation = if (newPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(), + enabled = !isLoading + ) + + OutlinedTextField( + value = confirmPassword, + onValueChange = { + confirmPassword = it + viewModel.resetResetPasswordState() + }, + label = { Text(stringResource(Res.string.auth_reset_confirm_password)) }, + leadingIcon = { + Icon(Icons.Default.Lock, contentDescription = null) + }, + trailingIcon = { + IconButton(onClick = { confirmPasswordVisible = !confirmPasswordVisible }) { + Icon( + if (confirmPasswordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff, + contentDescription = if (confirmPasswordVisible) "Hide password" else "Show password" + ) + } + }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + visualTransformation = if (confirmPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(), + enabled = !isLoading + ) + + ErrorCard(message = errorMessage) + + OrganicDivider( + modifier = Modifier.fillMaxWidth() + ) + + OrganicPrimaryButton( + text = if (isLoggingIn) "Logging in..." else stringResource(Res.string.auth_reset_button), + onClick = { + viewModel.resetPassword(newPassword, confirmPassword) + }, + enabled = isFormValid && !isLoading && !isLoggingIn, + isLoading = isLoading || isLoggingIn + ) } } } diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ResidenceDetailScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ResidenceDetailScreen.kt index d961146..22bef23 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ResidenceDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ResidenceDetailScreen.kt @@ -39,6 +39,7 @@ import com.example.casera.util.DateUtils import com.example.casera.platform.rememberShareResidence import com.example.casera.analytics.PostHogAnalytics import com.example.casera.analytics.AnalyticsEvents +import com.example.casera.ui.theme.* import casera.composeapp.generated.resources.* import org.jetbrains.compose.resources.stringResource @@ -539,50 +540,49 @@ fun ResidenceDetailScreen( } } ) { paddingValues -> - ApiResultHandler( - state = residenceState, - onRetry = { - residenceViewModel.getResidence(residenceId) { result -> - residenceState = result + WarmGradientBackground { + ApiResultHandler( + state = residenceState, + onRetry = { + residenceViewModel.getResidence(residenceId) { result -> + residenceState = result + } + }, + modifier = Modifier.padding(paddingValues), + errorTitle = stringResource(Res.string.properties_failed_to_load), + loadingContent = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy) + ) { + CircularProgressIndicator() + Text( + text = stringResource(Res.string.properties_loading), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } } - }, - modifier = Modifier.padding(paddingValues), - errorTitle = stringResource(Res.string.properties_failed_to_load), - loadingContent = { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - CircularProgressIndicator() - Text( - text = stringResource(Res.string.properties_loading), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - ) { residence -> - LazyColumn( + ) { residence -> + LazyColumn( modifier = Modifier .fillMaxSize() .padding(paddingValues), - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) + contentPadding = PaddingValues(OrganicSpacing.cozy), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy) ) { // Property Header Card item { - Card( + OrganicCard( modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer - ), - shape = RoundedCornerShape(20.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) + accentColor = MaterialTheme.colorScheme.primary, + showBlob = true, + blobVariation = 0 ) { Column( modifier = Modifier .fillMaxWidth() - .padding(24.dp) + .padding(OrganicSpacing.comfortable) ) { Row( modifier = Modifier.fillMaxWidth(), @@ -594,7 +594,7 @@ fun ResidenceDetailScreen( text = residence.name, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onPrimaryContainer + color = MaterialTheme.colorScheme.textPrimary ) } } @@ -607,21 +607,60 @@ fun ResidenceDetailScreen( residence.stateProvince != null || residence.postalCode != null || residence.country != null) { item { - InfoCard( - icon = Icons.Default.LocationOn, - title = stringResource(Res.string.properties_address_section) + OrganicCard( + modifier = Modifier.fillMaxWidth(), + showBlob = true, + blobVariation = 1 ) { - if (residence.streetAddress != null) { - Text(text = residence.streetAddress) - } - if (residence.apartmentUnit != null) { - Text(text = "Unit: ${residence.apartmentUnit}") - } - if (residence.city != null || residence.stateProvince != null || residence.postalCode != null) { - Text(text = "${residence.city ?: ""}, ${residence.stateProvince ?: ""} ${residence.postalCode ?: ""}") - } - if (residence.country != null) { - Text(text = residence.country) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(OrganicSpacing.cozy), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact) + ) { + OrganicIconContainer( + icon = Icons.Default.LocationOn, + size = 40.dp, + iconScale = 0.5f, + backgroundColor = MaterialTheme.colorScheme.primary, + iconColor = MaterialTheme.colorScheme.onPrimary + ) + Text( + text = stringResource(Res.string.properties_address_section), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.textPrimary + ) + } + OrganicDivider(horizontalPadding = OrganicSpacing.compact) + if (residence.streetAddress != null) { + Text( + text = residence.streetAddress, + color = MaterialTheme.colorScheme.textSecondary + ) + } + if (residence.apartmentUnit != null) { + Text( + text = "Unit: ${residence.apartmentUnit}", + color = MaterialTheme.colorScheme.textSecondary + ) + } + if (residence.city != null || residence.stateProvince != null || residence.postalCode != null) { + Text( + text = "${residence.city ?: ""}, ${residence.stateProvince ?: ""} ${residence.postalCode ?: ""}", + color = MaterialTheme.colorScheme.textSecondary + ) + } + if (residence.country != null) { + Text( + text = residence.country, + color = MaterialTheme.colorScheme.textSecondary + ) + } } } } @@ -631,30 +670,57 @@ fun ResidenceDetailScreen( if (residence.bedrooms != null || residence.bathrooms != null || residence.squareFootage != null || residence.yearBuilt != null) { item { - InfoCard( - icon = Icons.Default.Info, - title = stringResource(Res.string.properties_property_details_section) + OrganicCard( + modifier = Modifier.fillMaxWidth(), + showBlob = true, + blobVariation = 2 ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly + Column( + modifier = Modifier + .fillMaxWidth() + .padding(OrganicSpacing.cozy), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact) ) { - residence.bedrooms?.let { - PropertyDetailItem(Icons.Default.Bed, "$it", "Bedrooms") + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact) + ) { + OrganicIconContainer( + icon = Icons.Default.Info, + size = 40.dp, + iconScale = 0.5f, + backgroundColor = MaterialTheme.colorScheme.primary, + iconColor = MaterialTheme.colorScheme.onPrimary + ) + Text( + text = stringResource(Res.string.properties_property_details_section), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.textPrimary + ) } - residence.bathrooms?.let { - PropertyDetailItem(Icons.Default.Bathroom, "$it", "Bathrooms") + OrganicDivider(horizontalPadding = OrganicSpacing.compact) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + residence.bedrooms?.let { + PropertyDetailItem(Icons.Default.Bed, "$it", "Bedrooms") + } + residence.bathrooms?.let { + PropertyDetailItem(Icons.Default.Bathroom, "$it", "Bathrooms") + } + } + Spacer(modifier = Modifier.height(OrganicSpacing.compact)) + residence.squareFootage?.let { + DetailRow(Icons.Default.SquareFoot, "Square Footage", "$it sq ft") + } + residence.lotSize?.let { + DetailRow(Icons.Default.Landscape, "Lot Size", "$it acres") + } + residence.yearBuilt?.let { + DetailRow(Icons.Default.CalendarToday, "Year Built", "$it") } - } - Spacer(modifier = Modifier.height(12.dp)) - residence.squareFootage?.let { - DetailRow(Icons.Default.SquareFoot, "Square Footage", "$it sq ft") - } - residence.lotSize?.let { - DetailRow(Icons.Default.Landscape, "Lot Size", "$it acres") - } - residence.yearBuilt?.let { - DetailRow(Icons.Default.CalendarToday, "Year Built", "$it") } } } @@ -663,15 +729,42 @@ fun ResidenceDetailScreen( // Description Card if (residence.description != null && !residence.description.isEmpty()) { item { - InfoCard( - icon = Icons.Default.Description, - title = stringResource(Res.string.properties_description_section) + OrganicCard( + modifier = Modifier.fillMaxWidth(), + showBlob = true, + blobVariation = 0 ) { - Text( - text = residence.description, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(OrganicSpacing.cozy), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact) + ) { + OrganicIconContainer( + icon = Icons.Default.Description, + size = 40.dp, + iconScale = 0.5f, + backgroundColor = MaterialTheme.colorScheme.primary, + iconColor = MaterialTheme.colorScheme.onPrimary + ) + Text( + text = stringResource(Res.string.properties_description_section), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.textPrimary + ) + } + OrganicDivider(horizontalPadding = OrganicSpacing.compact) + Text( + text = residence.description, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.textSecondary + ) + } } } } @@ -679,15 +772,42 @@ fun ResidenceDetailScreen( // Purchase Information if (residence.purchaseDate != null || residence.purchasePrice != null) { item { - InfoCard( - icon = Icons.Default.AttachMoney, - title = stringResource(Res.string.properties_purchase_info) + OrganicCard( + modifier = Modifier.fillMaxWidth(), + showBlob = true, + blobVariation = 1 ) { - residence.purchaseDate?.let { - DetailRow(Icons.Default.Event, "Purchase Date", DateUtils.formatDateMedium(it)) - } - residence.purchasePrice?.let { - DetailRow(Icons.Default.Payment, "Purchase Price", "$$it") + Column( + modifier = Modifier + .fillMaxWidth() + .padding(OrganicSpacing.cozy), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact) + ) { + OrganicIconContainer( + icon = Icons.Default.AttachMoney, + size = 40.dp, + iconScale = 0.5f, + backgroundColor = MaterialTheme.colorScheme.primary, + iconColor = MaterialTheme.colorScheme.onPrimary + ) + Text( + text = stringResource(Res.string.properties_purchase_info), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.textPrimary + ) + } + OrganicDivider(horizontalPadding = OrganicSpacing.compact) + residence.purchaseDate?.let { + DetailRow(Icons.Default.Event, "Purchase Date", DateUtils.formatDateMedium(it)) + } + residence.purchasePrice?.let { + DetailRow(Icons.Default.Payment, "Purchase Price", "$$it") + } } } } @@ -698,21 +818,22 @@ fun ResidenceDetailScreen( Row( modifier = Modifier .fillMaxWidth() - .padding(vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically + .padding(vertical = OrganicSpacing.compact), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact) ) { - Icon( - Icons.Default.Assignment, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(28.dp) + OrganicIconContainer( + icon = Icons.Default.Assignment, + size = 36.dp, + iconScale = 0.5f, + backgroundColor = MaterialTheme.colorScheme.primary, + iconColor = MaterialTheme.colorScheme.onPrimary ) - Spacer(modifier = Modifier.width(8.dp)) Text( text = stringResource(Res.string.tasks_title), style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary + color = MaterialTheme.colorScheme.textPrimary ) } } @@ -732,16 +853,15 @@ fun ResidenceDetailScreen( } is ApiResult.Error -> { item { - Card( - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.errorContainer - ), - shape = RoundedCornerShape(12.dp) + OrganicCard( + modifier = Modifier.fillMaxWidth(), + accentColor = MaterialTheme.colorScheme.error, + showBlob = false ) { Text( text = "Error loading tasks: ${com.example.casera.util.ErrorMessageParser.parse((tasksState as ApiResult.Error).message)}", - color = MaterialTheme.colorScheme.onErrorContainer, - modifier = Modifier.padding(16.dp) + color = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(OrganicSpacing.cozy) ) } } @@ -751,32 +871,35 @@ fun ResidenceDetailScreen( val allTasksEmpty = taskData.columns.all { it.tasks.isEmpty() } if (allTasksEmpty) { item { - Card( + OrganicCard( modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp) + showBlob = true, + blobVariation = 2 ) { Column( modifier = Modifier .fillMaxWidth() - .padding(32.dp), - horizontalAlignment = Alignment.CenterHorizontally + .padding(OrganicSpacing.spacious), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy) ) { - Icon( - Icons.Default.Assignment, - contentDescription = null, - modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f) + OrganicIconContainer( + icon = Icons.Default.Assignment, + size = 64.dp, + iconScale = 0.5f, + backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f), + iconColor = MaterialTheme.colorScheme.primary ) - Spacer(modifier = Modifier.height(16.dp)) Text( stringResource(Res.string.properties_no_tasks), style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.SemiBold + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.textPrimary ) Text( stringResource(Res.string.properties_add_task_start), style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.textSecondary ) } } @@ -833,25 +956,26 @@ fun ResidenceDetailScreen( // Contractors Section Header item { - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(OrganicSpacing.cozy)) Row( modifier = Modifier .fillMaxWidth() - .padding(vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically + .padding(vertical = OrganicSpacing.compact), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact) ) { - Icon( - Icons.Default.People, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(28.dp) + OrganicIconContainer( + icon = Icons.Default.People, + size = 36.dp, + iconScale = 0.5f, + backgroundColor = MaterialTheme.colorScheme.primary, + iconColor = MaterialTheme.colorScheme.onPrimary ) - Spacer(modifier = Modifier.width(8.dp)) Text( text = stringResource(Res.string.contractors_title), style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary + color = MaterialTheme.colorScheme.textPrimary ) } } @@ -871,16 +995,15 @@ fun ResidenceDetailScreen( } is ApiResult.Error -> { item { - Card( - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.errorContainer - ), - shape = RoundedCornerShape(12.dp) + OrganicCard( + modifier = Modifier.fillMaxWidth(), + accentColor = MaterialTheme.colorScheme.error, + showBlob = false ) { Text( text = "Error loading contractors: ${com.example.casera.util.ErrorMessageParser.parse((contractorsState as ApiResult.Error).message)}", - color = MaterialTheme.colorScheme.onErrorContainer, - modifier = Modifier.padding(16.dp) + color = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(OrganicSpacing.cozy) ) } } @@ -889,32 +1012,35 @@ fun ResidenceDetailScreen( val contractors = (contractorsState as ApiResult.Success>).data if (contractors.isEmpty()) { item { - Card( + OrganicCard( modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp) + showBlob = true, + blobVariation = 1 ) { Column( modifier = Modifier .fillMaxWidth() - .padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally + .padding(OrganicSpacing.comfortable), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact) ) { - Icon( - Icons.Default.PersonAdd, - contentDescription = null, - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + OrganicIconContainer( + icon = Icons.Default.PersonAdd, + size = 56.dp, + iconScale = 0.5f, + backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f), + iconColor = MaterialTheme.colorScheme.primary ) - Spacer(modifier = Modifier.height(12.dp)) Text( stringResource(Res.string.properties_no_contractors), style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.textPrimary ) Text( stringResource(Res.string.properties_add_contractors_hint), style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.textSecondary ) } } @@ -939,4 +1065,5 @@ fun ResidenceDetailScreen( } } } + } } diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ResidenceFormScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ResidenceFormScreen.kt index 535b812..dea5012 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ResidenceFormScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ResidenceFormScreen.kt @@ -26,6 +26,7 @@ import com.example.casera.network.ResidenceApi import com.example.casera.storage.TokenStorage import com.example.casera.analytics.PostHogAnalytics import com.example.casera.analytics.AnalyticsEvents +import com.example.casera.ui.theme.* import casera.composeapp.generated.resources.* import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource @@ -170,282 +171,276 @@ fun ResidenceFormScreen( ) } ) { paddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - .padding(16.dp) - .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - // Basic Information section - Text( - text = stringResource(Res.string.properties_details), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary - ) - - OutlinedTextField( - value = name, - onValueChange = { name = it }, - label = { Text(stringResource(Res.string.properties_form_name_required)) }, - modifier = Modifier.fillMaxWidth(), - isError = nameError.isNotEmpty(), - supportingText = if (nameError.isNotEmpty()) { - { Text(nameError, color = MaterialTheme.colorScheme.error) } - } else { - { Text(stringResource(Res.string.properties_form_required), color = MaterialTheme.colorScheme.error) } - } - ) - - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { expanded = it } + WarmGradientBackground { + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(OrganicSpacing.cozy) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy) ) { - OutlinedTextField( - value = propertyType?.name?.replaceFirstChar { it.uppercase() } ?: "", - onValueChange = {}, - readOnly = true, - label = { Text(stringResource(Res.string.properties_type_label)) }, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, - modifier = Modifier - .fillMaxWidth() - .menuAnchor(), - enabled = propertyTypes.isNotEmpty() - ) - ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - propertyTypes.forEach { type -> - DropdownMenuItem( - text = { Text(type.name.replaceFirstChar { it.uppercase() }) }, - onClick = { - propertyType = type - expanded = false - } - ) - } - } - } - - // Address section - Text( - text = stringResource(Res.string.properties_address_section), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary - ) - - OutlinedTextField( - value = streetAddress, - onValueChange = { streetAddress = it }, - label = { Text(stringResource(Res.string.properties_form_street)) }, - modifier = Modifier.fillMaxWidth() - ) - - OutlinedTextField( - value = apartmentUnit, - onValueChange = { apartmentUnit = it }, - label = { Text(stringResource(Res.string.properties_form_apartment)) }, - modifier = Modifier.fillMaxWidth() - ) - - OutlinedTextField( - value = city, - onValueChange = { city = it }, - label = { Text(stringResource(Res.string.properties_form_city)) }, - modifier = Modifier.fillMaxWidth() - ) - - OutlinedTextField( - value = stateProvince, - onValueChange = { stateProvince = it }, - label = { Text(stringResource(Res.string.properties_form_state)) }, - modifier = Modifier.fillMaxWidth() - ) - - OutlinedTextField( - value = postalCode, - onValueChange = { postalCode = it }, - label = { Text(stringResource(Res.string.properties_form_postal)) }, - modifier = Modifier.fillMaxWidth() - ) - - OutlinedTextField( - value = country, - onValueChange = { country = it }, - label = { Text(stringResource(Res.string.properties_form_country)) }, - modifier = Modifier.fillMaxWidth() - ) - - // Optional fields section - Divider() - Text( - text = stringResource(Res.string.properties_form_optional), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary - ) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - OutlinedTextField( - value = bedrooms, - onValueChange = { bedrooms = it.filter { char -> char.isDigit() } }, - label = { Text(stringResource(Res.string.properties_bedrooms)) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.weight(1f) - ) - - OutlinedTextField( - value = bathrooms, - onValueChange = { bathrooms = it }, - label = { Text(stringResource(Res.string.properties_bathrooms)) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), - modifier = Modifier.weight(1f) - ) - } - - OutlinedTextField( - value = squareFootage, - onValueChange = { squareFootage = it.filter { char -> char.isDigit() } }, - label = { Text(stringResource(Res.string.properties_form_sqft)) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.fillMaxWidth() - ) - - OutlinedTextField( - value = lotSize, - onValueChange = { lotSize = it }, - label = { Text(stringResource(Res.string.properties_form_lot_size)) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), - modifier = Modifier.fillMaxWidth() - ) - - OutlinedTextField( - value = yearBuilt, - onValueChange = { yearBuilt = it.filter { char -> char.isDigit() } }, - label = { Text(stringResource(Res.string.properties_year_built)) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.fillMaxWidth() - ) - - OutlinedTextField( - value = description, - onValueChange = { description = it }, - label = { Text(stringResource(Res.string.properties_form_description)) }, - modifier = Modifier.fillMaxWidth(), - minLines = 3, - maxLines = 5 - ) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text(stringResource(Res.string.properties_form_primary)) - Switch( - checked = isPrimary, - onCheckedChange = { isPrimary = it } - ) - } - - // Users section (edit mode only, owner only) - if (isEditMode && isCurrentUserOwner) { - Divider() + // Basic Information section Text( - text = "Shared Users (${users.size})", + text = stringResource(Res.string.properties_details), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary ) - if (isLoadingUsers) { - Box( - modifier = Modifier.fillMaxWidth().padding(16.dp), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator(modifier = Modifier.size(24.dp)) + OutlinedTextField( + value = name, + onValueChange = { name = it }, + label = { Text(stringResource(Res.string.properties_form_name_required)) }, + modifier = Modifier.fillMaxWidth(), + isError = nameError.isNotEmpty(), + supportingText = if (nameError.isNotEmpty()) { + { Text(nameError, color = MaterialTheme.colorScheme.error) } + } else { + { Text(stringResource(Res.string.properties_form_required), color = MaterialTheme.colorScheme.error) } } - } else if (users.isEmpty()) { - Text( - text = "No shared users", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(vertical = 8.dp) + ) + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = it } + ) { + OutlinedTextField( + value = propertyType?.name?.replaceFirstChar { it.uppercase() } ?: "", + onValueChange = {}, + readOnly = true, + label = { Text(stringResource(Res.string.properties_type_label)) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + modifier = Modifier + .fillMaxWidth() + .menuAnchor(), + enabled = propertyTypes.isNotEmpty() ) - } else { - users.forEach { user -> - UserListItem( - user = user, - onRemove = { - userToRemove = user - showRemoveUserConfirmation = true - } - ) - } - } - - Text( - text = "Users with access to this residence. Use the share button to invite others.", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(top = 4.dp) - ) - } - - // Error message - if (operationState is ApiResult.Error) { - Text( - text = com.example.casera.util.ErrorMessageParser.parse((operationState as ApiResult.Error).message), - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall - ) - } - - // Submit button - Button( - onClick = { - if (validateForm()) { - val request = ResidenceCreateRequest( - name = name, - propertyTypeId = propertyType?.id, - streetAddress = streetAddress.ifBlank { null }, - apartmentUnit = apartmentUnit.ifBlank { null }, - city = city.ifBlank { null }, - stateProvince = stateProvince.ifBlank { null }, - postalCode = postalCode.ifBlank { null }, - country = country.ifBlank { null }, - bedrooms = bedrooms.toIntOrNull(), - bathrooms = bathrooms.toDoubleOrNull(), - squareFootage = squareFootage.toIntOrNull(), - lotSize = lotSize.toDoubleOrNull(), - yearBuilt = yearBuilt.toIntOrNull(), - description = description.ifBlank { null }, - isPrimary = isPrimary - ) - - if (isEditMode && existingResidence != null) { - viewModel.updateResidence(existingResidence.id, request) - } else { - viewModel.createResidence(request) + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + propertyTypes.forEach { type -> + DropdownMenuItem( + text = { Text(type.name.replaceFirstChar { it.uppercase() }) }, + onClick = { + propertyType = type + expanded = false + } + ) } } - }, - modifier = Modifier.fillMaxWidth(), - enabled = validateForm() - ) { - if (operationState is ApiResult.Loading) { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - color = MaterialTheme.colorScheme.onPrimary - ) - } else { - Text(if (isEditMode) stringResource(Res.string.properties_form_update) else stringResource(Res.string.properties_form_create)) } - } - Spacer(modifier = Modifier.height(16.dp)) + // Address section + Text( + text = stringResource(Res.string.properties_address_section), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary + ) + + OutlinedTextField( + value = streetAddress, + onValueChange = { streetAddress = it }, + label = { Text(stringResource(Res.string.properties_form_street)) }, + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = apartmentUnit, + onValueChange = { apartmentUnit = it }, + label = { Text(stringResource(Res.string.properties_form_apartment)) }, + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = city, + onValueChange = { city = it }, + label = { Text(stringResource(Res.string.properties_form_city)) }, + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = stateProvince, + onValueChange = { stateProvince = it }, + label = { Text(stringResource(Res.string.properties_form_state)) }, + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = postalCode, + onValueChange = { postalCode = it }, + label = { Text(stringResource(Res.string.properties_form_postal)) }, + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = country, + onValueChange = { country = it }, + label = { Text(stringResource(Res.string.properties_form_country)) }, + modifier = Modifier.fillMaxWidth() + ) + + // Optional fields section + OrganicDivider() + Text( + text = stringResource(Res.string.properties_form_optional), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact) + ) { + OutlinedTextField( + value = bedrooms, + onValueChange = { bedrooms = it.filter { char -> char.isDigit() } }, + label = { Text(stringResource(Res.string.properties_bedrooms)) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.weight(1f) + ) + + OutlinedTextField( + value = bathrooms, + onValueChange = { bathrooms = it }, + label = { Text(stringResource(Res.string.properties_bathrooms)) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + modifier = Modifier.weight(1f) + ) + } + + OutlinedTextField( + value = squareFootage, + onValueChange = { squareFootage = it.filter { char -> char.isDigit() } }, + label = { Text(stringResource(Res.string.properties_form_sqft)) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = lotSize, + onValueChange = { lotSize = it }, + label = { Text(stringResource(Res.string.properties_form_lot_size)) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = yearBuilt, + onValueChange = { yearBuilt = it.filter { char -> char.isDigit() } }, + label = { Text(stringResource(Res.string.properties_year_built)) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = description, + onValueChange = { description = it }, + label = { Text(stringResource(Res.string.properties_form_description)) }, + modifier = Modifier.fillMaxWidth(), + minLines = 3, + maxLines = 5 + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(stringResource(Res.string.properties_form_primary)) + Switch( + checked = isPrimary, + onCheckedChange = { isPrimary = it } + ) + } + + // Users section (edit mode only, owner only) + if (isEditMode && isCurrentUserOwner) { + OrganicDivider() + Text( + text = "Shared Users (${users.size})", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary + ) + + if (isLoadingUsers) { + Box( + modifier = Modifier.fillMaxWidth().padding(OrganicSpacing.cozy), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(modifier = Modifier.size(24.dp)) + } + } else if (users.isEmpty()) { + Text( + text = "No shared users", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(vertical = OrganicSpacing.compact) + ) + } else { + users.forEach { user -> + UserListItem( + user = user, + onRemove = { + userToRemove = user + showRemoveUserConfirmation = true + } + ) + } + } + + Text( + text = "Users with access to this residence. Use the share button to invite others.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 4.dp) + ) + } + + // Error message + if (operationState is ApiResult.Error) { + Text( + text = com.example.casera.util.ErrorMessageParser.parse((operationState as ApiResult.Error).message), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall + ) + } + + // Submit button + OrganicPrimaryButton( + text = if (isEditMode) stringResource(Res.string.properties_form_update) else stringResource(Res.string.properties_form_create), + onClick = { + if (validateForm()) { + val request = ResidenceCreateRequest( + name = name, + propertyTypeId = propertyType?.id, + streetAddress = streetAddress.ifBlank { null }, + apartmentUnit = apartmentUnit.ifBlank { null }, + city = city.ifBlank { null }, + stateProvince = stateProvince.ifBlank { null }, + postalCode = postalCode.ifBlank { null }, + country = country.ifBlank { null }, + bedrooms = bedrooms.toIntOrNull(), + bathrooms = bathrooms.toDoubleOrNull(), + squareFootage = squareFootage.toIntOrNull(), + lotSize = lotSize.toDoubleOrNull(), + yearBuilt = yearBuilt.toIntOrNull(), + description = description.ifBlank { null }, + isPrimary = isPrimary + ) + + if (isEditMode && existingResidence != null) { + viewModel.updateResidence(existingResidence.id, request) + } else { + viewModel.createResidence(request) + } + } + }, + enabled = validateForm(), + isLoading = operationState is ApiResult.Loading + ) + + Spacer(modifier = Modifier.height(OrganicSpacing.cozy)) + } } } @@ -508,8 +503,9 @@ private fun UserListItem( user: ResidenceUser, onRemove: () -> Unit ) { - Card( - modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp) + OrganicCard( + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), + showBlob = false ) { Row( modifier = Modifier.fillMaxWidth().padding(12.dp), diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ResidencesScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ResidencesScreen.kt index 451b8fd..46dad55 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ResidencesScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ResidencesScreen.kt @@ -37,6 +37,7 @@ import com.example.casera.cache.SubscriptionCache import com.example.casera.analytics.PostHogAnalytics import com.example.casera.analytics.AnalyticsEvents import com.example.casera.data.DataManager +import com.example.casera.ui.theme.* import casera.composeapp.generated.resources.* import org.jetbrains.compose.resources.stringResource @@ -225,14 +226,14 @@ fun ResidencesScreen( ) { Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier.padding(24.dp) + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy), + modifier = Modifier.padding(OrganicSpacing.comfortable) ) { - Icon( - Icons.Default.Home, - contentDescription = null, - modifier = Modifier.size(80.dp), - tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f) + OrganicIconContainer( + icon = Icons.Default.Home, + size = 80.dp, + iconScale = 0.6f, + backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f) ) Text( stringResource(Res.string.properties_empty_title), @@ -244,7 +245,7 @@ fun ResidencesScreen( style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(OrganicSpacing.compact)) // Only show Add Property button if not blocked (limit>0) if (!isBlocked.allowed) { Button( @@ -263,7 +264,7 @@ fun ResidencesScreen( shape = RoundedCornerShape(12.dp) ) { Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact), verticalAlignment = Alignment.CenterVertically ) { Icon(Icons.Default.Add, contentDescription = null) @@ -274,7 +275,7 @@ fun ResidencesScreen( ) } } - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(OrganicSpacing.compact)) OutlinedButton( onClick = { val (allowed, triggerKey) = canAddProperty() @@ -291,7 +292,7 @@ fun ResidencesScreen( shape = RoundedCornerShape(12.dp) ) { Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact), verticalAlignment = Alignment.CenterVertically ) { Icon(Icons.Default.GroupAdd, contentDescription = null) @@ -315,7 +316,7 @@ fun ResidencesScreen( shape = RoundedCornerShape(12.dp) ) { Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact), verticalAlignment = Alignment.CenterVertically ) { Icon(Icons.Default.Star, contentDescription = null) @@ -344,28 +345,27 @@ fun ResidencesScreen( LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues( - start = 16.dp, - end = 16.dp, - top = 16.dp, + start = OrganicSpacing.cozy, + end = OrganicSpacing.cozy, + top = OrganicSpacing.cozy, bottom = 96.dp ), - verticalArrangement = Arrangement.spacedBy(16.dp) + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy) ) { // Summary Card item { - Card( + OrganicCard( modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer - ), - shape = RoundedCornerShape(20.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) + accentColor = MaterialTheme.colorScheme.primary, + showBlob = true, + blobVariation = 0, + shadowIntensity = ShadowIntensity.Medium ) { Column( modifier = Modifier .fillMaxWidth() - .padding(20.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) + .padding(OrganicSpacing.cozy), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy) ) { Row( verticalAlignment = Alignment.CenterVertically @@ -373,15 +373,15 @@ fun ResidencesScreen( Icon( Icons.Default.Dashboard, contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimaryContainer, + tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(24.dp) ) - Spacer(modifier = Modifier.width(8.dp)) + Spacer(modifier = Modifier.width(OrganicSpacing.compact)) Text( text = stringResource(Res.string.home_overview), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onPrimaryContainer + color = MaterialTheme.colorScheme.textPrimary ) } @@ -401,8 +401,8 @@ fun ResidencesScreen( ) } - HorizontalDivider( - color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.2f) + OrganicDivider( + color = MaterialTheme.colorScheme.textSecondary.copy(alpha = 0.2f) ) Row( @@ -436,7 +436,7 @@ fun ResidencesScreen( text = stringResource(Res.string.home_your_properties), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, - modifier = Modifier.padding(top = 8.dp) + modifier = Modifier.padding(top = OrganicSpacing.compact) ) } @@ -456,46 +456,41 @@ fun ResidencesScreen( label = "pulseScale" ) - Card( + OrganicCard( modifier = Modifier .fillMaxWidth() .clickable { onResidenceClick(residence.id) }, - shape = MaterialTheme.shapes.large, - elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ) + accentColor = if (hasOverdue) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary, + showBlob = true, + blobVariation = residence.id % 3, + shadowIntensity = ShadowIntensity.Subtle ) { Column( modifier = Modifier .fillMaxWidth() - .padding(20.dp) + .padding(OrganicSpacing.cozy) ) { Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(16.dp), + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy), verticalAlignment = Alignment.CenterVertically ) { - // Pulsing circular house icon when overdue + // Pulsing organic icon container when overdue Box( - modifier = Modifier - .size(56.dp) - .then( - if (hasOverdue) Modifier.scale(pulseScale) else Modifier - ) - .clip(CircleShape) - .background( - if (hasOverdue) MaterialTheme.colorScheme.errorContainer - else MaterialTheme.colorScheme.primaryContainer - ), - contentAlignment = Alignment.Center + modifier = if (hasOverdue) Modifier.scale(pulseScale) else Modifier ) { - Icon( - Icons.Default.Home, - contentDescription = null, - tint = if (hasOverdue) MaterialTheme.colorScheme.onErrorContainer - else MaterialTheme.colorScheme.onPrimaryContainer, - modifier = Modifier.size(28.dp) + OrganicIconContainer( + icon = Icons.Default.Home, + size = 56.dp, + iconScale = 0.5f, + backgroundColor = if (hasOverdue) + MaterialTheme.colorScheme.errorContainer + else + MaterialTheme.colorScheme.primaryContainer, + iconColor = if (hasOverdue) + MaterialTheme.colorScheme.onErrorContainer + else + MaterialTheme.colorScheme.onPrimaryContainer ) } @@ -582,9 +577,9 @@ fun ResidencesScreen( ) } - Spacer(modifier = Modifier.height(16.dp)) - HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(OrganicSpacing.cozy)) + OrganicDivider(color = MaterialTheme.colorScheme.textSecondary.copy(alpha = 0.15f)) + Spacer(modifier = Modifier.height(OrganicSpacing.cozy)) // Fully dynamic task summary from API - show first 3 categories val displayCategories = residence.taskSummary.categories.take(3) diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/TasksScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/TasksScreen.kt index 31ea83c..b1581a8 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/TasksScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/TasksScreen.kt @@ -1,5 +1,6 @@ package com.example.casera.ui.screens +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -21,6 +22,7 @@ import com.example.casera.viewmodel.TaskViewModel import com.example.casera.network.ApiResult import com.example.casera.analytics.PostHogAnalytics import com.example.casera.analytics.AnalyticsEvents +import com.example.casera.ui.theme.* import casera.composeapp.generated.resources.* import org.jetbrains.compose.resources.stringResource @@ -66,163 +68,116 @@ fun TasksScreen( } } - Scaffold( - topBar = { - TopAppBar( - title = { Text(stringResource(Res.string.tasks_title)) }, - navigationIcon = { - IconButton(onClick = onNavigateBack) { - Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back)) - } - } - ) - }, - // No FAB on Tasks screen - tasks are added from within residences - ) { paddingValues -> - when (tasksState) { - is ApiResult.Idle, is ApiResult.Loading, is ApiResult.Error -> { - Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), - contentAlignment = androidx.compose.ui.Alignment.Center - ) { - CircularProgressIndicator() - } - } - is ApiResult.Success -> { - val taskData = (tasksState as ApiResult.Success).data - val hasNoTasks = taskData.columns.all { it.tasks.isEmpty() } - - if (hasNoTasks) { + WarmGradientBackground { + Scaffold( + containerColor = androidx.compose.ui.graphics.Color.Transparent, + topBar = { + TopAppBar( + title = { Text(stringResource(Res.string.tasks_title)) }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back)) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = androidx.compose.ui.graphics.Color.Transparent + ) + ) + }, + // No FAB on Tasks screen - tasks are added from within residences + ) { paddingValues -> + when (tasksState) { + is ApiResult.Idle, is ApiResult.Loading, is ApiResult.Error -> { Box( modifier = Modifier .fillMaxSize() .padding(paddingValues), contentAlignment = androidx.compose.ui.Alignment.Center ) { - Column( - horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier.padding(24.dp) - ) { - Icon( - Icons.Default.Assignment, - contentDescription = null, - modifier = Modifier.size(80.dp), - tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f) - ) - Text( - stringResource(Res.string.tasks_empty_title), - style = MaterialTheme.typography.headlineSmall, - fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold - ) - Text( - stringResource(Res.string.tasks_empty_subtitle), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } + CircularProgressIndicator( + color = MaterialTheme.colorScheme.primary + ) } - } else { - LazyColumn( - modifier = Modifier - .fillMaxSize(), - contentPadding = PaddingValues( - top = paddingValues.calculateTopPadding() + 16.dp, - bottom = paddingValues.calculateBottomPadding() + 16.dp, - start = 16.dp, - end = 16.dp - ), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - // Task summary pills - dynamically generated from all columns - item { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) + } + is ApiResult.Success -> { + val taskData = (tasksState as ApiResult.Success).data + val hasNoTasks = taskData.columns.all { it.tasks.isEmpty() } + + if (hasNoTasks) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + contentAlignment = androidx.compose.ui.Alignment.Center + ) { + Column( + horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy), + modifier = Modifier.padding(OrganicSpacing.comfortable) ) { - taskData.columns.forEach { column -> - TaskPill( - count = column.count, - label = column.displayName, - color = hexToColor(column.color) - ) - } + OrganicIconContainer( + icon = Icons.Default.Assignment, + size = 80.dp, + iconScale = 0.6f, + backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f), + iconColor = MaterialTheme.colorScheme.primary + ) + Text( + stringResource(Res.string.tasks_empty_title), + style = MaterialTheme.typography.headlineSmall, + fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold, + color = MaterialTheme.colorScheme.textPrimary + ) + Text( + stringResource(Res.string.tasks_empty_subtitle), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.textSecondary + ) } } - - // Dynamically render all columns - taskData.columns.forEachIndexed { index, column -> - if (column.tasks.isNotEmpty()) { - // First column (index 0) expanded by default, others collapsible - if (index == 0) { - // First column - always expanded, show tasks directly - item { - Text( - text = "${column.displayName} (${column.tasks.size})", - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(top = 8.dp) + } else { + LazyColumn( + modifier = Modifier + .fillMaxSize(), + contentPadding = PaddingValues( + top = paddingValues.calculateTopPadding() + OrganicSpacing.cozy, + bottom = paddingValues.calculateBottomPadding() + OrganicSpacing.cozy, + start = OrganicSpacing.cozy, + end = OrganicSpacing.cozy + ), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact) + ) { + // Task summary pills - dynamically generated from all columns + item { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact) + ) { + taskData.columns.forEach { column -> + TaskPill( + count = column.count, + label = column.displayName, + color = hexToColor(column.color) ) } + } + } - items(column.tasks) { task -> - TaskCard( - task = task, - onCompleteClick = { - selectedTask = task - showCompleteDialog = true - }, - onEditClick = { }, - onCancelClick = { }, - onUncancelClick = { } - ) - } - } else { - // Other columns - collapsible - val isExpanded = expandedColumns.contains(column.name) - - item { - Card( - modifier = Modifier.fillMaxWidth(), - onClick = { - expandedColumns = if (isExpanded) { - expandedColumns - column.name - } else { - expandedColumns + column.name - } - } - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = androidx.compose.ui.Alignment.CenterVertically - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = androidx.compose.ui.Alignment.CenterVertically - ) { - Icon( - getIconFromName(column.icons["android"] ?: "List"), - contentDescription = null, - tint = hexToColor(column.color) - ) - Text( - text = "${column.displayName} (${column.tasks.size})", - style = MaterialTheme.typography.titleMedium - ) - } - Icon( - if (isExpanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown, - contentDescription = if (isExpanded) "Collapse" else "Expand" - ) - } + // Dynamically render all columns + taskData.columns.forEachIndexed { index, column -> + if (column.tasks.isNotEmpty()) { + // First column (index 0) expanded by default, others collapsible + if (index == 0) { + // First column - always expanded, show tasks directly + item { + Text( + text = "${column.displayName} (${column.tasks.size})", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.textPrimary, + modifier = Modifier.padding(top = OrganicSpacing.compact) + ) } - } - if (isExpanded) { items(column.tasks) { task -> TaskCard( task = task, @@ -235,15 +190,80 @@ fun TasksScreen( onUncancelClick = { } ) } + } else { + // Other columns - collapsible + val isExpanded = expandedColumns.contains(column.name) + + item { + OrganicCard( + modifier = Modifier + .fillMaxWidth() + .clickable { + expandedColumns = if (isExpanded) { + expandedColumns - column.name + } else { + expandedColumns + column.name + } + }, + showBlob = false, + shadowIntensity = ShadowIntensity.Subtle + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(OrganicSpacing.cozy), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = androidx.compose.ui.Alignment.CenterVertically + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact), + verticalAlignment = androidx.compose.ui.Alignment.CenterVertically + ) { + OrganicIconContainer( + icon = getIconFromName(column.icons["android"] ?: "List"), + size = 40.dp, + iconScale = 0.5f, + backgroundColor = hexToColor(column.color).copy(alpha = 0.2f), + iconColor = hexToColor(column.color) + ) + Text( + text = "${column.displayName} (${column.tasks.size})", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.textPrimary + ) + } + Icon( + if (isExpanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown, + contentDescription = if (isExpanded) "Collapse" else "Expand", + tint = MaterialTheme.colorScheme.textSecondary + ) + } + } + } + + if (isExpanded) { + items(column.tasks) { task -> + TaskCard( + task = task, + onCompleteClick = { + selectedTask = task + showCompleteDialog = true + }, + onEditClick = { }, + onCancelClick = { }, + onUncancelClick = { } + ) + } + } } } } } } } - } - else -> {} + else -> {} + } } } diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/VerifyEmailScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/VerifyEmailScreen.kt index 4759915..ac5d312 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/VerifyEmailScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/VerifyEmailScreen.kt @@ -2,7 +2,6 @@ package com.example.casera.ui.screens import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -21,6 +20,7 @@ import com.example.casera.ui.components.auth.AuthHeader import com.example.casera.ui.components.common.ErrorCard import com.example.casera.viewmodel.AuthViewModel import com.example.casera.network.ApiResult +import com.example.casera.ui.theme.* import casera.composeapp.generated.resources.* import org.jetbrains.compose.resources.stringResource @@ -89,119 +89,118 @@ fun VerifyEmailScreen( ) } ) { paddingValues -> - 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)) - - AuthHeader( - icon = Icons.Default.MarkEmailRead, - title = stringResource(Res.string.auth_verify_title), - subtitle = stringResource(Res.string.auth_verify_subtitle) - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.errorContainer - ), - shape = RoundedCornerShape(12.dp) - ) { - Column( - modifier = Modifier.padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - Icons.Default.Info, - contentDescription = null, - tint = MaterialTheme.colorScheme.onErrorContainer - ) - Text( - text = "Email verification is required. Check your inbox for a 6-digit code.", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onErrorContainer, - textAlign = TextAlign.Center, - fontWeight = FontWeight.SemiBold - ) - } - } - - OutlinedTextField( - value = code, - onValueChange = { - if (it.length <= 6 && it.all { char -> char.isDigit() }) { - code = it - } - }, - label = { Text(stringResource(Res.string.auth_verify_code_label)) }, - leadingIcon = { - Icon(Icons.Default.Pin, contentDescription = null) - }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - shape = RoundedCornerShape(12.dp), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - placeholder = { Text("000000") } - ) - - if (errorMessage.isNotEmpty()) { - ErrorCard( - message = errorMessage - ) - } - - Button( - onClick = { - if (code.length == 6) { - isLoading = true - viewModel.verifyEmail(code) - } else { - errorMessage = "Please enter a valid 6-digit code" - } - }, + WarmGradientBackground { + Column( modifier = Modifier - .fillMaxWidth() - .height(56.dp), - shape = RoundedCornerShape(12.dp), - enabled = !isLoading && code.length == 6 + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + .padding(OrganicSpacing.comfortable), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy) ) { - if (isLoading) { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - color = MaterialTheme.colorScheme.onPrimary - ) - } else { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically + Spacer(modifier = Modifier.height(OrganicSpacing.compact)) + + OrganicIconContainer( + icon = Icons.Default.MarkEmailRead, + size = 80.dp, + iconScale = 0.5f, + backgroundColor = MaterialTheme.colorScheme.primary, + iconColor = MaterialTheme.colorScheme.onPrimary + ) + + Text( + text = stringResource(Res.string.auth_verify_title), + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.textPrimary, + textAlign = TextAlign.Center + ) + + Text( + text = stringResource(Res.string.auth_verify_subtitle), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.textSecondary, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(OrganicSpacing.cozy)) + + OrganicCard( + modifier = Modifier.fillMaxWidth(), + accentColor = MaterialTheme.colorScheme.error, + showBlob = false + ) { + Column( + modifier = Modifier.padding(OrganicSpacing.cozy), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact) ) { - Icon(Icons.Default.CheckCircle, contentDescription = null) + Icon( + Icons.Default.Info, + contentDescription = null, + tint = MaterialTheme.colorScheme.error + ) Text( - stringResource(Res.string.auth_verify_button), - style = MaterialTheme.typography.titleMedium, + text = "Email verification is required. Check your inbox for a 6-digit code.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.textPrimary, + textAlign = TextAlign.Center, fontWeight = FontWeight.SemiBold ) } } + + OutlinedTextField( + value = code, + onValueChange = { + if (it.length <= 6 && it.all { char -> char.isDigit() }) { + code = it + } + }, + label = { Text(stringResource(Res.string.auth_verify_code_label)) }, + leadingIcon = { + Icon(Icons.Default.Pin, contentDescription = null) + }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + placeholder = { Text("000000") } + ) + + if (errorMessage.isNotEmpty()) { + ErrorCard( + message = errorMessage + ) + } + + OrganicDivider( + modifier = Modifier.fillMaxWidth() + ) + + OrganicPrimaryButton( + text = stringResource(Res.string.auth_verify_button), + onClick = { + if (code.length == 6) { + isLoading = true + viewModel.verifyEmail(code) + } else { + errorMessage = "Please enter a valid 6-digit code" + } + }, + enabled = !isLoading && code.length == 6, + isLoading = isLoading + ) + + Spacer(modifier = Modifier.height(OrganicSpacing.compact)) + + Text( + text = "Didn't receive the code? Check your spam folder or contact support.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.textSecondary, + textAlign = TextAlign.Center + ) } - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = "Didn't receive the code? Check your spam folder or contact support.", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) } } } diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/VerifyResetCodeScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/VerifyResetCodeScreen.kt index c260d15..3030094 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/VerifyResetCodeScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/VerifyResetCodeScreen.kt @@ -1,8 +1,6 @@ package com.example.casera.ui.screens -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* @@ -19,6 +17,7 @@ import com.example.casera.ui.components.auth.AuthHeader import com.example.casera.ui.components.common.ErrorCard import com.example.casera.viewmodel.PasswordResetViewModel import com.example.casera.network.ApiResult +import com.example.casera.ui.theme.* import casera.composeapp.generated.resources.* import org.jetbrains.compose.resources.stringResource @@ -70,184 +69,185 @@ fun VerifyResetCodeScreen( ) } ) { paddingValues -> - Box( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .padding(paddingValues), - contentAlignment = Alignment.Center - ) { - Card( + WarmGradientBackground { + Box( modifier = Modifier - .fillMaxWidth(0.9f) - .wrapContentHeight(), - shape = RoundedCornerShape(24.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ), - elevation = CardDefaults.cardElevation(defaultElevation = 8.dp) + .fillMaxSize() + .padding(paddingValues), + contentAlignment = Alignment.Center ) { - Column( + OrganicCard( modifier = Modifier - .fillMaxWidth() - .padding(32.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(20.dp) + .fillMaxWidth(0.9f) + .wrapContentHeight(), + showBlob = true, + blobVariation = 1 ) { - AuthHeader( - icon = Icons.Default.MarkEmailRead, - title = "Check Your Email", - subtitle = "We sent a 6-digit code to" - ) - - Text( - email, - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.SemiBold, - textAlign = TextAlign.Center - ) - - Card( - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer - ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(OrganicSpacing.spacious), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy) ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - Icons.Default.Timer, - contentDescription = null, - tint = MaterialTheme.colorScheme.secondary - ) - Spacer(modifier = Modifier.width(12.dp)) - Text( - "Code expires in 15 minutes", - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.SemiBold - ) - } - } + OrganicIconContainer( + icon = Icons.Default.MarkEmailRead, + size = 80.dp, + iconScale = 0.5f, + backgroundColor = MaterialTheme.colorScheme.primary, + iconColor = MaterialTheme.colorScheme.onPrimary + ) - Spacer(modifier = Modifier.height(8.dp)) - - OutlinedTextField( - value = code, - onValueChange = { - if (it.length <= 6 && it.all { char -> char.isDigit() }) { - code = it - viewModel.resetVerifyCodeState() - } - }, - label = { Text(stringResource(Res.string.auth_verify_code_label)) }, - placeholder = { Text("000000") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - shape = RoundedCornerShape(12.dp), - enabled = !isLoading, - textStyle = MaterialTheme.typography.headlineMedium.copy( + Text( + text = "Check Your Email", + style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.textPrimary, textAlign = TextAlign.Center ) - ) - Text( - "Enter the 6-digit code from your email", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) + Text( + text = "We sent a 6-digit code to", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.textSecondary, + textAlign = TextAlign.Center + ) - ErrorCard(message = errorMessage) + Text( + email, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.textPrimary, + textAlign = TextAlign.Center + ) - if (isSuccess) { - Card( + OrganicCard( modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer - ) + accentColor = MaterialTheme.colorScheme.secondary, + showBlob = false ) { Row( modifier = Modifier .fillMaxWidth() - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically + .padding(OrganicSpacing.cozy), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact) ) { Icon( - Icons.Default.CheckCircle, + Icons.Default.Timer, contentDescription = null, - tint = MaterialTheme.colorScheme.primary + tint = MaterialTheme.colorScheme.secondary ) - Spacer(modifier = Modifier.width(12.dp)) Text( - "Code verified! Now set your new password", + "Code expires in 15 minutes", style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onPrimaryContainer + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.textPrimary ) } } - } - Button( - onClick = { - viewModel.verifyResetCode(email, code) - }, - modifier = Modifier - .fillMaxWidth() - .height(56.dp), - enabled = code.length == 6 && !isLoading, - shape = RoundedCornerShape(12.dp) - ) { - if (isLoading) { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - color = MaterialTheme.colorScheme.onPrimary, - strokeWidth = 2.dp - ) - } else { - Icon(Icons.Default.CheckCircle, contentDescription = null) - Spacer(modifier = Modifier.width(8.dp)) - Text( - stringResource(Res.string.auth_verify_button), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold - ) - } - } + Spacer(modifier = Modifier.height(OrganicSpacing.compact)) - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - "Didn't receive the code?", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + OutlinedTextField( + value = code, + onValueChange = { + if (it.length <= 6 && it.all { char -> char.isDigit() }) { + code = it + viewModel.resetVerifyCodeState() + } + }, + label = { Text(stringResource(Res.string.auth_verify_code_label)) }, + placeholder = { Text("000000") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + enabled = !isLoading, + textStyle = MaterialTheme.typography.headlineMedium.copy( + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) ) - TextButton(onClick = { - code = "" - viewModel.resetVerifyCodeState() - viewModel.moveToPreviousStep() - onNavigateBack() - }) { - Text( - stringResource(Res.string.auth_verify_resend), - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.SemiBold - ) - } - Text( - "Check your spam folder if you don't see it", + "Enter the 6-digit code from your email", style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = MaterialTheme.colorScheme.textSecondary, textAlign = TextAlign.Center ) + + ErrorCard(message = errorMessage) + + if (isSuccess) { + OrganicCard( + modifier = Modifier.fillMaxWidth(), + accentColor = MaterialTheme.colorScheme.primary, + showBlob = false + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(OrganicSpacing.cozy), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact) + ) { + Icon( + Icons.Default.CheckCircle, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + Text( + "Code verified! Now set your new password", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.textPrimary + ) + } + } + } + + OrganicDivider( + modifier = Modifier.fillMaxWidth() + ) + + OrganicPrimaryButton( + text = stringResource(Res.string.auth_verify_button), + onClick = { + viewModel.verifyResetCode(email, code) + }, + enabled = code.length == 6 && !isLoading, + isLoading = isLoading + ) + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact) + ) { + Text( + "Didn't receive the code?", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.textSecondary + ) + + TextButton(onClick = { + code = "" + viewModel.resetVerifyCodeState() + viewModel.moveToPreviousStep() + onNavigateBack() + }) { + Text( + stringResource(Res.string.auth_verify_resend), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold + ) + } + + Text( + "Check your spam folder if you don't see it", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.textSecondary, + textAlign = TextAlign.Center + ) + } } } } diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingCreateAccountContent.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingCreateAccountContent.kt index f1a7c6f..0e08a46 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingCreateAccountContent.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingCreateAccountContent.kt @@ -5,10 +5,8 @@ import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -17,15 +15,12 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.example.casera.network.ApiResult -import com.example.casera.ui.theme.AppRadius -import com.example.casera.ui.theme.AppSpacing +import com.example.casera.ui.theme.* import com.example.casera.viewmodel.OnboardingViewModel import casera.composeapp.generated.resources.* import org.jetbrains.compose.resources.stringResource @@ -65,245 +60,224 @@ fun OnboardingCreateAccountContent( password.isNotBlank() && password == confirmPassword - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(horizontal = AppSpacing.xl) + WarmGradientBackground( + modifier = Modifier.fillMaxSize() ) { - Spacer(modifier = Modifier.height(AppSpacing.xl)) - - // Header Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(AppSpacing.sm) + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = OrganicSpacing.xl) ) { - // Icon - Box( - modifier = Modifier - .size(80.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = Icons.Default.PersonAdd, - contentDescription = null, - modifier = Modifier.size(40.dp), - tint = MaterialTheme.colorScheme.primary - ) - } + Spacer(modifier = Modifier.height(OrganicSpacing.xl)) - Text( - text = stringResource(Res.string.onboarding_create_account_title), - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onBackground, - textAlign = TextAlign.Center - ) - - Text( - text = stringResource(Res.string.onboarding_create_account_subtitle), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) - } - - Spacer(modifier = Modifier.height(AppSpacing.xl * 2)) - - // Create with Email section - if (!isFormExpanded) { - // Collapsed state - show button - Button( - onClick = { isFormExpanded = true }, - modifier = Modifier - .fillMaxWidth() - .height(56.dp), - shape = RoundedCornerShape(AppRadius.md), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f), - contentColor = MaterialTheme.colorScheme.primary - ) - ) { - Icon(Icons.Default.Email, contentDescription = null) - Spacer(modifier = Modifier.width(AppSpacing.sm)) - Text( - text = stringResource(Res.string.onboarding_create_with_email), - fontWeight = FontWeight.Medium - ) - } - } - - // Expanded form - AnimatedVisibility( - visible = isFormExpanded, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { + // Header Column( - verticalArrangement = Arrangement.spacedBy(AppSpacing.md) + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm) ) { - // Username - OutlinedTextField( - value = username, - onValueChange = { - username = it - localErrorMessage = null - }, - label = { Text(stringResource(Res.string.auth_register_username)) }, - leadingIcon = { - Icon(Icons.Default.Person, contentDescription = null) - }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - shape = RoundedCornerShape(AppRadius.md), - enabled = !isLoading + // Icon + OrganicIconContainer( + icon = Icons.Default.PersonAdd, + size = 80.dp, + iconSize = 40.dp, + contentDescription = null ) - // Email - OutlinedTextField( - value = email, - onValueChange = { - email = it - localErrorMessage = null - }, - label = { Text(stringResource(Res.string.auth_register_email)) }, - leadingIcon = { - Icon(Icons.Default.Email, contentDescription = null) - }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - shape = RoundedCornerShape(AppRadius.md), - enabled = !isLoading + Text( + text = stringResource(Res.string.onboarding_create_account_title), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.Center ) - // Password - OutlinedTextField( - value = password, - onValueChange = { - password = it - localErrorMessage = null - }, - label = { Text(stringResource(Res.string.auth_register_password)) }, - leadingIcon = { - Icon(Icons.Default.Lock, contentDescription = null) - }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - visualTransformation = PasswordVisualTransformation(), - shape = RoundedCornerShape(AppRadius.md), - enabled = !isLoading + Text( + text = stringResource(Res.string.onboarding_create_account_subtitle), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center ) + } - // Confirm Password - OutlinedTextField( - value = confirmPassword, - onValueChange = { - confirmPassword = it - localErrorMessage = null - }, - label = { Text(stringResource(Res.string.auth_register_confirm_password)) }, - leadingIcon = { - Icon(Icons.Default.Lock, contentDescription = null) - }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - visualTransformation = PasswordVisualTransformation(), - shape = RoundedCornerShape(AppRadius.md), - enabled = !isLoading, - isError = confirmPassword.isNotEmpty() && password != confirmPassword, - supportingText = if (confirmPassword.isNotEmpty() && password != confirmPassword) { - { Text(stringResource(Res.string.auth_passwords_dont_match)) } - } else null - ) + Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2)) - // Error message - if (localErrorMessage != null) { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.errorContainer - ), - shape = RoundedCornerShape(AppRadius.md) - ) { - Row( - modifier = Modifier.padding(AppSpacing.md), - horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - Icons.Default.Error, - contentDescription = null, - tint = MaterialTheme.colorScheme.error - ) - Text( - text = localErrorMessage ?: "", - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall - ) - } - } - } - - Spacer(modifier = Modifier.height(AppSpacing.sm)) - - // Create Account button + // Create with Email section + if (!isFormExpanded) { + // Collapsed state - show button Button( - onClick = { - if (password == confirmPassword) { - viewModel.register(username, email, password) - } else { - localErrorMessage = "Passwords don't match" - } - }, + onClick = { isFormExpanded = true }, modifier = Modifier .fillMaxWidth() .height(56.dp), - shape = RoundedCornerShape(AppRadius.md), - enabled = isFormValid && !isLoading + shape = RoundedCornerShape(OrganicRadius.md), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f), + contentColor = MaterialTheme.colorScheme.primary + ) ) { - if (isLoading) { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - color = MaterialTheme.colorScheme.onPrimary, - strokeWidth = 2.dp - ) - } else { - Text( - text = stringResource(Res.string.auth_register_button), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold - ) - } + Icon(Icons.Default.Email, contentDescription = null) + Spacer(modifier = Modifier.width(OrganicSpacing.sm)) + Text( + text = stringResource(Res.string.onboarding_create_with_email), + fontWeight = FontWeight.Medium + ) } } - } - Spacer(modifier = Modifier.height(AppSpacing.xl)) + // Expanded form + AnimatedVisibility( + visible = isFormExpanded, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + Column( + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md) + ) { + // Username + OutlinedTextField( + value = username, + onValueChange = { + username = it + localErrorMessage = null + }, + label = { Text(stringResource(Res.string.auth_register_username)) }, + leadingIcon = { + Icon(Icons.Default.Person, contentDescription = null) + }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + shape = RoundedCornerShape(OrganicRadius.md), + enabled = !isLoading + ) - // Already have an account - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(Res.string.auth_have_account).substringBefore("?") + "?", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - TextButton(onClick = { showLoginDialog = true }) { - Text( - text = stringResource(Res.string.auth_login_button), - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.primary - ) + // Email + OutlinedTextField( + value = email, + onValueChange = { + email = it + localErrorMessage = null + }, + label = { Text(stringResource(Res.string.auth_register_email)) }, + leadingIcon = { + Icon(Icons.Default.Email, contentDescription = null) + }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + shape = RoundedCornerShape(OrganicRadius.md), + enabled = !isLoading + ) + + // Password + OutlinedTextField( + value = password, + onValueChange = { + password = it + localErrorMessage = null + }, + label = { Text(stringResource(Res.string.auth_register_password)) }, + leadingIcon = { + Icon(Icons.Default.Lock, contentDescription = null) + }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + visualTransformation = PasswordVisualTransformation(), + shape = RoundedCornerShape(OrganicRadius.md), + enabled = !isLoading + ) + + // Confirm Password + OutlinedTextField( + value = confirmPassword, + onValueChange = { + confirmPassword = it + localErrorMessage = null + }, + label = { Text(stringResource(Res.string.auth_register_confirm_password)) }, + leadingIcon = { + Icon(Icons.Default.Lock, contentDescription = null) + }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + visualTransformation = PasswordVisualTransformation(), + shape = RoundedCornerShape(OrganicRadius.md), + enabled = !isLoading, + isError = confirmPassword.isNotEmpty() && password != confirmPassword, + supportingText = if (confirmPassword.isNotEmpty() && password != confirmPassword) { + { Text(stringResource(Res.string.auth_passwords_dont_match)) } + } else null + ) + + // Error message + if (localErrorMessage != null) { + OrganicCard( + modifier = Modifier.fillMaxWidth(), + accentColor = MaterialTheme.colorScheme.error, + showBlob = false + ) { + Row( + modifier = Modifier.padding(OrganicSpacing.md), + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Default.Error, + contentDescription = null, + tint = MaterialTheme.colorScheme.error + ) + Text( + text = localErrorMessage ?: "", + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall + ) + } + } + } + + Spacer(modifier = Modifier.height(OrganicSpacing.sm)) + + // Create Account button + OrganicPrimaryButton( + text = stringResource(Res.string.auth_register_button), + onClick = { + if (password == confirmPassword) { + viewModel.register(username, email, password) + } else { + localErrorMessage = "Passwords don't match" + } + }, + modifier = Modifier.fillMaxWidth(), + enabled = isFormValid && !isLoading, + isLoading = isLoading + ) + } } - } - Spacer(modifier = Modifier.height(AppSpacing.xl * 2)) + Spacer(modifier = Modifier.height(OrganicSpacing.xl)) + + // Already have an account + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(Res.string.auth_have_account).substringBefore("?") + "?", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + TextButton(onClick = { showLoginDialog = true }) { + Text( + text = stringResource(Res.string.auth_login_button), + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.primary + ) + } + } + + Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2)) + } } // Login dialog @@ -356,7 +330,7 @@ private fun OnboardingLoginDialog( }, text = { Column( - verticalArrangement = Arrangement.spacedBy(AppSpacing.md) + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md) ) { OutlinedTextField( value = username, @@ -364,7 +338,7 @@ private fun OnboardingLoginDialog( label = { Text(stringResource(Res.string.auth_login_username_label)) }, singleLine = true, modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(AppRadius.md), + shape = RoundedCornerShape(OrganicRadius.md), enabled = !isLoading ) @@ -374,7 +348,7 @@ private fun OnboardingLoginDialog( label = { Text(stringResource(Res.string.auth_login_password_label)) }, singleLine = true, modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(AppRadius.md), + shape = RoundedCornerShape(OrganicRadius.md), visualTransformation = PasswordVisualTransformation(), enabled = !isLoading ) diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingFirstTaskContent.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingFirstTaskContent.kt index 997cb60..1e4ca9a 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingFirstTaskContent.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingFirstTaskContent.kt @@ -24,8 +24,7 @@ import androidx.compose.ui.unit.dp import com.example.casera.data.DataManager import com.example.casera.models.TaskCreateRequest import com.example.casera.network.ApiResult -import com.example.casera.ui.theme.AppRadius -import com.example.casera.ui.theme.AppSpacing +import com.example.casera.ui.theme.* import com.example.casera.viewmodel.OnboardingViewModel import casera.composeapp.generated.resources.* import com.example.casera.util.DateUtils @@ -163,7 +162,7 @@ fun OnboardingFirstTaskContent( modifier = Modifier .fillMaxWidth() .weight(1f), - contentPadding = PaddingValues(horizontal = AppSpacing.lg, vertical = AppSpacing.md) + contentPadding = PaddingValues(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.md) ) { // Header item { @@ -171,30 +170,19 @@ fun OnboardingFirstTaskContent( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { - // Celebration icon - Box( - modifier = Modifier - .size(80.dp) - .clip(CircleShape) - .background( - Brush.linearGradient( - colors = listOf( - MaterialTheme.colorScheme.primary, - MaterialTheme.colorScheme.secondary - ) - ) - ), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = Icons.Default.Celebration, - contentDescription = null, - modifier = Modifier.size(40.dp), - tint = Color.White - ) - } + // Celebration icon using OrganicIconContainer + OrganicIconContainer( + icon = Icons.Default.Celebration, + size = 80.dp, + iconSize = 40.dp, + gradientColors = listOf( + MaterialTheme.colorScheme.primary, + MaterialTheme.colorScheme.secondary + ), + contentDescription = null + ) - Spacer(modifier = Modifier.height(AppSpacing.lg)) + Spacer(modifier = Modifier.height(OrganicSpacing.lg)) Text( text = stringResource(Res.string.onboarding_tasks_title), @@ -203,7 +191,7 @@ fun OnboardingFirstTaskContent( color = MaterialTheme.colorScheme.onBackground ) - Spacer(modifier = Modifier.height(AppSpacing.sm)) + Spacer(modifier = Modifier.height(OrganicSpacing.sm)) Text( text = stringResource(Res.string.onboarding_tasks_subtitle), @@ -212,11 +200,11 @@ fun OnboardingFirstTaskContent( textAlign = TextAlign.Center ) - Spacer(modifier = Modifier.height(AppSpacing.lg)) + Spacer(modifier = Modifier.height(OrganicSpacing.lg)) // Selection counter Surface( - shape = RoundedCornerShape(AppRadius.xl), + shape = RoundedCornerShape(OrganicRadius.xl), color = if (isAtMaxSelection) { MaterialTheme.colorScheme.tertiary.copy(alpha = 0.1f) } else { @@ -224,8 +212,8 @@ fun OnboardingFirstTaskContent( } ) { Row( - modifier = Modifier.padding(horizontal = AppSpacing.lg, vertical = AppSpacing.sm), - horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm), + modifier = Modifier.padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.sm), + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm), verticalAlignment = Alignment.CenterVertically ) { Icon( @@ -243,7 +231,7 @@ fun OnboardingFirstTaskContent( } } - Spacer(modifier = Modifier.height(AppSpacing.xl)) + Spacer(modifier = Modifier.height(OrganicSpacing.xl)) } } @@ -267,7 +255,7 @@ fun OnboardingFirstTaskContent( } } ) - Spacer(modifier = Modifier.height(AppSpacing.md)) + Spacer(modifier = Modifier.height(OrganicSpacing.md)) } // Add popular tasks button @@ -291,7 +279,7 @@ fun OnboardingFirstTaskContent( modifier = Modifier .fillMaxWidth() .height(56.dp), - shape = RoundedCornerShape(AppRadius.lg), + shape = RoundedCornerShape(OrganicRadius.lg), border = ButtonDefaults.outlinedButtonBorder.copy( brush = Brush.linearGradient( colors = listOf( @@ -302,7 +290,7 @@ fun OnboardingFirstTaskContent( ) ) { Icon(Icons.Default.AutoAwesome, contentDescription = null) - Spacer(modifier = Modifier.width(AppSpacing.sm)) + Spacer(modifier = Modifier.width(OrganicSpacing.sm)) Text( text = stringResource(Res.string.onboarding_tasks_add_popular), fontWeight = FontWeight.Medium @@ -318,9 +306,14 @@ fun OnboardingFirstTaskContent( shadowElevation = 8.dp ) { Column( - modifier = Modifier.padding(AppSpacing.lg) + modifier = Modifier.padding(OrganicSpacing.lg) ) { - Button( + OrganicPrimaryButton( + text = if (selectedCount > 0) { + "Add $selectedCount Task${if (selectedCount == 1) "" else "s"} & Continue" + } else { + stringResource(Res.string.onboarding_tasks_skip) + }, onClick = { if (selectedTaskIds.isEmpty()) { onTasksAdded() @@ -360,32 +353,11 @@ fun OnboardingFirstTaskContent( } } }, - modifier = Modifier - .fillMaxWidth() - .height(56.dp), - shape = RoundedCornerShape(AppRadius.lg), - enabled = !isCreatingTasks - ) { - if (isCreatingTasks) { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - color = MaterialTheme.colorScheme.onPrimary, - strokeWidth = 2.dp - ) - } else { - Text( - text = if (selectedCount > 0) { - "Add $selectedCount Task${if (selectedCount == 1) "" else "s"} & Continue" - } else { - stringResource(Res.string.onboarding_tasks_skip) - }, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) - Spacer(modifier = Modifier.width(AppSpacing.sm)) - Icon(Icons.Default.ArrowForward, contentDescription = null) - } - } + modifier = Modifier.fillMaxWidth(), + enabled = !isCreatingTasks, + isLoading = isCreatingTasks, + icon = Icons.Default.ArrowForward + ) } } } @@ -402,39 +374,32 @@ private fun TaskCategorySection( ) { val selectedInCategory = category.tasks.count { it.id in selectedTaskIds } - Column( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(AppRadius.lg)) + OrganicCard( + modifier = Modifier.fillMaxWidth(), + accentColor = category.color, + showBlob = false ) { - // Header - Surface( - modifier = Modifier - .fillMaxWidth() - .clickable { onToggleExpand() }, - color = MaterialTheme.colorScheme.surfaceVariant + Column( + modifier = Modifier.fillMaxWidth() ) { + // Header Row( - modifier = Modifier.padding(AppSpacing.md), + modifier = Modifier + .fillMaxWidth() + .clickable { onToggleExpand() } + .padding(OrganicSpacing.md), verticalAlignment = Alignment.CenterVertically ) { // Category icon - Box( - modifier = Modifier - .size(44.dp) - .clip(CircleShape) - .background(category.color), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = category.icon, - contentDescription = null, - tint = Color.White, - modifier = Modifier.size(24.dp) - ) - } + OrganicIconContainer( + icon = category.icon, + size = 44.dp, + iconSize = 24.dp, + gradientColors = listOf(category.color), + contentDescription = null + ) - Spacer(modifier = Modifier.width(AppSpacing.md)) + Spacer(modifier = Modifier.width(OrganicSpacing.md)) Text( text = category.name, @@ -459,7 +424,7 @@ private fun TaskCategorySection( color = Color.White ) } - Spacer(modifier = Modifier.width(AppSpacing.sm)) + Spacer(modifier = Modifier.width(OrganicSpacing.sm)) } Icon( @@ -468,36 +433,34 @@ private fun TaskCategorySection( tint = MaterialTheme.colorScheme.onSurfaceVariant ) } - } - // Expanded content - AnimatedVisibility( - visible = isExpanded, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) + // Expanded content + AnimatedVisibility( + visible = isExpanded, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() ) { - category.tasks.forEachIndexed { index, task -> - val isSelected = task.id in selectedTaskIds - val isDisabled = isAtMaxSelection && !isSelected + Column( + modifier = Modifier.fillMaxWidth() + ) { + category.tasks.forEachIndexed { index, task -> + val isSelected = task.id in selectedTaskIds + val isDisabled = isAtMaxSelection && !isSelected - TaskTemplateRow( - task = task, - isSelected = isSelected, - isDisabled = isDisabled, - categoryColor = category.color, - onClick = { onToggleTask(task.id) } - ) - - if (index < category.tasks.lastIndex) { - Divider( - modifier = Modifier.padding(start = 60.dp), - color = MaterialTheme.colorScheme.outline.copy(alpha = 0.2f) + TaskTemplateRow( + task = task, + isSelected = isSelected, + isDisabled = isDisabled, + categoryColor = category.color, + onClick = { onToggleTask(task.id) } ) + + if (index < category.tasks.lastIndex) { + OrganicDivider( + modifier = Modifier.padding(start = 60.dp), + color = MaterialTheme.colorScheme.outline.copy(alpha = 0.2f) + ) + } } } } @@ -517,7 +480,7 @@ private fun TaskTemplateRow( modifier = Modifier .fillMaxWidth() .clickable(enabled = !isDisabled) { onClick() } - .padding(horizontal = AppSpacing.md, vertical = AppSpacing.sm), + .padding(horizontal = OrganicSpacing.md, vertical = OrganicSpacing.sm), verticalAlignment = Alignment.CenterVertically ) { // Checkbox @@ -541,7 +504,7 @@ private fun TaskTemplateRow( } } - Spacer(modifier = Modifier.width(AppSpacing.md)) + Spacer(modifier = Modifier.width(OrganicSpacing.md)) Column(modifier = Modifier.weight(1f)) { Text( diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingJoinResidenceContent.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingJoinResidenceContent.kt index f754cc7..d4997bd 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingJoinResidenceContent.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingJoinResidenceContent.kt @@ -1,8 +1,6 @@ package com.example.casera.ui.screens.onboarding -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons @@ -11,14 +9,12 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.example.casera.network.ApiResult -import com.example.casera.ui.theme.AppRadius -import com.example.casera.ui.theme.AppSpacing +import com.example.casera.ui.theme.* import com.example.casera.viewmodel.OnboardingViewModel import casera.composeapp.generated.resources.* import org.jetbrains.compose.resources.stringResource @@ -48,163 +44,142 @@ fun OnboardingJoinResidenceContent( val isLoading = joinState is ApiResult.Loading val isCodeValid = shareCode.length == 6 - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = AppSpacing.xl), - horizontalAlignment = Alignment.CenterHorizontally + WarmGradientBackground( + modifier = Modifier.fillMaxSize() ) { - Spacer(modifier = Modifier.weight(1f)) - - // Header Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(AppSpacing.lg) + modifier = Modifier + .fillMaxSize() + .padding(horizontal = OrganicSpacing.xl), + horizontalAlignment = Alignment.CenterHorizontally ) { - // Icon - Box( - modifier = Modifier - .size(100.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = Icons.Default.GroupAdd, - contentDescription = null, - modifier = Modifier.size(50.dp), - tint = MaterialTheme.colorScheme.primary - ) - } + Spacer(modifier = Modifier.weight(1f)) - // Title and subtitle + // Header Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(AppSpacing.sm) + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg) ) { - Text( - text = stringResource(Res.string.onboarding_join_title), - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onBackground + // Icon + OrganicIconContainer( + icon = Icons.Default.GroupAdd, + size = 100.dp, + iconSize = 50.dp, + contentDescription = null ) - Text( - text = stringResource(Res.string.onboarding_join_subtitle), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) - } - } + // Title and subtitle + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm) + ) { + Text( + text = stringResource(Res.string.onboarding_join_title), + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onBackground + ) - Spacer(modifier = Modifier.height(AppSpacing.xl * 2)) - - // Share code input - OutlinedTextField( - value = shareCode, - onValueChange = { - if (it.length <= 6) { - shareCode = it.uppercase() - localErrorMessage = null + Text( + text = stringResource(Res.string.onboarding_join_subtitle), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) } - }, - placeholder = { - Text( - stringResource(Res.string.onboarding_join_placeholder), - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) - ) - }, - leadingIcon = { - Icon(Icons.Default.Key, contentDescription = null) - }, - modifier = Modifier.fillMaxWidth(), - textStyle = LocalTextStyle.current.copy( - textAlign = TextAlign.Center, - fontWeight = FontWeight.Bold - ), - shape = RoundedCornerShape(AppRadius.md), - singleLine = true, - keyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.Characters - ), - enabled = !isLoading - ) + } - // Error message - if (localErrorMessage != null) { - Spacer(modifier = Modifier.height(AppSpacing.md)) - Card( + Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2)) + + // Share code input + OutlinedTextField( + value = shareCode, + onValueChange = { + if (it.length <= 6) { + shareCode = it.uppercase() + localErrorMessage = null + } + }, + placeholder = { + Text( + stringResource(Res.string.onboarding_join_placeholder), + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + ) + }, + leadingIcon = { + Icon(Icons.Default.Key, contentDescription = null) + }, modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.errorContainer + textStyle = LocalTextStyle.current.copy( + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold ), - shape = RoundedCornerShape(AppRadius.md) - ) { + shape = RoundedCornerShape(OrganicRadius.md), + singleLine = true, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.Characters + ), + enabled = !isLoading + ) + + // Error message + if (localErrorMessage != null) { + Spacer(modifier = Modifier.height(OrganicSpacing.md)) + OrganicCard( + modifier = Modifier.fillMaxWidth(), + accentColor = MaterialTheme.colorScheme.error, + showBlob = false + ) { + Row( + modifier = Modifier.padding(OrganicSpacing.md), + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Default.Error, + contentDescription = null, + tint = MaterialTheme.colorScheme.error + ) + Text( + text = localErrorMessage ?: "", + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall + ) + } + } + } + + // Loading indicator + if (isLoading) { + Spacer(modifier = Modifier.height(OrganicSpacing.md)) Row( - modifier = Modifier.padding(AppSpacing.md), - horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm), + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm), verticalAlignment = Alignment.CenterVertically ) { - Icon( - Icons.Default.Error, - contentDescription = null, - tint = MaterialTheme.colorScheme.error + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + strokeWidth = 2.dp ) Text( - text = localErrorMessage ?: "", - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall + text = "Joining residence...", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } } + + Spacer(modifier = Modifier.weight(1f)) + + // Join button + OrganicPrimaryButton( + text = stringResource(Res.string.onboarding_join_button), + onClick = { viewModel.joinResidence(shareCode) }, + modifier = Modifier.fillMaxWidth(), + enabled = isCodeValid && !isLoading, + isLoading = isLoading + ) + + Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2)) } - - // Loading indicator - if (isLoading) { - Spacer(modifier = Modifier.height(AppSpacing.md)) - Row( - horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm), - verticalAlignment = Alignment.CenterVertically - ) { - CircularProgressIndicator( - modifier = Modifier.size(20.dp), - strokeWidth = 2.dp - ) - Text( - text = "Joining residence...", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - Spacer(modifier = Modifier.weight(1f)) - - // Join button - Button( - onClick = { viewModel.joinResidence(shareCode) }, - modifier = Modifier - .fillMaxWidth() - .height(56.dp), - shape = RoundedCornerShape(AppRadius.md), - enabled = isCodeValid && !isLoading - ) { - if (isLoading) { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - color = MaterialTheme.colorScheme.onPrimary, - strokeWidth = 2.dp - ) - } else { - Text( - text = stringResource(Res.string.onboarding_join_button), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold - ) - } - } - - Spacer(modifier = Modifier.height(AppSpacing.xl * 2)) } } diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingNameResidenceContent.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingNameResidenceContent.kt index 6fcd70d..b59e714 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingNameResidenceContent.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingNameResidenceContent.kt @@ -1,8 +1,6 @@ package com.example.casera.ui.screens.onboarding -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowForward @@ -11,14 +9,10 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import com.example.casera.ui.theme.AppRadius -import com.example.casera.ui.theme.AppSpacing +import com.example.casera.ui.theme.* import com.example.casera.viewmodel.OnboardingViewModel import casera.composeapp.generated.resources.* import org.jetbrains.compose.resources.stringResource @@ -31,120 +25,95 @@ fun OnboardingNameResidenceContent( val residenceName by viewModel.residenceName.collectAsState() var localName by remember { mutableStateOf(residenceName) } - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = AppSpacing.xl), - horizontalAlignment = Alignment.CenterHorizontally + WarmGradientBackground( + modifier = Modifier.fillMaxSize() ) { - Spacer(modifier = Modifier.weight(1f)) - - // Header with icon Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(AppSpacing.lg) + modifier = Modifier + .fillMaxSize() + .padding(horizontal = OrganicSpacing.xl), + horizontalAlignment = Alignment.CenterHorizontally ) { - // Icon with gradient background - Box( - modifier = Modifier - .size(100.dp) - .shadow( - elevation = 16.dp, - shape = CircleShape, - spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.4f) - ) - .clip(CircleShape) - .background( - Brush.linearGradient( - colors = listOf( - MaterialTheme.colorScheme.primary, - MaterialTheme.colorScheme.primary.copy(alpha = 0.8f) - ) - ) - ), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = Icons.Default.Home, - contentDescription = null, - modifier = Modifier.size(50.dp), - tint = MaterialTheme.colorScheme.onPrimary - ) - } + Spacer(modifier = Modifier.weight(1f)) - // Title and subtitle + // Header with icon Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(AppSpacing.sm) + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg) ) { - Text( - text = stringResource(Res.string.onboarding_name_residence_title), - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onBackground + // Icon with OrganicIconContainer + OrganicIconContainer( + icon = Icons.Default.Home, + size = 100.dp, + iconSize = 50.dp, + contentDescription = null ) - Text( - text = stringResource(Res.string.onboarding_name_residence_subtitle), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) + // Title and subtitle + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm) + ) { + Text( + text = stringResource(Res.string.onboarding_name_residence_title), + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onBackground + ) + + Text( + text = stringResource(Res.string.onboarding_name_residence_subtitle), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + } } - } - Spacer(modifier = Modifier.height(AppSpacing.xl * 2)) + Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2)) - // Name input - OutlinedTextField( - value = localName, - onValueChange = { localName = it }, - placeholder = { - Text( - stringResource(Res.string.onboarding_name_residence_placeholder), - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + // Name input + OutlinedTextField( + value = localName, + onValueChange = { localName = it }, + placeholder = { + Text( + stringResource(Res.string.onboarding_name_residence_placeholder), + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + ) + }, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(OrganicRadius.md), + singleLine = true, + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MaterialTheme.colorScheme.primary, + unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f) ) - }, - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(AppRadius.md), - singleLine = true, - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f) ) - ) - Spacer(modifier = Modifier.height(AppSpacing.sm)) + Spacer(modifier = Modifier.height(OrganicSpacing.sm)) - Text( - text = stringResource(Res.string.onboarding_name_residence_hint), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) - ) - - Spacer(modifier = Modifier.weight(1f)) - - // Continue button - Button( - onClick = { - viewModel.setResidenceName(localName) - onContinue() - }, - modifier = Modifier - .fillMaxWidth() - .height(56.dp), - shape = RoundedCornerShape(AppRadius.md), - enabled = localName.isNotBlank() - ) { Text( - text = stringResource(Res.string.onboarding_continue), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold + text = stringResource(Res.string.onboarding_name_residence_hint), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) ) - Spacer(modifier = Modifier.width(AppSpacing.sm)) - Icon(Icons.Default.ArrowForward, contentDescription = null) - } - Spacer(modifier = Modifier.height(AppSpacing.xl * 2)) + Spacer(modifier = Modifier.weight(1f)) + + // Continue button + OrganicPrimaryButton( + text = stringResource(Res.string.onboarding_continue), + onClick = { + viewModel.setResidenceName(localName) + onContinue() + }, + modifier = Modifier.fillMaxWidth(), + enabled = localName.isNotBlank(), + icon = Icons.Default.ArrowForward + ) + + Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2)) + } } } diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingScreen.kt index 6944823..bc48b9b 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingScreen.kt @@ -14,7 +14,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import com.example.casera.ui.theme.AppSpacing +import com.example.casera.ui.theme.* import com.example.casera.viewmodel.OnboardingStep import com.example.casera.viewmodel.OnboardingViewModel import com.example.casera.viewmodel.OnboardingIntent @@ -189,7 +189,7 @@ private fun OnboardingNavigationBar( Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = AppSpacing.lg, vertical = AppSpacing.md), + .padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.md), verticalAlignment = Alignment.CenterVertically ) { // Back button @@ -240,7 +240,7 @@ fun OnboardingProgressIndicator( totalSteps: Int ) { Row( - horizontalArrangement = Arrangement.spacedBy(AppSpacing.xs), + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.xs), verticalAlignment = Alignment.CenterVertically ) { repeat(totalSteps) { index -> diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingSubscriptionContent.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingSubscriptionContent.kt index a3ffb56..30ec02a 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingSubscriptionContent.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingSubscriptionContent.kt @@ -23,8 +23,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import com.example.casera.ui.theme.AppRadius -import com.example.casera.ui.theme.AppSpacing +import com.example.casera.ui.theme.* import casera.composeapp.generated.resources.* import org.jetbrains.compose.resources.stringResource @@ -117,227 +116,201 @@ fun OnboardingSubscriptionContent( ) ) - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) + WarmGradientBackground( + modifier = Modifier.fillMaxSize() ) { Column( - modifier = Modifier.padding(horizontal = AppSpacing.xl), - horizontalAlignment = Alignment.CenterHorizontally + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) ) { - Spacer(modifier = Modifier.height(AppSpacing.lg)) - - // Crown header with animation - Box( - modifier = Modifier.size(180.dp), - contentAlignment = Alignment.Center + Column( + modifier = Modifier.padding(horizontal = OrganicSpacing.xl), + horizontalAlignment = Alignment.CenterHorizontally ) { - // Glow effect - Box( - modifier = Modifier - .size(140.dp) - .scale(scale) - .clip(CircleShape) - .background( - Brush.radialGradient( - colors = listOf( - MaterialTheme.colorScheme.tertiary.copy(alpha = 0.3f), - Color.Transparent - ) - ) - ) - ) + Spacer(modifier = Modifier.height(OrganicSpacing.lg)) - // Crown icon + // Crown header with animation Box( - modifier = Modifier - .size(100.dp) - .clip(CircleShape) - .background( - Brush.linearGradient( - colors = listOf( - MaterialTheme.colorScheme.tertiary, - Color(0xFFFF9500) - ) - ) - ), + modifier = Modifier.size(180.dp), contentAlignment = Alignment.Center ) { - Icon( - imageVector = Icons.Default.EmojiEvents, - contentDescription = null, - modifier = Modifier.size(50.dp), - tint = Color.White + // Glow effect + Box( + modifier = Modifier + .size(140.dp) + .scale(scale) + .clip(CircleShape) + .background( + Brush.radialGradient( + colors = listOf( + MaterialTheme.colorScheme.tertiary.copy(alpha = 0.3f), + Color.Transparent + ) + ) + ) + ) + + // Crown icon using OrganicIconContainer + OrganicIconContainer( + icon = Icons.Default.EmojiEvents, + size = 100.dp, + iconSize = 50.dp, + gradientColors = listOf( + MaterialTheme.colorScheme.tertiary, + Color(0xFFFF9500) + ), + contentDescription = null ) } - } - // PRO badge - Surface( - shape = RoundedCornerShape(AppRadius.full), - color = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.15f) - ) { + // PRO badge + Surface( + shape = RoundedCornerShape(OrganicRadius.full), + color = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.15f) + ) { + Row( + modifier = Modifier.padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.sm), + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.xs), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Default.AutoAwesome, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.tertiary + ) + Text( + text = stringResource(Res.string.onboarding_subscription_pro), + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Black, + color = MaterialTheme.colorScheme.tertiary + ) + Icon( + Icons.Default.AutoAwesome, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.tertiary + ) + } + } + + Spacer(modifier = Modifier.height(OrganicSpacing.md)) + + Text( + text = stringResource(Res.string.onboarding_subscription_subtitle), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(OrganicSpacing.sm)) + + // Social proof Row( - modifier = Modifier.padding(horizontal = AppSpacing.lg, vertical = AppSpacing.sm), - horizontalArrangement = Arrangement.spacedBy(AppSpacing.xs), + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.xs), verticalAlignment = Alignment.CenterVertically ) { - Icon( - Icons.Default.AutoAwesome, - contentDescription = null, - modifier = Modifier.size(16.dp), - tint = MaterialTheme.colorScheme.tertiary - ) + repeat(5) { + Icon( + Icons.Default.Star, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.tertiary + ) + } Text( - text = stringResource(Res.string.onboarding_subscription_pro), - style = MaterialTheme.typography.labelLarge, - fontWeight = FontWeight.Black, - color = MaterialTheme.colorScheme.tertiary - ) - Icon( - Icons.Default.AutoAwesome, - contentDescription = null, - modifier = Modifier.size(16.dp), - tint = MaterialTheme.colorScheme.tertiary + text = stringResource(Res.string.onboarding_subscription_social_proof), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } - } - Spacer(modifier = Modifier.height(AppSpacing.md)) + Spacer(modifier = Modifier.height(OrganicSpacing.xl)) - Text( - text = stringResource(Res.string.onboarding_subscription_subtitle), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onBackground, - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(AppSpacing.sm)) - - // Social proof - Row( - horizontalArrangement = Arrangement.spacedBy(AppSpacing.xs), - verticalAlignment = Alignment.CenterVertically - ) { - repeat(5) { - Icon( - Icons.Default.Star, - contentDescription = null, - modifier = Modifier.size(16.dp), - tint = MaterialTheme.colorScheme.tertiary - ) + // Benefits list + Column( + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm) + ) { + benefits.forEach { benefit -> + BenefitRow(benefit = benefit) + } } + + Spacer(modifier = Modifier.height(OrganicSpacing.xl)) + + // Plan selection Text( - text = stringResource(Res.string.onboarding_subscription_social_proof), + text = stringResource(Res.string.onboarding_subscription_choose_plan), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onBackground + ) + + Spacer(modifier = Modifier.height(OrganicSpacing.md)) + + // Yearly plan + PlanCard( + plan = SubscriptionPlan.YEARLY, + isSelected = selectedPlan == SubscriptionPlan.YEARLY, + onClick = { selectedPlan = SubscriptionPlan.YEARLY } + ) + + Spacer(modifier = Modifier.height(OrganicSpacing.sm)) + + // Monthly plan + PlanCard( + plan = SubscriptionPlan.MONTHLY, + isSelected = selectedPlan == SubscriptionPlan.MONTHLY, + onClick = { selectedPlan = SubscriptionPlan.MONTHLY } + ) + + Spacer(modifier = Modifier.height(OrganicSpacing.xl)) + + // Start trial button + OrganicPrimaryButton( + text = stringResource(Res.string.onboarding_subscription_start_trial), + onClick = { + isLoading = true + // Simulate subscription flow + onSubscribe() + }, + modifier = Modifier.fillMaxWidth(), + enabled = !isLoading, + isLoading = isLoading, + icon = Icons.Default.ArrowForward + ) + + Spacer(modifier = Modifier.height(OrganicSpacing.md)) + + // Continue free button + TextButton(onClick = onSkip) { + Text( + text = stringResource(Res.string.onboarding_subscription_continue_free), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + Spacer(modifier = Modifier.height(OrganicSpacing.sm)) + + // Legal text + Text( + text = "7-day free trial, then ${if (selectedPlan == SubscriptionPlan.YEARLY) "$23.99/year" else "$2.99/month"}", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) - } - Spacer(modifier = Modifier.height(AppSpacing.xl)) - - // Benefits list - Column( - verticalArrangement = Arrangement.spacedBy(AppSpacing.sm) - ) { - benefits.forEach { benefit -> - BenefitRow(benefit = benefit) - } - } - - Spacer(modifier = Modifier.height(AppSpacing.xl)) - - // Plan selection - Text( - text = stringResource(Res.string.onboarding_subscription_choose_plan), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onBackground - ) - - Spacer(modifier = Modifier.height(AppSpacing.md)) - - // Yearly plan - PlanCard( - plan = SubscriptionPlan.YEARLY, - isSelected = selectedPlan == SubscriptionPlan.YEARLY, - onClick = { selectedPlan = SubscriptionPlan.YEARLY } - ) - - Spacer(modifier = Modifier.height(AppSpacing.sm)) - - // Monthly plan - PlanCard( - plan = SubscriptionPlan.MONTHLY, - isSelected = selectedPlan == SubscriptionPlan.MONTHLY, - onClick = { selectedPlan = SubscriptionPlan.MONTHLY } - ) - - Spacer(modifier = Modifier.height(AppSpacing.xl)) - - // Start trial button - Button( - onClick = { - isLoading = true - // Simulate subscription flow - onSubscribe() - }, - modifier = Modifier - .fillMaxWidth() - .height(56.dp), - shape = RoundedCornerShape(AppRadius.lg), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.tertiary - ), - enabled = !isLoading - ) { - if (isLoading) { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - color = MaterialTheme.colorScheme.onTertiary, - strokeWidth = 2.dp - ) - } else { - Text( - text = stringResource(Res.string.onboarding_subscription_start_trial), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) - Spacer(modifier = Modifier.width(AppSpacing.sm)) - Icon(Icons.Default.ArrowForward, contentDescription = null) - } - } - - Spacer(modifier = Modifier.height(AppSpacing.md)) - - // Continue free button - TextButton(onClick = onSkip) { Text( - text = stringResource(Res.string.onboarding_subscription_continue_free), - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "Cancel anytime in Settings • No commitment", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) ) + + Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2)) } - - Spacer(modifier = Modifier.height(AppSpacing.sm)) - - // Legal text - Text( - text = "7-day free trial, then ${if (selectedPlan == SubscriptionPlan.YEARLY) "$23.99/year" else "$2.99/month"}", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Text( - text = "Cancel anytime in Settings • No commitment", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) - ) - - Spacer(modifier = Modifier.height(AppSpacing.xl * 2)) } } } @@ -347,28 +320,19 @@ private fun BenefitRow(benefit: SubscriptionBenefit) { Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = AppSpacing.md, vertical = AppSpacing.sm), + .padding(horizontal = OrganicSpacing.md, vertical = OrganicSpacing.sm), verticalAlignment = Alignment.CenterVertically ) { - // Gradient icon - Box( - modifier = Modifier - .size(44.dp) - .clip(CircleShape) - .background( - Brush.linearGradient(benefit.gradientColors) - ), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = benefit.icon, - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = Color.White - ) - } + // Gradient icon using OrganicIconContainer + OrganicIconContainer( + icon = benefit.icon, + size = 44.dp, + iconSize = 24.dp, + gradientColors = benefit.gradientColors, + contentDescription = null + ) - Spacer(modifier = Modifier.width(AppSpacing.md)) + Spacer(modifier = Modifier.width(OrganicSpacing.md)) Column(modifier = Modifier.weight(1f)) { Text( @@ -402,26 +366,15 @@ private fun PlanCard( ) { val isYearly = plan == SubscriptionPlan.YEARLY - Surface( + OrganicCard( modifier = Modifier .fillMaxWidth() .clickable { onClick() }, - shape = RoundedCornerShape(AppRadius.lg), - color = MaterialTheme.colorScheme.surfaceVariant, - border = if (isSelected) { - ButtonDefaults.outlinedButtonBorder.copy( - brush = Brush.linearGradient( - colors = listOf( - MaterialTheme.colorScheme.tertiary, - Color(0xFFFF9500) - ) - ), - width = 2.dp - ) - } else null + accentColor = if (isSelected) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.surfaceVariant, + showBlob = isSelected ) { Row( - modifier = Modifier.padding(AppSpacing.lg), + modifier = Modifier.padding(OrganicSpacing.lg), verticalAlignment = Alignment.CenterVertically ) { // Selection indicator @@ -446,11 +399,11 @@ private fun PlanCard( } } - Spacer(modifier = Modifier.width(AppSpacing.md)) + Spacer(modifier = Modifier.width(OrganicSpacing.md)) Column(modifier = Modifier.weight(1f)) { Row( - horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm), + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm), verticalAlignment = Alignment.CenterVertically ) { Text( @@ -466,7 +419,7 @@ private fun PlanCard( if (isYearly) { Surface( - shape = RoundedCornerShape(AppRadius.full), + shape = RoundedCornerShape(OrganicRadius.full), color = Color(0xFF34C759) ) { Text( @@ -474,7 +427,7 @@ private fun PlanCard( style = MaterialTheme.typography.labelSmall, fontWeight = FontWeight.Bold, color = Color.White, - modifier = Modifier.padding(horizontal = AppSpacing.sm, vertical = 2.dp) + modifier = Modifier.padding(horizontal = OrganicSpacing.sm, vertical = 2.dp) ) } } diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingValuePropsContent.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingValuePropsContent.kt index fbc5502..d5235ab 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingValuePropsContent.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingValuePropsContent.kt @@ -21,8 +21,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import com.example.casera.ui.theme.AppRadius -import com.example.casera.ui.theme.AppSpacing +import com.example.casera.ui.theme.* import casera.composeapp.generated.resources.* import kotlinx.coroutines.delay import org.jetbrains.compose.resources.stringResource @@ -111,67 +110,62 @@ fun OnboardingValuePropsContent( } } - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = AppSpacing.xl), - horizontalAlignment = Alignment.CenterHorizontally + WarmGradientBackground( + modifier = Modifier.fillMaxSize() ) { - Spacer(modifier = Modifier.weight(0.5f)) - - // Feature carousel - HorizontalPager( - state = pagerState, + Column( modifier = Modifier - .fillMaxWidth() - .weight(2f) - ) { page -> - FeatureCard(feature = features[page]) - } - - Spacer(modifier = Modifier.height(AppSpacing.xl)) - - // Page indicators - Row( - horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm), - verticalAlignment = Alignment.CenterVertically + .fillMaxSize() + .padding(horizontal = OrganicSpacing.xl), + horizontalAlignment = Alignment.CenterHorizontally ) { - repeat(features.size) { index -> - Box( - modifier = Modifier - .size(if (index == pagerState.currentPage) 10.dp else 8.dp) - .clip(CircleShape) - .background( - if (index == pagerState.currentPage) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) - } - ) - ) + Spacer(modifier = Modifier.weight(0.5f)) + + // Feature carousel + HorizontalPager( + state = pagerState, + modifier = Modifier + .fillMaxWidth() + .weight(2f) + ) { page -> + FeatureCard(feature = features[page]) } - } - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(OrganicSpacing.xl)) - // Continue button - Button( - onClick = onContinue, - modifier = Modifier - .fillMaxWidth() - .height(56.dp), - shape = RoundedCornerShape(AppRadius.md) - ) { - Text( + // Page indicators + Row( + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm), + verticalAlignment = Alignment.CenterVertically + ) { + repeat(features.size) { index -> + Box( + modifier = Modifier + .size(if (index == pagerState.currentPage) 10.dp else 8.dp) + .clip(CircleShape) + .background( + if (index == pagerState.currentPage) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) + } + ) + ) + } + } + + Spacer(modifier = Modifier.weight(1f)) + + // Continue button + OrganicPrimaryButton( text = stringResource(Res.string.onboarding_continue), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold + onClick = onContinue, + modifier = Modifier.fillMaxWidth(), + icon = Icons.Default.ArrowForward ) - Spacer(modifier = Modifier.width(AppSpacing.sm)) - Icon(Icons.Default.ArrowForward, contentDescription = null) - } - Spacer(modifier = Modifier.height(AppSpacing.xl * 2)) + Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2)) + } } } @@ -180,29 +174,20 @@ private fun FeatureCard(feature: FeatureItem) { Column( modifier = Modifier .fillMaxWidth() - .padding(horizontal = AppSpacing.md), + .padding(horizontal = OrganicSpacing.md), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { - // Icon with gradient background - Box( - modifier = Modifier - .size(120.dp) - .clip(CircleShape) - .background( - Brush.linearGradient(feature.gradientColors) - ), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = feature.icon, - contentDescription = null, - modifier = Modifier.size(60.dp), - tint = Color.White - ) - } + // Icon with gradient background using OrganicIconContainer + OrganicIconContainer( + icon = feature.icon, + size = 120.dp, + iconSize = 60.dp, + gradientColors = feature.gradientColors, + contentDescription = null + ) - Spacer(modifier = Modifier.height(AppSpacing.xl * 2)) + Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2)) // Title Text( @@ -213,7 +198,7 @@ private fun FeatureCard(feature: FeatureItem) { textAlign = TextAlign.Center ) - Spacer(modifier = Modifier.height(AppSpacing.md)) + Spacer(modifier = Modifier.height(OrganicSpacing.md)) // Description Text( diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingVerifyEmailContent.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingVerifyEmailContent.kt index 7079ff9..2227bd0 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingVerifyEmailContent.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingVerifyEmailContent.kt @@ -1,8 +1,6 @@ package com.example.casera.ui.screens.onboarding -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons @@ -11,15 +9,12 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.example.casera.network.ApiResult -import com.example.casera.ui.theme.AppRadius -import com.example.casera.ui.theme.AppSpacing +import com.example.casera.ui.theme.* import com.example.casera.viewmodel.OnboardingViewModel import casera.composeapp.generated.resources.* import org.jetbrains.compose.resources.stringResource @@ -56,173 +51,152 @@ fun OnboardingVerifyEmailContent( val isLoading = verifyState is ApiResult.Loading - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = AppSpacing.xl), - horizontalAlignment = Alignment.CenterHorizontally + WarmGradientBackground( + modifier = Modifier.fillMaxSize() ) { - Spacer(modifier = Modifier.weight(1f)) - - // Header Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(AppSpacing.lg) + modifier = Modifier + .fillMaxSize() + .padding(horizontal = OrganicSpacing.xl), + horizontalAlignment = Alignment.CenterHorizontally ) { - // Icon with gradient background - Box( - modifier = Modifier - .size(100.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = Icons.Default.MarkEmailRead, - contentDescription = null, - modifier = Modifier.size(50.dp), - tint = MaterialTheme.colorScheme.primary - ) - } + Spacer(modifier = Modifier.weight(1f)) - // Title and subtitle + // Header Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(AppSpacing.sm) + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg) ) { - Text( - text = stringResource(Res.string.onboarding_verify_email_title), - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onBackground + // Icon with OrganicIconContainer + OrganicIconContainer( + icon = Icons.Default.MarkEmailRead, + size = 100.dp, + iconSize = 50.dp, + contentDescription = null ) - Text( - text = stringResource(Res.string.onboarding_verify_email_subtitle), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) - } - } + // Title and subtitle + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm) + ) { + Text( + text = stringResource(Res.string.onboarding_verify_email_title), + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onBackground + ) - Spacer(modifier = Modifier.height(AppSpacing.xl * 2)) - - // Code input - OutlinedTextField( - value = code, - onValueChange = { - if (it.length <= 6 && it.all { char -> char.isDigit() }) { - code = it - localErrorMessage = null + Text( + text = stringResource(Res.string.onboarding_verify_email_subtitle), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) } - }, - placeholder = { - Text( - "000000", - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() - ) - }, - leadingIcon = { - Icon(Icons.Default.Pin, contentDescription = null) - }, - modifier = Modifier.fillMaxWidth(), - textStyle = LocalTextStyle.current.copy( - textAlign = TextAlign.Center, - fontWeight = FontWeight.Bold - ), - shape = RoundedCornerShape(AppRadius.md), - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - enabled = !isLoading - ) + } - // Error message - if (localErrorMessage != null) { - Spacer(modifier = Modifier.height(AppSpacing.md)) - Card( + Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2)) + + // Code input + OutlinedTextField( + value = code, + onValueChange = { + if (it.length <= 6 && it.all { char -> char.isDigit() }) { + code = it + localErrorMessage = null + } + }, + placeholder = { + Text( + "000000", + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + }, + leadingIcon = { + Icon(Icons.Default.Pin, contentDescription = null) + }, modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.errorContainer + textStyle = LocalTextStyle.current.copy( + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold ), - shape = RoundedCornerShape(AppRadius.md) - ) { + shape = RoundedCornerShape(OrganicRadius.md), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + enabled = !isLoading + ) + + // Error message + if (localErrorMessage != null) { + Spacer(modifier = Modifier.height(OrganicSpacing.md)) + OrganicCard( + modifier = Modifier.fillMaxWidth(), + accentColor = MaterialTheme.colorScheme.error, + showBlob = false + ) { + Row( + modifier = Modifier.padding(OrganicSpacing.md), + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Default.Error, + contentDescription = null, + tint = MaterialTheme.colorScheme.error + ) + Text( + text = localErrorMessage ?: "", + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall + ) + } + } + } + + // Loading indicator + if (isLoading) { + Spacer(modifier = Modifier.height(OrganicSpacing.md)) Row( - modifier = Modifier.padding(AppSpacing.md), - horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm), + horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm), verticalAlignment = Alignment.CenterVertically ) { - Icon( - Icons.Default.Error, - contentDescription = null, - tint = MaterialTheme.colorScheme.error + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + strokeWidth = 2.dp ) Text( - text = localErrorMessage ?: "", - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall + text = "Verifying...", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } } + + Spacer(modifier = Modifier.height(OrganicSpacing.lg)) + + // Hint text + Text( + text = stringResource(Res.string.onboarding_verify_email_hint), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.weight(1f)) + + // Verify button + OrganicPrimaryButton( + text = stringResource(Res.string.auth_verify_button), + onClick = { viewModel.verifyEmail(code) }, + modifier = Modifier.fillMaxWidth(), + enabled = code.length == 6 && !isLoading, + isLoading = isLoading + ) + + Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2)) } - - // Loading indicator - if (isLoading) { - Spacer(modifier = Modifier.height(AppSpacing.md)) - Row( - horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm), - verticalAlignment = Alignment.CenterVertically - ) { - CircularProgressIndicator( - modifier = Modifier.size(20.dp), - strokeWidth = 2.dp - ) - Text( - text = "Verifying...", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - Spacer(modifier = Modifier.height(AppSpacing.lg)) - - // Hint text - Text( - text = stringResource(Res.string.onboarding_verify_email_hint), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.weight(1f)) - - // Verify button - Button( - onClick = { viewModel.verifyEmail(code) }, - modifier = Modifier - .fillMaxWidth() - .height(56.dp), - shape = RoundedCornerShape(AppRadius.md), - enabled = code.length == 6 && !isLoading - ) { - if (isLoading) { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - color = MaterialTheme.colorScheme.onPrimary, - strokeWidth = 2.dp - ) - } else { - Text( - text = stringResource(Res.string.auth_verify_button), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold - ) - } - } - - Spacer(modifier = Modifier.height(AppSpacing.xl * 2)) } } diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingWelcomeContent.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingWelcomeContent.kt index feeb19a..59145ce 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingWelcomeContent.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingWelcomeContent.kt @@ -1,7 +1,6 @@ package com.example.casera.ui.screens.onboarding import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -11,16 +10,11 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import com.example.casera.ui.theme.AppRadius -import com.example.casera.ui.theme.AppSpacing +import com.example.casera.ui.theme.* import com.example.casera.viewmodel.AuthViewModel import com.example.casera.network.ApiResult import casera.composeapp.generated.resources.* @@ -35,132 +29,106 @@ fun OnboardingWelcomeContent( ) { var showLoginDialog by remember { mutableStateOf(false) } - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = AppSpacing.xl), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + WarmGradientBackground( + modifier = Modifier.fillMaxSize() ) { - Spacer(modifier = Modifier.weight(1f)) - - // Hero section Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = OrganicSpacing.xl), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(AppSpacing.xl) + verticalArrangement = Arrangement.Center ) { - // App icon with shadow - Box( - modifier = Modifier - .size(120.dp) - .shadow( - elevation = 20.dp, - shape = RoundedCornerShape(AppRadius.xxl), - spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f) - ) - .clip(RoundedCornerShape(AppRadius.xxl)) - .background(MaterialTheme.colorScheme.surface) - ) { - Icon( - imageVector = Icons.Default.Home, - contentDescription = null, - modifier = Modifier - .size(80.dp) - .align(Alignment.Center), - tint = MaterialTheme.colorScheme.primary - ) - } + Spacer(modifier = Modifier.weight(1f)) - // Welcome text + // Hero section Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(AppSpacing.sm) + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xl) ) { - Text( - text = stringResource(Res.string.onboarding_welcome_title), - style = MaterialTheme.typography.headlineLarge, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onBackground + // App icon with OrganicIconContainer + OrganicIconContainer( + icon = Icons.Default.Home, + size = 120.dp, + iconSize = 80.dp, + contentDescription = null ) - Text( - text = stringResource(Res.string.onboarding_welcome_subtitle), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) + // Welcome text + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm) + ) { + Text( + text = stringResource(Res.string.onboarding_welcome_title), + style = MaterialTheme.typography.headlineLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onBackground + ) + + Text( + text = stringResource(Res.string.onboarding_welcome_subtitle), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + } } - } - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.weight(1f)) - // Action buttons - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(AppSpacing.md) - ) { - // Primary CTA - Start Fresh - Button( - onClick = onStartFresh, - modifier = Modifier - .fillMaxWidth() - .height(56.dp), - shape = RoundedCornerShape(AppRadius.md), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary - ) + // Action buttons + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md) ) { - Icon( - imageVector = Icons.Default.Home, - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(AppSpacing.sm)) - Text( + // Primary CTA - Start Fresh + OrganicPrimaryButton( text = stringResource(Res.string.onboarding_start_fresh), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold + onClick = onStartFresh, + modifier = Modifier.fillMaxWidth(), + icon = Icons.Default.Home ) + + // Secondary CTA - Join Existing + OutlinedButton( + onClick = onJoinExisting, + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + shape = RoundedCornerShape(OrganicRadius.md), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.primary + ) + ) { + Icon( + imageVector = Icons.Default.People, + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(OrganicSpacing.sm)) + Text( + text = stringResource(Res.string.onboarding_join_existing), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Medium + ) + } + + // Returning user login + TextButton( + onClick = { showLoginDialog = true }, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = stringResource(Res.string.onboarding_already_have_account), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } } - // Secondary CTA - Join Existing - OutlinedButton( - onClick = onJoinExisting, - modifier = Modifier - .fillMaxWidth() - .height(56.dp), - shape = RoundedCornerShape(AppRadius.md), - colors = ButtonDefaults.outlinedButtonColors( - contentColor = MaterialTheme.colorScheme.primary - ) - ) { - Icon( - imageVector = Icons.Default.People, - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(AppSpacing.sm)) - Text( - text = stringResource(Res.string.onboarding_join_existing), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium - ) - } - - // Returning user login - TextButton( - onClick = { showLoginDialog = true }, - modifier = Modifier.fillMaxWidth() - ) { - Text( - text = stringResource(Res.string.onboarding_already_have_account), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } + Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2)) } - - Spacer(modifier = Modifier.height(AppSpacing.xl * 2)) } // Login dialog @@ -212,7 +180,7 @@ private fun LoginDialog( }, text = { Column( - verticalArrangement = Arrangement.spacedBy(AppSpacing.md) + verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md) ) { OutlinedTextField( value = username, @@ -220,7 +188,7 @@ private fun LoginDialog( label = { Text(stringResource(Res.string.auth_login_username_label)) }, singleLine = true, modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(AppRadius.md), + shape = RoundedCornerShape(OrganicRadius.md), enabled = !isLoading ) @@ -230,7 +198,7 @@ private fun LoginDialog( label = { Text(stringResource(Res.string.auth_login_password_label)) }, singleLine = true, modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(AppRadius.md), + shape = RoundedCornerShape(OrganicRadius.md), visualTransformation = androidx.compose.ui.text.input.PasswordVisualTransformation(), enabled = !isLoading ) diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/theme/OrganicDesign.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/theme/OrganicDesign.kt new file mode 100644 index 0000000..af73afb --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/theme/OrganicDesign.kt @@ -0,0 +1,745 @@ +package com.example.casera.ui.theme + +import androidx.compose.animation.core.* +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Eco +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.blur +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.clipRect +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.sin +import kotlin.random.Random + +// MARK: - Organic Design System +// Warm, natural aesthetic with soft shapes, subtle textures, and flowing layouts + +// MARK: - Organic Shapes + +/** + * Soft organic blob shape for backgrounds + * Matches iOS OrganicBlobShape + */ +class OrganicBlobShape(private val variation: Int = 0) : Shape { + override fun createOutline( + size: Size, + layoutDirection: androidx.compose.ui.unit.LayoutDirection, + density: androidx.compose.ui.unit.Density + ): Outline { + val path = Path() + val w = size.width + val h = size.height + + when (variation % 3) { + 0 -> { + // Soft cloud-like blob + path.moveTo(w * 0.1f, h * 0.5f) + path.cubicTo( + w * 0.0f, h * 0.1f, + w * 0.25f, h * 0.0f, + w * 0.5f, h * 0.05f + ) + path.cubicTo( + w * 0.75f, h * 0.1f, + w * 1.0f, h * 0.25f, + w * 0.95f, h * 0.45f + ) + path.cubicTo( + w * 0.9f, h * 0.7f, + w * 0.8f, h * 0.95f, + w * 0.55f, h * 0.95f + ) + path.cubicTo( + w * 0.25f, h * 0.95f, + w * 0.05f, h * 0.75f, + w * 0.1f, h * 0.5f + ) + } + 1 -> { + // Pebble shape + path.moveTo(w * 0.15f, h * 0.4f) + path.cubicTo( + w * 0.1f, h * 0.15f, + w * 0.35f, h * 0.05f, + w * 0.6f, h * 0.08f + ) + path.cubicTo( + w * 0.85f, h * 0.12f, + w * 0.95f, h * 0.35f, + w * 0.9f, h * 0.55f + ) + path.cubicTo( + w * 0.85f, h * 0.8f, + w * 0.65f, h * 0.95f, + w * 0.45f, h * 0.92f + ) + path.cubicTo( + w * 0.2f, h * 0.88f, + w * 0.08f, h * 0.65f, + w * 0.15f, h * 0.4f + ) + } + else -> { + // Leaf-like shape + path.moveTo(w * 0.05f, h * 0.5f) + path.cubicTo( + w * 0.05f, h * 0.2f, + w * 0.25f, h * 0.02f, + w * 0.5f, h * 0.02f + ) + path.cubicTo( + w * 0.75f, h * 0.02f, + w * 0.95f, h * 0.2f, + w * 0.95f, h * 0.5f + ) + path.cubicTo( + w * 0.95f, h * 0.8f, + w * 0.75f, h * 0.98f, + w * 0.5f, h * 0.98f + ) + path.cubicTo( + w * 0.25f, h * 0.98f, + w * 0.05f, h * 0.8f, + w * 0.05f, h * 0.5f + ) + } + } + + path.close() + return Outline.Generic(path) + } +} + +/** + * Super soft rounded rectangle for organic cards + * Uses continuous corner radius matching iOS .continuous style + */ +val OrganicRoundedShape = RoundedCornerShape(28.dp) + +// MARK: - Grain Texture Overlay + +/** + * Grain texture overlay for natural feel + * Matches iOS GrainTexture + */ +@Composable +fun GrainTexture( + modifier: Modifier = Modifier, + opacity: Float = 0.03f +) { + val grainColor = Color.Black.copy(alpha = opacity) + + Canvas(modifier = modifier.fillMaxSize()) { + val density = size.width * size.height / 50 + val random = Random(42) // Fixed seed for consistent pattern + + repeat(density.toInt().coerceAtMost(5000)) { + val x = random.nextFloat() * size.width + val y = random.nextFloat() * size.height + val grainOpacity = random.nextFloat() * 0.7f + 0.3f + + drawCircle( + color = grainColor.copy(alpha = grainColor.alpha * grainOpacity), + radius = 0.5f, + center = Offset(x, y) + ) + } + } +} + +// MARK: - Organic Card Background + +/** + * Organic card background with subtle accent blob and grain texture + * Matches iOS OrganicCardBackground + */ +@Composable +fun OrganicCardBackground( + modifier: Modifier = Modifier, + accentColor: Color = MaterialTheme.colorScheme.primary, + showBlob: Boolean = true, + blobVariation: Int = 0 +) { + Box(modifier = modifier) { + // Main card fill + Box( + modifier = Modifier + .fillMaxSize() + .clip(OrganicRoundedShape) + .background(MaterialTheme.colorScheme.backgroundSecondary) + ) + + // Subtle accent blob in corner + if (showBlob) { + Box( + modifier = Modifier + .fillMaxSize() + .clip(OrganicRoundedShape) + ) { + Box( + modifier = Modifier + .fillMaxWidth(0.6f) + .fillMaxHeight(0.7f) + .offset(x = 80.dp, y = (-20).dp) + .align(Alignment.TopEnd) + .clip(OrganicBlobShape(blobVariation)) + .background( + brush = Brush.linearGradient( + colors = listOf( + accentColor.copy(alpha = 0.08f), + accentColor.copy(alpha = 0.02f) + ) + ) + ) + .blur(20.dp) + ) + } + } + + // Grain texture + Box( + modifier = Modifier + .fillMaxSize() + .clip(OrganicRoundedShape) + ) { + GrainTexture(opacity = 0.015f) + } + } +} + +// MARK: - Natural Shadow + +/** + * Shadow intensity levels matching iOS NaturalShadow + */ +enum class ShadowIntensity { + Subtle, + Medium, + Pronounced; + + val elevation: Dp + get() = when (this) { + Subtle -> 4.dp + Medium -> 8.dp + Pronounced -> 12.dp + } + + val ambientAlpha: Float + get() = when (this) { + Subtle -> 0.04f + Medium -> 0.08f + Pronounced -> 0.12f + } +} + +/** + * Natural shadow modifier matching iOS NaturalShadow + */ +fun Modifier.naturalShadow( + intensity: ShadowIntensity = ShadowIntensity.Medium, + shape: Shape = OrganicRoundedShape +): Modifier = this + .shadow( + elevation = intensity.elevation, + shape = shape, + ambientColor = Color.Black.copy(alpha = intensity.ambientAlpha), + spotColor = Color.Black.copy(alpha = intensity.ambientAlpha * 0.5f) + ) + +// MARK: - Organic Icon Container + +/** + * Icon with soft organic background and inner glow + * Matches iOS OrganicIconContainer + * + * @param icon The icon to display (ImageVector) + * @param modifier Modifier for the container + * @param size The size of the container (default 48.dp) + * @param iconScale Scale of the icon relative to container (default 0.5f) + * @param iconSize Optional explicit icon size (overrides iconScale if provided) + * @param backgroundColor Background color for the container + * @param iconColor Tint color for the icon + * @param containerColor Alias for backgroundColor (for compatibility) + * @param iconTint Alias for iconColor (for compatibility) + * @param gradientColors Optional custom gradient colors (overrides backgroundColor) + * @param contentDescription Accessibility description + */ +@Composable +fun OrganicIconContainer( + icon: ImageVector, + modifier: Modifier = Modifier, + size: Dp = 48.dp, + iconScale: Float = 0.5f, + iconSize: Dp? = null, + backgroundColor: Color? = null, + iconColor: Color? = null, + containerColor: Color? = null, + iconTint: Color? = null, + gradientColors: List? = null, + contentDescription: String? = null +) { + // Resolve colors with fallback chain: explicit param -> alias -> default + val resolvedBackgroundColor = backgroundColor ?: containerColor ?: MaterialTheme.colorScheme.primary + val resolvedIconColor = iconColor ?: iconTint ?: MaterialTheme.colorScheme.onPrimary + + val actualGradientColors = gradientColors ?: listOf( + resolvedBackgroundColor, + resolvedBackgroundColor.copy(alpha = 0.85f) + ) + val actualIconSize = iconSize ?: (size * iconScale) + + Box( + modifier = modifier + .size(size) + .naturalShadow(ShadowIntensity.Subtle, CircleShape), + contentAlignment = Alignment.Center + ) { + // Soft organic background with radial gradient + Canvas(modifier = Modifier.fillMaxSize()) { + drawCircle( + brush = Brush.radialGradient( + colors = actualGradientColors, + center = Offset(this.size.width * 0.3f, this.size.height * 0.3f), + radius = this.size.maxDimension + ) + ) + } + + // Inner glow + Canvas(modifier = Modifier.fillMaxSize()) { + drawCircle( + brush = Brush.radialGradient( + colors = listOf( + Color.White.copy(alpha = 0.2f), + Color.Transparent + ), + center = Offset(this.size.width * 0.3f, this.size.height * 0.3f), + radius = this.size.maxDimension * 0.5f + ) + ) + } + + // Icon + Icon( + imageVector = icon, + contentDescription = contentDescription, + modifier = Modifier.size(actualIconSize), + tint = resolvedIconColor + ) + } +} + +// MARK: - Organic Stat Pill + +/** + * Stat display pill with icon and label + * Matches iOS OrganicStatPill + */ +@Composable +fun OrganicStatPill( + icon: ImageVector, + value: String, + label: String, + modifier: Modifier = Modifier, + color: Color = MaterialTheme.colorScheme.primary +) { + Row( + modifier = modifier + .background( + color = MaterialTheme.colorScheme.backgroundSecondary, + shape = RoundedCornerShape(50) + ) + .drawBehind { + // Border + drawRoundRect( + color = color.copy(alpha = 0.15f), + style = androidx.compose.ui.graphics.drawscope.Stroke(width = 1.dp.toPx()), + cornerRadius = androidx.compose.ui.geometry.CornerRadius(50.dp.toPx()) + ) + } + .padding(horizontal = 14.dp, vertical = 10.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Icon with soft background + Box( + modifier = Modifier + .size(32.dp) + .background( + color = color.copy(alpha = 0.12f), + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(13.dp), + tint = color + ) + } + + Column(verticalArrangement = Arrangement.spacedBy(1.dp)) { + Text( + text = value, + style = MaterialTheme.typography.labelLarge.copy( + fontSize = 15.sp, + fontWeight = androidx.compose.ui.text.font.FontWeight.Bold + ), + color = MaterialTheme.colorScheme.textPrimary + ) + Text( + text = label, + style = MaterialTheme.typography.labelSmall.copy( + fontSize = 11.sp, + fontWeight = androidx.compose.ui.text.font.FontWeight.Medium + ), + color = MaterialTheme.colorScheme.textSecondary + ) + } + } +} + +// MARK: - Organic Divider + +/** + * Gradient divider that fades at edges + * Matches iOS OrganicDivider + */ +@Composable +fun OrganicDivider( + modifier: Modifier = Modifier, + color: Color = MaterialTheme.colorScheme.textSecondary.copy(alpha = 0.15f), + height: Dp = 1.dp, + horizontalPadding: Dp = 0.dp, + vertical: Boolean = false +) { + if (vertical) { + Box( + modifier = modifier + .width(1.dp) + .background( + brush = Brush.verticalGradient( + colors = listOf( + color.copy(alpha = 0f), + color, + color, + color.copy(alpha = 0f) + ) + ) + ) + ) + } else { + Box( + modifier = modifier + .padding(horizontal = horizontalPadding) + .fillMaxWidth() + .height(height) + .background( + brush = Brush.horizontalGradient( + colors = listOf( + color.copy(alpha = 0f), + color, + color, + color.copy(alpha = 0f) + ) + ) + ) + ) + } +} + +// MARK: - Warm Gradient Background + +/** + * Screen background with subtle warm gradient and grain + * Matches iOS WarmGradientBackground + */ +@Composable +fun WarmGradientBackground( + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit = {} +) { + val isDark = isSystemInDarkTheme() + val primaryColor = MaterialTheme.colorScheme.primary + val backgroundColor = MaterialTheme.colorScheme.backgroundPrimary + + Box(modifier = modifier.fillMaxSize()) { + // Base background + Box( + modifier = Modifier + .fillMaxSize() + .background(backgroundColor) + ) + + // Subtle warm gradient overlay + Box( + modifier = Modifier + .fillMaxSize() + .background( + brush = Brush.linearGradient( + colors = listOf( + primaryColor.copy(alpha = if (isDark) 0.05f else 0.03f), + Color.Transparent + ), + start = Offset.Zero, + end = Offset.Infinite + ) + ) + ) + + // Grain for natural feel + GrainTexture(opacity = 0.02f) + + // Content + content() + } +} + +// MARK: - Organic Card Modifier + +/** + * Organic card styling modifier + * Matches iOS organicCard view modifier + */ +@Composable +fun Modifier.organicCard( + accentColor: Color = MaterialTheme.colorScheme.primary, + showBlob: Boolean = true, + shadowIntensity: ShadowIntensity = ShadowIntensity.Medium, + blobVariation: Int = 0 +): Modifier = this + .naturalShadow(shadowIntensity) + .clip(OrganicRoundedShape) + .drawBehind { + // Main fill + drawRoundRect( + color = Color.Transparent, // Background handled by OrganicCardBackground + cornerRadius = androidx.compose.ui.geometry.CornerRadius(28.dp.toPx()) + ) + } + +/** + * Composable wrapper for organic card styling + */ +@Composable +fun OrganicCard( + modifier: Modifier = Modifier, + accentColor: Color = MaterialTheme.colorScheme.primary, + showBlob: Boolean = true, + shadowIntensity: ShadowIntensity = ShadowIntensity.Medium, + blobVariation: Int = 0, + content: @Composable BoxScope.() -> Unit +) { + Box( + modifier = modifier.naturalShadow(shadowIntensity, OrganicRoundedShape) + ) { + OrganicCardBackground( + accentColor = accentColor, + showBlob = showBlob, + blobVariation = blobVariation + ) + Box( + modifier = Modifier + .fillMaxWidth() + .clip(OrganicRoundedShape), + content = content + ) + } +} + +// MARK: - Organic Spacing + +/** + * Organic spacing constants matching iOS OrganicSpacing + * Provides both semantic names (compact, cozy, etc.) and standard names (xs, sm, etc.) + */ +object OrganicSpacing { + // Semantic names (original) + val compact = 8.dp + val cozy = 20.dp + val comfortable = 24.dp + val spacious = 32.dp + val airy = 40.dp + + // Standard size names (for compatibility) + val xs = 4.dp + val sm = 8.dp + val md = 12.dp + val lg = 16.dp + val xl = 24.dp + val xxl = 32.dp + + // Additional aliases + val extraSmall = xs + val small = sm + val medium = md + val large = lg + val extraLarge = xl + + // Extended aliases (matching iOS naming) + val minimal = 2.dp + val generous = 48.dp +} + +/** + * Organic radius constants for rounded corners + * Alias for AppRadius with organic naming + */ +object OrganicRadius { + val xs = 4.dp + val sm = 8.dp + val md = 12.dp + val lg = 16.dp + val xl = 20.dp + val xxl = 24.dp + val full = 50.dp +} + +/** + * Organic shapes for consistency + */ +object OrganicShapes { + val extraSmall = RoundedCornerShape(OrganicRadius.xs) + val small = RoundedCornerShape(OrganicRadius.sm) + val medium = RoundedCornerShape(OrganicRadius.md) + val large = RoundedCornerShape(OrganicRadius.lg) + val extraLarge = RoundedCornerShape(OrganicRadius.xl) +} + +// MARK: - Floating Leaf Decoration + +/** + * Animated floating leaf decoration + * Matches iOS FloatingLeaf + */ +@Composable +fun FloatingLeaf( + modifier: Modifier = Modifier, + delay: Int = 0, + size: Dp = 20.dp, + color: Color = MaterialTheme.colorScheme.primary +) { + val infiniteTransition = rememberInfiniteTransition(label = "leaf") + + val rotation by infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = 15f, + animationSpec = infiniteRepeatable( + animation = tween( + durationMillis = 4000, + delayMillis = delay, + easing = FastOutSlowInEasing + ), + repeatMode = RepeatMode.Reverse + ), + label = "leafRotation" + ) + + val offsetY by infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = 8f, + animationSpec = infiniteRepeatable( + animation = tween( + durationMillis = 4000, + delayMillis = delay, + easing = FastOutSlowInEasing + ), + repeatMode = RepeatMode.Reverse + ), + label = "leafOffset" + ) + + Icon( + imageVector = Icons.Default.Eco, + contentDescription = null, + modifier = modifier + .size(size) + .rotate(rotation) + .offset(y = offsetY.dp), + tint = color.copy(alpha = 0.15f) + ) +} + +// MARK: - Organic Button + +/** + * Primary button with organic gradient styling + */ +@Composable +fun OrganicPrimaryButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + isLoading: Boolean = false, + icon: ImageVector? = null +) { + val primaryColor = MaterialTheme.colorScheme.primary + + androidx.compose.material3.Button( + onClick = onClick, + modifier = modifier + .fillMaxWidth() + .height(56.dp), + enabled = enabled && !isLoading, + shape = RoundedCornerShape(28.dp), + colors = androidx.compose.material3.ButtonDefaults.buttonColors( + containerColor = primaryColor, + contentColor = MaterialTheme.colorScheme.onPrimary, + disabledContainerColor = primaryColor.copy(alpha = 0.5f), + disabledContentColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.7f) + ) + ) { + if (isLoading) { + androidx.compose.material3.CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = MaterialTheme.colorScheme.onPrimary, + strokeWidth = 2.dp + ) + } else { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = text, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold + ) + ) + if (icon != null) { + Spacer(modifier = Modifier.width(8.dp)) + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + } + } + } + } +} diff --git a/iosApp/.claude/settings.local.json b/iosApp/.claude/settings.local.json new file mode 100644 index 0000000..bf8c4b4 --- /dev/null +++ b/iosApp/.claude/settings.local.json @@ -0,0 +1,6 @@ +{ + "enabledMcpjsonServers": [ + "ios-simulator" + ], + "enableAllProjectMcpServers": true +}