Browse Source

10/20/2026

master
osman160128 1 week ago
parent
commit
71829bc4f2
  1. 12
      app/src/main/AndroidManifest.xml
  2. 0
      app/src/main/java/com/example/dattingapp/EditMyProfileActivty.kt
  3. 677
      app/src/main/java/com/example/dattingapp/FriendProfile.kt
  4. 90
      app/src/main/java/com/example/dattingapp/LikeANdMatchActivity.kt
  5. 100
      app/src/main/java/com/example/dattingapp/ShowPostingActivity.kt
  6. 959
      app/src/main/java/com/example/dattingapp/homes/MyProfile.kt
  7. 22
      app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeApiService.kt
  8. 197
      app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeViewModelClass.kt
  9. 14
      app/src/main/java/com/example/dattingapp/homes/profile/UserResponseData.kt
  10. 11
      app/src/main/java/com/example/dattingapp/homes/profile/ui/theme/Color.kt
  11. 58
      app/src/main/java/com/example/dattingapp/homes/profile/ui/theme/Theme.kt
  12. 34
      app/src/main/java/com/example/dattingapp/homes/profile/ui/theme/Type.kt
  13. 2
      app/src/main/java/com/example/dattingapp/registration/AuthApiService.kt
  14. 2
      app/src/main/res/values/strings.xml

12
app/src/main/AndroidManifest.xml

@ -145,6 +145,18 @@
android:label="@string/title_activity_matches" android:label="@string/title_activity_matches"
android:theme="@style/Theme.DattingAPP" android:theme="@style/Theme.DattingAPP"
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />
<activity
android:name=".homes.profile.EditProfileActivity"
android:exported="false"
android:label="@string/title_activity_edit_profile"
android:theme="@style/Theme.DattingAPP"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".EditProfileActivty"
android:exported="false"
android:label="@string/title_activity_edit_profile_activty"
android:theme="@style/Theme.DattingAPP"
android:windowSoftInputMode="adjustResize" />
</application> </application>
</manifest> </manifest>

0
app/src/main/java/com/example/dattingapp/EditMyProfileActivty.kt

677
app/src/main/java/com/example/dattingapp/FriendProfile.kt

@ -7,6 +7,11 @@ import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
@ -18,17 +23,15 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.TextSnippet import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.ChatBubble import androidx.compose.material.icons.filled.ChatBubble
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FlashOn import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.Mood import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material.icons.filled.MoreHoriz import androidx.compose.material.icons.filled.MoreHoriz
import androidx.compose.material.icons.filled.MusicNote import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.filled.TextSnippet
import androidx.compose.material.icons.outlined.LocationOn
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
@ -48,18 +51,12 @@ import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.example.dattingapp.homes.networkandapi.* import com.example.dattingapp.homes.networkandapi.*
import com.example.dattingapp.homes.profile.FrienPostMedia
import com.example.dattingapp.homes.profile.FriendAuthor
import com.example.dattingapp.homes.profile.FriendPost import com.example.dattingapp.homes.profile.FriendPost
import com.example.dattingapp.homes.profile.FriendProfileResponse import com.example.dattingapp.homes.profile.FriendProfileResponse
import com.example.dattingapp.homes.profile.LikedUserResponse
import com.example.dattingapp.ui.theme.* import com.example.dattingapp.ui.theme.*
import java.time.LocalDate import java.time.LocalDate
import java.time.Period import java.time.Period
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import kotlin.jvm.java
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString
class FriendProfile : ComponentActivity() { class FriendProfile : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -126,9 +123,9 @@ fun FriendProfileScreen(
val matchesViewModel: MatchesViewModel = viewModel() val matchesViewModel: MatchesViewModel = viewModel()
// Collect response states // Collect response states
val likesState by likedViewModel.likesUiState.collectAsState() // Users who liked me val likesState by likedViewModel.likesUiState.collectAsState()
val likedState by likedViewModel.likedUiState.collectAsState() // Users I liked val likedState by likedViewModel.likedUiState.collectAsState()
val matchesState by matchesViewModel.state.collectAsState() // Match list from server val matchesState by matchesViewModel.state.collectAsState()
// Fetch lists once // Fetch lists once
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@ -154,16 +151,12 @@ fun FriendProfileScreen(
// Track local tap in current screen session // Track local tap in current screen session
var localLiked by rememberSaveable { mutableStateOf(false) } var localLiked by rememberSaveable { mutableStateOf(false) }
// ========================================================================= // Toggle state for extra profile details (hidden by default, persists across state changes)
var showExtraInfo by rememberSaveable { mutableStateOf(false) }
// 2-STEP MATCH EVALUATION LOGIC // 2-STEP MATCH EVALUATION LOGIC
// =========================================================================
// STEP 1: Check online match list first
val isOnlineMatch = friendProfileData.id in matchIds val isOnlineMatch = friendProfileData.id in matchIds
// STEP 2: If NOT an online match, check if he liked me + local like action
val isLocalMatch = !isOnlineMatch && heLikedMe && localLiked val isLocalMatch = !isOnlineMatch && heLikedMe && localLiked
// Final match decision
val isMatched = isOnlineMatch || isLocalMatch val isMatched = isOnlineMatch || isLocalMatch
// Derived button flags // Derived button flags
@ -175,176 +168,174 @@ fun FriendProfileScreen(
calculateAge(friendProfileData.birth_date) calculateAge(friendProfileData.birth_date)
} }
val scrollState = rememberScrollState()
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(Color.White) .background(Color.White)
.verticalScroll(rememberScrollState()) .verticalScroll(scrollState)
.padding(16.dp)
) { ) {
// Header Cover & Profile Avatar // ====================================================
Box( // 1. HEADER: Active Now & Action Icons
modifier = Modifier // ====================================================
.fillMaxWidth()
.height(250.dp)
) {
Image(
painter = painterResource(id = R.drawable.gallarypermissionbg),
contentDescription = "cover photo",
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.clip(RoundedCornerShape(bottomStart = 10.dp, bottomEnd = 20.dp)),
contentScale = ContentScale.Crop
)
Row( Row(
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 25.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Surface(
shape = RoundedCornerShape(16.dp),
color = Color(0xFFF2EBF9)
) {
Row(
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Box(
modifier = Modifier
.size(8.dp)
.clip(CircleShape)
.background(BrandMagenta)
)
Text(
text = "Active now",
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
color = BrandMagenta
)
}
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
IconButton( IconButton(
onClick = onBackClick, onClick = onBackClick,
modifier = Modifier.size(20.dp).clip(CircleShape) modifier = Modifier
.size(36.dp)
.clip(CircleShape)
.background(Color(0xFFF7F7F7))
) { ) {
Icon( Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack, imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "back button" contentDescription = "Back",
tint = textDark,
modifier = Modifier.size(18.dp)
) )
} }
IconButton( IconButton(
onClick = onMoreClick, onClick = onMoreClick,
modifier = Modifier.size(20.dp).clip(CircleShape) modifier = Modifier
.size(36.dp)
.clip(CircleShape)
.background(Color(0xFFF7F7F7))
) { ) {
Icon( Icon(
imageVector = Icons.Default.MoreHoriz, imageVector = Icons.Default.MoreHoriz,
contentDescription = "more options" contentDescription = "More Options",
tint = textDark,
modifier = Modifier.size(18.dp)
) )
} }
} }
}
Spacer(modifier = Modifier.height(16.dp))
// ====================================================
// 2. PROFILE IDENTITY
// ====================================================
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Box(
modifier = Modifier
.size(84.dp)
.clip(CircleShape)
.background(brush = Brush.linearGradient(colors = listOf(BrandMagenta, BrandPurple)))
.padding(3.dp)
) {
AsyncImage( AsyncImage(
model = friendProfileData.profile_image_url, model = friendProfileData.profile_image_url ?: "https://picsum.photos/300/300",
contentDescription = "Profile Avatar", contentDescription = "Profile Picture",
contentScale = ContentScale.Crop,
placeholder = painterResource(id = R.drawable.locationpermissionbg), placeholder = painterResource(id = R.drawable.locationpermissionbg),
error = painterResource(id = R.drawable.locationpermissionbg), error = painterResource(id = R.drawable.locationpermissionbg),
modifier = Modifier modifier = Modifier
.padding(start = 20.dp) .fillMaxSize()
.align(Alignment.BottomStart)
.size(100.dp)
.clip(CircleShape) .clip(CircleShape)
.border(4.dp, Color.White, CircleShape),
contentScale = ContentScale.Crop
) )
} }
Spacer(modifier = Modifier.height(12.dp)) Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
// Profile Main Details
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(6.dp)
) { ) {
val displayName = friendProfileData.full_name ?: "User" val displayName = friendProfileData.full_name ?: "Alex Rivera"
val nameAndAge = if (age != null) "$displayName, $age" else displayName
Text( Text(
text = nameAndAge, text = displayName,
fontSize = 24.sp, fontSize = 20.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = textDark color = textDark
) )
Surface( if (age != null) {
shape = RoundedCornerShape(12.dp),
color = BadgePink
) {
Row(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Icon(
imageVector = Icons.Default.Favorite,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(10.dp)
)
Text( Text(
text = "${friendProfileData.county ?: 0}% Match", text = "$age",
color = Color.White, fontSize = 20.sp,
fontSize = 10.sp, fontWeight = FontWeight.Normal,
fontWeight = FontWeight.Bold color = textDark
) )
} }
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.padding(vertical = 4.dp)
) {
Icon( Icon(
imageVector = Icons.Outlined.LocationOn, imageVector = Icons.Default.Check,
contentDescription = null, contentDescription = "Verified",
tint = Color.Gray, tint = BrandMagenta,
modifier = Modifier.size(14.dp) modifier = Modifier.size(16.dp)
)
Text(
text = "${friendProfileData.city ?: "Unknown"}, ${friendProfileData.country ?: ""}",
fontSize = 12.sp,
color = Color.Gray
) )
} }
Surface( val city = friendProfileData.city ?: "New York"
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
shape = RoundedCornerShape(12.dp),
color = Color(0xFFF9F9F9),
shadowElevation = 1.dp
) {
Text( Text(
text = friendProfileData.bio ?: "", text = "📍 $city, NY • 2 miles away",
fontSize = 13.sp, fontSize = 12.sp,
color = Color(0xFF4A4A4A), color = BrandPurple,
modifier = Modifier.padding(12.dp), fontWeight = FontWeight.SemiBold
maxLines = 2,
lineHeight = 18.sp
) )
} }
}
// ========================================================================= Spacer(modifier = Modifier.height(16.dp))
// ACTION BUTTON ROW WITH EVALUATED MATCH STATES
// ========================================================================= // ====================================================
// 3. ACTION BUTTON ROW (Message, Like, Super Like, Dislike)
// ====================================================
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 8.dp), .padding(vertical = 4.dp),
horizontalArrangement = Arrangement.spacedBy(10.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// 💬 Message Button — Enabled ONLY when matched // 💬 Message Button
Card( Card(
onClick = onMessageClick, onClick = onMessageClick,
enabled = isMessageClickable, enabled = isMessageClickable,
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.height(48.dp), .height(48.dp),
shape = RoundedCornerShape(10.dp), shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = if (isMessageClickable) Color.White else Color(0xFFF0F0F0), containerColor = if (isMessageClickable) Color.White else Color(0xFFF0F0F0),
disabledContainerColor = Color(0xFFF0F0F0) disabledContainerColor = Color(0xFFF0F0F0)
), ),
elevation = CardDefaults.cardElevation( elevation = CardDefaults.cardElevation(
defaultElevation = if (isMessageClickable) 4.dp else 0.dp defaultElevation = if (isMessageClickable) 2.dp else 0.dp
) ),
border = CardDefaults.outlinedCardBorder().copy(width = 0.5.dp)
) { ) {
Row( Row(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@ -355,12 +346,12 @@ fun FriendProfileScreen(
imageVector = Icons.Default.ChatBubble, imageVector = Icons.Default.ChatBubble,
contentDescription = "Message", contentDescription = "Message",
tint = if (isMessageClickable) textDark else Color(0xFFBDBDBD), tint = if (isMessageClickable) textDark else Color(0xFFBDBDBD),
modifier = Modifier.size(18.dp) modifier = Modifier.size(16.dp)
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(6.dp))
Text( Text(
text = "Message", text = "Message",
fontSize = 16.sp, fontSize = 14.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = if (isMessageClickable) textDark else Color(0xFFBDBDBD) color = if (isMessageClickable) textDark else Color(0xFFBDBDBD)
) )
@ -369,7 +360,6 @@ fun FriendProfileScreen(
// ❤️ Like & ⭐ Super Like Buttons — Hidden when matched // ❤️ Like & ⭐ Super Like Buttons — Hidden when matched
if (showLikeAndSuperLike) { if (showLikeAndSuperLike) {
// ❤️ Like Button
Card( Card(
onClick = { onClick = {
onLikeClick() onLikeClick()
@ -379,14 +369,15 @@ fun FriendProfileScreen(
}, },
enabled = !likeDisabled, enabled = !likeDisabled,
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
shape = RoundedCornerShape(10.dp), shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = if (heLikedMe) Color(0xFFE53935) else Color.White, containerColor = if (heLikedMe) Color(0xFFE53935) else Color.White,
disabledContainerColor = if (heLikedMe) Color(0xFFE53935) else Color(0xFFF0F0F0) disabledContainerColor = if (heLikedMe) Color(0xFFE53935) else Color(0xFFF0F0F0)
), ),
elevation = CardDefaults.cardElevation( elevation = CardDefaults.cardElevation(
defaultElevation = if (likeDisabled) 0.dp else 2.dp defaultElevation = if (likeDisabled) 0.dp else 2.dp
) ),
border = CardDefaults.outlinedCardBorder().copy(width = 0.5.dp)
) { ) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Icon( Icon(
@ -402,7 +393,6 @@ fun FriendProfileScreen(
} }
} }
// ⭐ Super Like (Star) Button
Card( Card(
onClick = { onClick = {
onStarClick() onStarClick()
@ -412,14 +402,15 @@ fun FriendProfileScreen(
}, },
enabled = !likeDisabled, enabled = !likeDisabled,
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
shape = RoundedCornerShape(10.dp), shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = Color.White, containerColor = Color.White,
disabledContainerColor = Color(0xFFF0F0F0) disabledContainerColor = Color(0xFFF0F0F0)
), ),
elevation = CardDefaults.cardElevation( elevation = CardDefaults.cardElevation(
defaultElevation = if (likeDisabled) 0.dp else 2.dp defaultElevation = if (likeDisabled) 0.dp else 2.dp
) ),
border = CardDefaults.outlinedCardBorder().copy(width = 0.5.dp)
) { ) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Icon( Icon(
@ -432,21 +423,21 @@ fun FriendProfileScreen(
} }
} }
// ❌ Cancel / Dislike Button — Resets everything back to initial state // ❌ Dislike / Cancel Button
Card( Card(
onClick = { onClick = {
onCancelClick() onCancelClick()
handleLikeAndDislikeAction(context, "pass", friendProfileData, swipeViewModel) handleLikeAndDislikeAction(context, "pass", friendProfileData, swipeViewModel)
localLiked = false localLiked = false
likedViewModel.likedResponse(context, "Likes") likedViewModel.likedResponse(context, "Likes")
likedViewModel.likedResponse(context, "Liked") likedViewModel.likedResponse(context, "Liked")
matchesViewModel.loadMatches(context) matchesViewModel.loadMatches(context)
}, },
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
shape = RoundedCornerShape(10.dp), shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = Color.White), colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
border = CardDefaults.outlinedCardBorder().copy(width = 0.5.dp)
) { ) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Icon( Icon(
@ -459,20 +450,359 @@ fun FriendProfileScreen(
} }
} }
Spacer(modifier = Modifier.height(12.dp))
// ====================================================
// 4. LOOKING FOR CARD (ALWAYS VISIBLE)
// ====================================================
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(14.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFFF7F4F9)),
border = CardDefaults.outlinedCardBorder().copy(width = 0.5.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(14.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
Box(
modifier = Modifier
.size(32.dp)
.clip(CircleShape)
.background(BrandMagenta.copy(alpha = 0.15f)),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.Favorite,
contentDescription = null,
tint = BrandMagenta,
modifier = Modifier.size(16.dp)
)
}
Column {
Text(
text = "LOOKING FOR",
fontSize = 10.sp,
fontWeight = FontWeight.Bold,
color = TextMuted
)
Text(
text = "Long-term relationship / Serious dating",
fontSize = 13.sp,
fontWeight = FontWeight.Bold,
color = textDark
)
}
}
Text(text = "›", fontSize = 18.sp, color = TextMuted)
}
}
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
// User Posts Section // ====================================================
// 5. VIDEO INTRO (ALWAYS VISIBLE)
// ====================================================
Column(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null, tint = BrandMagenta, modifier = Modifier.size(18.dp))
Text(text = "Video Intro", fontSize = 15.sp, fontWeight = FontWeight.Bold, color = textDark)
}
Surface(
shape = RoundedCornerShape(12.dp),
color = Color(0xFFF2EBF9)
) {
Text(
text = "Say hello! 📹",
fontSize = 11.sp,
color = BrandMagenta,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
)
}
}
Spacer(modifier = Modifier.height(8.dp))
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.clip(RoundedCornerShape(16.dp))
.background(Color.Black)
) {
AsyncImage(
model = "https://picsum.photos/600/300",
contentDescription = "Video Intro",
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(Color.White.copy(alpha = 0.9f))
.align(Alignment.Center),
contentAlignment = Alignment.Center
) {
Icon(imageVector = Icons.Default.PlayArrow, contentDescription = "Play", tint = BrandMagenta, modifier = Modifier.size(28.dp))
}
Surface(
color = Color.Black.copy(alpha = 0.6f),
shape = RoundedCornerShape(8.dp),
modifier = Modifier
.align(Alignment.BottomStart)
.padding(10.dp)
) {
Text(
text = "0:45s \"Quick vibe & Sunday routine\"",
color = Color.White,
fontSize = 10.sp,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// ====================================================
// 6. TOGGLE BUTTON FOR EXTRA PROFILE DETAILS
// ====================================================
Surface(
modifier = Modifier
.fillMaxWidth()
.clickable { showExtraInfo = !showExtraInfo },
shape = RoundedCornerShape(12.dp),
color = Color(0xFFF2EBF9)
) {
Row(
modifier = Modifier.padding(vertical = 12.dp, horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = if (showExtraInfo) "Hide Extra Profile Details" else "View All Profile Details",
fontSize = 13.sp,
fontWeight = FontWeight.Bold,
color = BrandMagenta
)
Icon(
imageVector = if (showExtraInfo) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
contentDescription = "Toggle Details",
tint = BrandMagenta
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// ====================================================
// 7. EXTRA PROFILE DETAILS (COLLAPSIBLE / HIDDEN BY DEFAULT)
// ====================================================
AnimatedVisibility(
visible = showExtraInfo,
enter = expandVertically() + fadeIn(),
exit = shrinkVertically() + fadeOut()
) {
Column(modifier = Modifier.fillMaxWidth()) {
// About Section
Text(
text = "About ${friendProfileData.full_name?.substringBefore(" ") ?: "Alex"}",
fontSize = 15.sp,
fontWeight = FontWeight.Bold,
color = textDark
)
Spacer(modifier = Modifier.height(6.dp))
Text(
text = friendProfileData.bio ?: "Adventure seeker, dog lover, and amateur chef. Always hunting for the crispiest croissants in Brooklyn.",
fontSize = 13.sp,
color = Color(0xFF555555),
lineHeight = 18.sp
)
Spacer(modifier = Modifier.height(16.dp))
// Dating Prompt Card
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFFF9F7FB)),
border = CardDefaults.outlinedCardBorder().copy(width = 0.5.dp)
) {
Column(modifier = Modifier.padding(14.dp)) {
Text(text = "DATING PROMPT", fontSize = 9.sp, fontWeight = FontWeight.Bold, color = BrandMagenta)
Spacer(modifier = Modifier.height(4.dp))
Text(text = "My Ideal weekend is...", fontSize = 15.sp, fontWeight = FontWeight.Bold, color = textDark)
Spacer(modifier = Modifier.height(6.dp))
Text(text = "Hunting down the crispiest croissants in Brooklyn, visiting a modern art exhibit, and sunset walks by the pier.", fontSize = 12.sp, color = Color(0xFF555555), lineHeight = 16.sp)
}
}
Spacer(modifier = Modifier.height(16.dp))
// Interests & Passions
Column(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Interests & Passions",
fontSize = 15.sp,
fontWeight = FontWeight.Bold,
color = textDark
)
Text(
text = "${friendProfileData.county ?: 8} Shared",
fontSize = 11.sp,
fontWeight = FontWeight.Bold,
color = BrandMagenta
)
}
Spacer(modifier = Modifier.height(8.dp))
val interestsList = listOf("📷 Photography", "🐶 Dogs", "☕ Coffee Culture", "✈️ Travel", "🎨 Art & Design", "🍷 Wine Tasting")
OptFlowRow(interestsList)
}
Spacer(modifier = Modifier.height(16.dp))
// Key Information
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Text(
text = "Key Information",
fontSize = 15.sp,
fontWeight = FontWeight.Bold,
color = textDark
)
InfoRow(icon = "💼", title = "Occupation", value = "Creative Director at Studio Drift")
InfoRow(icon = "🎓", title = "Education", value = "NYU Gallatin School")
InfoRow(icon = "📏", title = "Height", value = "5'9\" (175 cm)")
InfoRow(icon = "🏛️", title = "Beliefs", value = "Spiritual / Agnostic")
}
Spacer(modifier = Modifier.height(16.dp))
// Lifestyle Details
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = "Lifestyle Details",
fontSize = 15.sp,
fontWeight = FontWeight.Bold,
color = textDark
)
Spacer(modifier = Modifier.height(8.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
LifestyleTile(icon = "🚭", label = "Smoking", detail = "Non-smoker", modifier = Modifier.weight(1f))
LifestyleTile(icon = "🍷", label = "Drinking", detail = "Socially", modifier = Modifier.weight(1f))
}
}
Spacer(modifier = Modifier.height(20.dp))
}
}
// ====================================================
// 8. USER POSTS / MASONRY GRID SECTION (ALWAYS VISIBLE)
// ====================================================
Text( Text(
text = "Posts", text = "Posts",
fontSize = 18.sp, fontSize = 16.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = textDark, color = textDark,
modifier = Modifier.padding(bottom = 8.dp) modifier = Modifier.padding(bottom = 8.dp)
) )
Spacer(modifier = Modifier.height(16.dp))
StaggeredFriendPostsGrid(posts = friendProfileData.posts) StaggeredFriendPostsGrid(posts = friendProfileData.posts)
Spacer(modifier = Modifier.height(30.dp))
}
}
// ============================================================
// HELPER COMPOSABLES
// ============================================================
@Composable
private fun InfoRow(icon: String, title: String, value: String) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Box(
modifier = Modifier
.size(32.dp)
.clip(CircleShape)
.background(Color(0xFFF5F5F5)),
contentAlignment = Alignment.Center
) {
Text(text = icon, fontSize = 14.sp)
}
Column {
Text(text = title, fontSize = 10.sp, color = TextMuted)
Text(text = value, fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = textDark)
}
}
}
@Composable
private fun LifestyleTile(icon: String, label: String, detail: String, modifier: Modifier = Modifier) {
Surface(
modifier = modifier,
shape = RoundedCornerShape(12.dp),
color = Color(0xFFF9F9F9)
) {
Row(
modifier = Modifier.padding(10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(text = icon, fontSize = 16.sp)
Column {
Text(text = label, fontSize = 9.sp, color = TextMuted)
Text(text = detail, fontSize = 11.sp, fontWeight = FontWeight.Bold, color = textDark)
}
}
}
}
@Composable
private fun OptFlowRow(items: List<String>) {
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
val chunked = items.chunked(3)
chunked.forEach { rowItems ->
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
rowItems.forEach { item ->
Surface(
shape = RoundedCornerShape(16.dp),
color = Color(0xFFF4F4F4)
) {
Text(
text = item,
fontSize = 11.sp,
fontWeight = FontWeight.Medium,
color = textDark,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp)
)
}
}
}
} }
} }
} }
@ -495,15 +825,15 @@ fun StaggeredFriendPostsGrid(posts: List<FriendPost>) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(3.dp) horizontalArrangement = Arrangement.spacedBy(6.dp)
) { ) {
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) { Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(6.dp)) {
columns.first.forEachIndexed { idx, post -> columns.first.forEachIndexed { idx, post ->
val aspectRatio = if (idx % 2 == 0) 0.8f else 1.2f val aspectRatio = if (idx % 2 == 0) 0.8f else 1.2f
MasonryPostItem(post = post, aspectRatio = aspectRatio) MasonryPostItem(post = post, aspectRatio = aspectRatio)
} }
} }
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) { Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(6.dp)) {
columns.second.forEachIndexed { idx, post -> columns.second.forEachIndexed { idx, post ->
val aspectRatio = if (idx % 2 == 0) 1.2f else 0.8f val aspectRatio = if (idx % 2 == 0) 1.2f else 0.8f
MasonryPostItem(post = post, aspectRatio = aspectRatio) MasonryPostItem(post = post, aspectRatio = aspectRatio)
@ -521,6 +851,7 @@ fun MasonryPostItem(post: FriendPost, aspectRatio: Float) {
Card( Card(
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
border = CardDefaults.outlinedCardBorder().copy(width = 0.5.dp),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.aspectRatio(aspectRatio) .aspectRatio(aspectRatio)
@ -556,28 +887,23 @@ fun MasonryPostItem(post: FriendPost, aspectRatio: Float) {
Column( Column(
modifier = Modifier modifier = Modifier
.align(Alignment.BottomStart) .align(Alignment.BottomStart)
.padding(2.dp), .padding(8.dp),
verticalArrangement = Arrangement.spacedBy(2.dp) verticalArrangement = Arrangement.spacedBy(2.dp)
) { ) {
if (!post.feeling.isNullOrBlank()) { if (!post.feeling.isNullOrBlank()) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Text( Text(
text = post.feeling, text = post.feeling,
color = Color.White.copy(alpha = 0.9f), color = Color.White.copy(alpha = 0.9f),
fontSize = 8.sp, fontSize = 9.sp,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
} }
}
if (!post.content.isNullOrBlank()) { if (!post.content.isNullOrBlank()) {
Text( Text(
text = post.content, text = post.content,
color = Color.White, color = Color.White,
fontSize = 10.sp, fontSize = 11.sp,
maxLines = 1, maxLines = 2,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
} }
@ -586,55 +912,21 @@ fun MasonryPostItem(post: FriendPost, aspectRatio: Float) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(BrandPurple.copy(alpha = 0.8f))
.padding(10.dp), .padding(10.dp),
verticalArrangement = Arrangement.SpaceBetween verticalArrangement = Arrangement.SpaceBetween
) { ) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(3.dp)
) {
Icon(
imageVector = when (post.post_type.lowercase()) {
"music" -> Icons.Default.MusicNote
else -> Icons.Default.TextSnippet
},
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(10.dp)
)
Text( Text(
text = post.post_type.uppercase(), text = post.post_type.uppercase(),
color = Color.White.copy(alpha = 0.9f), color = Color.White.copy(alpha = 0.9f),
fontSize = 7.sp, fontSize = 8.sp,
fontWeight = FontWeight.ExtraBold fontWeight = FontWeight.ExtraBold
) )
}
}
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
if (!post.feeling.isNullOrBlank()) { if (!post.content.isNullOrBlank()) {
Surface(
shape = RoundedCornerShape(6.dp),
color = Color.White.copy(alpha = 0.2f)
) {
Text(
text = "feeling ${post.feeling}",
color = Color.White,
fontSize = 7.sp,
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
Text( Text(
text = post.content ?: "", text = post.content,
color = Color.White, color = Color.White,
fontSize = 11.sp, fontSize = 11.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
@ -643,6 +935,7 @@ fun MasonryPostItem(post: FriendPost, aspectRatio: Float) {
lineHeight = 14.sp lineHeight = 14.sp
) )
} }
}
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,

90
app/src/main/java/com/example/dattingapp/LikeANdMatchActivity.kt

@ -13,14 +13,16 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.LocationOn import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -35,6 +37,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview 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.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.example.dattingapp.homes.networkandapi.LikedUiState import com.example.dattingapp.homes.networkandapi.LikedUiState
@ -62,7 +66,8 @@ class LikeANdMatchActivity : ComponentActivity() {
else -> likedViewModel.matchesUiState.collectAsState() else -> likedViewModel.matchesUiState.collectAsState()
} }
LaunchedEffect(Unit) { // Runs on first open AND every time the user comes back to this screen
LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
likedViewModel.likedResponse(context, type) likedViewModel.likedResponse(context, type)
} }
@ -106,14 +111,19 @@ class LikeANdMatchActivity : ComponentActivity() {
is LikedUiState.Idle, is LikedUiState.Loading -> { is LikedUiState.Idle, is LikedUiState.Loading -> {
CircularProgressIndicator() CircularProgressIndicator()
} }
is LikedUiState.Success -> { is LikedUiState.Success -> {
ProfileGrid( ProfileGrid(
context = context, context = context,
likedResponse = state.LikeData, likedResponse = state.LikeData,
isLoading = false, isLoading = false,
errorMessage = null errorMessage = null,
onSwipe = { profile, direction ->
likedViewModel.swipeUser(context, type, profile.id, direction)
}
) )
} }
is LikedUiState.Error -> { is LikedUiState.Error -> {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
@ -144,6 +154,9 @@ fun ProfileCard(
likedProfile: LikedUserResponse, likedProfile: LikedUserResponse,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onNameClick: () -> Unit = {}, onNameClick: () -> Unit = {},
onCancel: (() -> Unit)? = null,
onSuperLike: (() -> Unit)? = null,
onLike: (() -> Unit)? = null
) { ) {
Card( Card(
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
@ -165,7 +178,7 @@ fun ProfileCard(
onNameClick() onNameClick()
} }
) { ) {
// FIX: media can be null from the server, so use ?. here // media can be null from the server, so use ?. here
AsyncImage( AsyncImage(
model = likedProfile.profile_image_url?.takeIf { it.isNotBlank() } model = likedProfile.profile_image_url?.takeIf { it.isNotBlank() }
?: likedProfile.media?.firstOrNull(), ?: likedProfile.media?.firstOrNull(),
@ -223,8 +236,6 @@ fun ProfileCard(
} }
val nameAndAge = if (age != null) "$displayName, $age" else displayName val nameAndAge = if (age != null) "$displayName, $age" else displayName
Log.d("user", "${likedProfile.id}")
Text( Text(
text = nameAndAge, text = nameAndAge,
color = Color.White, color = Color.White,
@ -249,6 +260,57 @@ fun ProfileCard(
} }
} }
} }
// Action buttons: Cancel (pass) / Super like / Like
if (onCancel != null && onSuperLike != null && onLike != null) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 8.dp, end = 8.dp, bottom = 10.dp),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
onClick = onCancel,
modifier = Modifier
.size(30.dp)
.clip(CircleShape)
.background(Color(0xFFEBEBEB))
) {
Icon(
Icons.Default.Close, "Pass",
tint = Color(0xFF555555),
modifier = Modifier.size(20.dp)
)
}
IconButton(
onClick = onSuperLike,
modifier = Modifier
.size(30.dp)
.clip(CircleShape)
.background(Color(0xFFEDE7F6))
) {
Icon(
Icons.Default.Star, "Super like",
tint = Color(0xFF7B1FA2),
modifier = Modifier.size(20.dp)
)
}
IconButton(
onClick = onLike,
modifier = Modifier
.size(30.dp)
.clip(CircleShape)
.background(Color(0xFFE91E63))
) {
Icon(
Icons.Default.Favorite, "Like",
tint = Color.White,
modifier = Modifier.size(20.dp)
)
}
}
}
} }
} }
} }
@ -260,7 +322,8 @@ fun ProfileGrid(
isLoading: Boolean, isLoading: Boolean,
errorMessage: String?, errorMessage: String?,
onRetry: () -> Unit = {}, onRetry: () -> Unit = {},
modifier: Modifier = Modifier modifier: Modifier = Modifier,
onSwipe: ((LikedUserResponse, String) -> Unit)? = null
) { ) {
if (likedResponse.isEmpty() && !isLoading) { if (likedResponse.isEmpty() && !isLoading) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
@ -276,8 +339,14 @@ fun ProfileGrid(
verticalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = modifier.fillMaxSize() modifier = modifier.fillMaxSize()
) { ) {
items(likedResponse) { profile -> items(likedResponse, key = { it.id }) { profile ->
ProfileCard(context, likedProfile = profile) ProfileCard(
context = context,
likedProfile = profile,
onCancel = onSwipe?.let { f -> { f(profile, "pass") } },
onSuperLike = onSwipe?.let { f -> { f(profile, "superlike") } },
onLike = onSwipe?.let { f -> { f(profile, "like") } }
)
} }
} }
} }
@ -321,7 +390,8 @@ fun ProfileGridPreview() {
context = context, context = context,
likedResponse = sampleLikedUsers, likedResponse = sampleLikedUsers,
isLoading = false, isLoading = false,
errorMessage = null errorMessage = null,
onSwipe = { _, _ -> }
) )
} }
} }

100
app/src/main/java/com/example/dattingapp/ShowPostingActivity.kt

@ -36,6 +36,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -171,6 +172,24 @@ fun PostCard(
var showPostMenu by remember { mutableStateOf(false) } var showPostMenu by remember { mutableStateOf(false) }
var showDeleteDialog by remember { mutableStateOf(false) } var showDeleteDialog by remember { mutableStateOf(false) }
// In-place post editing
var isEditingPost by remember { mutableStateOf(false) }
var postEditText by remember { mutableStateOf("") }
val cancelPostEdit = {
isEditingPost = false
postEditText = ""
}
val savePostEdit = {
if (postEditText.isNotBlank() &&
postEditText.trim() != post.content.orEmpty().trim()
) {
showPostViewModel.updatePost(context, post.id, postEditText)
}
isEditingPost = false
postEditText = ""
}
Column( Column(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
@ -254,7 +273,8 @@ fun PostCard(
text = { Text("Edit") }, text = { Text("Edit") },
onClick = { onClick = {
showPostMenu = false showPostMenu = false
// TODO: edit post (empty for now) postEditText = post.content.orEmpty()
isEditingPost = true
} }
) )
DropdownMenuItem( DropdownMenuItem(
@ -272,7 +292,29 @@ fun PostCard(
// --- 2. Media / text content --- // --- 2. Media / text content ---
if (post.media.isNotEmpty()) { if (post.media.isNotEmpty()) {
if (!post.content.isNullOrEmpty()) { // Post text: becomes an edit field in the same place
if (isEditingPost) {
BasicTextField(
value = postEditText,
onValueChange = { postEditText = it },
textStyle = MaterialTheme.typography.bodyMedium,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 4.dp)
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = cancelPostEdit) { Text("Cancel") }
TextButton(
enabled = postEditText.isNotBlank(),
onClick = savePostEdit
) { Text("Save", color = AccentPink) }
}
} else if (!post.content.isNullOrEmpty()) {
Text( Text(
text = post.content, text = post.content,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
@ -281,6 +323,7 @@ fun PostCard(
.padding(horizontal = 12.dp, vertical = 4.dp) .padding(horizontal = 12.dp, vertical = 4.dp)
) )
} }
Card( Card(
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
modifier = Modifier modifier = Modifier
@ -288,9 +331,7 @@ fun PostCard(
.padding(start = 10.dp, end = 10.dp) .padding(start = 10.dp, end = 10.dp)
.height(360.dp), .height(360.dp),
) { ) {
// FIX: was AsyncImage(model = post.media.first().url) — showed ONLY // Swipeable pager over ALL media, images AND videos
// the first media item, always as an image, with the raw URL.
// Now: swipeable pager over ALL media, images AND videos.
PostMediaPager( PostMediaPager(
media = post.media, media = post.media,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
@ -299,31 +340,46 @@ fun PostCard(
} else { } else {
Card( // Text-only post: no card, no background, just the text
shape = RoundedCornerShape(16.dp), val textSize = if ((post.content?.length ?: 0) > 120) 18.sp else 22.sp
if (isEditingPost) {
BasicTextField(
value = postEditText,
onValueChange = { postEditText = it },
textStyle = TextStyle(
color = TextPostTextColor,
fontSize = textSize,
fontWeight = FontWeight.Medium
),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(start = 10.dp, end = 10.dp) .padding(horizontal = 12.dp, vertical = 8.dp)
.height(360.dp), )
colors = CardDefaults.cardColors(containerColor = Color.White) Row(
) { modifier = Modifier
Box( .fillMaxWidth()
modifier = Modifier.fillMaxSize(), .padding(horizontal = 12.dp),
contentAlignment = Alignment.Center horizontalArrangement = Arrangement.End
) { ) {
TextButton(onClick = cancelPostEdit) { Text("Cancel") }
TextButton(
enabled = postEditText.isNotBlank(),
onClick = savePostEdit
) { Text("Save", color = AccentPink) }
}
} else if (!post.content.isNullOrEmpty()) {
Text( Text(
text = post.content.orEmpty(), text = post.content,
color = TextPostTextColor, color = TextPostTextColor,
fontSize = if ((post.content?.length ?: 0) > 120) 18.sp else 24.sp, fontSize = textSize,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Medium,
textAlign = TextAlign.Center, modifier = Modifier
maxLines = 8, .fillMaxWidth()
overflow = TextOverflow.Ellipsis, .padding(horizontal = 12.dp, vertical = 8.dp)
modifier = Modifier.padding(horizontal = 32.dp)
) )
} }
} }
}
// --- 3. Interaction Buttons --- // --- 3. Interaction Buttons ---
Row( Row(

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

File diff suppressed because it is too large

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

@ -16,7 +16,10 @@ import retrofit2.http.Part
import com.example.dattingapp.homes.profile.UserResponse import com.example.dattingapp.homes.profile.UserResponse
import com.example.dattingapp.homes.profile.LikedUserResponse import com.example.dattingapp.homes.profile.LikedUserResponse
import com.example.dattingapp.homes.profile.MatchListItem import com.example.dattingapp.homes.profile.MatchListItem
import com.example.dattingapp.homes.profile.UpdateProfileRequest
import retrofit2.http.DELETE import retrofit2.http.DELETE
import retrofit2.http.PATCH
import retrofit2.http.PUT
interface HomeApiService { interface HomeApiService {
@ -163,4 +166,23 @@ interface HomeApiService {
suspend fun unlikePost( suspend fun unlikePost(
@Path("post_id") postId: Int @Path("post_id") postId: Int
): Response<LikePostResponse> ): Response<LikePostResponse>
@PATCH("api/v1/explore/comments/{comment_id}") // use @PATCH if the backend needs it
suspend fun editComment(
@Path("comment_id") commentId: Int,
@Body request: CreateCommentRequest
): Response<CommentResponse>
@PATCH("api/v1/explore/posts/{post_id}")
suspend fun updatePost(
@Path("post_id") postId: Int,
@Body body: CreatePostRequest
): Response<CreatePostResponse>
@PATCH("api/v1/users/me") // @PUT if the backend needs it
suspend fun updateMyProfile(
@Body body: UpdateProfileRequest
): Response<UserResponse>
} }

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

@ -26,6 +26,7 @@ import java.io.FileOutputStream
import java.lang.Exception import java.lang.Exception
import com.example.dattingapp.homes.profile.LikedUserResponse import com.example.dattingapp.homes.profile.LikedUserResponse
import com.example.dattingapp.homes.profile.MatchListItem import com.example.dattingapp.homes.profile.MatchListItem
import com.example.dattingapp.homes.profile.UpdateProfileRequest
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import org.json.JSONObject import org.json.JSONObject
@ -666,6 +667,91 @@ class LikedViewModel : ViewModel() {
} }
} }
} }
fun swipeUser(context: Context, type: String, userId: Int, direction: String) {
val appContext = context.applicationContext
val flow = when (type) {
"Likes" -> _likesUiState
"Liked" -> _likedUiState
else -> _matchesUiState
}
val before = flow.value as? LikedUiState.Success ?: return
val index = before.LikeData.indexOfFirst { it.id == userId }
if (index < 0) return
val removed = before.LikeData[index]
// 1. Remove the card immediately
flow.value = LikedUiState.Success(before.LikeData.filter { it.id != userId })
// 2. Send the swipe; put the card back if it fails
viewModelScope.launch {
fun restore() {
val now = flow.value as? LikedUiState.Success ?: return
if (now.LikeData.any { it.id == userId }) return
val list = now.LikeData.toMutableList()
list.add(index.coerceAtMost(list.size), removed)
flow.value = LikedUiState.Success(list)
Toast.makeText(appContext, "Action failed. Please try again.", Toast.LENGTH_SHORT).show()
}
try {
val response = HomeNetworkModule
.getApiService(appContext)
.sendSwipe(SwipeRequest(userId, direction))
if (!response.isSuccessful) {
Log.e("LikedSwipe", "HTTP ${response.code()}: ${response.errorBody()?.string()}")
restore()
}
} catch (e: kotlinx.coroutines.CancellationException) {
throw e
} catch (e: Exception) {
Log.e("LikedSwipe", "Swipe failed", e)
restore()
}
}
}
}
sealed interface EditProfileUiState {
object Idle : EditProfileUiState
object Saving : EditProfileUiState
object Saved : EditProfileUiState
data class Error(val message: String) : EditProfileUiState
}
class EditProfileViewModel : ViewModel() {
private val _state = MutableStateFlow<EditProfileUiState>(EditProfileUiState.Idle)
val state: StateFlow<EditProfileUiState> = _state.asStateFlow()
fun save(context: Context, request: UpdateProfileRequest) {
if (_state.value is EditProfileUiState.Saving) return
val appContext = context.applicationContext
viewModelScope.launch {
_state.value = EditProfileUiState.Saving
try {
val response = HomeNetworkModule.getApiService(appContext).updateMyProfile(request)
if (response.isSuccessful) {
_state.value = EditProfileUiState.Saved
} else {
val raw = try { response.errorBody()?.string() } catch (e: Exception) { null }
Log.e("EditProfile", "HTTP ${response.code()}: $raw url=${response.raw().request.url}")
_state.value = EditProfileUiState.Error(
when (response.code()) {
401 -> "Authentication failed. Please log in again."
422 -> "Invalid data: ${raw ?: "check the fields"}"
else -> "Save failed (${response.code()})"
}
)
}
} catch (e: kotlinx.coroutines.CancellationException) {
throw e
} catch (e: Exception) {
Log.e("EditProfile", "Save failed", e)
_state.value = EditProfileUiState.Error("Request failed: ${e.javaClass.simpleName}")
}
}
}
fun reset() { _state.value = EditProfileUiState.Idle }
} }
sealed interface ShowPostUiState { sealed interface ShowPostUiState {
@ -734,6 +820,61 @@ class ShowPostViewModel : ViewModel() {
} }
} }
fun updatePost(context: Context, postId: Int, newContent: String) {
val appContext = context.applicationContext
val text = newContent.trim()
if (text.isEmpty()) return
// keep the existing post type and feeling, change only the text
val current = (_showPostUiState.value as? ShowPostUiState.Success)?.post
viewModelScope.launch {
try {
val api = HomeNetworkModule.getApiService(appContext)
val response = api.updatePost(
postId,
CreatePostRequest(
content = text,
post_type = current?.postType ?: "text",
content_format = "plain",
feeling = current?.feeling,
location = null
)
)
if (response.isSuccessful) {
// silent refresh: no spinner, the screen just shows the new text
val fresh = api.getPostById(postId)
if (fresh.isSuccessful && fresh.body() != null) {
val post = fresh.body()!!
PostInteractionStore.setFromServer(
post.id, post.liked_by_me, post.like_count, post.comment_count
)
_showPostUiState.value = ShowPostUiState.Success(post)
}
} else {
val raw = try { response.errorBody()?.string() } catch (e: Exception) { null }
Log.e("UpdatePost", "Failed ${response.code()}: $raw url=${response.raw().request.url}")
Toast.makeText(
appContext,
when (response.code()) {
403 -> "You can only edit your own posts."
404 -> "Post not found."
422 -> "Invalid post content."
else -> "Update failed (${response.code()})"
},
Toast.LENGTH_LONG
).show()
}
} catch (e: kotlinx.coroutines.CancellationException) {
throw e
} catch (e: Exception) {
Log.e("UpdatePost", "Exception while updating post", e)
Toast.makeText(appContext, "Update failed: ${e.javaClass.simpleName}", Toast.LENGTH_LONG).show()
}
}
}
fun addCommentLocallyAndServer(context: Context, postId: Int, content: String, currentUserName: String?) { fun addCommentLocallyAndServer(context: Context, postId: Int, content: String, currentUserName: String?) {
val appContext = context.applicationContext val appContext = context.applicationContext
@ -799,6 +940,62 @@ class ShowPostViewModel : ViewModel() {
} }
} }
fun editComment(context: Context, postId: Int, commentId: Int, newContent: String) {
val appContext = context.applicationContext
val text = newContent.trim()
if (text.isEmpty()) return
// Optimistic update: the new text shows immediately
val previousList = _commentsUiState.value
_commentsUiState.value = previousList.map {
if (it.id == commentId) it.copy(content = text) else it
}
viewModelScope.launch {
try {
val response = HomeNetworkModule
.getApiService(appContext)
.editComment(commentId, CreateCommentRequest(content = text))
if (response.isSuccessful) {
fetchComments(appContext, postId) // sync with the server
} else {
val rawError = try {
response.errorBody()?.string() ?: "(empty error body)"
} catch (e: Exception) {
"(failed to read error body)"
}
val detail = try {
JSONObject(rawError).optString("detail")
} catch (e: Exception) { "" }
Log.e(
"CommentError",
"Edit failed code=${response.code()} url=${response.raw().request.url} body=$rawError"
)
_commentsUiState.value = previousList // roll back
Toast.makeText(
appContext,
when (response.code()) {
403 -> "You can only edit your own comments."
404 -> "Comment not found."
405 -> "Edit method not allowed (try PATCH instead of PUT)."
else -> "Edit failed (${response.code()}): ${detail.ifBlank { rawError }}"
},
Toast.LENGTH_LONG
).show()
}
} catch (e: kotlinx.coroutines.CancellationException) {
throw e
} catch (e: Exception) {
Log.e("CommentError", "Exception while editing commentId=$commentId", e)
_commentsUiState.value = previousList
Toast.makeText(appContext, "Edit failed: ${e.javaClass.simpleName}", Toast.LENGTH_LONG).show()
}
}
}
// NEW: no longer depends on ShowPostUiState.Success, so it works from the feed too // NEW: no longer depends on ShowPostUiState.Success, so it works from the feed too
fun toggleLikeLocallyAndServer(context: Context, postId: Int, currentlyLiked: Boolean) { fun toggleLikeLocallyAndServer(context: Context, postId: Int, currentlyLiked: Boolean) {
val appContext = context.applicationContext val appContext = context.applicationContext

14
app/src/main/java/com/example/dattingapp/homes/profile/UserResponseData.kt

@ -1,5 +1,8 @@
package com.example.dattingapp.homes.profile package com.example.dattingapp.homes.profile
import kotlinx.serialization.Serializable
@Serializable
data class UserResponse( data class UserResponse(
val id: Int, val id: Int,
val email_or_phone: String, val email_or_phone: String,
@ -131,3 +134,14 @@ data class MatchedUser(
val distance_km: Double? = null, val distance_km: Double? = null,
val compatibility_percentage: Int? = null val compatibility_percentage: Int? = null
) )
data class UpdateProfileRequest(
val full_name: String? = null,
val gender: String? = null,
val birth_date: String? = null,
val city: String? = null,
val country: String? = null,
val relationship_goal: String? = null,
val bio: String? = null,
val interests: List<String>? = null
)

11
app/src/main/java/com/example/dattingapp/homes/profile/ui/theme/Color.kt

@ -0,0 +1,11 @@
package com.example.dattingapp.homes.profile.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

58
app/src/main/java/com/example/dattingapp/homes/profile/ui/theme/Theme.kt

@ -0,0 +1,58 @@
package com.example.dattingapp.homes.profile.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun DattingAPPTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

34
app/src/main/java/com/example/dattingapp/homes/profile/ui/theme/Type.kt

@ -0,0 +1,34 @@
package com.example.dattingapp.homes.profile.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)

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

@ -30,4 +30,6 @@ interface AuthApiService {
suspend fun login( suspend fun login(
@Body request: LoginRequest @Body request: LoginRequest
): Response<LoginResponse> ): Response<LoginResponse>
} }

2
app/src/main/res/values/strings.xml

@ -92,6 +92,8 @@
<string name="title_activity_like_and_match">LikeANdMatchActivity</string> <string name="title_activity_like_and_match">LikeANdMatchActivity</string>
<string name="title_activity_show_posting">ShowPostingActivity</string> <string name="title_activity_show_posting">ShowPostingActivity</string>
<string name="title_activity_matches">MatchesActivity</string> <string name="title_activity_matches">MatchesActivity</string>
<string name="title_activity_edit_profile">EditProfileActivity</string>
<string name="title_activity_edit_profile_activty">EditProfileActivty</string>
</resources> </resources>
Loading…
Cancel
Save