diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index ca16a99..b49fb06 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -6,6 +6,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 9d57ed1..5568588 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -81,6 +81,9 @@ dependencies {
implementation(libs.media3.exoplayer)
implementation(libs.media3.ui)
+ implementation("androidx.security:security-crypto:1.1.0-alpha06")
+
+
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ac16119..692fcac 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -123,30 +123,20 @@
android:label="@string/title_activity_meet"
android:theme="@style/Theme.DattingAPP"
android:windowSoftInputMode="adjustResize" />
-
-
-
-
-
-
+
+ android:exported="false" />
+
+
+
+
\ 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
index c8377a8..87fb5ba 100644
--- a/app/src/main/java/com/example/dattingapp/RegistrationScreen.kt
+++ b/app/src/main/java/com/example/dattingapp/RegistrationScreen.kt
@@ -28,9 +28,6 @@ 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
@@ -38,12 +35,9 @@ 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
+import com.example.dattingapp.registration.UploadProfileImageActivity
// Global Email Validation Regex
val EMAIL_PATTERN_REGEX = Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
diff --git a/app/src/main/java/com/example/dattingapp/UploadIntroVideoActivity.kt b/app/src/main/java/com/example/dattingapp/UploadIntroVideoActivity.kt
deleted file mode 100644
index f1d17a7..0000000
--- a/app/src/main/java/com/example/dattingapp/UploadIntroVideoActivity.kt
+++ /dev/null
@@ -1,439 +0,0 @@
-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
deleted file mode 100644
index 668f6a6..0000000
--- a/app/src/main/java/com/example/dattingapp/UploadProfileImageActivity.kt
+++ /dev/null
@@ -1,417 +0,0 @@
-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
deleted file mode 100644
index 20e81cc..0000000
--- a/app/src/main/java/com/example/dattingapp/registration/ApiService.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-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/AuthApiService.kt b/app/src/main/java/com/example/dattingapp/registration/AuthApiService.kt
new file mode 100644
index 0000000..ca51452
--- /dev/null
+++ b/app/src/main/java/com/example/dattingapp/registration/AuthApiService.kt
@@ -0,0 +1,19 @@
+package com.example.dattingapp.registration
+
+import okhttp3.MultipartBody
+import retrofit2.Response
+import retrofit2.http.Body
+import retrofit2.http.Multipart
+import retrofit2.http.POST
+import retrofit2.http.Part
+
+interface AuthApiService {
+ @POST("/api/v1/auth/register")
+ suspend fun registerUser(@Body request: RegisterRequest): Response
+
+ @Multipart
+ @POST("api/v1/users/me/image")
+ suspend fun uploadProfileImage(
+ @Part image: MultipartBody.Part
+ ): Response
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/dattingapp/registration/NetworkModule.kt b/app/src/main/java/com/example/dattingapp/registration/NetworkModule.kt
new file mode 100644
index 0000000..701b495
--- /dev/null
+++ b/app/src/main/java/com/example/dattingapp/registration/NetworkModule.kt
@@ -0,0 +1,76 @@
+package com.example.dattingapp.registration
+
+import android.content.Context
+import android.net.Uri
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.MultipartBody
+import okhttp3.OkHttpClient
+import okhttp3.RequestBody.Companion.asRequestBody
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import java.io.File
+import java.io.FileOutputStream
+
+object NetworkModule {
+
+ private const val BASE_URL = "https://melove.youthome.com.cn"
+
+ @Volatile
+ private var apiService: AuthApiService? = null
+
+ fun getApiService(context: Context): AuthApiService {
+ return apiService ?: synchronized(this) {
+ apiService ?: buildRetrofit(context.applicationContext).create(AuthApiService::class.java).also {
+ apiService = it
+ }
+ }
+ }
+
+ private fun buildRetrofit(context: Context): Retrofit {
+ val tokenManager = TokenManager.getInstance(context)
+
+ val okHttpClient = OkHttpClient.Builder()
+ .addInterceptor { chain ->
+ val requestBuilder = chain.request().newBuilder()
+ val token = tokenManager.getAccessToken()
+
+ if (!token.isNullOrEmpty()) {
+ requestBuilder.addHeader("Authorization", "Bearer $token")
+ }
+
+ chain.proceed(requestBuilder.build())
+ }
+ .build()
+
+ return Retrofit.Builder()
+ .baseUrl(BASE_URL)
+ .client(okHttpClient)
+ .addConverterFactory(GsonConverterFactory.create())
+ .build()
+ }
+
+ fun prepareImagePart(context: Context, uri: Uri, partName: String = "image"): MultipartBody.Part? {
+ return try {
+ val contentResolver = context.contentResolver
+ val mimeType = contentResolver.getType(uri) ?: "image/jpeg"
+ val inputStream = contentResolver.openInputStream(uri) ?: return null
+
+ val extension = when (mimeType) {
+ "image/png" -> ".png"
+ "image/webp" -> ".webp"
+ else -> ".jpg"
+ }
+
+ val tempFile = File.createTempFile("upload_image_", extension, context.cacheDir)
+ FileOutputStream(tempFile).use { output ->
+ inputStream.copyTo(output)
+ }
+
+ val requestFile = tempFile.asRequestBody(mimeType.toMediaTypeOrNull())
+ MultipartBody.Part.createFormData(partName, tempFile.name, requestFile)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/dattingapp/registration/RegisterRequest.kt b/app/src/main/java/com/example/dattingapp/registration/RegisterRequest.kt
new file mode 100644
index 0000000..6307624
--- /dev/null
+++ b/app/src/main/java/com/example/dattingapp/registration/RegisterRequest.kt
@@ -0,0 +1,45 @@
+package com.example.dattingapp.registration
+
+import com.google.gson.annotations.SerializedName
+
+// Request Data Model matching your JSON specification
+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? = null,
+ @SerializedName("video_intro") val videoIntro: String? = null,
+ @SerializedName("Profile_image") val profileImage: String? = null,
+ @SerializedName("interests") val interests: List? = null
+)
+
+data class RegisterResponse(
+ @SerializedName("access_token")
+ val accessToken: String,
+
+ @SerializedName("refresh_token")
+ val refreshToken: String,
+
+ @SerializedName("token_type")
+ val tokenType: String
+)
+
+// Response data class representing the API response
+data class ProfileImageUploadResponse(
+ val status: Boolean,
+ val message: String?,
+ val data: ProfileImageData?
+)
+
+data class ProfileImageData(
+ val url: String? // Adjust field name according to your API response JSON structure
+)
+
+data class ImageUploadResponse(
+ val message: String,
+ val imageUrl: String?
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/example/dattingapp/registration/RegistrationFileUtils.kt b/app/src/main/java/com/example/dattingapp/registration/RegistrationFileUtils.kt
deleted file mode 100644
index fcddb65..0000000
--- a/app/src/main/java/com/example/dattingapp/registration/RegistrationFileUtils.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-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
deleted file mode 100644
index 70e54bd..0000000
--- a/app/src/main/java/com/example/dattingapp/registration/RegistrationModels.kt
+++ /dev/null
@@ -1,96 +0,0 @@
-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
deleted file mode 100644
index 9529c51..0000000
--- a/app/src/main/java/com/example/dattingapp/registration/RegistrationModule.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-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
deleted file mode 100644
index 6fbfebc..0000000
--- a/app/src/main/java/com/example/dattingapp/registration/RegistrationRepository.kt
+++ /dev/null
@@ -1,187 +0,0 @@
-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
index bc352de..836a3c1 100644
--- a/app/src/main/java/com/example/dattingapp/registration/RegistrationScreen.kt
+++ b/app/src/main/java/com/example/dattingapp/registration/RegistrationScreen.kt
@@ -1,6 +1,8 @@
package com.example.dattingapp.registration
+import android.content.Intent
import android.os.Bundle
+import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
@@ -9,6 +11,7 @@ import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
@@ -19,25 +22,15 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowForward
-import androidx.compose.material.icons.filled.Add
-import androidx.compose.material.icons.filled.AddCircle
import androidx.compose.material.icons.filled.AlternateEmail
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.Badge
-import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.CheckCircle
-import androidx.compose.material.icons.filled.Close
-import androidx.compose.material.icons.filled.Female
-import androidx.compose.material.icons.filled.Groups
-import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Mail
-import androidx.compose.material.icons.filled.Male
import androidx.compose.material.icons.filled.PhoneIphone
-import androidx.compose.material.icons.filled.Public
import androidx.compose.material.icons.filled.RadioButtonUnchecked
import androidx.compose.material.icons.filled.SmartToy
-import androidx.compose.material.icons.filled.VerifiedUser
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.*
@@ -63,6 +56,21 @@ import com.example.dattingapp.ui.theme.PinkGradientStart
import com.example.dattingapp.ui.theme.PurpleGradientEnd
import com.example.dattingapp.ui.theme.textDark
import com.example.dattingapp.ui.theme.textPink
+import androidx.compose.material.icons.filled.CalendarToday
+import androidx.compose.material.icons.filled.Cake
+import androidx.compose.ui.platform.LocalContext
+import androidx.lifecycle.viewmodel.compose.viewModel
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Date
+import java.util.Locale
+import androidx.compose.animation.core.*
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.draw.scale
+
class RegistrationScreen : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -77,11 +85,19 @@ class RegistrationScreen : ComponentActivity() {
}
@Composable
-fun RegistrationFlowContainer() {
+fun RegistrationFlowContainer(
+ registrationViewModel: RegistrationViewModel = viewModel()
+) {
+ val context = LocalContext.current
+ val uiState by registrationViewModel.uiState.collectAsState()
+
var currentStep by remember { mutableIntStateOf(1) }
val totalSteps = 6
- BackHandler(enabled = currentStep > 1) { currentStep-- }
+ BackHandler(enabled = currentStep > 1 && uiState !is RegistrationUiState.Loading) {
+ currentStep--
+ }
+ // Input States
var nameInput by rememberSaveable { mutableStateOf("") }
var nameError by rememberSaveable { mutableStateOf(null) }
val nameRegex = remember { Regex("^[a-zA-Z\\s]+$") }
@@ -98,8 +114,33 @@ fun RegistrationFlowContainer() {
var passwordError by remember { mutableStateOf(null) }
var confirmPasswordError by remember { mutableStateOf(null) }
+ var selectedBirthDateMillis by rememberSaveable { mutableStateOf(null) }
+ var addressInput by rememberSaveable { mutableStateOf("") }
+ var selectedGender by rememberSaveable { mutableStateOf("") }
+
val scrollState = rememberScrollState()
+ // Handle Network Success & Error Reactions
+ LaunchedEffect(uiState) {
+ when (val state = uiState) {
+ is RegistrationUiState.Success -> {
+ Toast.makeText(context, "Registration Successful!", Toast.LENGTH_SHORT).show()
+
+ // Navigate to UploadProfileImageActivity and finish flow
+ val intent = Intent(context, UploadProfileImageActivity::class.java).apply {
+ putExtra("ACCESS_TOKEN", state.response.accessToken)
+ putExtra("REFRESH_TOKEN", state.response.refreshToken)
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ }
+ context.startActivity(intent)
+ }
+ is RegistrationUiState.Error -> {
+ Toast.makeText(context, state.message, Toast.LENGTH_LONG).show()
+ }
+ else -> {}
+ }
+ }
+
Box(
modifier = Modifier
.fillMaxSize()
@@ -107,301 +148,298 @@ fun RegistrationFlowContainer() {
.navigationBarsPadding()
.imePadding()
) {
- // -------------------------------------------------------------
- // SINGLE SCROLLABLE COLUMN — everything scrolls together
- // -------------------------------------------------------------
- Column(
- modifier = Modifier
- .fillMaxSize()
- .verticalScroll(scrollState)
- .padding(horizontal = 24.dp)
- ) {
- Spacer(modifier = Modifier.height(16.dp))
-
- // 1. Static Top App Bar
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
+ // Show Full Screen Loading Overlay if API call is active
+ if (uiState is RegistrationUiState.Loading) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.White.copy(alpha = 0.95f)),
+ contentAlignment = Alignment.Center
+ ) {
+ UniquePulseLoader(message = "Setting up your profile...")
+ }
+ } else {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(scrollState)
+ .padding(horizontal = 24.dp)
) {
- IconButton(
- onClick = { if (currentStep > 1) currentStep-- },
- enabled = currentStep > 1
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // 1. App Bar Header
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
) {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = "Back",
- tint = if (currentStep > 1) textDark else Color.Transparent
+ IconButton(
+ onClick = { if (currentStep > 1) currentStep-- },
+ enabled = currentStep > 1
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = "Back",
+ tint = if (currentStep > 1) textDark else Color.Transparent
+ )
+ }
+ Text(
+ text = "VibeCheck",
+ style = MaterialTheme.typography.titleLarge.copy(
+ fontWeight = FontWeight.Bold,
+ color = PurpleGradientEnd,
+ fontSize = 24.sp
+ )
)
+ TextButton(onClick = { /* Skip */ }) {
+ Text(text = "SKIP", color = Color.Gray, fontWeight = FontWeight.Medium)
+ }
}
- Text(
- text = "VibeCheck",
- style = MaterialTheme.typography.titleLarge.copy(
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // 2. Step Indicator Text
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = "STEP $currentStep OF $totalSteps",
+ color = textPink,
fontWeight = FontWeight.Bold,
- color = PurpleGradientEnd,
- fontSize = 24.sp
+ fontSize = 12.sp
+ )
+ Text(
+ text = when (currentStep) {
+ 1 -> "Getting Started"
+ 2 -> "Contact Details"
+ 3 -> "Password"
+ 4 -> "Date of Birth"
+ 5 -> "Location"
+ 6 -> "Interested Gender"
+ else -> ""
+ },
+ color = Color.Gray,
+ fontSize = 12.sp
)
- )
- TextButton(onClick = { /* Skip logic */ }) {
- Text(text = "SKIP", color = Color.Gray, fontWeight = FontWeight.Medium)
}
- }
-
- Spacer(modifier = Modifier.height(16.dp))
-
- // 2. Dynamic Step Counter Header
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Text(
- text = "STEP $currentStep OF $totalSteps",
- color = textPink,
- fontWeight = FontWeight.Bold,
- fontSize = 12.sp
- )
- Text(
- text = when (currentStep) {
- 1 -> "Getting Started"
- 2 -> "Contact Details"
- 3 -> "Gender Identity"
- 4 -> "Date of Birth"
- 5 -> "Location"
- 6 -> "Interests"
- else -> ""
- },
- color = Color.Gray,
- fontSize = 12.sp
- )
- }
- Spacer(modifier = Modifier.height(8.dp))
+ Spacer(modifier = Modifier.height(8.dp))
- // 3. Step Progress Bar
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(6.dp)
- ) {
- for (i in 1..totalSteps) {
- Box(
- modifier = Modifier
- .weight(1f)
- .height(6.dp)
- .clip(RoundedCornerShape(3.dp))
- .background(
- if (i <= currentStep)
- Brush.horizontalGradient(listOf(PinkGradientStart, PurpleGradientEnd))
- else
- Brush.horizontalGradient(listOf(Color(0xFFE0E0E0), Color(0xFFE0E0E0)))
- )
- )
+ // 3. Step Progress Bar
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(6.dp)
+ ) {
+ for (i in 1..totalSteps) {
+ Box(
+ modifier = Modifier
+ .weight(1f)
+ .height(6.dp)
+ .clip(RoundedCornerShape(3.dp))
+ .background(
+ if (i <= currentStep)
+ Brush.horizontalGradient(listOf(PinkGradientStart, PurpleGradientEnd))
+ else
+ Brush.horizontalGradient(listOf(Color(0xFFE0E0E0), Color(0xFFE0E0E0)))
+ )
+ )
+ }
}
- }
- Spacer(modifier = Modifier.height(20.dp))
+ Spacer(modifier = Modifier.height(20.dp))
- // 4. Mascot Frame
- Box(
- modifier = Modifier.fillMaxWidth(),
- contentAlignment = Alignment.Center
- ) {
+ // 4. Avatar Frame
Box(
- modifier = Modifier
- .size(90.dp)
- .clip(RoundedCornerShape(20.dp))
- .background(Color(0xFFFDE8ED)),
+ modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
- Icon(
- imageVector = Icons.Default.SmartToy,
- contentDescription = "Mascot",
- tint = PinkGradientStart,
- modifier = Modifier.size(50.dp)
- )
+ Box(
+ modifier = Modifier
+ .size(90.dp)
+ .clip(RoundedCornerShape(20.dp))
+ .background(Color(0xFFFDE8ED)),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.Default.SmartToy,
+ contentDescription = "Mascot",
+ tint = PinkGradientStart,
+ modifier = Modifier.size(50.dp)
+ )
+ }
}
- Spacer(modifier = Modifier.height(8.dp))
- Text(
- text = "VIBEBOT",
- color = textPink,
- fontSize = 10.sp,
- fontWeight = FontWeight.Bold
- )
-
- }
+ Spacer(modifier = Modifier.height(20.dp))
- Spacer(modifier = Modifier.height(20.dp))
-
- // 5. Dynamic Middle Content Block
- AnimatedContent(
- targetState = currentStep,
- label = "StepAnimation"
- ) { step ->
- when (step) {
- 1 -> StepOneContent(
- name = nameInput,
- errorMessage = nameError,
- onNameChange = { updatedName ->
- nameInput = updatedName
- nameError = null
- }
- )
- 2 -> StepTwoContent(
- selectedTab = selectedTab,
- onTabSelected = { newTab ->
- selectedTab = newTab
- emailError = null
- phoneError = null
- },
- emailText = emailInput,
- onEmailChange = { updatedEmail ->
- emailInput = updatedEmail
- emailError = null
- },
- phoneText = phoneInput,
- onPhoneChange = { updatedPhone ->
- if (updatedPhone.all { it.isDigit() }) {
- phoneInput = updatedPhone
- phoneError = null
- }
- },
- countryCode = countryCode,
- emailError = emailError,
- phoneError = phoneError,
- isEmailValid = emailInput.trim().matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$".toRegex()),
- isPhoneValid = phoneInput.trim().length in 10..15
- )
- 3 -> StepThreeContent(
- passwordInput = passwordInput,
- onPasswordChange = {
- passwordInput = it
- passwordError = null
- },
- confirmPasswordInput = confirmPasswordInput,
- onConfirmPasswordChange = {
- confirmPasswordInput = it
- confirmPasswordError = null
- },
- passwordError = passwordError,
- confirmPasswordError = confirmPasswordError
- )
- 4 -> StepFourContent()
- 5 -> StepFiveContent()
- 6 -> StepSixContent()
- else -> Text("Step $step content coming soon!")
+ // 5. Dynamic Flow Steps
+ AnimatedContent(
+ targetState = currentStep,
+ label = "StepAnimation"
+ ) { step ->
+ when (step) {
+ 1 -> NameCompose(
+ name = nameInput,
+ errorMessage = nameError,
+ onNameChange = { nameInput = it; nameError = null }
+ )
+ 2 -> EmailorPhoneCompose(
+ selectedTab = selectedTab,
+ onTabSelected = { selectedTab = it; emailError = null; phoneError = null },
+ emailText = emailInput,
+ onEmailChange = { emailInput = it; emailError = null },
+ phoneText = phoneInput,
+ onPhoneChange = { if (it.all { char -> char.isDigit() }) { phoneInput = it; phoneError = null } },
+ countryCode = countryCode,
+ emailError = emailError,
+ phoneError = phoneError,
+ isEmailValid = emailInput.trim().matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$".toRegex()),
+ isPhoneValid = phoneInput.trim().length in 10..15
+ )
+ 3 -> PasswordComose(
+ passwordInput = passwordInput,
+ onPasswordChange = { passwordInput = it; passwordError = null },
+ confirmPasswordInput = confirmPasswordInput,
+ onConfirmPasswordChange = { confirmPasswordInput = it; confirmPasswordError = null },
+ passwordError = passwordError,
+ confirmPasswordError = confirmPasswordError
+ )
+ 4 -> BirthDayCompose(
+ selectedDateInMillis = selectedBirthDateMillis,
+ onDateSelected = { selectedBirthDateMillis = it }
+ )
+ 5 -> AddressCompose(
+ addressText = addressInput,
+ onAddressChange = { addressInput = it }
+ )
+ 6 -> GenderCompose(
+ selectedGender = selectedGender,
+ onGenderSelected = { selectedGender = it }
+ )
+ else -> Text("Step $step")
+ }
}
- }
- Spacer(modifier = Modifier.height(100.dp))
-
- // -------------------------------------------------------------
- // CONTINUE BUTTON — inside scroll, scrolls with content
- // -------------------------------------------------------------
- Button(
- onClick = {
- when (currentStep) {
- 1 -> {
- val trimmed = nameInput.trim()
- when {
- trimmed.isEmpty() -> nameError = "Name cannot be empty"
- !nameRegex.matches(trimmed) -> nameError = "Numbers and special characters are not allowed"
- else -> {
- nameError = null
- currentStep++
+ Spacer(modifier = Modifier.height(100.dp))
+
+ // 6. Action Button
+ Button(
+ onClick = {
+ when (currentStep) {
+ 1 -> {
+ val trimmed = nameInput.trim()
+ when {
+ trimmed.isEmpty() -> nameError = "Name cannot be empty"
+ !nameRegex.matches(trimmed) -> nameError = "Numbers and special characters not allowed"
+ else -> currentStep++
}
}
- }
- 2 -> {
- if (selectedTab == 0) {
- val trimmedEmail = emailInput.trim()
- val emailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$".toRegex()
- when {
- trimmedEmail.isEmpty() -> emailError = "Email cannot be empty"
- !emailRegex.matches(trimmedEmail) -> emailError = "Enter a valid email address"
- else -> {
- emailError = null
- currentStep++
- }
+ 2 -> {
+ if (selectedTab == 0) {
+ val trimmedEmail = emailInput.trim()
+ val emailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$".toRegex()
+ if (trimmedEmail.isEmpty() || !emailRegex.matches(trimmedEmail)) {
+ emailError = "Enter a valid email"
+ } else currentStep++
+ } else {
+ val trimmedPhone = phoneInput.trim()
+ if (trimmedPhone.isEmpty() || trimmedPhone.length !in 10..15) {
+ phoneError = "Enter a valid phone number"
+ } else currentStep++
}
- } else {
- val trimmedPhone = phoneInput.trim()
- val phoneDigits = trimmedPhone.filter { it.isDigit() }
- when {
- trimmedPhone.isEmpty() -> phoneError = "Phone number cannot be empty"
- phoneDigits.length !in 10..15 -> phoneError = "Enter a valid phone number (10-15 digits)"
- else -> {
- phoneError = null
- currentStep++
- }
+ }
+ 3 -> {
+ val trimmedPassword = passwordInput.trim()
+ val trimmedConfirm = confirmPasswordInput.trim()
+ var isValid = true
+
+ if (trimmedPassword.length !in 8..20 || !trimmedPassword.any { it.isDigit() }) {
+ passwordError = "Password must be 8-20 characters with letters and numbers"
+ isValid = false
+ }
+ if (trimmedPassword != trimmedConfirm) {
+ confirmPasswordError = "Passwords do not match"
+ isValid = false
}
+ if (isValid) currentStep++
}
- }
- 3 -> {
- val trimmedPassword = passwordInput.trim()
- val trimmedConfirmPassword = confirmPasswordInput.trim()
- val hasLength = trimmedPassword.length in 8..20
- val hasLetterAndDigit = trimmedPassword.any { it.isLetter() } && trimmedPassword.any { it.isDigit() }
- val hasSpecialChar = trimmedPassword.any { !it.isLetterOrDigit() }
-
- var isValid = true
-
- when {
- trimmedPassword.isEmpty() -> { passwordError = "Password cannot be empty"; isValid = false }
- !hasLength -> { passwordError = "Password must be between 8 and 20 characters"; isValid = false }
- !hasLetterAndDigit -> { passwordError = "Password must contain both letters and numbers"; isValid = false }
- !hasSpecialChar -> { passwordError = "Password must contain at least one special character"; isValid = false }
- else -> passwordError = null
+ 4 -> {
+ if (selectedBirthDateMillis == null) {
+ Toast.makeText(context, "Select date of birth", Toast.LENGTH_SHORT).show()
+ } else currentStep++
}
-
- when {
- trimmedConfirmPassword.isEmpty() -> { confirmPasswordError = "Please confirm your password"; isValid = false }
- trimmedPassword != trimmedConfirmPassword -> { confirmPasswordError = "Passwords do not match"; isValid = false }
- else -> confirmPasswordError = null
+ 5 -> {
+ if (addressInput.trim().isEmpty()) {
+ Toast.makeText(context, "Enter location", Toast.LENGTH_SHORT).show()
+ } else currentStep++
}
+ 6 -> {
+ if (selectedGender.isBlank()) {
+ Toast.makeText(context, "Select gender interest", Toast.LENGTH_SHORT).show()
+ return@Button
+ }
- if (isValid) currentStep++
- }
- else -> {
- if (currentStep < totalSteps) currentStep++
+ val formattedDate = selectedBirthDateMillis?.let { millis ->
+ SimpleDateFormat("yyyy-MM-dd", Locale.US).format(Date(millis))
+ } ?: ""
+
+ val contact = if (selectedTab == 0) emailInput.trim() else "$countryCode${phoneInput.trim()}"
+
+ val payload = RegisterRequest(
+ emailOrPhone = contact,
+ password = passwordInput,
+ confirmPassword = confirmPasswordInput,
+ fullName = nameInput.trim(),
+ gender = selectedGender.lowercase(Locale.ROOT),
+ birthDate = formattedDate,
+ city = addressInput.trim(),
+ bio = null,
+ videoIntro = null,
+ profileImage = null,
+ interests = null
+ )
+
+ registrationViewModel.registerUser(context, payload)
+ }
}
- }
- },
- modifier = Modifier
- .fillMaxWidth()
- .height(52.dp)
- .background(
- brush = Brush.horizontalGradient(listOf(PinkGradientStart, PurpleGradientEnd)),
- shape = RoundedCornerShape(26.dp)
- ),
- colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent),
- contentPadding = PaddingValues()
- ) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Center
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(52.dp)
+ .background(
+ brush = Brush.horizontalGradient(listOf(PinkGradientStart, PurpleGradientEnd)),
+ shape = RoundedCornerShape(26.dp)
+ ),
+ colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent),
+ contentPadding = PaddingValues()
) {
- Text(
- text = if (currentStep < totalSteps) "Continue" else "Finish Registration",
- fontSize = 16.sp,
- fontWeight = FontWeight.Bold,
- color = Color.White
- )
- Spacer(modifier = Modifier.width(8.dp))
- Icon(
- imageVector = Icons.AutoMirrored.Filled.ArrowForward,
- contentDescription = null,
- tint = Color.White
- )
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Text(
+ text = if (currentStep < totalSteps) "Continue" else "Finish Registration",
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Bold,
+ color = Color.White
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowForward,
+ contentDescription = null,
+ tint = Color.White
+ )
+ }
}
- }
- Spacer(modifier = Modifier.height(24.dp))
+ Spacer(modifier = Modifier.height(24.dp))
+ }
}
}
}
// --- Step Content Components ---
@Composable
-fun StepOneContent(
+fun NameCompose(
name: String,
errorMessage: String?,
onNameChange: (String) -> Unit
@@ -487,7 +525,7 @@ fun StepOneContent(
}
@Composable
-fun StepTwoContent(selectedTab: Int, onTabSelected: (Int) -> Unit, emailText: String, onEmailChange: (String) -> Unit,
+fun EmailorPhoneCompose(selectedTab: Int, onTabSelected: (Int) -> Unit, emailText: String, onEmailChange: (String) -> Unit,
phoneText: String, onPhoneChange: (String) -> Unit, countryCode: String, emailError: String?,
phoneError: String?, isEmailValid: Boolean = false, isPhoneValid: Boolean = false
) {
@@ -770,7 +808,7 @@ fun StepTwoContent(selectedTab: Int, onTabSelected: (Int) -> Unit, emailText: St
}
@Composable
-fun StepThreeContent(
+fun PasswordComose(
passwordInput: String,
onPasswordChange: (String) -> Unit,
confirmPasswordInput: String,
@@ -1034,28 +1072,87 @@ private fun ValidationRequirementRow(
}
}
+@Composable
+fun AddressCompose(
+ addressText: String = "",
+ onAddressChange: (String) -> Unit = {}
+) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(20.dp),
+ colors = CardDefaults.cardColors(containerColor = Color.White),
+ border = BorderStroke(1.dp, Color(0xFFE0E0E0))
+ ) {
+ Column(
+ modifier = Modifier.padding(16.dp),
+ horizontalAlignment = Alignment.Start
+ ) {
+ Text(
+ text = "WHERE ARE YOU LOCATED?",
+ color = Color(0xFFFF4081),
+ fontSize = 11.sp,
+ fontWeight = FontWeight.Bold
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ OutlinedTextField(
+ value = addressText,
+ onValueChange = onAddressChange,
+ modifier = Modifier.fillMaxWidth(),
+ placeholder = { Text("Enter city or location", color = Color.Gray) },
+ singleLine = true,
+ shape = RoundedCornerShape(12.dp),
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedContainerColor = Color(0xFFF8F9FA),
+ unfocusedContainerColor = Color(0xFFF8F9FA),
+ focusedBorderColor = Color(0xFFFF4081),
+ unfocusedBorderColor = Color.Transparent
+ )
+ )
+ }
+ }
+}
+data class GenderPreferenceOption(
+ val id: String,
+ val title: String,
+ val subtitle: String,
+ val icon: ImageVector
+)
+
+
+
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun StepFiveContent() {
- var cityText by remember { mutableStateOf("") }
- var countryText by remember { mutableStateOf("China") }
- var isCountryDropdownExpanded by remember { mutableStateOf(false) }
-
- val countryList = listOf(
- "United States",
- "United Kingdom",
- "Canada",
- "Australia",
- "Germany",
- "France",
- "Japan"
- )
+fun BirthDayCompose(
+ selectedDateInMillis: Long? = null,
+ onDateSelected: (Long) -> Unit = {}
+) {
+ var showDatePickerDialog by rememberSaveable { mutableStateOf(false) }
+ var selectedMillis by rememberSaveable { mutableStateOf(selectedDateInMillis) }
+ var ageError by rememberSaveable { mutableStateOf(null) }
+
+ // Date Formatter
+ val dateFormatter = remember { SimpleDateFormat("MMMM dd, yyyy", Locale.getDefault()) }
+
+ // Calculate age from selected milliseconds
+ val calculatedAge = remember(selectedMillis) {
+ selectedMillis?.let { millis ->
+ val dob = Calendar.getInstance().apply { timeInMillis = millis }
+ val today = Calendar.getInstance()
+ var age = today.get(Calendar.YEAR) - dob.get(Calendar.YEAR)
+ if (today.get(Calendar.DAY_OF_YEAR) < dob.get(Calendar.DAY_OF_YEAR)) {
+ age--
+ }
+ age
+ }
+ }
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
- // 1. Header Title Card
+ // Header Title Card
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
@@ -1067,7 +1164,7 @@ fun StepFiveContent() {
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
- text = "Where are you located?",
+ text = "When is your birthday?",
style = MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
@@ -1076,7 +1173,7 @@ fun StepFiveContent() {
)
Spacer(modifier = Modifier.height(4.dp))
Text(
- text = "City and country name only to help discover vibes nearby.",
+ text = "Your profile will display your age, not your birthday.",
fontSize = 12.sp,
color = Color.Gray,
textAlign = TextAlign.Center
@@ -1086,498 +1183,224 @@ fun StepFiveContent() {
Spacer(modifier = Modifier.height(16.dp))
- // 2. Input Fields Card
+ // Input Card with Calendar Trigger Button
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
- border = BorderStroke(1.dp, LightBorderColor)
+ border = BorderStroke(
+ width = 1.dp,
+ color = if (ageError != null) Color.Red else LightBorderColor
+ )
) {
Column(modifier = Modifier.padding(16.dp)) {
-
- // --- CITY FIELD ---
Text(
- text = "CITY",
- color = textPink,
+ text = "DATE OF BIRTH",
+ color = if (ageError != null) Color.Red else textPink,
fontSize = 11.sp,
fontWeight = FontWeight.Bold
)
- Spacer(modifier = Modifier.height(8.dp))
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ // Date Display Field & Select Button
Row(
modifier = Modifier
.fillMaxWidth()
- .clip(RoundedCornerShape(12.dp))
- .background(InputBg)
- .border(1.dp, Color(0xFFE5E7EB), RoundedCornerShape(12.dp))
- .padding(horizontal = 12.dp, vertical = 12.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Icon(
- imageVector = Icons.Default.LocationOn,
- contentDescription = null,
- tint = Color.Gray,
- modifier = Modifier.size(20.dp)
- )
- Spacer(modifier = Modifier.width(8.dp))
- BasicTextField(
- value = cityText,
- onValueChange = { cityText = it },
- modifier = Modifier.weight(1f),
- textStyle = LocalTextStyle.current.copy(
- fontSize = 14.sp,
- fontWeight = FontWeight.Medium,
- color = textDark
- ),
- singleLine = true,
- decorationBox = { innerTextField ->
- if (cityText.isEmpty()) {
- Text(
- text = "Enter your city",
- color = Color.LightGray,
- fontSize = 14.sp
- )
- }
- innerTextField()
- }
- )
- }
-
- Spacer(modifier = Modifier.height(16.dp))
-
- // --- COUNTRY FIELD (EXPOSED DROPDOWN MENU) ---
- Text(
- text = "COUNTRY",
- color = textPink,
- fontSize = 11.sp,
- fontWeight = FontWeight.Bold
- )
- Spacer(modifier = Modifier.height(8.dp))
- ExposedDropdownMenuBox(
- expanded = isCountryDropdownExpanded,
- onExpandedChange = { isCountryDropdownExpanded = !isCountryDropdownExpanded }
+ .background(InputBg, shape = RoundedCornerShape(12.dp))
+ .padding(horizontal = 12.dp, vertical = 8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
) {
Row(
- modifier = Modifier
- .menuAnchor()
- .fillMaxWidth()
- .clip(RoundedCornerShape(12.dp))
- .background(InputBg)
- .border(1.dp, Color(0xFFE5E7EB), RoundedCornerShape(12.dp))
- .padding(horizontal = 12.dp, vertical = 12.dp),
- verticalAlignment = Alignment.CenterVertically
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.weight(1f)
) {
Icon(
- imageVector = Icons.Default.Public,
+ imageVector = Icons.Default.Cake,
contentDescription = null,
tint = Color.Gray,
modifier = Modifier.size(20.dp)
)
- Spacer(modifier = Modifier.width(8.dp))
- BasicTextField(
- value = countryText,
- onValueChange = { countryText = it },
- modifier = Modifier.weight(1f),
- textStyle = LocalTextStyle.current.copy(
+ Spacer(modifier = Modifier.width(10.dp))
+ Text(
+ text = selectedMillis?.let { dateFormatter.format(Date(it)) }
+ ?: "Select your birth date",
+ style = MaterialTheme.typography.bodyMedium.copy(
fontSize = 14.sp,
- fontWeight = FontWeight.Medium,
- color = textDark
- ),
- singleLine = true,
- decorationBox = { innerTextField ->
- if (countryText.isEmpty()) {
- Text(
- text = "Select or enter your country",
- color = Color.LightGray,
- fontSize = 14.sp
- )
- }
- innerTextField()
- }
- )
- Icon(
- imageVector = Icons.Default.ArrowDropDown,
- contentDescription = "Dropdown",
- tint = Color.Gray,
- modifier = Modifier.size(20.dp)
+ fontWeight = if (selectedMillis != null) FontWeight.SemiBold else FontWeight.Normal,
+ color = if (selectedMillis != null) textDark else Color.Gray
+ )
)
}
- ExposedDropdownMenu(
- expanded = isCountryDropdownExpanded,
- onDismissRequest = { isCountryDropdownExpanded = false },
- modifier = Modifier.background(Color.White)
+ // Open Calendar Button
+ Button(
+ onClick = { showDatePickerDialog = true },
+ colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent),
+ contentPadding = PaddingValues(horizontal = 12.dp, vertical = 6.dp),
+ modifier = Modifier.background(
+ brush = Brush.horizontalGradient(listOf(PinkGradientStart, PurpleGradientEnd)),
+ shape = RoundedCornerShape(8.dp)
+ )
) {
- countryList.forEach { country ->
- DropdownMenuItem(
- text = { Text(text = country, fontSize = 14.sp, color = textDark) },
- onClick = {
- countryText = country
- isCountryDropdownExpanded = false
- }
- )
- }
+ Icon(
+ imageVector = Icons.Default.CalendarToday,
+ contentDescription = "Open Calendar",
+ tint = Color.White,
+ modifier = Modifier.size(16.dp)
+ )
+ Spacer(modifier = Modifier.width(6.dp))
+ Text(
+ text = "Choose",
+ color = Color.White,
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Bold
+ )
}
}
- Spacer(modifier = Modifier.height(16.dp))
-
- // Privacy Note Row
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.Top
- ) {
- Icon(
- imageVector = Icons.Default.VerifiedUser,
- contentDescription = null,
- tint = Color(0xFF1976D2),
- modifier = Modifier.size(16.dp)
- )
- Spacer(modifier = Modifier.width(8.dp))
+ // Display Age Badge or Error Message
+ if (calculatedAge != null) {
+ Spacer(modifier = Modifier.height(10.dp))
Text(
- text = "We only display your general city and country. Your exact address is never stored.",
- fontSize = 11.sp,
- color = Color.Gray,
- lineHeight = 15.sp
+ text = "You are $calculatedAge years old",
+ color = if (calculatedAge >= 18) textPink else Color.Red,
+ fontSize = 13.sp,
+ fontWeight = FontWeight.Medium,
+ modifier = Modifier.padding(start = 4.dp)
)
}
- }
- }
- }
-}
-
-data class InterestOption(
- val id: String,
- val emoji: String,
- val label: String
-)
-
-@OptIn(ExperimentalLayoutApi::class)
-@Composable
-fun StepFourContent() {
- val availableInterests = listOf(
- InterestOption("1", "🎮", "Gaming"),
- InterestOption("2", "⚽", "Football"),
- InterestOption("3", "📚", "Reading Books"),
- InterestOption("4", "☕", "Coffee Culture"),
- InterestOption("5", "🎵", "Music & Concerts"),
- InterestOption("6", "✈️", "Travel"),
- InterestOption("7", "🍕", "Foodie"),
- InterestOption("8", "🎨", "Art & Design")
- )
- // Tracks selected interest IDs
- val selectedIds = remember { mutableStateListOf("1", "2", "3") }
-
- Column(
- modifier = Modifier.fillMaxWidth(),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- // 1. Header Card
- Card(
- modifier = Modifier.fillMaxWidth(),
- shape = RoundedCornerShape(20.dp),
- colors = CardDefaults.cardColors(containerColor = Color.White),
- border = BorderStroke(1.dp, LightBorderColor)
- ) {
- Column(
- modifier = Modifier.padding(16.dp),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text(
- text = "What are you into?",
- style = MaterialTheme.typography.titleMedium.copy(
- fontWeight = FontWeight.Bold,
- fontSize = 18.sp,
- color = textDark
+ if (ageError != null) {
+ Spacer(modifier = Modifier.height(6.dp))
+ Text(
+ text = ageError!!,
+ color = Color.Red,
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Medium,
+ modifier = Modifier.padding(start = 4.dp)
)
- )
- Spacer(modifier = Modifier.height(4.dp))
- Text(
- text = "Pick your favorite hobbies and interests to find matching vibes.",
- fontSize = 12.sp,
- color = Color.Gray,
- textAlign = TextAlign.Center
- )
+ }
}
}
+ }
- Spacer(modifier = Modifier.height(16.dp))
-
- // 2. Chip Selection Card
- Card(
- modifier = Modifier.fillMaxWidth(),
- shape = RoundedCornerShape(20.dp),
- colors = CardDefaults.cardColors(containerColor = Color.White),
- border = BorderStroke(1.dp, LightBorderColor)
- ) {
- Column(modifier = Modifier.padding(16.dp)) {
-
- // Flow layout dynamically wraps interest chips
- FlowRow(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.Center,
- verticalArrangement = Arrangement.spacedBy(10.dp),
- maxItemsInEachRow = 3
- ) {
- availableInterests.forEach { interest ->
- val isSelected = selectedIds.contains(interest.id)
+ // Material 3 Date Picker Dialog
+ if (showDatePickerDialog) {
+ val datePickerState = rememberDatePickerState(
+ initialSelectedDateMillis = selectedMillis ?: System.currentTimeMillis()
+ )
- Surface(
- onClick = {
- if (isSelected) {
- selectedIds.remove(interest.id)
- } else {
- selectedIds.add(interest.id)
- }
- },
- shape = RoundedCornerShape(20.dp),
- color = Color.Transparent,
- modifier = Modifier
- .padding(horizontal = 4.dp)
- .clip(RoundedCornerShape(20.dp))
- .then(
- if (isSelected) {
- Modifier.background(
- brush = Brush.horizontalGradient(
- colors = listOf(
- PinkGradientStart,
- PurpleGradientEnd
- )
- )
- )
- } else {
- Modifier.background(Color(0xFFEFF3F8))
- }
- )
- ) {
- Row(
- modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Center
- ) {
- Text(
- text = "${interest.emoji} ${interest.label}",
- fontSize = 13.sp,
- fontWeight = FontWeight.Bold,
- color = if (isSelected) Color.White else textDark
- )
- Spacer(modifier = Modifier.width(6.dp))
- Icon(
- imageVector = if (isSelected) Icons.Default.Close else Icons.Default.Add,
- contentDescription = null,
- tint = if (isSelected) Color.White.copy(alpha = 0.8f) else Color.Gray,
- modifier = Modifier.size(16.dp)
- )
+ DatePickerDialog(
+ onDismissRequest = { showDatePickerDialog = false },
+ confirmButton = {
+ TextButton(
+ onClick = {
+ datePickerState.selectedDateMillis?.let { millis ->
+ // Calculate Age Validation
+ val dob = Calendar.getInstance().apply { timeInMillis = millis }
+ val today = Calendar.getInstance()
+ var age = today.get(Calendar.YEAR) - dob.get(Calendar.YEAR)
+ if (today.get(Calendar.DAY_OF_YEAR) < dob.get(Calendar.DAY_OF_YEAR)) age--
+
+ if (age < 18) {
+ ageError = "You must be at least 18 years old to register."
+ } else {
+ ageError = null
+ selectedMillis = millis
+ onDateSelected(millis)
}
}
+ showDatePickerDialog = false
}
-
- // "Add more" pill button
- Surface(
- onClick = { /* Open custom interest modal */ },
- shape = RoundedCornerShape(20.dp),
- color = Color(0xFFEFF3F8),
- modifier = Modifier.padding(horizontal = 4.dp)
- ) {
- Row(
- modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Center
- ) {
- Icon(
- imageVector = Icons.Default.AddCircle,
- contentDescription = null,
- tint = PurpleGradientEnd,
- modifier = Modifier.size(18.dp)
- )
- Spacer(modifier = Modifier.width(6.dp))
- Text(
- text = "Add more",
- fontSize = 13.sp,
- fontWeight = FontWeight.Bold,
- color = PurpleGradientEnd
- )
- }
- }
- }
-
- Spacer(modifier = Modifier.height(20.dp))
-
- // Selection Counter Row
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
) {
- Text(
- text = "Selected",
- fontSize = 12.sp,
- color = Color.Gray,
- fontWeight = FontWeight.Medium
- )
- Surface(
- shape = RoundedCornerShape(10.dp),
- color = Color(0xFFE8EAF6)
- ) {
- Text(
- text = "${selectedIds.size} selected",
- fontSize = 11.sp,
- fontWeight = FontWeight.Bold,
- color = PurpleGradientEnd,
- modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp)
- )
- }
+ Text("OK", color = textPink, fontWeight = FontWeight.Bold)
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = { showDatePickerDialog = false }) {
+ Text("Cancel", color = Color.Gray)
}
}
+ ) {
+ DatePicker(
+ state = datePickerState,
+ colors = DatePickerDefaults.colors(
+ selectedDayContainerColor = PinkGradientStart,
+ todayDateBorderColor = PurpleGradientEnd
+ )
+ )
}
}
}
-data class GenderPreferenceOption(
- val id: String,
- val title: String,
- val subtitle: String,
- val icon: ImageVector
-)
+
+
+
@Composable
-fun StepSixContent() {
- var selectedOptionId by remember { mutableStateOf("women") }
-
- val options = listOf(
- GenderPreferenceOption(
- id = "women",
- title = "Women",
- subtitle = "Female profiles",
- icon = Icons.Default.Female
- ),
- GenderPreferenceOption(
- id = "men",
- title = "Men",
- subtitle = "Male profiles",
- icon = Icons.Default.Male
- ),
- GenderPreferenceOption(
- id = "everyone",
- title = "Everyone",
- subtitle = "Open to all matches",
- icon = Icons.Default.Groups
- )
- )
+fun GenderCompose(
+ selectedGender: String = "",
+ onGenderSelected: (String) -> Unit = {}
+) {
+ val options = listOf("Men", "Women", "Everyone")
- Column(
+ Card(
modifier = Modifier.fillMaxWidth(),
- horizontalAlignment = Alignment.CenterHorizontally
+ shape = RoundedCornerShape(20.dp),
+ colors = CardDefaults.cardColors(containerColor = Color.White),
+ border = BorderStroke(1.dp, Color(0xFFE0E0E0))
) {
- // 1. Title Header Card
- Card(
- modifier = Modifier.fillMaxWidth(),
- shape = RoundedCornerShape(20.dp),
- colors = CardDefaults.cardColors(containerColor = Color.White),
- border = BorderStroke(1.dp, LightBorderColor)
+ Column(
+ modifier = Modifier.padding(16.dp),
+ horizontalAlignment = Alignment.Start
) {
- Column(
- modifier = Modifier.padding(16.dp),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text(
- text = "Who are you interested in?",
- style = MaterialTheme.typography.titleMedium.copy(
- fontWeight = FontWeight.Bold,
- fontSize = 18.sp,
- color = textDark
- )
- )
- Spacer(modifier = Modifier.height(4.dp))
- Text(
- text = "Select the gender you'd like to match with.",
- fontSize = 12.sp,
- color = Color.Gray,
- textAlign = TextAlign.Center
- )
- }
- }
+ Text(
+ text = "I AM INTERESTED IN",
+ color = Color(0xFFFF4081),
+ fontSize = 11.sp,
+ fontWeight = FontWeight.Bold
+ )
- Spacer(modifier = Modifier.height(16.dp))
+ Spacer(modifier = Modifier.height(12.dp))
- // 2. Selection Option Cards
- Column(
- verticalArrangement = Arrangement.spacedBy(12.dp)
- ) {
options.forEach { option ->
- val isSelected = selectedOptionId == option.id
+ val isSelected = selectedGender.equals(option, ignoreCase = true)
- Surface(
- onClick = { selectedOptionId = option.id },
- modifier = Modifier.fillMaxWidth(),
- shape = RoundedCornerShape(20.dp),
- color = Color.White,
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 4.dp)
+ .clickable { onGenderSelected(option) },
+ shape = RoundedCornerShape(12.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = if (isSelected) Color(0xFFFF4081).copy(alpha = 0.1f) else Color(0xFFF8F9FA)
+ ),
border = BorderStroke(
- width = if (isSelected) 2.dp else 1.dp,
- color = if (isSelected) textPink else LightBorderColor
+ width = 1.dp,
+ color = if (isSelected) Color(0xFFFF4081) else Color.Transparent
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
- verticalAlignment = Alignment.CenterVertically
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
) {
- // Icon Box
- Box(
- modifier = Modifier
- .size(48.dp)
- .background(
- color = Color(0xFFEFF3F8),
- shape = RoundedCornerShape(12.dp)
- ),
- contentAlignment = Alignment.Center
- ) {
- Icon(
- imageVector = option.icon,
- contentDescription = null,
- tint = Color(0xFF1976D2),
- modifier = Modifier.size(24.dp)
- )
- }
-
- Spacer(modifier = Modifier.width(16.dp))
-
- // Labels
- Column(modifier = Modifier.weight(1f)) {
- Text(
- text = option.title,
- fontSize = 16.sp,
- fontWeight = FontWeight.Bold,
- color = textDark
- )
- Spacer(modifier = Modifier.height(2.dp))
- Text(
- text = option.subtitle,
- fontSize = 12.sp,
- color = Color.Gray
+ Text(
+ text = option,
+ style = MaterialTheme.typography.bodyMedium.copy(
+ fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
+ color = if (isSelected) Color(0xFFFF4081) else Color(0xFF212121)
)
- }
-
- // Selection Indicator (Radio / Check)
- Box(
- modifier = Modifier
- .size(24.dp)
- .background(
- color = if (isSelected) Color.Black else Color(0xFFE8EEF5),
- shape = CircleShape
- ),
- contentAlignment = Alignment.Center
- ) {
- if (isSelected) {
- Icon(
- imageVector = Icons.Default.Check,
- contentDescription = "Selected",
- tint = Color.White,
- modifier = Modifier.size(16.dp)
- )
- }
- }
+ )
+ RadioButton(
+ selected = isSelected,
+ onClick = { onGenderSelected(option) },
+ colors = RadioButtonDefaults.colors(selectedColor = Color(0xFFFF4081))
+ )
}
}
}
@@ -1585,16 +1408,97 @@ fun StepSixContent() {
}
}
-@Preview(showBackground = true, showSystemUi = true)
@Composable
-fun RegistrationFlowContainerPreview() {
- RegistrationFlowContainer()
-}
+fun UniquePulseLoader(
+ modifier: Modifier = Modifier,
+ message: String = "Creating your account..."
+) {
+ val infiniteTransition = rememberInfiniteTransition(label = "PulseTransition")
+
+ // Animated scales for 3 staggered expanding rings
+ val scale1 by infiniteTransition.animateFloat(
+ initialValue = 0.6f, targetValue = 1.4f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(1400, easing = LinearOutSlowInEasing),
+ repeatMode = RepeatMode.Restart
+ ), label = "Ring1"
+ )
+ val alpha1 by infiniteTransition.animateFloat(
+ initialValue = 0.8f, targetValue = 0f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(1400, easing = LinearOutSlowInEasing),
+ repeatMode = RepeatMode.Restart
+ ), label = "Alpha1"
+ )
-@Preview(showBackground = true, showSystemUi = true)
-@Composable
-fun RegistrationFlowContainerPreviewss() {
- StepFiveContent()
+ val scale2 by infiniteTransition.animateFloat(
+ initialValue = 0.6f, targetValue = 1.4f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(1400, delayMillis = 450, easing = LinearOutSlowInEasing),
+ repeatMode = RepeatMode.Restart
+ ), label = "Ring2"
+ )
+ val alpha2 by infiniteTransition.animateFloat(
+ initialValue = 0.8f, targetValue = 0f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(1400, delayMillis = 450, easing = LinearOutSlowInEasing),
+ repeatMode = RepeatMode.Restart
+ ), label = "Alpha2"
+ )
+
+ Column(
+ modifier = modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Box(
+ modifier = Modifier.size(140.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ // Outer Ring 2
+ Box(
+ modifier = Modifier
+ .size(100.dp)
+ .scale(scale2)
+ .background(PurpleGradientEnd.copy(alpha = alpha2), CircleShape)
+ )
+
+ // Outer Ring 1
+ Box(
+ modifier = Modifier
+ .size(100.dp)
+ .scale(scale1)
+ .background(PinkGradientStart.copy(alpha = alpha1), CircleShape)
+ )
+
+ // Center Gradient Orb with Mascot Icon
+ Box(
+ modifier = Modifier
+ .size(80.dp)
+ .background(
+ brush = Brush.linearGradient(listOf(PinkGradientStart, PurpleGradientEnd)),
+ shape = CircleShape
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.Default.SmartToy,
+ contentDescription = null,
+ tint = Color.White,
+ modifier = Modifier.size(42.dp)
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Text(
+ text = message,
+ fontSize = 15.sp,
+ fontWeight = FontWeight.SemiBold,
+ color = PurpleGradientEnd
+ )
+ }
}
// 1. Email Tab Preview (Normal State)
@@ -1602,7 +1506,7 @@ fun RegistrationFlowContainerPreviewss() {
@Composable
fun StepTwoContentEmailPreview() {
MaterialTheme {
- StepTwoContent(
+ EmailorPhoneCompose(
selectedTab = 0,
onTabSelected = {},
emailText = "john.doe@example.com",
@@ -1622,7 +1526,7 @@ fun StepTwoContentEmailPreview() {
@Composable
fun StepTwoContentPhonePreview() {
MaterialTheme {
- StepTwoContent(
+ EmailorPhoneCompose(
selectedTab = 1,
onTabSelected = {},
emailText = "",
diff --git a/app/src/main/java/com/example/dattingapp/registration/RegistrationViewModel.kt b/app/src/main/java/com/example/dattingapp/registration/RegistrationViewModel.kt
new file mode 100644
index 0000000..112cfb9
--- /dev/null
+++ b/app/src/main/java/com/example/dattingapp/registration/RegistrationViewModel.kt
@@ -0,0 +1,103 @@
+package com.example.dattingapp.registration
+
+import android.content.Context
+import android.net.Uri
+import android.util.Log
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.example.dattingapp.registration.NetworkModule.prepareImagePart
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+
+sealed class RegistrationUiState {
+ object Idle : RegistrationUiState()
+ object Loading : RegistrationUiState()
+ data class Success(val response: RegisterResponse) : RegistrationUiState()
+ data class ImageUploadSuccess(val response: ProfileImageUploadResponse) : RegistrationUiState()
+ data class Error(val message: String) : RegistrationUiState()
+}
+
+class RegistrationViewModel : ViewModel() {
+
+ private val TAG = "RegistrationViewModel"
+
+ private val _uiState = MutableStateFlow(RegistrationUiState.Idle)
+ val uiState: StateFlow = _uiState.asStateFlow()
+
+ fun registerUser(context: Context, request: RegisterRequest) {
+ viewModelScope.launch {
+ _uiState.value = RegistrationUiState.Loading
+ try {
+ val apiService = NetworkModule.getApiService(context)
+ val response = apiService.registerUser(request)
+ if (response.isSuccessful && response.body() != null) {
+ val body = response.body()!!
+
+ TokenManager.getInstance(context.applicationContext).saveTokens(
+ accessToken = body.accessToken,
+ refreshToken = body.refreshToken
+ )
+
+ Log.d(TAG, "Registration successful. Access token saved.")
+ _uiState.value = RegistrationUiState.Success(body)
+ } else {
+ val errorMsg = response.errorBody()?.string()
+ ?: "Registration failed with code ${response.code()}"
+ Log.e(TAG, "Registration failed: $errorMsg")
+ _uiState.value = RegistrationUiState.Error(errorMsg)
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Registration exception", e)
+ _uiState.value = RegistrationUiState.Error(e.localizedMessage ?: "An unexpected error occurred")
+ }
+ }
+ }
+
+ fun uploadProfileImage(context: Context, imageUri: Uri) {
+ val tokenManager = TokenManager.getInstance(context.applicationContext)
+ val token = tokenManager.getAccessToken()
+
+ if (token.isNullOrEmpty()) {
+ Log.e(TAG, "Upload failed: Access token is missing from storage.")
+ _uiState.value = RegistrationUiState.Error("Authentication token is missing.")
+ return
+ }
+
+ viewModelScope.launch {
+ _uiState.value = RegistrationUiState.Loading
+ Log.d(TAG, "Preparing image for upload from URI: $imageUri")
+
+ val imagePart = prepareImagePart(context, imageUri, partName = "image")
+ if (imagePart == null) {
+ Log.e(TAG, "Failed to create MultipartBody.Part from Uri.")
+ _uiState.value = RegistrationUiState.Error("Failed to prepare image file for upload.")
+ return@launch
+ }
+
+ try {
+ Log.d(TAG, "Uploading image via API...")
+ val apiService = NetworkModule.getApiService(context)
+ val response = apiService.uploadProfileImage(imagePart)
+
+ if (response.isSuccessful && response.body() != null) {
+ Log.d(TAG, "Image upload successful: ${response.body()}")
+ _uiState.value = RegistrationUiState.ImageUploadSuccess(response.body()!!)
+ } else {
+ val errorMsg = response.errorBody()?.string()
+ ?: "Image upload failed with code ${response.code()}"
+ Log.e(TAG, "Image upload error [Code ${response.code()}]: $errorMsg")
+ _uiState.value = RegistrationUiState.Error(errorMsg)
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Exception during image upload", e)
+ _uiState.value = RegistrationUiState.Error(e.localizedMessage ?: "Failed to upload image")
+ }
+ }
+ }
+
+ fun resetUiState() {
+ _uiState.value = RegistrationUiState.Idle
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/dattingapp/registration/TokenManager.kt b/app/src/main/java/com/example/dattingapp/registration/TokenManager.kt
new file mode 100644
index 0000000..2bab6df
--- /dev/null
+++ b/app/src/main/java/com/example/dattingapp/registration/TokenManager.kt
@@ -0,0 +1,51 @@
+package com.example.dattingapp.registration
+
+import android.content.Context
+import android.content.SharedPreferences
+
+class TokenManager private constructor(context: Context) {
+
+ private val sharedPreferences: SharedPreferences = context.applicationContext.getSharedPreferences(
+ PREF_NAME,
+ Context.MODE_PRIVATE
+ )
+
+ fun saveTokens(accessToken: String, refreshToken: String?) {
+ sharedPreferences.edit().apply {
+ putString(KEY_ACCESS_TOKEN, accessToken)
+ putString(KEY_REFRESH_TOKEN, refreshToken)
+ apply()
+ }
+ }
+
+ fun getAccessToken(): String? {
+ return sharedPreferences.getString(KEY_ACCESS_TOKEN, null)
+ }
+
+ fun getRefreshToken(): String? {
+ return sharedPreferences.getString(KEY_REFRESH_TOKEN, null)
+ }
+
+ fun clearTokens() {
+ sharedPreferences.edit().apply {
+ remove(KEY_ACCESS_TOKEN)
+ remove(KEY_REFRESH_TOKEN)
+ apply()
+ }
+ }
+
+ companion object {
+ private const val PREF_NAME = "user_tokens"
+ private const val KEY_ACCESS_TOKEN = "access_token"
+ private const val KEY_REFRESH_TOKEN = "refresh_token"
+
+ @Volatile
+ private var INSTANCE: TokenManager? = null
+
+ fun getInstance(context: Context): TokenManager {
+ return INSTANCE ?: synchronized(this) {
+ INSTANCE ?: TokenManager(context.applicationContext).also { INSTANCE = it }
+ }
+ }
+ }
+}
\ 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
index 3d3dd42..2da99d0 100644
--- a/app/src/main/java/com/example/dattingapp/registration/UploadIntroVideoActivity.kt
+++ b/app/src/main/java/com/example/dattingapp/registration/UploadIntroVideoActivity.kt
@@ -1,10 +1,8 @@
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
@@ -20,7 +18,6 @@ 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
@@ -34,236 +31,50 @@ 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()
+ // Retrieve Profile Image URI string
val profileImageUriString = intent.getStringExtra("EXTRA_PROFILE_IMAGE_URI")
- val profileImageUri = profileImageUriString?.let { Uri.parse(it) }
-
- // =====================================================
- // COMPOSE
- // =====================================================
+ val profileImageUri: Uri? = profileImageUriString?.let { Uri.parse(it) }
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
- }
- )
+ 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()
}
- }
- }
- }
- }
-
- // ==========================================================
- // 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?,
@@ -271,985 +82,345 @@ fun UploadIntroVideoScreen(
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
- }
+ 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
}
+ }
-
- // ==========================================================
- // Camera video
- // ==========================================================
-
- val recordVideoLauncher =
- rememberLauncherForActivityResult(
- contract =
- ActivityResultContracts.CaptureVideo()
- ) { success ->
-
- if (
- success &&
- recordedVideoUri != null
- ) {
-
- displayMode =
- DisplayMode.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
- }
+ 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)
- )
- )
-
-
- // ==========================================================
- // MAIN COLUMN
- // ==========================================================
+ val buttonGradient = Brush.horizontalGradient(
+ colors = listOf(Color(0xFFB70054), Color(0xFF6700C7))
+ )
Column(
-
- modifier =
- Modifier
- .fillMaxSize()
- .background(
- Color(0xFFF9F9F9)
- )
- .systemBarsPadding()
- .imePadding()
- .padding(
- horizontal = 20.dp,
- vertical = 12.dp
- ),
-
- horizontalAlignment =
- Alignment.CenterHorizontally,
-
- verticalArrangement =
- Arrangement.SpaceBetween
+ 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
- // ======================================================
-
+ // --- Header Section ---
Row(
-
- modifier =
- Modifier.fillMaxWidth(),
-
- horizontalArrangement =
- Arrangement.SpaceBetween,
-
- verticalAlignment =
- Alignment.CenterVertically
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
) {
-
- IconButton(
- onClick =
- onBackClick
- ) {
-
- Icon(
- Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = "Back",
- tint = Color.Black
- )
+ 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
- )
+ TextButton(onClick = onSkipClick) {
+ Text(text = "SKIP", color = Color.Gray, fontWeight = FontWeight.Bold, fontSize = 12.sp)
}
}
-
- // ======================================================
- // TITLE
- // ======================================================
-
- Column(
- horizontalAlignment =
- Alignment.CenterHorizontally
- ) {
-
+ // --- Header Title ---
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
- text =
- if (
- displayMode ==
- DisplayMode.BIO
- ) {
- "Your Bio"
- } else {
- "Video Introduction"
- },
-
+ 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)
+ 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."
- },
-
+ 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
+ color = Color.Gray
)
}
-
- // ======================================================
- // MAIN CONTAINER
- // ======================================================
-
+ // --- Central Media / Text Container ---
Box(
-
- modifier =
- Modifier
- .fillMaxWidth()
- .weight(1f)
- .padding(
- vertical = 12.dp
- )
- .clip(
- RoundedCornerShape(
- 20.dp
- )
- )
- .background(
- Color.LightGray
- ),
-
- contentAlignment =
- Alignment.Center
+ 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
- ) {
-
+ if (capturedPhotoUri != null || profileImageUri != null) {
Image(
-
- painter =
- rememberAsyncImagePainter(
- model =
- capturedPhotoUri
- ?: profileImageUri
- ),
-
- contentDescription =
- "Profile Image",
-
- modifier =
- Modifier.fillMaxSize(),
-
- contentScale =
- ContentScale.Crop
+ painter = rememberAsyncImagePainter(model = capturedPhotoUri ?: profileImageUri),
+ contentDescription = "Profile Image Preview",
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.Crop
)
}
}
-
- // =================================================
- // VIDEO
- // =================================================
-
DisplayMode.VIDEO -> {
-
- recordedVideoUri?.let {
-
- VideoPlayer(
- videoUri = it
- )
+ recordedVideoUri?.let { uri ->
+ VideoPlayer(videoUri = uri)
}
}
-
- // =================================================
- // 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..."
- )
+ value = bioText,
+ onValueChange = { input ->
+ val words = input.trim().split("\\s+".toRegex())
+ if (input.isEmpty() || words.size <= 200) bioText = input
},
-
- colors =
- TextFieldDefaults.colors(
-
- unfocusedContainerColor =
- Color.White,
-
- focusedContainerColor =
- Color.White
- )
+ 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
+ )
)
}
}
-
- // =====================================================
- // MEDIA BUTTONS
- // =====================================================
-
- if (
- displayMode !=
- DisplayMode.BIO
- ) {
+ // 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
+ 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
+ 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)
+ imageVector = Icons.Default.FileUpload,
+ contentDescription = "Upload Gallery Video",
+ tint = Color(0xFF475569),
+ modifier = Modifier.size(24.dp)
)
}
-
- // =================================================
- // 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
+ 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
+ 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
- )
+ imageVector = 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
+ 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(
-
- Icons.Default.PhotoCamera,
-
- contentDescription =
- "Take Photo",
-
- tint =
- Color(0xFF475569)
+ imageVector = Icons.Default.PhotoCamera,
+ contentDescription = "Take Photo",
+ tint = Color(0xFF475569),
+ modifier = Modifier.size(24.dp)
)
}
}
}
}
-
- // ==========================================================
- // BOTTOM BUTTONS
- // ==========================================================
-
+ // --- Bottom Buttons ---
Column(
-
- modifier =
- Modifier.fillMaxWidth(),
-
- verticalArrangement =
- Arrangement.spacedBy(
- 10.dp
- )
+ 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
- )
+ 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
- )
+ 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
- }
+ 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
- )
+ 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
+ text = if (displayMode == DisplayMode.BIO) "VIDEO INTRO" else "ADD BIO",
+ color = Color(0xFFE11D48),
+ fontWeight = FontWeight.Bold,
+ fontSize = 14.sp
)
}
}
}
}
-
-// ==========================================================
-// VIDEO PLAYER
-// ==========================================================
-
+// 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
- }
+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
- ) {
-
+ DisposableEffect(Unit) {
onDispose {
-
exoPlayer.release()
}
}
-
AndroidView(
-
factory = { ctx ->
-
PlayerView(ctx).apply {
-
- player =
- exoPlayer
-
- useController =
- true
+ player = exoPlayer
+ useController = true
}
},
-
- modifier =
- Modifier.fillMaxSize()
+ modifier = Modifier.fillMaxSize()
)
}
-
-// ==========================================================
-// CREATE VIDEO URI
-// ==========================================================
-
-fun createVideoUri(
- context: Context
-): Uri {
-
- val file =
- File.createTempFile(
- "intro_video_",
- ".mp4",
- context.cacheDir
- )
-
-
+// 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",
-
- file
+ tempFile
)
}
-
-// ==========================================================
-// CREATE IMAGE URI
-// ==========================================================
-
-fun createIntroImageUri(
- context: Context
-): Uri {
-
- val file =
- File.createTempFile(
- "profile_image_",
- ".jpg",
- context.cacheDir
- )
-
-
+fun createImageUrl(context: Context): Uri {
+ val tempFile = File.createTempFile("intro_photo_", ".jpg", context.cacheDir).apply {
+ createNewFile()
+ deleteOnExit()
+ }
return FileProvider.getUriForFile(
-
context,
-
"${context.packageName}.provider",
-
- file
+ tempFile
)
}
-
-@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 Previews ---
+@Preview(
+ name = "Upload Intro Screen - Full Preview",
+ showBackground = true,
+ showSystemUi = true
+)
@Composable
-fun VideoUploadErrorScreenPreview() {
- MaterialTheme {
- VideoUploadLoadingScreen(
- errorMessage = "Network connection failed. Please try again.",
- onRetry = {}
+fun UploadIntroVideoScreenPreview() {
+ DattingAPPTheme {
+ UploadIntroVideoScreen(
+ profileImageUri = null,
+ onSubmit = { _, _ -> }
)
}
}
\ 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
index 33794d8..b7bc219 100644
--- a/app/src/main/java/com/example/dattingapp/registration/UploadProfileImageActivity.kt
+++ b/app/src/main/java/com/example/dattingapp/registration/UploadProfileImageActivity.kt
@@ -1,7 +1,9 @@
package com.example.dattingapp.registration
+import android.Manifest
import android.content.Context
import android.content.Intent
+import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.util.Log
@@ -11,6 +13,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.viewModels
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -43,79 +46,73 @@ 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.ContextCompat
import androidx.core.content.FileProvider
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.rememberAsyncImagePainter
+import com.example.dattingapp.ui.theme.backgroundGradient
+import com.example.dattingapp.ui.theme.buttonGradient
+import com.example.dattingapp.ui.theme.circleBorderGradient
import java.io.File
class UploadProfileImageActivity : ComponentActivity() {
- private val TAG = "kk"
+ private val TAG = "UploadProfileImage"
+ private val viewModel: RegistrationViewModel by viewModels()
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 uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ // Observe UI state for upload status
+ LaunchedEffect(uiState) {
+ when (uiState) {
+ is RegistrationUiState.ImageUploadSuccess -> {
+ Toast.makeText(this@UploadProfileImageActivity, "Image uploaded successfully!", Toast.LENGTH_SHORT).show()
+
+ val intent = Intent(this@UploadProfileImageActivity, UploadIntroVideoActivity::class.java)
+ startActivity(intent)
+ }
+ is RegistrationUiState.Error -> {
+ val message = (uiState as RegistrationUiState.Error).message
+ Toast.makeText(this@UploadProfileImageActivity, message, Toast.LENGTH_LONG).show()
+ viewModel.resetUiState()
}
+ else -> {}
+ }
+ }
- 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())
+ Box(modifier = Modifier.fillMaxSize()) {
+ UploadProfileImageScreen(
+ onBackClick = { finish() },
+ onContinueClick = { selectedUri ->
+ if (selectedUri == null) {
+ Toast.makeText(this@UploadProfileImageActivity, "Please select or take a photo first", Toast.LENGTH_SHORT).show()
+ } else {
+ viewModel.uploadProfileImage(
+ context = this@UploadProfileImageActivity,
+ imageUri = selectedUri
+ )
+ }
}
+ )
- startActivity(intent)
+ if (uiState is RegistrationUiState.Loading) {
+ CircularProgressIndicator(
+ modifier = Modifier.align(Alignment.Center),
+ color = Color(0xFFC2185B)
+ )
}
- )
+ }
}
}
}
}
-// 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()
@@ -135,20 +132,17 @@ fun UploadProfileImageScreen(
) {
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
+ // Gallery Launcher
val galleryLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
) { uri: Uri? ->
- if (uri != null) {
- selectedImageUri = uri
- }
+ uri?.let { selectedImageUri = it }
}
- // 2. Camera Launcher
+ // Camera Launcher
val cameraLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.TakePicture()
) { success: Boolean ->
@@ -157,30 +151,33 @@ fun UploadProfileImageScreen(
}
}
- // Design Gradients
- val backgroundGradient = Brush.verticalGradient(
- colors = listOf(
- Color(0xFFFFF5F5),
- Color(0xFFFAFAFA),
- Color(0xFFF3E8FF)
- )
- )
-
- val buttonGradient = Brush.horizontalGradient(
- colors = listOf(
- Color(0xFFC2185B),
- Color(0xFF7B1FA2)
- )
- )
+ // Camera Permission Launcher
+ val permissionLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.RequestPermission()
+ ) { isGranted ->
+ if (isGranted) {
+ val uri = createImageUri(context)
+ tempCameraUri = uri
+ cameraLauncher.launch(uri)
+ } else {
+ Toast.makeText(context, "Camera permission required to take photos", Toast.LENGTH_SHORT).show()
+ }
+ }
- val circleBorderGradient = Brush.sweepGradient(
- colors = listOf(
- Color(0xFF7B1FA2),
- Color(0xFFC2185B),
- Color(0xFFE91E63),
- Color(0xFF7B1FA2)
- )
- )
+ val launchCameraWithPermission = {
+ val hasPermission = ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.CAMERA
+ ) == PackageManager.PERMISSION_GRANTED
+
+ if (hasPermission) {
+ val uri = createImageUri(context)
+ tempCameraUri = uri
+ cameraLauncher.launch(uri)
+ } else {
+ permissionLauncher.launch(Manifest.permission.CAMERA)
+ }
+ }
Box(
modifier = Modifier
@@ -207,7 +204,7 @@ fun UploadProfileImageScreen(
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = "Back",
+ contentDescription = "Navigate back",
tint = Color.Black
)
}
@@ -273,7 +270,7 @@ fun UploadProfileImageScreen(
if (selectedImageUri != null) {
Image(
painter = rememberAsyncImagePainter(selectedImageUri),
- contentDescription = "Profile Photo",
+ contentDescription = "Selected Profile Photo",
modifier = Modifier
.fillMaxSize()
.clip(CircleShape),
@@ -282,7 +279,7 @@ fun UploadProfileImageScreen(
} else {
Icon(
imageVector = Icons.Outlined.AddAPhoto,
- contentDescription = "Camera Icon",
+ contentDescription = "Placeholder photo icon",
tint = Color(0xFFE57373),
modifier = Modifier.size(44.dp)
)
@@ -292,14 +289,14 @@ fun UploadProfileImageScreen(
Spacer(modifier = Modifier.height(36.dp))
- // Upload from gallery Button
+ // Upload from gallery Card
Card(
+ onClick = { galleryLauncher.launch("image/*") },
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFFEDE7F6)),
modifier = Modifier
.fillMaxWidth()
.height(52.dp)
- .clickable { galleryLauncher.launch("image/*") }
) {
Row(
modifier = Modifier.fillMaxSize(),
@@ -308,7 +305,7 @@ fun UploadProfileImageScreen(
) {
Icon(
imageVector = Icons.Outlined.PhotoLibrary,
- contentDescription = "Gallery",
+ contentDescription = null,
tint = Color(0xFF673AB7),
modifier = Modifier.size(20.dp)
)
@@ -324,18 +321,14 @@ fun UploadProfileImageScreen(
Spacer(modifier = Modifier.height(12.dp))
- // Take a photo Button
+ // Take a photo Card
Card(
+ onClick = launchCameraWithPermission,
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(),
@@ -344,7 +337,7 @@ fun UploadProfileImageScreen(
) {
Icon(
imageVector = Icons.Outlined.CameraAlt,
- contentDescription = "Camera",
+ contentDescription = null,
tint = Color(0xFF673AB7),
modifier = Modifier.size(20.dp)
)
@@ -393,16 +386,15 @@ fun UploadProfileImageScreen(
},
fontSize = 12.sp,
color = Color.Gray,
- modifier = Modifier.clickable { }
+ modifier = Modifier.clickable { /* Handle support click */ }
)
}
}
}
}
-// 1. Preview default state (No photo selected yet)
@Preview(
- name = "Upload Photo - Empty State",
+ name = "Upload Profile Image Screen",
showBackground = true,
showSystemUi = true
)
@@ -414,4 +406,4 @@ fun UploadProfileImageScreenPreview() {
onContinueClick = {}
)
}
-}
+}
\ No newline at end of file
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 f3a64bf..a88474e 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
@@ -63,6 +63,16 @@ val avatarGradientBorder = Brush.sweepGradient(
)
)
+val circleBorderGradient = Brush.sweepGradient(
+ colors = listOf(
+ Color(0xFF7B1FA2),
+ Color(0xFFC2185B),
+ Color(0xFFE91E63),
+ Color(0xFF7B1FA2)
+ )
+ )
+
+
// Color definitions matching the design mockup
val BackgroundGradientTop = Color(0xFF916A8C)
diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml
deleted file mode 100644
index 08f6445..0000000
--- a/app/src/main/res/xml/file_paths.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file