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