android ui

This commit is contained in:
Trey t
2025-12-18 12:18:33 -06:00
parent b39d37a6e8
commit 59cbc60668
34 changed files with 6112 additions and 5767 deletions

View File

@@ -23,6 +23,7 @@ import com.example.casera.viewmodel.TaskCompletionViewModel
import com.example.casera.viewmodel.TaskViewModel import com.example.casera.viewmodel.TaskViewModel
import com.example.casera.models.TaskDetail import com.example.casera.models.TaskDetail
import com.example.casera.network.ApiResult import com.example.casera.network.ApiResult
import com.example.casera.ui.theme.*
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -105,7 +106,9 @@ fun AllTasksScreen(
} }
} }
WarmGradientBackground {
Scaffold( Scaffold(
containerColor = androidx.compose.ui.graphics.Color.Transparent,
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { title = {
@@ -135,7 +138,7 @@ fun AllTasksScreen(
} }
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface containerColor = androidx.compose.ui.graphics.Color.Transparent
) )
) )
} }
@@ -156,46 +159,35 @@ fun AllTasksScreen(
) { ) {
Column( Column(
horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally, horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy),
modifier = Modifier.padding(24.dp) modifier = Modifier.padding(OrganicSpacing.comfortable)
) { ) {
Icon( OrganicIconContainer(
Icons.Default.Assignment, icon = Icons.Default.Assignment,
contentDescription = null, size = 80.dp,
modifier = Modifier.size(80.dp), iconScale = 0.6f,
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f) backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f),
iconColor = MaterialTheme.colorScheme.primary
) )
Text( Text(
"No tasks yet", "No tasks yet",
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.textPrimary
) )
Text( Text(
"Create your first task to get started", "Create your first task to get started",
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.textSecondary
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(OrganicSpacing.compact))
Button( OrganicPrimaryButton(
text = "Add Task",
onClick = { showNewTaskDialog = true }, onClick = { showNewTaskDialog = true },
modifier = Modifier modifier = Modifier.fillMaxWidth(0.7f),
.fillMaxWidth(0.7f)
.height(56.dp),
enabled = myResidencesState is ApiResult.Success && enabled = myResidencesState is ApiResult.Success &&
(myResidencesState as ApiResult.Success).data.residences.isNotEmpty() (myResidencesState as ApiResult.Success).data.residences.isNotEmpty()
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
) {
Icon(Icons.Default.Add, contentDescription = null)
Text(
"Add Task",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
) )
}
}
if (myResidencesState is ApiResult.Success && if (myResidencesState is ApiResult.Success &&
(myResidencesState as ApiResult.Success).data.residences.isEmpty()) { (myResidencesState as ApiResult.Success).data.residences.isEmpty()) {
Text( Text(
@@ -266,6 +258,7 @@ fun AllTasksScreen(
} }
} }
} }
}
if (showCompleteDialog && selectedTask != null) { if (showCompleteDialog && selectedTask != null) {
CompleteTaskDialog( CompleteTaskDialog(

View File

@@ -9,7 +9,6 @@ import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons 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.models.ContractorSummary
import com.example.casera.network.ApiResult import com.example.casera.network.ApiResult
import com.example.casera.platform.* import com.example.casera.platform.*
import com.example.casera.ui.theme.AppRadius import com.example.casera.ui.theme.*
import com.example.casera.ui.theme.AppSpacing
import com.example.casera.viewmodel.ContractorViewModel import com.example.casera.viewmodel.ContractorViewModel
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -74,7 +72,9 @@ fun CompleteTaskScreen(
} }
} }
WarmGradientBackground {
Scaffold( Scaffold(
containerColor = androidx.compose.ui.graphics.Color.Transparent,
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { title = {
@@ -90,7 +90,7 @@ fun CompleteTaskScreen(
} }
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface containerColor = androidx.compose.ui.graphics.Color.Transparent
) )
) )
} }
@@ -102,17 +102,13 @@ fun CompleteTaskScreen(
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
// Task Info Section // Task Info Section
Card( OrganicCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = AppSpacing.lg, vertical = AppSpacing.md), .padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.md)
shape = RoundedCornerShape(AppRadius.md),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) { ) {
Column( Column(
modifier = Modifier.padding(AppSpacing.lg) modifier = Modifier.padding(OrganicSpacing.lg)
) { ) {
Text( Text(
text = taskTitle, text = taskTitle,
@@ -120,7 +116,7 @@ fun CompleteTaskScreen(
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
Spacer(modifier = Modifier.height(AppSpacing.sm)) Spacer(modifier = Modifier.height(OrganicSpacing.sm))
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -130,7 +126,7 @@ fun CompleteTaskScreen(
if (residenceName.isNotEmpty()) { if (residenceName.isNotEmpty()) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(AppSpacing.xs) horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
) { ) {
Icon( Icon(
Icons.Default.Home, Icons.Default.Home,
@@ -155,31 +151,26 @@ fun CompleteTaskScreen(
subtitle = stringResource(Res.string.completions_contractor_helper) subtitle = stringResource(Res.string.completions_contractor_helper)
) )
Card( OrganicCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = AppSpacing.lg) .padding(horizontal = OrganicSpacing.lg)
.clickable { showContractorPicker = true }, .clickable { showContractorPicker = true }
shape = RoundedCornerShape(AppRadius.md),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(AppSpacing.lg), .padding(OrganicSpacing.lg),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(AppSpacing.md) horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
Icon( OrganicIconContainer(
Icons.Default.Build, icon = Icons.Default.Build,
contentDescription = null, size = 24.dp
tint = MaterialTheme.colorScheme.primary
) )
Column { Column {
Text( Text(
@@ -204,7 +195,7 @@ fun CompleteTaskScreen(
} }
} }
Spacer(modifier = Modifier.height(AppSpacing.lg)) Spacer(modifier = Modifier.height(OrganicSpacing.lg))
// Completion Details Section // Completion Details Section
SectionHeader( SectionHeader(
@@ -213,8 +204,8 @@ fun CompleteTaskScreen(
) )
Column( Column(
modifier = Modifier.padding(horizontal = AppSpacing.lg), modifier = Modifier.padding(horizontal = OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(AppSpacing.md) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
// Completed By Name // Completed By Name
OutlinedTextField( OutlinedTextField(
@@ -226,7 +217,7 @@ fun CompleteTaskScreen(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true, singleLine = true,
enabled = selectedContractor == null, enabled = selectedContractor == null,
shape = RoundedCornerShape(AppRadius.md) shape = OrganicShapes.medium
) )
// Actual Cost // Actual Cost
@@ -239,11 +230,11 @@ fun CompleteTaskScreen(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true, singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
shape = RoundedCornerShape(AppRadius.md) shape = OrganicShapes.medium
) )
} }
Spacer(modifier = Modifier.height(AppSpacing.lg)) Spacer(modifier = Modifier.height(OrganicSpacing.lg))
// Notes Section // Notes Section
SectionHeader( SectionHeader(
@@ -257,12 +248,12 @@ fun CompleteTaskScreen(
placeholder = { Text(stringResource(Res.string.completions_notes_placeholder)) }, placeholder = { Text(stringResource(Res.string.completions_notes_placeholder)) },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = AppSpacing.lg) .padding(horizontal = OrganicSpacing.lg)
.height(120.dp), .height(120.dp),
shape = RoundedCornerShape(AppRadius.md) shape = OrganicShapes.medium
) )
Spacer(modifier = Modifier.height(AppSpacing.lg)) Spacer(modifier = Modifier.height(OrganicSpacing.lg))
// Rating Section // Rating Section
SectionHeader( SectionHeader(
@@ -270,19 +261,15 @@ fun CompleteTaskScreen(
subtitle = stringResource(Res.string.completions_rate_quality) subtitle = stringResource(Res.string.completions_rate_quality)
) )
Card( OrganicCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = AppSpacing.lg), .padding(horizontal = OrganicSpacing.lg)
shape = RoundedCornerShape(AppRadius.md),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(AppSpacing.lg), .padding(OrganicSpacing.lg),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text( Text(
@@ -291,7 +278,7 @@ fun CompleteTaskScreen(
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
Spacer(modifier = Modifier.height(AppSpacing.md)) Spacer(modifier = Modifier.height(OrganicSpacing.md))
Row( Row(
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
@@ -325,7 +312,7 @@ fun CompleteTaskScreen(
} }
} }
Spacer(modifier = Modifier.height(AppSpacing.lg)) Spacer(modifier = Modifier.height(OrganicSpacing.lg))
// Photos Section // Photos Section
SectionHeader( SectionHeader(
@@ -334,12 +321,12 @@ fun CompleteTaskScreen(
) )
Column( Column(
modifier = Modifier.padding(horizontal = AppSpacing.lg), modifier = Modifier.padding(horizontal = OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(AppSpacing.md) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(AppSpacing.md) horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
OutlinedButton( OutlinedButton(
onClick = { onClick = {
@@ -350,7 +337,7 @@ fun CompleteTaskScreen(
enabled = selectedImages.size < MAX_IMAGES enabled = selectedImages.size < MAX_IMAGES
) { ) {
Icon(Icons.Default.CameraAlt, null, modifier = Modifier.size(20.dp)) Icon(Icons.Default.CameraAlt, null, modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(AppSpacing.sm)) Spacer(modifier = Modifier.width(OrganicSpacing.sm))
Text(stringResource(Res.string.completions_camera)) Text(stringResource(Res.string.completions_camera))
} }
@@ -363,7 +350,7 @@ fun CompleteTaskScreen(
enabled = selectedImages.size < MAX_IMAGES enabled = selectedImages.size < MAX_IMAGES
) { ) {
Icon(Icons.Default.PhotoLibrary, null, modifier = Modifier.size(20.dp)) Icon(Icons.Default.PhotoLibrary, null, modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(AppSpacing.sm)) Spacer(modifier = Modifier.width(OrganicSpacing.sm))
Text(stringResource(Res.string.completions_library)) Text(stringResource(Res.string.completions_library))
} }
} }
@@ -373,7 +360,7 @@ fun CompleteTaskScreen(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.horizontalScroll(rememberScrollState()), .horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(AppSpacing.md) horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
selectedImages.forEachIndexed { index, imageData -> selectedImages.forEachIndexed { index, imageData ->
ImageThumbnailCard( ImageThumbnailCard(
@@ -390,10 +377,11 @@ fun CompleteTaskScreen(
} }
} }
Spacer(modifier = Modifier.height(AppSpacing.xl)) Spacer(modifier = Modifier.height(OrganicSpacing.xl))
// Complete Button // Complete Button
Button( OrganicPrimaryButton(
text = stringResource(Res.string.completions_complete_button),
onClick = { onClick = {
isSubmitting = true isSubmitting = true
val notesWithContractor = buildString { val notesWithContractor = buildString {
@@ -424,28 +412,14 @@ fun CompleteTaskScreen(
}, },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = AppSpacing.lg) .padding(horizontal = OrganicSpacing.lg),
.height(56.dp),
enabled = !isSubmitting, enabled = !isSubmitting,
shape = RoundedCornerShape(AppRadius.md) isLoading = isSubmitting,
) { icon = Icons.Default.CheckCircle
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
)
}
}
Spacer(modifier = Modifier.height(AppSpacing.xl)) Spacer(modifier = Modifier.height(OrganicSpacing.xl))
}
} }
} }
@@ -475,7 +449,7 @@ private fun SectionHeader(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = AppSpacing.lg, vertical = AppSpacing.sm) .padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.sm)
) { ) {
Text( Text(
text = title, text = title,
@@ -503,7 +477,7 @@ private fun ImageThumbnailCard(
Box( Box(
modifier = Modifier modifier = Modifier
.size(100.dp) .size(100.dp)
.clip(RoundedCornerShape(AppRadius.md)) .clip(OrganicShapes.medium)
.background(MaterialTheme.colorScheme.surfaceVariant) .background(MaterialTheme.colorScheme.surfaceVariant)
) { ) {
if (imageBitmap != null) { if (imageBitmap != null) {
@@ -530,7 +504,7 @@ private fun ImageThumbnailCard(
Box( Box(
modifier = Modifier modifier = Modifier
.align(Alignment.TopEnd) .align(Alignment.TopEnd)
.padding(AppSpacing.xs) .padding(OrganicSpacing.xs)
.size(24.dp) .size(24.dp)
.clip(CircleShape) .clip(CircleShape)
.background(MaterialTheme.colorScheme.error) .background(MaterialTheme.colorScheme.error)
@@ -565,16 +539,16 @@ private fun ContractorPickerSheet(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = AppSpacing.xl) .padding(bottom = OrganicSpacing.xl)
) { ) {
Text( Text(
text = stringResource(Res.string.completions_select_contractor), text = stringResource(Res.string.completions_select_contractor),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold, 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 // None option
ListItem( ListItem(
@@ -592,13 +566,13 @@ private fun ContractorPickerSheet(
modifier = Modifier.clickable { onSelect(null) } modifier = Modifier.clickable { onSelect(null) }
) )
HorizontalDivider() OrganicDivider()
if (isLoading) { if (isLoading) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(AppSpacing.xl), .padding(OrganicSpacing.xl),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
CircularProgressIndicator() CircularProgressIndicator()

View File

@@ -1,19 +1,15 @@
package com.example.casera.ui.screens package com.example.casera.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items 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.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalUriHandler 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.platform.rememberShareContractor
import com.example.casera.utils.SubscriptionHelper import com.example.casera.utils.SubscriptionHelper
import com.example.casera.ui.subscription.UpgradePromptDialog import com.example.casera.ui.subscription.UpgradePromptDialog
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.* import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -123,16 +120,16 @@ fun ContractorDetailScreen(
} }
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color(0xFFF9FAFB) containerColor = MaterialTheme.colorScheme.surface
) )
) )
} }
) { padding -> ) { padding ->
WarmGradientBackground {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(padding) .padding(padding)
.background(Color(0xFFF9FAFB))
) { ) {
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
val residences = DataManager.residences.value val residences = DataManager.residences.value
@@ -147,40 +144,28 @@ fun ContractorDetailScreen(
) { contractor -> ) { contractor ->
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(OrganicSpacing.medium),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.medium)
) { ) {
// Header Card // Header Card
item { item {
Card( OrganicCard(modifier = Modifier.fillMaxWidth()) {
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(24.dp), .padding(OrganicSpacing.large),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
// Avatar // Avatar
Box( OrganicIconContainer(
modifier = Modifier icon = Icons.Default.Person,
.size(80.dp) size = 80.dp,
.clip(CircleShape) iconSize = 48.dp,
.background(MaterialTheme.colorScheme.primaryContainer), containerColor = MaterialTheme.colorScheme.primaryContainer,
contentAlignment = Alignment.Center iconTint = MaterialTheme.colorScheme.primary
) {
Icon(
Icons.Default.Person,
contentDescription = null,
modifier = Modifier.size(48.dp),
tint = MaterialTheme.colorScheme.primary
) )
}
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(OrganicSpacing.medium))
Text( Text(
text = contractor.name, text = contractor.name,
@@ -198,18 +183,18 @@ fun ContractorDetailScreen(
} }
if (contractor.specialties.isNotEmpty()) { if (contractor.specialties.isNotEmpty()) {
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(OrganicSpacing.medium))
FlowRow( FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.small, Alignment.CenterHorizontally),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.small)
) { ) {
contractor.specialties.forEach { specialty -> contractor.specialties.forEach { specialty ->
Surface( Surface(
shape = RoundedCornerShape(20.dp), shape = OrganicShapes.large,
color = MaterialTheme.colorScheme.primaryContainer color = MaterialTheme.colorScheme.primaryContainer
) { ) {
Row( Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), modifier = Modifier.padding(horizontal = OrganicSpacing.medium, vertical = OrganicSpacing.extraSmall),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
@@ -218,7 +203,7 @@ fun ContractorDetailScreen(
modifier = Modifier.size(16.dp), modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.primary
) )
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(OrganicSpacing.extraSmall))
Text( Text(
text = specialty.name, text = specialty.name,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
@@ -232,7 +217,7 @@ fun ContractorDetailScreen(
} }
if (contractor.rating != null && contractor.rating > 0) { if (contractor.rating != null && contractor.rating > 0) {
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(OrganicSpacing.medium))
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
repeat(5) { index -> repeat(5) { index ->
Icon( Icon(
@@ -242,7 +227,7 @@ fun ContractorDetailScreen(
tint = Color(0xFFF59E0B) tint = Color(0xFFF59E0B)
) )
} }
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(OrganicSpacing.small))
Text( Text(
text = ((contractor.rating * 10).toInt() / 10.0).toString(), text = ((contractor.rating * 10).toInt() / 10.0).toString(),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
@@ -253,7 +238,7 @@ fun ContractorDetailScreen(
} }
if (contractor.taskCount > 0) { if (contractor.taskCount > 0) {
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(OrganicSpacing.extraSmall))
Text( Text(
text = stringResource(Res.string.contractors_completed_tasks, contractor.taskCount), text = stringResource(Res.string.contractors_completed_tasks, contractor.taskCount),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
@@ -269,7 +254,7 @@ fun ContractorDetailScreen(
item { item {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp) horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.medium)
) { ) {
contractor.phone?.let { phone -> contractor.phone?.let { phone ->
QuickActionButton( QuickActionButton(
@@ -388,7 +373,7 @@ fun ContractorDetailScreen(
text = stringResource(Res.string.contractors_no_contact_info), text = stringResource(Res.string.contractors_no_contact_info),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(OrganicSpacing.medium)
) )
} }
} }
@@ -459,8 +444,8 @@ fun ContractorDetailScreen(
item { item {
DetailSection(title = stringResource(Res.string.contractors_notes)) { DetailSection(title = stringResource(Res.string.contractors_notes)) {
Row( Row(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), modifier = Modifier.padding(horizontal = OrganicSpacing.medium, vertical = OrganicSpacing.medium),
horizontalArrangement = Arrangement.spacedBy(12.dp) horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.medium)
) { ) {
Icon( Icon(
Icons.Default.Notes, Icons.Default.Notes,
@@ -484,7 +469,7 @@ fun ContractorDetailScreen(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(OrganicSpacing.medium),
horizontalArrangement = Arrangement.SpaceEvenly horizontalArrangement = Arrangement.SpaceEvenly
) { ) {
StatCard( StatCard(
@@ -516,7 +501,7 @@ fun ContractorDetailScreen(
value = createdBy.username, value = createdBy.username,
iconTint = MaterialTheme.colorScheme.onSurfaceVariant iconTint = MaterialTheme.colorScheme.onSurfaceVariant
) )
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp)) OrganicDivider(modifier = Modifier.padding(horizontal = OrganicSpacing.medium))
} }
DetailRow( DetailRow(
@@ -531,6 +516,7 @@ fun ContractorDetailScreen(
} }
} }
} }
}
if (showEditDialog) { if (showEditDialog) {
AddContractorDialog( AddContractorDialog(
@@ -565,8 +551,8 @@ fun ContractorDetailScreen(
Text(stringResource(Res.string.common_cancel)) Text(stringResource(Res.string.common_cancel))
} }
}, },
containerColor = Color.White, containerColor = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(16.dp) shape = OrganicShapes.large
) )
} }
@@ -591,19 +577,14 @@ fun DetailSection(
title: String, title: String,
content: @Composable ColumnScope.() -> Unit content: @Composable ColumnScope.() -> Unit
) { ) {
Card( OrganicCard(modifier = Modifier.fillMaxWidth()) {
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
) {
Column(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.fillMaxWidth()) {
Text( Text(
text = title, text = title,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(16.dp).padding(bottom = 0.dp) modifier = Modifier.padding(OrganicSpacing.medium).padding(bottom = 0.dp)
) )
content() content()
} }
@@ -620,7 +601,7 @@ fun DetailRow(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp), .padding(horizontal = OrganicSpacing.medium, vertical = OrganicSpacing.medium),
verticalAlignment = Alignment.Top verticalAlignment = Alignment.Top
) { ) {
Icon( Icon(
@@ -629,7 +610,7 @@ fun DetailRow(
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
tint = iconTint tint = iconTint
) )
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(OrganicSpacing.medium))
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
text = label, text = label,
@@ -658,7 +639,7 @@ fun ClickableDetailRow(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable(onClick = onClick) .clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 12.dp), .padding(horizontal = OrganicSpacing.medium, vertical = OrganicSpacing.medium),
verticalAlignment = Alignment.Top verticalAlignment = Alignment.Top
) { ) {
Icon( Icon(
@@ -667,7 +648,7 @@ fun ClickableDetailRow(
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
tint = iconTint tint = iconTint
) )
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(OrganicSpacing.medium))
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
text = label, text = label,
@@ -698,33 +679,23 @@ fun QuickActionButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onClick: () -> Unit onClick: () -> Unit
) { ) {
Card( OrganicCard(
modifier = modifier.clickable(onClick = onClick), modifier = modifier.clickable(onClick = onClick)
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 12.dp), .padding(vertical = OrganicSpacing.medium),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Box( OrganicIconContainer(
modifier = Modifier icon = icon,
.size(44.dp) size = 44.dp,
.clip(CircleShape) iconSize = 22.dp,
.background(color.copy(alpha = 0.1f)), containerColor = color.copy(alpha = 0.1f),
contentAlignment = Alignment.Center iconTint = color
) {
Icon(
icon,
contentDescription = null,
modifier = Modifier.size(22.dp),
tint = color
) )
} Spacer(modifier = Modifier.height(OrganicSpacing.small))
Spacer(modifier = Modifier.height(8.dp))
Text( Text(
text = label, text = label,
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
@@ -745,21 +716,14 @@ fun StatCard(
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Box( OrganicIconContainer(
modifier = Modifier icon = icon,
.size(44.dp) size = 44.dp,
.clip(CircleShape) iconSize = 22.dp,
.background(color.copy(alpha = 0.1f)), containerColor = color.copy(alpha = 0.1f),
contentAlignment = Alignment.Center iconTint = color
) {
Icon(
icon,
contentDescription = null,
modifier = Modifier.size(22.dp),
tint = color
) )
} Spacer(modifier = Modifier.height(OrganicSpacing.small))
Spacer(modifier = Modifier.height(8.dp))
Text( Text(
text = value, text = value,
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,

View File

@@ -1,12 +1,9 @@
package com.example.casera.ui.screens package com.example.casera.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items 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.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material3.* import androidx.compose.material3.*
@@ -14,8 +11,6 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp 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.utils.SubscriptionHelper
import com.example.casera.analytics.PostHogAnalytics import com.example.casera.analytics.PostHogAnalytics
import com.example.casera.analytics.AnalyticsEvents import com.example.casera.analytics.AnalyticsEvents
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.* import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -215,11 +211,11 @@ fun ContractorsScreen(
) )
} }
} else { } else {
WarmGradientBackground {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(padding) .padding(padding)
.background(MaterialTheme.colorScheme.background)
) { ) {
// Search bar // Search bar
OutlinedTextField( OutlinedTextField(
@@ -227,7 +223,7 @@ fun ContractorsScreen(
onValueChange = { searchQuery = it }, onValueChange = { searchQuery = it },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp), .padding(horizontal = OrganicSpacing.medium, vertical = OrganicSpacing.small),
placeholder = { Text(stringResource(Res.string.contractors_search)) }, placeholder = { Text(stringResource(Res.string.contractors_search)) },
leadingIcon = { Icon(Icons.Default.Search, stringResource(Res.string.common_search)) }, leadingIcon = { Icon(Icons.Default.Search, stringResource(Res.string.common_search)) },
trailingIcon = { trailingIcon = {
@@ -238,7 +234,7 @@ fun ContractorsScreen(
} }
}, },
singleLine = true, singleLine = true,
shape = RoundedCornerShape(12.dp), shape = OrganicShapes.medium,
colors = OutlinedTextFieldDefaults.colors( colors = OutlinedTextFieldDefaults.colors(
focusedContainerColor = MaterialTheme.colorScheme.surface, focusedContainerColor = MaterialTheme.colorScheme.surface,
unfocusedContainerColor = MaterialTheme.colorScheme.surface, unfocusedContainerColor = MaterialTheme.colorScheme.surface,
@@ -252,8 +248,8 @@ fun ContractorsScreen(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp), .padding(horizontal = OrganicSpacing.medium, vertical = OrganicSpacing.extraSmall),
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.small)
) { ) {
if (showFavoritesOnly) { if (showFavoritesOnly) {
FilterChip( FilterChip(
@@ -288,33 +284,37 @@ fun ContractorsScreen(
) { _ -> ) { _ ->
// Use filteredContractors for client-side filtering // Use filteredContractors for client-side filtering
if (filteredContractors.isEmpty()) { if (filteredContractors.isEmpty()) {
// Empty state // Empty state with organic styling
Box( Box(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.small)
) { ) {
Icon( OrganicIconContainer(
Icons.Default.PersonAdd, icon = Icons.Default.PersonAdd,
contentDescription = null, size = 80.dp,
modifier = Modifier.size(64.dp), iconSize = 40.dp,
tint = MaterialTheme.colorScheme.onSurfaceVariant containerColor = MaterialTheme.colorScheme.primaryContainer,
iconTint = MaterialTheme.colorScheme.onPrimaryContainer
) )
Spacer(modifier = Modifier.height(OrganicSpacing.small))
Text( Text(
if (searchQuery.isNotEmpty() || selectedFilter != null || showFavoritesOnly) if (searchQuery.isNotEmpty() || selectedFilter != null || showFavoritesOnly)
stringResource(Res.string.contractors_no_results) stringResource(Res.string.contractors_no_results)
else else
stringResource(Res.string.contractors_empty_title), stringResource(Res.string.contractors_empty_title),
color = MaterialTheme.colorScheme.onSurfaceVariant style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurface
) )
if (searchQuery.isEmpty() && selectedFilter == null && !showFavoritesOnly) { if (searchQuery.isEmpty() && selectedFilter == null && !showFavoritesOnly) {
Text( Text(
stringResource(Res.string.contractors_empty_subtitle_first), stringResource(Res.string.contractors_empty_subtitle_first),
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall style = MaterialTheme.typography.bodyMedium
) )
} }
} }
@@ -330,8 +330,8 @@ fun ContractorsScreen(
) { ) {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(OrganicSpacing.medium),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.medium)
) { ) {
items(filteredContractors, key = { it.id }) { contractor -> items(filteredContractors, key = { it.id }) { contractor ->
ContractorCard( ContractorCard(
@@ -347,6 +347,7 @@ fun ContractorsScreen(
} }
} }
} }
}
if (showAddDialog) { if (showAddDialog) {
AddContractorDialog( AddContractorDialog(
@@ -381,41 +382,27 @@ fun ContractorCard(
onToggleFavorite: (Int) -> Unit, onToggleFavorite: (Int) -> Unit,
onClick: (Int) -> Unit onClick: (Int) -> Unit
) { ) {
Card( OrganicCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { onClick(contractor.id) }, .clickable { onClick(contractor.id) }
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(
defaultElevation = 1.dp
)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(OrganicSpacing.medium),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// Avatar/Icon // Avatar/Icon
Box( OrganicIconContainer(
modifier = Modifier icon = Icons.Default.Person,
.size(56.dp) size = 56.dp,
.clip(CircleShape) iconSize = 32.dp,
.background(MaterialTheme.colorScheme.primaryContainer), containerColor = MaterialTheme.colorScheme.primaryContainer,
contentAlignment = Alignment.Center iconTint = MaterialTheme.colorScheme.onPrimaryContainer
) {
Icon(
Icons.Default.Person,
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = MaterialTheme.colorScheme.onPrimaryContainer
) )
}
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(OrganicSpacing.medium))
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
@@ -428,7 +415,7 @@ fun ContractorCard(
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
if (contractor.isFavorite) { if (contractor.isFavorite) {
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(OrganicSpacing.extraSmall))
Icon( Icon(
Icons.Default.Star, Icons.Default.Star,
contentDescription = stringResource(Res.string.contractors_favorite), 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( Row(
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.medium),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
if (contractor.specialties.isNotEmpty()) { if (contractor.specialties.isNotEmpty()) {
@@ -462,7 +449,7 @@ fun ContractorCard(
modifier = Modifier.size(14.dp), modifier = Modifier.size(14.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant tint = MaterialTheme.colorScheme.onSurfaceVariant
) )
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(OrganicSpacing.extraSmall))
Text( Text(
text = contractor.specialties.first().name, text = contractor.specialties.first().name,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
@@ -479,7 +466,7 @@ fun ContractorCard(
modifier = Modifier.size(14.dp), modifier = Modifier.size(14.dp),
tint = MaterialTheme.colorScheme.tertiary tint = MaterialTheme.colorScheme.tertiary
) )
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(OrganicSpacing.extraSmall))
Text( Text(
text = "${(contractor.rating * 10).toInt() / 10.0}", text = "${(contractor.rating * 10).toInt() / 10.0}",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
@@ -497,7 +484,7 @@ fun ContractorCard(
modifier = Modifier.size(14.dp), modifier = Modifier.size(14.dp),
tint = MaterialTheme.colorScheme.secondary tint = MaterialTheme.colorScheme.secondary
) )
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(OrganicSpacing.extraSmall))
Text( Text(
text = stringResource(Res.string.contractors_tasks_count, contractor.taskCount), text = stringResource(Res.string.contractors_tasks_count, contractor.taskCount),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
@@ -528,4 +515,3 @@ fun ContractorCard(
} }
} }
} }

View File

@@ -36,6 +36,7 @@ import coil3.compose.SubcomposeAsyncImageContent
import coil3.compose.AsyncImagePainter import coil3.compose.AsyncImagePainter
import com.example.casera.ui.components.AuthenticatedImage import com.example.casera.ui.components.AuthenticatedImage
import com.example.casera.util.DateUtils import com.example.casera.util.DateUtils
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.* import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -96,6 +97,7 @@ fun DocumentDetailScreen(
) )
} }
) { padding -> ) { padding ->
WarmGradientBackground {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -110,8 +112,8 @@ fun DocumentDetailScreen(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 96.dp), .padding(start = OrganicSpacing.lg, end = OrganicSpacing.lg, top = OrganicSpacing.lg, bottom = 96.dp),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg)
) { ) {
// Status badge (for warranties) // Status badge (for warranties)
if (document.documentType == "warranty") { if (document.documentType == "warranty") {
@@ -124,16 +126,14 @@ fun DocumentDetailScreen(
else -> Color(0xFF10B981) else -> Color(0xFF10B981)
} }
Card( OrganicCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors( accentColor = statusColor.copy(alpha = 0.1f)
containerColor = statusColor.copy(alpha = 0.1f)
)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(OrganicSpacing.lg),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -175,17 +175,17 @@ fun DocumentDetailScreen(
} }
// Basic Information // Basic Information
Card(modifier = Modifier.fillMaxWidth()) { OrganicCard(modifier = Modifier.fillMaxWidth()) {
Column( Column(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
Text( Text(
stringResource(Res.string.documents_basic_info), stringResource(Res.string.documents_basic_info),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Divider() OrganicDivider()
DetailRow(stringResource(Res.string.documents_title_label), document.title) DetailRow(stringResource(Res.string.documents_title_label), document.title)
DetailRow(stringResource(Res.string.documents_type_label), DocumentType.fromValue(document.documentType).displayName) DetailRow(stringResource(Res.string.documents_type_label), DocumentType.fromValue(document.documentType).displayName)
@@ -202,17 +202,17 @@ fun DocumentDetailScreen(
if (document.documentType == "warranty" && if (document.documentType == "warranty" &&
(document.itemName != null || document.modelNumber != null || (document.itemName != null || document.modelNumber != null ||
document.serialNumber != null || document.provider != null)) { document.serialNumber != null || document.provider != null)) {
Card(modifier = Modifier.fillMaxWidth()) { OrganicCard(modifier = Modifier.fillMaxWidth()) {
Column( Column(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
Text( Text(
stringResource(Res.string.documents_item_details), stringResource(Res.string.documents_item_details),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Divider() OrganicDivider()
document.itemName?.let { DetailRow(stringResource(Res.string.documents_item_name), it) } document.itemName?.let { DetailRow(stringResource(Res.string.documents_item_name), it) }
document.modelNumber?.let { DetailRow(stringResource(Res.string.documents_model_number), it) } document.modelNumber?.let { DetailRow(stringResource(Res.string.documents_model_number), it) }
@@ -227,17 +227,17 @@ fun DocumentDetailScreen(
if (document.documentType == "warranty" && if (document.documentType == "warranty" &&
(document.claimPhone != null || document.claimEmail != null || (document.claimPhone != null || document.claimEmail != null ||
document.claimWebsite != null)) { document.claimWebsite != null)) {
Card(modifier = Modifier.fillMaxWidth()) { OrganicCard(modifier = Modifier.fillMaxWidth()) {
Column( Column(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
Text( Text(
stringResource(Res.string.documents_claim_info), stringResource(Res.string.documents_claim_info),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Divider() OrganicDivider()
document.claimPhone?.let { DetailRow(stringResource(Res.string.documents_claim_phone), it) } document.claimPhone?.let { DetailRow(stringResource(Res.string.documents_claim_phone), it) }
document.claimEmail?.let { DetailRow(stringResource(Res.string.documents_claim_email), it) } document.claimEmail?.let { DetailRow(stringResource(Res.string.documents_claim_email), it) }
@@ -249,17 +249,17 @@ fun DocumentDetailScreen(
// Dates // Dates
if (document.purchaseDate != null || document.startDate != null || if (document.purchaseDate != null || document.startDate != null ||
document.endDate != null) { document.endDate != null) {
Card(modifier = Modifier.fillMaxWidth()) { OrganicCard(modifier = Modifier.fillMaxWidth()) {
Column( Column(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
Text( Text(
stringResource(Res.string.documents_important_dates), stringResource(Res.string.documents_important_dates),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Divider() OrganicDivider()
document.purchaseDate?.let { DetailRow(stringResource(Res.string.documents_purchase_date), DateUtils.formatDateMedium(it)) } 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)) } document.startDate?.let { DetailRow(stringResource(Res.string.documents_start_date), DateUtils.formatDateMedium(it)) }
@@ -269,17 +269,17 @@ fun DocumentDetailScreen(
} }
// Residence & Contractor // Residence & Contractor
Card(modifier = Modifier.fillMaxWidth()) { OrganicCard(modifier = Modifier.fillMaxWidth()) {
Column( Column(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
Text( Text(
stringResource(Res.string.documents_associations), stringResource(Res.string.documents_associations),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Divider() OrganicDivider()
document.residenceAddress?.let { DetailRow(stringResource(Res.string.documents_residence), it) } document.residenceAddress?.let { DetailRow(stringResource(Res.string.documents_residence), it) }
document.contractorName?.let { DetailRow(stringResource(Res.string.documents_contractor), it) } document.contractorName?.let { DetailRow(stringResource(Res.string.documents_contractor), it) }
@@ -289,17 +289,17 @@ fun DocumentDetailScreen(
// Additional Information // Additional Information
if (document.tags != null || document.notes != null) { if (document.tags != null || document.notes != null) {
Card(modifier = Modifier.fillMaxWidth()) { OrganicCard(modifier = Modifier.fillMaxWidth()) {
Column( Column(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
Text( Text(
stringResource(Res.string.documents_additional_info), stringResource(Res.string.documents_additional_info),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Divider() OrganicDivider()
document.tags?.let { DetailRow(stringResource(Res.string.documents_tags), it) } document.tags?.let { DetailRow(stringResource(Res.string.documents_tags), it) }
document.notes?.let { DetailRow(stringResource(Res.string.documents_notes), it) } document.notes?.let { DetailRow(stringResource(Res.string.documents_notes), it) }
@@ -309,22 +309,22 @@ fun DocumentDetailScreen(
// Images // Images
if (document.images.isNotEmpty()) { if (document.images.isNotEmpty()) {
Card(modifier = Modifier.fillMaxWidth()) { OrganicCard(modifier = Modifier.fillMaxWidth()) {
Column( Column(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
Text( Text(
stringResource(Res.string.documents_images, document.images.size), stringResource(Res.string.documents_images, document.images.size),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Divider() OrganicDivider()
// Image grid // Image grid
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
) { ) {
document.images.take(4).forEachIndexed { index, image -> document.images.take(4).forEachIndexed { index, image ->
Box( Box(
@@ -366,17 +366,17 @@ fun DocumentDetailScreen(
// File Information // File Information
if (document.fileUrl != null) { if (document.fileUrl != null) {
Card(modifier = Modifier.fillMaxWidth()) { OrganicCard(modifier = Modifier.fillMaxWidth()) {
Column( Column(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
Text( Text(
stringResource(Res.string.documents_attached_file), stringResource(Res.string.documents_attached_file),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Divider() OrganicDivider()
document.fileType?.let { DetailRow(stringResource(Res.string.documents_file_type), it) } document.fileType?.let { DetailRow(stringResource(Res.string.documents_file_type), it) }
document.fileSize?.let { document.fileSize?.let {
@@ -388,7 +388,7 @@ fun DocumentDetailScreen(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
Icon(Icons.Default.Download, null) Icon(Icons.Default.Download, null)
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(OrganicSpacing.sm))
Text(stringResource(Res.string.documents_download_file)) Text(stringResource(Res.string.documents_download_file))
} }
} }
@@ -396,17 +396,17 @@ fun DocumentDetailScreen(
} }
// Metadata // Metadata
Card(modifier = Modifier.fillMaxWidth()) { OrganicCard(modifier = Modifier.fillMaxWidth()) {
Column( Column(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
Text( Text(
stringResource(Res.string.documents_metadata), stringResource(Res.string.documents_metadata),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Divider() OrganicDivider()
document.uploadedByUsername?.let { DetailRow(stringResource(Res.string.documents_uploaded_by), it) } document.uploadedByUsername?.let { DetailRow(stringResource(Res.string.documents_uploaded_by), it) }
document.createdAt?.let { DetailRow(stringResource(Res.string.documents_created), DateUtils.formatDateMedium(it)) } document.createdAt?.let { DetailRow(stringResource(Res.string.documents_created), DateUtils.formatDateMedium(it)) }
@@ -417,6 +417,7 @@ fun DocumentDetailScreen(
} }
} }
} }
}
// Delete confirmation dialog // Delete confirmation dialog
if (showDeleteDialog) { if (showDeleteDialog) {
@@ -463,7 +464,7 @@ fun DetailRow(label: String, value: String) {
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = Color.Gray color = Color.Gray
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(OrganicSpacing.xs))
Text( Text(
value, value,
style = MaterialTheme.typography.bodyLarge style = MaterialTheme.typography.bodyLarge
@@ -498,7 +499,7 @@ fun DocumentImageViewer(
modifier = Modifier modifier = Modifier
.fillMaxWidth(0.95f) .fillMaxWidth(0.95f)
.fillMaxHeight(0.9f), .fillMaxHeight(0.9f),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(OrganicSpacing.lg),
color = MaterialTheme.colorScheme.background color = MaterialTheme.colorScheme.background
) { ) {
Column( Column(
@@ -508,7 +509,7 @@ fun DocumentImageViewer(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(OrganicSpacing.lg),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -531,7 +532,7 @@ fun DocumentImageViewer(
} }
} }
HorizontalDivider() OrganicDivider()
// Content // Content
if (showFullImage) { if (showFullImage) {
@@ -539,7 +540,7 @@ fun DocumentImageViewer(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(16.dp), .padding(OrganicSpacing.lg),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
@@ -553,16 +554,13 @@ fun DocumentImageViewer(
) )
images[selectedIndex].caption?.let { caption -> images[selectedIndex].caption?.let { caption ->
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(OrganicSpacing.lg))
Card( OrganicCard(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
Text( Text(
text = caption, text = caption,
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(OrganicSpacing.lg),
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium
) )
} }
@@ -570,7 +568,7 @@ fun DocumentImageViewer(
// Navigation buttons // Navigation buttons
if (images.size > 1) { if (images.size > 1) {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(OrganicSpacing.lg))
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
@@ -580,7 +578,7 @@ fun DocumentImageViewer(
enabled = selectedIndex > 0 enabled = selectedIndex > 0
) { ) {
Icon(Icons.Default.ArrowBack, "Previous") Icon(Icons.Default.ArrowBack, "Previous")
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(OrganicSpacing.sm))
Text("Previous") Text("Previous")
} }
Button( Button(
@@ -588,7 +586,7 @@ fun DocumentImageViewer(
enabled = selectedIndex < images.size - 1 enabled = selectedIndex < images.size - 1
) { ) {
Text("Next") Text("Next")
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(OrganicSpacing.sm))
Icon(Icons.Default.ArrowForward, "Next") Icon(Icons.Default.ArrowForward, "Next")
} }
} }
@@ -600,19 +598,19 @@ fun DocumentImageViewer(
columns = GridCells.Fixed(2), columns = GridCells.Fixed(2),
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(16.dp), .padding(OrganicSpacing.lg),
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
items(images.size) { index -> items(images.size) { index ->
val image = images[index] val image = images[index]
Card( OrganicCard(
onClick = { modifier = Modifier
.fillMaxWidth()
.clickable {
selectedIndex = index selectedIndex = index
showFullImage = true showFullImage = true
}, }
shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) { ) {
Column { Column {
AuthenticatedImage( AuthenticatedImage(
@@ -627,7 +625,7 @@ fun DocumentImageViewer(
image.caption?.let { caption -> image.caption?.let { caption ->
Text( Text(
text = caption, text = caption,
modifier = Modifier.padding(8.dp), modifier = Modifier.padding(OrganicSpacing.sm),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
maxLines = 2 maxLines = 2
) )

View File

@@ -28,6 +28,7 @@ import com.example.casera.platform.rememberImagePicker
import com.example.casera.platform.rememberCameraPicker import com.example.casera.platform.rememberCameraPicker
import com.example.casera.analytics.PostHogAnalytics import com.example.casera.analytics.PostHogAnalytics
import com.example.casera.analytics.AnalyticsEvents import com.example.casera.analytics.AnalyticsEvents
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.* import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -201,13 +202,14 @@ fun DocumentFormScreen(
) )
} }
) { padding -> ) { padding ->
WarmGradientBackground {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(padding) .padding(padding)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 96.dp), .padding(start = OrganicSpacing.cozy, end = OrganicSpacing.cozy, top = OrganicSpacing.cozy, bottom = 96.dp),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
) { ) {
// Loading state for edit mode // Loading state for edit mode
if (isEditMode && documentDetailState is ApiResult.Loading) { if (isEditMode && documentDetailState is ApiResult.Loading) {
@@ -497,14 +499,12 @@ fun DocumentFormScreen(
// Existing images (edit mode only) // Existing images (edit mode only)
if (isEditMode && existingImages.isNotEmpty()) { if (isEditMode && existingImages.isNotEmpty()) {
Card( OrganicCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors( showBlob = false
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) { ) {
Column( Column(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(OrganicSpacing.cozy),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
Text( Text(
@@ -529,14 +529,12 @@ fun DocumentFormScreen(
} }
// Image upload section // Image upload section
Card( OrganicCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors( showBlob = false
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) { ) {
Column( Column(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(OrganicSpacing.cozy),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
Text( Text(
@@ -549,7 +547,7 @@ fun DocumentFormScreen(
) )
Row( Row(
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) { ) {
Button( Button(
onClick = { cameraPicker() }, onClick = { cameraPicker() },
@@ -575,7 +573,7 @@ fun DocumentFormScreen(
// Display selected images // Display selected images
if (selectedImages.isNotEmpty()) { if (selectedImages.isNotEmpty()) {
Column( Column(
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) { ) {
selectedImages.forEachIndexed { index, image -> selectedImages.forEachIndexed { index, image ->
Row( Row(
@@ -584,7 +582,7 @@ fun DocumentFormScreen(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
@@ -617,16 +615,14 @@ fun DocumentFormScreen(
// Error message // Error message
if (operationState is ApiResult.Error) { if (operationState is ApiResult.Error) {
Card( OrganicCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors( showBlob = false
containerColor = MaterialTheme.colorScheme.errorContainer
)
) { ) {
Text( Text(
com.example.casera.util.ErrorMessageParser.parse((operationState as ApiResult.Error).message), com.example.casera.util.ErrorMessageParser.parse((operationState as ApiResult.Error).message),
modifier = Modifier.padding(12.dp), modifier = Modifier.padding(12.dp),
color = MaterialTheme.colorScheme.onErrorContainer color = MaterialTheme.colorScheme.error
) )
} }
} }
@@ -638,7 +634,13 @@ fun DocumentFormScreen(
val providerRequiredError = stringResource(Res.string.documents_form_provider_error) val providerRequiredError = stringResource(Res.string.documents_form_provider_error)
// Save Button // Save Button
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 = { onClick = {
// Validate // Validate
var hasError = false var hasError = false
@@ -723,25 +725,10 @@ fun DocumentFormScreen(
} }
} }
}, },
modifier = Modifier.fillMaxWidth(), enabled = operationState !is ApiResult.Loading,
enabled = operationState !is ApiResult.Loading isLoading = 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)
}
) )
} }
} }
} }
} }
}

View File

@@ -18,6 +18,7 @@ import com.example.casera.viewmodel.DocumentViewModel
import com.example.casera.models.* import com.example.casera.models.*
import com.example.casera.analytics.PostHogAnalytics import com.example.casera.analytics.PostHogAnalytics
import com.example.casera.analytics.AnalyticsEvents import com.example.casera.analytics.AnalyticsEvents
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.* import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -120,7 +121,7 @@ fun DocumentsScreen(
showFiltersMenu = false showFiltersMenu = false
} }
) )
Divider() OrganicDivider()
DocumentCategory.values().forEach { category -> DocumentCategory.values().forEach { category ->
DropdownMenuItem( DropdownMenuItem(
text = { Text(category.displayName) }, text = { Text(category.displayName) },
@@ -138,7 +139,7 @@ fun DocumentsScreen(
showFiltersMenu = false showFiltersMenu = false
} }
) )
Divider() OrganicDivider()
DocumentType.values().forEach { type -> DocumentType.values().forEach { type ->
DropdownMenuItem( DropdownMenuItem(
text = { Text(type.displayName) }, text = { Text(type.displayName) },
@@ -199,6 +200,7 @@ fun DocumentsScreen(
} }
} }
) { padding -> ) { padding ->
WarmGradientBackground {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -227,6 +229,7 @@ fun DocumentsScreen(
} }
} }
} }
}
// Show upgrade dialog when user hits limit // Show upgrade dialog when user hits limit
if (showUpgradeDialog) { if (showUpgradeDialog) {

View File

@@ -17,6 +17,7 @@ import com.example.casera.viewmodel.ResidenceViewModel
import com.example.casera.repository.LookupsRepository import com.example.casera.repository.LookupsRepository
import com.example.casera.models.* import com.example.casera.models.*
import com.example.casera.network.ApiResult import com.example.casera.network.ApiResult
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.* import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -103,13 +104,14 @@ fun EditTaskScreen(
) )
} }
) { paddingValues -> ) { paddingValues ->
WarmGradientBackground {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
.padding(16.dp) .padding(OrganicSpacing.cozy)
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
) { ) {
// Required fields section // Required fields section
Text( Text(
@@ -298,7 +300,8 @@ fun EditTaskScreen(
} }
// Submit button // Submit button
Button( OrganicPrimaryButton(
text = stringResource(Res.string.tasks_update),
onClick = { onClick = {
if (validateForm() && selectedCategory != null && if (validateForm() && selectedCategory != null &&
selectedFrequency != null && selectedPriority != null) { selectedFrequency != null && selectedPriority != null) {
@@ -321,21 +324,13 @@ fun EditTaskScreen(
) )
} }
}, },
modifier = Modifier.fillMaxWidth(),
enabled = validateForm() && selectedCategory != null && enabled = validateForm() && selectedCategory != null &&
selectedFrequency != null && selectedPriority != null selectedFrequency != null && selectedPriority != null,
) { isLoading = updateTaskState is ApiResult.Loading
if (updateTaskState is ApiResult.Loading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary
) )
} else {
Text(stringResource(Res.string.tasks_update))
}
}
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(OrganicSpacing.cozy))
}
} }
} }
} }

View File

@@ -1,8 +1,6 @@
package com.example.casera.ui.screens package com.example.casera.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material3.* 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.ui.components.common.ErrorCard
import com.example.casera.viewmodel.PasswordResetViewModel import com.example.casera.viewmodel.PasswordResetViewModel
import com.example.casera.network.ApiResult import com.example.casera.network.ApiResult
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.* import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -74,37 +73,51 @@ fun ForgotPasswordScreen(
) )
} }
) { paddingValues -> ) { paddingValues ->
WarmGradientBackground {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.padding(paddingValues), .padding(paddingValues),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Card( OrganicCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth(0.9f) .fillMaxWidth(0.9f)
.wrapContentHeight(), .wrapContentHeight(),
shape = RoundedCornerShape(24.dp), showBlob = true,
colors = CardDefaults.cardColors( blobVariation = 0
containerColor = MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(32.dp), .padding(OrganicSpacing.spacious),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
) { ) {
AuthHeader( OrganicIconContainer(
icon = Icons.Default.Key, icon = Icons.Default.Key,
title = stringResource(Res.string.auth_forgot_title), size = 80.dp,
subtitle = stringResource(Res.string.auth_forgot_subtitle) iconScale = 0.5f,
backgroundColor = MaterialTheme.colorScheme.primary,
iconColor = MaterialTheme.colorScheme.onPrimary
) )
Spacer(modifier = Modifier.height(8.dp)) 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( OutlinedTextField(
value = email, value = email,
@@ -118,74 +131,58 @@ fun ForgotPasswordScreen(
}, },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true, singleLine = true,
shape = RoundedCornerShape(12.dp),
enabled = !isLoading enabled = !isLoading
) )
Text( Text(
"We'll send a 6-digit verification code to this address", "We'll send a 6-digit verification code to this address",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.textSecondary,
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
ErrorCard(message = errorMessage) ErrorCard(message = errorMessage)
if (isSuccess) { if (isSuccess) {
Card( OrganicCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors( accentColor = MaterialTheme.colorScheme.primary,
containerColor = MaterialTheme.colorScheme.primaryContainer showBlob = false
)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(OrganicSpacing.cozy),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) { ) {
Icon( Icon(
Icons.Default.CheckCircle, Icons.Default.CheckCircle,
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.primary
) )
Spacer(modifier = Modifier.width(12.dp))
Text( Text(
"Check your email for a 6-digit verification code", "Check your email for a 6-digit verification code",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer color = MaterialTheme.colorScheme.textPrimary
) )
} }
} }
} }
Button( OrganicDivider(
modifier = Modifier.fillMaxWidth()
)
OrganicPrimaryButton(
text = stringResource(Res.string.auth_forgot_button),
onClick = { onClick = {
viewModel.setEmail(email) viewModel.setEmail(email)
viewModel.requestPasswordReset(email) viewModel.requestPasswordReset(email)
}, },
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
enabled = email.isNotEmpty() && !isLoading, enabled = email.isNotEmpty() && !isLoading,
shape = RoundedCornerShape(12.dp) isLoading = isLoading
) {
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))
Text(
stringResource(Res.string.auth_forgot_button),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
}
}
TextButton( TextButton(
onClick = onNavigateBack, onClick = onNavigateBack,
@@ -201,3 +198,4 @@ fun ForgotPasswordScreen(
} }
} }
} }
}

View File

@@ -1,22 +1,18 @@
package com.example.casera.ui.screens package com.example.casera.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.casera.ui.components.HandleErrors 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.ResidenceViewModel
import com.example.casera.viewmodel.TaskViewModel import com.example.casera.viewmodel.TaskViewModel
import com.example.casera.network.ApiResult import com.example.casera.network.ApiResult
@@ -67,17 +63,17 @@ fun HomeScreen(
) )
} }
) { paddingValues -> ) { paddingValues ->
WarmGradientBackground {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.padding(paddingValues) .padding(paddingValues)
.padding(horizontal = 16.dp, vertical = 12.dp), .padding(horizontal = OrganicSpacing.comfortable, vertical = OrganicSpacing.cozy),
verticalArrangement = Arrangement.spacedBy(20.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.generous)
) { ) {
// Personalized Greeting // Personalized Greeting
Column( Column(
modifier = Modifier.padding(vertical = 8.dp) modifier = Modifier.padding(vertical = OrganicSpacing.cozy)
) { ) {
Text( Text(
text = stringResource(Res.string.home_welcome), text = stringResource(Res.string.home_welcome),
@@ -94,45 +90,23 @@ fun HomeScreen(
when (summaryState) { when (summaryState) {
is ApiResult.Success -> { is ApiResult.Success -> {
val summary = (summaryState as ApiResult.Success).data val summary = (summaryState as ApiResult.Success).data
Card( OrganicCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.large, showBlob = true,
colors = CardDefaults.cardColors( blobVariation = 0
containerColor = MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier.fillMaxWidth()
.fillMaxWidth()
.padding(20.dp)
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp) horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
) { ) {
// Gradient circular icon // Gradient circular icon
Box( OrganicIconContainer(
modifier = Modifier icon = Icons.Default.Home,
.size(44.dp) size = 44.dp
.clip(CircleShape)
.background(
Brush.linearGradient(
listOf(
Color(0xFF2563EB),
Color(0xFF8B5CF6)
) )
)
),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.Home,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(24.dp)
)
}
Column { Column {
Text( Text(
text = stringResource(Res.string.home_overview), text = stringResource(Res.string.home_overview),
@@ -147,35 +121,38 @@ fun HomeScreen(
} }
} }
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(OrganicSpacing.generous))
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly horizontalArrangement = Arrangement.SpaceEvenly
) { ) {
StatItem( OrganicStatPill(
icon = Icons.Default.Home,
value = "${summary.residences.size}", value = "${summary.residences.size}",
label = stringResource(Res.string.home_properties), label = stringResource(Res.string.home_properties),
color = Color(0xFF3B82F6) color = Color(0xFF3B82F6)
) )
Divider( OrganicDivider(
modifier = Modifier modifier = Modifier
.height(48.dp) .height(48.dp)
.width(1.dp), .width(1.dp),
color = MaterialTheme.colorScheme.outlineVariant vertical = true
) )
StatItem( OrganicStatPill(
icon = Icons.Default.Task,
value = "${totalSummary?.totalTasks ?: 0}", value = "${totalSummary?.totalTasks ?: 0}",
label = stringResource(Res.string.home_total_tasks), label = stringResource(Res.string.home_total_tasks),
color = Color(0xFF8B5CF6) color = Color(0xFF8B5CF6)
) )
Divider( OrganicDivider(
modifier = Modifier modifier = Modifier
.height(48.dp) .height(48.dp)
.width(1.dp), .width(1.dp),
color = MaterialTheme.colorScheme.outlineVariant vertical = true
) )
StatItem( OrganicStatPill(
icon = Icons.Default.Schedule,
value = "${totalSummary?.totalPending ?: 0}", value = "${totalSummary?.totalPending ?: 0}",
label = stringResource(Res.string.home_pending), label = stringResource(Res.string.home_pending),
color = Color(0xFFF59E0B) color = Color(0xFFF59E0B)
@@ -185,7 +162,7 @@ fun HomeScreen(
} }
} }
is ApiResult.Idle, is ApiResult.Loading -> { is ApiResult.Idle, is ApiResult.Loading -> {
Card(modifier = Modifier.fillMaxWidth()) { OrganicCard(modifier = Modifier.fillMaxWidth()) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -223,36 +200,6 @@ fun HomeScreen(
} }
} }
} }
@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 @Composable
@@ -263,45 +210,24 @@ private fun NavigationCard(
iconColor: Color, iconColor: Color,
onClick: () -> Unit onClick: () -> Unit
) { ) {
Card( OrganicCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { onClick() }, .clickable { onClick() },
shape = MaterialTheme.shapes.large, showBlob = true,
colors = CardDefaults.cardColors( blobVariation = 1
containerColor = MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.fillMaxWidth()
.padding(20.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp) horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.comfortable)
) { ) {
// Gradient circular icon // Gradient circular icon
Box( OrganicIconContainer(
modifier = Modifier icon = icon,
.size(56.dp) size = 56.dp,
.clip(CircleShape) iconColor = iconColor
.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)
)
}
Column( Column(
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
@@ -311,7 +237,7 @@ private fun NavigationCard(
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(OrganicSpacing.minimal))
Text( Text(
text = subtitle, text = subtitle,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,

View File

@@ -29,6 +29,7 @@ import com.example.casera.viewmodel.AuthViewModel
import com.example.casera.network.ApiResult import com.example.casera.network.ApiResult
import com.example.casera.analytics.PostHogAnalytics import com.example.casera.analytics.PostHogAnalytics
import com.example.casera.analytics.AnalyticsEvents import com.example.casera.analytics.AnalyticsEvents
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.* import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -93,28 +94,25 @@ fun LoginScreen(
val isLoading = loginState is ApiResult.Loading || googleSignInState is ApiResult.Loading val isLoading = loginState is ApiResult.Loading || googleSignInState is ApiResult.Loading
WarmGradientBackground {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize(),
.background(MaterialTheme.colorScheme.background),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Card( OrganicCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth(0.9f) .fillMaxWidth(0.9f)
.wrapContentHeight(), .wrapContentHeight(),
shape = MaterialTheme.shapes.extraLarge, showBlob = true,
colors = CardDefaults.cardColors( blobVariation = 0
containerColor = MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(32.dp), .padding(OrganicSpacing.xxl),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg)
) { ) {
AuthHeader( AuthHeader(
icon = Icons.Default.Home, icon = Icons.Default.Home,
@@ -122,7 +120,7 @@ fun LoginScreen(
subtitle = stringResource(Res.string.app_tagline) subtitle = stringResource(Res.string.app_tagline)
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(OrganicSpacing.sm))
OutlinedTextField( OutlinedTextField(
value = username, value = username,
@@ -133,7 +131,7 @@ fun LoginScreen(
}, },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true, singleLine = true,
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(OrganicRadius.md),
keyboardOptions = KeyboardOptions( keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email, keyboardType = KeyboardType.Email,
imeAction = ImeAction.Next imeAction = ImeAction.Next
@@ -158,7 +156,7 @@ fun LoginScreen(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true, singleLine = true,
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
shape = RoundedCornerShape(12.dp) shape = RoundedCornerShape(OrganicRadius.md)
) )
ErrorCard(message = errorMessage) ErrorCard(message = errorMessage)
@@ -168,75 +166,33 @@ fun LoginScreen(
googleSignInError = null googleSignInError = null
} }
// Gradient button // Organic Primary Button
Box( OrganicPrimaryButton(
modifier = Modifier text = stringResource(Res.string.auth_login_button),
.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
) {
Button(
onClick = { onClick = {
viewModel.login(username, password) viewModel.login(username, password)
}, },
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxWidth(),
enabled = username.isNotEmpty() && password.isNotEmpty(), enabled = username.isNotEmpty() && password.isNotEmpty(),
shape = MaterialTheme.shapes.medium, isLoading = isLoading
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent,
disabledContainerColor = Color.Transparent
) )
) {
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
)
}
}
}
// Divider with "or" // Divider with "or"
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
HorizontalDivider( OrganicDivider(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f)
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
) )
Text( Text(
text = "or", text = "or",
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = OrganicSpacing.lg),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
HorizontalDivider( OrganicDivider(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f)
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
) )
} }
@@ -278,3 +234,4 @@ fun LoginScreen(
} }
} }
} }
}

View File

@@ -17,6 +17,7 @@ import com.example.casera.models.Residence
import com.example.casera.models.TaskDetail import com.example.casera.models.TaskDetail
import com.example.casera.storage.TokenStorage import com.example.casera.storage.TokenStorage
import com.example.casera.ui.subscription.UpgradeScreen import com.example.casera.ui.subscription.UpgradeScreen
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.* import casera.composeapp.generated.resources.*
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -45,7 +46,9 @@ fun MainScreen(
} }
} }
WarmGradientBackground {
Scaffold( Scaffold(
containerColor = androidx.compose.ui.graphics.Color.Transparent,
bottomBar = { bottomBar = {
NavigationBar( NavigationBar(
containerColor = MaterialTheme.colorScheme.surfaceContainer, containerColor = MaterialTheme.colorScheme.surfaceContainer,
@@ -123,24 +126,6 @@ fun MainScreen(
unselectedTextColor = 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 -> ) { paddingValues ->
@@ -347,3 +332,4 @@ fun MainScreen(
} }
} }
} }
}

View File

@@ -3,7 +3,6 @@ package com.example.casera.ui.screens
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material3.* 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.ApiResult
import com.example.casera.network.ResidenceApi import com.example.casera.network.ResidenceApi
import com.example.casera.storage.TokenStorage import com.example.casera.storage.TokenStorage
import com.example.casera.ui.theme.AppRadius import com.example.casera.ui.theme.*
import com.example.casera.ui.theme.AppSpacing
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -68,7 +66,9 @@ fun ManageUsersScreen(
} }
} }
WarmGradientBackground {
Scaffold( Scaffold(
containerColor = androidx.compose.ui.graphics.Color.Transparent,
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { title = {
@@ -90,7 +90,7 @@ fun ManageUsersScreen(
} }
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface containerColor = androidx.compose.ui.graphics.Color.Transparent
) )
) )
}, },
@@ -114,7 +114,7 @@ fun ManageUsersScreen(
) { ) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.md) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
Icon( Icon(
Icons.Default.Error, Icons.Default.Error,
@@ -133,26 +133,23 @@ fun ManageUsersScreen(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues), .padding(paddingValues),
contentPadding = PaddingValues(AppSpacing.lg), contentPadding = PaddingValues(OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(AppSpacing.lg) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg)
) { ) {
// Share sections (primary owner only) // Share sections (primary owner only)
if (isPrimaryOwner) { if (isPrimaryOwner) {
// Easy Share Section // Easy Share Section
item { item {
Card( OrganicCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(AppRadius.lg), accentColor = MaterialTheme.colorScheme.primaryContainer
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
)
) { ) {
Column( Column(
modifier = Modifier.padding(AppSpacing.lg) modifier = Modifier.padding(OrganicSpacing.lg)
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm) horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
) { ) {
Icon( Icon(
Icons.Default.Share, Icons.Default.Share,
@@ -167,24 +164,16 @@ fun ManageUsersScreen(
) )
} }
Spacer(modifier = Modifier.height(AppSpacing.md)) Spacer(modifier = Modifier.height(OrganicSpacing.md))
Button( OrganicPrimaryButton(
text = stringResource(Res.string.manage_users_send_invite),
onClick = onSharePackage, onClick = onSharePackage,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors( icon = Icons.Default.Send
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)) Spacer(modifier = Modifier.height(OrganicSpacing.sm))
Text( Text(
text = stringResource(Res.string.manage_users_easy_share_desc), text = stringResource(Res.string.manage_users_easy_share_desc),
@@ -201,37 +190,32 @@ fun ManageUsersScreen(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
HorizontalDivider(modifier = Modifier.weight(1f)) OrganicDivider(modifier = Modifier.weight(1f))
Text( Text(
text = stringResource(Res.string.manage_users_or), text = stringResource(Res.string.manage_users_or),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = AppSpacing.lg) modifier = Modifier.padding(horizontal = OrganicSpacing.lg)
) )
HorizontalDivider(modifier = Modifier.weight(1f)) OrganicDivider(modifier = Modifier.weight(1f))
} }
} }
// Share Code Section // Share Code Section
item { item {
Card( OrganicCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth()
shape = RoundedCornerShape(AppRadius.lg),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) { ) {
Column( Column(
modifier = Modifier.padding(AppSpacing.lg) modifier = Modifier.padding(OrganicSpacing.lg)
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm) horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
) { ) {
Icon( OrganicIconContainer(
Icons.Default.Key, icon = Icons.Default.Key,
contentDescription = null, size = 24.dp
tint = MaterialTheme.colorScheme.primary
) )
Text( Text(
text = stringResource(Res.string.manage_users_share_code), text = stringResource(Res.string.manage_users_share_code),
@@ -240,20 +224,17 @@ fun ManageUsersScreen(
) )
} }
Spacer(modifier = Modifier.height(AppSpacing.lg)) Spacer(modifier = Modifier.height(OrganicSpacing.lg))
// Share code display // Share code display
Card( OrganicCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(AppRadius.md), accentColor = MaterialTheme.colorScheme.surface
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(AppSpacing.lg), .padding(OrganicSpacing.lg),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -291,9 +272,11 @@ fun ManageUsersScreen(
} }
} }
Spacer(modifier = Modifier.height(AppSpacing.md)) Spacer(modifier = Modifier.height(OrganicSpacing.md))
Button( OrganicPrimaryButton(
text = if (shareCode != null) stringResource(Res.string.manage_users_generate_new)
else stringResource(Res.string.manage_users_generate),
onClick = { onClick = {
scope.launch { scope.launch {
isGeneratingCode = true isGeneratingCode = true
@@ -312,27 +295,14 @@ fun ManageUsersScreen(
isGeneratingCode = false isGeneratingCode = false
} }
}, },
modifier = Modifier.fillMaxWidth(),
enabled = !isGeneratingCode, enabled = !isGeneratingCode,
modifier = Modifier.fillMaxWidth() isLoading = isGeneratingCode,
) { icon = Icons.Default.Refresh
if (isGeneratingCode) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 2.dp
) )
} 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) { if (shareCode != null) {
Spacer(modifier = Modifier.height(AppSpacing.sm)) Spacer(modifier = Modifier.height(OrganicSpacing.sm))
Text( Text(
text = stringResource(Res.string.manage_users_code_desc), text = stringResource(Res.string.manage_users_code_desc),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
@@ -344,7 +314,7 @@ fun ManageUsersScreen(
} }
item { item {
HorizontalDivider() OrganicDivider()
} }
} }
@@ -369,7 +339,8 @@ fun ManageUsersScreen(
// Bottom spacing // Bottom spacing
item { item {
Spacer(modifier = Modifier.height(AppSpacing.xl)) Spacer(modifier = Modifier.height(OrganicSpacing.xl))
}
} }
} }
} }
@@ -424,27 +395,23 @@ private fun UserCard(
canRemove: Boolean, canRemove: Boolean,
onRemove: () -> Unit onRemove: () -> Unit
) { ) {
Card( OrganicCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth()
shape = RoundedCornerShape(AppRadius.md),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(AppSpacing.lg), .padding(OrganicSpacing.lg),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(AppSpacing.md) horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
// Avatar // Avatar
Surface( Surface(
shape = RoundedCornerShape(AppRadius.md), shape = OrganicShapes.medium,
color = MaterialTheme.colorScheme.primaryContainer, color = MaterialTheme.colorScheme.primaryContainer,
modifier = Modifier.size(48.dp) modifier = Modifier.size(48.dp)
) { ) {
@@ -461,7 +428,7 @@ private fun UserCard(
Column { Column {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm) horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
) { ) {
Text( Text(
text = user.username, text = user.username,
@@ -471,13 +438,13 @@ private fun UserCard(
if (isOwner) { if (isOwner) {
Surface( Surface(
color = MaterialTheme.colorScheme.primaryContainer, color = MaterialTheme.colorScheme.primaryContainer,
shape = RoundedCornerShape(AppRadius.xs) shape = OrganicShapes.extraSmall
) { ) {
Text( Text(
text = stringResource(Res.string.manage_users_owner_badge), text = stringResource(Res.string.manage_users_owner_badge),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onPrimaryContainer, color = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.padding(horizontal = AppSpacing.sm, vertical = 2.dp) modifier = Modifier.padding(horizontal = OrganicSpacing.sm, vertical = 2.dp)
) )
} }
} }

View File

@@ -3,7 +3,6 @@ package com.example.casera.ui.screens
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* 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.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.casera.network.ApiResult import com.example.casera.network.ApiResult
import com.example.casera.ui.theme.AppRadius import com.example.casera.ui.theme.*
import com.example.casera.ui.theme.AppSpacing
import com.example.casera.util.DateUtils import com.example.casera.util.DateUtils
import com.example.casera.viewmodel.NotificationPreferencesViewModel import com.example.casera.viewmodel.NotificationPreferencesViewModel
import com.example.casera.analytics.PostHogAnalytics import com.example.casera.analytics.PostHogAnalytics
@@ -93,7 +91,9 @@ fun NotificationPreferencesScreen(
} }
} }
WarmGradientBackground {
Scaffold( Scaffold(
containerColor = androidx.compose.ui.graphics.Color.Transparent,
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { Text(stringResource(Res.string.notifications_title), fontWeight = FontWeight.SemiBold) }, title = { Text(stringResource(Res.string.notifications_title), fontWeight = FontWeight.SemiBold) },
@@ -103,7 +103,7 @@ fun NotificationPreferencesScreen(
} }
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface containerColor = androidx.compose.ui.graphics.Color.Transparent
) )
) )
} }
@@ -113,29 +113,23 @@ fun NotificationPreferencesScreen(
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(horizontal = AppSpacing.lg, vertical = AppSpacing.md), .padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.md),
verticalArrangement = Arrangement.spacedBy(AppSpacing.md) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
// Header // Header
Card( OrganicCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth()
shape = RoundedCornerShape(AppRadius.lg),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(AppSpacing.xl), .padding(OrganicSpacing.xl),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.md) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
Icon( OrganicIconContainer(
imageVector = Icons.Default.Notifications, icon = Icons.Default.Notifications,
contentDescription = null, size = 60.dp
modifier = Modifier.size(60.dp),
tint = MaterialTheme.colorScheme.primary
) )
Text( Text(
@@ -157,7 +151,7 @@ fun NotificationPreferencesScreen(
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(AppSpacing.xl), .padding(OrganicSpacing.xl),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
CircularProgressIndicator() CircularProgressIndicator()
@@ -165,21 +159,18 @@ fun NotificationPreferencesScreen(
} }
is ApiResult.Error -> { is ApiResult.Error -> {
Card( OrganicCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors( accentColor = MaterialTheme.colorScheme.errorContainer
containerColor = MaterialTheme.colorScheme.errorContainer
),
shape = RoundedCornerShape(AppRadius.md)
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(AppSpacing.lg), .padding(OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(AppSpacing.md) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(AppSpacing.md), horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
@@ -194,12 +185,11 @@ fun NotificationPreferencesScreen(
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
} }
Button( OrganicPrimaryButton(
text = stringResource(Res.string.common_retry),
onClick = { viewModel.loadPreferences() }, onClick = { viewModel.loadPreferences() },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { )
Text(stringResource(Res.string.common_retry))
}
} }
} }
} }
@@ -210,15 +200,11 @@ fun NotificationPreferencesScreen(
stringResource(Res.string.notifications_task_section), stringResource(Res.string.notifications_task_section),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(top = AppSpacing.md) modifier = Modifier.padding(top = OrganicSpacing.md)
) )
Card( OrganicCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth()
shape = RoundedCornerShape(AppRadius.md),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) { ) {
Column { Column {
NotificationToggle( NotificationToggle(
@@ -247,9 +233,8 @@ fun NotificationPreferencesScreen(
) )
} }
HorizontalDivider( OrganicDivider(
modifier = Modifier.padding(horizontal = AppSpacing.lg), modifier = Modifier.padding(horizontal = OrganicSpacing.lg)
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
) )
NotificationToggle( NotificationToggle(
@@ -278,9 +263,8 @@ fun NotificationPreferencesScreen(
) )
} }
HorizontalDivider( OrganicDivider(
modifier = Modifier.padding(horizontal = AppSpacing.lg), modifier = Modifier.padding(horizontal = OrganicSpacing.lg)
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
) )
NotificationToggle( NotificationToggle(
@@ -295,9 +279,8 @@ fun NotificationPreferencesScreen(
} }
) )
HorizontalDivider( OrganicDivider(
modifier = Modifier.padding(horizontal = AppSpacing.lg), modifier = Modifier.padding(horizontal = OrganicSpacing.lg)
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
) )
NotificationToggle( NotificationToggle(
@@ -346,15 +329,11 @@ fun NotificationPreferencesScreen(
stringResource(Res.string.notifications_other_section), stringResource(Res.string.notifications_other_section),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(top = AppSpacing.md) modifier = Modifier.padding(top = OrganicSpacing.md)
) )
Card( OrganicCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth()
shape = RoundedCornerShape(AppRadius.md),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) { ) {
Column { Column {
NotificationToggle( NotificationToggle(
@@ -369,9 +348,8 @@ fun NotificationPreferencesScreen(
} }
) )
HorizontalDivider( OrganicDivider(
modifier = Modifier.padding(horizontal = AppSpacing.lg), modifier = Modifier.padding(horizontal = OrganicSpacing.lg)
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
) )
NotificationToggle( NotificationToggle(
@@ -386,9 +364,8 @@ fun NotificationPreferencesScreen(
} }
) )
HorizontalDivider( OrganicDivider(
modifier = Modifier.padding(horizontal = AppSpacing.lg), modifier = Modifier.padding(horizontal = OrganicSpacing.lg)
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
) )
NotificationToggle( NotificationToggle(
@@ -438,15 +415,11 @@ fun NotificationPreferencesScreen(
stringResource(Res.string.notifications_email_section), stringResource(Res.string.notifications_email_section),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(top = AppSpacing.md) modifier = Modifier.padding(top = OrganicSpacing.md)
) )
Card( OrganicCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth()
shape = RoundedCornerShape(AppRadius.md),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) { ) {
Column { Column {
NotificationToggle( NotificationToggle(
@@ -463,7 +436,8 @@ fun NotificationPreferencesScreen(
} }
} }
Spacer(modifier = Modifier.height(AppSpacing.xl)) Spacer(modifier = Modifier.height(OrganicSpacing.xl))
}
} }
} }
} }
@@ -482,8 +456,8 @@ private fun NotificationToggle(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(AppSpacing.lg), .padding(OrganicSpacing.lg),
horizontalArrangement = Arrangement.spacedBy(AppSpacing.md), horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
@@ -528,8 +502,8 @@ private fun NotificationTimePickerRow(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(start = AppSpacing.lg + 24.dp + AppSpacing.md, end = AppSpacing.lg, bottom = AppSpacing.md), .padding(start = OrganicSpacing.lg + 24.dp + OrganicSpacing.md, end = OrganicSpacing.lg, bottom = OrganicSpacing.md),
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm), horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
@@ -584,7 +558,7 @@ private fun HourPickerDialog(
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.md) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
Text( Text(
text = DateUtils.formatHour(selectedHour), text = DateUtils.formatHour(selectedHour),
@@ -601,7 +575,7 @@ private fun HourPickerDialog(
// AM hours (6 AM - 11 AM) // AM hours (6 AM - 11 AM)
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
) { ) {
Text( Text(
"AM", "AM",
@@ -620,7 +594,7 @@ private fun HourPickerDialog(
// PM hours (12 PM - 5 PM) // PM hours (12 PM - 5 PM)
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
) { ) {
Text( Text(
"PM", "PM",
@@ -639,7 +613,7 @@ private fun HourPickerDialog(
// Evening hours (6 PM - 11 PM) // Evening hours (6 PM - 11 PM)
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
) { ) {
Text( Text(
"EVE", "EVE",
@@ -687,7 +661,7 @@ private fun HourChip(
modifier = Modifier modifier = Modifier
.width(56.dp) .width(56.dp)
.clickable { onClick() }, .clickable { onClick() },
shape = RoundedCornerShape(AppRadius.sm), shape = OrganicShapes.small,
color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant
) { ) {
Text( Text(
@@ -695,7 +669,7 @@ private fun HourChip(
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal, fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant, 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 textAlign = androidx.compose.ui.text.style.TextAlign.Center
) )
} }

View File

@@ -23,6 +23,7 @@ import com.example.casera.utils.SubscriptionHelper
import com.example.casera.ui.theme.AppRadius import com.example.casera.ui.theme.AppRadius
import com.example.casera.ui.theme.AppSpacing import com.example.casera.ui.theme.AppSpacing
import com.example.casera.ui.theme.ThemeManager import com.example.casera.ui.theme.ThemeManager
import com.example.casera.ui.theme.*
import com.example.casera.viewmodel.AuthViewModel import com.example.casera.viewmodel.AuthViewModel
import com.example.casera.network.ApiResult import com.example.casera.network.ApiResult
import com.example.casera.storage.TokenStorage import com.example.casera.storage.TokenStorage
@@ -140,6 +141,7 @@ fun ProfileScreen(
) )
} }
) { paddingValues -> ) { paddingValues ->
WarmGradientBackground {
if (isLoadingUser) { if (isLoadingUser) {
Box( Box(
modifier = Modifier modifier = Modifier
@@ -157,16 +159,15 @@ fun ProfileScreen(
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(start = 24.dp, end = 24.dp, top = 24.dp, bottom = 96.dp), .padding(start = 24.dp, end = 24.dp, top = 24.dp, bottom = 96.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(OrganicSpacing.sm))
// Profile Icon // Profile Icon
Icon( OrganicIconContainer(
Icons.Default.AccountCircle, icon = Icons.Default.AccountCircle,
contentDescription = null, size = 80.dp,
modifier = Modifier.size(80.dp), iconSize = 48.dp
tint = MaterialTheme.colorScheme.primary
) )
Text( Text(
@@ -175,27 +176,24 @@ fun ProfileScreen(
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(OrganicSpacing.sm))
// Edit Profile Section (scrolls down to profile fields) // Edit Profile Section
Card( OrganicCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { /* Profile fields are below - could add scroll behavior */ }, .clickable { /* Profile fields are below - could add scroll behavior */ }
shape = RoundedCornerShape(AppRadius.md), .naturalShadow()
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(AppSpacing.lg), .padding(OrganicSpacing.lg),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Column( Column(
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
) { ) {
Text( Text(
text = stringResource(Res.string.profile_edit_profile), text = stringResource(Res.string.profile_edit_profile),
@@ -219,24 +217,21 @@ fun ProfileScreen(
} }
// Theme Selector Section // Theme Selector Section
Card( OrganicCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { showThemePicker = true }, .clickable { showThemePicker = true }
shape = RoundedCornerShape(AppRadius.md), .naturalShadow()
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(AppSpacing.lg), .padding(OrganicSpacing.lg),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Column( Column(
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
) { ) {
Text( Text(
text = stringResource(Res.string.profile_appearance), text = stringResource(Res.string.profile_appearance),
@@ -258,24 +253,21 @@ fun ProfileScreen(
} }
// Notification Preferences Section // Notification Preferences Section
Card( OrganicCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { onNavigateToNotificationPreferences() }, .clickable { onNavigateToNotificationPreferences() }
shape = RoundedCornerShape(AppRadius.md), .naturalShadow()
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(AppSpacing.lg), .padding(OrganicSpacing.lg),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Column( Column(
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
) { ) {
Text( Text(
text = stringResource(Res.string.profile_notifications), text = stringResource(Res.string.profile_notifications),
@@ -298,26 +290,23 @@ fun ProfileScreen(
// Contact Support Section // Contact Support Section
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
Card( OrganicCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
uriHandler.openUri("mailto:caseraSupport@treymail.com?subject=Casera%20Support%20Request") uriHandler.openUri("mailto:caseraSupport@treymail.com?subject=Casera%20Support%20Request")
}, }
shape = RoundedCornerShape(AppRadius.md), .naturalShadow()
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(AppSpacing.lg), .padding(OrganicSpacing.lg),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Column( Column(
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
) { ) {
Text( Text(
text = stringResource(Res.string.profile_contact_support), text = stringResource(Res.string.profile_contact_support),
@@ -339,26 +328,23 @@ fun ProfileScreen(
} }
// Privacy Policy Section // Privacy Policy Section
Card( OrganicCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
uriHandler.openUri("https://mycrib.treytartt.com/privacy") uriHandler.openUri("https://mycrib.treytartt.com/privacy")
}, }
shape = RoundedCornerShape(AppRadius.md), .naturalShadow()
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(AppSpacing.lg), .padding(OrganicSpacing.lg),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Column( Column(
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
) { ) {
Text( Text(
text = stringResource(Res.string.profile_privacy), text = stringResource(Res.string.profile_privacy),
@@ -381,24 +367,22 @@ fun ProfileScreen(
// Subscription Section - Only show if limitations are enabled // Subscription Section - Only show if limitations are enabled
if (currentSubscription?.limitationsEnabled == true) { if (currentSubscription?.limitationsEnabled == true) {
Divider(modifier = Modifier.padding(vertical = AppSpacing.sm)) OrganicDivider(modifier = Modifier.padding(vertical = OrganicSpacing.sm))
Card( OrganicCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
shape = RoundedCornerShape(AppRadius.md), .fillMaxWidth()
colors = CardDefaults.cardColors( .naturalShadow()
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(AppSpacing.lg), .padding(OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(AppSpacing.md) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(AppSpacing.md) horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
Icon( Icon(
imageVector = Icons.Default.Star, imageVector = Icons.Default.Star,
@@ -407,7 +391,7 @@ fun ProfileScreen(
) )
Column( Column(
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
) { ) {
Text( Text(
text = if (SubscriptionHelper.currentTier == "pro") stringResource(Res.string.profile_pro_plan) else stringResource(Res.string.profile_free_plan), text = if (SubscriptionHelper.currentTier == "pro") stringResource(Res.string.profile_pro_plan) else stringResource(Res.string.profile_free_plan),
@@ -430,8 +414,8 @@ fun ProfileScreen(
if (SubscriptionHelper.currentTier != "pro") { if (SubscriptionHelper.currentTier != "pro") {
// Upgrade Benefits List // Upgrade Benefits List
Column( Column(
modifier = Modifier.padding(vertical = AppSpacing.sm), modifier = Modifier.padding(vertical = OrganicSpacing.sm),
verticalArrangement = Arrangement.spacedBy(AppSpacing.sm) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
) { ) {
Text( Text(
text = stringResource(Res.string.profile_upgrade_benefits_title), text = stringResource(Res.string.profile_upgrade_benefits_title),
@@ -483,7 +467,7 @@ fun ProfileScreen(
imageVector = Icons.Default.KeyboardArrowUp, imageVector = Icons.Default.KeyboardArrowUp,
contentDescription = null contentDescription = null
) )
Spacer(modifier = Modifier.width(AppSpacing.sm)) Spacer(modifier = Modifier.width(OrganicSpacing.sm))
Text(stringResource(Res.string.profile_upgrade_to_pro), fontWeight = FontWeight.SemiBold) Text(stringResource(Res.string.profile_upgrade_to_pro), fontWeight = FontWeight.SemiBold)
} }
} else { } else {
@@ -491,13 +475,13 @@ fun ProfileScreen(
text = stringResource(Res.string.profile_manage_subscription), text = stringResource(Res.string.profile_manage_subscription),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = AppSpacing.xs) modifier = Modifier.padding(top = OrganicSpacing.xs)
) )
} }
} }
} }
Divider(modifier = Modifier.padding(vertical = AppSpacing.sm)) OrganicDivider(modifier = Modifier.padding(vertical = OrganicSpacing.sm))
} }
Text( Text(
@@ -610,7 +594,7 @@ fun ProfileScreen(
} }
} }
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(OrganicSpacing.sm))
Button( Button(
onClick = { onClick = {
@@ -648,14 +632,14 @@ fun ProfileScreen(
} }
} }
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(OrganicSpacing.lg))
// App Version Section // App Version Section
HorizontalDivider(modifier = Modifier.padding(vertical = AppSpacing.md)) OrganicDivider(modifier = Modifier.padding(vertical = OrganicSpacing.md))
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
) { ) {
Text( Text(
text = stringResource(Res.string.profile_app_name), text = stringResource(Res.string.profile_app_name),
@@ -670,7 +654,8 @@ fun ProfileScreen(
) )
} }
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(OrganicSpacing.lg))
}
} }
} }
@@ -713,7 +698,7 @@ private fun UpgradeBenefitRow(
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm) horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
) { ) {
Icon( Icon(
imageVector = icon, imageVector = icon,

View File

@@ -17,6 +17,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.casera.ui.components.HandleErrors import com.example.casera.ui.components.HandleErrors
import com.example.casera.ui.components.auth.AuthHeader import com.example.casera.ui.components.auth.AuthHeader
import com.example.casera.ui.components.common.ErrorCard import com.example.casera.ui.components.common.ErrorCard
import com.example.casera.ui.theme.*
import com.example.casera.viewmodel.AuthViewModel import com.example.casera.viewmodel.AuthViewModel
import com.example.casera.network.ApiResult import com.example.casera.network.ApiResult
import com.example.casera.analytics.PostHogAnalytics import com.example.casera.analytics.PostHogAnalytics
@@ -65,6 +66,7 @@ fun RegisterScreen(
} }
} }
WarmGradientBackground {
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@@ -75,21 +77,22 @@ fun RegisterScreen(
} }
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f)
) )
) )
} },
containerColor = androidx.compose.ui.graphics.Color.Transparent
) { paddingValues -> ) { paddingValues ->
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(24.dp), .padding(OrganicSpacing.xl),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg)
) { ) {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(OrganicSpacing.sm))
AuthHeader( AuthHeader(
icon = Icons.Default.PersonAdd, icon = Icons.Default.PersonAdd,
@@ -97,8 +100,14 @@ fun RegisterScreen(
subtitle = stringResource(Res.string.auth_register_subtitle) subtitle = stringResource(Res.string.auth_register_subtitle)
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(OrganicSpacing.md))
OrganicCard(
modifier = Modifier.fillMaxWidth()
) {
Column(
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
OutlinedTextField( OutlinedTextField(
value = username, value = username,
onValueChange = { username = it }, onValueChange = { username = it },
@@ -123,6 +132,8 @@ fun RegisterScreen(
shape = RoundedCornerShape(12.dp) shape = RoundedCornerShape(12.dp)
) )
OrganicDivider()
OutlinedTextField( OutlinedTextField(
value = password, value = password,
onValueChange = { password = it }, onValueChange = { password = it },
@@ -148,13 +159,16 @@ fun RegisterScreen(
visualTransformation = PasswordVisualTransformation(), visualTransformation = PasswordVisualTransformation(),
shape = RoundedCornerShape(12.dp) shape = RoundedCornerShape(12.dp)
) )
}
}
ErrorCard(message = errorMessage) ErrorCard(message = errorMessage)
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(OrganicSpacing.sm))
val passwordsDontMatchMessage = stringResource(Res.string.auth_passwords_dont_match) val passwordsDontMatchMessage = stringResource(Res.string.auth_passwords_dont_match)
Button( OrganicPrimaryButton(
text = stringResource(Res.string.auth_register_button),
onClick = { onClick = {
when { when {
password != confirmPassword -> { password != confirmPassword -> {
@@ -167,29 +181,14 @@ fun RegisterScreen(
} }
} }
}, },
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.fillMaxWidth()
.height(56.dp),
enabled = username.isNotEmpty() && email.isNotEmpty() && enabled = username.isNotEmpty() && email.isNotEmpty() &&
password.isNotEmpty() && !isLoading, password.isNotEmpty() && !isLoading,
shape = RoundedCornerShape(12.dp) isLoading = isLoading
) {
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(16.dp)) Spacer(modifier = Modifier.height(OrganicSpacing.md))
}
} }
} }
} }

View File

@@ -1,8 +1,6 @@
package com.example.casera.ui.screens package com.example.casera.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material3.* 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.ui.components.common.ErrorCard
import com.example.casera.viewmodel.PasswordResetViewModel import com.example.casera.viewmodel.PasswordResetViewModel
import com.example.casera.network.ApiResult import com.example.casera.network.ApiResult
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.* import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -80,90 +79,116 @@ fun ResetPasswordScreen(
) )
} }
) { paddingValues -> ) { paddingValues ->
WarmGradientBackground {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.padding(paddingValues), .padding(paddingValues),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Card( OrganicCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth(0.9f) .fillMaxWidth(0.9f)
.wrapContentHeight(), .wrapContentHeight(),
shape = RoundedCornerShape(24.dp), showBlob = true,
colors = CardDefaults.cardColors( blobVariation = 2
containerColor = MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(32.dp), .padding(OrganicSpacing.spacious),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
) { ) {
if (isSuccess) { if (isSuccess) {
// Success State // Success State
AuthHeader( OrganicIconContainer(
icon = Icons.Default.CheckCircle, icon = Icons.Default.CheckCircle,
title = "Success!", size = 80.dp,
subtitle = "Your password has been reset successfully" iconScale = 0.5f,
backgroundColor = MaterialTheme.colorScheme.primary,
iconColor = MaterialTheme.colorScheme.onPrimary
) )
Card( Text(
modifier = Modifier.fillMaxWidth(), text = "Success!",
colors = CardDefaults.cardColors( style = MaterialTheme.typography.headlineMedium,
containerColor = MaterialTheme.colorScheme.primaryContainer fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.textPrimary,
textAlign = TextAlign.Center
) )
Text(
text = "Your password has been reset successfully",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.textSecondary,
textAlign = TextAlign.Center
)
OrganicCard(
modifier = Modifier.fillMaxWidth(),
accentColor = MaterialTheme.colorScheme.primary,
showBlob = false
) { ) {
Text( Text(
"You can now log in with your new password", "You can now log in with your new password",
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(OrganicSpacing.cozy),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer, color = MaterialTheme.colorScheme.textPrimary,
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
} }
Button( OrganicDivider(
onClick = onPasswordResetSuccess, modifier = Modifier.fillMaxWidth()
modifier = Modifier )
.fillMaxWidth()
.height(56.dp), OrganicPrimaryButton(
shape = RoundedCornerShape(12.dp) text = "Return to Login",
) { onClick = onPasswordResetSuccess
Text(
"Return to Login",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
) )
}
} else { } else {
// Reset Password Form // Reset Password Form
AuthHeader( OrganicIconContainer(
icon = Icons.Default.LockReset, icon = Icons.Default.LockReset,
title = "Set New Password", size = 80.dp,
subtitle = "Create a strong password to secure your account" 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 // Password Requirements
Card( OrganicCard(
colors = CardDefaults.cardColors( modifier = Modifier.fillMaxWidth(),
containerColor = MaterialTheme.colorScheme.secondaryContainer accentColor = MaterialTheme.colorScheme.secondary,
) showBlob = false
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(OrganicSpacing.cozy),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) { ) {
Text( Text(
"Password Requirements", "Password Requirements",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.textPrimary
) )
RequirementItem( RequirementItem(
@@ -206,7 +231,6 @@ fun ResetPasswordScreen(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true, singleLine = true,
visualTransformation = if (newPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(), visualTransformation = if (newPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(),
shape = RoundedCornerShape(12.dp),
enabled = !isLoading enabled = !isLoading
) )
@@ -231,41 +255,22 @@ fun ResetPasswordScreen(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true, singleLine = true,
visualTransformation = if (confirmPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(), visualTransformation = if (confirmPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(),
shape = RoundedCornerShape(12.dp),
enabled = !isLoading enabled = !isLoading
) )
ErrorCard(message = errorMessage) ErrorCard(message = errorMessage)
Button( OrganicDivider(
modifier = Modifier.fillMaxWidth()
)
OrganicPrimaryButton(
text = if (isLoggingIn) "Logging in..." else stringResource(Res.string.auth_reset_button),
onClick = { onClick = {
viewModel.resetPassword(newPassword, confirmPassword) viewModel.resetPassword(newPassword, confirmPassword)
}, },
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
enabled = isFormValid && !isLoading && !isLoggingIn, enabled = isFormValid && !isLoading && !isLoggingIn,
shape = RoundedCornerShape(12.dp) isLoading = isLoading || isLoggingIn
) {
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
) )
} }
} }
@@ -274,4 +279,3 @@ fun ResetPasswordScreen(
} }
} }
} }
}

View File

@@ -39,6 +39,7 @@ import com.example.casera.util.DateUtils
import com.example.casera.platform.rememberShareResidence import com.example.casera.platform.rememberShareResidence
import com.example.casera.analytics.PostHogAnalytics import com.example.casera.analytics.PostHogAnalytics
import com.example.casera.analytics.AnalyticsEvents import com.example.casera.analytics.AnalyticsEvents
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.* import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -539,6 +540,7 @@ fun ResidenceDetailScreen(
} }
} }
) { paddingValues -> ) { paddingValues ->
WarmGradientBackground {
ApiResultHandler( ApiResultHandler(
state = residenceState, state = residenceState,
onRetry = { onRetry = {
@@ -551,7 +553,7 @@ fun ResidenceDetailScreen(
loadingContent = { loadingContent = {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
) { ) {
CircularProgressIndicator() CircularProgressIndicator()
Text( Text(
@@ -566,23 +568,21 @@ fun ResidenceDetailScreen(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues), .padding(paddingValues),
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(OrganicSpacing.cozy),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
) { ) {
// Property Header Card // Property Header Card
item { item {
Card( OrganicCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors( accentColor = MaterialTheme.colorScheme.primary,
containerColor = MaterialTheme.colorScheme.primaryContainer showBlob = true,
), blobVariation = 0
shape = RoundedCornerShape(20.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(24.dp) .padding(OrganicSpacing.comfortable)
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -594,7 +594,7 @@ fun ResidenceDetailScreen(
text = residence.name, text = residence.name,
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onPrimaryContainer color = MaterialTheme.colorScheme.textPrimary
) )
} }
} }
@@ -607,21 +607,60 @@ fun ResidenceDetailScreen(
residence.stateProvince != null || residence.postalCode != null || residence.stateProvince != null || residence.postalCode != null ||
residence.country != null) { residence.country != null) {
item { item {
InfoCard( OrganicCard(
icon = Icons.Default.LocationOn, modifier = Modifier.fillMaxWidth(),
title = stringResource(Res.string.properties_address_section) showBlob = true,
blobVariation = 1
) { ) {
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) { if (residence.streetAddress != null) {
Text(text = residence.streetAddress) Text(
text = residence.streetAddress,
color = MaterialTheme.colorScheme.textSecondary
)
} }
if (residence.apartmentUnit != null) { if (residence.apartmentUnit != null) {
Text(text = "Unit: ${residence.apartmentUnit}") Text(
text = "Unit: ${residence.apartmentUnit}",
color = MaterialTheme.colorScheme.textSecondary
)
} }
if (residence.city != null || residence.stateProvince != null || residence.postalCode != null) { if (residence.city != null || residence.stateProvince != null || residence.postalCode != null) {
Text(text = "${residence.city ?: ""}, ${residence.stateProvince ?: ""} ${residence.postalCode ?: ""}") Text(
text = "${residence.city ?: ""}, ${residence.stateProvince ?: ""} ${residence.postalCode ?: ""}",
color = MaterialTheme.colorScheme.textSecondary
)
} }
if (residence.country != null) { if (residence.country != null) {
Text(text = residence.country) Text(
text = residence.country,
color = MaterialTheme.colorScheme.textSecondary
)
}
} }
} }
} }
@@ -631,10 +670,36 @@ fun ResidenceDetailScreen(
if (residence.bedrooms != null || residence.bathrooms != null || if (residence.bedrooms != null || residence.bathrooms != null ||
residence.squareFootage != null || residence.yearBuilt != null) { residence.squareFootage != null || residence.yearBuilt != null) {
item { item {
InfoCard( OrganicCard(
icon = Icons.Default.Info, modifier = Modifier.fillMaxWidth(),
title = stringResource(Res.string.properties_property_details_section) showBlob = true,
blobVariation = 2
) { ) {
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.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
)
}
OrganicDivider(horizontalPadding = OrganicSpacing.compact)
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly horizontalArrangement = Arrangement.SpaceEvenly
@@ -646,7 +711,7 @@ fun ResidenceDetailScreen(
PropertyDetailItem(Icons.Default.Bathroom, "$it", "Bathrooms") PropertyDetailItem(Icons.Default.Bathroom, "$it", "Bathrooms")
} }
} }
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(OrganicSpacing.compact))
residence.squareFootage?.let { residence.squareFootage?.let {
DetailRow(Icons.Default.SquareFoot, "Square Footage", "$it sq ft") DetailRow(Icons.Default.SquareFoot, "Square Footage", "$it sq ft")
} }
@@ -659,30 +724,84 @@ fun ResidenceDetailScreen(
} }
} }
} }
}
// Description Card // Description Card
if (residence.description != null && !residence.description.isEmpty()) { if (residence.description != null && !residence.description.isEmpty()) {
item { item {
InfoCard( OrganicCard(
icon = Icons.Default.Description, modifier = Modifier.fillMaxWidth(),
title = stringResource(Res.string.properties_description_section) showBlob = true,
blobVariation = 0
) { ) {
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(
text = residence.description, text = residence.description,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.textSecondary
) )
} }
} }
} }
}
// Purchase Information // Purchase Information
if (residence.purchaseDate != null || residence.purchasePrice != null) { if (residence.purchaseDate != null || residence.purchasePrice != null) {
item { item {
InfoCard( OrganicCard(
icon = Icons.Default.AttachMoney, modifier = Modifier.fillMaxWidth(),
title = stringResource(Res.string.properties_purchase_info) showBlob = true,
blobVariation = 1
) { ) {
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 { residence.purchaseDate?.let {
DetailRow(Icons.Default.Event, "Purchase Date", DateUtils.formatDateMedium(it)) DetailRow(Icons.Default.Event, "Purchase Date", DateUtils.formatDateMedium(it))
} }
@@ -692,27 +811,29 @@ fun ResidenceDetailScreen(
} }
} }
} }
}
// Tasks Section Header // Tasks Section Header
item { item {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 8.dp), .padding(vertical = OrganicSpacing.compact),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) { ) {
Icon( OrganicIconContainer(
Icons.Default.Assignment, icon = Icons.Default.Assignment,
contentDescription = null, size = 36.dp,
tint = MaterialTheme.colorScheme.primary, iconScale = 0.5f,
modifier = Modifier.size(28.dp) backgroundColor = MaterialTheme.colorScheme.primary,
iconColor = MaterialTheme.colorScheme.onPrimary
) )
Spacer(modifier = Modifier.width(8.dp))
Text( Text(
text = stringResource(Res.string.tasks_title), text = stringResource(Res.string.tasks_title),
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.textPrimary
) )
} }
} }
@@ -732,16 +853,15 @@ fun ResidenceDetailScreen(
} }
is ApiResult.Error -> { is ApiResult.Error -> {
item { item {
Card( OrganicCard(
colors = CardDefaults.cardColors( modifier = Modifier.fillMaxWidth(),
containerColor = MaterialTheme.colorScheme.errorContainer accentColor = MaterialTheme.colorScheme.error,
), showBlob = false
shape = RoundedCornerShape(12.dp)
) { ) {
Text( Text(
text = "Error loading tasks: ${com.example.casera.util.ErrorMessageParser.parse((tasksState as ApiResult.Error).message)}", text = "Error loading tasks: ${com.example.casera.util.ErrorMessageParser.parse((tasksState as ApiResult.Error).message)}",
color = MaterialTheme.colorScheme.onErrorContainer, color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(OrganicSpacing.cozy)
) )
} }
} }
@@ -751,32 +871,35 @@ fun ResidenceDetailScreen(
val allTasksEmpty = taskData.columns.all { it.tasks.isEmpty() } val allTasksEmpty = taskData.columns.all { it.tasks.isEmpty() }
if (allTasksEmpty) { if (allTasksEmpty) {
item { item {
Card( OrganicCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp) showBlob = true,
blobVariation = 2
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(32.dp), .padding(OrganicSpacing.spacious),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
) { ) {
Icon( OrganicIconContainer(
Icons.Default.Assignment, icon = Icons.Default.Assignment,
contentDescription = null, size = 64.dp,
modifier = Modifier.size(64.dp), iconScale = 0.5f,
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f) backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),
iconColor = MaterialTheme.colorScheme.primary
) )
Spacer(modifier = Modifier.height(16.dp))
Text( Text(
stringResource(Res.string.properties_no_tasks), stringResource(Res.string.properties_no_tasks),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.textPrimary
) )
Text( Text(
stringResource(Res.string.properties_add_task_start), stringResource(Res.string.properties_add_task_start),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.textSecondary
) )
} }
} }
@@ -833,25 +956,26 @@ fun ResidenceDetailScreen(
// Contractors Section Header // Contractors Section Header
item { item {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(OrganicSpacing.cozy))
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 8.dp), .padding(vertical = OrganicSpacing.compact),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) { ) {
Icon( OrganicIconContainer(
Icons.Default.People, icon = Icons.Default.People,
contentDescription = null, size = 36.dp,
tint = MaterialTheme.colorScheme.primary, iconScale = 0.5f,
modifier = Modifier.size(28.dp) backgroundColor = MaterialTheme.colorScheme.primary,
iconColor = MaterialTheme.colorScheme.onPrimary
) )
Spacer(modifier = Modifier.width(8.dp))
Text( Text(
text = stringResource(Res.string.contractors_title), text = stringResource(Res.string.contractors_title),
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.textPrimary
) )
} }
} }
@@ -871,16 +995,15 @@ fun ResidenceDetailScreen(
} }
is ApiResult.Error -> { is ApiResult.Error -> {
item { item {
Card( OrganicCard(
colors = CardDefaults.cardColors( modifier = Modifier.fillMaxWidth(),
containerColor = MaterialTheme.colorScheme.errorContainer accentColor = MaterialTheme.colorScheme.error,
), showBlob = false
shape = RoundedCornerShape(12.dp)
) { ) {
Text( Text(
text = "Error loading contractors: ${com.example.casera.util.ErrorMessageParser.parse((contractorsState as ApiResult.Error).message)}", text = "Error loading contractors: ${com.example.casera.util.ErrorMessageParser.parse((contractorsState as ApiResult.Error).message)}",
color = MaterialTheme.colorScheme.onErrorContainer, color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(OrganicSpacing.cozy)
) )
} }
} }
@@ -889,32 +1012,35 @@ fun ResidenceDetailScreen(
val contractors = (contractorsState as ApiResult.Success<List<ContractorSummary>>).data val contractors = (contractorsState as ApiResult.Success<List<ContractorSummary>>).data
if (contractors.isEmpty()) { if (contractors.isEmpty()) {
item { item {
Card( OrganicCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp) showBlob = true,
blobVariation = 1
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(24.dp), .padding(OrganicSpacing.comfortable),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) { ) {
Icon( OrganicIconContainer(
Icons.Default.PersonAdd, icon = Icons.Default.PersonAdd,
contentDescription = null, size = 56.dp,
modifier = Modifier.size(48.dp), iconScale = 0.5f,
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),
iconColor = MaterialTheme.colorScheme.primary
) )
Spacer(modifier = Modifier.height(12.dp))
Text( Text(
stringResource(Res.string.properties_no_contractors), stringResource(Res.string.properties_no_contractors),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.textPrimary
) )
Text( Text(
stringResource(Res.string.properties_add_contractors_hint), stringResource(Res.string.properties_add_contractors_hint),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.textSecondary
) )
} }
} }
@@ -940,3 +1066,4 @@ fun ResidenceDetailScreen(
} }
} }
} }
}

View File

@@ -26,6 +26,7 @@ import com.example.casera.network.ResidenceApi
import com.example.casera.storage.TokenStorage import com.example.casera.storage.TokenStorage
import com.example.casera.analytics.PostHogAnalytics import com.example.casera.analytics.PostHogAnalytics
import com.example.casera.analytics.AnalyticsEvents import com.example.casera.analytics.AnalyticsEvents
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.* import casera.composeapp.generated.resources.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -170,13 +171,14 @@ fun ResidenceFormScreen(
) )
} }
) { paddingValues -> ) { paddingValues ->
WarmGradientBackground {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
.padding(16.dp) .padding(OrganicSpacing.cozy)
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
) { ) {
// Basic Information section // Basic Information section
Text( Text(
@@ -279,7 +281,7 @@ fun ResidenceFormScreen(
) )
// Optional fields section // Optional fields section
Divider() OrganicDivider()
Text( Text(
text = stringResource(Res.string.properties_form_optional), text = stringResource(Res.string.properties_form_optional),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
@@ -288,7 +290,7 @@ fun ResidenceFormScreen(
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) { ) {
OutlinedTextField( OutlinedTextField(
value = bedrooms, value = bedrooms,
@@ -353,7 +355,7 @@ fun ResidenceFormScreen(
// Users section (edit mode only, owner only) // Users section (edit mode only, owner only)
if (isEditMode && isCurrentUserOwner) { if (isEditMode && isCurrentUserOwner) {
Divider() OrganicDivider()
Text( Text(
text = "Shared Users (${users.size})", text = "Shared Users (${users.size})",
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
@@ -362,7 +364,7 @@ fun ResidenceFormScreen(
if (isLoadingUsers) { if (isLoadingUsers) {
Box( Box(
modifier = Modifier.fillMaxWidth().padding(16.dp), modifier = Modifier.fillMaxWidth().padding(OrganicSpacing.cozy),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
CircularProgressIndicator(modifier = Modifier.size(24.dp)) CircularProgressIndicator(modifier = Modifier.size(24.dp))
@@ -372,7 +374,7 @@ fun ResidenceFormScreen(
text = "No shared users", text = "No shared users",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(vertical = 8.dp) modifier = Modifier.padding(vertical = OrganicSpacing.compact)
) )
} else { } else {
users.forEach { user -> users.forEach { user ->
@@ -404,7 +406,8 @@ fun ResidenceFormScreen(
} }
// Submit button // Submit button
Button( OrganicPrimaryButton(
text = if (isEditMode) stringResource(Res.string.properties_form_update) else stringResource(Res.string.properties_form_create),
onClick = { onClick = {
if (validateForm()) { if (validateForm()) {
val request = ResidenceCreateRequest( val request = ResidenceCreateRequest(
@@ -432,20 +435,12 @@ fun ResidenceFormScreen(
} }
} }
}, },
modifier = Modifier.fillMaxWidth(), enabled = validateForm(),
enabled = validateForm() isLoading = operationState is ApiResult.Loading
) {
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)) Spacer(modifier = Modifier.height(OrganicSpacing.cozy))
}
} }
} }
@@ -508,8 +503,9 @@ private fun UserListItem(
user: ResidenceUser, user: ResidenceUser,
onRemove: () -> Unit onRemove: () -> Unit
) { ) {
Card( OrganicCard(
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp) modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
showBlob = false
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth().padding(12.dp), modifier = Modifier.fillMaxWidth().padding(12.dp),

View File

@@ -37,6 +37,7 @@ import com.example.casera.cache.SubscriptionCache
import com.example.casera.analytics.PostHogAnalytics import com.example.casera.analytics.PostHogAnalytics
import com.example.casera.analytics.AnalyticsEvents import com.example.casera.analytics.AnalyticsEvents
import com.example.casera.data.DataManager import com.example.casera.data.DataManager
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.* import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -225,14 +226,14 @@ fun ResidencesScreen(
) { ) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy),
modifier = Modifier.padding(24.dp) modifier = Modifier.padding(OrganicSpacing.comfortable)
) { ) {
Icon( OrganicIconContainer(
Icons.Default.Home, icon = Icons.Default.Home,
contentDescription = null, size = 80.dp,
modifier = Modifier.size(80.dp), iconScale = 0.6f,
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f) backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
) )
Text( Text(
stringResource(Res.string.properties_empty_title), stringResource(Res.string.properties_empty_title),
@@ -244,7 +245,7 @@ fun ResidencesScreen(
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant 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) // Only show Add Property button if not blocked (limit>0)
if (!isBlocked.allowed) { if (!isBlocked.allowed) {
Button( Button(
@@ -263,7 +264,7 @@ fun ResidencesScreen(
shape = RoundedCornerShape(12.dp) shape = RoundedCornerShape(12.dp)
) { ) {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon(Icons.Default.Add, contentDescription = null) 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( OutlinedButton(
onClick = { onClick = {
val (allowed, triggerKey) = canAddProperty() val (allowed, triggerKey) = canAddProperty()
@@ -291,7 +292,7 @@ fun ResidencesScreen(
shape = RoundedCornerShape(12.dp) shape = RoundedCornerShape(12.dp)
) { ) {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon(Icons.Default.GroupAdd, contentDescription = null) Icon(Icons.Default.GroupAdd, contentDescription = null)
@@ -315,7 +316,7 @@ fun ResidencesScreen(
shape = RoundedCornerShape(12.dp) shape = RoundedCornerShape(12.dp)
) { ) {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon(Icons.Default.Star, contentDescription = null) Icon(Icons.Default.Star, contentDescription = null)
@@ -344,28 +345,27 @@ fun ResidencesScreen(
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues( contentPadding = PaddingValues(
start = 16.dp, start = OrganicSpacing.cozy,
end = 16.dp, end = OrganicSpacing.cozy,
top = 16.dp, top = OrganicSpacing.cozy,
bottom = 96.dp bottom = 96.dp
), ),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
) { ) {
// Summary Card // Summary Card
item { item {
Card( OrganicCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors( accentColor = MaterialTheme.colorScheme.primary,
containerColor = MaterialTheme.colorScheme.primaryContainer showBlob = true,
), blobVariation = 0,
shape = RoundedCornerShape(20.dp), shadowIntensity = ShadowIntensity.Medium
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(20.dp), .padding(OrganicSpacing.cozy),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@@ -373,15 +373,15 @@ fun ResidencesScreen(
Icon( Icon(
Icons.Default.Dashboard, Icons.Default.Dashboard,
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimaryContainer, tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp) modifier = Modifier.size(24.dp)
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(OrganicSpacing.compact))
Text( Text(
text = stringResource(Res.string.home_overview), text = stringResource(Res.string.home_overview),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onPrimaryContainer color = MaterialTheme.colorScheme.textPrimary
) )
} }
@@ -401,8 +401,8 @@ fun ResidencesScreen(
) )
} }
HorizontalDivider( OrganicDivider(
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.2f) color = MaterialTheme.colorScheme.textSecondary.copy(alpha = 0.2f)
) )
Row( Row(
@@ -436,7 +436,7 @@ fun ResidencesScreen(
text = stringResource(Res.string.home_your_properties), text = stringResource(Res.string.home_your_properties),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier.padding(top = 8.dp) modifier = Modifier.padding(top = OrganicSpacing.compact)
) )
} }
@@ -456,46 +456,41 @@ fun ResidencesScreen(
label = "pulseScale" label = "pulseScale"
) )
Card( OrganicCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { onResidenceClick(residence.id) }, .clickable { onResidenceClick(residence.id) },
shape = MaterialTheme.shapes.large, accentColor = if (hasOverdue) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), showBlob = true,
colors = CardDefaults.cardColors( blobVariation = residence.id % 3,
containerColor = MaterialTheme.colorScheme.surface shadowIntensity = ShadowIntensity.Subtle
)
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(20.dp) .padding(OrganicSpacing.cozy)
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// Pulsing circular house icon when overdue // Pulsing organic icon container when overdue
Box( Box(
modifier = Modifier modifier = if (hasOverdue) Modifier.scale(pulseScale) else 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
) { ) {
Icon( OrganicIconContainer(
Icons.Default.Home, icon = Icons.Default.Home,
contentDescription = null, size = 56.dp,
tint = if (hasOverdue) MaterialTheme.colorScheme.onErrorContainer iconScale = 0.5f,
else MaterialTheme.colorScheme.onPrimaryContainer, backgroundColor = if (hasOverdue)
modifier = Modifier.size(28.dp) 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)) Spacer(modifier = Modifier.height(OrganicSpacing.cozy))
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) OrganicDivider(color = MaterialTheme.colorScheme.textSecondary.copy(alpha = 0.15f))
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(OrganicSpacing.cozy))
// Fully dynamic task summary from API - show first 3 categories // Fully dynamic task summary from API - show first 3 categories
val displayCategories = residence.taskSummary.categories.take(3) val displayCategories = residence.taskSummary.categories.take(3)

View File

@@ -1,5 +1,6 @@
package com.example.casera.ui.screens package com.example.casera.ui.screens
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items 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.network.ApiResult
import com.example.casera.analytics.PostHogAnalytics import com.example.casera.analytics.PostHogAnalytics
import com.example.casera.analytics.AnalyticsEvents import com.example.casera.analytics.AnalyticsEvents
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.* import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -66,7 +68,9 @@ fun TasksScreen(
} }
} }
WarmGradientBackground {
Scaffold( Scaffold(
containerColor = androidx.compose.ui.graphics.Color.Transparent,
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { Text(stringResource(Res.string.tasks_title)) }, title = { Text(stringResource(Res.string.tasks_title)) },
@@ -74,7 +78,10 @@ fun TasksScreen(
IconButton(onClick = onNavigateBack) { IconButton(onClick = onNavigateBack) {
Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back)) 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 // No FAB on Tasks screen - tasks are added from within residences
@@ -87,7 +94,9 @@ fun TasksScreen(
.padding(paddingValues), .padding(paddingValues),
contentAlignment = androidx.compose.ui.Alignment.Center contentAlignment = androidx.compose.ui.Alignment.Center
) { ) {
CircularProgressIndicator() CircularProgressIndicator(
color = MaterialTheme.colorScheme.primary
)
} }
} }
is ApiResult.Success -> { is ApiResult.Success -> {
@@ -103,24 +112,26 @@ fun TasksScreen(
) { ) {
Column( Column(
horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally, horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy),
modifier = Modifier.padding(24.dp) modifier = Modifier.padding(OrganicSpacing.comfortable)
) { ) {
Icon( OrganicIconContainer(
Icons.Default.Assignment, icon = Icons.Default.Assignment,
contentDescription = null, size = 80.dp,
modifier = Modifier.size(80.dp), iconScale = 0.6f,
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f) backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f),
iconColor = MaterialTheme.colorScheme.primary
) )
Text( Text(
stringResource(Res.string.tasks_empty_title), stringResource(Res.string.tasks_empty_title),
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold,
color = MaterialTheme.colorScheme.textPrimary
) )
Text( Text(
stringResource(Res.string.tasks_empty_subtitle), stringResource(Res.string.tasks_empty_subtitle),
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.textSecondary
) )
} }
} }
@@ -129,18 +140,18 @@ fun TasksScreen(
modifier = Modifier modifier = Modifier
.fillMaxSize(), .fillMaxSize(),
contentPadding = PaddingValues( contentPadding = PaddingValues(
top = paddingValues.calculateTopPadding() + 16.dp, top = paddingValues.calculateTopPadding() + OrganicSpacing.cozy,
bottom = paddingValues.calculateBottomPadding() + 16.dp, bottom = paddingValues.calculateBottomPadding() + OrganicSpacing.cozy,
start = 16.dp, start = OrganicSpacing.cozy,
end = 16.dp end = OrganicSpacing.cozy
), ),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) { ) {
// Task summary pills - dynamically generated from all columns // Task summary pills - dynamically generated from all columns
item { item {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) { ) {
taskData.columns.forEach { column -> taskData.columns.forEach { column ->
TaskPill( TaskPill(
@@ -162,7 +173,8 @@ fun TasksScreen(
Text( Text(
text = "${column.displayName} (${column.tasks.size})", text = "${column.displayName} (${column.tasks.size})",
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(top = 8.dp) color = MaterialTheme.colorScheme.textPrimary,
modifier = Modifier.padding(top = OrganicSpacing.compact)
) )
} }
@@ -183,40 +195,47 @@ fun TasksScreen(
val isExpanded = expandedColumns.contains(column.name) val isExpanded = expandedColumns.contains(column.name)
item { item {
Card( OrganicCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
onClick = { .fillMaxWidth()
.clickable {
expandedColumns = if (isExpanded) { expandedColumns = if (isExpanded) {
expandedColumns - column.name expandedColumns - column.name
} else { } else {
expandedColumns + column.name expandedColumns + column.name
} }
} },
showBlob = false,
shadowIntensity = ShadowIntensity.Subtle
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(OrganicSpacing.cozy),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
) { ) {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact),
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
) { ) {
Icon( OrganicIconContainer(
getIconFromName(column.icons["android"] ?: "List"), icon = getIconFromName(column.icons["android"] ?: "List"),
contentDescription = null, size = 40.dp,
tint = hexToColor(column.color) iconScale = 0.5f,
backgroundColor = hexToColor(column.color).copy(alpha = 0.2f),
iconColor = hexToColor(column.color)
) )
Text( Text(
text = "${column.displayName} (${column.tasks.size})", text = "${column.displayName} (${column.tasks.size})",
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.textPrimary
) )
} }
Icon( Icon(
if (isExpanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown, if (isExpanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
contentDescription = if (isExpanded) "Collapse" else "Expand" contentDescription = if (isExpanded) "Collapse" else "Expand",
tint = MaterialTheme.colorScheme.textSecondary
) )
} }
} }
@@ -246,6 +265,7 @@ fun TasksScreen(
else -> {} else -> {}
} }
} }
}
if (showCompleteDialog && selectedTask != null) { if (showCompleteDialog && selectedTask != null) {
CompleteTaskDialog( CompleteTaskDialog(

View File

@@ -2,7 +2,6 @@ package com.example.casera.ui.screens
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons 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.ui.components.common.ErrorCard
import com.example.casera.viewmodel.AuthViewModel import com.example.casera.viewmodel.AuthViewModel
import com.example.casera.network.ApiResult import com.example.casera.network.ApiResult
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.* import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -89,46 +89,62 @@ fun VerifyEmailScreen(
) )
} }
) { paddingValues -> ) { paddingValues ->
WarmGradientBackground {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(24.dp), .padding(OrganicSpacing.comfortable),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
) { ) {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(OrganicSpacing.compact))
AuthHeader( OrganicIconContainer(
icon = Icons.Default.MarkEmailRead, icon = Icons.Default.MarkEmailRead,
title = stringResource(Res.string.auth_verify_title), size = 80.dp,
subtitle = stringResource(Res.string.auth_verify_subtitle) iconScale = 0.5f,
backgroundColor = MaterialTheme.colorScheme.primary,
iconColor = MaterialTheme.colorScheme.onPrimary
) )
Spacer(modifier = Modifier.height(16.dp)) Text(
text = stringResource(Res.string.auth_verify_title),
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.textPrimary,
textAlign = TextAlign.Center
)
Card( 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(), modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors( accentColor = MaterialTheme.colorScheme.error,
containerColor = MaterialTheme.colorScheme.errorContainer showBlob = false
),
shape = RoundedCornerShape(12.dp)
) { ) {
Column( Column(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(OrganicSpacing.cozy),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) { ) {
Icon( Icon(
Icons.Default.Info, Icons.Default.Info,
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.onErrorContainer tint = MaterialTheme.colorScheme.error
) )
Text( Text(
text = "Email verification is required. Check your inbox for a 6-digit code.", text = "Email verification is required. Check your inbox for a 6-digit code.",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onErrorContainer, color = MaterialTheme.colorScheme.textPrimary,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
@@ -148,7 +164,6 @@ fun VerifyEmailScreen(
}, },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true, singleLine = true,
shape = RoundedCornerShape(12.dp),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
placeholder = { Text("000000") } placeholder = { Text("000000") }
) )
@@ -159,7 +174,12 @@ fun VerifyEmailScreen(
) )
} }
Button( OrganicDivider(
modifier = Modifier.fillMaxWidth()
)
OrganicPrimaryButton(
text = stringResource(Res.string.auth_verify_button),
onClick = { onClick = {
if (code.length == 6) { if (code.length == 6) {
isLoading = true isLoading = true
@@ -168,40 +188,19 @@ fun VerifyEmailScreen(
errorMessage = "Please enter a valid 6-digit code" errorMessage = "Please enter a valid 6-digit code"
} }
}, },
modifier = Modifier enabled = !isLoading && code.length == 6,
.fillMaxWidth() isLoading = isLoading
.height(56.dp),
shape = RoundedCornerShape(12.dp),
enabled = !isLoading && code.length == 6
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary
) )
} else {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Default.CheckCircle, contentDescription = null)
Text(
stringResource(Res.string.auth_verify_button),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
}
}
}
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(OrganicSpacing.compact))
Text( Text(
text = "Didn't receive the code? Check your spam folder or contact support.", text = "Didn't receive the code? Check your spam folder or contact support.",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.textSecondary,
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
} }
} }
} }
}

View File

@@ -1,8 +1,6 @@
package com.example.casera.ui.screens package com.example.casera.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* 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.ui.components.common.ErrorCard
import com.example.casera.viewmodel.PasswordResetViewModel import com.example.casera.viewmodel.PasswordResetViewModel
import com.example.casera.network.ApiResult import com.example.casera.network.ApiResult
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.* import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -70,69 +69,85 @@ fun VerifyResetCodeScreen(
) )
} }
) { paddingValues -> ) { paddingValues ->
WarmGradientBackground {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.padding(paddingValues), .padding(paddingValues),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Card( OrganicCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth(0.9f) .fillMaxWidth(0.9f)
.wrapContentHeight(), .wrapContentHeight(),
shape = RoundedCornerShape(24.dp), showBlob = true,
colors = CardDefaults.cardColors( blobVariation = 1
containerColor = MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(32.dp), .padding(OrganicSpacing.spacious),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
) { ) {
AuthHeader( OrganicIconContainer(
icon = Icons.Default.MarkEmailRead, icon = Icons.Default.MarkEmailRead,
title = "Check Your Email", size = 80.dp,
subtitle = "We sent a 6-digit code to" iconScale = 0.5f,
backgroundColor = MaterialTheme.colorScheme.primary,
iconColor = MaterialTheme.colorScheme.onPrimary
)
Text(
text = "Check Your Email",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.textPrimary,
textAlign = TextAlign.Center
)
Text(
text = "We sent a 6-digit code to",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.textSecondary,
textAlign = TextAlign.Center
) )
Text( Text(
email, email,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.textPrimary,
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
Card( OrganicCard(
colors = CardDefaults.cardColors( modifier = Modifier.fillMaxWidth(),
containerColor = MaterialTheme.colorScheme.secondaryContainer accentColor = MaterialTheme.colorScheme.secondary,
) showBlob = false
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(OrganicSpacing.cozy),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) { ) {
Icon( Icon(
Icons.Default.Timer, Icons.Default.Timer,
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.secondary tint = MaterialTheme.colorScheme.secondary
) )
Spacer(modifier = Modifier.width(12.dp))
Text( Text(
"Code expires in 15 minutes", "Code expires in 15 minutes",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.textPrimary
) )
} }
} }
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(OrganicSpacing.compact))
OutlinedTextField( OutlinedTextField(
value = code, value = code,
@@ -147,7 +162,6 @@ fun VerifyResetCodeScreen(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true, singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
shape = RoundedCornerShape(12.dp),
enabled = !isLoading, enabled = !isLoading,
textStyle = MaterialTheme.typography.headlineMedium.copy( textStyle = MaterialTheme.typography.headlineMedium.copy(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
@@ -158,75 +172,60 @@ fun VerifyResetCodeScreen(
Text( Text(
"Enter the 6-digit code from your email", "Enter the 6-digit code from your email",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.textSecondary,
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
ErrorCard(message = errorMessage) ErrorCard(message = errorMessage)
if (isSuccess) { if (isSuccess) {
Card( OrganicCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors( accentColor = MaterialTheme.colorScheme.primary,
containerColor = MaterialTheme.colorScheme.primaryContainer showBlob = false
)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(OrganicSpacing.cozy),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) { ) {
Icon( Icon(
Icons.Default.CheckCircle, Icons.Default.CheckCircle,
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.primary
) )
Spacer(modifier = Modifier.width(12.dp))
Text( Text(
"Code verified! Now set your new password", "Code verified! Now set your new password",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer color = MaterialTheme.colorScheme.textPrimary
) )
} }
} }
} }
Button( OrganicDivider(
modifier = Modifier.fillMaxWidth()
)
OrganicPrimaryButton(
text = stringResource(Res.string.auth_verify_button),
onClick = { onClick = {
viewModel.verifyResetCode(email, code) viewModel.verifyResetCode(email, code)
}, },
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
enabled = code.length == 6 && !isLoading, enabled = code.length == 6 && !isLoading,
shape = RoundedCornerShape(12.dp) isLoading = isLoading
) {
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
)
}
}
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) { ) {
Text( Text(
"Didn't receive the code?", "Didn't receive the code?",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.textSecondary
) )
TextButton(onClick = { TextButton(onClick = {
@@ -245,7 +244,7 @@ fun VerifyResetCodeScreen(
Text( Text(
"Check your spam folder if you don't see it", "Check your spam folder if you don't see it",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.textSecondary,
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
} }
@@ -254,3 +253,4 @@ fun VerifyResetCodeScreen(
} }
} }
} }
}

View File

@@ -5,10 +5,8 @@ import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -17,15 +15,12 @@ import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.font.FontWeight
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.example.casera.network.ApiResult import com.example.casera.network.ApiResult
import com.example.casera.ui.theme.AppRadius import com.example.casera.ui.theme.*
import com.example.casera.ui.theme.AppSpacing
import com.example.casera.viewmodel.OnboardingViewModel import com.example.casera.viewmodel.OnboardingViewModel
import casera.composeapp.generated.resources.* import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -65,35 +60,30 @@ fun OnboardingCreateAccountContent(
password.isNotBlank() && password.isNotBlank() &&
password == confirmPassword password == confirmPassword
WarmGradientBackground(
modifier = Modifier.fillMaxSize()
) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(horizontal = AppSpacing.xl) .padding(horizontal = OrganicSpacing.xl)
) { ) {
Spacer(modifier = Modifier.height(AppSpacing.xl)) Spacer(modifier = Modifier.height(OrganicSpacing.xl))
// Header // Header
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.sm) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
) { ) {
// Icon // Icon
Box( OrganicIconContainer(
modifier = Modifier icon = Icons.Default.PersonAdd,
.size(80.dp) size = 80.dp,
.clip(CircleShape) iconSize = 40.dp,
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)), contentDescription = null
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.PersonAdd,
contentDescription = null,
modifier = Modifier.size(40.dp),
tint = MaterialTheme.colorScheme.primary
) )
}
Text( Text(
text = stringResource(Res.string.onboarding_create_account_title), text = stringResource(Res.string.onboarding_create_account_title),
@@ -111,7 +101,7 @@ fun OnboardingCreateAccountContent(
) )
} }
Spacer(modifier = Modifier.height(AppSpacing.xl * 2)) Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
// Create with Email section // Create with Email section
if (!isFormExpanded) { if (!isFormExpanded) {
@@ -121,14 +111,14 @@ fun OnboardingCreateAccountContent(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(56.dp), .height(56.dp),
shape = RoundedCornerShape(AppRadius.md), shape = RoundedCornerShape(OrganicRadius.md),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f), containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
contentColor = MaterialTheme.colorScheme.primary contentColor = MaterialTheme.colorScheme.primary
) )
) { ) {
Icon(Icons.Default.Email, contentDescription = null) Icon(Icons.Default.Email, contentDescription = null)
Spacer(modifier = Modifier.width(AppSpacing.sm)) Spacer(modifier = Modifier.width(OrganicSpacing.sm))
Text( Text(
text = stringResource(Res.string.onboarding_create_with_email), text = stringResource(Res.string.onboarding_create_with_email),
fontWeight = FontWeight.Medium fontWeight = FontWeight.Medium
@@ -143,7 +133,7 @@ fun OnboardingCreateAccountContent(
exit = fadeOut() + shrinkVertically() exit = fadeOut() + shrinkVertically()
) { ) {
Column( Column(
verticalArrangement = Arrangement.spacedBy(AppSpacing.md) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
// Username // Username
OutlinedTextField( OutlinedTextField(
@@ -158,7 +148,7 @@ fun OnboardingCreateAccountContent(
}, },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true, singleLine = true,
shape = RoundedCornerShape(AppRadius.md), shape = RoundedCornerShape(OrganicRadius.md),
enabled = !isLoading enabled = !isLoading
) )
@@ -175,7 +165,7 @@ fun OnboardingCreateAccountContent(
}, },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true, singleLine = true,
shape = RoundedCornerShape(AppRadius.md), shape = RoundedCornerShape(OrganicRadius.md),
enabled = !isLoading enabled = !isLoading
) )
@@ -193,7 +183,7 @@ fun OnboardingCreateAccountContent(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true, singleLine = true,
visualTransformation = PasswordVisualTransformation(), visualTransformation = PasswordVisualTransformation(),
shape = RoundedCornerShape(AppRadius.md), shape = RoundedCornerShape(OrganicRadius.md),
enabled = !isLoading enabled = !isLoading
) )
@@ -211,7 +201,7 @@ fun OnboardingCreateAccountContent(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true, singleLine = true,
visualTransformation = PasswordVisualTransformation(), visualTransformation = PasswordVisualTransformation(),
shape = RoundedCornerShape(AppRadius.md), shape = RoundedCornerShape(OrganicRadius.md),
enabled = !isLoading, enabled = !isLoading,
isError = confirmPassword.isNotEmpty() && password != confirmPassword, isError = confirmPassword.isNotEmpty() && password != confirmPassword,
supportingText = if (confirmPassword.isNotEmpty() && password != confirmPassword) { supportingText = if (confirmPassword.isNotEmpty() && password != confirmPassword) {
@@ -221,16 +211,14 @@ fun OnboardingCreateAccountContent(
// Error message // Error message
if (localErrorMessage != null) { if (localErrorMessage != null) {
Card( OrganicCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors( accentColor = MaterialTheme.colorScheme.error,
containerColor = MaterialTheme.colorScheme.errorContainer showBlob = false
),
shape = RoundedCornerShape(AppRadius.md)
) { ) {
Row( Row(
modifier = Modifier.padding(AppSpacing.md), modifier = Modifier.padding(OrganicSpacing.md),
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm), horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
@@ -247,10 +235,11 @@ fun OnboardingCreateAccountContent(
} }
} }
Spacer(modifier = Modifier.height(AppSpacing.sm)) Spacer(modifier = Modifier.height(OrganicSpacing.sm))
// Create Account button // Create Account button
Button( OrganicPrimaryButton(
text = stringResource(Res.string.auth_register_button),
onClick = { onClick = {
if (password == confirmPassword) { if (password == confirmPassword) {
viewModel.register(username, email, password) viewModel.register(username, email, password)
@@ -258,30 +247,14 @@ fun OnboardingCreateAccountContent(
localErrorMessage = "Passwords don't match" localErrorMessage = "Passwords don't match"
} }
}, },
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.fillMaxWidth() enabled = isFormValid && !isLoading,
.height(56.dp), isLoading = isLoading
shape = RoundedCornerShape(AppRadius.md),
enabled = isFormValid && !isLoading
) {
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
)
}
}
} }
} }
Spacer(modifier = Modifier.height(AppSpacing.xl)) Spacer(modifier = Modifier.height(OrganicSpacing.xl))
// Already have an account // Already have an account
Row( Row(
@@ -303,7 +276,8 @@ fun OnboardingCreateAccountContent(
} }
} }
Spacer(modifier = Modifier.height(AppSpacing.xl * 2)) Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
}
} }
// Login dialog // Login dialog
@@ -356,7 +330,7 @@ private fun OnboardingLoginDialog(
}, },
text = { text = {
Column( Column(
verticalArrangement = Arrangement.spacedBy(AppSpacing.md) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
OutlinedTextField( OutlinedTextField(
value = username, value = username,
@@ -364,7 +338,7 @@ private fun OnboardingLoginDialog(
label = { Text(stringResource(Res.string.auth_login_username_label)) }, label = { Text(stringResource(Res.string.auth_login_username_label)) },
singleLine = true, singleLine = true,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(AppRadius.md), shape = RoundedCornerShape(OrganicRadius.md),
enabled = !isLoading enabled = !isLoading
) )
@@ -374,7 +348,7 @@ private fun OnboardingLoginDialog(
label = { Text(stringResource(Res.string.auth_login_password_label)) }, label = { Text(stringResource(Res.string.auth_login_password_label)) },
singleLine = true, singleLine = true,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(AppRadius.md), shape = RoundedCornerShape(OrganicRadius.md),
visualTransformation = PasswordVisualTransformation(), visualTransformation = PasswordVisualTransformation(),
enabled = !isLoading enabled = !isLoading
) )

View File

@@ -24,8 +24,7 @@ import androidx.compose.ui.unit.dp
import com.example.casera.data.DataManager import com.example.casera.data.DataManager
import com.example.casera.models.TaskCreateRequest import com.example.casera.models.TaskCreateRequest
import com.example.casera.network.ApiResult import com.example.casera.network.ApiResult
import com.example.casera.ui.theme.AppRadius import com.example.casera.ui.theme.*
import com.example.casera.ui.theme.AppSpacing
import com.example.casera.viewmodel.OnboardingViewModel import com.example.casera.viewmodel.OnboardingViewModel
import casera.composeapp.generated.resources.* import casera.composeapp.generated.resources.*
import com.example.casera.util.DateUtils import com.example.casera.util.DateUtils
@@ -163,7 +162,7 @@ fun OnboardingFirstTaskContent(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(1f), .weight(1f),
contentPadding = PaddingValues(horizontal = AppSpacing.lg, vertical = AppSpacing.md) contentPadding = PaddingValues(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.md)
) { ) {
// Header // Header
item { item {
@@ -171,30 +170,19 @@ fun OnboardingFirstTaskContent(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
// Celebration icon // Celebration icon using OrganicIconContainer
Box( OrganicIconContainer(
modifier = Modifier icon = Icons.Default.Celebration,
.size(80.dp) size = 80.dp,
.clip(CircleShape) iconSize = 40.dp,
.background( gradientColors = listOf(
Brush.linearGradient(
colors = listOf(
MaterialTheme.colorScheme.primary, MaterialTheme.colorScheme.primary,
MaterialTheme.colorScheme.secondary MaterialTheme.colorScheme.secondary
)
)
), ),
contentAlignment = Alignment.Center contentDescription = null
) {
Icon(
imageVector = Icons.Default.Celebration,
contentDescription = null,
modifier = Modifier.size(40.dp),
tint = Color.White
) )
}
Spacer(modifier = Modifier.height(AppSpacing.lg)) Spacer(modifier = Modifier.height(OrganicSpacing.lg))
Text( Text(
text = stringResource(Res.string.onboarding_tasks_title), text = stringResource(Res.string.onboarding_tasks_title),
@@ -203,7 +191,7 @@ fun OnboardingFirstTaskContent(
color = MaterialTheme.colorScheme.onBackground color = MaterialTheme.colorScheme.onBackground
) )
Spacer(modifier = Modifier.height(AppSpacing.sm)) Spacer(modifier = Modifier.height(OrganicSpacing.sm))
Text( Text(
text = stringResource(Res.string.onboarding_tasks_subtitle), text = stringResource(Res.string.onboarding_tasks_subtitle),
@@ -212,11 +200,11 @@ fun OnboardingFirstTaskContent(
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
Spacer(modifier = Modifier.height(AppSpacing.lg)) Spacer(modifier = Modifier.height(OrganicSpacing.lg))
// Selection counter // Selection counter
Surface( Surface(
shape = RoundedCornerShape(AppRadius.xl), shape = RoundedCornerShape(OrganicRadius.xl),
color = if (isAtMaxSelection) { color = if (isAtMaxSelection) {
MaterialTheme.colorScheme.tertiary.copy(alpha = 0.1f) MaterialTheme.colorScheme.tertiary.copy(alpha = 0.1f)
} else { } else {
@@ -224,8 +212,8 @@ fun OnboardingFirstTaskContent(
} }
) { ) {
Row( Row(
modifier = Modifier.padding(horizontal = AppSpacing.lg, vertical = AppSpacing.sm), modifier = Modifier.padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.sm),
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm), horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( 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 // Add popular tasks button
@@ -291,7 +279,7 @@ fun OnboardingFirstTaskContent(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(56.dp), .height(56.dp),
shape = RoundedCornerShape(AppRadius.lg), shape = RoundedCornerShape(OrganicRadius.lg),
border = ButtonDefaults.outlinedButtonBorder.copy( border = ButtonDefaults.outlinedButtonBorder.copy(
brush = Brush.linearGradient( brush = Brush.linearGradient(
colors = listOf( colors = listOf(
@@ -302,7 +290,7 @@ fun OnboardingFirstTaskContent(
) )
) { ) {
Icon(Icons.Default.AutoAwesome, contentDescription = null) Icon(Icons.Default.AutoAwesome, contentDescription = null)
Spacer(modifier = Modifier.width(AppSpacing.sm)) Spacer(modifier = Modifier.width(OrganicSpacing.sm))
Text( Text(
text = stringResource(Res.string.onboarding_tasks_add_popular), text = stringResource(Res.string.onboarding_tasks_add_popular),
fontWeight = FontWeight.Medium fontWeight = FontWeight.Medium
@@ -318,9 +306,14 @@ fun OnboardingFirstTaskContent(
shadowElevation = 8.dp shadowElevation = 8.dp
) { ) {
Column( 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 = { onClick = {
if (selectedTaskIds.isEmpty()) { if (selectedTaskIds.isEmpty()) {
onTasksAdded() onTasksAdded()
@@ -360,32 +353,11 @@ fun OnboardingFirstTaskContent(
} }
} }
}, },
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.fillMaxWidth() enabled = !isCreatingTasks,
.height(56.dp), isLoading = isCreatingTasks,
shape = RoundedCornerShape(AppRadius.lg), icon = Icons.Default.ArrowForward
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)
}
}
} }
} }
} }
@@ -402,39 +374,32 @@ private fun TaskCategorySection(
) { ) {
val selectedInCategory = category.tasks.count { it.id in selectedTaskIds } val selectedInCategory = category.tasks.count { it.id in selectedTaskIds }
OrganicCard(
modifier = Modifier.fillMaxWidth(),
accentColor = category.color,
showBlob = false
) {
Column( Column(
modifier = Modifier modifier = Modifier.fillMaxWidth()
.fillMaxWidth()
.clip(RoundedCornerShape(AppRadius.lg))
) { ) {
// Header // Header
Surface( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { onToggleExpand() }, .clickable { onToggleExpand() }
color = MaterialTheme.colorScheme.surfaceVariant .padding(OrganicSpacing.md),
) {
Row(
modifier = Modifier.padding(AppSpacing.md),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// Category icon // Category icon
Box( OrganicIconContainer(
modifier = Modifier icon = category.icon,
.size(44.dp) size = 44.dp,
.clip(CircleShape) iconSize = 24.dp,
.background(category.color), gradientColors = listOf(category.color),
contentAlignment = Alignment.Center contentDescription = null
) {
Icon(
imageVector = category.icon,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(24.dp)
) )
}
Spacer(modifier = Modifier.width(AppSpacing.md)) Spacer(modifier = Modifier.width(OrganicSpacing.md))
Text( Text(
text = category.name, text = category.name,
@@ -459,7 +424,7 @@ private fun TaskCategorySection(
color = Color.White color = Color.White
) )
} }
Spacer(modifier = Modifier.width(AppSpacing.sm)) Spacer(modifier = Modifier.width(OrganicSpacing.sm))
} }
Icon( Icon(
@@ -468,7 +433,6 @@ private fun TaskCategorySection(
tint = MaterialTheme.colorScheme.onSurfaceVariant tint = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
}
// Expanded content // Expanded content
AnimatedVisibility( AnimatedVisibility(
@@ -477,9 +441,7 @@ private fun TaskCategorySection(
exit = fadeOut() + shrinkVertically() exit = fadeOut() + shrinkVertically()
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier.fillMaxWidth()
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
) { ) {
category.tasks.forEachIndexed { index, task -> category.tasks.forEachIndexed { index, task ->
val isSelected = task.id in selectedTaskIds val isSelected = task.id in selectedTaskIds
@@ -494,7 +456,7 @@ private fun TaskCategorySection(
) )
if (index < category.tasks.lastIndex) { if (index < category.tasks.lastIndex) {
Divider( OrganicDivider(
modifier = Modifier.padding(start = 60.dp), modifier = Modifier.padding(start = 60.dp),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.2f) color = MaterialTheme.colorScheme.outline.copy(alpha = 0.2f)
) )
@@ -504,6 +466,7 @@ private fun TaskCategorySection(
} }
} }
} }
}
@Composable @Composable
private fun TaskTemplateRow( private fun TaskTemplateRow(
@@ -517,7 +480,7 @@ private fun TaskTemplateRow(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable(enabled = !isDisabled) { onClick() } .clickable(enabled = !isDisabled) { onClick() }
.padding(horizontal = AppSpacing.md, vertical = AppSpacing.sm), .padding(horizontal = OrganicSpacing.md, vertical = OrganicSpacing.sm),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// Checkbox // 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)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(

View File

@@ -1,8 +1,6 @@
package com.example.casera.ui.screens.onboarding package com.example.casera.ui.screens.onboarding
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -11,14 +9,12 @@ import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.example.casera.network.ApiResult import com.example.casera.network.ApiResult
import com.example.casera.ui.theme.AppRadius import com.example.casera.ui.theme.*
import com.example.casera.ui.theme.AppSpacing
import com.example.casera.viewmodel.OnboardingViewModel import com.example.casera.viewmodel.OnboardingViewModel
import casera.composeapp.generated.resources.* import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -48,10 +44,13 @@ fun OnboardingJoinResidenceContent(
val isLoading = joinState is ApiResult.Loading val isLoading = joinState is ApiResult.Loading
val isCodeValid = shareCode.length == 6 val isCodeValid = shareCode.length == 6
WarmGradientBackground(
modifier = Modifier.fillMaxSize()
) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(horizontal = AppSpacing.xl), .padding(horizontal = OrganicSpacing.xl),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
@@ -59,28 +58,20 @@ fun OnboardingJoinResidenceContent(
// Header // Header
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.lg) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg)
) { ) {
// Icon // Icon
Box( OrganicIconContainer(
modifier = Modifier icon = Icons.Default.GroupAdd,
.size(100.dp) size = 100.dp,
.clip(CircleShape) iconSize = 50.dp,
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)), contentDescription = null
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.GroupAdd,
contentDescription = null,
modifier = Modifier.size(50.dp),
tint = MaterialTheme.colorScheme.primary
) )
}
// Title and subtitle // Title and subtitle
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.sm) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
) { ) {
Text( Text(
text = stringResource(Res.string.onboarding_join_title), text = stringResource(Res.string.onboarding_join_title),
@@ -98,7 +89,7 @@ fun OnboardingJoinResidenceContent(
} }
} }
Spacer(modifier = Modifier.height(AppSpacing.xl * 2)) Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
// Share code input // Share code input
OutlinedTextField( OutlinedTextField(
@@ -123,7 +114,7 @@ fun OnboardingJoinResidenceContent(
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
), ),
shape = RoundedCornerShape(AppRadius.md), shape = RoundedCornerShape(OrganicRadius.md),
singleLine = true, singleLine = true,
keyboardOptions = KeyboardOptions( keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Characters capitalization = KeyboardCapitalization.Characters
@@ -133,17 +124,15 @@ fun OnboardingJoinResidenceContent(
// Error message // Error message
if (localErrorMessage != null) { if (localErrorMessage != null) {
Spacer(modifier = Modifier.height(AppSpacing.md)) Spacer(modifier = Modifier.height(OrganicSpacing.md))
Card( OrganicCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors( accentColor = MaterialTheme.colorScheme.error,
containerColor = MaterialTheme.colorScheme.errorContainer showBlob = false
),
shape = RoundedCornerShape(AppRadius.md)
) { ) {
Row( Row(
modifier = Modifier.padding(AppSpacing.md), modifier = Modifier.padding(OrganicSpacing.md),
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm), horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
@@ -162,9 +151,9 @@ fun OnboardingJoinResidenceContent(
// Loading indicator // Loading indicator
if (isLoading) { if (isLoading) {
Spacer(modifier = Modifier.height(AppSpacing.md)) Spacer(modifier = Modifier.height(OrganicSpacing.md))
Row( Row(
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm), horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
CircularProgressIndicator( CircularProgressIndicator(
@@ -182,29 +171,15 @@ fun OnboardingJoinResidenceContent(
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
// Join button // Join button
Button( OrganicPrimaryButton(
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), text = stringResource(Res.string.onboarding_join_button),
style = MaterialTheme.typography.titleMedium, onClick = { viewModel.joinResidence(shareCode) },
fontWeight = FontWeight.SemiBold modifier = Modifier.fillMaxWidth(),
enabled = isCodeValid && !isLoading,
isLoading = isLoading
) )
}
}
Spacer(modifier = Modifier.height(AppSpacing.xl * 2)) Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
}
} }
} }

View File

@@ -1,8 +1,6 @@
package com.example.casera.ui.screens.onboarding package com.example.casera.ui.screens.onboarding
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowForward import androidx.compose.material.icons.filled.ArrowForward
@@ -11,14 +9,10 @@ import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.example.casera.ui.theme.AppRadius import com.example.casera.ui.theme.*
import com.example.casera.ui.theme.AppSpacing
import com.example.casera.viewmodel.OnboardingViewModel import com.example.casera.viewmodel.OnboardingViewModel
import casera.composeapp.generated.resources.* import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -31,10 +25,13 @@ fun OnboardingNameResidenceContent(
val residenceName by viewModel.residenceName.collectAsState() val residenceName by viewModel.residenceName.collectAsState()
var localName by remember { mutableStateOf(residenceName) } var localName by remember { mutableStateOf(residenceName) }
WarmGradientBackground(
modifier = Modifier.fillMaxSize()
) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(horizontal = AppSpacing.xl), .padding(horizontal = OrganicSpacing.xl),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
@@ -42,40 +39,20 @@ fun OnboardingNameResidenceContent(
// Header with icon // Header with icon
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.lg) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg)
) { ) {
// Icon with gradient background // Icon with OrganicIconContainer
Box( OrganicIconContainer(
modifier = Modifier icon = Icons.Default.Home,
.size(100.dp) size = 100.dp,
.shadow( iconSize = 50.dp,
elevation = 16.dp, contentDescription = null
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
)
}
// Title and subtitle // Title and subtitle
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.sm) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
) { ) {
Text( Text(
text = stringResource(Res.string.onboarding_name_residence_title), text = stringResource(Res.string.onboarding_name_residence_title),
@@ -93,7 +70,7 @@ fun OnboardingNameResidenceContent(
} }
} }
Spacer(modifier = Modifier.height(AppSpacing.xl * 2)) Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
// Name input // Name input
OutlinedTextField( OutlinedTextField(
@@ -106,7 +83,7 @@ fun OnboardingNameResidenceContent(
) )
}, },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(AppRadius.md), shape = RoundedCornerShape(OrganicRadius.md),
singleLine = true, singleLine = true,
colors = OutlinedTextFieldDefaults.colors( colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary, focusedBorderColor = MaterialTheme.colorScheme.primary,
@@ -114,7 +91,7 @@ fun OnboardingNameResidenceContent(
) )
) )
Spacer(modifier = Modifier.height(AppSpacing.sm)) Spacer(modifier = Modifier.height(OrganicSpacing.sm))
Text( Text(
text = stringResource(Res.string.onboarding_name_residence_hint), text = stringResource(Res.string.onboarding_name_residence_hint),
@@ -125,26 +102,18 @@ fun OnboardingNameResidenceContent(
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
// Continue button // Continue button
Button( OrganicPrimaryButton(
text = stringResource(Res.string.onboarding_continue),
onClick = { onClick = {
viewModel.setResidenceName(localName) viewModel.setResidenceName(localName)
onContinue() onContinue()
}, },
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.fillMaxWidth() enabled = localName.isNotBlank(),
.height(56.dp), icon = Icons.Default.ArrowForward
shape = RoundedCornerShape(AppRadius.md),
enabled = localName.isNotBlank()
) {
Text(
text = stringResource(Res.string.onboarding_continue),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
) )
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))
}
} }
} }

View File

@@ -14,7 +14,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel 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.OnboardingStep
import com.example.casera.viewmodel.OnboardingViewModel import com.example.casera.viewmodel.OnboardingViewModel
import com.example.casera.viewmodel.OnboardingIntent import com.example.casera.viewmodel.OnboardingIntent
@@ -189,7 +189,7 @@ private fun OnboardingNavigationBar(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = AppSpacing.lg, vertical = AppSpacing.md), .padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.md),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// Back button // Back button
@@ -240,7 +240,7 @@ fun OnboardingProgressIndicator(
totalSteps: Int totalSteps: Int
) { ) {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(AppSpacing.xs), horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.xs),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
repeat(totalSteps) { index -> repeat(totalSteps) { index ->

View File

@@ -23,8 +23,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.example.casera.ui.theme.AppRadius import com.example.casera.ui.theme.*
import com.example.casera.ui.theme.AppSpacing
import casera.composeapp.generated.resources.* import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -117,16 +116,19 @@ fun OnboardingSubscriptionContent(
) )
) )
WarmGradientBackground(
modifier = Modifier.fillMaxSize()
) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
Column( Column(
modifier = Modifier.padding(horizontal = AppSpacing.xl), modifier = Modifier.padding(horizontal = OrganicSpacing.xl),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Spacer(modifier = Modifier.height(AppSpacing.lg)) Spacer(modifier = Modifier.height(OrganicSpacing.lg))
// Crown header with animation // Crown header with animation
Box( Box(
@@ -149,38 +151,27 @@ fun OnboardingSubscriptionContent(
) )
) )
// Crown icon // Crown icon using OrganicIconContainer
Box( OrganicIconContainer(
modifier = Modifier icon = Icons.Default.EmojiEvents,
.size(100.dp) size = 100.dp,
.clip(CircleShape) iconSize = 50.dp,
.background( gradientColors = listOf(
Brush.linearGradient(
colors = listOf(
MaterialTheme.colorScheme.tertiary, MaterialTheme.colorScheme.tertiary,
Color(0xFFFF9500) Color(0xFFFF9500)
)
)
), ),
contentAlignment = Alignment.Center contentDescription = null
) {
Icon(
imageVector = Icons.Default.EmojiEvents,
contentDescription = null,
modifier = Modifier.size(50.dp),
tint = Color.White
) )
} }
}
// PRO badge // PRO badge
Surface( Surface(
shape = RoundedCornerShape(AppRadius.full), shape = RoundedCornerShape(OrganicRadius.full),
color = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.15f) color = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.15f)
) { ) {
Row( Row(
modifier = Modifier.padding(horizontal = AppSpacing.lg, vertical = AppSpacing.sm), modifier = Modifier.padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.sm),
horizontalArrangement = Arrangement.spacedBy(AppSpacing.xs), horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.xs),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
@@ -204,7 +195,7 @@ fun OnboardingSubscriptionContent(
} }
} }
Spacer(modifier = Modifier.height(AppSpacing.md)) Spacer(modifier = Modifier.height(OrganicSpacing.md))
Text( Text(
text = stringResource(Res.string.onboarding_subscription_subtitle), text = stringResource(Res.string.onboarding_subscription_subtitle),
@@ -214,11 +205,11 @@ fun OnboardingSubscriptionContent(
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
Spacer(modifier = Modifier.height(AppSpacing.sm)) Spacer(modifier = Modifier.height(OrganicSpacing.sm))
// Social proof // Social proof
Row( Row(
horizontalArrangement = Arrangement.spacedBy(AppSpacing.xs), horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.xs),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
repeat(5) { repeat(5) {
@@ -236,18 +227,18 @@ fun OnboardingSubscriptionContent(
) )
} }
Spacer(modifier = Modifier.height(AppSpacing.xl)) Spacer(modifier = Modifier.height(OrganicSpacing.xl))
// Benefits list // Benefits list
Column( Column(
verticalArrangement = Arrangement.spacedBy(AppSpacing.sm) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
) { ) {
benefits.forEach { benefit -> benefits.forEach { benefit ->
BenefitRow(benefit = benefit) BenefitRow(benefit = benefit)
} }
} }
Spacer(modifier = Modifier.height(AppSpacing.xl)) Spacer(modifier = Modifier.height(OrganicSpacing.xl))
// Plan selection // Plan selection
Text( Text(
@@ -257,7 +248,7 @@ fun OnboardingSubscriptionContent(
color = MaterialTheme.colorScheme.onBackground color = MaterialTheme.colorScheme.onBackground
) )
Spacer(modifier = Modifier.height(AppSpacing.md)) Spacer(modifier = Modifier.height(OrganicSpacing.md))
// Yearly plan // Yearly plan
PlanCard( PlanCard(
@@ -266,7 +257,7 @@ fun OnboardingSubscriptionContent(
onClick = { selectedPlan = SubscriptionPlan.YEARLY } onClick = { selectedPlan = SubscriptionPlan.YEARLY }
) )
Spacer(modifier = Modifier.height(AppSpacing.sm)) Spacer(modifier = Modifier.height(OrganicSpacing.sm))
// Monthly plan // Monthly plan
PlanCard( PlanCard(
@@ -275,42 +266,23 @@ fun OnboardingSubscriptionContent(
onClick = { selectedPlan = SubscriptionPlan.MONTHLY } onClick = { selectedPlan = SubscriptionPlan.MONTHLY }
) )
Spacer(modifier = Modifier.height(AppSpacing.xl)) Spacer(modifier = Modifier.height(OrganicSpacing.xl))
// Start trial button // Start trial button
Button( OrganicPrimaryButton(
text = stringResource(Res.string.onboarding_subscription_start_trial),
onClick = { onClick = {
isLoading = true isLoading = true
// Simulate subscription flow // Simulate subscription flow
onSubscribe() onSubscribe()
}, },
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.fillMaxWidth() enabled = !isLoading,
.height(56.dp), isLoading = isLoading,
shape = RoundedCornerShape(AppRadius.lg), icon = Icons.Default.ArrowForward
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)) Spacer(modifier = Modifier.height(OrganicSpacing.md))
// Continue free button // Continue free button
TextButton(onClick = onSkip) { TextButton(onClick = onSkip) {
@@ -322,7 +294,7 @@ fun OnboardingSubscriptionContent(
) )
} }
Spacer(modifier = Modifier.height(AppSpacing.sm)) Spacer(modifier = Modifier.height(OrganicSpacing.sm))
// Legal text // Legal text
Text( Text(
@@ -337,7 +309,8 @@ fun OnboardingSubscriptionContent(
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
) )
Spacer(modifier = Modifier.height(AppSpacing.xl * 2)) Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
}
} }
} }
} }
@@ -347,28 +320,19 @@ private fun BenefitRow(benefit: SubscriptionBenefit) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = AppSpacing.md, vertical = AppSpacing.sm), .padding(horizontal = OrganicSpacing.md, vertical = OrganicSpacing.sm),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// Gradient icon // Gradient icon using OrganicIconContainer
Box( OrganicIconContainer(
modifier = Modifier icon = benefit.icon,
.size(44.dp) size = 44.dp,
.clip(CircleShape) iconSize = 24.dp,
.background( gradientColors = benefit.gradientColors,
Brush.linearGradient(benefit.gradientColors) contentDescription = null
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = benefit.icon,
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = Color.White
) )
}
Spacer(modifier = Modifier.width(AppSpacing.md)) Spacer(modifier = Modifier.width(OrganicSpacing.md))
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
@@ -402,26 +366,15 @@ private fun PlanCard(
) { ) {
val isYearly = plan == SubscriptionPlan.YEARLY val isYearly = plan == SubscriptionPlan.YEARLY
Surface( OrganicCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { onClick() }, .clickable { onClick() },
shape = RoundedCornerShape(AppRadius.lg), accentColor = if (isSelected) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.surfaceVariant,
color = MaterialTheme.colorScheme.surfaceVariant, showBlob = isSelected
border = if (isSelected) {
ButtonDefaults.outlinedButtonBorder.copy(
brush = Brush.linearGradient(
colors = listOf(
MaterialTheme.colorScheme.tertiary,
Color(0xFFFF9500)
)
),
width = 2.dp
)
} else null
) { ) {
Row( Row(
modifier = Modifier.padding(AppSpacing.lg), modifier = Modifier.padding(OrganicSpacing.lg),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// Selection indicator // 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)) { Column(modifier = Modifier.weight(1f)) {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm), horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
@@ -466,7 +419,7 @@ private fun PlanCard(
if (isYearly) { if (isYearly) {
Surface( Surface(
shape = RoundedCornerShape(AppRadius.full), shape = RoundedCornerShape(OrganicRadius.full),
color = Color(0xFF34C759) color = Color(0xFF34C759)
) { ) {
Text( Text(
@@ -474,7 +427,7 @@ private fun PlanCard(
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = Color.White, color = Color.White,
modifier = Modifier.padding(horizontal = AppSpacing.sm, vertical = 2.dp) modifier = Modifier.padding(horizontal = OrganicSpacing.sm, vertical = 2.dp)
) )
} }
} }

View File

@@ -21,8 +21,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.example.casera.ui.theme.AppRadius import com.example.casera.ui.theme.*
import com.example.casera.ui.theme.AppSpacing
import casera.composeapp.generated.resources.* import casera.composeapp.generated.resources.*
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -111,10 +110,13 @@ fun OnboardingValuePropsContent(
} }
} }
WarmGradientBackground(
modifier = Modifier.fillMaxSize()
) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(horizontal = AppSpacing.xl), .padding(horizontal = OrganicSpacing.xl),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Spacer(modifier = Modifier.weight(0.5f)) Spacer(modifier = Modifier.weight(0.5f))
@@ -129,11 +131,11 @@ fun OnboardingValuePropsContent(
FeatureCard(feature = features[page]) FeatureCard(feature = features[page])
} }
Spacer(modifier = Modifier.height(AppSpacing.xl)) Spacer(modifier = Modifier.height(OrganicSpacing.xl))
// Page indicators // Page indicators
Row( Row(
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm), horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
repeat(features.size) { index -> repeat(features.size) { index ->
@@ -155,23 +157,15 @@ fun OnboardingValuePropsContent(
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
// Continue button // Continue button
Button( OrganicPrimaryButton(
onClick = onContinue,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(AppRadius.md)
) {
Text(
text = stringResource(Res.string.onboarding_continue), text = stringResource(Res.string.onboarding_continue),
style = MaterialTheme.typography.titleMedium, onClick = onContinue,
fontWeight = FontWeight.SemiBold 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( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = AppSpacing.md), .padding(horizontal = OrganicSpacing.md),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
// Icon with gradient background // Icon with gradient background using OrganicIconContainer
Box( OrganicIconContainer(
modifier = Modifier icon = feature.icon,
.size(120.dp) size = 120.dp,
.clip(CircleShape) iconSize = 60.dp,
.background( gradientColors = feature.gradientColors,
Brush.linearGradient(feature.gradientColors) contentDescription = null
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = feature.icon,
contentDescription = null,
modifier = Modifier.size(60.dp),
tint = Color.White
) )
}
Spacer(modifier = Modifier.height(AppSpacing.xl * 2)) Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
// Title // Title
Text( Text(
@@ -213,7 +198,7 @@ private fun FeatureCard(feature: FeatureItem) {
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
Spacer(modifier = Modifier.height(AppSpacing.md)) Spacer(modifier = Modifier.height(OrganicSpacing.md))
// Description // Description
Text( Text(

View File

@@ -1,8 +1,6 @@
package com.example.casera.ui.screens.onboarding package com.example.casera.ui.screens.onboarding
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -11,15 +9,12 @@ import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.example.casera.network.ApiResult import com.example.casera.network.ApiResult
import com.example.casera.ui.theme.AppRadius import com.example.casera.ui.theme.*
import com.example.casera.ui.theme.AppSpacing
import com.example.casera.viewmodel.OnboardingViewModel import com.example.casera.viewmodel.OnboardingViewModel
import casera.composeapp.generated.resources.* import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -56,10 +51,13 @@ fun OnboardingVerifyEmailContent(
val isLoading = verifyState is ApiResult.Loading val isLoading = verifyState is ApiResult.Loading
WarmGradientBackground(
modifier = Modifier.fillMaxSize()
) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(horizontal = AppSpacing.xl), .padding(horizontal = OrganicSpacing.xl),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
@@ -67,28 +65,20 @@ fun OnboardingVerifyEmailContent(
// Header // Header
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.lg) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg)
) { ) {
// Icon with gradient background // Icon with OrganicIconContainer
Box( OrganicIconContainer(
modifier = Modifier icon = Icons.Default.MarkEmailRead,
.size(100.dp) size = 100.dp,
.clip(CircleShape) iconSize = 50.dp,
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)), contentDescription = null
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.MarkEmailRead,
contentDescription = null,
modifier = Modifier.size(50.dp),
tint = MaterialTheme.colorScheme.primary
) )
}
// Title and subtitle // Title and subtitle
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.sm) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
) { ) {
Text( Text(
text = stringResource(Res.string.onboarding_verify_email_title), text = stringResource(Res.string.onboarding_verify_email_title),
@@ -106,7 +96,7 @@ fun OnboardingVerifyEmailContent(
} }
} }
Spacer(modifier = Modifier.height(AppSpacing.xl * 2)) Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
// Code input // Code input
OutlinedTextField( OutlinedTextField(
@@ -133,7 +123,7 @@ fun OnboardingVerifyEmailContent(
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
), ),
shape = RoundedCornerShape(AppRadius.md), shape = RoundedCornerShape(OrganicRadius.md),
singleLine = true, singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
enabled = !isLoading enabled = !isLoading
@@ -141,17 +131,15 @@ fun OnboardingVerifyEmailContent(
// Error message // Error message
if (localErrorMessage != null) { if (localErrorMessage != null) {
Spacer(modifier = Modifier.height(AppSpacing.md)) Spacer(modifier = Modifier.height(OrganicSpacing.md))
Card( OrganicCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors( accentColor = MaterialTheme.colorScheme.error,
containerColor = MaterialTheme.colorScheme.errorContainer showBlob = false
),
shape = RoundedCornerShape(AppRadius.md)
) { ) {
Row( Row(
modifier = Modifier.padding(AppSpacing.md), modifier = Modifier.padding(OrganicSpacing.md),
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm), horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
@@ -170,9 +158,9 @@ fun OnboardingVerifyEmailContent(
// Loading indicator // Loading indicator
if (isLoading) { if (isLoading) {
Spacer(modifier = Modifier.height(AppSpacing.md)) Spacer(modifier = Modifier.height(OrganicSpacing.md))
Row( Row(
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm), horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
CircularProgressIndicator( CircularProgressIndicator(
@@ -187,7 +175,7 @@ fun OnboardingVerifyEmailContent(
} }
} }
Spacer(modifier = Modifier.height(AppSpacing.lg)) Spacer(modifier = Modifier.height(OrganicSpacing.lg))
// Hint text // Hint text
Text( Text(
@@ -200,29 +188,15 @@ fun OnboardingVerifyEmailContent(
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
// Verify button // Verify button
Button( OrganicPrimaryButton(
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), text = stringResource(Res.string.auth_verify_button),
style = MaterialTheme.typography.titleMedium, onClick = { viewModel.verifyEmail(code) },
fontWeight = FontWeight.SemiBold modifier = Modifier.fillMaxWidth(),
enabled = code.length == 6 && !isLoading,
isLoading = isLoading
) )
}
}
Spacer(modifier = Modifier.height(AppSpacing.xl * 2)) Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
}
} }
} }

View File

@@ -1,7 +1,6 @@
package com.example.casera.ui.screens.onboarding package com.example.casera.ui.screens.onboarding
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -11,16 +10,11 @@ import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.casera.ui.theme.AppRadius import com.example.casera.ui.theme.*
import com.example.casera.ui.theme.AppSpacing
import com.example.casera.viewmodel.AuthViewModel import com.example.casera.viewmodel.AuthViewModel
import com.example.casera.network.ApiResult import com.example.casera.network.ApiResult
import casera.composeapp.generated.resources.* import casera.composeapp.generated.resources.*
@@ -35,10 +29,13 @@ fun OnboardingWelcomeContent(
) { ) {
var showLoginDialog by remember { mutableStateOf(false) } var showLoginDialog by remember { mutableStateOf(false) }
WarmGradientBackground(
modifier = Modifier.fillMaxSize()
) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(horizontal = AppSpacing.xl), .padding(horizontal = OrganicSpacing.xl),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
@@ -47,34 +44,20 @@ fun OnboardingWelcomeContent(
// Hero section // Hero section
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.xl) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xl)
) { ) {
// App icon with shadow // App icon with OrganicIconContainer
Box( OrganicIconContainer(
modifier = Modifier icon = Icons.Default.Home,
.size(120.dp) size = 120.dp,
.shadow( iconSize = 80.dp,
elevation = 20.dp, contentDescription = null
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
)
}
// Welcome text // Welcome text
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.sm) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
) { ) {
Text( Text(
text = stringResource(Res.string.onboarding_welcome_title), text = stringResource(Res.string.onboarding_welcome_title),
@@ -97,31 +80,15 @@ fun OnboardingWelcomeContent(
// Action buttons // Action buttons
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(AppSpacing.md) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
// Primary CTA - Start Fresh // Primary CTA - Start Fresh
Button( OrganicPrimaryButton(
onClick = onStartFresh,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(AppRadius.md),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
)
) {
Icon(
imageVector = Icons.Default.Home,
contentDescription = null,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(AppSpacing.sm))
Text(
text = stringResource(Res.string.onboarding_start_fresh), text = stringResource(Res.string.onboarding_start_fresh),
style = MaterialTheme.typography.titleMedium, onClick = onStartFresh,
fontWeight = FontWeight.SemiBold modifier = Modifier.fillMaxWidth(),
icon = Icons.Default.Home
) )
}
// Secondary CTA - Join Existing // Secondary CTA - Join Existing
OutlinedButton( OutlinedButton(
@@ -129,7 +96,7 @@ fun OnboardingWelcomeContent(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(56.dp), .height(56.dp),
shape = RoundedCornerShape(AppRadius.md), shape = RoundedCornerShape(OrganicRadius.md),
colors = ButtonDefaults.outlinedButtonColors( colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.primary contentColor = MaterialTheme.colorScheme.primary
) )
@@ -139,7 +106,7 @@ fun OnboardingWelcomeContent(
contentDescription = null, contentDescription = null,
modifier = Modifier.size(24.dp) modifier = Modifier.size(24.dp)
) )
Spacer(modifier = Modifier.width(AppSpacing.sm)) Spacer(modifier = Modifier.width(OrganicSpacing.sm))
Text( Text(
text = stringResource(Res.string.onboarding_join_existing), text = stringResource(Res.string.onboarding_join_existing),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
@@ -160,7 +127,8 @@ fun OnboardingWelcomeContent(
} }
} }
Spacer(modifier = Modifier.height(AppSpacing.xl * 2)) Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
}
} }
// Login dialog // Login dialog
@@ -212,7 +180,7 @@ private fun LoginDialog(
}, },
text = { text = {
Column( Column(
verticalArrangement = Arrangement.spacedBy(AppSpacing.md) verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) { ) {
OutlinedTextField( OutlinedTextField(
value = username, value = username,
@@ -220,7 +188,7 @@ private fun LoginDialog(
label = { Text(stringResource(Res.string.auth_login_username_label)) }, label = { Text(stringResource(Res.string.auth_login_username_label)) },
singleLine = true, singleLine = true,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(AppRadius.md), shape = RoundedCornerShape(OrganicRadius.md),
enabled = !isLoading enabled = !isLoading
) )
@@ -230,7 +198,7 @@ private fun LoginDialog(
label = { Text(stringResource(Res.string.auth_login_password_label)) }, label = { Text(stringResource(Res.string.auth_login_password_label)) },
singleLine = true, singleLine = true,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(AppRadius.md), shape = RoundedCornerShape(OrganicRadius.md),
visualTransformation = androidx.compose.ui.text.input.PasswordVisualTransformation(), visualTransformation = androidx.compose.ui.text.input.PasswordVisualTransformation(),
enabled = !isLoading enabled = !isLoading
) )

View File

@@ -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<Color>? = 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)
)
}
}
}
}
}

View File

@@ -0,0 +1,6 @@
{
"enabledMcpjsonServers": [
"ios-simulator"
],
"enableAllProjectMcpServers": true
}