Browse Source

update meet

master
osman160128 4 weeks ago
parent
commit
5b6ce5f751
  1. 68
      app/src/main/java/com/example/dattingapp/homes/MeetScreen.kt
  2. 58
      app/src/main/java/com/example/dattingapp/homes/MyProfile.kt
  3. 7
      app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeApiService.kt
  4. 23
      app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeDataClass.kt
  5. 73
      app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeViewModelClass.kt
  6. 4
      app/src/main/java/com/example/dattingapp/registration/AuthApiService.kt

68
app/src/main/java/com/example/dattingapp/homes/MeetScreen.kt

@ -1,6 +1,8 @@
package com.example.dattingapp.homes package com.example.dattingapp.homes
import android.content.Context
import android.util.Log import android.util.Log
import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
@ -21,11 +23,15 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.request.ImageRequest import coil.request.ImageRequest
import com.example.dattingapp.homes.networkandapi.MeetUserProfile import com.example.dattingapp.homes.networkandapi.MeetUserProfile
import com.example.dattingapp.homes.networkandapi.SwipeRequest
import com.example.dattingapp.homes.networkandapi.SwipeViewModel
import com.example.dattingapp.homes.networkandapi.TokenManager import com.example.dattingapp.homes.networkandapi.TokenManager
import com.example.dattingapp.ui.theme.BrandPink import com.example.dattingapp.ui.theme.BrandPink
import com.example.dattingapp.ui.theme.BrandPurple import com.example.dattingapp.ui.theme.BrandPurple
@ -43,9 +49,10 @@ fun MeetDiscoveryScreen(
onSearchClick: () -> Unit = {}, onSearchClick: () -> Unit = {},
onSuperLikeClick: (MeetUserProfile) -> Unit = {}, onSuperLikeClick: (MeetUserProfile) -> Unit = {},
onRejectClick: (MeetUserProfile) -> Unit = {}, onRejectClick: (MeetUserProfile) -> Unit = {},
onLikeClick: (MeetUserProfile) -> Unit = {} onLikeClick: (MeetUserProfile) -> Unit = {},
swipeViewModel: SwipeViewModel = viewModel()
) { ) {
// Index tracking current active user profile (starts at 0) val context = LocalContext.current
var currentIndex by remember { mutableIntStateOf(0) } var currentIndex by remember { mutableIntStateOf(0) }
Box( Box(
@ -121,7 +128,6 @@ fun MeetDiscoveryScreen(
} }
profiles.isNotEmpty() && currentIndex < profiles.size -> { profiles.isNotEmpty() && currentIndex < profiles.size -> {
// Current profile starting at index 0
val currentProfile = profiles[currentIndex] val currentProfile = profiles[currentIndex]
ProfileCardItem( ProfileCardItem(
@ -131,10 +137,12 @@ fun MeetDiscoveryScreen(
currentIndex++ currentIndex++
}, },
onReject = { onReject = {
handleSwipeAction(context,direction="pass", currentProfile, swipeViewModel)
onRejectClick(currentProfile) onRejectClick(currentProfile)
currentIndex++ currentIndex++
}, },
onLike = { onLike = {
handleSwipeAction(context,direction="like", currentProfile, swipeViewModel)
onLikeClick(currentProfile) onLikeClick(currentProfile)
currentIndex++ currentIndex++
} }
@ -161,20 +169,22 @@ private fun ProfileCardItem(
onReject: () -> Unit, onReject: () -> Unit,
onLike: () -> Unit onLike: () -> Unit
) { ) {
// Extract primary photo URL from media list val context = LocalContext.current
val scope = rememberCoroutineScope()
// Reset drag offset whenever the profile changes
val offsetX = remember(profile) { Animatable(0f) }
val photoUrl = remember(profile) { val photoUrl = remember(profile) {
profile.media profile.media
?.firstOrNull { it.mediaType.equals("image", ignoreCase = true) || it.url != null } ?.firstOrNull { it.mediaType.equals("image", ignoreCase = true) || it.url != null }
?.fullMediaUrl ?.fullMediaUrl
} }
Log.d("photourl", "$photoUrl")
// Helper to calculate age from birth_date string (formatted e.g. "YYYY-MM-DD")
val age = remember(profile.birthDate) { val age = remember(profile.birthDate) {
calculateAge(profile.birthDate) calculateAge(profile.birthDate)
} }
// Location display formatting
val locationText = remember(profile) { val locationText = remember(profile) {
val loc = listOfNotNull(profile.city, profile.country).joinToString(", ") val loc = listOfNotNull(profile.city, profile.country).joinToString(", ")
val dist = profile.distanceKm?.let { "${it.toInt()} km away" } val dist = profile.distanceKm?.let { "${it.toInt()} km away" }
@ -186,10 +196,8 @@ private fun ProfileCardItem(
.fillMaxSize() .fillMaxSize()
.clip(RoundedCornerShape(20.dp)) .clip(RoundedCornerShape(20.dp))
) { ) {
// BACKEND PROFILE IMAGE
val context = LocalContext.current
val token = remember(context) { val token = remember(context) {
TokenManager.getInstance(context).getAccessToken() TokenManager.getInstance(context).getAccessToken()
} }
AsyncImage( AsyncImage(
@ -197,7 +205,6 @@ private fun ProfileCardItem(
.data(photoUrl) .data(photoUrl)
.apply { .apply {
if (!token.isNullOrBlank()) { if (!token.isNullOrBlank()) {
// Formatting matches "Bearer <token>" or raw token based on backend preference
val authHeader = if (token.startsWith("Bearer ", ignoreCase = true)) token else "Bearer $token" val authHeader = if (token.startsWith("Bearer ", ignoreCase = true)) token else "Bearer $token"
addHeader("Authorization", authHeader) addHeader("Authorization", authHeader)
} }
@ -295,7 +302,6 @@ private fun ProfileCardItem(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
// Name & Age
val displayName = profile.fullName ?: "User" val displayName = profile.fullName ?: "User"
val nameAndAge = if (age != null) "$displayName, $age" else displayName val nameAndAge = if (age != null) "$displayName, $age" else displayName
@ -306,7 +312,6 @@ private fun ProfileCardItem(
color = Color.White color = Color.White
) )
// Compatibility / Match Percentage Badge
profile.compatibilityPercentage?.let { match -> profile.compatibilityPercentage?.let { match ->
Surface( Surface(
shape = RoundedCornerShape(10.dp), shape = RoundedCornerShape(10.dp),
@ -336,7 +341,6 @@ private fun ProfileCardItem(
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
// Location & Distance
if (locationText.isNotBlank()) { if (locationText.isNotBlank()) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -356,7 +360,6 @@ private fun ProfileCardItem(
} }
} }
// Bio
profile.bio?.takeIf { it.isNotBlank() }?.let { bio -> profile.bio?.takeIf { it.isNotBlank() }?.let { bio ->
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(
@ -370,16 +373,43 @@ private fun ProfileCardItem(
} }
} }
// Utility to calculate age from date string (e.g. "1999-05-15") //Helper to calculate age from birth date string (e.g., "1999-05-15")
private fun calculateAge(birthDateStr: String?): Int? { private fun calculateAge(birthDateStr: String?): Int? {
if (birthDateStr.isNull_or_blank()) return null if (birthDateStr.isNullOrBlank()) return null
return try { return try {
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
val birthDate = LocalDate.parse(birthDateStr?.take(10), formatter) val birthDate = LocalDate.parse(birthDateStr.take(10), formatter)
Period.between(birthDate, LocalDate.now()).years Period.between(birthDate, LocalDate.now()).years
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
} }
private fun String?.isNull_or_blank(): Boolean = this == null || this.trim().isEmpty() // Helper method for reject swipe network call
private fun handleSwipeAction(
context: Context,
direction:String,
profile: MeetUserProfile,
swipeViewModel: SwipeViewModel
) {
val targetUserId = profile.id
val swipeRequest = SwipeRequest(targetUserId, direction)
swipeViewModel.sendSwipeResponse(context, swipeRequest)
}
@Preview(
name = "Discovery Screen - Active Card",
showBackground = true,
widthDp = 390,
heightDp = 844
)
@Composable
fun MeetDiscoveryScreenPreview() {
MaterialTheme {
MeetDiscoveryScreen(
profiles = emptyList(),
isLoading = false,
errorMessage = null
)
}
}

58
app/src/main/java/com/example/dattingapp/homes/MyProfile.kt

@ -47,7 +47,6 @@ import com.example.dattingapp.ui.theme.TextMuted
@Composable @Composable
fun MyProfileScreen( fun MyProfileScreen(
profile: MyProfileResponse?, // Change from MyProfileData? to MyProfileResponse? profile: MyProfileResponse?, // Change from MyProfileData? to MyProfileResponse?
isLoading: Boolean, isLoading: Boolean,
errorMessage: String?, errorMessage: String?,
@ -57,39 +56,25 @@ fun MyProfileScreen(
onSettingsClick: () -> Unit = {} onSettingsClick: () -> Unit = {}
) { ) {
// ======================================================== // ========================================================
// FULL SCREEN SCROLL // FULL SCREEN SCROLL
// ======================================================== // ========================================================
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
Column( Column(
modifier = Modifier.fillMaxSize().background(Color.White).verticalScroll(scrollState)) { modifier = Modifier.fillMaxSize().background(Color.White).verticalScroll(scrollState)) {
// ==================================================== // ====================================================
// HEADER // HEADER
// ==================================================== // ====================================================
Box( Box(
modifier = Modifier.fillMaxWidth().height(260.dp) modifier = Modifier.fillMaxWidth().height(260.dp)
) { ) {
// ================================================= // =================================================
// COVER IMAGE // COVER IMAGE
// ================================================= // =================================================
Image( Image(
painter = painterResource( painter = painterResource(id = R.drawable.locationpermissionbg),
id = R.drawable.locationpermissionbg
),
contentDescription = "Cover Banner", contentDescription = "Cover Banner",
modifier = Modifier.fillMaxWidth().height(200.dp), modifier = Modifier.fillMaxWidth().height(200.dp),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
@ -179,7 +164,7 @@ fun MyProfileScreen(
) { ) {
Text( Text(
text = "Alex, 28", text = profile?.profile?.fullName?:"",
fontSize = 22.sp, fontSize = 22.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = BrandMagenta color = BrandMagenta
@ -222,7 +207,7 @@ fun MyProfileScreen(
Text( Text(
text = "Adventure seeker, dog lover, and amateur chef. Let's find a vibe!", text = profile?.profile?.bio?:"",
fontSize = 13.sp, fontSize = 13.sp,
color = TextMuted, color = TextMuted,
lineHeight = 18.sp lineHeight = 18.sp
@ -230,9 +215,7 @@ fun MyProfileScreen(
} }
Spacer( Spacer(modifier = Modifier.height(12.dp))
modifier = Modifier.height(12.dp)
)
// ==================================================== // ====================================================
// PREMIUM BANNER // PREMIUM BANNER
// ==================================================== // ====================================================
@ -443,3 +426,36 @@ private fun StatCard(
// ============================================================ // ============================================================
// PREVIEW // PREVIEW
// ============================================================ // ============================================================
// 1. Success State Preview (With Mock User Profile Data)
@Preview(showBackground = true, showSystemUi = true)
@Composable
fun MyProfileScreenSuccessPreview() {
DattingAPPTheme {
// Create dummy profile response matching your backend structure
val mockProfileResponse = MyProfileResponse(
id = 1,
emailOrPhone = "alex.dev@example.com",
status = "active",
profile = com.example.dattingapp.homes.networkandapi.UserProfileDetails(
fullName = "Alex",
gender = "Male",
city = "New York",
country = "USA",
bio = "Adventure seeker, dog lover, and amateur chef. Let's find a vibe!",
profileImageUrl = "https://picsum.photos/300/300", // Sample remote URL
videoUrl = null
),
createdAt = "2026-01-01"
)
MyProfileScreen(
profile = mockProfileResponse,
isLoading = false,
errorMessage = null,
onRetry = {},
onEditClick = {},
onUpgradeClick = {},
onSettingsClick = {}
)
}
}

7
app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeApiService.kt

@ -1,7 +1,9 @@
package com.example.dattingapp.homes.networkandapi package com.example.dattingapp.homes.networkandapi
import retrofit2.Response import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query import retrofit2.http.Query
interface HomeApiService { interface HomeApiService {
@ -14,4 +16,9 @@ interface HomeApiService {
@GET("api/v1/users/me") @GET("api/v1/users/me")
suspend fun getCurrentUserProfile(): Response<MyProfileResponse> suspend fun getCurrentUserProfile(): Response<MyProfileResponse>
@POST("api/v1/meet/swipes")
suspend fun recordSwipe(
@Body request: SwipeRequest
): Response<SwipeResponse>
} }

23
app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeDataClass.kt

@ -90,4 +90,25 @@ data class MediaItem(
) { ) {
val fullMediaUrl: String? val fullMediaUrl: String?
get() = resolveMediaUrl(url) get() = resolveMediaUrl(url)
} }
//swife data
// Request payload
data class SwipeRequest(
@SerializedName("target_user_id")
val targetUserId: Int,
@SerializedName("direction")
val direction: String // "like", "dislike", "super_like"
)
data class SwipeResponse(
@SerializedName("direction")
val direction: String?,
@SerializedName("matched")
val matched: Boolean = false,
@SerializedName("match_id")
val matchId: Int? = null
)

73
app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeViewModelClass.kt

@ -93,6 +93,8 @@ class MeetViewModel : ViewModel() {
// Myprofle ViewModel---------------------------------------------------------------------------------------
sealed interface MyProfileUiState { sealed interface MyProfileUiState {
object Idle : MyProfileUiState object Idle : MyProfileUiState
object Loading : MyProfileUiState object Loading : MyProfileUiState
@ -152,4 +154,73 @@ class MyProfileViewModel : ViewModel() {
} }
} }
} }
} }
// Swife ViewModel---------------------------------------------------------------------------------------
sealed interface SwipeUiState {
object Idle : SwipeUiState
object Loading : SwipeUiState
data class Success(val swipedata: SwipeResponse) : SwipeUiState // Ensure this is MyProfileData, not MyProfileResponse
data class Error(val message: String) : SwipeUiState
}
class SwipeViewModel : ViewModel() {
private val _swipeUiState = MutableStateFlow<SwipeUiState>(SwipeUiState.Idle)
val swipeUiState: StateFlow<SwipeUiState> = _swipeUiState.asStateFlow()
fun sendSwipeResponse(context: Context,swipeRequest: SwipeRequest) {
if (_swipeUiState.value is SwipeUiState.Loading) return
val appContext = context.applicationContext
viewModelScope.launch {
_swipeUiState.value = SwipeUiState.Loading
if (TokenManager.getInstance(appContext).getAccessToken().isNullOrBlank()) {
Log.e("MeetApi", "No access token in user_tokens preferences")
_swipeUiState.value = SwipeUiState.Error("No access token found. Please log in again.")
return@launch
}
try {
val response = HomeNetworkModule
.getApiService(appContext)
.recordSwipe(swipeRequest)
val statusCode = response.code()
Log.d("swipe code", "Response Code: $statusCode")
if (!response.isSuccessful) {
response.errorBody()?.close()
val message = when (response.code()) {
401 -> "Authentication failed. Please log in again."
403 -> "The server refused access to these profiles."
404 -> "Meet endpoint was not found."
else -> "Request failed: HTTP ${response.code()}."
}
_swipeUiState.value = SwipeUiState.Error(message)
return@launch
}
// FIX: Use response.body() directly as profileData
val swipeData = response.body()
if ( swipeData!= null) {
_swipeUiState.value = SwipeUiState.Success(swipeData)
} else {
_swipeUiState.value = SwipeUiState.Error("Profile data is empty.")
}
} catch (e: Exception) {
Log.e("MeetApi", "Unexpected request failure", e)
_swipeUiState.value = SwipeUiState.Error("Request failed: ${e.javaClass.simpleName}")
}
}
}
}

4
app/src/main/java/com/example/dattingapp/registration/AuthApiService.kt

@ -9,7 +9,9 @@ import retrofit2.http.Part
interface AuthApiService { interface AuthApiService {
@POST("/api/v1/auth/register") @POST("/api/v1/auth/register")
suspend fun registerUser(@Body request: RegisterRequest): Response<RegisterResponse> suspend fun registerUser(
@Body request: RegisterRequest
): Response<RegisterResponse>
@Multipart @Multipart
@POST("api/v1/users/me/image") @POST("api/v1/users/me/image")

Loading…
Cancel
Save