package com.example.dattingapp.homes import android.content.Context import android.content.Intent import android.util.Log import androidx.compose.animation.core.Animatable import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectDragGestures 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.filled.* import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.LocationOn import androidx.compose.material.icons.outlined.Search 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.shadow import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity 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.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage import coil.request.ImageRequest import com.example.dattingapp.FriendProfile 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.ui.theme.BrandPink import com.example.dattingapp.ui.theme.BrandPurple import kotlinx.coroutines.launch import java.time.LocalDate import java.time.Period import java.time.format.DateTimeFormatter import kotlin.math.abs // Direction strings sent to the backend. CHECK "superlike" matches what your API expects. private const val DIR_LIKE = "like" private const val DIR_PASS = "pass" private const val DIR_SUPER_LIKE = "superlike" @Composable fun MeetDiscoveryScreen( modifier: Modifier = Modifier, profiles: List = emptyList(), isLoading: Boolean = false, errorMessage: String? = null, onAddClick: () -> Unit = {}, onSearchClick: () -> Unit = {}, onSuperLikeClick: (MeetUserProfile) -> Unit = {}, onRejectClick: (MeetUserProfile) -> Unit = {}, onLikeClick: (MeetUserProfile) -> Unit = {}, swipeViewModel: SwipeViewModel = viewModel() ) { val context = LocalContext.current var currentIndex by remember { mutableIntStateOf(0) } Box( modifier = modifier .fillMaxSize() .background(Color(0xFFF8F9FA)) ) { Column( modifier = Modifier .fillMaxSize() .padding(horizontal = 16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { // TOP BAR Row( modifier = Modifier .fillMaxWidth() .padding(top = 12.dp, bottom = 12.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { IconButton( onClick = onAddClick, modifier = Modifier .size(38.dp) .clip(CircleShape) .background(Color(0xFFEBEBEB)) ) { Icon( imageVector = Icons.Default.Add, contentDescription = "Add", tint = BrandPink, modifier = Modifier.size(20.dp) ) } Text( text = "VibeCheck", fontSize = 24.sp, fontWeight = FontWeight.Bold, color = BrandPurple ) IconButton(onClick = onSearchClick) { Icon( imageVector = Icons.Outlined.Search, contentDescription = "Search", tint = BrandPink, modifier = Modifier.size(26.dp) ) } } // MAIN CONTENT CARD Box( modifier = Modifier .fillMaxWidth() .weight(1f) .padding(bottom = 16.dp), contentAlignment = Alignment.Center ) { when { isLoading -> { CircularProgressIndicator(color = BrandPink) } errorMessage != null -> { Text( text = errorMessage, color = MaterialTheme.colorScheme.error, fontSize = 16.sp ) } profiles.isNotEmpty() && currentIndex < profiles.size -> { val currentProfile = profiles[currentIndex] ProfileCardItem( profile = currentProfile, onSuperLike = { handleSwipeAction(context, DIR_SUPER_LIKE, currentProfile, swipeViewModel) onSuperLikeClick(currentProfile) currentIndex++ }, onReject = { handleSwipeAction(context, DIR_PASS, currentProfile, swipeViewModel) onRejectClick(currentProfile) currentIndex++ }, onLike = { handleSwipeAction(context, DIR_LIKE, currentProfile, swipeViewModel) onLikeClick(currentProfile) currentIndex++ } ) } else -> { Text( text = "No more profiles available!", fontSize = 16.sp, color = Color.Gray ) } } } } } } @Composable private fun ProfileCardItem( profile: MeetUserProfile, onSuperLike: () -> Unit, onReject: () -> Unit, onLike: () -> Unit ) { val context = LocalContext.current val scope = rememberCoroutineScope() // Drag offsets. Reset whenever the profile changes. val offsetX = remember(profile) { Animatable(0f) } val offsetY = remember(profile) { Animatable(0f) } // Distance (in px) the card must be dragged before it counts as a swipe val threshold = with(LocalDensity.current) { 120.dp.toPx() } val flyDistance = with(LocalDensity.current) { 700.dp.toPx() } val photoUrl = remember(profile) { profile.media ?.firstOrNull { it.mediaType.equals("image", ignoreCase = true) || it.url != null } ?.fullMediaUrl } val age = remember(profile.birthDate) { calculateAge(profile.birthDate) } val locationText = remember(profile) { val loc = listOfNotNull(profile.city, profile.country).joinToString(", ") val dist = profile.distanceKm?.let { "${it.toInt()} km away" } listOfNotNull(dist, loc.takeIf { it.isNotBlank() }).joinToString(" • ") } // How strongly each stamp should show (0f..1f), based on the current drag val x = offsetX.value val y = offsetY.value val verticalDominant = abs(y) > abs(x) val likeAlpha = if (!verticalDominant) (x / threshold).coerceIn(0f, 1f) else 0f val nopeAlpha = if (!verticalDominant) (-x / threshold).coerceIn(0f, 1f) else 0f val superAlpha = if (verticalDominant) (-y / threshold).coerceIn(0f, 1f) else 0f Box( modifier = Modifier .fillMaxSize() // pointerInput comes BEFORE graphicsLayer so drag deltas are not // measured in the moved/rotated coordinates (that would cause jitter) .pointerInput(profile) { detectDragGestures( onDrag = { change, dragAmount -> change.consume() scope.launch { offsetX.snapTo(offsetX.value + dragAmount.x) offsetY.snapTo(offsetY.value + dragAmount.y) } }, onDragEnd = { scope.launch { val dx = offsetX.value val dy = offsetY.value when { // UP -> super like dy < -threshold && abs(dy) > abs(dx) -> { offsetY.animateTo(-flyDistance) onSuperLike() } // RIGHT -> like dx > threshold -> { offsetX.animateTo(flyDistance) onLike() } // LEFT -> reject dx < -threshold -> { offsetX.animateTo(-flyDistance) onReject() } // Not far enough: snap back else -> { launch { offsetX.animateTo(0f) } launch { offsetY.animateTo(0f) } } } } }, onDragCancel = { scope.launch { launch { offsetX.animateTo(0f) } launch { offsetY.animateTo(0f) } } } ) } .graphicsLayer { translationX = offsetX.value translationY = offsetY.value rotationZ = offsetX.value / 40f // slight tilt while dragging sideways } .clip(RoundedCornerShape(20.dp)) ) { val token = remember(context) { TokenManager.getInstance(context).getAccessToken() } val defaultPainter = rememberVectorPainter(Icons.Default.Person) if (!photoUrl.isNullOrBlank()) { AsyncImage( model = ImageRequest.Builder(context) .data(photoUrl) .apply { if (!token.isNullOrBlank()) { val authHeader = if (token.startsWith("Bearer ", ignoreCase = true)) token else "Bearer $token" addHeader("Authorization", authHeader) } } .crossfade(true) .build(), placeholder = defaultPainter, error = defaultPainter, contentDescription = "Profile Image", modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop, onError = { errorState -> Log.e("CoilStatus", "Failed to load image", errorState.result.throwable) } ) } else { Image( painter = defaultPainter, contentDescription = "Profile Image", modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop ) } // DARK GRADIENT OVERLAY Box( modifier = Modifier .fillMaxSize() .background( Brush.verticalGradient( colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.85f)), startY = 350f ) ) ) // SWIPE STAMPS (fade in as you drag) SwipeStamp( text = "LIKE", color = Color(0xFF2ECC71), alpha = likeAlpha, modifier = Modifier .align(Alignment.TopStart) .padding(24.dp) ) SwipeStamp( text = "NOPE", color = Color(0xFFE74C3C), alpha = nopeAlpha, modifier = Modifier .align(Alignment.TopEnd) .padding(24.dp) ) SwipeStamp( text = "SUPER LIKE", color = Color(0xFF3498DB), alpha = superAlpha, modifier = Modifier .align(Alignment.Center) ) // ACTION BUTTONS (Right Side) Column( modifier = Modifier .align(Alignment.BottomEnd) .padding(end = 14.dp, bottom = 20.dp), verticalArrangement = Arrangement.spacedBy(10.dp), horizontalAlignment = Alignment.CenterHorizontally ) { // Super Like (Star) IconButton( onClick = onSuperLike, modifier = Modifier .size(50.dp) .shadow(6.dp, CircleShape) .clip(CircleShape) .background(Color.White) ) { Icon( imageVector = Icons.Default.Star, contentDescription = "Super Like", tint = BrandPurple, modifier = Modifier.size(24.dp) ) } // Reject (X) IconButton( onClick = onReject, modifier = Modifier .size(54.dp) .shadow(6.dp, CircleShape) .clip(CircleShape) .background(Color.White) ) { Icon( imageVector = Icons.Outlined.Close, contentDescription = "Reject", tint = Color(0xFF555555), modifier = Modifier.size(26.dp) ) } // Like (Heart) IconButton( onClick = onLike, modifier = Modifier .size(60.dp) .shadow(8.dp, CircleShape) .clip(CircleShape) .background(BrandPink) ) { Icon( imageVector = Icons.Default.Favorite, contentDescription = "Like", tint = Color.White, modifier = Modifier.size(30.dp) ) } } // PROFILE DETAILS (Bottom Left) Column( modifier = Modifier .align(Alignment.BottomStart) .padding(16.dp) .fillMaxWidth(0.70f) ) { Row( modifier = Modifier .fillMaxWidth() .clickable { val intent = Intent(context, FriendProfile::class.java).apply { putExtra("USER_ID", profile.id) } context.startActivity(intent) }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { val displayName = profile.fullName ?: "User" val nameAndAge = if (age != null) "$displayName, $age" else displayName Text( text = nameAndAge, fontSize = 26.sp, fontWeight = FontWeight.Bold, color = Color.White ) profile.compatibilityPercentage?.let { match -> Surface( shape = RoundedCornerShape(10.dp), color = BrandPink ) { Row( modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(2.dp) ) { Icon( imageVector = Icons.Default.Verified, contentDescription = null, tint = Color.White, modifier = Modifier.size(10.dp) ) Text( text = "$match%", color = Color.White, fontSize = 10.sp, fontWeight = FontWeight.Bold ) } } } } Spacer(modifier = Modifier.height(4.dp)) if (locationText.isNotBlank()) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp) ) { Icon( imageVector = Icons.Outlined.LocationOn, contentDescription = null, tint = Color.White.copy(alpha = 0.9f), modifier = Modifier.size(14.dp) ) Text( text = locationText, fontSize = 13.sp, color = Color.White.copy(alpha = 0.9f) ) } } profile.bio?.takeIf { it.isNotBlank() }?.let { bio -> Spacer(modifier = Modifier.height(8.dp)) Text( text = bio, fontSize = 12.sp, color = Color.White.copy(alpha = 0.85f), maxLines = 2 ) } } } } // The LIKE / NOPE / SUPER LIKE label that fades in while dragging @Composable private fun SwipeStamp( text: String, color: Color, alpha: Float, modifier: Modifier = Modifier ) { if (alpha <= 0f) return Surface( modifier = modifier.graphicsLayer { this.alpha = alpha }, color = Color.Transparent, shape = RoundedCornerShape(8.dp), border = androidx.compose.foundation.BorderStroke(3.dp, color) ) { Text( text = text, color = color, fontSize = 28.sp, fontWeight = FontWeight.ExtraBold, modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp) ) } } // Helper to calculate age from birth date string (e.g., "1999-05-15") private fun calculateAge(birthDateStr: String?): Int? { if (birthDateStr.isNullOrBlank()) return null return try { val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") val birthDate = LocalDate.parse(birthDateStr.take(10), formatter) Period.between(birthDate, LocalDate.now()).years } catch (e: Exception) { null } } // Sends the swipe (like / pass / superlike) to the server private fun handleSwipeAction( context: Context, direction: String, profile: MeetUserProfile, swipeViewModel: SwipeViewModel ) { val swipeRequest = SwipeRequest(profile.id, direction) swipeViewModel.sendSwipeResponse(context, swipeRequest) } // ========================================== // PREVIEWS & MOCK DATA // ========================================== private val mockProfiles = listOf( MeetUserProfile( id = 1, fullName = "Sophia Chen", birthDate = "1998-06-15", city = "San Francisco", country = "USA", distanceKm = 4.2, bio = "Coffee enthusiast, UI designer, and full-time dog lover 🐶✨ Looking for someone to explore new coffee shops with!", compatibilityPercentage = 92, media = null, gender = "Male", videoUrl = null ), MeetUserProfile( id = 2, fullName = "Alex Rivera", birthDate = "1995-11-03", city = "Los Angeles", country = "USA", distanceKm = 12.0, bio = "Software engineer by day, musician by night 🎸 Let's jam or talk about tech!", compatibilityPercentage = 85, media = null, gender = "Male", videoUrl = null ) ) @Preview(name = "Discovery Screen - Loaded State", showBackground = true) @Composable fun MeetDiscoveryScreenPreview() { MaterialTheme { MeetDiscoveryScreen( profiles = mockProfiles, isLoading = false, errorMessage = null, onAddClick = {}, onSearchClick = {}, onSuperLikeClick = {}, onRejectClick = {}, onLikeClick = {} ) } }