This is a dating app
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1805 lines
73 KiB

package com.example.dattingapp.registration
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
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.ArrowForward
import androidx.compose.material.icons.filled.AlternateEmail
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.Badge
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Mail
import androidx.compose.material.icons.filled.PhoneIphone
import androidx.compose.material.icons.filled.RadioButtonUnchecked
import androidx.compose.material.icons.filled.SmartToy
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.dattingapp.ui.theme.InputBg
import com.example.dattingapp.ui.theme.LightBorderColor
import com.example.dattingapp.ui.theme.PinkGradientStart
import com.example.dattingapp.ui.theme.PurpleGradientEnd
import com.example.dattingapp.ui.theme.textDark
import com.example.dattingapp.ui.theme.textPink
import androidx.compose.material.icons.filled.Cake
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
import androidx.compose.animation.core.*
import androidx.compose.material.icons.filled.CompassCalibration
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.LocalBar
import androidx.compose.material.icons.filled.People
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.draw.scale
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import android.location.LocationManager
import android.os.Looper
import android.Manifest
import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.material.icons.filled.LocationOn
// Permission check
import androidx.core.content.ContextCompat
import com.example.dattingapp.homes.networkandapi.TokenManager
class RegistrationScreen : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
RegistrationFlowContainer()
}
}
}
}
@Composable
fun RegistrationFlowContainer(
registrationViewModel: RegistrationViewModel = viewModel()
) {
val context = LocalContext.current
val uiState by registrationViewModel.uiState.collectAsState()
var currentStep by remember { mutableIntStateOf(1) }
val totalSteps = 7
BackHandler(enabled = currentStep > 1 && uiState !is RegistrationUiState.Loading) {
currentStep--
}
// Input States
var nameInput by rememberSaveable { mutableStateOf("") }
var nameError by rememberSaveable { mutableStateOf<String?>(null) }
val nameRegex = remember { Regex("^[a-zA-Z\\s]+$") }
var selectedTab by rememberSaveable { mutableIntStateOf(0) }
var emailInput by rememberSaveable { mutableStateOf("") }
var phoneInput by rememberSaveable { mutableStateOf("") }
var countryCode by rememberSaveable { mutableStateOf("+86") }
var emailError by rememberSaveable { mutableStateOf<String?>(null) }
var phoneError by rememberSaveable { mutableStateOf<String?>(null) }
var passwordInput by remember { mutableStateOf("") }
var confirmPasswordInput by remember { mutableStateOf("") }
var passwordError by remember { mutableStateOf<String?>(null) }
var confirmPasswordError by remember { mutableStateOf<String?>(null) }
var selectedBirthDateMillis by rememberSaveable { mutableStateOf<Long?>(null) }
// var addressInput by rememberSaveable { mutableStateOf("") }
var selectedGender by rememberSaveable { mutableStateOf("") }
var selectedRealationStatus by remember { mutableStateOf("") }
var userLat by rememberSaveable { mutableStateOf<Double?>(null) }
var userLng by rememberSaveable { mutableStateOf<Double?>(null) }
val scrollState = rememberScrollState()
// Handle Network Success & Error Reactions
LaunchedEffect(uiState) {
when (val state = uiState) {
is RegistrationUiState.Success -> {
Toast.makeText(context, "Registration Successful!", Toast.LENGTH_SHORT).show()
val tokenManager = TokenManager.getInstance(context)
tokenManager.saveTokens(
accessToken = state.response.accessToken,
refreshToken = state.response.refreshToken
)
// Navigate to UploadProfileImageActivity and finish flow
val intent = Intent(context, UploadProfileImageActivity::class.java).apply {
putExtra("ACCESS_TOKEN", state.response.accessToken)
putExtra("REFRESH_TOKEN", state.response.refreshToken)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
context.startActivity(intent)
}
is RegistrationUiState.Error -> {
Toast.makeText(context, state.message, Toast.LENGTH_LONG).show()
}
else -> {}
}
}
Box(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding()
.navigationBarsPadding()
.imePadding()
) {
// Show Full Screen Loading Overlay if API call is active
if (uiState is RegistrationUiState.Loading) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White.copy(alpha = 0.95f)),
contentAlignment = Alignment.Center
) {
UniquePulseLoader(message = "Setting up your profile...")
}
} else {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(horizontal = 24.dp)
) {
Spacer(modifier = Modifier.height(16.dp))
// 1. App Bar Header
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
onClick = { if (currentStep > 1) currentStep-- },
enabled = currentStep > 1
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
tint = if (currentStep > 1) textDark else Color.Transparent
)
}
Text(
text = "VibeCheck",
style = MaterialTheme.typography.titleLarge.copy(
fontWeight = FontWeight.Bold,
color = PurpleGradientEnd,
fontSize = 24.sp
)
)
TextButton(onClick = { /* Skip */ }) {
Text(text = "SKIP", color = Color.Gray, fontWeight = FontWeight.Medium)
}
}
Spacer(modifier = Modifier.height(16.dp))
// 2. Step Indicator Text
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "STEP $currentStep OF $totalSteps",
color = textPink,
fontWeight = FontWeight.Bold,
fontSize = 12.sp
)
Text(
text = when (currentStep) {
1 -> "Getting Started"
2 -> "Contact Details"
3 -> "Password"
4 -> "Date of Birth"
5 -> "Location"
6 -> "Interested Gender"
7-> "Relationship status"
else -> ""
},
color = Color.Gray,
fontSize = 12.sp
)
}
Spacer(modifier = Modifier.height(8.dp))
// 3. Step Progress Bar
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
for (i in 1..totalSteps) {
Box(
modifier = Modifier
.weight(1f)
.height(6.dp)
.clip(RoundedCornerShape(3.dp))
.background(
if (i <= currentStep)
Brush.horizontalGradient(listOf(PinkGradientStart, PurpleGradientEnd))
else
Brush.horizontalGradient(listOf(Color(0xFFE0E0E0), Color(0xFFE0E0E0)))
)
)
}
}
Spacer(modifier = Modifier.height(20.dp))
// 4. Avatar Frame
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.size(90.dp)
.clip(RoundedCornerShape(20.dp))
.background(Color(0xFFFDE8ED)),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.SmartToy,
contentDescription = "Mascot",
tint = PinkGradientStart,
modifier = Modifier.size(50.dp)
)
}
}
Spacer(modifier = Modifier.height(20.dp))
// 5. Dynamic Flow Steps
AnimatedContent(
targetState = currentStep,
label = "StepAnimation"
) { step ->
when (step) {
1 -> NameCompose(
name = nameInput,
errorMessage = nameError,
onNameChange = { nameInput = it; nameError = null }
)
2 -> EmailorPhoneCompose(
selectedTab = selectedTab,
onTabSelected = { selectedTab = it; emailError = null; phoneError = null },
emailText = emailInput,
onEmailChange = { emailInput = it; emailError = null },
phoneText = phoneInput,
onPhoneChange = { if (it.all { char -> char.isDigit() }) { phoneInput = it; phoneError = null } },
countryCode = countryCode,
emailError = emailError,
phoneError = phoneError,
isEmailValid = emailInput.trim().matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$".toRegex()),
isPhoneValid = phoneInput.trim().length in 10..15
)
3 -> PasswordComose(
passwordInput = passwordInput,
onPasswordChange = { passwordInput = it; passwordError = null },
confirmPasswordInput = confirmPasswordInput,
onConfirmPasswordChange = { confirmPasswordInput = it; confirmPasswordError = null },
passwordError = passwordError,
confirmPasswordError = confirmPasswordError
)
4 -> BirthDayCompose(
selectedDateInMillis = selectedBirthDateMillis,
onDateSelected = { selectedBirthDateMillis = it }
)
5 -> AddressCompose(
latitude = userLat,
longitude = userLng,
onLocationFetched = { lat, lng ->
userLat = lat
userLng = lng
}
)
6 -> VibeCheckScreen(
selectedRealationStatus = selectedRealationStatus ?: "",
onRelationSelected = { selectedRealationStatus = it }
)
7 -> GenderCompose(
selectedGender = selectedGender,
onGenderSelected = { selectedGender = it }
)
else -> Text("Step $step")
}
}
Spacer(modifier = Modifier.height(50.dp))
// 6. Action Button
Button(
onClick = {
when (currentStep) {
1 -> {
val trimmed = nameInput.trim()
when {
trimmed.isEmpty() -> nameError = "Name cannot be empty"
!nameRegex.matches(trimmed) -> nameError = "Numbers and special characters not allowed"
else -> currentStep++
}
}
2 -> {
if (selectedTab == 0) {
val trimmedEmail = emailInput.trim()
val emailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$".toRegex()
if (trimmedEmail.isEmpty() || !emailRegex.matches(trimmedEmail)) {
emailError = "Enter a valid email"
} else currentStep++
} else {
val trimmedPhone = phoneInput.trim()
if (trimmedPhone.isEmpty() || trimmedPhone.length !in 10..15) {
phoneError = "Enter a valid phone number"
} else currentStep++
}
}
3 -> {
val trimmedPassword = passwordInput.trim()
val trimmedConfirm = confirmPasswordInput.trim()
var isValid = true
if (trimmedPassword.length !in 8..20 || !trimmedPassword.any { it.isDigit() }) {
passwordError = "Password must be 8-20 characters with letters and numbers"
isValid = false
}
if (trimmedPassword != trimmedConfirm) {
confirmPasswordError = "Passwords do not match"
isValid = false
}
if (isValid) currentStep++
}
4 -> {
if (selectedBirthDateMillis == null) {
Toast.makeText(context, "Select date of birth", Toast.LENGTH_SHORT).show()
} else currentStep++
}
5 -> {
currentStep++
}
6 -> {
if (selectedRealationStatus.isNullOrEmpty()) {
Toast.makeText(context, "Please select your vibe", Toast.LENGTH_SHORT).show()
} else {
currentStep++
}
}
7 -> {
if (selectedGender.isBlank()) {
Toast.makeText(context, "Select gender interest", Toast.LENGTH_SHORT).show()
return@Button
}
val formattedDate = selectedBirthDateMillis?.let { millis ->
SimpleDateFormat("yyyy-MM-dd", Locale.US).format(Date(millis))
} ?: ""
val contact = if (selectedTab == 0) emailInput.trim() else "$countryCode${phoneInput.trim()}"
val payload = RegisterRequest(
emailOrPhone = contact,
password = passwordInput,
confirmPassword = confirmPasswordInput,
relationshipGoal = selectedRealationStatus.lowercase(Locale.ROOT).replace(" ", "_"),
fullName = nameInput.trim(),
gender = selectedGender.lowercase(Locale.ROOT),
birthDate = formattedDate,
city = "lat: $userLat" + "lng: $userLng",
bio = null,
videoIntro = null,
profileImage = null,
interests = null
)
registrationViewModel.registerUser(context, payload)
}
}
},
modifier = Modifier
.fillMaxWidth()
.height(52.dp)
.background(
brush = Brush.horizontalGradient(listOf(PinkGradientStart, PurpleGradientEnd)),
shape = RoundedCornerShape(26.dp)
),
colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent),
contentPadding = PaddingValues()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Text(
text = if (currentStep < totalSteps) "Continue" else "Finish Registration",
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color.White
)
Spacer(modifier = Modifier.width(8.dp))
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowForward,
contentDescription = null,
tint = Color.White
)
}
}
Spacer(modifier = Modifier.height(24.dp))
}
}
}
}
// --- Step Content Components ---
@Composable
fun NameCompose(
name: String,
errorMessage: String?,
onNameChange: (String) -> Unit
) {
Column {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
border = BorderStroke(1.dp, LightBorderColor)
) {
Text(
text = "Welcome! What should we call you?",
style = MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
color = textDark
),
modifier = Modifier.padding(20.dp)
)
}
Spacer(modifier = Modifier.height(25.dp))
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
border = BorderStroke(
width = 2.dp,
color = if (errorMessage != null) Color.Red else textPink
)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "YOUR FULL NAME OR FIRST NAME",
color = if (errorMessage != null) Color.Red else textPink,
fontSize = 11.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(InputBg)
.border(
width = 1.dp,
color = if (errorMessage != null) Color.Red else Color(0xFFE5E7EB),
shape = RoundedCornerShape(12.dp)
)
.padding(horizontal = 12.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Badge,
contentDescription = null,
tint = Color.Gray
)
Spacer(modifier = Modifier.width(8.dp))
BasicTextField(
value = name,
onValueChange = onNameChange,
modifier = Modifier.weight(1f),
singleLine = true
)
}
// Display the error message if present
if (errorMessage != null) {
Spacer(modifier = Modifier.height(6.dp))
Text(
text = errorMessage,
color = Color.Red,
fontSize = 12.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(start = 4.dp)
)
}
}
}
}
}
@Composable
fun EmailorPhoneCompose(selectedTab: Int, onTabSelected: (Int) -> Unit, emailText: String, onEmailChange: (String) -> Unit,
phoneText: String, onPhoneChange: (String) -> Unit, countryCode: String, emailError: String?,
phoneError: String?, isEmailValid: Boolean = false, isPhoneValid: Boolean = false
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Title Header Card
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
border = BorderStroke(1.dp, LightBorderColor)
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = if (selectedTab == 0) "How should we reach you?" else "What is your phone number?",
style = MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
color = textDark
)
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = if (selectedTab == 0)
"Choose whether you'd like to register with your email or phone number."
else
"We'll send a quick verification code to confirm it's you.",
fontSize = 12.sp,
color = Color.Gray,
textAlign = TextAlign.Center
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// Tab Selector (Email / Phone Number)
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
color = Color(0xFFEEF0F4)
) {
Row(modifier = Modifier.padding(4.dp)) {
Surface(
onClick = { onTabSelected(0) },
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(12.dp),
color = if (selectedTab == 0) Color.White else Color.Transparent,
shadowElevation = if (selectedTab == 0) 2.dp else 0.dp
) {
Row(
modifier = Modifier.padding(vertical = 12.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Mail,
contentDescription = null,
tint = if (selectedTab == 0) textPink else Color.Gray,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = "Email",
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = if (selectedTab == 0) textPink else Color.Gray
)
}
}
Surface(
onClick = { onTabSelected(1) },
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(12.dp),
color = if (selectedTab == 1) Color.White else Color.Transparent,
shadowElevation = if (selectedTab == 1) 2.dp else 0.dp
) {
Row(
modifier = Modifier.padding(vertical = 12.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.PhoneIphone,
contentDescription = null,
tint = if (selectedTab == 1) textPink else Color.Gray,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = "Phone Number",
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = if (selectedTab == 1) textPink else Color.Gray
)
}
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// Dynamic Input Section Card
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
border = BorderStroke(
width = 1.dp,
color = if ((selectedTab == 0 && emailError != null) || (selectedTab == 1 && phoneError != null)) Color.Red else LightBorderColor
)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = if (selectedTab == 0) "YOUR EMAIL ADDRESS" else "YOUR PHONE NUMBER",
color = if ((selectedTab == 0 && emailError != null) || (selectedTab == 1 && phoneError != null)) Color.Red else textPink,
fontSize = 11.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
if (selectedTab == 0) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(InputBg)
.border(
width = 1.dp,
color = if (emailError != null) Color.Red else Color(0xFFE5E7EB),
shape = RoundedCornerShape(12.dp)
)
.padding(horizontal = 12.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.AlternateEmail,
contentDescription = null,
tint = Color.Gray,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
BasicTextField(
value = emailText,
onValueChange = onEmailChange,
modifier = Modifier.weight(1f),
textStyle = LocalTextStyle.current.copy(
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = textDark
),
singleLine = true
)
if (isEmailValid) {
Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = "Valid",
tint = Color(0xFF00C853),
modifier = Modifier.size(20.dp)
)
}
}
if (emailError != null) {
Spacer(modifier = Modifier.height(6.dp))
Text(
text = emailError,
color = Color.Red,
fontSize = 12.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(start = 4.dp)
)
}
} else {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Row(
modifier = Modifier
.clip(RoundedCornerShape(12.dp))
.background(InputBg)
.border(1.dp, Color(0xFFE5E7EB), RoundedCornerShape(12.dp))
.padding(horizontal = 10.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = "🇨🇳", fontSize = 16.sp)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = countryCode,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = textDark
)
Icon(
imageVector = Icons.Default.ArrowDropDown,
contentDescription = "Select Country",
tint = Color.Gray,
modifier = Modifier.size(18.dp)
)
}
Row(
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(12.dp))
.background(InputBg)
.border(
width = 1.dp,
color = if (phoneError != null) Color.Red else Color(0xFFE5E7EB),
shape = RoundedCornerShape(12.dp)
)
.padding(horizontal = 12.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
BasicTextField(
value = phoneText,
onValueChange = onPhoneChange,
modifier = Modifier.weight(1f),
textStyle = LocalTextStyle.current.copy(
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = textDark
),
singleLine = true
)
if (isPhoneValid) {
Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = "Valid",
tint = Color(0xFF00C853),
modifier = Modifier.size(20.dp)
)
}
}
}
if (phoneError != null) {
Spacer(modifier = Modifier.height(6.dp))
Text(
text = phoneError,
color = Color.Red,
fontSize = 12.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(start = 4.dp)
)
}
}
Spacer(modifier = Modifier.height(8.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = Icons.Default.Lock,
contentDescription = null,
tint = Color.Gray,
modifier = Modifier.size(12.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = if (selectedTab == 0)
"We'll send an instant verification link. Never shared publicly."
else
"We'll text you a 6-digit verification code. Standard rates may apply.",
fontSize = 10.sp,
color = Color.Gray
)
}
}
}
}
}
@Composable
fun PasswordComose(
passwordInput: String,
onPasswordChange: (String) -> Unit,
confirmPasswordInput: String,
onConfirmPasswordChange: (String) -> Unit,
passwordError: String?,
confirmPasswordError: String?
) {
var isPasswordVisible by remember { mutableStateOf(false) }
var isConfirmPasswordVisible by remember { mutableStateOf(false) }
// Real-time requirement trackers for the UI indicators
val hasValidLength = passwordInput.length in 8..20
val hasLetterAndNumber = passwordInput.any { it.isLetter() } && passwordInput.any { it.isDigit() }
val hasSpecialChar = passwordInput.any { !it.isLetterOrDigit() }
val passwordsMatch = passwordInput.isNotEmpty() && passwordInput == confirmPasswordInput
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// 1. Header Card
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
border = BorderStroke(1.dp, LightBorderColor)
) {
Column(
modifier = Modifier.padding(10.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Create a secure password",
style = MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
color = textDark
)
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Choose a strong password to protect your VibeCheck account.",
fontSize = 12.sp,
color = Color.Gray,
textAlign = TextAlign.Center
)
}
}
Spacer(modifier = Modifier.height(5.dp))
// 2. Main Input Card
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
border = BorderStroke(1.dp, LightBorderColor)
) {
Column(modifier = Modifier.padding(16.dp)) {
// --- PASSWORD FIELD ---
Text(
text = "PASSWORD",
color = textPink,
fontSize = 11.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(InputBg)
.border(
width = 1.dp,
color = if (passwordError != null) Color.Red else Color(0xFFE5E7EB),
shape = RoundedCornerShape(12.dp)
)
.padding(horizontal = 12.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Lock,
contentDescription = null,
tint = Color.Gray,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(3.dp))
BasicTextField(
value = passwordInput,
onValueChange = onPasswordChange,
modifier = Modifier.weight(1f),
textStyle = LocalTextStyle.current.copy(
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = textDark
),
singleLine = true,
visualTransformation = if (isPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
decorationBox = { innerTextField ->
if (passwordInput.isEmpty()) {
Text(
text = "Enter secure password",
color = Color.LightGray,
fontSize = 14.sp
)
}
innerTextField()
}
)
IconButton(
onClick = { isPasswordVisible = !isPasswordVisible },
modifier = Modifier.size(24.dp)
) {
Icon(
imageVector = if (isPasswordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff,
contentDescription = "Toggle Password Visibility",
tint = Color.Gray,
modifier = Modifier.size(20.dp)
)
}
}
// Password Error Message
if (passwordError != null) {
Text(
text = passwordError,
color = Color.Red,
fontSize = 12.sp,
modifier = Modifier.padding(top = 4.dp, start = 4.dp)
)
}
Spacer(modifier = Modifier.height(5.dp))
// --- CONFIRM PASSWORD FIELD ---
Text(
text = "CONFIRM PASSWORD",
color = textPink,
fontSize = 11.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(InputBg)
.border(
width = 1.dp,
color = if (confirmPasswordError != null) Color.Red else Color(0xFFE5E7EB),
shape = RoundedCornerShape(12.dp)
)
.padding(horizontal = 12.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Lock,
contentDescription = null,
tint = Color.Gray,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
BasicTextField(
value = confirmPasswordInput,
onValueChange = onConfirmPasswordChange,
modifier = Modifier.weight(1f),
textStyle = LocalTextStyle.current.copy(
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = textDark
),
singleLine = true,
visualTransformation = if (isConfirmPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
decorationBox = { innerTextField ->
if (confirmPasswordInput.isEmpty()) {
Text(
text = "Re-enter password",
color = Color.LightGray,
fontSize = 14.sp
)
}
innerTextField()
}
)
IconButton(
onClick = { isConfirmPasswordVisible = !isConfirmPasswordVisible },
modifier = Modifier.size(24.dp)
) {
Icon(
imageVector = if (passwordsMatch) Icons.Default.CheckCircle else if (isConfirmPasswordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff,
contentDescription = "Toggle Password Visibility",
tint = if (passwordsMatch) Color(0xFF00C853) else Color.Gray,
modifier = Modifier.size(20.dp)
)
}
}
// Confirm Password Error Message
if (confirmPasswordError != null) {
Text(
text = confirmPasswordError,
color = Color.Red,
fontSize = 12.sp,
modifier = Modifier.padding(top = 4.dp, start = 4.dp)
)
}
Spacer(modifier = Modifier.height(5.dp))
// --- PASSWORD REQUIREMENTS BOX ---
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(Color(0xFFF8F9FA))
.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
ValidationRequirementRow(
isMet = hasValidLength,
label = "8 to 20 characters long"
)
ValidationRequirementRow(
isMet = hasLetterAndNumber,
label = "Includes letters and numbers"
)
ValidationRequirementRow(
isMet = hasSpecialChar,
label = "Includes at least one special character"
)
}
}
}
}
}
@Composable
private fun ValidationRequirementRow(
isMet: Boolean,
label: String
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
imageVector = if (isMet) Icons.Default.CheckCircle else Icons.Default.RadioButtonUnchecked,
contentDescription = null,
tint = if (isMet) Color(0xFF00C853) else Color.Gray,
modifier = Modifier.size(16.dp)
)
Text(
text = label,
fontSize = 12.sp,
color = if (isMet) textDark else Color.Gray,
fontWeight = if (isMet) FontWeight.Medium else FontWeight.Normal
)
}
}
@Composable
fun AddressCompose(
addressText: String = "",
latitude: Double? = null,
longitude: Double? = null,
onLocationFetched: (Double, Double) -> Unit = { _, _ -> }
) {
val context = LocalContext.current
var isLoading by remember { mutableStateOf(false) }
val locationManager = remember {
context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
}
@SuppressLint("MissingPermission")
fun fetchLocation() {
isLoading = true
val hasGps = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
val hasNetwork = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
if (!hasGps && !hasNetwork) {
isLoading = false
Toast.makeText(context, "请打开手机定位服务", Toast.LENGTH_SHORT).show()
return
}
// 1) Try last known location first (instant)
val lastKnown = listOf(
LocationManager.GPS_PROVIDER,
LocationManager.NETWORK_PROVIDER
).mapNotNull { provider ->
try { locationManager.getLastKnownLocation(provider) } catch (e: SecurityException) { null }
}.maxByOrNull { it.time }
if (lastKnown != null) {
isLoading = false
onLocationFetched(lastKnown.latitude, lastKnown.longitude)
return
}
// 2) No cached fix → request a fresh single update
val provider = if (hasGps) LocationManager.GPS_PROVIDER else LocationManager.NETWORK_PROVIDER
try {
locationManager.requestSingleUpdate(
provider,
{ location ->
isLoading = false
onLocationFetched(location.latitude, location.longitude)
},
Looper.getMainLooper()
)
} catch (e: SecurityException) {
isLoading = false
}
}
val permissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { result ->
if (result.values.any { it }) fetchLocation()
else Toast.makeText(context, "需要定位权限才能获取位置", Toast.LENGTH_SHORT).show()
}
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
border = BorderStroke(1.dp, Color(0xFFE0E0E0))
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.Start
) {
Text(
text = "WHERE ARE YOU LOCATED?",
color = Color(0xFFFF4081),
fontSize = 11.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(12.dp))
Button(
onClick = {
val granted = ContextCompat.checkSelfPermission(
context, Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(
context, Manifest.permission.ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED
if (granted) fetchLocation()
else permissionLauncher.launch(
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
)
},
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
shape = RoundedCornerShape(12.dp),
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFF8F9FA)),
enabled = !isLoading
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
strokeWidth = 2.dp,
color = Color(0xFFFF4081)
)
} else {
Icon(
imageVector = Icons.Default.LocationOn,
contentDescription = null,
tint = Color(0xFFFF4081)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = if (latitude != null) "位置已获取 ✓" else "获取我的位置",
color = Color(0xFF333333)
)
}
}
if (latitude != null && longitude != null) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "📍 ${"%.5f".format(latitude)}, ${"%.5f".format(longitude)}",
fontSize = 13.sp,
color = Color.Gray
)
}
}
}
}
data class GenderPreferenceOption(
val id: String,
val title: String,
val subtitle: String,
val icon: ImageVector
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BirthDayCompose(
selectedDateInMillis: Long? = null,
onDateSelected: (Long) -> Unit = {}
) {
var showDatePickerDialog by rememberSaveable { mutableStateOf(false) }
var selectedMillis by rememberSaveable { mutableStateOf(selectedDateInMillis) }
var ageError by rememberSaveable { mutableStateOf<String?>(null) }
// Date Formatter
val dateFormatter = remember { SimpleDateFormat("MMMM dd, yyyy", Locale.getDefault()) }
// Calculate age from selected milliseconds
val calculatedAge = remember(selectedMillis) {
selectedMillis?.let { millis ->
val dob = Calendar.getInstance().apply { timeInMillis = millis }
val today = Calendar.getInstance()
var age = today.get(Calendar.YEAR) - dob.get(Calendar.YEAR)
if (today.get(Calendar.DAY_OF_YEAR) < dob.get(Calendar.DAY_OF_YEAR)) {
age--
}
age
}
}
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Header Title Card
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
border = BorderStroke(1.dp, LightBorderColor)
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "When is your birthday?",
style = MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
color = textDark
)
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Your profile will display your age, not your birthday.",
fontSize = 12.sp,
color = Color.Gray,
textAlign = TextAlign.Center
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// Input Card with Calendar Trigger Button
Card(
modifier = Modifier.fillMaxWidth()
.clickable { showDatePickerDialog = true } ,
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
border = BorderStroke(
width = 1.dp,
color = if (ageError != null) Color.Red else LightBorderColor
)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "DATE OF BIRTH",
color = if (ageError != null) Color.Red else textPink,
fontSize = 11.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(12.dp))
// Date Display Field & Select Button
Row(
modifier = Modifier
.fillMaxWidth()
.background(InputBg, shape = RoundedCornerShape(12.dp))
.clickable { showDatePickerDialog = true } // Added clickable modifier here
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
){
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.weight(1f)
) {
Icon(
imageVector = Icons.Default.Cake,
contentDescription = null,
tint = Color.Gray,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(10.dp))
Text(
text = selectedMillis?.let { dateFormatter.format(Date(it)) }
?: "Select your birth date",
style = MaterialTheme.typography.bodyMedium.copy(
fontSize = 14.sp,
fontWeight = if (selectedMillis != null) FontWeight.SemiBold else FontWeight.Normal,
color = if (selectedMillis != null) textDark else Color.Gray
)
)
}
// Open Calendar Button
}
// Display Age Badge or Error Message
if (calculatedAge != null) {
Spacer(modifier = Modifier.height(10.dp))
Text(
text = "You are $calculatedAge years old",
color = if (calculatedAge >= 18) textPink else Color.Red,
fontSize = 13.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(start = 4.dp)
)
}
if (ageError != null) {
Spacer(modifier = Modifier.height(6.dp))
Text(
text = ageError!!,
color = Color.Red,
fontSize = 12.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(start = 4.dp)
)
}
}
}
}
// Material 3 Date Picker Dialog
if (showDatePickerDialog) {
val datePickerState = rememberDatePickerState(
initialSelectedDateMillis = selectedMillis ?: System.currentTimeMillis()
)
DatePickerDialog(
onDismissRequest = { showDatePickerDialog = false },
confirmButton = {
TextButton(
onClick = {
datePickerState.selectedDateMillis?.let { millis ->
// Calculate Age Validation
val dob = Calendar.getInstance().apply { timeInMillis = millis }
val today = Calendar.getInstance()
var age = today.get(Calendar.YEAR) - dob.get(Calendar.YEAR)
if (today.get(Calendar.DAY_OF_YEAR) < dob.get(Calendar.DAY_OF_YEAR)) age--
if (age < 18) {
ageError = "You must be at least 18 years old to register."
} else {
ageError = null
selectedMillis = millis
onDateSelected(millis)
}
}
showDatePickerDialog = false
}
) {
Text("OK", color = textPink, fontWeight = FontWeight.Bold)
}
},
dismissButton = {
TextButton(onClick = { showDatePickerDialog = false }) {
Text("Cancel", color = Color.Gray)
}
}
) {
DatePicker(
state = datePickerState,
colors = DatePickerDefaults.colors(
selectedDayContainerColor = PinkGradientStart,
todayDateBorderColor = PurpleGradientEnd
)
)
}
}
}
@Composable
fun GenderCompose(
selectedGender: String = "",
onGenderSelected: (String) -> Unit = {}
) {
val options = listOf("Men", "Women", "Everyone")
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
border = BorderStroke(1.dp, Color(0xFFE0E0E0))
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.Start
) {
Text(
text = "I AM INTERESTED IN",
color = Color(0xFFFF4081),
fontSize = 11.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(12.dp))
options.forEach { option ->
val isSelected = selectedGender.equals(option, ignoreCase = true)
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
.clickable { onGenderSelected(option) },
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
containerColor = if (isSelected) Color(0xFFFF4081).copy(alpha = 0.1f) else Color(0xFFF8F9FA)
),
border = BorderStroke(
width = 1.dp,
color = if (isSelected) Color(0xFFFF4081) else Color.Transparent
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = option,
style = MaterialTheme.typography.bodyMedium.copy(
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
color = if (isSelected) Color(0xFFFF4081) else Color(0xFF212121)
)
)
RadioButton(
selected = isSelected,
onClick = { onGenderSelected(option) },
colors = RadioButtonDefaults.colors(selectedColor = Color(0xFFFF4081))
)
}
}
}
}
}
}
@Composable
fun UniquePulseLoader(
modifier: Modifier = Modifier,
message: String = "Creating your account..."
) {
val infiniteTransition = rememberInfiniteTransition(label = "PulseTransition")
// Animated scales for 3 staggered expanding rings
val scale1 by infiniteTransition.animateFloat(
initialValue = 0.6f, targetValue = 1.4f,
animationSpec = infiniteRepeatable(
animation = tween(1400, easing = LinearOutSlowInEasing),
repeatMode = RepeatMode.Restart
), label = "Ring1"
)
val alpha1 by infiniteTransition.animateFloat(
initialValue = 0.8f, targetValue = 0f,
animationSpec = infiniteRepeatable(
animation = tween(1400, easing = LinearOutSlowInEasing),
repeatMode = RepeatMode.Restart
), label = "Alpha1"
)
val scale2 by infiniteTransition.animateFloat(
initialValue = 0.6f, targetValue = 1.4f,
animationSpec = infiniteRepeatable(
animation = tween(1400, delayMillis = 450, easing = LinearOutSlowInEasing),
repeatMode = RepeatMode.Restart
), label = "Ring2"
)
val alpha2 by infiniteTransition.animateFloat(
initialValue = 0.8f, targetValue = 0f,
animationSpec = infiniteRepeatable(
animation = tween(1400, delayMillis = 450, easing = LinearOutSlowInEasing),
repeatMode = RepeatMode.Restart
), label = "Alpha2"
)
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Box(
modifier = Modifier.size(140.dp),
contentAlignment = Alignment.Center
) {
// Outer Ring 2
Box(
modifier = Modifier
.size(100.dp)
.scale(scale2)
.background(PurpleGradientEnd.copy(alpha = alpha2), CircleShape)
)
// Outer Ring 1
Box(
modifier = Modifier
.size(100.dp)
.scale(scale1)
.background(PinkGradientStart.copy(alpha = alpha1), CircleShape)
)
// Center Gradient Orb with Mascot Icon
Box(
modifier = Modifier
.size(80.dp)
.background(
brush = Brush.linearGradient(listOf(PinkGradientStart, PurpleGradientEnd)),
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.SmartToy,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(42.dp)
)
}
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = message,
fontSize = 15.sp,
fontWeight = FontWeight.SemiBold,
color = PurpleGradientEnd
)
}
}
data class VibeOption(
val title: String,
val value: String, // Value sent to backend API
val description: String,
val icon: ImageVector,
val iconBgColor: Color,
val iconTint: Color
)
@Composable
fun VibeCheckScreen(
selectedRealationStatus: String = "",
onRelationSelected: (String) -> Unit = {}
) {
val vibeOptions = remember {
listOf(
VibeOption(
title = "Serious Relationship",
value = "serious_relationship",
description = "Looking for the one",
icon = Icons.Default.FavoriteBorder,
iconBgColor = Color(0xFFFFE3E3),
iconTint = Color(0xFFC2185B)
),
VibeOption(
title = "Casual Dates",
value = "casual_dates",
description = "Drinks & good times",
icon = Icons.Default.LocalBar,
iconBgColor = Color(0xFFEDE7F6),
iconTint = Color(0xFF673AB7)
),
VibeOption(
title = "New Friends",
value = "new_friends",
description = "Expand my circle",
icon = Icons.Default.People,
iconBgColor = Color(0xFFFFE0B2),
iconTint = Color(0xFFE65100)
),
VibeOption(
title = "Marriage",
value = "marriage",
description = "Seeing where things go",
icon = Icons.Default.CompassCalibration,
iconBgColor = Color(0xFFE0E0E0),
iconTint = Color(0xFF424242)
)
)
}
// ... rest of background and header layout ...
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxWidth()
) {
vibeOptions.forEach { option ->
// Check selection against option.value
val isSelected = option.value == selectedRealationStatus
VibeCard(
option = option,
isSelected = isSelected,
onClick = {
// Send backend value instead of UI title
onRelationSelected(option.value)
}
)
}
}
}
@Composable
fun VibeCard(
option: VibeOption,
isSelected: Boolean,
onClick: () -> Unit
) {
val borderColor = if (isSelected) Color(0xFFB0003A) else Color(0xFFFFE4E6)
val borderWidth = if (isSelected) 2.dp else 1.dp
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(Color.White)
.border(
width = borderWidth,
color = borderColor,
shape = RoundedCornerShape(16.dp)
)
.clickable { onClick() }
.padding(horizontal = 16.dp, vertical = 14.dp)
) {
// Rounded Icon Container
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(44.dp)
.clip(CircleShape)
.background(option.iconBgColor)
) {
Icon(
imageVector = option.icon,
contentDescription = option.title,
tint = option.iconTint,
modifier = Modifier.size(22.dp)
)
}
Spacer(modifier = Modifier.width(16.dp))
// Label and Description
Column {
Text(
text = option.title,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF1E293B)
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = option.description,
fontSize = 12.sp,
color = Color(0xFF64748B)
)
}
}
}
@Preview(showBackground = true, showSystemUi = true)
@Composable
fun VibeCheckScreenPreview() {
VibeCheckScreen()
}
// 1. Email Tab Preview (Normal State)
@Preview(showBackground = true, name = "Step Two - Email Tab")
@Composable
fun StepTwoContentEmailPreview() {
MaterialTheme {
EmailorPhoneCompose(
selectedTab = 0,
onTabSelected = {},
emailText = "john.doe@example.com",
onEmailChange = {},
phoneText = "",
onPhoneChange = {},
countryCode = "+1",
emailError = null,
phoneError = null,
isEmailValid = true,
isPhoneValid = false
)
}
}
@Preview(showBackground = true, name = "Step Two - Phone Tab with Error")
@Composable
fun StepTwoContentPhonePreview() {
MaterialTheme {
EmailorPhoneCompose(
selectedTab = 1,
onTabSelected = {},
emailText = "",
onEmailChange = {},
phoneText = "12345",
onPhoneChange = {},
countryCode = "+1",
emailError = null,
phoneError = "Enter a valid phone number (10-15 digits)",
isEmailValid = false,
isPhoneValid = false
)
}
}