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
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
|
|
)
|
|
}
|
|
}
|