diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3fb775e..305a9c4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -59,7 +59,27 @@ dependencies { androidTestImplementation(libs.androidx.junit) debugImplementation(libs.androidx.compose.ui.test.manifest) debugImplementation(libs.androidx.compose.ui.tooling) - implementation("androidx.compose.material:material-icons-extended:1.7.0") + implementation("androidx.compose.material:material-icons-extended:1.7.8") implementation("io.coil-kt:coil-compose:2.6.0") implementation("androidx.navigation:navigation-compose:2.9.3") + implementation("androidx.media3:media3-exoplayer:1.4.1") + + // Media3 UI components (PlayerView) + implementation("androidx.media3:media3-ui:1.4.1") + + // Media3 Common library (contains MediaItem) + implementation("androidx.media3:media3-common:1.4.1") + + implementation("com.squareup.retrofit2:retrofit:2.11.0") + implementation("com.squareup.retrofit2:converter-gson:2.11.0") + + implementation("com.squareup.okhttp3:logging-interceptor:5.5.0") + + implementation("io.coil-kt:coil-compose:2.7.0") + + implementation(libs.media3.exoplayer) + implementation(libs.media3.ui) + + + } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 602e940..ac16119 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,6 +10,7 @@ + - - + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/dattingapp/Login/LoginActivity.kt b/app/src/main/java/com/example/dattingapp/Login/LoginActivity.kt index c37d2f6..791e2cc 100644 --- a/app/src/main/java/com/example/dattingapp/Login/LoginActivity.kt +++ b/app/src/main/java/com/example/dattingapp/Login/LoginActivity.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.dattingapp.ui.theme.DattingAPPTheme import com.example.dattingapp.R +import com.example.dattingapp.registration.RegistrationScreen // Renamed Activity to LoginActivity to fix the shadowing collision class LoginActivity : ComponentActivity() { diff --git a/app/src/main/java/com/example/dattingapp/Login/RegistrationScreen.kt b/app/src/main/java/com/example/dattingapp/Login/RegistrationScreen.kt deleted file mode 100644 index 6238e04..0000000 --- a/app/src/main/java/com/example/dattingapp/Login/RegistrationScreen.kt +++ /dev/null @@ -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 -) { - 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 = {} - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/example/dattingapp/Login/UploadProfileImgae.kt b/app/src/main/java/com/example/dattingapp/Login/UploadProfileImgae.kt deleted file mode 100644 index 4804f02..0000000 --- a/app/src/main/java/com/example/dattingapp/Login/UploadProfileImgae.kt +++ /dev/null @@ -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() -} diff --git a/app/src/main/java/com/example/dattingapp/Login/VibeType.kt b/app/src/main/java/com/example/dattingapp/Login/VibeType.kt deleted file mode 100644 index 5fa7ebc..0000000 --- a/app/src/main/java/com/example/dattingapp/Login/VibeType.kt +++ /dev/null @@ -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(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() -} \ No newline at end of file diff --git a/app/src/main/java/com/example/dattingapp/Login/VideoIntroScreen.kt b/app/src/main/java/com/example/dattingapp/Login/VideoIntroScreen.kt deleted file mode 100644 index 7e5418a..0000000 --- a/app/src/main/java/com/example/dattingapp/Login/VideoIntroScreen.kt +++ /dev/null @@ -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 - } - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/example/dattingapp/Login/ui/theme/Color.kt b/app/src/main/java/com/example/dattingapp/Login/ui/theme/Color.kt new file mode 100644 index 0000000..7b2bd68 --- /dev/null +++ b/app/src/main/java/com/example/dattingapp/Login/ui/theme/Color.kt @@ -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) \ No newline at end of file diff --git a/app/src/main/java/com/example/dattingapp/Login/ui/theme/Theme.kt b/app/src/main/java/com/example/dattingapp/Login/ui/theme/Theme.kt new file mode 100644 index 0000000..2ca16c6 --- /dev/null +++ b/app/src/main/java/com/example/dattingapp/Login/ui/theme/Theme.kt @@ -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 + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/dattingapp/Login/ui/theme/Type.kt b/app/src/main/java/com/example/dattingapp/Login/ui/theme/Type.kt new file mode 100644 index 0000000..a47b73f --- /dev/null +++ b/app/src/main/java/com/example/dattingapp/Login/ui/theme/Type.kt @@ -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 + ) + */ +) \ No newline at end of file diff --git a/app/src/main/java/com/example/dattingapp/MainActivity.kt b/app/src/main/java/com/example/dattingapp/MainActivity.kt index ea8da14..2ecae70 100644 --- a/app/src/main/java/com/example/dattingapp/MainActivity.kt +++ b/app/src/main/java/com/example/dattingapp/MainActivity.kt @@ -6,8 +6,6 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.ui.platform.LocalContext -import com.example.dattingapp.HomeActivity -import com.example.dattingapp.permission.LocationPermision import com.example.dattingapp.permission.NotificationPermission import com.example.dattingapp.ui.theme.DattingAPPTheme @@ -18,7 +16,7 @@ class MainActivity : ComponentActivity() { setContent { DattingAPPTheme { val context = LocalContext.current - val intent = Intent(context, HomeActivity::class.java) + val intent = Intent(context, NotificationPermission::class.java) context.startActivity(intent) finish() } diff --git a/app/src/main/java/com/example/dattingapp/MeetActivity.kt b/app/src/main/java/com/example/dattingapp/MeetActivity.kt new file mode 100644 index 0000000..662b725 --- /dev/null +++ b/app/src/main/java/com/example/dattingapp/MeetActivity.kt @@ -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 = 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() + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/dattingapp/RegistrationScreen.kt b/app/src/main/java/com/example/dattingapp/RegistrationScreen.kt new file mode 100644 index 0000000..c8377a8 --- /dev/null +++ b/app/src/main/java/com/example/dattingapp/RegistrationScreen.kt @@ -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(null) } + + var passwordInput by remember { mutableStateOf("") } + var confirmPasswordInput by remember { mutableStateOf("") } + var passwordError by remember { mutableStateOf(null) } + + var nameInput by remember { mutableStateOf("") } + var nameError by remember { mutableStateOf(null) } + + var dobInput by remember { mutableStateOf("") } + var dobError by remember { mutableStateOf(null) } + + var genderInput by remember { mutableStateOf("") } + var genderError by remember { mutableStateOf(null) } + + val selectedInterests = remember { mutableStateListOf() } + var interestsError by remember { mutableStateOf(null) } + + + var vibeInput by remember { mutableStateOf("") } + var vibeError by remember { mutableStateOf(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, + 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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/dattingapp/UploadIntroVideoActivity.kt b/app/src/main/java/com/example/dattingapp/UploadIntroVideoActivity.kt new file mode 100644 index 0000000..f1d17a7 --- /dev/null +++ b/app/src/main/java/com/example/dattingapp/UploadIntroVideoActivity.kt @@ -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(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(null) } + var capturedPhotoUri by remember { mutableStateOf(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 = { _, _ -> } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/dattingapp/UploadProfileImageActivity.kt b/app/src/main/java/com/example/dattingapp/UploadProfileImageActivity.kt new file mode 100644 index 0000000..668f6a6 --- /dev/null +++ b/app/src/main/java/com/example/dattingapp/UploadProfileImageActivity.kt @@ -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(null) } + var tempCameraUri by remember { mutableStateOf(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 = {} + ) + } +} diff --git a/app/src/main/java/com/example/dattingapp/registration/ApiService.kt b/app/src/main/java/com/example/dattingapp/registration/ApiService.kt new file mode 100644 index 0000000..20e81cc --- /dev/null +++ b/app/src/main/java/com/example/dattingapp/registration/ApiService.kt @@ -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 + + // ========================================== + // Upload Video + // ========================================== + + @Multipart + @POST("api/v1/users/me/video") + suspend fun uploadProfileVideo( + @Header("Authorization") authorization: String, + @Part video: MultipartBody.Part + ): Response + + // ========================================== + // Final Registration + // ========================================== + + @POST("api/v1/auth/register") + suspend fun register( + @Body request: RegisterRequest + ): Response +} + diff --git a/app/src/main/java/com/example/dattingapp/registration/RegistrationFileUtils.kt b/app/src/main/java/com/example/dattingapp/registration/RegistrationFileUtils.kt new file mode 100644 index 0000000..fcddb65 --- /dev/null +++ b/app/src/main/java/com/example/dattingapp/registration/RegistrationFileUtils.kt @@ -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 + ) +} diff --git a/app/src/main/java/com/example/dattingapp/registration/RegistrationModels.kt b/app/src/main/java/com/example/dattingapp/registration/RegistrationModels.kt new file mode 100644 index 0000000..70e54bd --- /dev/null +++ b/app/src/main/java/com/example/dattingapp/registration/RegistrationModels.kt @@ -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 +) + + +// =============================== +// 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? +) + + diff --git a/app/src/main/java/com/example/dattingapp/registration/RegistrationModule.kt b/app/src/main/java/com/example/dattingapp/registration/RegistrationModule.kt new file mode 100644 index 0000000..9529c51 --- /dev/null +++ b/app/src/main/java/com/example/dattingapp/registration/RegistrationModule.kt @@ -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) + } +} + diff --git a/app/src/main/java/com/example/dattingapp/registration/RegistrationRepository.kt b/app/src/main/java/com/example/dattingapp/registration/RegistrationRepository.kt new file mode 100644 index 0000000..6fbfebc --- /dev/null +++ b/app/src/main/java/com/example/dattingapp/registration/RegistrationRepository.kt @@ -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 { + + 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 { + + 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 { + + 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) + } + } +} + diff --git a/app/src/main/java/com/example/dattingapp/registration/RegistrationScreen.kt b/app/src/main/java/com/example/dattingapp/registration/RegistrationScreen.kt new file mode 100644 index 0000000..563ffd8 --- /dev/null +++ b/app/src/main/java/com/example/dattingapp/registration/RegistrationScreen.kt @@ -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(null) } + + var passwordInput by remember { mutableStateOf("") } + var confirmPasswordInput by remember { mutableStateOf("") } + var passwordError by remember { mutableStateOf(null) } + + var nameInput by remember { mutableStateOf("") } + var nameError by remember { mutableStateOf(null) } + + var dobInput by remember { mutableStateOf("") } + var dobError by remember { mutableStateOf(null) } + + var genderInput by remember { mutableStateOf("") } + var genderError by remember { mutableStateOf(null) } + + val selectedInterests = remember { mutableStateListOf() } + var interestsError by remember { mutableStateOf(null) } + + + var vibeInput by remember { mutableStateOf("") } + var vibeError by remember { mutableStateOf(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, + 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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/dattingapp/registration/UploadIntroVideoActivity.kt b/app/src/main/java/com/example/dattingapp/registration/UploadIntroVideoActivity.kt new file mode 100644 index 0000000..3d3dd42 --- /dev/null +++ b/app/src/main/java/com/example/dattingapp/registration/UploadIntroVideoActivity.kt @@ -0,0 +1,1255 @@ +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.filled.Error +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.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 androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.FileProvider +import androidx.lifecycle.lifecycleScope +import androidx.media3.common.MediaItem +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.ui.PlayerView +import coil.compose.rememberAsyncImagePainter +import com.example.dattingapp.HomeActivity +import com.example.dattingapp.ui.theme.BackgroundGradientBottom +import com.example.dattingapp.ui.theme.BackgroundGradientTop +import com.example.dattingapp.ui.theme.CardBackground +import com.example.dattingapp.ui.theme.DarkText +import com.example.dattingapp.ui.theme.DattingAPPTheme +import com.example.dattingapp.ui.theme.LightBlueIconBg +import com.example.dattingapp.ui.theme.MutedText +import com.example.dattingapp.ui.theme.PrimaryBlue +import kotlinx.coroutines.launch +import java.io.File + + +// ========================================================== +// DISPLAY MODE +// ========================================================== + +enum class DisplayMode { + IMAGE, + VIDEO, + BIO +} + + +// ========================================================== +// ACTIVITY +// ========================================================== + +class UploadIntroVideoActivity : ComponentActivity() { + + private var isRegistrationLoading by mutableStateOf(false) + private var registrationError by mutableStateOf(null) + + override fun onCreate(savedInstanceState: Bundle?) { + + + + super.onCreate(savedInstanceState) + + enableEdgeToEdge() + + // ===================================================== + // Get previous registration information + // ===================================================== + + 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 profileImageUriString = intent.getStringExtra("EXTRA_PROFILE_IMAGE_URI") + val profileImageUri = profileImageUriString?.let { Uri.parse(it) } + + // ===================================================== + // COMPOSE + // ===================================================== + + setContent { + DattingAPPTheme { + Box(modifier = Modifier.fillMaxSize()) { + UploadIntroVideoScreen( + profileImageUri = profileImageUri, + onBackClick = { finish() }, + onSkipClick = { /* TODO: handle skip */ }, + onSubmit = { videoUri, bioText -> + lifecycleScope.launch { + submitRegistration( + isPhone = isPhone, + countryCode = countryCode, + contactValue = contactValue, + password = password, + name = name, + dob = dob, + gender = gender, + interests = interests, + profileImageUri = profileImageUri, + videoUri = videoUri, + bio = bioText + ) + } + } + ) + + // Overlay: shows loading spinner, or error state if registrationError is set + if (isRegistrationLoading) { + VideoUploadLoadingScreen( + errorMessage = registrationError, + onRetry = { + isRegistrationLoading = false + registrationError = null + } + ) + } + } + } + } + } + + // ========================================================== + // COMPLETE REGISTRATION FLOW + // ========================================================== + + private suspend fun submitRegistration( + isPhone: Boolean, + countryCode: String, + contactValue: String, + password: String, + name: String, + dob: String, + gender: String, + interests: ArrayList, + profileImageUri: Uri?, + videoUri: Uri?, + bio: String + ) { + isRegistrationLoading = true + registrationError = null + + val repository = RegistrationRepository(this) + + try { + // STEP 1: Prepare email / phone + val emailOrPhone = if (isPhone) { + countryCode + contactValue + } else { + contactValue + } + + // STEP 2: REGISTER USER + Toast.makeText(this, "Creating your account...", Toast.LENGTH_SHORT).show() + val registerRequest = RegisterRequest( + emailOrPhone = emailOrPhone, + password = password, + confirmPassword = password, + fullName = name, + gender = gender, + birthDate = dob, + city = "", + bio = bio, + videoIntro = null, + profileImage = null, + interests = interests + ) + val registrationResult = repository.register(registerRequest) + + // STEP 3: CHECK REGISTRATION + if (registrationResult.isFailure) { + val error = registrationResult.exceptionOrNull()?.message ?: "Registration failed" + Log.e("REGISTRATION", "Registration failed", registrationResult.exceptionOrNull()) + registrationError = "Registration failed: $error" + return + } + + // STEP 4: GET RESPONSE + val response = registrationResult.getOrNull() + if (response == null) { + registrationError = "Empty registration response" + return + } + + // STEP 5: GET ACCESS TOKEN + val accessToken = response.accessToken + if (accessToken.isNullOrBlank()) { + Log.e("REGISTRATION", "Access token is empty") + registrationError = "Access token was not received" + return + } + + Log.d("REGISTRATION", "Registration successful. Token received.") + Toast.makeText(this, "Account created successfully!", Toast.LENGTH_SHORT).show() + + // STEP 6: UPLOAD PROFILE IMAGE + if (profileImageUri != null) { + Toast.makeText(this, "Uploading profile image...", Toast.LENGTH_SHORT).show() + val imageResult = repository.uploadImage(token = "Bearer $accessToken", uri = profileImageUri) + if (imageResult.isFailure) { + val error = imageResult.exceptionOrNull()?.message ?: "Image upload failed" + Log.e("UPLOAD_IMAGE", "Image upload failed", imageResult.exceptionOrNull()) + registrationError = "Image upload failed: $error" + return + } + Log.d("UPLOAD_IMAGE", "Profile Image URL = ${imageResult.getOrNull()}") + Toast.makeText(this, "Profile image uploaded", Toast.LENGTH_SHORT).show() + } + + // STEP 7: UPLOAD INTRO VIDEO + if (videoUri != null) { + Toast.makeText(this, "Uploading intro video...", Toast.LENGTH_SHORT).show() + val videoResult = repository.uploadVideo(token = "Bearer $accessToken", uri = videoUri) + if (videoResult.isFailure) { + val error = videoResult.exceptionOrNull()?.message ?: "Video upload failed" + Log.e("UPLOAD_VIDEO", "Video upload failed", videoResult.exceptionOrNull()) + registrationError = "Video upload failed: $error" + return + } + Log.d("UPLOAD_VIDEO", "Video URL = ${videoResult.getOrNull()}") + Toast.makeText(this, "Video uploaded", Toast.LENGTH_SHORT).show() + } + + // STEP 8: SUCCESS — GO TO HOME + Toast.makeText(this, "Registration completed successfully!", Toast.LENGTH_LONG).show() + startActivity(Intent(this, HomeActivity::class.java)) + finish() + + } catch (e: Exception) { + Log.e("REGISTRATION", "Unexpected registration error", e) + registrationError = e.message ?: "Something went wrong" + } + } +} + + +// ========================================================== +// COMPOSE UI +// ========================================================== + +@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(null) + } + + var bioText by remember { + mutableStateOf("") + } + + + var tempCameraPhotoUri by remember { + mutableStateOf(null) + } + + var capturedPhotoUri by remember { + mutableStateOf(null) + } + + + // ========================================================== + // Gallery video picker + // ========================================================== + + val videoPickerLauncher = + rememberLauncherForActivityResult( + contract = + ActivityResultContracts.GetContent() + ) { uri -> + + if (uri != null) { + + recordedVideoUri = + uri + + displayMode = + DisplayMode.VIDEO + } + } + + + // ========================================================== + // Camera video + // ========================================================== + + val recordVideoLauncher = + rememberLauncherForActivityResult( + contract = + ActivityResultContracts.CaptureVideo() + ) { success -> + + if ( + success && + recordedVideoUri != null + ) { + + displayMode = + DisplayMode.VIDEO + } + } + + + // ========================================================== + // Camera image + // ========================================================== + + 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) + ) + ) + + + // ========================================================== + // MAIN COLUMN + // ========================================================== + + Column( + + modifier = + Modifier + .fillMaxSize() + .background( + Color(0xFFF9F9F9) + ) + .systemBarsPadding() + .imePadding() + .padding( + horizontal = 20.dp, + vertical = 12.dp + ), + + horizontalAlignment = + Alignment.CenterHorizontally, + + verticalArrangement = + Arrangement.SpaceBetween + ) { + + + // ====================================================== + // HEADER + // ====================================================== + + 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 + ) + } + } + + + // ====================================================== + // 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." + } else { + "Say hello! Share a bit about yourself." + }, + + fontSize = 13.sp, + + color = + Color.Gray + ) + } + + + // ====================================================== + // MAIN CONTAINER + // ====================================================== + + Box( + + modifier = + Modifier + .fillMaxWidth() + .weight(1f) + .padding( + vertical = 12.dp + ) + .clip( + RoundedCornerShape( + 20.dp + ) + ) + .background( + Color.LightGray + ), + + contentAlignment = + Alignment.Center + ) { + + when (displayMode) { + + + // ================================================= + // IMAGE + // ================================================= + + DisplayMode.IMAGE -> { + + if ( + capturedPhotoUri != null || + profileImageUri != null + ) { + + Image( + + painter = + rememberAsyncImagePainter( + model = + capturedPhotoUri + ?: profileImageUri + ), + + contentDescription = + "Profile Image", + + modifier = + Modifier.fillMaxSize(), + + contentScale = + ContentScale.Crop + ) + } + } + + + // ================================================= + // VIDEO + // ================================================= + + DisplayMode.VIDEO -> { + + recordedVideoUri?.let { + + VideoPlayer( + videoUri = it + ) + } + } + + + // ================================================= + // BIO + // ================================================= + + DisplayMode.BIO -> { + + OutlinedTextField( + + value = + bioText, + + onValueChange = { + + val words = + it.trim() + .split( + "\\s+".toRegex() + ) + + if ( + it.isEmpty() || + words.size <= 200 + ) { + + bioText = + it + } + }, + + modifier = + Modifier + .fillMaxSize() + .background( + Color.White + ) + .padding(12.dp), + + placeholder = { + Text( + "Write something about yourself..." + ) + }, + + colors = + TextFieldDefaults.colors( + + unfocusedContainerColor = + Color.White, + + focusedContainerColor = + Color.White + ) + ) + } + } + + + // ===================================================== + // MEDIA BUTTONS + // ===================================================== + + if ( + displayMode != + DisplayMode.BIO + ) { + + Row( + + modifier = + Modifier + .align( + Alignment.BottomCenter + ) + .padding( + bottom = 20.dp + ), + + horizontalArrangement = + Arrangement.spacedBy( + 16.dp + ), + + verticalAlignment = + Alignment.CenterVertically + ) { + + + // ================================================= + // GALLERY VIDEO + // ================================================= + + Box( + + modifier = + Modifier + .size(48.dp) + .clip( + CircleShape + ) + .background( + Color.White.copy( + alpha = 0.85f + ) + ) + .clickable { + + videoPickerLauncher.launch( + "video/*" + ) + }, + + contentAlignment = + Alignment.Center + ) { + + Icon( + + Icons.Default.FileUpload, + + contentDescription = + "Upload Video", + + tint = + Color(0xFF475569) + ) + } + + + // ================================================= + // RECORD VIDEO + // ================================================= + + Box( + + modifier = + Modifier + .size(64.dp) + .clip( + CircleShape + ) + .background( + Color.White + ) + .border( + 2.dp, + Color(0xFFE11D48), + CircleShape + ) + .clickable { + + val uri = + createVideoUri( + context + ) + + recordedVideoUri = + uri + + recordVideoLauncher.launch( + uri + ) + }, + + contentAlignment = + Alignment.Center + ) { + + Box( + + modifier = + Modifier + .size(48.dp) + .clip( + CircleShape + ) + .background( + Color(0xFFE11D48) + ), + + contentAlignment = + Alignment.Center + ) { + + Icon( + + Icons.Default.Videocam, + + contentDescription = + "Record Video", + + tint = + Color.White, + + modifier = + Modifier.size( + 26.dp + ) + ) + } + } + + + // ================================================= + // TAKE PHOTO + // ================================================= + + Box( + + modifier = + Modifier + .size(48.dp) + .clip( + CircleShape + ) + .background( + Color.White.copy( + alpha = 0.85f + ) + ) + .clickable { + + val uri = + createIntroImageUri( + context + ) + + tempCameraPhotoUri = + uri + + takePhotoLauncher.launch( + uri + ) + }, + + contentAlignment = + Alignment.Center + ) { + + Icon( + + Icons.Default.PhotoCamera, + + contentDescription = + "Take Photo", + + tint = + Color(0xFF475569) + ) + } + } + } + } + + + // ========================================================== + // BOTTOM BUTTONS + // ========================================================== + + Column( + + modifier = + Modifier.fillMaxWidth(), + + verticalArrangement = + Arrangement.spacedBy( + 10.dp + ) + ) { + + Button( + + onClick = { + + onSubmit( + recordedVideoUri, + bioText + ) + }, + + modifier = + Modifier + .fillMaxWidth() + .height(50.dp) + .background( + buttonGradient, + 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 + ) + ) { + + Text( + + text = + if ( + displayMode == + DisplayMode.BIO + ) { + "VIDEO INTRO" + } else { + "ADD BIO" + }, + + color = + Color(0xFFE11D48), + + fontWeight = + FontWeight.Bold, + + fontSize = + 14.sp + ) + } + } + } +} + + +// ========================================================== +// VIDEO PLAYER +// ========================================================== + +@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( + exoPlayer + ) { + + onDispose { + + exoPlayer.release() + } + } + + + AndroidView( + + factory = { ctx -> + + PlayerView(ctx).apply { + + player = + exoPlayer + + useController = + true + } + }, + + modifier = + Modifier.fillMaxSize() + ) +} + + +// ========================================================== +// CREATE VIDEO URI +// ========================================================== + +fun createVideoUri( + context: Context +): Uri { + + val file = + File.createTempFile( + "intro_video_", + ".mp4", + context.cacheDir + ) + + + return FileProvider.getUriForFile( + + context, + + "${context.packageName}.provider", + + file + ) +} + + +// ========================================================== +// CREATE IMAGE URI +// ========================================================== + +fun createIntroImageUri( + context: Context +): Uri { + + val file = + File.createTempFile( + "profile_image_", + ".jpg", + context.cacheDir + ) + + + return FileProvider.getUriForFile( + + context, + + "${context.packageName}.provider", + + file + ) +} + + +@Composable +fun VideoUploadLoadingScreen( + errorMessage: String? = null, + onRetry: () -> Unit = {} +) { + Box( + modifier = Modifier + .fillMaxSize() + .background( + brush = Brush.verticalGradient( + colors = listOf( + BackgroundGradientTop, + BackgroundGradientBottom + ) + ) + ), + contentAlignment = Alignment.Center + ) { + Card( + modifier = Modifier + .fillMaxWidth(0.85f) + .wrapContentHeight(), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors(containerColor = CardBackground), + elevation = CardDefaults.cardElevation(defaultElevation = 8.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 36.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + if (errorMessage == null) { + // ==================== LOADING STATE ==================== + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.size(80.dp) + ) { + CircularProgressIndicator( + modifier = Modifier.fillMaxSize(), + color = PrimaryBlue, + strokeWidth = 3.dp + ) + Box( + modifier = Modifier + .size(56.dp) + .clip(RoundedCornerShape(16.dp)) + .background(LightBlueIconBg), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.Videocam, + contentDescription = "Video Icon", + tint = Color.White, + modifier = Modifier.size(32.dp) + ) + } + } + + Spacer(modifier = Modifier.height(28.dp)) + + Text( + text = "Uploading your vibe...", + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = DarkText, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "This will only take a moment.", + fontSize = 13.sp, + color = MutedText, + textAlign = TextAlign.Center + ) + + } else { + // ==================== ERROR STATE ==================== + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.size(80.dp) + ) { + Box( + modifier = Modifier + .size(64.dp) + .clip(RoundedCornerShape(16.dp)) + .background(Color(0xFFFFEBEE)), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Filled.Error, + contentDescription = "Error", + tint = Color(0xFFE53935), + modifier = Modifier.size(32.dp) + ) + } + } + + Spacer(modifier = Modifier.height(28.dp)) + + Text( + text = "Upload Failed", + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = DarkText, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = errorMessage, + fontSize = 13.sp, + color = MutedText, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Button( + onClick = onRetry, + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFFE53935) + ), + shape = RoundedCornerShape(12.dp) + ) { + Text( + text = "Try Again", + color = Color.White, + fontWeight = FontWeight.Bold, + fontSize = 16.sp + ) + } + } + } + } + } +} + +@Preview(showBackground = true, widthDp = 360, heightDp = 800) +@Composable +fun VideoUploadLoadingScreenPreview() { + MaterialTheme { + VideoUploadLoadingScreen() + } +} + +@Preview(showBackground = true, widthDp = 360, heightDp = 800) +@Composable +fun VideoUploadErrorScreenPreview() { + MaterialTheme { + VideoUploadLoadingScreen( + errorMessage = "Network connection failed. Please try again.", + onRetry = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/dattingapp/registration/UploadProfileImageActivity.kt b/app/src/main/java/com/example/dattingapp/registration/UploadProfileImageActivity.kt new file mode 100644 index 0000000..33794d8 --- /dev/null +++ b/app/src/main/java/com/example/dattingapp/registration/UploadProfileImageActivity.kt @@ -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(null) } + var tempCameraUri by remember { mutableStateOf(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 = {} + ) + } +} diff --git a/app/src/main/java/com/example/dattingapp/ui/theme/Color.kt b/app/src/main/java/com/example/dattingapp/ui/theme/Color.kt index 952c8b1..c6425a2 100644 --- a/app/src/main/java/com/example/dattingapp/ui/theme/Color.kt +++ b/app/src/main/java/com/example/dattingapp/ui/theme/Color.kt @@ -1,5 +1,6 @@ package com.example.dattingapp.ui.theme +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color val Purple80 = Color(0xFFD0BCFF) @@ -35,3 +36,40 @@ val AccentPurple = Color(0xFF6C11D9) val BadgePink = Color(0xFFFF2A7A) val ChipBgPink = Color(0xFFFDE8EF) val ChipTextPink = Color(0xFFD81B60) +val primaryColor = Color(0xFFC2185B) +val lightChipBg = Color(0xFFFCE4EC) +val CrimsonPrimary = Color(0xFFC2185B) // Primary brand accent color used for toggles, icons, & titles +val LightCardBg = Color(0xFFF9F9F9) // Background color for rounded card sections + +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) + ) +) + + +// Color definitions matching the design mockup +val BackgroundGradientTop = Color(0xFF916A8C) +val BackgroundGradientBottom = Color(0xFF635270) +val PrimaryBlue = Color(0xFF0066FF) +val LightBlueIconBg = Color(0xFF8CAEF8) +val DarkText = Color(0xFF1E232A) +val MutedText = Color(0xFF757575) + diff --git a/app/src/main/res/drawable/loverobot.png b/app/src/main/res/drawable/loverobot.png new file mode 100644 index 0000000..ae85040 Binary files /dev/null and b/app/src/main/res/drawable/loverobot.png differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4be336d..81f8ef8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -85,6 +85,10 @@ ExploreScreen ChatScreen FriendProfile + MeetActivity + SettingScreen + UploadImageActivity + UploadIntroVideoActivity \ No newline at end of file diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..08f6445 --- /dev/null +++ b/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ef7459c..d7053ab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,8 @@ material = "1.10.0" appcompat = "1.7.1" constraintlayout = "2.1.4" activityKtx = "1.8.0" +media3ExoplayerVersion = "1.5.1" +media3UiVersion = "1.5.1" navigationFragmentKtx = "2.6.0" navigationUiKtx = "2.6.0" material3 = "1.4.0" @@ -38,6 +40,8 @@ androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", ve androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigationFragmentKtx" } androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" } androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } +media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3UiVersion" } +media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3ExoplayerVersion" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }