8 changed files with 736 additions and 2001 deletions
File diff suppressed because it is too large
@ -1,938 +0,0 @@ |
|||||
package com.example.dattingapp |
|
||||
|
|
||||
import android.content.Intent |
|
||||
import android.os.Bundle |
|
||||
import androidx.activity.ComponentActivity |
|
||||
import androidx.activity.compose.setContent |
|
||||
import androidx.activity.enableEdgeToEdge |
|
||||
import androidx.compose.animation.* |
|
||||
import androidx.compose.animation.core.tween |
|
||||
import androidx.compose.foundation.background |
|
||||
import androidx.compose.foundation.clickable |
|
||||
import androidx.compose.foundation.layout.* |
|
||||
import androidx.compose.foundation.text.KeyboardOptions |
|
||||
import androidx.compose.material3.* |
|
||||
import androidx.compose.runtime.* |
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList |
|
||||
import androidx.compose.ui.Alignment |
|
||||
import androidx.compose.ui.Modifier |
|
||||
import androidx.compose.ui.draw.clip |
|
||||
import androidx.compose.ui.graphics.Color |
|
||||
import androidx.compose.ui.graphics.vector.ImageVector |
|
||||
import androidx.compose.ui.text.font.FontWeight |
|
||||
import androidx.compose.ui.text.input.KeyboardType |
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation |
|
||||
import androidx.compose.ui.tooling.preview.Preview |
|
||||
import androidx.compose.ui.unit.dp |
|
||||
import androidx.compose.ui.unit.sp |
|
||||
import java.text.SimpleDateFormat |
|
||||
import java.util.* |
|
||||
import androidx.compose.foundation.BorderStroke |
|
||||
import androidx.compose.foundation.shape.CircleShape |
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape |
|
||||
import androidx.compose.material.icons.Icons |
|
||||
import androidx.compose.material.icons.outlined.Explore |
|
||||
import androidx.compose.material.icons.outlined.FavoriteBorder |
|
||||
import androidx.compose.material.icons.outlined.Group |
|
||||
import androidx.compose.material.icons.outlined.LocalBar |
|
||||
import androidx.compose.ui.platform.LocalContext |
|
||||
import androidx.compose.ui.text.style.TextAlign |
|
||||
import com.example.dattingapp.registration.UploadProfileImageActivity |
|
||||
|
|
||||
// Global Email Validation Regex |
|
||||
val EMAIL_PATTERN_REGEX = Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$") |
|
||||
|
|
||||
data class VibeOption( |
|
||||
val title: String, |
|
||||
val subtitle: String, |
|
||||
val icon: ImageVector, |
|
||||
val iconBgColor: Color, |
|
||||
val iconTintColor: Color |
|
||||
) |
|
||||
class RegistrationScreen : ComponentActivity() { |
|
||||
override fun onCreate(savedInstanceState: Bundle?) { |
|
||||
enableEdgeToEdge() |
|
||||
super.onCreate(savedInstanceState) |
|
||||
setContent { |
|
||||
MaterialTheme { |
|
||||
FullscreenRegistrationWizard() |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Composable |
|
||||
fun FullscreenRegistrationWizard() { |
|
||||
// Active step state |
|
||||
var currentStep by remember { mutableIntStateOf(1) } |
|
||||
val totalSteps = 7 |
|
||||
|
|
||||
// Form Field States |
|
||||
var isPhoneSelected by remember { mutableStateOf(false) } |
|
||||
var countryCode by remember { mutableStateOf("+880") } |
|
||||
var contactInput by remember { mutableStateOf("") } |
|
||||
var contactError by remember { mutableStateOf<String?>(null) } |
|
||||
|
|
||||
var passwordInput by remember { mutableStateOf("") } |
|
||||
var confirmPasswordInput by remember { mutableStateOf("") } |
|
||||
var passwordError by remember { mutableStateOf<String?>(null) } |
|
||||
|
|
||||
var nameInput by remember { mutableStateOf("") } |
|
||||
var nameError by remember { mutableStateOf<String?>(null) } |
|
||||
|
|
||||
var dobInput by remember { mutableStateOf("") } |
|
||||
var dobError by remember { mutableStateOf<String?>(null) } |
|
||||
|
|
||||
var genderInput by remember { mutableStateOf("") } |
|
||||
var genderError by remember { mutableStateOf<String?>(null) } |
|
||||
|
|
||||
val selectedInterests = remember { mutableStateListOf<String>() } |
|
||||
var interestsError by remember { mutableStateOf<String?>(null) } |
|
||||
|
|
||||
|
|
||||
var vibeInput by remember { mutableStateOf("") } |
|
||||
var vibeError by remember { mutableStateOf<String?>(null) } |
|
||||
|
|
||||
// Inside your FullscreenRegistrationWizard composable: |
|
||||
val context = LocalContext.current |
|
||||
|
|
||||
// Validation & Navigation |
|
||||
fun validateAndProceed() { |
|
||||
when (currentStep) { |
|
||||
|
|
||||
|
|
||||
|
|
||||
1 -> { |
|
||||
if (nameInput.trim().isBlank()) { |
|
||||
nameError = "Please enter your name" |
|
||||
} else { |
|
||||
nameError = null |
|
||||
currentStep = 2 |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
|
|
||||
2 -> { |
|
||||
if (isPhoneSelected) { |
|
||||
when { |
|
||||
contactInput.isBlank() -> contactError = "Please enter a phone number" |
|
||||
contactInput.length < 7 || contactInput.length > 15 -> contactError = "Enter 7-15 digits" |
|
||||
else -> { contactError = null; currentStep = 2 } |
|
||||
} |
|
||||
} else { |
|
||||
val trimmedEmail = contactInput.trim() |
|
||||
when { |
|
||||
trimmedEmail.isBlank() -> contactError = "Please enter an email address" |
|
||||
!EMAIL_PATTERN_REGEX.matches(trimmedEmail) -> contactError = "Invalid email format" |
|
||||
else -> { contactError = null; currentStep = 4 } |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
3 -> { |
|
||||
|
|
||||
when { |
|
||||
passwordInput.length < 6 -> passwordError = "Password must be at least 6 characters" |
|
||||
passwordInput != confirmPasswordInput -> passwordError = "Passwords do not match" |
|
||||
else -> { passwordError = null; currentStep = 3 } |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
4 -> { |
|
||||
if (dobInput.isBlank()) { |
|
||||
dobError = "Please select your date of birth" |
|
||||
} else { |
|
||||
dobError = null |
|
||||
currentStep = 5 |
|
||||
} |
|
||||
} |
|
||||
5 -> { |
|
||||
if (genderInput.isBlank()) { |
|
||||
genderError = "Please select your gender" |
|
||||
} else { |
|
||||
genderError = null |
|
||||
currentStep = 6 |
|
||||
} |
|
||||
} |
|
||||
6 -> { |
|
||||
if (selectedInterests.size < 2) { |
|
||||
interestsError = "Please select at least 2 interests" |
|
||||
} else { |
|
||||
interestsError = null |
|
||||
// Submit registration payload |
|
||||
currentStep = 7 |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
7 -> { |
|
||||
if (vibeInput.isBlank()) { |
|
||||
vibeError = "Please select your vibe to continue" |
|
||||
} else { |
|
||||
vibeError = null |
|
||||
|
|
||||
// Launch UploadImageActivity with all collected data |
|
||||
val intent = Intent(context, UploadProfileImageActivity::class.java).apply { |
|
||||
putExtra("EXTRA_IS_PHONE", isPhoneSelected) |
|
||||
putExtra("EXTRA_COUNTRY_CODE", countryCode) |
|
||||
putExtra("EXTRA_CONTACT_VALUE", contactInput) |
|
||||
putExtra("EXTRA_PASSWORD", passwordInput) |
|
||||
putExtra("EXTRA_NAME", nameInput) |
|
||||
putExtra("EXTRA_DOB", dobInput) |
|
||||
putExtra("EXTRA_GENDER", genderInput) |
|
||||
putExtra("EXTRA_INTERESTS", ArrayList(selectedInterests)) |
|
||||
putExtra("EXTRA_VIBE", vibeInput) |
|
||||
} |
|
||||
context.startActivity(intent) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
Box( |
|
||||
modifier = Modifier |
|
||||
.fillMaxSize() |
|
||||
.background(Color(0xFFF8F9FA)) |
|
||||
.systemBarsPadding() |
|
||||
) { |
|
||||
Column(modifier = Modifier.fillMaxSize()) { |
|
||||
// TOP BAR & PROGRESS |
|
||||
Column( |
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.padding(horizontal = 20.dp, vertical = 16.dp) |
|
||||
) { |
|
||||
Row( |
|
||||
modifier = Modifier.fillMaxWidth(), |
|
||||
verticalAlignment = Alignment.CenterVertically, |
|
||||
horizontalArrangement = Arrangement.SpaceBetween |
|
||||
) { |
|
||||
if (currentStep > 1) { |
|
||||
TextButton( |
|
||||
onClick = { currentStep-- }, |
|
||||
contentPadding = PaddingValues(0.dp) |
|
||||
) { |
|
||||
Text("Back", color = Color.Gray, fontSize = 16.sp) |
|
||||
} |
|
||||
} else { |
|
||||
Spacer(modifier = Modifier.width(48.dp)) |
|
||||
} |
|
||||
|
|
||||
Text( |
|
||||
text = "Step $currentStep of $totalSteps", |
|
||||
fontWeight = FontWeight.Bold, |
|
||||
fontSize = 16.sp, |
|
||||
color = Color.Black |
|
||||
) |
|
||||
|
|
||||
Spacer(modifier = Modifier.width(48.dp)) |
|
||||
} |
|
||||
|
|
||||
Spacer(modifier = Modifier.height(12.dp)) |
|
||||
|
|
||||
LinearProgressIndicator( |
|
||||
progress = { currentStep / totalSteps.toFloat() }, |
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.height(6.dp), |
|
||||
color = Color.Black, |
|
||||
trackColor = Color(0xFFE0E0E0) |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
// SLIDING CONTENT CONTAINER |
|
||||
Box( |
|
||||
modifier = Modifier |
|
||||
.weight(1f) |
|
||||
.fillMaxWidth() |
|
||||
.padding(horizontal = 24.dp), |
|
||||
contentAlignment = Alignment.Center |
|
||||
) { |
|
||||
AnimatedContent( |
|
||||
targetState = currentStep, |
|
||||
transitionSpec = { |
|
||||
if (targetState > initialState) { |
|
||||
slideIntoContainer( |
|
||||
AnimatedContentTransitionScope.SlideDirection.Left, |
|
||||
animationSpec = tween(400) |
|
||||
) + fadeIn(animationSpec = tween(400)) togetherWith |
|
||||
slideOutOfContainer( |
|
||||
AnimatedContentTransitionScope.SlideDirection.Left, |
|
||||
animationSpec = tween(400) |
|
||||
) + fadeOut(animationSpec = tween(400)) |
|
||||
} else { |
|
||||
slideIntoContainer( |
|
||||
AnimatedContentTransitionScope.SlideDirection.Right, |
|
||||
animationSpec = tween(400) |
|
||||
) + fadeIn(animationSpec = tween(400)) togetherWith |
|
||||
slideOutOfContainer( |
|
||||
AnimatedContentTransitionScope.SlideDirection.Right, |
|
||||
animationSpec = tween(400) |
|
||||
) + fadeOut(animationSpec = tween(400)) |
|
||||
} |
|
||||
}, |
|
||||
label = "WizardSlideAnimation" |
|
||||
) { step -> |
|
||||
Column( |
|
||||
modifier = Modifier.fillMaxWidth(), |
|
||||
horizontalAlignment = Alignment.CenterHorizontally |
|
||||
) { |
|
||||
when (step) { |
|
||||
|
|
||||
1 -> NameInputWidget( |
|
||||
name = nameInput, |
|
||||
onNameChange = { nameInput = it; nameError = null }, |
|
||||
errorMessage = nameError |
|
||||
) |
|
||||
2 -> ContactInputWidget( |
|
||||
isPhoneSelected = isPhoneSelected, |
|
||||
onModeChange = { isPhoneSelected = it; contactError = null }, |
|
||||
countryCode = countryCode, |
|
||||
onCountryCodeChange = { countryCode = it }, |
|
||||
contactValue = contactInput, |
|
||||
onContactChange = { contactInput = it; contactError = null }, |
|
||||
errorMessage = contactError |
|
||||
) |
|
||||
3 -> PasswordInputWidget( |
|
||||
password = passwordInput, |
|
||||
onPasswordChange = { passwordInput = it; passwordError = null }, |
|
||||
confirmPassword = confirmPasswordInput, |
|
||||
onConfirmPasswordChange = { confirmPasswordInput = it; passwordError = null }, |
|
||||
errorMessage = passwordError |
|
||||
) |
|
||||
|
|
||||
4 -> DobInputWidget( |
|
||||
dob = dobInput, |
|
||||
onDobChange = { dobInput = it; dobError = null }, |
|
||||
errorMessage = dobError |
|
||||
) |
|
||||
5 -> GenderInputWidget( |
|
||||
selectedGender = genderInput, |
|
||||
onGenderSelect = { genderInput = it; genderError = null }, |
|
||||
errorMessage = genderError |
|
||||
) |
|
||||
6 -> InterestsInputWidget( |
|
||||
selectedInterests = selectedInterests, |
|
||||
errorMessage = interestsError, |
|
||||
onSelectionChanged = { interestsError = null } |
|
||||
) |
|
||||
7 -> VibeCheckWidget( |
|
||||
selectedVibe = vibeInput, |
|
||||
onVibeSelected = { |
|
||||
vibeInput = it |
|
||||
vibeError = null |
|
||||
}, |
|
||||
errorMessage = vibeError |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// BOTTOM ACTION BUTTON |
|
||||
Box( |
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.padding(20.dp) |
|
||||
) { |
|
||||
Button( |
|
||||
onClick = { validateAndProceed() }, |
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.height(54.dp), |
|
||||
shape = RoundedCornerShape(16.dp), |
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color.Black) |
|
||||
) { |
|
||||
Text( |
|
||||
text = if (currentStep == totalSteps) "Complete Registration" else "Continue", |
|
||||
color = Color.White, |
|
||||
fontSize = 16.sp, |
|
||||
fontWeight = FontWeight.Bold |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// ==================== STEP WIDGET COMPOSABLES ==================== |
|
||||
|
|
||||
@Composable |
|
||||
fun ContactInputWidget( |
|
||||
isPhoneSelected: Boolean, |
|
||||
onModeChange: (Boolean) -> Unit, |
|
||||
countryCode: String, |
|
||||
onCountryCodeChange: (String) -> Unit, |
|
||||
contactValue: String, |
|
||||
onContactChange: (String) -> Unit, |
|
||||
errorMessage: String? |
|
||||
) { |
|
||||
Column( |
|
||||
modifier = Modifier.fillMaxWidth(), |
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp) |
|
||||
) { |
|
||||
Text( |
|
||||
text = "ENTER YOUR CONTACT INFORMATION", |
|
||||
fontSize = 11.sp, |
|
||||
fontWeight = FontWeight.Bold, |
|
||||
color = Color.Gray |
|
||||
) |
|
||||
|
|
||||
Row( |
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.height(48.dp) |
|
||||
.clip(RoundedCornerShape(24.dp)) |
|
||||
.background(Color(0xFFEFEFEF)) |
|
||||
.padding(4.dp), |
|
||||
verticalAlignment = Alignment.CenterVertically |
|
||||
) { |
|
||||
Box( |
|
||||
modifier = Modifier |
|
||||
.weight(1f) |
|
||||
.fillMaxHeight() |
|
||||
.clip(RoundedCornerShape(20.dp)) |
|
||||
.background(if (isPhoneSelected) Color.Black else Color.Transparent) |
|
||||
.clickable { onModeChange(true) }, |
|
||||
contentAlignment = Alignment.Center |
|
||||
) { |
|
||||
Text( |
|
||||
text = "Phone", |
|
||||
fontWeight = FontWeight.Bold, |
|
||||
fontSize = 14.sp, |
|
||||
color = if (isPhoneSelected) Color.White else Color.DarkGray |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
Box( |
|
||||
modifier = Modifier |
|
||||
.weight(1f) |
|
||||
.fillMaxHeight() |
|
||||
.clip(RoundedCornerShape(20.dp)) |
|
||||
.background(if (!isPhoneSelected) Color.Black else Color.Transparent) |
|
||||
.clickable { onModeChange(false) }, |
|
||||
contentAlignment = Alignment.Center |
|
||||
) { |
|
||||
Text( |
|
||||
text = "Email", |
|
||||
fontWeight = FontWeight.Bold, |
|
||||
fontSize = 14.sp, |
|
||||
color = if (!isPhoneSelected) Color.White else Color.DarkGray |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
if (isPhoneSelected) { |
|
||||
Row( |
|
||||
modifier = Modifier.fillMaxWidth(), |
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp) |
|
||||
) { |
|
||||
OutlinedTextField( |
|
||||
value = countryCode, |
|
||||
onValueChange = { if (it.length <= 4) onCountryCodeChange(it) }, |
|
||||
placeholder = { Text("+880") }, |
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone), |
|
||||
singleLine = true, |
|
||||
isError = errorMessage != null, |
|
||||
modifier = Modifier.width(90.dp), |
|
||||
shape = RoundedCornerShape(12.dp), |
|
||||
colors = OutlinedTextFieldDefaults.colors( |
|
||||
focusedContainerColor = Color.White, |
|
||||
unfocusedContainerColor = Color.White |
|
||||
) |
|
||||
) |
|
||||
|
|
||||
OutlinedTextField( |
|
||||
value = contactValue, |
|
||||
onValueChange = { if (it.length <= 15) onContactChange(it) }, |
|
||||
placeholder = { Text("Phone number") }, |
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), |
|
||||
singleLine = true, |
|
||||
isError = errorMessage != null, |
|
||||
modifier = Modifier.weight(1f), |
|
||||
shape = RoundedCornerShape(12.dp), |
|
||||
colors = OutlinedTextFieldDefaults.colors( |
|
||||
focusedContainerColor = Color.White, |
|
||||
unfocusedContainerColor = Color.White |
|
||||
) |
|
||||
) |
|
||||
} |
|
||||
} else { |
|
||||
OutlinedTextField( |
|
||||
value = contactValue, |
|
||||
onValueChange = onContactChange, |
|
||||
placeholder = { Text("Enter email address") }, |
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email), |
|
||||
singleLine = true, |
|
||||
isError = errorMessage != null, |
|
||||
modifier = Modifier.fillMaxWidth(), |
|
||||
shape = RoundedCornerShape(12.dp), |
|
||||
colors = OutlinedTextFieldDefaults.colors( |
|
||||
focusedContainerColor = Color.White, |
|
||||
unfocusedContainerColor = Color.White |
|
||||
) |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
if (errorMessage != null) { |
|
||||
Text( |
|
||||
text = errorMessage, |
|
||||
color = MaterialTheme.colorScheme.error, |
|
||||
fontSize = 12.sp, |
|
||||
fontWeight = FontWeight.Medium, |
|
||||
modifier = Modifier.padding(start = 4.dp) |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Composable |
|
||||
fun PasswordInputWidget( |
|
||||
password: String, |
|
||||
onPasswordChange: (String) -> Unit, |
|
||||
confirmPassword: String, |
|
||||
onConfirmPasswordChange: (String) -> Unit, |
|
||||
errorMessage: String? |
|
||||
) { |
|
||||
Column( |
|
||||
modifier = Modifier.fillMaxWidth(), |
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp) |
|
||||
) { |
|
||||
Text( |
|
||||
text = "CREATE A PASSWORD", |
|
||||
fontSize = 11.sp, |
|
||||
fontWeight = FontWeight.Bold, |
|
||||
color = Color.Gray |
|
||||
) |
|
||||
|
|
||||
OutlinedTextField( |
|
||||
value = password, |
|
||||
onValueChange = onPasswordChange, |
|
||||
placeholder = { Text("Enter password") }, |
|
||||
visualTransformation = PasswordVisualTransformation(), |
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), |
|
||||
singleLine = true, |
|
||||
isError = errorMessage != null, |
|
||||
modifier = Modifier.fillMaxWidth(), |
|
||||
shape = RoundedCornerShape(12.dp), |
|
||||
colors = OutlinedTextFieldDefaults.colors( |
|
||||
focusedContainerColor = Color.White, |
|
||||
unfocusedContainerColor = Color.White |
|
||||
) |
|
||||
) |
|
||||
|
|
||||
OutlinedTextField( |
|
||||
value = confirmPassword, |
|
||||
onValueChange = onConfirmPasswordChange, |
|
||||
placeholder = { Text("Confirm password") }, |
|
||||
visualTransformation = PasswordVisualTransformation(), |
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), |
|
||||
singleLine = true, |
|
||||
isError = errorMessage != null, |
|
||||
modifier = Modifier.fillMaxWidth(), |
|
||||
shape = RoundedCornerShape(12.dp), |
|
||||
colors = OutlinedTextFieldDefaults.colors( |
|
||||
focusedContainerColor = Color.White, |
|
||||
unfocusedContainerColor = Color.White |
|
||||
) |
|
||||
) |
|
||||
|
|
||||
if (errorMessage != null) { |
|
||||
Text( |
|
||||
text = errorMessage, |
|
||||
color = MaterialTheme.colorScheme.error, |
|
||||
fontSize = 12.sp, |
|
||||
fontWeight = FontWeight.Medium, |
|
||||
modifier = Modifier.padding(start = 4.dp) |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Composable |
|
||||
fun NameInputWidget( |
|
||||
name: String, |
|
||||
onNameChange: (String) -> Unit, |
|
||||
errorMessage: String? |
|
||||
) { |
|
||||
Column( |
|
||||
modifier = Modifier.fillMaxWidth(), |
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp) |
|
||||
) { |
|
||||
Text( |
|
||||
text = "WHAT IS YOUR NAME?", |
|
||||
fontSize = 11.sp, |
|
||||
fontWeight = FontWeight.Bold, |
|
||||
color = Color.Gray |
|
||||
) |
|
||||
|
|
||||
OutlinedTextField( |
|
||||
value = name, |
|
||||
onValueChange = onNameChange, |
|
||||
placeholder = { Text("Enter your full name") }, |
|
||||
singleLine = true, |
|
||||
isError = errorMessage != null, |
|
||||
modifier = Modifier.fillMaxWidth(), |
|
||||
shape = RoundedCornerShape(12.dp), |
|
||||
colors = OutlinedTextFieldDefaults.colors( |
|
||||
focusedContainerColor = Color.White, |
|
||||
unfocusedContainerColor = Color.White |
|
||||
) |
|
||||
) |
|
||||
|
|
||||
if (errorMessage != null) { |
|
||||
Text( |
|
||||
text = errorMessage, |
|
||||
color = MaterialTheme.colorScheme.error, |
|
||||
fontSize = 12.sp, |
|
||||
fontWeight = FontWeight.Medium, |
|
||||
modifier = Modifier.padding(start = 4.dp) |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@OptIn(ExperimentalMaterial3Api::class) |
|
||||
@Composable |
|
||||
fun DobInputWidget( |
|
||||
dob: String, |
|
||||
onDobChange: (String) -> Unit, |
|
||||
errorMessage: String? |
|
||||
) { |
|
||||
var showDatePicker by remember { mutableStateOf(false) } |
|
||||
val datePickerState = rememberDatePickerState() |
|
||||
|
|
||||
if (showDatePicker) { |
|
||||
DatePickerDialog( |
|
||||
onDismissRequest = { showDatePicker = false }, |
|
||||
confirmButton = { |
|
||||
TextButton(onClick = { |
|
||||
datePickerState.selectedDateMillis?.let { millis -> |
|
||||
val formatter = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) |
|
||||
onDobChange(formatter.format(Date(millis))) |
|
||||
} |
|
||||
showDatePicker = false |
|
||||
}) { |
|
||||
Text("OK") |
|
||||
} |
|
||||
}, |
|
||||
dismissButton = { |
|
||||
TextButton(onClick = { showDatePicker = false }) { |
|
||||
Text("Cancel") |
|
||||
} |
|
||||
} |
|
||||
) { |
|
||||
DatePicker(state = datePickerState) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
Column( |
|
||||
modifier = Modifier.fillMaxWidth(), |
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp) |
|
||||
) { |
|
||||
Text( |
|
||||
text = "YOUR DATE OF BIRTH", |
|
||||
fontSize = 11.sp, |
|
||||
fontWeight = FontWeight.Bold, |
|
||||
color = Color.Gray |
|
||||
) |
|
||||
|
|
||||
Box(modifier = Modifier.fillMaxWidth()) { |
|
||||
OutlinedTextField( |
|
||||
value = dob, |
|
||||
onValueChange = {}, |
|
||||
readOnly = true, |
|
||||
placeholder = { Text("DD/MM/YYYY") }, |
|
||||
isError = errorMessage != null, |
|
||||
modifier = Modifier.fillMaxWidth(), |
|
||||
shape = RoundedCornerShape(12.dp), |
|
||||
colors = OutlinedTextFieldDefaults.colors( |
|
||||
focusedContainerColor = Color.White, |
|
||||
unfocusedContainerColor = Color.White |
|
||||
) |
|
||||
) |
|
||||
Box( |
|
||||
modifier = Modifier |
|
||||
.matchParentSize() |
|
||||
.clickable { showDatePicker = true } |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
if (errorMessage != null) { |
|
||||
Text( |
|
||||
text = errorMessage, |
|
||||
color = MaterialTheme.colorScheme.error, |
|
||||
fontSize = 12.sp, |
|
||||
fontWeight = FontWeight.Medium, |
|
||||
modifier = Modifier.padding(start = 4.dp) |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Composable |
|
||||
fun GenderInputWidget( |
|
||||
selectedGender: String, |
|
||||
onGenderSelect: (String) -> Unit, |
|
||||
errorMessage: String? |
|
||||
) { |
|
||||
val options = listOf("Male", "Female", "Other") |
|
||||
|
|
||||
Column( |
|
||||
modifier = Modifier.fillMaxWidth(), |
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp) |
|
||||
) { |
|
||||
Text( |
|
||||
text = "SELECT YOUR GENDER", |
|
||||
fontSize = 11.sp, |
|
||||
fontWeight = FontWeight.Bold, |
|
||||
color = Color.Gray |
|
||||
) |
|
||||
|
|
||||
options.forEach { gender -> |
|
||||
val isSelected = selectedGender == gender |
|
||||
Card( |
|
||||
shape = RoundedCornerShape(12.dp), |
|
||||
colors = CardDefaults.cardColors( |
|
||||
containerColor = if (isSelected) Color.Black else Color.White |
|
||||
), |
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.clickable { onGenderSelect(gender) } |
|
||||
) { |
|
||||
Box( |
|
||||
modifier = Modifier.padding(16.dp), |
|
||||
contentAlignment = Alignment.CenterStart |
|
||||
) { |
|
||||
Text( |
|
||||
text = gender, |
|
||||
color = if (isSelected) Color.White else Color.Black, |
|
||||
fontWeight = FontWeight.SemiBold, |
|
||||
fontSize = 15.sp |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
if (errorMessage != null) { |
|
||||
Text( |
|
||||
text = errorMessage, |
|
||||
color = MaterialTheme.colorScheme.error, |
|
||||
fontSize = 12.sp, |
|
||||
fontWeight = FontWeight.Medium, |
|
||||
modifier = Modifier.padding(start = 4.dp) |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@OptIn(ExperimentalLayoutApi::class) |
|
||||
@Composable |
|
||||
fun InterestsInputWidget( |
|
||||
selectedInterests: SnapshotStateList<String>, |
|
||||
errorMessage: String? = null, |
|
||||
onSelectionChanged: () -> Unit = {} |
|
||||
) { |
|
||||
val allInterests = listOf("Photography", "Dogs", "Travel", "Music", "Gaming", "Fitness", "Cooking") |
|
||||
|
|
||||
Card( |
|
||||
shape = RoundedCornerShape(16.dp), |
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White), |
|
||||
modifier = Modifier.fillMaxWidth() |
|
||||
) { |
|
||||
Column(modifier = Modifier.padding(16.dp)) { |
|
||||
Text( |
|
||||
text = "INTERESTS (SELECT AT LEAST 2)", |
|
||||
fontSize = 11.sp, |
|
||||
fontWeight = FontWeight.Bold, |
|
||||
color = Color.Gray |
|
||||
) |
|
||||
Spacer(modifier = Modifier.height(12.dp)) |
|
||||
|
|
||||
FlowRow( |
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp), |
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp) |
|
||||
) { |
|
||||
allInterests.forEach { interest -> |
|
||||
val isSelected = selectedInterests.contains(interest) |
|
||||
FilterChip( |
|
||||
selected = isSelected, |
|
||||
onClick = { |
|
||||
if (isSelected) selectedInterests.remove(interest) |
|
||||
else selectedInterests.add(interest) |
|
||||
onSelectionChanged() |
|
||||
}, |
|
||||
label = { Text(interest) } |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
if (errorMessage != null) { |
|
||||
Spacer(modifier = Modifier.height(8.dp)) |
|
||||
Text( |
|
||||
text = errorMessage, |
|
||||
color = MaterialTheme.colorScheme.error, |
|
||||
fontSize = 12.sp, |
|
||||
fontWeight = FontWeight.Medium |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
@Composable |
|
||||
fun VibeCheckWidget( |
|
||||
selectedVibe: String, |
|
||||
onVibeSelected: (String) -> Unit, |
|
||||
errorMessage: String? = null |
|
||||
) { |
|
||||
val options = listOf( |
|
||||
VibeOption("Serious Relationship", "Looking for the one", Icons.Outlined.FavoriteBorder, Color(0xFFFFEAEA), Color(0xFFE91E63)), |
|
||||
VibeOption("Casual Dates", "Drinks & good times", Icons.Outlined.LocalBar, Color(0xFFEDE7F6), Color(0xFF673AB7)), |
|
||||
VibeOption("New Friends", "Expand my circle", Icons.Outlined.Group, Color(0xFFFFECB3), Color(0xFFFF8F00)), |
|
||||
VibeOption("Marraige", "Seeing where things go", Icons.Outlined.Explore, Color(0xFFE0F2F1), Color(0xFF00897B)) |
|
||||
) |
|
||||
|
|
||||
Column( |
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.padding(horizontal = 8.dp), |
|
||||
horizontalAlignment = Alignment.CenterHorizontally |
|
||||
) { |
|
||||
// App Title / Brand Header |
|
||||
Text( |
|
||||
text = "VibeCheck", |
|
||||
fontSize = 24.sp, |
|
||||
fontWeight = FontWeight.Bold, |
|
||||
color = Color(0xFFC2185B) |
|
||||
) |
|
||||
|
|
||||
Spacer(modifier = Modifier.height(8.dp)) |
|
||||
|
|
||||
// Three Indicator Bars |
|
||||
Row( |
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp), |
|
||||
verticalAlignment = Alignment.CenterVertically |
|
||||
) { |
|
||||
Box(modifier = Modifier.width(20.dp).height(4.dp).background(Color(0xFFE0E0E0), CircleShape)) |
|
||||
Box(modifier = Modifier.width(20.dp).height(4.dp).background(Color(0xFFC2185B), CircleShape)) |
|
||||
Box(modifier = Modifier.width(20.dp).height(4.dp).background(Color(0xFFE0E0E0), CircleShape)) |
|
||||
} |
|
||||
|
|
||||
Spacer(modifier = Modifier.height(20.dp)) |
|
||||
|
|
||||
// Title & Description |
|
||||
Text( |
|
||||
text = "What's your vibe?", |
|
||||
fontSize = 20.sp, |
|
||||
fontWeight = FontWeight.Bold, |
|
||||
color = Color.Black |
|
||||
) |
|
||||
|
|
||||
Spacer(modifier = Modifier.height(8.dp)) |
|
||||
|
|
||||
Text( |
|
||||
text = "Select your main goal right now to help us find the right connections.", |
|
||||
fontSize = 13.sp, |
|
||||
color = Color.Gray, |
|
||||
textAlign = TextAlign.Center, |
|
||||
modifier = Modifier.padding(horizontal = 16.dp) |
|
||||
) |
|
||||
|
|
||||
Spacer(modifier = Modifier.height(24.dp)) |
|
||||
|
|
||||
// Vibe Cards List |
|
||||
Column( |
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp), |
|
||||
modifier = Modifier.fillMaxWidth() |
|
||||
) { |
|
||||
options.forEach { option -> |
|
||||
val isSelected = selectedVibe == option.title |
|
||||
|
|
||||
Card( |
|
||||
shape = RoundedCornerShape(16.dp), |
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White), |
|
||||
border = BorderStroke( |
|
||||
width = if (isSelected) 2.dp else 1.dp, |
|
||||
color = if (isSelected) Color(0xFFC2185B) else Color(0xFFEEEEEE) |
|
||||
), |
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.clickable { onVibeSelected(option.title) } |
|
||||
) { |
|
||||
Row( |
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.padding(16.dp), |
|
||||
verticalAlignment = Alignment.CenterVertically |
|
||||
) { |
|
||||
// Round Icon Container |
|
||||
Box( |
|
||||
modifier = Modifier |
|
||||
.size(42.dp) |
|
||||
.background(option.iconBgColor, CircleShape), |
|
||||
contentAlignment = Alignment.Center |
|
||||
) { |
|
||||
Icon( |
|
||||
imageVector = option.icon, |
|
||||
contentDescription = option.title, |
|
||||
tint = option.iconTintColor, |
|
||||
modifier = Modifier.size(22.dp) |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
Spacer(modifier = Modifier.width(16.dp)) |
|
||||
|
|
||||
// Text Labels |
|
||||
Column { |
|
||||
Text( |
|
||||
text = option.title, |
|
||||
fontSize = 15.sp, |
|
||||
fontWeight = FontWeight.Bold, |
|
||||
color = Color.Black |
|
||||
) |
|
||||
Text( |
|
||||
text = option.subtitle, |
|
||||
fontSize = 12.sp, |
|
||||
color = Color.Gray |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
if (errorMessage != null) { |
|
||||
Spacer(modifier = Modifier.height(12.dp)) |
|
||||
Text( |
|
||||
text = errorMessage, |
|
||||
color = MaterialTheme.colorScheme.error, |
|
||||
fontSize = 12.sp, |
|
||||
fontWeight = FontWeight.Medium |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
Spacer(modifier = Modifier.height(16.dp)) |
|
||||
|
|
||||
Text( |
|
||||
text = "You can always change this later in your profile.", |
|
||||
fontSize = 11.sp, |
|
||||
color = Color.Gray |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Preview( |
|
||||
name = "Light Mode - Full Wizard", |
|
||||
showBackground = true, |
|
||||
showSystemUi = true |
|
||||
) |
|
||||
@Composable |
|
||||
fun FullscreenRegistrationWizardPreview() { |
|
||||
Surface(modifier = Modifier.fillMaxSize()) { |
|
||||
FullscreenRegistrationWizard() |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,692 @@ |
|||||
|
package com.example.dattingapp.homes |
||||
|
|
||||
|
|
||||
|
import android.net.Uri |
||||
|
import androidx.activity.compose.rememberLauncherForActivityResult |
||||
|
import androidx.activity.result.PickVisualMediaRequest |
||||
|
import androidx.activity.result.contract.ActivityResultContracts |
||||
|
import androidx.compose.foundation.Image |
||||
|
import androidx.compose.foundation.background |
||||
|
import androidx.compose.foundation.border |
||||
|
import androidx.compose.foundation.clickable |
||||
|
import androidx.compose.foundation.layout.* |
||||
|
import androidx.compose.foundation.lazy.LazyRow |
||||
|
import androidx.compose.foundation.lazy.grid.GridCells |
||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid |
||||
|
import androidx.compose.foundation.lazy.itemsIndexed |
||||
|
import androidx.compose.foundation.rememberScrollState |
||||
|
import androidx.compose.foundation.shape.CircleShape |
||||
|
import androidx.compose.foundation.shape.RoundedCornerShape |
||||
|
import androidx.compose.foundation.verticalScroll |
||||
|
import androidx.compose.material.icons.Icons |
||||
|
import androidx.compose.material.icons.filled.* |
||||
|
import androidx.compose.material.icons.outlined.* |
||||
|
import androidx.compose.material3.* |
||||
|
import androidx.compose.runtime.* |
||||
|
import androidx.compose.ui.Alignment |
||||
|
import androidx.compose.ui.Modifier |
||||
|
import androidx.compose.ui.draw.clip |
||||
|
import androidx.compose.ui.graphics.Brush |
||||
|
import androidx.compose.ui.graphics.Color |
||||
|
import androidx.compose.ui.graphics.vector.ImageVector |
||||
|
import androidx.compose.ui.layout.ContentScale |
||||
|
import androidx.compose.ui.platform.LocalContext |
||||
|
import androidx.compose.ui.text.SpanStyle |
||||
|
import androidx.compose.ui.text.buildAnnotatedString |
||||
|
import androidx.compose.ui.text.font.FontWeight |
||||
|
import androidx.compose.ui.text.withStyle |
||||
|
import androidx.compose.ui.tooling.preview.Preview |
||||
|
import androidx.compose.ui.unit.dp |
||||
|
import androidx.compose.ui.unit.sp |
||||
|
import androidx.compose.ui.viewinterop.AndroidView |
||||
|
import androidx.core.content.FileProvider |
||||
|
import androidx.media3.common.MediaItem |
||||
|
import androidx.media3.exoplayer.ExoPlayer |
||||
|
import androidx.media3.ui.PlayerView |
||||
|
import coil.compose.rememberAsyncImagePainter |
||||
|
import com.example.dattingapp.homes.networkandapi.VibeFeeling |
||||
|
import com.example.dattingapp.homes.networkandapi.vibeFeelingsList |
||||
|
import java.io.File |
||||
|
import com.example.dattingapp.ui.theme.LightPinkBg |
||||
|
import com.example.dattingapp.ui.theme.LightPurpleBg |
||||
|
import com.example.dattingapp.ui.theme.DarkOverlay |
||||
|
import com.example.dattingapp.ui.theme.LightBlueBg |
||||
|
|
||||
|
import androidx.compose.foundation.lazy.grid.items |
||||
|
|
||||
|
// Sealed class representing selectable media types (Photo or Video) |
||||
|
sealed class MediaContentItem(val uri: Uri) { |
||||
|
class Photo(uri: Uri) : MediaContentItem(uri) |
||||
|
class Video(uri: Uri) : MediaContentItem(uri) |
||||
|
} |
||||
|
|
||||
|
@OptIn(ExperimentalMaterial3Api::class) |
||||
|
@Composable |
||||
|
fun CreateVibePostScreen( |
||||
|
onCloseClick: () -> Unit = {}, |
||||
|
onShareClick: () -> Unit = {} |
||||
|
) { |
||||
|
val context = LocalContext.current |
||||
|
var textState by remember { mutableStateOf("") } |
||||
|
val maxChar = 500 |
||||
|
|
||||
|
// Holds selected media items (photos, videos, or both) |
||||
|
val selectedMediaList = remember { mutableStateListOf<MediaContentItem>() } |
||||
|
|
||||
|
var tempCameraImageUri by remember { mutableStateOf<Uri?>(null) } |
||||
|
var tempCameraVideoUri by remember { mutableStateOf<Uri?>(null) } |
||||
|
var showImagePickerDialog by remember { mutableStateOf(false) } |
||||
|
|
||||
|
var showFeelingSheet by remember { mutableStateOf(false) } |
||||
|
var selectedFeeling by remember { mutableStateOf<VibeFeeling?>(null) } |
||||
|
|
||||
|
// Launcher for Multi-Media Gallery Picker (Supports both Photos and Videos simultaneously) |
||||
|
val galleryLauncher = rememberLauncherForActivityResult( |
||||
|
contract = ActivityResultContracts.PickMultipleVisualMedia() |
||||
|
) { uris: List<Uri> -> |
||||
|
uris.forEach { uri -> |
||||
|
val mimeType = context.contentResolver.getType(uri) |
||||
|
if (mimeType?.startsWith("video") == true) { |
||||
|
selectedMediaList.add(MediaContentItem.Video(uri)) |
||||
|
} else { |
||||
|
selectedMediaList.add(MediaContentItem.Photo(uri)) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Temporary file helper for Camera captures |
||||
|
fun createTempFileUri(extension: String, prefix: String): Uri { |
||||
|
val tempFile = File.createTempFile(prefix, extension, context.cacheDir).apply { |
||||
|
createNewFile() |
||||
|
deleteOnExit() |
||||
|
} |
||||
|
return FileProvider.getUriForFile( |
||||
|
context, |
||||
|
"${context.packageName}.provider", |
||||
|
tempFile |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// Camera Launchers for taking photos or recording videos |
||||
|
val cameraPhotoLauncher = rememberLauncherForActivityResult( |
||||
|
contract = ActivityResultContracts.TakePicture() |
||||
|
) { success: Boolean -> |
||||
|
if (success && tempCameraImageUri != null) { |
||||
|
selectedMediaList.add(MediaContentItem.Photo(tempCameraImageUri!!)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
val cameraVideoLauncher = rememberLauncherForActivityResult( |
||||
|
contract = ActivityResultContracts.CaptureVideo() |
||||
|
) { success: Boolean -> |
||||
|
if (success && tempCameraVideoUri != null) { |
||||
|
selectedMediaList.add(MediaContentItem.Video(tempCameraVideoUri!!)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Scaffold( |
||||
|
bottomBar = { |
||||
|
Row( |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.background(Color.White) |
||||
|
.padding(horizontal = 16.dp, vertical = 12.dp), |
||||
|
verticalAlignment = Alignment.CenterVertically, |
||||
|
horizontalArrangement = Arrangement.SpaceBetween |
||||
|
) { |
||||
|
// 1. Scrollable option chips on the left |
||||
|
LazyRow( |
||||
|
modifier = Modifier.weight(1f), |
||||
|
horizontalArrangement = Arrangement.spacedBy(10.dp), |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
item { |
||||
|
VibeOptionChip( |
||||
|
icon = Icons.Outlined.PhotoLibrary, |
||||
|
backgroundColor = LightPinkBg, |
||||
|
contentColor = Color(0xFFE91E63), |
||||
|
onClick = { showImagePickerDialog = true } |
||||
|
) |
||||
|
} |
||||
|
item { |
||||
|
VibeOptionChip( |
||||
|
icon = Icons.Outlined.MyLocation, |
||||
|
backgroundColor = LightBlueBg, |
||||
|
contentColor = Color(0xFF1E88E5) |
||||
|
) |
||||
|
} |
||||
|
item { |
||||
|
VibeOptionChip( |
||||
|
icon = Icons.Outlined.Mood, |
||||
|
backgroundColor = LightPinkBg, |
||||
|
contentColor = Color(0xFFE91E63), |
||||
|
onClick = { showFeelingSheet = true } // Opens bottom sheet |
||||
|
) |
||||
|
} |
||||
|
item { |
||||
|
VibeOptionChip( |
||||
|
icon = Icons.Outlined.MusicNote, |
||||
|
backgroundColor = LightPurpleBg, |
||||
|
contentColor = Color(0xFF7B1FA2) |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Spacer(modifier = Modifier.width(8.dp)) |
||||
|
|
||||
|
// 2. Share button on the right |
||||
|
Button( |
||||
|
onClick = onShareClick, |
||||
|
modifier = Modifier |
||||
|
.wrapContentWidth() |
||||
|
.height(40.dp), |
||||
|
shape = RoundedCornerShape(12.dp), |
||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent), |
||||
|
contentPadding = PaddingValues(0.dp) |
||||
|
) { |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.background( |
||||
|
brush = Brush.horizontalGradient( |
||||
|
colors = listOf(Color(0xFFC2185B), Color(0xFF7B1FA2)) |
||||
|
), |
||||
|
shape = RoundedCornerShape(12.dp) |
||||
|
) |
||||
|
.padding(horizontal = 12.dp, vertical = 8.dp), |
||||
|
contentAlignment = Alignment.Center |
||||
|
) { |
||||
|
Row(verticalAlignment = Alignment.CenterVertically) { |
||||
|
Text( |
||||
|
text = "Share", |
||||
|
color = Color.White, |
||||
|
fontWeight = FontWeight.Bold, |
||||
|
fontSize = 14.sp |
||||
|
) |
||||
|
Spacer(modifier = Modifier.width(6.dp)) |
||||
|
Icon( |
||||
|
imageVector = Icons.Default.Send, |
||||
|
contentDescription = null, |
||||
|
tint = Color.White, |
||||
|
modifier = Modifier.size(16.dp) |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
containerColor = Color.White |
||||
|
) { innerPadding -> |
||||
|
|
||||
|
Column( |
||||
|
modifier = Modifier |
||||
|
.fillMaxSize() |
||||
|
.padding(innerPadding) |
||||
|
.padding(horizontal = 16.dp) |
||||
|
.verticalScroll(rememberScrollState()) |
||||
|
|
||||
|
) { |
||||
|
// User Profile Header |
||||
|
Row( |
||||
|
verticalAlignment = Alignment.CenterVertically, |
||||
|
modifier = Modifier.fillMaxWidth() |
||||
|
) { |
||||
|
Box(modifier = Modifier.size(48.dp)) { |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.size(48.dp) |
||||
|
.clip(CircleShape) |
||||
|
.background(Color.Gray) |
||||
|
) |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.size(12.dp) |
||||
|
.clip(CircleShape) |
||||
|
.background(Color(0xFFE91E63)) |
||||
|
.align(Alignment.BottomEnd) |
||||
|
.border(2.dp, Color.White, CircleShape) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
Spacer(modifier = Modifier.width(12.dp)) |
||||
|
|
||||
|
Column(modifier = Modifier.weight(1f)) { |
||||
|
Row(verticalAlignment = Alignment.CenterVertically) { |
||||
|
Text( |
||||
|
text = buildAnnotatedString { |
||||
|
// "Alex Rivera " - Bold |
||||
|
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { |
||||
|
append("Alex Rivera ") |
||||
|
} |
||||
|
|
||||
|
// "- feeling " - Normal weight (also fixed "felling" to "feeling") |
||||
|
selectedFeeling?.let { feeling -> |
||||
|
withStyle(style = SpanStyle(fontWeight = FontWeight.Normal)) { |
||||
|
append(" — feeling ") |
||||
|
} |
||||
|
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { |
||||
|
// Change feeling.name to feeling.label (or feeling.title depending on your data class) |
||||
|
append("${feeling.name} ${feeling.emoji}") |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
fontSize = 16.sp |
||||
|
) |
||||
|
Spacer(modifier = Modifier.width(4.dp)) |
||||
|
|
||||
|
//this is for vibe |
||||
|
|
||||
|
} |
||||
|
|
||||
|
Spacer(modifier = Modifier.height(4.dp)) |
||||
|
|
||||
|
Row( |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth(), |
||||
|
horizontalArrangement = Arrangement.SpaceBetween |
||||
|
) { |
||||
|
Row(verticalAlignment = Alignment.CenterVertically) { |
||||
|
Icon( |
||||
|
imageVector = Icons.Outlined.LocationOn, |
||||
|
contentDescription = null, |
||||
|
tint = Color(0xFFE91E63), |
||||
|
modifier = Modifier.size(20.dp) |
||||
|
) |
||||
|
Spacer(modifier = Modifier.width(8.dp)) |
||||
|
Text( |
||||
|
text = "Blue Bottle Coffee, Soho NYC", |
||||
|
fontWeight = FontWeight.Medium, |
||||
|
fontSize = 13.sp |
||||
|
) |
||||
|
} |
||||
|
Icon( |
||||
|
imageVector = Icons.Default.Close, |
||||
|
contentDescription = "Remove location", |
||||
|
tint = Color.Gray, |
||||
|
modifier = Modifier.size(16.dp) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Spacer(modifier = Modifier.height(16.dp)) |
||||
|
|
||||
|
// Text Area |
||||
|
TextField( |
||||
|
value = textState, |
||||
|
onValueChange = { if (it.length <= maxChar) textState = it }, |
||||
|
placeholder = { |
||||
|
Text( |
||||
|
"What's your vibe today? Share a moment, date idea, or story...", |
||||
|
color = Color.Gray, |
||||
|
fontSize = 15.sp |
||||
|
) |
||||
|
}, |
||||
|
modifier = Modifier.fillMaxWidth(), |
||||
|
colors = TextFieldDefaults.colors( |
||||
|
focusedContainerColor = Color.Transparent, |
||||
|
unfocusedContainerColor = Color.Transparent, |
||||
|
focusedIndicatorColor = Color.Transparent, |
||||
|
unfocusedIndicatorColor = Color.Transparent |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
Spacer(modifier = Modifier.height(8.dp)) |
||||
|
|
||||
|
// Formatting & Character Counter Bar |
||||
|
Row( |
||||
|
modifier = Modifier.fillMaxWidth(), |
||||
|
horizontalArrangement = Arrangement.SpaceBetween, |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { |
||||
|
Text("B", fontWeight = FontWeight.Bold, fontSize = 16.sp, color = Color.Gray) |
||||
|
Text("I", fontWeight = FontWeight.Bold, fontSize = 16.sp, color = Color.Gray) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Spacer(modifier = Modifier.height(16.dp)) |
||||
|
|
||||
|
// Horizontal Scrollable Media Row (Contains Photos and Videos) |
||||
|
if (selectedMediaList.isNotEmpty()) { |
||||
|
LazyRow( |
||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp), |
||||
|
modifier = Modifier.fillMaxWidth() |
||||
|
) { |
||||
|
itemsIndexed(selectedMediaList) { index, item -> |
||||
|
Card( |
||||
|
shape = RoundedCornerShape(20.dp), |
||||
|
modifier = Modifier |
||||
|
.width(280.dp) |
||||
|
.height(220.dp) |
||||
|
) { |
||||
|
Box(modifier = Modifier.fillMaxSize()) { |
||||
|
when (item) { |
||||
|
is MediaContentItem.Photo -> { |
||||
|
Image( |
||||
|
painter = rememberAsyncImagePainter(item.uri), |
||||
|
contentDescription = "Photo Preview", |
||||
|
contentScale = ContentScale.Crop, |
||||
|
modifier = Modifier.fillMaxSize() |
||||
|
) |
||||
|
} |
||||
|
is MediaContentItem.Video -> { |
||||
|
VideoPlayerItem(videoUri = item.uri) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Delete Card Button |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.align(Alignment.TopEnd) |
||||
|
.padding(10.dp) |
||||
|
) { |
||||
|
OverlayIconButton( |
||||
|
icon = Icons.Default.Close, |
||||
|
onClick = { selectedMediaList.removeAt(index) } |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// Media Type Badge Overlay (Top Left) |
||||
|
Surface( |
||||
|
color = DarkOverlay, |
||||
|
shape = RoundedCornerShape(8.dp), |
||||
|
modifier = Modifier |
||||
|
.align(Alignment.TopStart) |
||||
|
.padding(10.dp) |
||||
|
) { |
||||
|
Row( |
||||
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
Icon( |
||||
|
imageVector = if (item is MediaContentItem.Video) Icons.Default.Videocam else Icons.Default.Image, |
||||
|
contentDescription = null, |
||||
|
tint = Color.White, |
||||
|
modifier = Modifier.size(14.dp) |
||||
|
) |
||||
|
Spacer(modifier = Modifier.width(4.dp)) |
||||
|
Text( |
||||
|
text = if (item is MediaContentItem.Video) "Video" else "Photo ${index + 1}", |
||||
|
color = Color.White, |
||||
|
fontSize = 11.sp |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Spacer(modifier = Modifier.height(16.dp)) |
||||
|
} |
||||
|
|
||||
|
// Location Tag Bar |
||||
|
|
||||
|
|
||||
|
Spacer(modifier = Modifier.height(20.dp)) |
||||
|
|
||||
|
|
||||
|
Spacer(modifier = Modifier.height(10.dp)) |
||||
|
|
||||
|
Spacer(modifier = Modifier.height(24.dp)) |
||||
|
} |
||||
|
|
||||
|
// Camera / Gallery Choice Bottom Sheet Dialog |
||||
|
if (showImagePickerDialog) { |
||||
|
ModalBottomSheet( |
||||
|
onDismissRequest = { showImagePickerDialog = false }, |
||||
|
containerColor = Color.White |
||||
|
) { |
||||
|
Column( |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.padding(24.dp) |
||||
|
) { |
||||
|
Text( |
||||
|
text = "Add Photos or Videos", |
||||
|
fontSize = 18.sp, |
||||
|
fontWeight = FontWeight.Bold, |
||||
|
modifier = Modifier.padding(bottom = 16.dp) |
||||
|
) |
||||
|
|
||||
|
// Pick from Gallery (Photo + Video combined option) |
||||
|
Row( |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.clickable { |
||||
|
showImagePickerDialog = false |
||||
|
galleryLauncher.launch( |
||||
|
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo) |
||||
|
) |
||||
|
} |
||||
|
.padding(vertical = 12.dp), |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
Icon( |
||||
|
imageVector = Icons.Default.PhotoLibrary, |
||||
|
contentDescription = "Gallery", |
||||
|
tint = Color(0xFF7B1FA2), |
||||
|
modifier = Modifier.size(24.dp) |
||||
|
) |
||||
|
Spacer(modifier = Modifier.width(16.dp)) |
||||
|
Text(text = "Choose Photos & Videos from Gallery", fontSize = 16.sp) |
||||
|
} |
||||
|
|
||||
|
// Take Photo |
||||
|
Row( |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.clickable { |
||||
|
showImagePickerDialog = false |
||||
|
val uri = createTempFileUri(".jpg", "camera_photo_") |
||||
|
tempCameraImageUri = uri |
||||
|
cameraPhotoLauncher.launch(uri) |
||||
|
} |
||||
|
.padding(vertical = 12.dp), |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
Icon( |
||||
|
imageVector = Icons.Default.CameraAlt, |
||||
|
contentDescription = "Take Photo", |
||||
|
tint = Color(0xFFE91E63), |
||||
|
modifier = Modifier.size(24.dp) |
||||
|
) |
||||
|
Spacer(modifier = Modifier.width(16.dp)) |
||||
|
Text(text = "Take Photo", fontSize = 16.sp) |
||||
|
} |
||||
|
|
||||
|
// Record Video |
||||
|
Row( |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.clickable { |
||||
|
showImagePickerDialog = false |
||||
|
val uri = createTempFileUri(".mp4", "camera_video_") |
||||
|
tempCameraVideoUri = uri |
||||
|
cameraVideoLauncher.launch(uri) |
||||
|
} |
||||
|
.padding(vertical = 12.dp), |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
Icon( |
||||
|
imageVector = Icons.Default.Videocam, |
||||
|
contentDescription = "Record Video", |
||||
|
tint = Color(0xFF1E88E5), |
||||
|
modifier = Modifier.size(24.dp) |
||||
|
) |
||||
|
Spacer(modifier = Modifier.width(16.dp)) |
||||
|
Text(text = "Record Video", fontSize = 16.sp) |
||||
|
} |
||||
|
Spacer(modifier = Modifier.height(16.dp)) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (showFeelingSheet) { |
||||
|
FeelingPickerBottomSheet( |
||||
|
onDismiss = { showFeelingSheet = false }, |
||||
|
onFeelingSelected = { feeling -> |
||||
|
selectedFeeling = feeling |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Composable |
||||
|
fun VideoPlayerItem(videoUri: Uri) { |
||||
|
val context = LocalContext.current |
||||
|
val exoPlayer = remember(videoUri) { |
||||
|
ExoPlayer.Builder(context).build().apply { |
||||
|
setMediaItem(MediaItem.fromUri(videoUri)) |
||||
|
prepare() |
||||
|
playWhenReady = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
DisposableEffect(Unit) { |
||||
|
onDispose { |
||||
|
exoPlayer.release() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
AndroidView( |
||||
|
factory = { ctx -> |
||||
|
PlayerView(ctx).apply { |
||||
|
player = exoPlayer |
||||
|
useController = true |
||||
|
} |
||||
|
}, |
||||
|
modifier = Modifier.fillMaxSize() |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
@Composable |
||||
|
fun OverlayIconButton(icon: ImageVector, onClick: () -> Unit = {}) { |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.size(32.dp) |
||||
|
.background(DarkOverlay, CircleShape) |
||||
|
.clickable { onClick() }, |
||||
|
contentAlignment = Alignment.Center |
||||
|
) { |
||||
|
Icon( |
||||
|
imageVector = icon, |
||||
|
contentDescription = null, |
||||
|
tint = Color.White, |
||||
|
modifier = Modifier.size(16.dp) |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Composable |
||||
|
fun VibeOptionChip( |
||||
|
icon: ImageVector, |
||||
|
backgroundColor: Color, |
||||
|
contentColor: Color, |
||||
|
onClick: () -> Unit = {} |
||||
|
) { |
||||
|
Surface( |
||||
|
shape = RoundedCornerShape(12.dp), |
||||
|
color = backgroundColor, |
||||
|
modifier = Modifier |
||||
|
.height(40.dp) |
||||
|
.clickable { onClick() } |
||||
|
) { |
||||
|
Row( |
||||
|
modifier = Modifier.padding(horizontal = 14.dp, vertical = 8.dp), |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
Icon( |
||||
|
imageVector = icon, |
||||
|
contentDescription = null, |
||||
|
tint = contentColor, |
||||
|
modifier = Modifier.size(18.dp) |
||||
|
) |
||||
|
Spacer(modifier = Modifier.width(6.dp)) |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
@OptIn(ExperimentalMaterial3Api::class) |
||||
|
@Composable |
||||
|
fun FeelingPickerBottomSheet( |
||||
|
onDismiss: () -> Unit, |
||||
|
onFeelingSelected: (VibeFeeling) -> Unit |
||||
|
) { |
||||
|
ModalBottomSheet( |
||||
|
onDismissRequest = onDismiss, |
||||
|
containerColor = Color.White |
||||
|
) { |
||||
|
Column( |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.padding(horizontal = 16.dp, vertical = 8.dp) |
||||
|
) { |
||||
|
Text( |
||||
|
text = "How are you feeling?", |
||||
|
fontSize = 18.sp, |
||||
|
fontWeight = FontWeight.Bold, |
||||
|
color = Color.Black, |
||||
|
modifier = Modifier.padding(bottom = 16.dp) |
||||
|
) |
||||
|
|
||||
|
LazyVerticalGrid( |
||||
|
columns = GridCells.Fixed(2), |
||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp), |
||||
|
verticalArrangement = Arrangement.spacedBy(8.dp), |
||||
|
modifier = Modifier.padding(bottom = 24.dp) |
||||
|
) { |
||||
|
items(vibeFeelingsList) { feeling -> |
||||
|
Row( |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.clip(RoundedCornerShape(12.dp)) |
||||
|
.background(Color(0xFFF8F9FA)) |
||||
|
.clickable { |
||||
|
onFeelingSelected(feeling) |
||||
|
onDismiss() |
||||
|
} |
||||
|
.padding(horizontal = 12.dp, vertical = 10.dp), |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
Text( |
||||
|
text = feeling.emoji, |
||||
|
fontSize = 20.sp |
||||
|
) |
||||
|
Spacer(modifier = Modifier.width(8.dp)) |
||||
|
Text( |
||||
|
text = feeling.name, |
||||
|
fontSize = 13.sp, |
||||
|
fontWeight = FontWeight.Medium, |
||||
|
color = Color(0xFF212121), |
||||
|
maxLines = 1 |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Preview( |
||||
|
name = "Create Vibe Post Screen", |
||||
|
showBackground = true, |
||||
|
showSystemUi = true |
||||
|
) |
||||
|
@Composable |
||||
|
fun CreateVibePostScreenPreview() { |
||||
|
MaterialTheme { |
||||
|
CreateVibePostScreen( |
||||
|
onCloseClick = {}, |
||||
|
onShareClick = {} |
||||
|
) |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue