28 changed files with 5524 additions and 1412 deletions
@ -1,486 +0,0 @@ |
|||||
package com.example.dattingapp.Login |
|
||||
|
|
||||
import android.os.Bundle |
|
||||
import android.view.WindowManager |
|
||||
import androidx.activity.ComponentActivity |
|
||||
import androidx.activity.compose.setContent |
|
||||
import androidx.activity.enableEdgeToEdge |
|
||||
import androidx.compose.animation.AnimatedVisibility |
|
||||
import androidx.compose.animation.fadeIn |
|
||||
import androidx.compose.animation.slideInVertically |
|
||||
import androidx.compose.foundation.background |
|
||||
import androidx.compose.foundation.clickable |
|
||||
import androidx.compose.foundation.layout.* |
|
||||
import androidx.compose.foundation.lazy.LazyColumn |
|
||||
import androidx.compose.foundation.lazy.items |
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState |
|
||||
import androidx.compose.foundation.shape.CircleShape |
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape |
|
||||
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.CheckCircle |
|
||||
import androidx.compose.material.icons.filled.DateRange |
|
||||
import androidx.compose.material.icons.filled.Person |
|
||||
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.Color |
|
||||
import androidx.compose.ui.text.font.FontWeight |
|
||||
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.DattingAPPTheme |
|
||||
import kotlinx.coroutines.delay |
|
||||
import kotlinx.coroutines.launch |
|
||||
import java.text.SimpleDateFormat |
|
||||
import java.util.Date |
|
||||
import java.util.Locale |
|
||||
import java.util.TimeZone |
|
||||
|
|
||||
/** |
|
||||
* Main Activity hosting the Chat Registration Screen. |
|
||||
*/ |
|
||||
class RegistrationScreen : ComponentActivity() { |
|
||||
override fun onCreate(savedInstanceState: Bundle?) { |
|
||||
super.onCreate(savedInstanceState) |
|
||||
|
|
||||
// Ensures the layout resizes and pans up when the soft keyboard opens |
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) |
|
||||
|
|
||||
enableEdgeToEdge() |
|
||||
setContent { |
|
||||
DattingAPPTheme { |
|
||||
ChatRegistrationScreen() |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Data model for individual chat history messages. |
|
||||
*/ |
|
||||
data class ChatMessage( |
|
||||
val id: Int, |
|
||||
val text: String, |
|
||||
val isBot: Boolean |
|
||||
) |
|
||||
|
|
||||
/** |
|
||||
* Main Registration Screen Composable. |
|
||||
*/ |
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) |
|
||||
@Composable |
|
||||
fun ChatRegistrationScreen( |
|
||||
onRegistrationComplete: () -> Unit = {} |
|
||||
) { |
|
||||
// Current step index (0 to 4) |
|
||||
var currentStep by remember { mutableIntStateOf(0) } |
|
||||
|
|
||||
// User form inputs |
|
||||
var nameInput by remember { mutableStateOf("") } |
|
||||
var dobInput by remember { mutableStateOf("") } |
|
||||
var locationInput by remember { mutableStateOf("Dhaka, Bangladesh") } |
|
||||
var selectedGender by remember { mutableStateOf("Man") } |
|
||||
val selectedInterests = remember { mutableStateListOf("Photography", "Dogs") } |
|
||||
var bioInput by remember { mutableStateOf("") } // Starts empty to display placeholder |
|
||||
|
|
||||
// Chat History State |
|
||||
val chatMessages = remember { |
|
||||
mutableStateListOf( |
|
||||
ChatMessage(1, "Welcome! I'll help you set up your profile. What is your name?", isBot = true) |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
val listState = rememberLazyListState() |
|
||||
val coroutineScope = rememberCoroutineScope() |
|
||||
|
|
||||
// Smooth scroll helper to push view to bottom |
|
||||
fun scrollToBottom() { |
|
||||
coroutineScope.launch { |
|
||||
delay(100) |
|
||||
if (chatMessages.isNotEmpty()) { |
|
||||
listState.animateScrollToItem(chatMessages.size) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// Auto-scroll whenever step updates to prevent text inputs from hiding under keyboard/bottom bar |
|
||||
LaunchedEffect(currentStep) { |
|
||||
scrollToBottom() |
|
||||
} |
|
||||
|
|
||||
Scaffold( |
|
||||
topBar = { |
|
||||
Column(modifier = Modifier.background(Color.White)) { |
|
||||
TopAppBar( |
|
||||
title = { |
|
||||
Text( |
|
||||
text = "VibeCheck", |
|
||||
fontWeight = FontWeight.Bold, |
|
||||
color = Color(0xFF6B008A), |
|
||||
fontSize = 20.sp |
|
||||
) |
|
||||
}, |
|
||||
navigationIcon = { |
|
||||
IconButton(onClick = { |
|
||||
if (currentStep > 0) currentStep-- |
|
||||
}) { |
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") |
|
||||
} |
|
||||
}, |
|
||||
actions = { |
|
||||
Text( |
|
||||
text = "Step ${currentStep + 1} of 5", |
|
||||
fontSize = 12.sp, |
|
||||
color = Color.Gray, |
|
||||
modifier = Modifier.padding(end = 16.dp) |
|
||||
) |
|
||||
} |
|
||||
) |
|
||||
// Step Progress Indicator |
|
||||
LinearProgressIndicator( |
|
||||
progress = { (currentStep + 1) / 5f }, |
|
||||
modifier = Modifier.fillMaxWidth(), |
|
||||
color = Color(0xFFC2003B), |
|
||||
trackColor = Color(0xFFF0F0F0) |
|
||||
) |
|
||||
} |
|
||||
}, |
|
||||
bottomBar = { |
|
||||
Surface( |
|
||||
shadowElevation = 8.dp, |
|
||||
color = Color.White |
|
||||
) { |
|
||||
Box( |
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.padding(16.dp) |
|
||||
) { |
|
||||
Button( |
|
||||
onClick = { |
|
||||
when (currentStep) { |
|
||||
0 -> { |
|
||||
if (nameInput.isNotBlank()) { |
|
||||
chatMessages.add(ChatMessage(chatMessages.size + 1, nameInput, isBot = false)) |
|
||||
chatMessages.add(ChatMessage(chatMessages.size + 1, "Great to meet you, $nameInput! When is your birthday?", isBot = true)) |
|
||||
currentStep = 1 |
|
||||
} |
|
||||
} |
|
||||
1 -> { |
|
||||
if (dobInput.isNotBlank()) { |
|
||||
chatMessages.add(ChatMessage(chatMessages.size + 1, dobInput, isBot = false)) |
|
||||
chatMessages.add(ChatMessage(chatMessages.size + 1, "Where are you located?", isBot = true)) |
|
||||
currentStep = 2 |
|
||||
} |
|
||||
} |
|
||||
2 -> { |
|
||||
chatMessages.add(ChatMessage(chatMessages.size + 1, locationInput, isBot = false)) |
|
||||
chatMessages.add(ChatMessage(chatMessages.size + 1, "How do you identify, and what are some of your interests?", isBot = true)) |
|
||||
currentStep = 3 |
|
||||
} |
|
||||
3 -> { |
|
||||
val summary = "$selectedGender | ${selectedInterests.joinToString(", ")}" |
|
||||
chatMessages.add(ChatMessage(chatMessages.size + 1, summary, isBot = false)) |
|
||||
chatMessages.add(ChatMessage(chatMessages.size + 1, "Almost done! Lastly, tell us a bit about yourself. What's your vibe?", isBot = true)) |
|
||||
currentStep = 4 |
|
||||
} |
|
||||
4 -> { |
|
||||
chatMessages.add(ChatMessage(chatMessages.size + 1, bioInput, isBot = false)) |
|
||||
onRegistrationComplete() |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.height(52.dp), |
|
||||
shape = RoundedCornerShape(16.dp), |
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFC2003B)) |
|
||||
) { |
|
||||
Row(verticalAlignment = Alignment.CenterVertically) { |
|
||||
Text( |
|
||||
text = if (currentStep == 4) "Complete Profile" else "Continue", |
|
||||
fontSize = 16.sp, |
|
||||
fontWeight = FontWeight.Bold, |
|
||||
color = Color.White |
|
||||
) |
|
||||
Spacer(modifier = Modifier.width(8.dp)) |
|
||||
Icon( |
|
||||
imageVector = if (currentStep == 4) Icons.Default.CheckCircle else Icons.AutoMirrored.Filled.ArrowForward, |
|
||||
contentDescription = null, |
|
||||
tint = Color.White |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
) { innerPadding -> |
|
||||
LazyColumn( |
|
||||
state = listState, |
|
||||
modifier = Modifier |
|
||||
.fillMaxSize() |
|
||||
.background(Color(0xFFF9F9FB)) |
|
||||
.padding(innerPadding) |
|
||||
.imePadding() // Dynamically adjusts list padding when soft keyboard appears |
|
||||
.padding(horizontal = 16.dp), |
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp), |
|
||||
contentPadding = PaddingValues(vertical = 16.dp) |
|
||||
) { |
|
||||
// Render chat history |
|
||||
items(chatMessages) { message -> |
|
||||
ChatBubbleItem(message = message) |
|
||||
} |
|
||||
|
|
||||
// Render current step widget |
|
||||
item { |
|
||||
AnimatedVisibility( |
|
||||
visible = true, |
|
||||
enter = fadeIn() + slideInVertically { it / 2 } |
|
||||
) { |
|
||||
when (currentStep) { |
|
||||
0 -> NameInputWidget(name = nameInput, onNameChange = { nameInput = it }) |
|
||||
1 -> DatePickerWidget(dob = dobInput, onDobChange = { dobInput = it }) |
|
||||
2 -> LocationInputWidget(location = locationInput, onLocationChange = { locationInput = it }) |
|
||||
3 -> GenderAndInterestsWidget( |
|
||||
selectedGender = selectedGender, |
|
||||
onGenderSelect = { selectedGender = it }, |
|
||||
selectedInterests = selectedInterests |
|
||||
) |
|
||||
4 -> BioInputWidget(bio = bioInput, onBioChange = { bioInput = it }) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Composable |
|
||||
fun ChatBubbleItem(message: ChatMessage) { |
|
||||
Row( |
|
||||
modifier = Modifier.fillMaxWidth(), |
|
||||
horizontalArrangement = if (message.isBot) Arrangement.Start else Arrangement.End, |
|
||||
verticalAlignment = Alignment.Top |
|
||||
) { |
|
||||
if (message.isBot) { |
|
||||
Box( |
|
||||
modifier = Modifier |
|
||||
.size(32.dp) |
|
||||
.clip(CircleShape) |
|
||||
.background(Color(0xFFC2003B)), |
|
||||
contentAlignment = Alignment.Center |
|
||||
) { |
|
||||
Icon( |
|
||||
imageVector = Icons.Default.Person, |
|
||||
contentDescription = "Bot", |
|
||||
tint = Color.White, |
|
||||
modifier = Modifier.size(18.dp) |
|
||||
) |
|
||||
} |
|
||||
Spacer(modifier = Modifier.width(8.dp)) |
|
||||
} |
|
||||
|
|
||||
Surface( |
|
||||
shape = RoundedCornerShape( |
|
||||
topStart = 16.dp, |
|
||||
topEnd = 16.dp, |
|
||||
bottomStart = if (message.isBot) 4.dp else 16.dp, |
|
||||
bottomEnd = if (message.isBot) 16.dp else 4.dp |
|
||||
), |
|
||||
color = if (message.isBot) Color(0xFFEEEEEE) else Color(0xFFC2003B), |
|
||||
modifier = Modifier.widthIn(max = 280.dp) |
|
||||
) { |
|
||||
Text( |
|
||||
text = message.text, |
|
||||
modifier = Modifier.padding(12.dp), |
|
||||
color = if (message.isBot) Color.DarkGray else Color.White, |
|
||||
fontSize = 14.sp, |
|
||||
fontWeight = FontWeight.Medium |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// STEP 0 WIDGET |
|
||||
@Composable |
|
||||
fun NameInputWidget(name: String, onNameChange: (String) -> Unit) { |
|
||||
OutlinedTextField( |
|
||||
value = name, |
|
||||
onValueChange = onNameChange, |
|
||||
placeholder = { Text("Enter your full name") }, |
|
||||
modifier = Modifier.fillMaxWidth(), |
|
||||
shape = RoundedCornerShape(12.dp), |
|
||||
colors = OutlinedTextFieldDefaults.colors( |
|
||||
focusedContainerColor = Color.White, |
|
||||
unfocusedContainerColor = Color.White |
|
||||
) |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
// STEP 1 WIDGET (MATERIAL 3 DATE PICKER) |
|
||||
@OptIn(ExperimentalMaterial3Api::class) |
|
||||
@Composable |
|
||||
fun DatePickerWidget(dob: String, onDobChange: (String) -> Unit) { |
|
||||
var showDatePicker by remember { mutableStateOf(false) } |
|
||||
val datePickerState = rememberDatePickerState() |
|
||||
|
|
||||
OutlinedTextField( |
|
||||
value = dob, |
|
||||
onValueChange = { }, |
|
||||
readOnly = true, |
|
||||
enabled = false, |
|
||||
label = { Text("SELECT DATE OF BIRTH") }, |
|
||||
placeholder = { Text("DD/MM/YYYY") }, |
|
||||
trailingIcon = { |
|
||||
Icon( |
|
||||
imageVector = Icons.Default.DateRange, |
|
||||
contentDescription = "Select Date" |
|
||||
) |
|
||||
}, |
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.clickable { showDatePicker = true }, |
|
||||
shape = RoundedCornerShape(12.dp), |
|
||||
colors = OutlinedTextFieldDefaults.colors( |
|
||||
disabledContainerColor = Color.White, |
|
||||
disabledTextColor = Color.Black, |
|
||||
disabledLabelColor = Color.Gray, |
|
||||
disabledBorderColor = Color.Gray, |
|
||||
disabledTrailingIconColor = Color(0xFFC2003B) |
|
||||
) |
|
||||
) |
|
||||
|
|
||||
if (showDatePicker) { |
|
||||
DatePickerDialog( |
|
||||
onDismissRequest = { showDatePicker = false }, |
|
||||
confirmButton = { |
|
||||
TextButton( |
|
||||
onClick = { |
|
||||
datePickerState.selectedDateMillis?.let { millis -> |
|
||||
val formatter = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()).apply { |
|
||||
timeZone = TimeZone.getTimeZone("UTC") |
|
||||
} |
|
||||
val formattedDate = formatter.format(Date(millis)) |
|
||||
onDobChange(formattedDate) |
|
||||
} |
|
||||
showDatePicker = false |
|
||||
} |
|
||||
) { |
|
||||
Text("OK", color = Color(0xFFC2003B)) |
|
||||
} |
|
||||
}, |
|
||||
dismissButton = { |
|
||||
TextButton(onClick = { showDatePicker = false }) { |
|
||||
Text("CANCEL", color = Color.Gray) |
|
||||
} |
|
||||
} |
|
||||
) { |
|
||||
DatePicker(state = datePickerState) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// STEP 2 WIDGET |
|
||||
@Composable |
|
||||
fun LocationInputWidget(location: String, onLocationChange: (String) -> Unit) { |
|
||||
OutlinedTextField( |
|
||||
value = location, |
|
||||
onValueChange = onLocationChange, |
|
||||
placeholder = { Text("Enter your city/location") }, |
|
||||
modifier = Modifier.fillMaxWidth(), |
|
||||
shape = RoundedCornerShape(12.dp), |
|
||||
colors = OutlinedTextFieldDefaults.colors( |
|
||||
focusedContainerColor = Color.White, |
|
||||
unfocusedContainerColor = Color.White |
|
||||
) |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
// STEP 3 WIDGET |
|
||||
@OptIn(ExperimentalLayoutApi::class) |
|
||||
@Composable |
|
||||
fun GenderAndInterestsWidget( |
|
||||
selectedGender: String, |
|
||||
onGenderSelect: (String) -> Unit, |
|
||||
selectedInterests: androidx.compose.runtime.snapshots.SnapshotStateList<String> |
|
||||
) { |
|
||||
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("I IDENTIFY AS", fontSize = 11.sp, fontWeight = FontWeight.Bold, color = Color.Gray) |
|
||||
Spacer(modifier = Modifier.height(8.dp)) |
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { |
|
||||
listOf("Woman", "Man").forEach { gender -> |
|
||||
FilterChip( |
|
||||
selected = selectedGender == gender, |
|
||||
onClick = { onGenderSelect(gender) }, |
|
||||
label = { Text(gender) } |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
Spacer(modifier = Modifier.height(16.dp)) |
|
||||
Text("INTERESTS (SELECT AT LEAST 2)", fontSize = 11.sp, fontWeight = FontWeight.Bold, color = Color.Gray) |
|
||||
Spacer(modifier = Modifier.height(8.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) |
|
||||
}, |
|
||||
label = { Text(interest) } |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// STEP 4 WIDGET |
|
||||
@Composable |
|
||||
fun BioInputWidget(bio: String, onBioChange: (String) -> Unit) { |
|
||||
OutlinedTextField( |
|
||||
value = bio, |
|
||||
onValueChange = { if (it.length <= 200) onBioChange(it) }, |
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.height(120.dp), |
|
||||
shape = RoundedCornerShape(12.dp), |
|
||||
placeholder = { Text("Write a short bio...", color = Color.Gray) }, |
|
||||
maxLines = 4, |
|
||||
singleLine = false, |
|
||||
colors = OutlinedTextFieldDefaults.colors( |
|
||||
focusedContainerColor = Color.White, |
|
||||
unfocusedContainerColor = Color.White |
|
||||
) |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
@Preview( |
|
||||
name = "Chat Registration - Phone Screen", |
|
||||
showBackground = true, |
|
||||
showSystemUi = true, |
|
||||
device = "spec:width=411dp,height=891dp,dpi=420" |
|
||||
) |
|
||||
@Composable |
|
||||
fun ChatRegistrationScreenPreview() { |
|
||||
DattingAPPTheme { |
|
||||
ChatRegistrationScreen( |
|
||||
onRegistrationComplete = {} |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
@ -1,300 +0,0 @@ |
|||||
package com.example.dattingapp.Login |
|
||||
|
|
||||
import android.os.Bundle |
|
||||
import androidx.activity.ComponentActivity |
|
||||
import androidx.activity.compose.setContent |
|
||||
import androidx.activity.enableEdgeToEdge |
|
||||
import androidx.compose.foundation.layout.fillMaxSize |
|
||||
import androidx.compose.foundation.layout.padding |
|
||||
import androidx.compose.material3.Scaffold |
|
||||
import androidx.compose.material3.Text |
|
||||
import com.example.dattingapp.ui.theme.DattingAPPTheme |
|
||||
import androidx.compose.foundation.Canvas |
|
||||
import androidx.compose.foundation.background |
|
||||
import androidx.compose.foundation.border |
|
||||
import androidx.compose.foundation.clickable |
|
||||
import androidx.compose.foundation.layout.* |
|
||||
import androidx.compose.foundation.shape.CircleShape |
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape |
|
||||
import androidx.compose.material.icons.Icons |
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack |
|
||||
import androidx.compose.material.icons.filled.AddAPhoto |
|
||||
import androidx.compose.material.icons.filled.CameraAlt |
|
||||
import androidx.compose.material.icons.filled.Image |
|
||||
import androidx.compose.material3.* |
|
||||
import androidx.compose.runtime.Composable |
|
||||
import androidx.compose.ui.Alignment |
|
||||
import androidx.compose.ui.Modifier |
|
||||
import androidx.compose.ui.draw.shadow |
|
||||
import androidx.compose.ui.graphics.Brush |
|
||||
import androidx.compose.ui.graphics.Color |
|
||||
import androidx.compose.ui.graphics.PathEffect |
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke |
|
||||
import androidx.compose.ui.text.font.FontWeight |
|
||||
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 |
|
||||
class UploadProfileImgae : ComponentActivity() { |
|
||||
override fun onCreate(savedInstanceState: Bundle?) { |
|
||||
super.onCreate(savedInstanceState) |
|
||||
enableEdgeToEdge() |
|
||||
setContent { |
|
||||
DattingAPPTheme { |
|
||||
ProfileImageUploadScreen() |
|
||||
|
|
||||
|
|
||||
} |
|
||||
|
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@OptIn(ExperimentalMaterial3Api::class) |
|
||||
@Composable |
|
||||
fun ProfileImageUploadScreen( |
|
||||
onBackClick: () -> Unit = {}, |
|
||||
onUploadFromGalleryClick: () -> Unit = {}, |
|
||||
onTakePhotoClick: () -> Unit = {}, |
|
||||
onContinueClick: () -> Unit = {}, |
|
||||
onContactSupportClick: () -> Unit = {} |
|
||||
) { |
|
||||
val backgroundGradient = Brush.verticalGradient( |
|
||||
colors = listOf( |
|
||||
Color(0xFFFFF7F8), |
|
||||
Color(0xFFFFFFFF), |
|
||||
Color(0xFFF7EFFC) |
|
||||
) |
|
||||
) |
|
||||
|
|
||||
val buttonGradient = Brush.horizontalGradient( |
|
||||
colors = listOf( |
|
||||
Color(0xFFD84B75), |
|
||||
Color(0xFF8E54C1) |
|
||||
) |
|
||||
) |
|
||||
|
|
||||
val avatarGradientBorder = Brush.sweepGradient( |
|
||||
colors = listOf( |
|
||||
Color(0xFFB00020), |
|
||||
Color(0xFF7B1FA2), |
|
||||
Color(0xFFB00020) |
|
||||
) |
|
||||
) |
|
||||
|
|
||||
Scaffold( |
|
||||
topBar = { |
|
||||
TopAppBar( |
|
||||
title = { |
|
||||
Box( |
|
||||
modifier = Modifier.fillMaxWidth(), |
|
||||
contentAlignment = Alignment.Center |
|
||||
) { |
|
||||
Text( |
|
||||
text = "VibeCheck", |
|
||||
fontWeight = FontWeight.Bold, |
|
||||
color = Color(0xFF9C0037), |
|
||||
fontSize = 20.sp, |
|
||||
modifier = Modifier.padding(end = 48.dp) // Offset navigation icon width to center text perfectly |
|
||||
) |
|
||||
} |
|
||||
}, |
|
||||
navigationIcon = { |
|
||||
IconButton(onClick = onBackClick) { |
|
||||
Icon( |
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack, |
|
||||
contentDescription = "Back", |
|
||||
tint = Color.Black |
|
||||
) |
|
||||
} |
|
||||
}, |
|
||||
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent) |
|
||||
) |
|
||||
}, |
|
||||
containerColor = Color.Transparent |
|
||||
) { innerPadding -> |
|
||||
Column( |
|
||||
modifier = Modifier |
|
||||
.fillMaxSize() |
|
||||
.background(brush = backgroundGradient) |
|
||||
.padding(innerPadding) |
|
||||
.padding(horizontal = 24.dp), |
|
||||
horizontalAlignment = Alignment.CenterHorizontally |
|
||||
) { |
|
||||
Spacer(modifier = Modifier.height(16.dp)) |
|
||||
|
|
||||
Text( |
|
||||
text = "Upload profile photo", |
|
||||
fontSize = 22.sp, |
|
||||
fontWeight = FontWeight.Bold, |
|
||||
color = Color(0xFF1D1B20) |
|
||||
) |
|
||||
|
|
||||
Spacer(modifier = Modifier.height(8.dp)) |
|
||||
|
|
||||
Text( |
|
||||
text = "Choose a photo that shows your\nbest self. You can always change\nthis later.", |
|
||||
fontSize = 13.sp, |
|
||||
color = Color(0xFF666666), |
|
||||
textAlign = TextAlign.Center, |
|
||||
lineHeight = 18.sp |
|
||||
) |
|
||||
|
|
||||
Spacer(modifier = Modifier.height(36.dp)) |
|
||||
|
|
||||
// Circular Avatar Placeholder with Gradient Ring |
|
||||
Box( |
|
||||
contentAlignment = Alignment.Center, |
|
||||
modifier = Modifier |
|
||||
.size(190.dp) |
|
||||
.shadow( |
|
||||
elevation = 16.dp, |
|
||||
shape = CircleShape, |
|
||||
spotColor = Color(0x33B00020), |
|
||||
ambientColor = Color(0x337B1FA2) |
|
||||
) |
|
||||
.background(Color.White, CircleShape) |
|
||||
.border(width = 3.dp, brush = avatarGradientBorder, shape = CircleShape) |
|
||||
) { |
|
||||
// Dashed Inner Circle Outline |
|
||||
Canvas(modifier = Modifier.size(160.dp)) { |
|
||||
val stroke = Stroke( |
|
||||
width = 2f, |
|
||||
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f) |
|
||||
) |
|
||||
drawCircle( |
|
||||
color = Color(0xFFE0C4CD), |
|
||||
style = stroke |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
// Center Camera Icon |
|
||||
Icon( |
|
||||
imageVector = Icons.Default.AddAPhoto, |
|
||||
contentDescription = "Add Photo", |
|
||||
tint = Color(0xFFE5A8B8), |
|
||||
modifier = Modifier.size(44.dp) |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
Spacer(modifier = Modifier.weight(1f)) |
|
||||
|
|
||||
// Secondary Action Options |
|
||||
Column( |
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp), |
|
||||
modifier = Modifier.fillMaxWidth() |
|
||||
) { |
|
||||
// Upload from gallery Button |
|
||||
Surface( |
|
||||
onClick = onUploadFromGalleryClick, |
|
||||
shape = RoundedCornerShape(12.dp), |
|
||||
color = Color(0xFFE8DDED).copy(alpha = 0.6f), |
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.height(48.dp) |
|
||||
) { |
|
||||
Row( |
|
||||
verticalAlignment = Alignment.CenterVertically, |
|
||||
horizontalArrangement = Arrangement.Center |
|
||||
) { |
|
||||
Icon( |
|
||||
imageVector = Icons.Default.Image, |
|
||||
contentDescription = null, |
|
||||
tint = Color(0xFF6B008A), |
|
||||
modifier = Modifier.size(18.dp) |
|
||||
) |
|
||||
Spacer(modifier = Modifier.width(8.dp)) |
|
||||
Text( |
|
||||
text = "Upload from gallery", |
|
||||
color = Color(0xFF6B008A), |
|
||||
fontSize = 13.sp, |
|
||||
fontWeight = FontWeight.SemiBold |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// Take a photo Button |
|
||||
Surface( |
|
||||
onClick = onTakePhotoClick, |
|
||||
shape = RoundedCornerShape(12.dp), |
|
||||
color = Color(0xFFE8DDED).copy(alpha = 0.6f), |
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.height(48.dp) |
|
||||
) { |
|
||||
Row( |
|
||||
verticalAlignment = Alignment.CenterVertically, |
|
||||
horizontalArrangement = Arrangement.Center |
|
||||
) { |
|
||||
Icon( |
|
||||
imageVector = Icons.Default.CameraAlt, |
|
||||
contentDescription = null, |
|
||||
tint = Color(0xFF6B008A), |
|
||||
modifier = Modifier.size(18.dp) |
|
||||
) |
|
||||
Spacer(modifier = Modifier.width(8.dp)) |
|
||||
Text( |
|
||||
text = "Take a photo", |
|
||||
color = Color(0xFF6B008A), |
|
||||
fontSize = 13.sp, |
|
||||
fontWeight = FontWeight.SemiBold |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
Spacer(modifier = Modifier.height(20.dp)) |
|
||||
|
|
||||
// Main Gradient Continue Button |
|
||||
Button( |
|
||||
onClick = onContinueClick, |
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.height(50.dp) |
|
||||
.background( |
|
||||
brush = buttonGradient, |
|
||||
shape = RoundedCornerShape(12.dp) |
|
||||
), |
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent), |
|
||||
shape = RoundedCornerShape(12.dp) |
|
||||
) { |
|
||||
Text( |
|
||||
text = "Continue", |
|
||||
color = Color.White, |
|
||||
fontWeight = FontWeight.Bold, |
|
||||
fontSize = 16.sp |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
Spacer(modifier = Modifier.height(16.dp)) |
|
||||
|
|
||||
// Need help? Contact support Footer |
|
||||
Row( |
|
||||
horizontalArrangement = Arrangement.Center, |
|
||||
verticalAlignment = Alignment.CenterVertically |
|
||||
) { |
|
||||
Text( |
|
||||
text = "Need help? ", |
|
||||
fontSize = 12.sp, |
|
||||
color = Color(0xFF666666) |
|
||||
) |
|
||||
Text( |
|
||||
text = "Contact support", |
|
||||
fontSize = 12.sp, |
|
||||
fontWeight = FontWeight.Bold, |
|
||||
color = Color(0xFFB00020), |
|
||||
modifier = Modifier.clickable { onContactSupportClick() } |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
Spacer(modifier = Modifier.height(16.dp)) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Preview(showBackground = true, device = "spec:width=411dp,height=891dp") |
|
||||
@Composable |
|
||||
fun ProfileImageUploadScreenPreview() { |
|
||||
ProfileImageUploadScreen() |
|
||||
} |
|
||||
@ -1,271 +0,0 @@ |
|||||
package com.example.dattingapp.Login |
|
||||
|
|
||||
import android.os.Bundle |
|
||||
import androidx.activity.ComponentActivity |
|
||||
import androidx.activity.compose.setContent |
|
||||
import androidx.activity.enableEdgeToEdge |
|
||||
import androidx.compose.foundation.layout.fillMaxSize |
|
||||
import androidx.compose.foundation.layout.padding |
|
||||
import androidx.compose.material3.Text |
|
||||
import androidx.compose.runtime.Composable |
|
||||
import androidx.compose.ui.Modifier |
|
||||
import androidx.compose.ui.tooling.preview.Preview |
|
||||
import com.example.dattingapp.ui.theme.DattingAPPTheme |
|
||||
import androidx.compose.foundation.background |
|
||||
import androidx.compose.foundation.border |
|
||||
import androidx.compose.foundation.clickable |
|
||||
import androidx.compose.foundation.layout.* |
|
||||
import androidx.compose.foundation.shape.CircleShape |
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape |
|
||||
import androidx.compose.material.icons.Icons |
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowForward |
|
||||
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.* |
|
||||
import androidx.compose.runtime.* |
|
||||
import androidx.compose.ui.Alignment |
|
||||
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.style.TextAlign |
|
||||
import androidx.compose.ui.unit.dp |
|
||||
import androidx.compose.ui.unit.sp |
|
||||
|
|
||||
class VedioIntroScreen : ComponentActivity() { |
|
||||
override fun onCreate(savedInstanceState: Bundle?) { |
|
||||
super.onCreate(savedInstanceState) |
|
||||
enableEdgeToEdge() |
|
||||
setContent { |
|
||||
DattingAPPTheme { |
|
||||
WhatsYourVibeScreen( |
|
||||
|
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
data class VibeOption( |
|
||||
val title: String, |
|
||||
val subtitle: String, |
|
||||
val icon: ImageVector, |
|
||||
val iconBackgroundColor: Color, |
|
||||
val iconTintColor: Color |
|
||||
) |
|
||||
|
|
||||
@Composable |
|
||||
fun WhatsYourVibeScreen( |
|
||||
onContinueClick: (VibeOption) -> Unit = {} |
|
||||
) { |
|
||||
val vibeOptions = listOf( |
|
||||
VibeOption( |
|
||||
title = "Serious Relationship", |
|
||||
subtitle = "Looking for the one", |
|
||||
icon = Icons.Default.FavoriteBorder, |
|
||||
iconBackgroundColor = Color(0xFFFFEAEA), |
|
||||
iconTintColor = Color(0xFFD32F2F) |
|
||||
), |
|
||||
VibeOption( |
|
||||
title = "Casual Dates", |
|
||||
subtitle = "Drinks & good times", |
|
||||
icon = Icons.Default.LocalBar, |
|
||||
iconBackgroundColor = Color(0xFFF0E5FF), |
|
||||
iconTintColor = Color(0xFF7B1FA2) |
|
||||
), |
|
||||
VibeOption( |
|
||||
title = "New Friends", |
|
||||
subtitle = "Expand my circle", |
|
||||
icon = Icons.Default.People, |
|
||||
iconBackgroundColor = Color(0xFFFFF3E0), |
|
||||
iconTintColor = Color(0xFFE65100) |
|
||||
), |
|
||||
VibeOption( |
|
||||
title = "Marriage", |
|
||||
subtitle = "Seeing where things go", |
|
||||
icon = Icons.Default.CompassCalibration, |
|
||||
iconBackgroundColor = Color(0xFFEFEFEF), |
|
||||
iconTintColor = Color(0xFF616161) |
|
||||
) |
|
||||
) |
|
||||
|
|
||||
var selectedOption by remember { mutableStateOf<VibeOption?>(null) } |
|
||||
|
|
||||
val backgroundGradient = Brush.verticalGradient( |
|
||||
colors = listOf( |
|
||||
Color(0xFFFFF5F7), |
|
||||
Color(0xFFFFFFFF), |
|
||||
Color(0xFFF9F0FF) |
|
||||
) |
|
||||
) |
|
||||
|
|
||||
Column( |
|
||||
modifier = Modifier |
|
||||
.fillMaxSize() |
|
||||
.background(brush = backgroundGradient) |
|
||||
.padding(horizontal = 24.dp, vertical = 16.dp), |
|
||||
horizontalAlignment = Alignment.CenterHorizontally |
|
||||
) { |
|
||||
Spacer(modifier = Modifier.height(24.dp)) |
|
||||
|
|
||||
Text( |
|
||||
text = "VibeCheck", |
|
||||
fontSize = 24.sp, |
|
||||
fontWeight = FontWeight.Bold, |
|
||||
color = Color(0xFFB00020) |
|
||||
) |
|
||||
|
|
||||
Spacer(modifier = Modifier.height(16.dp)) |
|
||||
|
|
||||
// Indicator Bar |
|
||||
|
|
||||
Spacer(modifier = Modifier.height(32.dp)) |
|
||||
|
|
||||
Text( |
|
||||
text = "What's your vibe?", |
|
||||
fontSize = 22.sp, |
|
||||
fontWeight = FontWeight.Bold, |
|
||||
color = Color(0xFF1D1B20) |
|
||||
) |
|
||||
|
|
||||
Spacer(modifier = Modifier.height(8.dp)) |
|
||||
|
|
||||
Text( |
|
||||
text = "Select your main goal right now to help us find the right connections.", |
|
||||
fontSize = 14.sp, |
|
||||
color = Color(0xFF666666), |
|
||||
textAlign = TextAlign.Center, |
|
||||
modifier = Modifier.padding(horizontal = 16.dp) |
|
||||
) |
|
||||
|
|
||||
Spacer(modifier = Modifier.height(32.dp)) |
|
||||
|
|
||||
// Options List |
|
||||
Column( |
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp), |
|
||||
modifier = Modifier.weight(1f) |
|
||||
) { |
|
||||
vibeOptions.forEach { option -> |
|
||||
val isSelected = selectedOption == option |
|
||||
|
|
||||
val borderModifier = if (isSelected) { |
|
||||
Modifier.border( |
|
||||
width = 2.dp, |
|
||||
color = Color(0xFFB00020), |
|
||||
shape = RoundedCornerShape(16.dp) |
|
||||
) |
|
||||
} else { |
|
||||
Modifier.border( |
|
||||
width = 1.dp, |
|
||||
color = Color(0xFFEAEAEA), |
|
||||
shape = RoundedCornerShape(16.dp) |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
Surface( |
|
||||
shape = RoundedCornerShape(16.dp), |
|
||||
color = Color.White, |
|
||||
shadowElevation = if (isSelected) 4.dp else 0.dp, |
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.then(borderModifier) |
|
||||
.clickable { selectedOption = option } |
|
||||
) { |
|
||||
Row( |
|
||||
modifier = Modifier.padding(16.dp), |
|
||||
verticalAlignment = Alignment.CenterVertically |
|
||||
) { |
|
||||
Box( |
|
||||
modifier = Modifier |
|
||||
.size(44.dp) |
|
||||
.background(option.iconBackgroundColor, 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)) |
|
||||
|
|
||||
Column { |
|
||||
Text( |
|
||||
text = option.title, |
|
||||
fontSize = 16.sp, |
|
||||
fontWeight = FontWeight.Bold, |
|
||||
color = Color(0xFF1D1B20) |
|
||||
) |
|
||||
Spacer(modifier = Modifier.height(2.dp)) |
|
||||
Text( |
|
||||
text = option.subtitle, |
|
||||
fontSize = 12.sp, |
|
||||
color = Color(0xFF757575) |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// Bottom Action Area |
|
||||
Button( |
|
||||
onClick = { selectedOption?.let { onContinueClick(it) } }, |
|
||||
enabled = selectedOption != null, |
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.height(52.dp) |
|
||||
.background( |
|
||||
brush = Brush.horizontalGradient( |
|
||||
colors = listOf(Color(0xFFB00020), Color(0xFF4A148C)) |
|
||||
), |
|
||||
shape = RoundedCornerShape(26.dp) |
|
||||
), |
|
||||
colors = ButtonDefaults.buttonColors( |
|
||||
containerColor = Color.Transparent, |
|
||||
disabledContainerColor = Color.Gray.copy(alpha = 0.3f) |
|
||||
), |
|
||||
shape = RoundedCornerShape(26.dp) |
|
||||
) { |
|
||||
Row( |
|
||||
verticalAlignment = Alignment.CenterVertically, |
|
||||
horizontalArrangement = Arrangement.Center |
|
||||
) { |
|
||||
Text( |
|
||||
text = "Continue", |
|
||||
color = Color.White, |
|
||||
fontSize = 16.sp, |
|
||||
fontWeight = FontWeight.Bold |
|
||||
) |
|
||||
Spacer(modifier = Modifier.width(8.dp)) |
|
||||
Icon( |
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowForward, |
|
||||
contentDescription = null, |
|
||||
tint = Color.White, |
|
||||
modifier = Modifier.size(18.dp) |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
Spacer(modifier = Modifier.height(12.dp)) |
|
||||
|
|
||||
Text( |
|
||||
text = "You can always change this later in your profile.", |
|
||||
fontSize = 12.sp, |
|
||||
color = Color(0xFF757575), |
|
||||
textAlign = TextAlign.Center |
|
||||
) |
|
||||
|
|
||||
Spacer(modifier = Modifier.height(8.dp)) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Preview(showBackground = true, device = "spec:width=411dp,height=891dp") |
|
||||
@Composable |
|
||||
fun WhatsYourVibeScreenPreview() { |
|
||||
WhatsYourVibeScreen() |
|
||||
} |
|
||||
@ -1,338 +0,0 @@ |
|||||
package com.example.dattingapp.Login |
|
||||
|
|
||||
import android.os.Bundle |
|
||||
import androidx.activity.ComponentActivity |
|
||||
import androidx.activity.compose.setContent |
|
||||
import androidx.activity.enableEdgeToEdge |
|
||||
import androidx.compose.foundation.layout.fillMaxSize |
|
||||
import androidx.compose.foundation.layout.padding |
|
||||
import androidx.compose.material3.Scaffold |
|
||||
import androidx.compose.material3.Text |
|
||||
import com.example.dattingapp.ui.theme.DattingAPPTheme |
|
||||
import androidx.compose.foundation.Image |
|
||||
import androidx.compose.foundation.Canvas |
|
||||
import androidx.compose.foundation.background |
|
||||
import androidx.compose.foundation.border |
|
||||
import androidx.compose.foundation.layout.* |
|
||||
import androidx.compose.foundation.shape.CircleShape |
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape |
|
||||
import androidx.compose.material.icons.Icons |
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack |
|
||||
import androidx.compose.material.icons.filled.Cameraswitch |
|
||||
import androidx.compose.material.icons.filled.FileUpload |
|
||||
import androidx.compose.material.icons.filled.Videocam |
|
||||
import androidx.compose.material3.* |
|
||||
import androidx.compose.runtime.Composable |
|
||||
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.PathEffect |
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke |
|
||||
import androidx.compose.ui.layout.ContentScale |
|
||||
import androidx.compose.ui.res.painterResource |
|
||||
import androidx.compose.ui.text.font.FontWeight |
|
||||
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.R |
|
||||
|
|
||||
class VideoIntroScreen : ComponentActivity() { |
|
||||
override fun onCreate(savedInstanceState: Bundle?) { |
|
||||
super.onCreate(savedInstanceState) |
|
||||
enableEdgeToEdge() |
|
||||
setContent { |
|
||||
DattingAPPTheme { |
|
||||
VideoIntroScreen( |
|
||||
onBackClick = { |
|
||||
// TODO: Navigate back |
|
||||
}, |
|
||||
onSkipClick = { |
|
||||
// TODO: Skip video intro step |
|
||||
}, |
|
||||
onUploadClick = { |
|
||||
// TODO: Process final upload or proceed to next screen |
|
||||
}, |
|
||||
onRecordClick = { |
|
||||
// TODO: Start or stop recording video |
|
||||
}, |
|
||||
onSelectFileClick = { |
|
||||
// TODO: Launch video picker from gallery |
|
||||
}, |
|
||||
onSwitchCameraClick = { |
|
||||
// TODO: Switch between front and rear cameras |
|
||||
} |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@OptIn(ExperimentalMaterial3Api::class) |
|
||||
@Composable |
|
||||
fun VideoIntroScreen( |
|
||||
onBackClick: () -> Unit = {}, |
|
||||
onSkipClick: () -> Unit = {}, |
|
||||
onUploadClick: () -> Unit = {}, |
|
||||
onRecordClick: () -> Unit = {}, |
|
||||
onSelectFileClick: () -> Unit = {}, |
|
||||
onSwitchCameraClick: () -> Unit = {} |
|
||||
) { |
|
||||
Scaffold( |
|
||||
topBar = { |
|
||||
TopAppBar( |
|
||||
title = { |
|
||||
Box( |
|
||||
modifier = Modifier.fillMaxWidth(), |
|
||||
contentAlignment = Alignment.Center |
|
||||
) { |
|
||||
Text( |
|
||||
text = "VibeCheck", |
|
||||
fontWeight = FontWeight.Bold, |
|
||||
color = Color(0xFF6B008A), |
|
||||
fontSize = 20.sp |
|
||||
) |
|
||||
} |
|
||||
}, |
|
||||
navigationIcon = { |
|
||||
IconButton(onClick = onBackClick) { |
|
||||
Icon( |
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack, |
|
||||
contentDescription = "Back", |
|
||||
tint = Color.Black |
|
||||
) |
|
||||
} |
|
||||
}, |
|
||||
actions = { |
|
||||
TextButton(onClick = onSkipClick) { |
|
||||
Text( |
|
||||
text = "SKIP", |
|
||||
color = Color.Gray, |
|
||||
fontSize = 12.sp, |
|
||||
fontWeight = FontWeight.Bold |
|
||||
) |
|
||||
} |
|
||||
}, |
|
||||
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.White) |
|
||||
) |
|
||||
}, |
|
||||
bottomBar = { |
|
||||
Column( |
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.padding(16.dp) |
|
||||
) { |
|
||||
Button( |
|
||||
onClick = onUploadClick, |
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.height(50.dp) |
|
||||
.background( |
|
||||
brush = Brush.horizontalGradient( |
|
||||
colors = listOf(Color(0xFFC2003B), Color(0xFF6B008A)) |
|
||||
), |
|
||||
shape = RoundedCornerShape(12.dp) |
|
||||
), |
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent), |
|
||||
shape = RoundedCornerShape(12.dp) |
|
||||
) { |
|
||||
Text( |
|
||||
text = "Upload", |
|
||||
color = Color.White, |
|
||||
fontWeight = FontWeight.Bold, |
|
||||
fontSize = 16.sp, |
|
||||
letterSpacing = 1.sp |
|
||||
) |
|
||||
} |
|
||||
Spacer(Modifier.height(10.dp)) |
|
||||
Button( |
|
||||
onClick = onUploadClick, |
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.height(50.dp) |
|
||||
.border( |
|
||||
width = 1.dp, |
|
||||
brush = Brush.horizontalGradient( |
|
||||
colors = listOf(Color(0xFFC2003B), Color(0xFF6B008A)) |
|
||||
), |
|
||||
shape = RoundedCornerShape(12.dp) |
|
||||
), |
|
||||
colors = ButtonDefaults.buttonColors( |
|
||||
containerColor = Color.Transparent |
|
||||
), |
|
||||
shape = RoundedCornerShape(12.dp) |
|
||||
) { |
|
||||
Text( |
|
||||
text = "Skip Now", |
|
||||
color = Color(0xFF6B008A), |
|
||||
fontWeight = FontWeight.Bold, |
|
||||
fontSize = 16.sp, |
|
||||
letterSpacing = 1.sp |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
) { innerPadding -> |
|
||||
Column( |
|
||||
modifier = Modifier |
|
||||
.fillMaxSize() |
|
||||
.background(Color.White) |
|
||||
.padding(innerPadding) |
|
||||
.padding(horizontal = 20.dp), |
|
||||
horizontalAlignment = Alignment.CenterHorizontally |
|
||||
) { |
|
||||
Spacer(modifier = Modifier.height(8.dp)) |
|
||||
|
|
||||
Text( |
|
||||
text = "Video Introduction", |
|
||||
fontSize = 22.sp, |
|
||||
fontWeight = FontWeight.Bold, |
|
||||
color = Color(0xFF222222) |
|
||||
) |
|
||||
|
|
||||
Spacer(modifier = Modifier.height(6.dp)) |
|
||||
|
|
||||
Text( |
|
||||
text = "Say hello! Share a bit about yourself in 60 seconds.", |
|
||||
fontSize = 13.sp, |
|
||||
color = Color.Gray, |
|
||||
textAlign = TextAlign.Center |
|
||||
) |
|
||||
|
|
||||
Spacer(modifier = Modifier.height(16.dp)) |
|
||||
|
|
||||
// Main Video/Camera Viewport Frame |
|
||||
Box( |
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.weight(1f) |
|
||||
.clip(RoundedCornerShape(20.dp)) |
|
||||
) { |
|
||||
// Background Video Preview Image |
|
||||
Image( |
|
||||
painter = painterResource(id = R.drawable.ic_launcher_background), // Replace with your image/Camera Preview |
|
||||
contentDescription = "Camera Feed", |
|
||||
contentScale = ContentScale.Crop, |
|
||||
modifier = Modifier.fillMaxSize() |
|
||||
) |
|
||||
|
|
||||
// Face Outline Overlay Guideline |
|
||||
val stroke = Stroke( |
|
||||
width = 3f, |
|
||||
pathEffect = PathEffect.dashPathEffect(floatArrayOf(12f, 12f), 0f) |
|
||||
) |
|
||||
Canvas(modifier = Modifier.fillMaxSize()) { |
|
||||
drawOval( |
|
||||
color = Color.White.copy(alpha = 0.5f), |
|
||||
topLeft = androidx.compose.ui.geometry.Offset(size.width * 0.15f, size.height * 0.1f), |
|
||||
size = androidx.compose.ui.geometry.Size(size.width * 0.7f, size.height * 0.62f), |
|
||||
style = stroke |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
// Timer Badge Overlay |
|
||||
Surface( |
|
||||
color = Color.White.copy(alpha = 0.75f), |
|
||||
shape = RoundedCornerShape(16.dp), |
|
||||
modifier = Modifier |
|
||||
.align(Alignment.TopEnd) |
|
||||
.padding(16.dp) |
|
||||
) { |
|
||||
Text( |
|
||||
text = "00:00 / 01:00", |
|
||||
color = Color(0xFF222222), |
|
||||
fontSize = 12.sp, |
|
||||
fontWeight = FontWeight.SemiBold, |
|
||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp) |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
// Bottom Controls Inside Preview Frame |
|
||||
Row( |
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.align(Alignment.BottomCenter) |
|
||||
.padding(bottom = 20.dp), |
|
||||
horizontalArrangement = Arrangement.SpaceEvenly, |
|
||||
verticalAlignment = Alignment.CenterVertically |
|
||||
) { |
|
||||
// Upload File Button |
|
||||
IconButton( |
|
||||
onClick = onSelectFileClick, |
|
||||
modifier = Modifier |
|
||||
.size(48.dp) |
|
||||
.background(Color.White.copy(alpha = 0.65f), CircleShape) |
|
||||
) { |
|
||||
Icon( |
|
||||
imageVector = Icons.Default.FileUpload, |
|
||||
contentDescription = "Upload File", |
|
||||
tint = Color.DarkGray |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
// Main Record Button |
|
||||
IconButton( |
|
||||
onClick = onRecordClick, |
|
||||
modifier = Modifier |
|
||||
.size(68.dp) |
|
||||
.border(3.dp, Color.White.copy(alpha = 0.8f), CircleShape) |
|
||||
.background(Color(0xFFC2003B), CircleShape) |
|
||||
) { |
|
||||
Icon( |
|
||||
imageVector = Icons.Default.Videocam, |
|
||||
contentDescription = "Record Video", |
|
||||
tint = Color.White, |
|
||||
modifier = Modifier.size(32.dp) |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
// Switch Camera / Rotate Button |
|
||||
IconButton( |
|
||||
onClick = onSwitchCameraClick, |
|
||||
modifier = Modifier |
|
||||
.size(48.dp) |
|
||||
.background(Color.White.copy(alpha = 0.65f), CircleShape) |
|
||||
) { |
|
||||
Icon( |
|
||||
imageVector = Icons.Default.Cameraswitch, |
|
||||
contentDescription = "Switch Camera", |
|
||||
tint = Color.DarkGray |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
Spacer(modifier = Modifier.height(12.dp)) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Preview(showBackground = true, device = "spec:width=411dp,height=891dp") |
|
||||
@Composable |
|
||||
fun VideoIntroScreenPreview() { |
|
||||
DattingAPPTheme { |
|
||||
VideoIntroScreen( |
|
||||
onBackClick = { |
|
||||
// TODO: Navigate back |
|
||||
}, |
|
||||
onSkipClick = { |
|
||||
// TODO: Skip video intro step |
|
||||
}, |
|
||||
onUploadClick = { |
|
||||
// TODO: Process final upload or proceed to next screen |
|
||||
}, |
|
||||
onRecordClick = { |
|
||||
// TODO: Start or stop recording video |
|
||||
}, |
|
||||
onSelectFileClick = { |
|
||||
// TODO: Launch video picker from gallery |
|
||||
}, |
|
||||
onSwitchCameraClick = { |
|
||||
// TODO: Switch between front and rear cameras |
|
||||
} |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,11 @@ |
|||||
|
package com.example.dattingapp.Login.ui.theme |
||||
|
|
||||
|
import androidx.compose.ui.graphics.Color |
||||
|
|
||||
|
val Purple80 = Color(0xFFD0BCFF) |
||||
|
val PurpleGrey80 = Color(0xFFCCC2DC) |
||||
|
val Pink80 = Color(0xFFEFB8C8) |
||||
|
|
||||
|
val Purple40 = Color(0xFF6650a4) |
||||
|
val PurpleGrey40 = Color(0xFF625b71) |
||||
|
val Pink40 = Color(0xFF7D5260) |
||||
@ -0,0 +1,58 @@ |
|||||
|
package com.example.dattingapp.Login.ui.theme |
||||
|
|
||||
|
import android.app.Activity |
||||
|
import android.os.Build |
||||
|
import androidx.compose.foundation.isSystemInDarkTheme |
||||
|
import androidx.compose.material3.MaterialTheme |
||||
|
import androidx.compose.material3.darkColorScheme |
||||
|
import androidx.compose.material3.dynamicDarkColorScheme |
||||
|
import androidx.compose.material3.dynamicLightColorScheme |
||||
|
import androidx.compose.material3.lightColorScheme |
||||
|
import androidx.compose.runtime.Composable |
||||
|
import androidx.compose.ui.platform.LocalContext |
||||
|
|
||||
|
private val DarkColorScheme = darkColorScheme( |
||||
|
primary = Purple80, |
||||
|
secondary = PurpleGrey80, |
||||
|
tertiary = Pink80 |
||||
|
) |
||||
|
|
||||
|
private val LightColorScheme = lightColorScheme( |
||||
|
primary = Purple40, |
||||
|
secondary = PurpleGrey40, |
||||
|
tertiary = Pink40 |
||||
|
|
||||
|
/* Other default colors to override |
||||
|
background = Color(0xFFFFFBFE), |
||||
|
surface = Color(0xFFFFFBFE), |
||||
|
onPrimary = Color.White, |
||||
|
onSecondary = Color.White, |
||||
|
onTertiary = Color.White, |
||||
|
onBackground = Color(0xFF1C1B1F), |
||||
|
onSurface = Color(0xFF1C1B1F), |
||||
|
*/ |
||||
|
) |
||||
|
|
||||
|
@Composable |
||||
|
fun DattingAPPTheme( |
||||
|
darkTheme: Boolean = isSystemInDarkTheme(), |
||||
|
// Dynamic color is available on Android 12+ |
||||
|
dynamicColor: Boolean = true, |
||||
|
content: @Composable () -> Unit |
||||
|
) { |
||||
|
val colorScheme = when { |
||||
|
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { |
||||
|
val context = LocalContext.current |
||||
|
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) |
||||
|
} |
||||
|
|
||||
|
darkTheme -> DarkColorScheme |
||||
|
else -> LightColorScheme |
||||
|
} |
||||
|
|
||||
|
MaterialTheme( |
||||
|
colorScheme = colorScheme, |
||||
|
typography = Typography, |
||||
|
content = content |
||||
|
) |
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
package com.example.dattingapp.Login.ui.theme |
||||
|
|
||||
|
import androidx.compose.material3.Typography |
||||
|
import androidx.compose.ui.text.TextStyle |
||||
|
import androidx.compose.ui.text.font.FontFamily |
||||
|
import androidx.compose.ui.text.font.FontWeight |
||||
|
import androidx.compose.ui.unit.sp |
||||
|
|
||||
|
// Set of Material typography styles to start with |
||||
|
val Typography = Typography( |
||||
|
bodyLarge = TextStyle( |
||||
|
fontFamily = FontFamily.Default, |
||||
|
fontWeight = FontWeight.Normal, |
||||
|
fontSize = 16.sp, |
||||
|
lineHeight = 24.sp, |
||||
|
letterSpacing = 0.5.sp |
||||
|
) |
||||
|
/* Other default text styles to override |
||||
|
titleLarge = TextStyle( |
||||
|
fontFamily = FontFamily.Default, |
||||
|
fontWeight = FontWeight.Normal, |
||||
|
fontSize = 22.sp, |
||||
|
lineHeight = 28.sp, |
||||
|
letterSpacing = 0.sp |
||||
|
), |
||||
|
labelSmall = TextStyle( |
||||
|
fontFamily = FontFamily.Default, |
||||
|
fontWeight = FontWeight.Medium, |
||||
|
fontSize = 11.sp, |
||||
|
lineHeight = 16.sp, |
||||
|
letterSpacing = 0.5.sp |
||||
|
) |
||||
|
*/ |
||||
|
) |
||||
@ -0,0 +1,479 @@ |
|||||
|
package com.example.dattingapp |
||||
|
|
||||
|
import android.os.Bundle |
||||
|
import androidx.activity.ComponentActivity |
||||
|
import androidx.activity.compose.setContent |
||||
|
import androidx.activity.enableEdgeToEdge |
||||
|
import androidx.compose.foundation.background |
||||
|
import androidx.compose.foundation.border |
||||
|
import androidx.compose.foundation.layout.* |
||||
|
import androidx.compose.foundation.lazy.LazyColumn |
||||
|
import androidx.compose.foundation.lazy.LazyRow |
||||
|
import androidx.compose.foundation.lazy.items |
||||
|
import androidx.compose.foundation.shape.CircleShape |
||||
|
import androidx.compose.foundation.shape.RoundedCornerShape |
||||
|
import androidx.compose.material.icons.Icons |
||||
|
import androidx.compose.material.icons.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.text.font.FontWeight |
||||
|
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.DattingAPPTheme |
||||
|
import com.example.dattingapp.ui.theme.lightChipBg |
||||
|
import com.example.dattingapp.ui.theme.primaryColor |
||||
|
|
||||
|
class MeetActivity : ComponentActivity() { |
||||
|
override fun onCreate(savedInstanceState: Bundle?) { |
||||
|
super.onCreate(savedInstanceState) |
||||
|
enableEdgeToEdge() |
||||
|
setContent { |
||||
|
DattingAPPTheme { |
||||
|
VibeCheckHomeScreen() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Data Models |
||||
|
data class VibeCategory( |
||||
|
val id: String, |
||||
|
val name: String, |
||||
|
val isSelected: Boolean = false |
||||
|
) |
||||
|
|
||||
|
data class EventCardData( |
||||
|
val id: String, |
||||
|
val title: String, |
||||
|
val description: String, |
||||
|
val imageUrl: String, |
||||
|
val tagLabel: String, |
||||
|
val isTrending: Boolean = false, |
||||
|
val tags: List<String> = emptyList() |
||||
|
) |
||||
|
|
||||
|
data class UserMatch( |
||||
|
val id: String, |
||||
|
val name: String, |
||||
|
val matchPercentage: Int, |
||||
|
val avatarUrl: String |
||||
|
) |
||||
|
|
||||
|
@Composable |
||||
|
fun VibeCheckHomeScreen( |
||||
|
modifier: Modifier = Modifier |
||||
|
) { |
||||
|
Scaffold( |
||||
|
modifier = modifier.fillMaxSize(), |
||||
|
topBar = { |
||||
|
VibeCheckTopAppBar(primaryColor) |
||||
|
} |
||||
|
) { innerPadding -> |
||||
|
LazyColumn( |
||||
|
modifier = Modifier |
||||
|
.fillMaxSize() |
||||
|
.padding(innerPadding) |
||||
|
.padding(horizontal = 16.dp), |
||||
|
verticalArrangement = Arrangement.spacedBy(16.dp) |
||||
|
) { |
||||
|
item { |
||||
|
SearchBarSection() |
||||
|
} |
||||
|
|
||||
|
item { |
||||
|
VibeFilterChipsSection( |
||||
|
selectedColor = primaryColor, |
||||
|
unselectedBg = lightChipBg |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
item { |
||||
|
FeaturedEventCard( |
||||
|
data = EventCardData( |
||||
|
id = "1", |
||||
|
title = "Coffee & Pottery", |
||||
|
description = "Get your hands dirty and your caffeine fix. Perfect for a low-pressure first date.", |
||||
|
imageUrl = "", |
||||
|
tagLabel = "Live", |
||||
|
isTrending = false, |
||||
|
tags = listOf("Creative", "Chill") |
||||
|
), |
||||
|
badgeColor = primaryColor |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
item { |
||||
|
MostMatchingSection(primaryColor) |
||||
|
} |
||||
|
|
||||
|
item { |
||||
|
FeaturedEventCard( |
||||
|
data = EventCardData( |
||||
|
id = "2", |
||||
|
title = "Sunset Beach Volley", |
||||
|
description = "Join a casual pickup game down at the pier. Bring sunscreen and good vibes.", |
||||
|
imageUrl = "", |
||||
|
tagLabel = "TRENDING", |
||||
|
isTrending = true |
||||
|
), |
||||
|
badgeColor = Color(0xFF7C4DFF) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
item { |
||||
|
Spacer(modifier = Modifier.height(16.dp)) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Composable |
||||
|
private fun VibeCheckTopAppBar( |
||||
|
primaryColor: Color |
||||
|
) { |
||||
|
Row( |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.padding(horizontal = 16.dp, vertical = 12.dp), |
||||
|
horizontalArrangement = Arrangement.SpaceBetween, |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
IconButton(onClick = { }) { |
||||
|
Icon( |
||||
|
imageVector = Icons.Default.Add, |
||||
|
contentDescription = "Add" |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
Row(verticalAlignment = Alignment.CenterVertically) { |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.size(32.dp) |
||||
|
.clip(CircleShape) |
||||
|
.background(Color.Gray) |
||||
|
) |
||||
|
|
||||
|
Spacer(modifier = Modifier.width(8.dp)) |
||||
|
|
||||
|
Text( |
||||
|
text = "VibeCheck", |
||||
|
color = primaryColor, |
||||
|
fontSize = 20.sp, |
||||
|
fontWeight = FontWeight.Bold |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
IconButton(onClick = { }) { |
||||
|
Icon( |
||||
|
imageVector = Icons.Default.Search, |
||||
|
contentDescription = "Search" |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Composable |
||||
|
private fun SearchBarSection() { |
||||
|
var searchText by remember { mutableStateOf("") } |
||||
|
|
||||
|
OutlinedTextField( |
||||
|
value = searchText, |
||||
|
onValueChange = { searchText = it }, |
||||
|
placeholder = { |
||||
|
Text(text = "Search", color = Color.Gray) |
||||
|
}, |
||||
|
leadingIcon = { |
||||
|
Icon( |
||||
|
imageVector = Icons.Default.Search, |
||||
|
contentDescription = null, |
||||
|
tint = Color.Gray |
||||
|
) |
||||
|
}, |
||||
|
trailingIcon = { |
||||
|
Row(modifier = Modifier.padding(end = 8.dp)) { |
||||
|
Icon( |
||||
|
imageVector = Icons.Outlined.Image, |
||||
|
contentDescription = null, |
||||
|
tint = Color.Gray |
||||
|
) |
||||
|
Spacer(modifier = Modifier.width(8.dp)) |
||||
|
Icon( |
||||
|
imageVector = Icons.Outlined.LocationOn, |
||||
|
contentDescription = null, |
||||
|
tint = Color.Gray |
||||
|
) |
||||
|
} |
||||
|
}, |
||||
|
shape = RoundedCornerShape(24.dp), |
||||
|
colors = OutlinedTextFieldDefaults.colors( |
||||
|
unfocusedContainerColor = Color(0xFFF5F5F5), |
||||
|
focusedContainerColor = Color(0xFFF5F5F5), |
||||
|
unfocusedBorderColor = Color.Transparent, |
||||
|
focusedBorderColor = Color.Transparent |
||||
|
), |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.height(50.dp) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
@Composable |
||||
|
private fun VibeFilterChipsSection( |
||||
|
selectedColor: Color, |
||||
|
unselectedBg: Color |
||||
|
) { |
||||
|
val categories = remember { |
||||
|
listOf( |
||||
|
VibeCategory(id = "1", name = "All Vibes", isSelected = true), |
||||
|
VibeCategory(id = "2", name = "Chill"), |
||||
|
VibeCategory(id = "3", name = "Active"), |
||||
|
VibeCategory(id = "4", name = "Creative"), |
||||
|
VibeCategory(id = "5", name = "Nightlife") |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
LazyRow( |
||||
|
modifier = Modifier.fillMaxWidth(), |
||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp) |
||||
|
) { |
||||
|
items(categories, key = { it.id }) { category -> |
||||
|
Surface( |
||||
|
shape = RoundedCornerShape(16.dp), |
||||
|
color = if (category.isSelected) selectedColor else unselectedBg, |
||||
|
modifier = Modifier.height(32.dp) |
||||
|
) { |
||||
|
Box( |
||||
|
contentAlignment = Alignment.Center, |
||||
|
modifier = Modifier.padding(horizontal = 14.dp) |
||||
|
) { |
||||
|
Text( |
||||
|
text = category.name, |
||||
|
color = if (category.isSelected) Color.White else selectedColor, |
||||
|
fontSize = 12.sp, |
||||
|
fontWeight = FontWeight.SemiBold |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Composable |
||||
|
private fun FeaturedEventCard( |
||||
|
data: EventCardData, |
||||
|
badgeColor: Color |
||||
|
) { |
||||
|
Card( |
||||
|
shape = RoundedCornerShape(16.dp), |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.height(360.dp) |
||||
|
) { |
||||
|
Box(modifier = Modifier.fillMaxSize()) { |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.fillMaxSize() |
||||
|
.background(Color.DarkGray) |
||||
|
) |
||||
|
|
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.fillMaxSize() |
||||
|
.background( |
||||
|
Brush.verticalGradient( |
||||
|
colors = listOf( |
||||
|
Color.Transparent, |
||||
|
Color.Black.copy(alpha = 0.85f) |
||||
|
), |
||||
|
startY = 200f |
||||
|
) |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
Surface( |
||||
|
color = badgeColor, |
||||
|
shape = RoundedCornerShape(12.dp), |
||||
|
modifier = Modifier |
||||
|
.padding(12.dp) |
||||
|
.align(Alignment.TopStart) |
||||
|
) { |
||||
|
Row( |
||||
|
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp), |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
Icon( |
||||
|
imageVector = if (!data.isTrending) Icons.Default.RadioButtonChecked else Icons.Default.Bolt, |
||||
|
contentDescription = null, |
||||
|
tint = Color.White, |
||||
|
modifier = Modifier.size(12.dp) |
||||
|
) |
||||
|
Spacer(modifier = Modifier.width(4.dp)) |
||||
|
Text( |
||||
|
text = data.tagLabel, |
||||
|
color = Color.White, |
||||
|
fontSize = 10.sp, |
||||
|
fontWeight = FontWeight.Bold |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (!data.isTrending) { |
||||
|
Column( |
||||
|
modifier = Modifier |
||||
|
.align(Alignment.CenterEnd) |
||||
|
.padding(end = 12.dp), |
||||
|
verticalArrangement = Arrangement.spacedBy(12.dp) |
||||
|
) { |
||||
|
IconButton(onClick = { }) { |
||||
|
Icon( |
||||
|
imageVector = Icons.Outlined.ThumbUp, |
||||
|
contentDescription = "Like", |
||||
|
tint = Color.White |
||||
|
) |
||||
|
} |
||||
|
IconButton(onClick = { }) { |
||||
|
Icon( |
||||
|
imageVector = Icons.Outlined.ChatBubbleOutline, |
||||
|
contentDescription = "Comment", |
||||
|
tint = Color.White |
||||
|
) |
||||
|
} |
||||
|
IconButton(onClick = { }) { |
||||
|
Icon( |
||||
|
imageVector = Icons.Outlined.BookmarkBorder, |
||||
|
contentDescription = "Save", |
||||
|
tint = Color.White |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Column( |
||||
|
modifier = Modifier |
||||
|
.align(Alignment.BottomStart) |
||||
|
.padding(16.dp) |
||||
|
) { |
||||
|
Text( |
||||
|
text = data.title, |
||||
|
color = Color.White, |
||||
|
fontSize = 20.sp, |
||||
|
fontWeight = FontWeight.Bold |
||||
|
) |
||||
|
|
||||
|
Spacer(modifier = Modifier.height(4.dp)) |
||||
|
|
||||
|
Text( |
||||
|
text = data.description, |
||||
|
color = Color.LightGray, |
||||
|
fontSize = 12.sp, |
||||
|
lineHeight = 16.sp |
||||
|
) |
||||
|
|
||||
|
if (data.tags.isNotEmpty()) { |
||||
|
Spacer(modifier = Modifier.height(8.dp)) |
||||
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { |
||||
|
data.tags.forEach { tag -> |
||||
|
Surface( |
||||
|
color = Color.White.copy(alpha = 0.2f), |
||||
|
shape = RoundedCornerShape(6.dp) |
||||
|
) { |
||||
|
Text( |
||||
|
text = tag, |
||||
|
color = Color.White, |
||||
|
fontSize = 10.sp, |
||||
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Composable |
||||
|
private fun MostMatchingSection( |
||||
|
primaryColor: Color |
||||
|
) { |
||||
|
val matches = remember { |
||||
|
listOf( |
||||
|
UserMatch("1", "Sarah", 98, ""), |
||||
|
UserMatch("2", "James", 94, ""), |
||||
|
UserMatch("3", "Elena", 92, ""), |
||||
|
UserMatch("4", "Marcus", 88, ""), |
||||
|
UserMatch("5", "Maya", 82, "") |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
Column(modifier = Modifier.fillMaxWidth()) { |
||||
|
Text( |
||||
|
text = "Most matching", |
||||
|
fontSize = 18.sp, |
||||
|
fontWeight = FontWeight.Bold, |
||||
|
modifier = Modifier.padding(bottom = 12.dp) |
||||
|
) |
||||
|
|
||||
|
LazyRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) { |
||||
|
items(matches, key = { it.id }) { user -> |
||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) { |
||||
|
Box(contentAlignment = Alignment.BottomEnd) { |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.size(56.dp) |
||||
|
.clip(CircleShape) |
||||
|
.border(2.dp, primaryColor, CircleShape) |
||||
|
.background(Color.LightGray) |
||||
|
) |
||||
|
|
||||
|
Surface( |
||||
|
color = primaryColor, |
||||
|
shape = RoundedCornerShape(8.dp), |
||||
|
modifier = Modifier.offset(x = 4.dp, y = 4.dp) |
||||
|
) { |
||||
|
Text( |
||||
|
text = "${user.matchPercentage}%", |
||||
|
color = Color.White, |
||||
|
fontSize = 8.sp, |
||||
|
fontWeight = FontWeight.Bold, |
||||
|
modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp) |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Spacer(modifier = Modifier.height(6.dp)) |
||||
|
|
||||
|
Text( |
||||
|
text = user.name, |
||||
|
fontSize = 12.sp, |
||||
|
fontWeight = FontWeight.Medium |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Preview( |
||||
|
showBackground = true, |
||||
|
showSystemUi = true, |
||||
|
widthDp = 360, |
||||
|
heightDp = 800 |
||||
|
) |
||||
|
@Composable |
||||
|
fun VibeCheckHomeScreenPreview() { |
||||
|
MaterialTheme { |
||||
|
Surface(modifier = Modifier.fillMaxSize()) { |
||||
|
VibeCheckHomeScreen( |
||||
|
modifier = Modifier.fillMaxSize() |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,944 @@ |
|||||
|
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.background |
||||
|
import androidx.compose.foundation.clickable |
||||
|
import androidx.compose.foundation.layout.* |
||||
|
import androidx.compose.foundation.shape.CircleShape |
||||
|
import androidx.compose.foundation.shape.RoundedCornerShape |
||||
|
import androidx.compose.material.icons.Icons |
||||
|
import androidx.compose.material.icons.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.material3.* |
||||
|
import androidx.compose.runtime.* |
||||
|
import androidx.compose.ui.platform.LocalContext |
||||
|
import androidx.compose.ui.text.style.TextAlign |
||||
|
import androidx.compose.ui.unit.dp |
||||
|
import androidx.compose.ui.unit.sp |
||||
|
|
||||
|
// 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,439 @@ |
|||||
|
package com.example.dattingapp |
||||
|
|
||||
|
import android.content.Context |
||||
|
import android.net.Uri |
||||
|
import android.os.Bundle |
||||
|
import android.widget.Toast |
||||
|
import androidx.activity.ComponentActivity |
||||
|
import androidx.activity.compose.rememberLauncherForActivityResult |
||||
|
import androidx.activity.compose.setContent |
||||
|
import androidx.activity.enableEdgeToEdge |
||||
|
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.shape.CircleShape |
||||
|
import androidx.compose.foundation.shape.RoundedCornerShape |
||||
|
import androidx.compose.material.icons.Icons |
||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack |
||||
|
import androidx.compose.material.icons.filled.FileUpload |
||||
|
import androidx.compose.material.icons.filled.PhotoCamera |
||||
|
import androidx.compose.material.icons.filled.Videocam |
||||
|
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.layout.ContentScale |
||||
|
import androidx.compose.ui.platform.LocalContext |
||||
|
import androidx.compose.ui.res.painterResource |
||||
|
import androidx.compose.ui.text.font.FontWeight |
||||
|
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.ui.theme.DattingAPPTheme |
||||
|
import java.io.File |
||||
|
|
||||
|
enum class DisplayMode { |
||||
|
IMAGE, |
||||
|
VIDEO, |
||||
|
BIO |
||||
|
} |
||||
|
|
||||
|
class UploadIntroVideoActivity : ComponentActivity() { |
||||
|
|
||||
|
override fun onCreate(savedInstanceState: Bundle?) { |
||||
|
super.onCreate(savedInstanceState) |
||||
|
enableEdgeToEdge() |
||||
|
|
||||
|
// Retrieve registration data from previous step |
||||
|
val isPhone = intent.getBooleanExtra("EXTRA_IS_PHONE", false) |
||||
|
val countryCode = intent.getStringExtra("EXTRA_COUNTRY_CODE") ?: "" |
||||
|
val contactValue = intent.getStringExtra("EXTRA_CONTACT_VALUE") ?: "" |
||||
|
val password = intent.getStringExtra("EXTRA_PASSWORD") ?: "" |
||||
|
val name = intent.getStringExtra("EXTRA_NAME") ?: "" |
||||
|
val dob = intent.getStringExtra("EXTRA_DOB") ?: "" |
||||
|
val gender = intent.getStringExtra("EXTRA_GENDER") ?: "" |
||||
|
val interests = intent.getStringArrayListExtra("EXTRA_INTERESTS") ?: arrayListOf() |
||||
|
val vibe = intent.getStringExtra("EXTRA_VIBE") ?: "" |
||||
|
|
||||
|
// Retrieve Profile Image URI string |
||||
|
val profileImageUriString = intent.getStringExtra("EXTRA_PROFILE_IMAGE_URI") |
||||
|
val profileImageUri: Uri? = profileImageUriString?.let { Uri.parse(it) } |
||||
|
|
||||
|
setContent { |
||||
|
DattingAPPTheme { |
||||
|
UploadIntroVideoScreen( |
||||
|
profileImageUri = profileImageUri, |
||||
|
onBackClick = { finish() }, |
||||
|
onSkipClick = { /* Handle skip */ }, |
||||
|
onSubmit = { videoUri, bioText -> |
||||
|
// Final registration submission logic |
||||
|
Toast.makeText(this, "Registration Data Ready!", Toast.LENGTH_SHORT).show() |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
@Composable |
||||
|
fun UploadIntroVideoScreen( |
||||
|
profileImageUri: Uri?, |
||||
|
onBackClick: () -> Unit = {}, |
||||
|
onSkipClick: () -> Unit = {}, |
||||
|
onSubmit: (videoUri: Uri?, bioText: String) -> Unit |
||||
|
) { |
||||
|
val context = LocalContext.current |
||||
|
var displayMode by remember { mutableStateOf(DisplayMode.IMAGE) } |
||||
|
var recordedVideoUri by remember { mutableStateOf<Uri?>(null) } |
||||
|
var bioText by remember { mutableStateOf("") } |
||||
|
|
||||
|
// Launchers remain the same... |
||||
|
val videoPickerLauncher = rememberLauncherForActivityResult( |
||||
|
contract = ActivityResultContracts.GetContent() |
||||
|
) { uri: Uri? -> |
||||
|
if (uri != null) { |
||||
|
recordedVideoUri = uri |
||||
|
displayMode = DisplayMode.VIDEO |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
val recordVideoLauncher = rememberLauncherForActivityResult( |
||||
|
contract = ActivityResultContracts.CaptureVideo() |
||||
|
) { success -> |
||||
|
if (success && recordedVideoUri != null) { |
||||
|
displayMode = DisplayMode.VIDEO |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var tempCameraPhotoUri by remember { mutableStateOf<Uri?>(null) } |
||||
|
var capturedPhotoUri by remember { mutableStateOf<Uri?>(null) } |
||||
|
val takePhotoLauncher = rememberLauncherForActivityResult( |
||||
|
contract = ActivityResultContracts.TakePicture() |
||||
|
) { success -> |
||||
|
if (success && tempCameraPhotoUri != null) { |
||||
|
capturedPhotoUri = tempCameraPhotoUri |
||||
|
displayMode = DisplayMode.IMAGE |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
val buttonGradient = Brush.horizontalGradient( |
||||
|
colors = listOf(Color(0xFFB70054), Color(0xFF6700C7)) |
||||
|
) |
||||
|
|
||||
|
Column( |
||||
|
modifier = Modifier |
||||
|
.fillMaxSize() |
||||
|
.background(Color(0xFFF9F9F9)) |
||||
|
.systemBarsPadding() |
||||
|
// 👇 ADD imePadding() HERE TO RESIZE WHEN KEYBOARD OPENS 👇 |
||||
|
.imePadding() |
||||
|
.padding(horizontal = 20.dp, vertical = 12.dp), |
||||
|
horizontalAlignment = Alignment.CenterHorizontally, |
||||
|
verticalArrangement = Arrangement.SpaceBetween |
||||
|
) { |
||||
|
// --- Header Section --- |
||||
|
Row( |
||||
|
modifier = Modifier.fillMaxWidth(), |
||||
|
horizontalArrangement = Arrangement.SpaceBetween, |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
IconButton(onClick = onBackClick) { |
||||
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", tint = Color.Black) |
||||
|
} |
||||
|
Text( |
||||
|
text = "VibeCheck", |
||||
|
fontSize = 22.sp, |
||||
|
fontWeight = FontWeight.Bold, |
||||
|
color = Color(0xFF9C27B0) |
||||
|
) |
||||
|
TextButton(onClick = onSkipClick) { |
||||
|
Text(text = "SKIP", color = Color.Gray, fontWeight = FontWeight.Bold, fontSize = 12.sp) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// --- Header Title --- |
||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) { |
||||
|
Text( |
||||
|
text = if (displayMode == DisplayMode.BIO) "Your Bio" else "Video Introduction", |
||||
|
fontSize = 22.sp, |
||||
|
fontWeight = FontWeight.Bold, |
||||
|
color = Color(0xFF1E293B) |
||||
|
) |
||||
|
Spacer(modifier = Modifier.height(4.dp)) |
||||
|
Text( |
||||
|
text = if (displayMode == DisplayMode.BIO) |
||||
|
"Write a short intro about yourself (up to 200 words)." |
||||
|
else |
||||
|
"Say hello! Share a bit about yourself in 60 seconds.", |
||||
|
fontSize = 13.sp, |
||||
|
color = Color.Gray |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// --- Central Media / Text Container --- |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.weight(1f) |
||||
|
.padding(vertical = 12.dp) |
||||
|
.clip(RoundedCornerShape(20.dp)) |
||||
|
.background(Color.LightGray), |
||||
|
contentAlignment = Alignment.Center |
||||
|
) { |
||||
|
when (displayMode) { |
||||
|
DisplayMode.IMAGE -> { |
||||
|
if (capturedPhotoUri != null || profileImageUri != null) { |
||||
|
Image( |
||||
|
painter = rememberAsyncImagePainter(model = capturedPhotoUri ?: profileImageUri), |
||||
|
contentDescription = "Profile Image Preview", |
||||
|
modifier = Modifier.fillMaxSize(), |
||||
|
contentScale = ContentScale.Crop |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
DisplayMode.VIDEO -> { |
||||
|
recordedVideoUri?.let { uri -> |
||||
|
VideoPlayer(videoUri = uri) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
DisplayMode.BIO -> { |
||||
|
OutlinedTextField( |
||||
|
value = bioText, |
||||
|
onValueChange = { input -> |
||||
|
val words = input.trim().split("\\s+".toRegex()) |
||||
|
if (input.isEmpty() || words.size <= 200) bioText = input |
||||
|
}, |
||||
|
modifier = Modifier |
||||
|
.fillMaxSize() |
||||
|
.background(Color.White) |
||||
|
.padding(12.dp), |
||||
|
placeholder = { Text("Write something about yourself (max 200 words)...") }, |
||||
|
colors = TextFieldDefaults.colors( |
||||
|
unfocusedContainerColor = Color.White, |
||||
|
focusedContainerColor = Color.White |
||||
|
) |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Overlay timer & action icons (hidden during Bio input) |
||||
|
if (displayMode != DisplayMode.BIO) { |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.padding(12.dp) |
||||
|
.align(Alignment.TopEnd) |
||||
|
.clip(RoundedCornerShape(12.dp)) |
||||
|
.background(Color.Black.copy(alpha = 0.5f)) |
||||
|
.padding(horizontal = 10.dp, vertical = 4.dp) |
||||
|
) { |
||||
|
Text( |
||||
|
text = "00:00 / 01:00", |
||||
|
color = Color.White, |
||||
|
fontSize = 11.sp, |
||||
|
fontWeight = FontWeight.SemiBold |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
Row( |
||||
|
modifier = Modifier |
||||
|
.align(Alignment.BottomCenter) |
||||
|
.padding(bottom = 20.dp), |
||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp), |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.size(48.dp) |
||||
|
.clip(CircleShape) |
||||
|
.background(Color.White.copy(alpha = 0.85f)) |
||||
|
.clickable { videoPickerLauncher.launch("video/*") }, |
||||
|
contentAlignment = Alignment.Center |
||||
|
) { |
||||
|
Icon( |
||||
|
imageVector = Icons.Default.FileUpload, |
||||
|
contentDescription = "Upload Gallery Video", |
||||
|
tint = Color(0xFF475569), |
||||
|
modifier = Modifier.size(24.dp) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.size(64.dp) |
||||
|
.clip(CircleShape) |
||||
|
.background(Color.White.copy(alpha = 0.9f)) |
||||
|
.border(2.dp, Color(0xFFE11D48), CircleShape) |
||||
|
.clickable { |
||||
|
val tempUri = createVideoUri(context) |
||||
|
recordedVideoUri = tempUri |
||||
|
recordVideoLauncher.launch(tempUri) |
||||
|
}, |
||||
|
contentAlignment = Alignment.Center |
||||
|
) { |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.size(48.dp) |
||||
|
.clip(CircleShape) |
||||
|
.background(Color(0xFFE11D48)), |
||||
|
contentAlignment = Alignment.Center |
||||
|
) { |
||||
|
Icon( |
||||
|
imageVector = Icons.Default.Videocam, |
||||
|
contentDescription = "Record Video", |
||||
|
tint = Color.White, |
||||
|
modifier = Modifier.size(26.dp) |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.size(48.dp) |
||||
|
.clip(CircleShape) |
||||
|
.background(Color.White.copy(alpha = 0.85f)) |
||||
|
.clickable { |
||||
|
val tempUri = createImageUri(context) |
||||
|
tempCameraPhotoUri = tempUri |
||||
|
takePhotoLauncher.launch(tempUri) |
||||
|
}, |
||||
|
contentAlignment = Alignment.Center |
||||
|
) { |
||||
|
Icon( |
||||
|
imageVector = Icons.Default.PhotoCamera, |
||||
|
contentDescription = "Take Photo", |
||||
|
tint = Color(0xFF475569), |
||||
|
modifier = Modifier.size(24.dp) |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// --- Bottom Buttons --- |
||||
|
Column( |
||||
|
modifier = Modifier.fillMaxWidth(), |
||||
|
verticalArrangement = Arrangement.spacedBy(10.dp) |
||||
|
) { |
||||
|
Button( |
||||
|
onClick = { onSubmit(recordedVideoUri, bioText) }, |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.height(50.dp) |
||||
|
.background(buttonGradient, shape = RoundedCornerShape(12.dp)), |
||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent), |
||||
|
shape = RoundedCornerShape(12.dp) |
||||
|
) { |
||||
|
Text(text = "UPLOAD", color = Color.White, fontWeight = FontWeight.Bold, fontSize = 16.sp) |
||||
|
} |
||||
|
|
||||
|
OutlinedButton( |
||||
|
onClick = { |
||||
|
displayMode = if (displayMode == DisplayMode.BIO) { |
||||
|
if (recordedVideoUri != null) DisplayMode.VIDEO else DisplayMode.IMAGE |
||||
|
} else { |
||||
|
DisplayMode.BIO |
||||
|
} |
||||
|
}, |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.height(50.dp), |
||||
|
shape = RoundedCornerShape(12.dp), |
||||
|
border = ButtonDefaults.outlinedButtonBorder.copy( |
||||
|
brush = Brush.horizontalGradient(listOf(Color(0xFFE11D48), Color(0xFFE11D48))) |
||||
|
) |
||||
|
) { |
||||
|
Text( |
||||
|
text = if (displayMode == DisplayMode.BIO) "VIDEO INTRO" else "ADD BIO", |
||||
|
color = Color(0xFFE11D48), |
||||
|
fontWeight = FontWeight.Bold, |
||||
|
fontSize = 14.sp |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Media3 ExoPlayer composable |
||||
|
@Composable |
||||
|
fun VideoPlayer(videoUri: Uri) { |
||||
|
val context = LocalContext.current |
||||
|
val exoPlayer = remember(videoUri) { |
||||
|
ExoPlayer.Builder(context).build().apply { |
||||
|
setMediaItem(MediaItem.fromUri(videoUri)) |
||||
|
prepare() |
||||
|
playWhenReady = true |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
DisposableEffect(Unit) { |
||||
|
onDispose { |
||||
|
exoPlayer.release() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
AndroidView( |
||||
|
factory = { ctx -> |
||||
|
PlayerView(ctx).apply { |
||||
|
player = exoPlayer |
||||
|
useController = true |
||||
|
} |
||||
|
}, |
||||
|
modifier = Modifier.fillMaxSize() |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// Helper functions to create temporary File Uris for Video and Image captures |
||||
|
fun createVideoUri(context: Context): Uri { |
||||
|
val tempFile = File.createTempFile("intro_video_", ".mp4", context.cacheDir).apply { |
||||
|
createNewFile() |
||||
|
deleteOnExit() |
||||
|
} |
||||
|
return FileProvider.getUriForFile( |
||||
|
context, |
||||
|
"${context.packageName}.provider", |
||||
|
tempFile |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
fun createImageUrl(context: Context): Uri { |
||||
|
val tempFile = File.createTempFile("intro_photo_", ".jpg", context.cacheDir).apply { |
||||
|
createNewFile() |
||||
|
deleteOnExit() |
||||
|
} |
||||
|
return FileProvider.getUriForFile( |
||||
|
context, |
||||
|
"${context.packageName}.provider", |
||||
|
tempFile |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// --- Composable Previews --- |
||||
|
@Preview( |
||||
|
name = "Upload Intro Screen - Full Preview", |
||||
|
showBackground = true, |
||||
|
showSystemUi = true |
||||
|
) |
||||
|
@Composable |
||||
|
fun UploadIntroVideoScreenPreview() { |
||||
|
DattingAPPTheme { |
||||
|
UploadIntroVideoScreen( |
||||
|
profileImageUri = null, |
||||
|
onSubmit = { _, _ -> } |
||||
|
) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,417 @@ |
|||||
|
package com.example.dattingapp |
||||
|
|
||||
|
import android.content.Context |
||||
|
import android.content.Intent |
||||
|
import android.net.Uri |
||||
|
import android.os.Bundle |
||||
|
import android.util.Log |
||||
|
import android.widget.Toast |
||||
|
import androidx.activity.ComponentActivity |
||||
|
import androidx.activity.compose.rememberLauncherForActivityResult |
||||
|
import androidx.activity.compose.setContent |
||||
|
import androidx.activity.enableEdgeToEdge |
||||
|
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.shape.CircleShape |
||||
|
import androidx.compose.foundation.shape.RoundedCornerShape |
||||
|
import androidx.compose.material.icons.Icons |
||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack |
||||
|
import androidx.compose.material.icons.outlined.AddAPhoto |
||||
|
import androidx.compose.material.icons.outlined.CameraAlt |
||||
|
import androidx.compose.material.icons.outlined.PhotoLibrary |
||||
|
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.draw.drawWithContent |
||||
|
import androidx.compose.ui.graphics.Brush |
||||
|
import androidx.compose.ui.graphics.Color |
||||
|
import androidx.compose.ui.graphics.PathEffect |
||||
|
import androidx.compose.ui.graphics.drawscope.Stroke |
||||
|
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.style.TextAlign |
||||
|
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.core.content.FileProvider |
||||
|
import coil.compose.rememberAsyncImagePainter |
||||
|
import java.io.File |
||||
|
|
||||
|
class UploadProfileImageActivity : ComponentActivity() { |
||||
|
|
||||
|
private val TAG = "kk" |
||||
|
|
||||
|
override fun onCreate(savedInstanceState: Bundle?) { |
||||
|
enableEdgeToEdge() |
||||
|
super.onCreate(savedInstanceState) |
||||
|
|
||||
|
// Retrieve intent extras passed from previous registration steps |
||||
|
val isPhone = intent.getBooleanExtra("EXTRA_IS_PHONE", false) |
||||
|
val countryCode = intent.getStringExtra("EXTRA_COUNTRY_CODE") ?: "" |
||||
|
val contactValue = intent.getStringExtra("EXTRA_CONTACT_VALUE") ?: "" |
||||
|
val password = intent.getStringExtra("EXTRA_PASSWORD") ?: "" |
||||
|
val name = intent.getStringExtra("EXTRA_NAME") ?: "" |
||||
|
val dob = intent.getStringExtra("EXTRA_DOB") ?: "" |
||||
|
val gender = intent.getStringExtra("EXTRA_GENDER") ?: "" |
||||
|
val interests = intent.getStringArrayListExtra("EXTRA_INTERESTS") ?: arrayListOf() |
||||
|
val vibe = intent.getStringExtra("EXTRA_VIBE") ?: "" |
||||
|
|
||||
|
Log.d(TAG, "=== Received Registration Data ===") |
||||
|
Log.d(TAG, "Is Phone: $isPhone") |
||||
|
Log.d(TAG, "Country Code: $countryCode") |
||||
|
Log.d(TAG, "Contact Value: $contactValue") |
||||
|
Log.d(TAG, "Password: $password") |
||||
|
Log.d(TAG, "Name: $name") |
||||
|
Log.d(TAG, "Date of Birth: $dob") |
||||
|
Log.d(TAG, "Gender: $gender") |
||||
|
Log.d(TAG, "Interests: $interests") |
||||
|
Log.d(TAG, "Vibe: $vibe") |
||||
|
Log.d(TAG, "=================================") |
||||
|
|
||||
|
setContent { |
||||
|
MaterialTheme { |
||||
|
UploadProfileImageScreen( |
||||
|
onBackClick = { finish() }, |
||||
|
onContinueClick = { selectedUri -> |
||||
|
if (selectedUri == null) { |
||||
|
Toast.makeText(this@UploadProfileImageActivity, "Please select or take a photo first", Toast.LENGTH_SHORT).show() |
||||
|
return@UploadProfileImageScreen |
||||
|
} |
||||
|
|
||||
|
val intent = Intent( |
||||
|
this@UploadProfileImageActivity, |
||||
|
UploadIntroVideoActivity::class.java |
||||
|
).apply { |
||||
|
// 1. Pass down all data received from previous steps |
||||
|
putExtra("EXTRA_IS_PHONE", isPhone) |
||||
|
putExtra("EXTRA_COUNTRY_CODE", countryCode) |
||||
|
putExtra("EXTRA_CONTACT_VALUE", contactValue) |
||||
|
putExtra("EXTRA_PASSWORD", password) |
||||
|
putExtra("EXTRA_NAME", name) |
||||
|
putExtra("EXTRA_DOB", dob) |
||||
|
putExtra("EXTRA_GENDER", gender) |
||||
|
putStringArrayListExtra("EXTRA_INTERESTS", interests) |
||||
|
putExtra("EXTRA_VIBE", vibe) |
||||
|
|
||||
|
// 2. Pass the selected Profile Image Uri as a String |
||||
|
putExtra("EXTRA_PROFILE_IMAGE_URI", selectedUri.toString()) |
||||
|
} |
||||
|
|
||||
|
startActivity(intent) |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Helper function to create temporary File & Uri for Camera launcher |
||||
|
fun createImageUri(context: Context): Uri { |
||||
|
val tempFile = File.createTempFile("profile_camera_", ".jpg", context.cacheDir).apply { |
||||
|
createNewFile() |
||||
|
deleteOnExit() |
||||
|
} |
||||
|
return FileProvider.getUriForFile( |
||||
|
context, |
||||
|
"${context.packageName}.provider", |
||||
|
tempFile |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
@Composable |
||||
|
fun UploadProfileImageScreen( |
||||
|
onBackClick: () -> Unit, |
||||
|
onContinueClick: (Uri?) -> Unit |
||||
|
) { |
||||
|
val context = LocalContext.current |
||||
|
|
||||
|
// Single state variable to store image source (from either Gallery OR Camera) |
||||
|
var selectedImageUri by remember { mutableStateOf<Uri?>(null) } |
||||
|
var tempCameraUri by remember { mutableStateOf<Uri?>(null) } |
||||
|
|
||||
|
// 1. Gallery Launcher |
||||
|
val galleryLauncher = rememberLauncherForActivityResult( |
||||
|
contract = ActivityResultContracts.GetContent() |
||||
|
) { uri: Uri? -> |
||||
|
if (uri != null) { |
||||
|
selectedImageUri = uri |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 2. Camera Launcher |
||||
|
val cameraLauncher = rememberLauncherForActivityResult( |
||||
|
contract = ActivityResultContracts.TakePicture() |
||||
|
) { success: Boolean -> |
||||
|
if (success && tempCameraUri != null) { |
||||
|
selectedImageUri = tempCameraUri |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Design Gradients |
||||
|
val backgroundGradient = Brush.verticalGradient( |
||||
|
colors = listOf( |
||||
|
Color(0xFFFFF5F5), |
||||
|
Color(0xFFFAFAFA), |
||||
|
Color(0xFFF3E8FF) |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
val buttonGradient = Brush.horizontalGradient( |
||||
|
colors = listOf( |
||||
|
Color(0xFFC2185B), |
||||
|
Color(0xFF7B1FA2) |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
val circleBorderGradient = Brush.sweepGradient( |
||||
|
colors = listOf( |
||||
|
Color(0xFF7B1FA2), |
||||
|
Color(0xFFC2185B), |
||||
|
Color(0xFFE91E63), |
||||
|
Color(0xFF7B1FA2) |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.fillMaxSize() |
||||
|
.background(backgroundGradient) |
||||
|
.systemBarsPadding() |
||||
|
) { |
||||
|
Column( |
||||
|
modifier = Modifier |
||||
|
.fillMaxSize() |
||||
|
.padding(horizontal = 24.dp, vertical = 16.dp), |
||||
|
horizontalAlignment = Alignment.CenterHorizontally, |
||||
|
verticalArrangement = Arrangement.SpaceBetween |
||||
|
) { |
||||
|
// Header Bar |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.padding(top = 8.dp) |
||||
|
) { |
||||
|
IconButton( |
||||
|
onClick = onBackClick, |
||||
|
modifier = Modifier.align(Alignment.CenterStart) |
||||
|
) { |
||||
|
Icon( |
||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack, |
||||
|
contentDescription = "Back", |
||||
|
tint = Color.Black |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
Text( |
||||
|
text = "VibeCheck", |
||||
|
fontSize = 22.sp, |
||||
|
fontWeight = FontWeight.Bold, |
||||
|
color = Color(0xFFC2185B), |
||||
|
modifier = Modifier.align(Alignment.Center) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// Central Content Area |
||||
|
Column( |
||||
|
horizontalAlignment = Alignment.CenterHorizontally, |
||||
|
modifier = Modifier.fillMaxWidth() |
||||
|
) { |
||||
|
Text( |
||||
|
text = "Upload profile photo", |
||||
|
fontSize = 24.sp, |
||||
|
fontWeight = FontWeight.Bold, |
||||
|
color = Color.Black |
||||
|
) |
||||
|
|
||||
|
Spacer(modifier = Modifier.height(8.dp)) |
||||
|
|
||||
|
Text( |
||||
|
text = "Choose a photo that shows your best self. You can always change this later.", |
||||
|
fontSize = 13.sp, |
||||
|
color = Color.Gray, |
||||
|
textAlign = TextAlign.Center, |
||||
|
modifier = Modifier.padding(horizontal = 20.dp) |
||||
|
) |
||||
|
|
||||
|
Spacer(modifier = Modifier.height(32.dp)) |
||||
|
|
||||
|
// Profile Circular Container |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.size(190.dp) |
||||
|
.clip(CircleShape) |
||||
|
.background(Color.White) |
||||
|
.border(3.dp, circleBorderGradient, CircleShape), |
||||
|
contentAlignment = Alignment.Center |
||||
|
) { |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.size(165.dp) |
||||
|
.clip(CircleShape) |
||||
|
.drawWithContent { |
||||
|
drawContent() |
||||
|
drawCircle( |
||||
|
color = Color(0xFFF48FB1), |
||||
|
style = Stroke( |
||||
|
width = 3f, |
||||
|
pathEffect = PathEffect.dashPathEffect(floatArrayOf(12f, 12f), 0f) |
||||
|
) |
||||
|
) |
||||
|
}, |
||||
|
contentAlignment = Alignment.Center |
||||
|
) { |
||||
|
if (selectedImageUri != null) { |
||||
|
Image( |
||||
|
painter = rememberAsyncImagePainter(selectedImageUri), |
||||
|
contentDescription = "Profile Photo", |
||||
|
modifier = Modifier |
||||
|
.fillMaxSize() |
||||
|
.clip(CircleShape), |
||||
|
contentScale = ContentScale.Crop |
||||
|
) |
||||
|
} else { |
||||
|
Icon( |
||||
|
imageVector = Icons.Outlined.AddAPhoto, |
||||
|
contentDescription = "Camera Icon", |
||||
|
tint = Color(0xFFE57373), |
||||
|
modifier = Modifier.size(44.dp) |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Spacer(modifier = Modifier.height(36.dp)) |
||||
|
|
||||
|
// Upload from gallery Button |
||||
|
Card( |
||||
|
shape = RoundedCornerShape(16.dp), |
||||
|
colors = CardDefaults.cardColors(containerColor = Color(0xFFEDE7F6)), |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.height(52.dp) |
||||
|
.clickable { galleryLauncher.launch("image/*") } |
||||
|
) { |
||||
|
Row( |
||||
|
modifier = Modifier.fillMaxSize(), |
||||
|
horizontalArrangement = Arrangement.Center, |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
Icon( |
||||
|
imageVector = Icons.Outlined.PhotoLibrary, |
||||
|
contentDescription = "Gallery", |
||||
|
tint = Color(0xFF673AB7), |
||||
|
modifier = Modifier.size(20.dp) |
||||
|
) |
||||
|
Spacer(modifier = Modifier.width(10.dp)) |
||||
|
Text( |
||||
|
text = "Upload from gallery", |
||||
|
color = Color(0xFF673AB7), |
||||
|
fontSize = 14.sp, |
||||
|
fontWeight = FontWeight.SemiBold |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Spacer(modifier = Modifier.height(12.dp)) |
||||
|
|
||||
|
// Take a photo Button |
||||
|
Card( |
||||
|
shape = RoundedCornerShape(16.dp), |
||||
|
colors = CardDefaults.cardColors(containerColor = Color(0xFFEDE7F6)), |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.height(52.dp) |
||||
|
.clickable { |
||||
|
val uri = createImageUri(context) |
||||
|
tempCameraUri = uri |
||||
|
cameraLauncher.launch(uri) |
||||
|
} |
||||
|
) { |
||||
|
Row( |
||||
|
modifier = Modifier.fillMaxSize(), |
||||
|
horizontalArrangement = Arrangement.Center, |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
Icon( |
||||
|
imageVector = Icons.Outlined.CameraAlt, |
||||
|
contentDescription = "Camera", |
||||
|
tint = Color(0xFF673AB7), |
||||
|
modifier = Modifier.size(20.dp) |
||||
|
) |
||||
|
Spacer(modifier = Modifier.width(10.dp)) |
||||
|
Text( |
||||
|
text = "Take a photo", |
||||
|
color = Color(0xFF673AB7), |
||||
|
fontSize = 14.sp, |
||||
|
fontWeight = FontWeight.SemiBold |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Bottom Continue Section |
||||
|
Column( |
||||
|
horizontalAlignment = Alignment.CenterHorizontally, |
||||
|
modifier = Modifier.fillMaxWidth() |
||||
|
) { |
||||
|
Button( |
||||
|
onClick = { onContinueClick(selectedImageUri) }, |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.height(52.dp) |
||||
|
.background(buttonGradient, shape = RoundedCornerShape(26.dp)), |
||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent), |
||||
|
shape = RoundedCornerShape(26.dp), |
||||
|
contentPadding = PaddingValues() |
||||
|
) { |
||||
|
Text( |
||||
|
text = "Continue", |
||||
|
color = Color.White, |
||||
|
fontSize = 16.sp, |
||||
|
fontWeight = FontWeight.Bold |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
Spacer(modifier = Modifier.height(14.dp)) |
||||
|
|
||||
|
Text( |
||||
|
text = buildAnnotatedString { |
||||
|
append("Need help? ") |
||||
|
withStyle(style = SpanStyle(color = Color(0xFFC2185B), fontWeight = FontWeight.Bold)) { |
||||
|
append("Contact support") |
||||
|
} |
||||
|
}, |
||||
|
fontSize = 12.sp, |
||||
|
color = Color.Gray, |
||||
|
modifier = Modifier.clickable { } |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 1. Preview default state (No photo selected yet) |
||||
|
@Preview( |
||||
|
name = "Upload Photo - Empty State", |
||||
|
showBackground = true, |
||||
|
showSystemUi = true |
||||
|
) |
||||
|
@Composable |
||||
|
fun UploadProfileImageScreenPreview() { |
||||
|
MaterialTheme { |
||||
|
UploadProfileImageScreen( |
||||
|
onBackClick = {}, |
||||
|
onContinueClick = {} |
||||
|
) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,44 @@ |
|||||
|
package com.example.dattingapp.registration |
||||
|
|
||||
|
import okhttp3.MultipartBody |
||||
|
import retrofit2.Response |
||||
|
import retrofit2.http.Body |
||||
|
import retrofit2.http.Header |
||||
|
import retrofit2.http.Multipart |
||||
|
import retrofit2.http.POST |
||||
|
import retrofit2.http.Part |
||||
|
|
||||
|
interface ApiService { |
||||
|
|
||||
|
// ========================================== |
||||
|
// Upload Profile Image |
||||
|
// ========================================== |
||||
|
|
||||
|
@Multipart |
||||
|
@POST("api/v1/users/me/image") |
||||
|
suspend fun uploadProfileImage( |
||||
|
@Header("Authorization") authorization: String, |
||||
|
@Part image: MultipartBody.Part |
||||
|
): Response<UploadResponse> |
||||
|
|
||||
|
// ========================================== |
||||
|
// Upload Video |
||||
|
// ========================================== |
||||
|
|
||||
|
@Multipart |
||||
|
@POST("api/v1/users/me/video") |
||||
|
suspend fun uploadProfileVideo( |
||||
|
@Header("Authorization") authorization: String, |
||||
|
@Part video: MultipartBody.Part |
||||
|
): Response<UploadResponse> |
||||
|
|
||||
|
// ========================================== |
||||
|
// Final Registration |
||||
|
// ========================================== |
||||
|
|
||||
|
@POST("api/v1/auth/register") |
||||
|
suspend fun register( |
||||
|
@Body request: RegisterRequest |
||||
|
): Response<RegisterResponse> |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,46 @@ |
|||||
|
package com.example.dattingapp.registration |
||||
|
|
||||
|
|
||||
|
import android.content.Context |
||||
|
import android.net.Uri |
||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull |
||||
|
import okhttp3.MultipartBody |
||||
|
import okhttp3.RequestBody.Companion.asRequestBody |
||||
|
import java.io.File |
||||
|
|
||||
|
fun uriToMultipart( |
||||
|
context: Context, |
||||
|
uri: Uri, |
||||
|
fieldName: String, |
||||
|
fileName: String |
||||
|
): MultipartBody.Part { |
||||
|
|
||||
|
val inputStream = context.contentResolver.openInputStream(uri) |
||||
|
?: throw Exception("Cannot open selected file") |
||||
|
|
||||
|
val tempFile = File( |
||||
|
context.cacheDir, |
||||
|
fileName |
||||
|
) |
||||
|
|
||||
|
inputStream.use { input -> |
||||
|
tempFile.outputStream().use { output -> |
||||
|
input.copyTo(output) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
val mimeType = |
||||
|
context.contentResolver.getType(uri) |
||||
|
?: "application/octet-stream" |
||||
|
|
||||
|
val requestBody = |
||||
|
tempFile.asRequestBody( |
||||
|
mimeType.toMediaTypeOrNull() |
||||
|
) |
||||
|
|
||||
|
return MultipartBody.Part.createFormData( |
||||
|
fieldName, |
||||
|
tempFile.name, |
||||
|
requestBody |
||||
|
) |
||||
|
} |
||||
@ -0,0 +1,96 @@ |
|||||
|
package com.example.dattingapp.registration |
||||
|
|
||||
|
|
||||
|
import com.google.gson.annotations.SerializedName |
||||
|
|
||||
|
// =============================== |
||||
|
// Upload response |
||||
|
// =============================== |
||||
|
|
||||
|
data class UploadResponse( |
||||
|
@SerializedName("url") |
||||
|
val url: String?, |
||||
|
|
||||
|
@SerializedName("image_url") |
||||
|
val imageUrl: String?, |
||||
|
|
||||
|
@SerializedName("video_url") |
||||
|
val videoUrl: String? |
||||
|
) { |
||||
|
fun getReturnedUrl(): String? { |
||||
|
return url ?: imageUrl ?: videoUrl |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
// =============================== |
||||
|
// Registration request |
||||
|
// =============================== |
||||
|
|
||||
|
data class RegisterRequest( |
||||
|
|
||||
|
@SerializedName("email_or_phone") |
||||
|
val emailOrPhone: String, |
||||
|
|
||||
|
@SerializedName("password") |
||||
|
val password: String, |
||||
|
|
||||
|
@SerializedName("confirm_password") |
||||
|
val confirmPassword: String, |
||||
|
|
||||
|
@SerializedName("full_name") |
||||
|
val fullName: String, |
||||
|
|
||||
|
@SerializedName("gender") |
||||
|
val gender: String, |
||||
|
|
||||
|
@SerializedName("birth_date") |
||||
|
val birthDate: String, |
||||
|
|
||||
|
@SerializedName("city") |
||||
|
val city: String, |
||||
|
|
||||
|
@SerializedName("bio") |
||||
|
val bio: String, |
||||
|
|
||||
|
@SerializedName("video_intro") |
||||
|
val videoIntro: String?, |
||||
|
|
||||
|
@SerializedName("profile_image") |
||||
|
val profileImage: String?, |
||||
|
|
||||
|
@SerializedName("interests") |
||||
|
val interests: List<String> |
||||
|
) |
||||
|
|
||||
|
|
||||
|
// =============================== |
||||
|
// Registration response |
||||
|
// =============================== |
||||
|
|
||||
|
data class RegisterResponse( |
||||
|
@SerializedName("access_token") |
||||
|
val accessToken: String?, |
||||
|
|
||||
|
@SerializedName("refresh_token") |
||||
|
val refreshToken: String?, |
||||
|
|
||||
|
@SerializedName("token_type") |
||||
|
val tokenType: String?, |
||||
|
|
||||
|
@SerializedName("user") |
||||
|
val user: User? |
||||
|
) |
||||
|
|
||||
|
data class User( |
||||
|
@SerializedName("id") |
||||
|
val id: Int?, |
||||
|
|
||||
|
@SerializedName("email_or_phone") |
||||
|
val emailOrPhone: String?, |
||||
|
|
||||
|
@SerializedName("full_name") |
||||
|
val fullName: String? |
||||
|
) |
||||
|
|
||||
|
|
||||
@ -0,0 +1,54 @@ |
|||||
|
package com.example.dattingapp.registration |
||||
|
|
||||
|
import okhttp3.OkHttpClient |
||||
|
import okhttp3.logging.HttpLoggingInterceptor |
||||
|
import retrofit2.Retrofit |
||||
|
import retrofit2.converter.gson.GsonConverterFactory |
||||
|
import java.util.concurrent.TimeUnit |
||||
|
|
||||
|
object NetworkModule { |
||||
|
|
||||
|
private const val AUTH_BASE_URL = |
||||
|
"https://counts-indicates-stud-addressing.trycloudflare.com" |
||||
|
|
||||
|
private const val UPLOAD_BASE_URL = |
||||
|
"https://counts-indicates-stud-addressing.trycloudflare.com/" |
||||
|
|
||||
|
private fun createClient(): OkHttpClient { |
||||
|
|
||||
|
val logging = HttpLoggingInterceptor().apply { |
||||
|
level = HttpLoggingInterceptor.Level.BODY |
||||
|
} |
||||
|
|
||||
|
return OkHttpClient.Builder() |
||||
|
.addInterceptor(logging) |
||||
|
.connectTimeout(60, TimeUnit.SECONDS) |
||||
|
.readTimeout(120, TimeUnit.SECONDS) |
||||
|
.writeTimeout(120, TimeUnit.SECONDS) |
||||
|
.build() |
||||
|
} |
||||
|
|
||||
|
private fun createRetrofit(baseUrl: String): Retrofit { |
||||
|
|
||||
|
return Retrofit.Builder() |
||||
|
.baseUrl(baseUrl) |
||||
|
.client(createClient()) |
||||
|
.addConverterFactory(GsonConverterFactory.create()) |
||||
|
.build() |
||||
|
} |
||||
|
|
||||
|
// Registration API |
||||
|
val authApi: ApiService by lazy { |
||||
|
|
||||
|
createRetrofit(AUTH_BASE_URL) |
||||
|
.create(ApiService::class.java) |
||||
|
} |
||||
|
|
||||
|
// Upload API |
||||
|
val uploadApi: ApiService by lazy { |
||||
|
|
||||
|
createRetrofit(UPLOAD_BASE_URL) |
||||
|
.create(ApiService::class.java) |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,187 @@ |
|||||
|
package com.example.dattingapp.registration |
||||
|
|
||||
|
|
||||
|
import android.content.Context |
||||
|
import android.net.Uri |
||||
|
import android.util.Log |
||||
|
|
||||
|
class RegistrationRepository( |
||||
|
private val context: Context |
||||
|
) { |
||||
|
|
||||
|
private val uploadApi = |
||||
|
NetworkModule.uploadApi |
||||
|
|
||||
|
private val authApi = |
||||
|
NetworkModule.authApi |
||||
|
|
||||
|
|
||||
|
// ========================================== |
||||
|
// Upload Image |
||||
|
// ========================================== |
||||
|
|
||||
|
suspend fun uploadImage( |
||||
|
token: String, |
||||
|
uri: Uri |
||||
|
): Result<String> { |
||||
|
|
||||
|
return try { |
||||
|
|
||||
|
val multipart = uriToMultipart( |
||||
|
context = context, |
||||
|
uri = uri, |
||||
|
fieldName = "image", |
||||
|
fileName = "profile_image.jpg" |
||||
|
) |
||||
|
|
||||
|
val response = |
||||
|
uploadApi.uploadProfileImage( |
||||
|
authorization = token, |
||||
|
image = multipart |
||||
|
) |
||||
|
|
||||
|
if (response.isSuccessful) { |
||||
|
|
||||
|
val body = response.body() |
||||
|
|
||||
|
val url = body?.getReturnedUrl() |
||||
|
|
||||
|
if (!url.isNullOrEmpty()) { |
||||
|
|
||||
|
Result.success(url) |
||||
|
|
||||
|
} else { |
||||
|
|
||||
|
Result.failure( |
||||
|
Exception( |
||||
|
"Image uploaded but backend did not return URL" |
||||
|
) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
} else { |
||||
|
|
||||
|
Result.failure( |
||||
|
Exception( |
||||
|
"Image upload failed: ${response.code()} ${response.message()}" |
||||
|
) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
} catch (e: Exception) { |
||||
|
|
||||
|
Result.failure(e) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
// ========================================== |
||||
|
// Upload Video |
||||
|
// ========================================== |
||||
|
|
||||
|
suspend fun uploadVideo( |
||||
|
token: String, |
||||
|
uri: Uri |
||||
|
): Result<String> { |
||||
|
|
||||
|
return try { |
||||
|
|
||||
|
val multipart = uriToMultipart( |
||||
|
context = context, |
||||
|
uri = uri, |
||||
|
fieldName = "video", |
||||
|
fileName = "intro_video.mp4" |
||||
|
) |
||||
|
|
||||
|
val response = uploadApi.uploadProfileVideo( |
||||
|
authorization = token, |
||||
|
video = multipart |
||||
|
) |
||||
|
|
||||
|
if (response.isSuccessful) { |
||||
|
|
||||
|
val body = response.body() |
||||
|
|
||||
|
val url = body?.getReturnedUrl() |
||||
|
|
||||
|
if (!url.isNullOrEmpty()) { |
||||
|
|
||||
|
Result.success(url) |
||||
|
|
||||
|
} else { |
||||
|
|
||||
|
Result.failure( |
||||
|
Exception( |
||||
|
"Video uploaded but backend did not return URL" |
||||
|
) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
} else { |
||||
|
|
||||
|
Result.failure( |
||||
|
Exception( |
||||
|
"Video upload failed: ${response.code()} ${response.message()}" |
||||
|
) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
} catch (e: Exception) { |
||||
|
|
||||
|
Log.e( |
||||
|
"UPLOAD_VIDEO", |
||||
|
"Exception while uploading video", |
||||
|
e |
||||
|
) |
||||
|
|
||||
|
Result.failure(e) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
// ========================================== |
||||
|
// Final Registration |
||||
|
// ========================================== |
||||
|
|
||||
|
suspend fun register( |
||||
|
request: RegisterRequest |
||||
|
): Result<RegisterResponse> { |
||||
|
|
||||
|
return try { |
||||
|
|
||||
|
val response = |
||||
|
authApi.register(request) |
||||
|
|
||||
|
if (response.isSuccessful) { |
||||
|
|
||||
|
val body = response.body() |
||||
|
|
||||
|
if (body != null) { |
||||
|
|
||||
|
Result.success(body) |
||||
|
|
||||
|
} else { |
||||
|
|
||||
|
Result.failure( |
||||
|
Exception( |
||||
|
"Registration response is empty" |
||||
|
) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
} else { |
||||
|
|
||||
|
Result.failure( |
||||
|
Exception( |
||||
|
"Registration failed: ${response.code()} ${response.message()}" |
||||
|
) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
} catch (e: Exception) { |
||||
|
|
||||
|
Result.failure(e) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,937 @@ |
|||||
|
package com.example.dattingapp.registration |
||||
|
|
||||
|
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 |
||||
|
|
||||
|
// 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 = 3 } |
||||
|
} |
||||
|
} 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 = 3 } |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
3 -> { |
||||
|
|
||||
|
when { |
||||
|
passwordInput.length < 6 -> passwordError = "Password must be at least 6 characters" |
||||
|
passwordInput != confirmPasswordInput -> passwordError = "Passwords do not match" |
||||
|
else -> { passwordError = null; currentStep = 4 } |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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() |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,417 @@ |
|||||
|
package com.example.dattingapp.registration |
||||
|
|
||||
|
import android.content.Context |
||||
|
import android.content.Intent |
||||
|
import android.net.Uri |
||||
|
import android.os.Bundle |
||||
|
import android.util.Log |
||||
|
import android.widget.Toast |
||||
|
import androidx.activity.ComponentActivity |
||||
|
import androidx.activity.compose.rememberLauncherForActivityResult |
||||
|
import androidx.activity.compose.setContent |
||||
|
import androidx.activity.enableEdgeToEdge |
||||
|
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.shape.CircleShape |
||||
|
import androidx.compose.foundation.shape.RoundedCornerShape |
||||
|
import androidx.compose.material.icons.Icons |
||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack |
||||
|
import androidx.compose.material.icons.outlined.AddAPhoto |
||||
|
import androidx.compose.material.icons.outlined.CameraAlt |
||||
|
import androidx.compose.material.icons.outlined.PhotoLibrary |
||||
|
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.draw.drawWithContent |
||||
|
import androidx.compose.ui.graphics.Brush |
||||
|
import androidx.compose.ui.graphics.Color |
||||
|
import androidx.compose.ui.graphics.PathEffect |
||||
|
import androidx.compose.ui.graphics.drawscope.Stroke |
||||
|
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.style.TextAlign |
||||
|
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.core.content.FileProvider |
||||
|
import coil.compose.rememberAsyncImagePainter |
||||
|
import java.io.File |
||||
|
|
||||
|
class UploadProfileImageActivity : ComponentActivity() { |
||||
|
|
||||
|
private val TAG = "kk" |
||||
|
|
||||
|
override fun onCreate(savedInstanceState: Bundle?) { |
||||
|
enableEdgeToEdge() |
||||
|
super.onCreate(savedInstanceState) |
||||
|
|
||||
|
// Retrieve intent extras passed from previous registration steps |
||||
|
val isPhone = intent.getBooleanExtra("EXTRA_IS_PHONE", false) |
||||
|
val countryCode = intent.getStringExtra("EXTRA_COUNTRY_CODE") ?: "" |
||||
|
val contactValue = intent.getStringExtra("EXTRA_CONTACT_VALUE") ?: "" |
||||
|
val password = intent.getStringExtra("EXTRA_PASSWORD") ?: "" |
||||
|
val name = intent.getStringExtra("EXTRA_NAME") ?: "" |
||||
|
val dob = intent.getStringExtra("EXTRA_DOB") ?: "" |
||||
|
val gender = intent.getStringExtra("EXTRA_GENDER") ?: "" |
||||
|
val interests = intent.getStringArrayListExtra("EXTRA_INTERESTS") ?: arrayListOf() |
||||
|
val vibe = intent.getStringExtra("EXTRA_VIBE") ?: "" |
||||
|
|
||||
|
Log.d(TAG, "=== Received Registration Data ===") |
||||
|
Log.d(TAG, "Is Phone: $isPhone") |
||||
|
Log.d(TAG, "Country Code: $countryCode") |
||||
|
Log.d(TAG, "Contact Value: $contactValue") |
||||
|
Log.d(TAG, "Password: $password") |
||||
|
Log.d(TAG, "Name: $name") |
||||
|
Log.d(TAG, "Date of Birth: $dob") |
||||
|
Log.d(TAG, "Gender: $gender") |
||||
|
Log.d(TAG, "Interests: $interests") |
||||
|
Log.d(TAG, "Vibe: $vibe") |
||||
|
Log.d(TAG, "=================================") |
||||
|
|
||||
|
setContent { |
||||
|
MaterialTheme { |
||||
|
UploadProfileImageScreen( |
||||
|
onBackClick = { finish() }, |
||||
|
onContinueClick = { selectedUri -> |
||||
|
if (selectedUri == null) { |
||||
|
Toast.makeText(this@UploadProfileImageActivity, "Please select or take a photo first", Toast.LENGTH_SHORT).show() |
||||
|
return@UploadProfileImageScreen |
||||
|
} |
||||
|
|
||||
|
val intent = Intent( |
||||
|
this@UploadProfileImageActivity, |
||||
|
UploadIntroVideoActivity::class.java |
||||
|
).apply { |
||||
|
// 1. Pass down all data received from previous steps |
||||
|
putExtra("EXTRA_IS_PHONE", isPhone) |
||||
|
putExtra("EXTRA_COUNTRY_CODE", countryCode) |
||||
|
putExtra("EXTRA_CONTACT_VALUE", contactValue) |
||||
|
putExtra("EXTRA_PASSWORD", password) |
||||
|
putExtra("EXTRA_NAME", name) |
||||
|
putExtra("EXTRA_DOB", dob) |
||||
|
putExtra("EXTRA_GENDER", gender) |
||||
|
putStringArrayListExtra("EXTRA_INTERESTS", interests) |
||||
|
putExtra("EXTRA_VIBE", vibe) |
||||
|
|
||||
|
// 2. Pass the selected Profile Image Uri as a String |
||||
|
putExtra("EXTRA_PROFILE_IMAGE_URI", selectedUri.toString()) |
||||
|
} |
||||
|
|
||||
|
startActivity(intent) |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Helper function to create temporary File & Uri for Camera launcher |
||||
|
fun createImageUri(context: Context): Uri { |
||||
|
val tempFile = File.createTempFile("profile_camera_", ".jpg", context.cacheDir).apply { |
||||
|
createNewFile() |
||||
|
deleteOnExit() |
||||
|
} |
||||
|
return FileProvider.getUriForFile( |
||||
|
context, |
||||
|
"${context.packageName}.provider", |
||||
|
tempFile |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
@Composable |
||||
|
fun UploadProfileImageScreen( |
||||
|
onBackClick: () -> Unit, |
||||
|
onContinueClick: (Uri?) -> Unit |
||||
|
) { |
||||
|
val context = LocalContext.current |
||||
|
|
||||
|
// Single state variable to store image source (from either Gallery OR Camera) |
||||
|
var selectedImageUri by remember { mutableStateOf<Uri?>(null) } |
||||
|
var tempCameraUri by remember { mutableStateOf<Uri?>(null) } |
||||
|
|
||||
|
// 1. Gallery Launcher |
||||
|
val galleryLauncher = rememberLauncherForActivityResult( |
||||
|
contract = ActivityResultContracts.GetContent() |
||||
|
) { uri: Uri? -> |
||||
|
if (uri != null) { |
||||
|
selectedImageUri = uri |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 2. Camera Launcher |
||||
|
val cameraLauncher = rememberLauncherForActivityResult( |
||||
|
contract = ActivityResultContracts.TakePicture() |
||||
|
) { success: Boolean -> |
||||
|
if (success && tempCameraUri != null) { |
||||
|
selectedImageUri = tempCameraUri |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Design Gradients |
||||
|
val backgroundGradient = Brush.verticalGradient( |
||||
|
colors = listOf( |
||||
|
Color(0xFFFFF5F5), |
||||
|
Color(0xFFFAFAFA), |
||||
|
Color(0xFFF3E8FF) |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
val buttonGradient = Brush.horizontalGradient( |
||||
|
colors = listOf( |
||||
|
Color(0xFFC2185B), |
||||
|
Color(0xFF7B1FA2) |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
val circleBorderGradient = Brush.sweepGradient( |
||||
|
colors = listOf( |
||||
|
Color(0xFF7B1FA2), |
||||
|
Color(0xFFC2185B), |
||||
|
Color(0xFFE91E63), |
||||
|
Color(0xFF7B1FA2) |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.fillMaxSize() |
||||
|
.background(backgroundGradient) |
||||
|
.systemBarsPadding() |
||||
|
) { |
||||
|
Column( |
||||
|
modifier = Modifier |
||||
|
.fillMaxSize() |
||||
|
.padding(horizontal = 24.dp, vertical = 16.dp), |
||||
|
horizontalAlignment = Alignment.CenterHorizontally, |
||||
|
verticalArrangement = Arrangement.SpaceBetween |
||||
|
) { |
||||
|
// Header Bar |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.padding(top = 8.dp) |
||||
|
) { |
||||
|
IconButton( |
||||
|
onClick = onBackClick, |
||||
|
modifier = Modifier.align(Alignment.CenterStart) |
||||
|
) { |
||||
|
Icon( |
||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack, |
||||
|
contentDescription = "Back", |
||||
|
tint = Color.Black |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
Text( |
||||
|
text = "VibeCheck", |
||||
|
fontSize = 22.sp, |
||||
|
fontWeight = FontWeight.Bold, |
||||
|
color = Color(0xFFC2185B), |
||||
|
modifier = Modifier.align(Alignment.Center) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// Central Content Area |
||||
|
Column( |
||||
|
horizontalAlignment = Alignment.CenterHorizontally, |
||||
|
modifier = Modifier.fillMaxWidth() |
||||
|
) { |
||||
|
Text( |
||||
|
text = "Upload profile photo", |
||||
|
fontSize = 24.sp, |
||||
|
fontWeight = FontWeight.Bold, |
||||
|
color = Color.Black |
||||
|
) |
||||
|
|
||||
|
Spacer(modifier = Modifier.height(8.dp)) |
||||
|
|
||||
|
Text( |
||||
|
text = "Choose a photo that shows your best self. You can always change this later.", |
||||
|
fontSize = 13.sp, |
||||
|
color = Color.Gray, |
||||
|
textAlign = TextAlign.Center, |
||||
|
modifier = Modifier.padding(horizontal = 20.dp) |
||||
|
) |
||||
|
|
||||
|
Spacer(modifier = Modifier.height(32.dp)) |
||||
|
|
||||
|
// Profile Circular Container |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.size(190.dp) |
||||
|
.clip(CircleShape) |
||||
|
.background(Color.White) |
||||
|
.border(3.dp, circleBorderGradient, CircleShape), |
||||
|
contentAlignment = Alignment.Center |
||||
|
) { |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.size(165.dp) |
||||
|
.clip(CircleShape) |
||||
|
.drawWithContent { |
||||
|
drawContent() |
||||
|
drawCircle( |
||||
|
color = Color(0xFFF48FB1), |
||||
|
style = Stroke( |
||||
|
width = 3f, |
||||
|
pathEffect = PathEffect.dashPathEffect(floatArrayOf(12f, 12f), 0f) |
||||
|
) |
||||
|
) |
||||
|
}, |
||||
|
contentAlignment = Alignment.Center |
||||
|
) { |
||||
|
if (selectedImageUri != null) { |
||||
|
Image( |
||||
|
painter = rememberAsyncImagePainter(selectedImageUri), |
||||
|
contentDescription = "Profile Photo", |
||||
|
modifier = Modifier |
||||
|
.fillMaxSize() |
||||
|
.clip(CircleShape), |
||||
|
contentScale = ContentScale.Crop |
||||
|
) |
||||
|
} else { |
||||
|
Icon( |
||||
|
imageVector = Icons.Outlined.AddAPhoto, |
||||
|
contentDescription = "Camera Icon", |
||||
|
tint = Color(0xFFE57373), |
||||
|
modifier = Modifier.size(44.dp) |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Spacer(modifier = Modifier.height(36.dp)) |
||||
|
|
||||
|
// Upload from gallery Button |
||||
|
Card( |
||||
|
shape = RoundedCornerShape(16.dp), |
||||
|
colors = CardDefaults.cardColors(containerColor = Color(0xFFEDE7F6)), |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.height(52.dp) |
||||
|
.clickable { galleryLauncher.launch("image/*") } |
||||
|
) { |
||||
|
Row( |
||||
|
modifier = Modifier.fillMaxSize(), |
||||
|
horizontalArrangement = Arrangement.Center, |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
Icon( |
||||
|
imageVector = Icons.Outlined.PhotoLibrary, |
||||
|
contentDescription = "Gallery", |
||||
|
tint = Color(0xFF673AB7), |
||||
|
modifier = Modifier.size(20.dp) |
||||
|
) |
||||
|
Spacer(modifier = Modifier.width(10.dp)) |
||||
|
Text( |
||||
|
text = "Upload from gallery", |
||||
|
color = Color(0xFF673AB7), |
||||
|
fontSize = 14.sp, |
||||
|
fontWeight = FontWeight.SemiBold |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Spacer(modifier = Modifier.height(12.dp)) |
||||
|
|
||||
|
// Take a photo Button |
||||
|
Card( |
||||
|
shape = RoundedCornerShape(16.dp), |
||||
|
colors = CardDefaults.cardColors(containerColor = Color(0xFFEDE7F6)), |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.height(52.dp) |
||||
|
.clickable { |
||||
|
val uri = createImageUri(context) |
||||
|
tempCameraUri = uri |
||||
|
cameraLauncher.launch(uri) |
||||
|
} |
||||
|
) { |
||||
|
Row( |
||||
|
modifier = Modifier.fillMaxSize(), |
||||
|
horizontalArrangement = Arrangement.Center, |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
Icon( |
||||
|
imageVector = Icons.Outlined.CameraAlt, |
||||
|
contentDescription = "Camera", |
||||
|
tint = Color(0xFF673AB7), |
||||
|
modifier = Modifier.size(20.dp) |
||||
|
) |
||||
|
Spacer(modifier = Modifier.width(10.dp)) |
||||
|
Text( |
||||
|
text = "Take a photo", |
||||
|
color = Color(0xFF673AB7), |
||||
|
fontSize = 14.sp, |
||||
|
fontWeight = FontWeight.SemiBold |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Bottom Continue Section |
||||
|
Column( |
||||
|
horizontalAlignment = Alignment.CenterHorizontally, |
||||
|
modifier = Modifier.fillMaxWidth() |
||||
|
) { |
||||
|
Button( |
||||
|
onClick = { onContinueClick(selectedImageUri) }, |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.height(52.dp) |
||||
|
.background(buttonGradient, shape = RoundedCornerShape(26.dp)), |
||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent), |
||||
|
shape = RoundedCornerShape(26.dp), |
||||
|
contentPadding = PaddingValues() |
||||
|
) { |
||||
|
Text( |
||||
|
text = "Continue", |
||||
|
color = Color.White, |
||||
|
fontSize = 16.sp, |
||||
|
fontWeight = FontWeight.Bold |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
Spacer(modifier = Modifier.height(14.dp)) |
||||
|
|
||||
|
Text( |
||||
|
text = buildAnnotatedString { |
||||
|
append("Need help? ") |
||||
|
withStyle(style = SpanStyle(color = Color(0xFFC2185B), fontWeight = FontWeight.Bold)) { |
||||
|
append("Contact support") |
||||
|
} |
||||
|
}, |
||||
|
fontSize = 12.sp, |
||||
|
color = Color.Gray, |
||||
|
modifier = Modifier.clickable { } |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 1. Preview default state (No photo selected yet) |
||||
|
@Preview( |
||||
|
name = "Upload Photo - Empty State", |
||||
|
showBackground = true, |
||||
|
showSystemUi = true |
||||
|
) |
||||
|
@Composable |
||||
|
fun UploadProfileImageScreenPreview() { |
||||
|
MaterialTheme { |
||||
|
UploadProfileImageScreen( |
||||
|
onBackClick = {}, |
||||
|
onContinueClick = {} |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1,5 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<paths xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
|
<cache-path name="cache_images" path="." /> |
||||
|
<external-cache-path name="external_cache_images" path="." /> |
||||
|
</paths> |
||||
Loading…
Reference in new issue