diff --git a/app/src/main/java/com/example/livingai_lg/ui/components/FilterOverlay.kt b/app/src/main/java/com/example/livingai_lg/ui/components/FilterOverlay.kt index ba065c9..d1b2313 100644 --- a/app/src/main/java/com/example/livingai_lg/ui/components/FilterOverlay.kt +++ b/app/src/main/java/com/example/livingai_lg/ui/components/FilterOverlay.kt @@ -1,7 +1,9 @@ -package com.example.livingai_lg.ui.screens +package com.example.livingai_lg.ui.components import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutHorizontally @@ -16,6 +18,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import com.example.livingai_lg.ui.screens.FilterScreen @Composable fun FilterOverlay( @@ -23,29 +26,40 @@ fun FilterOverlay( onDismiss: () -> Unit, onSubmitClick: () -> Unit = {}, ) { - if (!visible) return - - BackHandler { onDismiss() } + BackHandler(enabled = visible) { onDismiss() } Box( - modifier = Modifier - .fillMaxSize() + modifier = Modifier.fillMaxSize() ) { // Dimmed background - Box( - modifier = Modifier - .fillMaxSize() - .background(Color.Black.copy(alpha = 0.35f)) - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { onDismiss() } - ) AnimatedVisibility( visible = visible, - enter = slideInHorizontally { fullWidth -> fullWidth }, - exit = slideOutHorizontally { fullWidth -> fullWidth }, - modifier = Modifier.fillMaxHeight() + enter = fadeIn(), + exit = fadeOut() + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.35f)) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { onDismiss() } + ) + } + + // Sliding panel + AnimatedVisibility( + visible = visible, + enter = slideInHorizontally( + initialOffsetX = { it } // from right + ), + exit = slideOutHorizontally( + targetOffsetX = { it } // to right + ), + modifier = Modifier + .fillMaxHeight() + .align(Alignment.CenterEnd) ) { FilterScreen( onBackClick = onDismiss, diff --git a/app/src/main/java/com/example/livingai_lg/ui/components/SortOverlay.kt b/app/src/main/java/com/example/livingai_lg/ui/components/SortOverlay.kt index d9d952e..9b596bd 100644 --- a/app/src/main/java/com/example/livingai_lg/ui/components/SortOverlay.kt +++ b/app/src/main/java/com/example/livingai_lg/ui/components/SortOverlay.kt @@ -1,6 +1,9 @@ package com.example.livingai_lg.ui.components +import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.foundation.background @@ -10,11 +13,16 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp import com.example.livingai_lg.ui.models.SortField +import com.example.livingai_lg.ui.screens.FilterScreen import com.example.livingai_lg.ui.screens.SortScreen @Composable @@ -23,17 +31,28 @@ fun SortOverlay( onApplyClick: (selected: List) -> Unit, onDismiss: () -> Unit, ) { - if (!visible) return + BackHandler(enabled = visible) { onDismiss() } - Box( - modifier = Modifier - .fillMaxSize() - .background(Color.Black.copy(alpha = 0.35f)) - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { onDismiss() } // tap outside closes - ) { + Box(modifier = Modifier.fillMaxSize()) { + + // Dim background + AnimatedVisibility( + visible = visible, + enter = fadeIn(), + exit = fadeOut() + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.35f)) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { onDismiss() } + ) + } + + // Slide-in panel from LEFT AnimatedVisibility( visible = visible, enter = slideInHorizontally( @@ -41,33 +60,29 @@ fun SortOverlay( ), exit = slideOutHorizontally( targetOffsetX = { -it } - ) + ), + modifier = Modifier.align(Alignment.CenterStart) ) { Box( modifier = Modifier - .fillMaxHeight() - .fillMaxWidth(0.85f) // 👈 DOES NOT cover full screen + .fillMaxHeight(0.85f) + .fillMaxWidth(0.85f) .background(Color(0xFFF7F4EE)) - ) { - // Prevent click-through - Box( - modifier = Modifier - .fillMaxSize() - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) {} - ) { - SortScreen( - onApplyClick = { selected -> - onApplyClick(selected) - onDismiss() - // TODO: apply sort - }, - onCancelClick = onDismiss, - onBackClick = onDismiss + .clip( + RoundedCornerShape( + topEnd = 24.dp, + bottomEnd = 24.dp + ) ) - } + ) { + SortScreen( + onApplyClick = { selected -> + onApplyClick(selected) + onDismiss() + }, + onCancelClick = onDismiss, + onBackClick = onDismiss + ) } } } diff --git a/app/src/main/java/com/example/livingai_lg/ui/navigation/AppNavigation.kt b/app/src/main/java/com/example/livingai_lg/ui/navigation/AppNavigation.kt index 20b7b92..6db3550 100644 --- a/app/src/main/java/com/example/livingai_lg/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/com/example/livingai_lg/ui/navigation/AppNavigation.kt @@ -96,7 +96,7 @@ object AppScreen { object Graph { const val AUTH = "auth" - const val MAIN = "auth" + const val MAIN = "main" fun auth(route: String)= "$AUTH/$route" diff --git a/app/src/main/java/com/example/livingai_lg/ui/screens/BuyScreen.kt b/app/src/main/java/com/example/livingai_lg/ui/screens/BuyScreen.kt index db23513..dc3ce7a 100644 --- a/app/src/main/java/com/example/livingai_lg/ui/screens/BuyScreen.kt +++ b/app/src/main/java/com/example/livingai_lg/ui/screens/BuyScreen.kt @@ -35,6 +35,7 @@ import com.example.livingai_lg.ui.models.userProfile import com.example.livingai_lg.R import com.example.livingai_lg.ui.components.ActionPopup import com.example.livingai_lg.ui.components.AddressSelectorOverlay +import com.example.livingai_lg.ui.components.FilterOverlay import com.example.livingai_lg.ui.components.NotificationsOverlay import com.example.livingai_lg.ui.components.SortOverlay import com.example.livingai_lg.ui.models.sampleNotifications diff --git a/app/src/main/java/com/example/livingai_lg/ui/screens/FilterScreen.kt b/app/src/main/java/com/example/livingai_lg/ui/screens/FilterScreen.kt index 0cf3291..8583c34 100644 --- a/app/src/main/java/com/example/livingai_lg/ui/screens/FilterScreen.kt +++ b/app/src/main/java/com/example/livingai_lg/ui/screens/FilterScreen.kt @@ -15,7 +15,9 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForwardIos import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.ArrowForwardIos import androidx.compose.material.icons.filled.Check import androidx.compose.material3.* import androidx.compose.runtime.* @@ -75,7 +77,7 @@ fun FilterScreen( Column( modifier = Modifier - .fillMaxSize() + .fillMaxHeight() .background(Color(0xFFF7F4EE)) ) { // Header @@ -95,7 +97,7 @@ fun FilterScreen( ) { IconButton(onClick = onBackClick) { Icon( - imageVector = Icons.Default.ArrowBack, + imageVector = Icons.AutoMirrored.Filled.ArrowForwardIos, contentDescription = "Back", tint = Color(0xFF0A0A0A) ) diff --git a/app/src/main/java/com/example/livingai_lg/ui/screens/SortScreen.kt b/app/src/main/java/com/example/livingai_lg/ui/screens/SortScreen.kt index 23db146..c3ff298 100644 --- a/app/src/main/java/com/example/livingai_lg/ui/screens/SortScreen.kt +++ b/app/src/main/java/com/example/livingai_lg/ui/screens/SortScreen.kt @@ -24,7 +24,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowBackIos import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.ArrowBackIos import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api @@ -112,7 +114,7 @@ fun SortScreen( Row(verticalAlignment = Alignment.CenterVertically) { IconButton(onClick = onBackClick) { Icon( - imageVector = Icons.Default.ArrowBack, + imageVector = Icons.AutoMirrored.Filled.ArrowBackIos, contentDescription = "Back", tint = Color(0xFF0A0A0A) ) diff --git a/app/src/main/java/com/example/livingai_lg/ui/screens/auth/OTPScreen.kt b/app/src/main/java/com/example/livingai_lg/ui/screens/auth/OTPScreen.kt index 436024e..89c145b 100644 --- a/app/src/main/java/com/example/livingai_lg/ui/screens/auth/OTPScreen.kt +++ b/app/src/main/java/com/example/livingai_lg/ui/screens/auth/OTPScreen.kt @@ -34,8 +34,13 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.input.key.* import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.unit.Dp import kotlinx.coroutines.launch +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.onPreviewKeyEvent @Composable @@ -79,158 +84,175 @@ fun OtpScreen( fun s(v: Float) = (v * scale).dp // dp scaling fun fs(v: Float) = (v * scale).sp // font scaling - - // --------------------------- - // "Enter OTP" Title - // --------------------------- - Text( - text = "Enter OTP", - color = Color(0xFF927B5E), - fontSize = fs(20f), - fontWeight = FontWeight.Medium, - modifier = Modifier.offset(x = s(139f), y = s(279f)), - style = LocalTextStyle.current.copy( - shadow = Shadow( - color = Color.Black.copy(alpha = 0.25f), - offset = Offset(0f, s(4f).value), - blurRadius = s(4f).value - ) +Column( + Modifier.fillMaxSize().padding(horizontal = 12.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally +) { + // --------------------------- + // "Enter OTP" Title + // --------------------------- + Text( + text = "Enter OTP", + color = Color(0xFF927B5E), + fontSize = fs(20f), + fontWeight = FontWeight.Medium, + modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp), + textAlign = TextAlign.Center, + style = LocalTextStyle.current.copy( + shadow = Shadow( + color = Color.Black.copy(alpha = 0.25f), + offset = Offset(0f, s(4f).value), + blurRadius = s(4f).value ) ) + ) - // --------------------------- - // OTP 4-Box Input Row - // --------------------------- -// Row( -// Modifier.offset(x = s(38f), y = s(319f)), -// horizontalArrangement = Arrangement.spacedBy(s(17f)) -// ) { -// repeat(4) { index -> -// OtpBox( -// index = index, -// otp = otp.value, -// onChange = { if (it.length <= 6) otp.value = it }, -// scale = scale -// ) -// } -// } + // --------------------------- + // OTP 4-Box Input Row + // --------------------------- + Row( + Modifier.fillMaxWidth().padding(horizontal = 12.dp), + horizontalArrangement = Arrangement.spacedBy(s(17f)) + ) { OtpInputRow( otpLength = 6, scale = scale, otp = otp.value, onOtpChange = { if (it.length <= 6) otp.value = it } ) - - // --------------------------- - // Continue Button - // --------------------------- - Box( - modifier = Modifier - .offset(x = s(57f), y = s(411f)) - .size(s(279.25f), s(55.99f)) - .shadow( - elevation = s(6f), - ambientColor = Color.Black.copy(alpha = 0.10f), - shape = RoundedCornerShape(s(16f)), - ) - .shadow( - elevation = s(15f), - ambientColor = Color.Black.copy(alpha = 0.10f), - shape = RoundedCornerShape(s(16f)), - ) - .background( - Brush.horizontalGradient( - listOf(Color(0xFFFD9900), Color(0xFFE17100)) - ), - shape = RoundedCornerShape(s(16f)) - ) - .clickable( - indication = LocalIndication.current, - interactionSource = remember { MutableInteractionSource() }, - onClick = { - scope.launch { - authManager.login(phoneNumber, otp.value) - .onSuccess { response -> - if (isSignInFlow) { - // For existing users, always go to the success screen. - onSuccess() + } + // --------------------------- + // Continue Button + // --------------------------- + Box( + modifier = Modifier + .fillMaxWidth().padding(vertical = 16.dp, horizontal = 48.dp) + .size(s(279.25f), s(55.99f)) + .shadow( + elevation = s(6f), + ambientColor = Color.Black.copy(alpha = 0.10f), + shape = RoundedCornerShape(s(16f)), + ) + .shadow( + elevation = s(15f), + ambientColor = Color.Black.copy(alpha = 0.10f), + shape = RoundedCornerShape(s(16f)), + ) + .background( + Brush.horizontalGradient( + listOf(Color(0xFFFD9900), Color(0xFFE17100)) + ), + shape = RoundedCornerShape(s(16f)) + ) + .clickable( + indication = LocalIndication.current, + interactionSource = remember { MutableInteractionSource() }, + onClick = { + scope.launch { + authManager.login(phoneNumber, otp.value) + .onSuccess { response -> + if (isSignInFlow) { + // For existing users, always go to the success screen. + onSuccess() + } else { + // For new users, check if a profile needs to be created. + if (response.needsProfile) { + onCreateProfile(name) } else { - // For new users, check if a profile needs to be created. - if (response.needsProfile) { - onCreateProfile(name) - } else { - onSuccess() - } + onSuccess() } } - .onFailure { - Toast.makeText(context, "Invalid or expired OTP", Toast.LENGTH_SHORT).show() - } - } + } + .onFailure { + Toast.makeText( + context, + "Invalid or expired OTP", + Toast.LENGTH_SHORT + ).show() + } } - ), - contentAlignment = Alignment.Center - ) { - Text( - "Continue", - color = Color.White, - fontSize = fs(16f), - fontWeight = FontWeight.Medium - ) - } + } + ), + contentAlignment = Alignment.Center + ) { + Text( + "Continue", + color = Color.White, + fontSize = fs(16f), + fontWeight = FontWeight.Medium + ) + } +} } } @Composable fun OtpInputRow( - otpLength: Int = 4, + otpLength: Int, scale: Float, otp: String, onOtpChange: (String) -> Unit ) { - val focusRequesters = remember { - List(otpLength) { FocusRequester() } - } + BoxWithConstraints( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + val maxRowWidth = maxWidth - Row(horizontalArrangement = Arrangement.spacedBy((12 * scale).dp)) { - repeat(otpLength) { index -> - OtpBox( - index = index, - otp = otp, - scale = scale, - focusRequester = focusRequesters[index], - onRequestFocus = { - val firstEmpty = otp.length.coerceAtMost(otpLength - 1) - focusRequesters[firstEmpty].requestFocus() - }, - onNextFocus = { - if (index + 1 < otpLength) { - focusRequesters[index + 1].requestFocus() - } - }, - onPrevFocus = { - if (index - 1 >= 0) { - focusRequesters[index - 1].requestFocus() - } - }, - onChange = onOtpChange - ) + val spacing = (12f * scale).dp + val totalSpacing = spacing * (otpLength - 1) + + val boxWidth = ((maxRowWidth - totalSpacing) / otpLength) + .coerceAtMost((66f * scale).dp) + + val focusRequesters = remember { + List(otpLength) { FocusRequester() } + } + + Row( + horizontalArrangement = Arrangement.spacedBy(spacing), + verticalAlignment = Alignment.CenterVertically + ) { + repeat(otpLength) { index -> + OtpBox( + index = index, + otp = otp, + scale = scale, + width = boxWidth, // 👈 fixed width + focusRequester = focusRequesters[index], + onRequestFocus = { + val firstEmpty = otp.length.coerceAtMost(otpLength - 1) + focusRequesters[firstEmpty].requestFocus() + }, + onNextFocus = { + if (index + 1 < otpLength) focusRequesters[index + 1].requestFocus() + }, + onPrevFocus = { + if (index - 1 >= 0) focusRequesters[index - 1].requestFocus() + }, + onChange = onOtpChange + ) + } } } } + + + @Composable private fun OtpBox( index: Int, otp: String, scale: Float, + width: Dp, focusRequester: FocusRequester, onRequestFocus: () -> Unit, onNextFocus: () -> Unit, onPrevFocus: () -> Unit, onChange: (String) -> Unit ) { - val boxW = 66f * scale val boxH = 52f * scale val radius = 16f * scale @@ -238,7 +260,7 @@ private fun OtpBox( Box( modifier = Modifier - .size(boxW.dp, boxH.dp) + .size(width, boxH.dp) .shadow((4f * scale).dp, RoundedCornerShape(radius.dp)) .background(Color.White, RoundedCornerShape(radius.dp)) .clickable { onRequestFocus() }, @@ -250,25 +272,38 @@ private fun OtpBox( when { // DIGIT ENTERED new.matches(Regex("\\d")) -> { - val updated = otp.padEnd(index, ' ').toMutableList() - if (updated.size > index) updated[index] = new.first() - else updated.add(new.first()) + val updated = otp.padEnd(index + 1, ' ').toMutableList() + updated[index] = new.first() onChange(updated.joinToString("").trim()) onNextFocus() } - // BACKSPACE - new.isEmpty() -> { - if (char.isNotEmpty()) { - val updated = otp.toMutableList() - updated.removeAt(index) - onChange(updated.joinToString("")) - } else { - onPrevFocus() - } + // BACKSPACE WHEN CHARACTER EXISTS + new.isEmpty() && char.isNotEmpty() -> { + val updated = otp.toMutableList() + updated.removeAt(index) + onChange(updated.joinToString("")) } } }, + modifier = Modifier + .focusRequester(focusRequester) + .onPreviewKeyEvent { event -> + if (event.type == KeyEventType.KeyDown && + event.key == Key.Backspace && + char.isEmpty() && + index > 0 + ) { + val updated = otp.toMutableList() + updated.removeAt(index - 1) // 👈 clear previous box + onChange(updated.joinToString("")) + onPrevFocus() + true + } + else { + false + } + }, textStyle = LocalTextStyle.current.copy( fontSize = (24f * scale).sp, fontWeight = FontWeight.Medium, @@ -277,11 +312,14 @@ private fun OtpBox( keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.NumberPassword ), - singleLine = true, - modifier = Modifier - .focusRequester(focusRequester) - .align(Alignment.Center) + singleLine = true ) + + + } } + + +