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:theme="@style/Theme.DattingAPP"
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>
</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.compose.setContent
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.background
import androidx.compose.foundation.border
@ -18,17 +23,15 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.TextSnippet
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.ChatBubble
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FlashOn
import androidx.compose.material.icons.filled.Mood
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
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.TextSnippet
import androidx.compose.material.icons.outlined.LocationOn
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
@ -48,18 +51,12 @@ import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
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.FriendProfileResponse
import com.example.dattingapp.homes.profile.LikedUserResponse
import com.example.dattingapp.ui.theme.*
import java.time.LocalDate
import java.time.Period
import java.time.format.DateTimeFormatter
import kotlin.jvm.java
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString
class FriendProfile : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@ -126,9 +123,9 @@ fun FriendProfileScreen(
val matchesViewModel: MatchesViewModel = viewModel()
// Collect response states
val likesState by likedViewModel.likesUiState.collectAsState() // Users who liked me
val likedState by likedViewModel.likedUiState.collectAsState() // Users I liked
val matchesState by matchesViewModel.state.collectAsState() // Match list from server
val likesState by likedViewModel.likesUiState.collectAsState()
val likedState by likedViewModel.likedUiState.collectAsState()
val matchesState by matchesViewModel.state.collectAsState()
// Fetch lists once
LaunchedEffect(Unit) {
@ -154,16 +151,12 @@ fun FriendProfileScreen(
// Track local tap in current screen session
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
// =========================================================================
// STEP 1: Check online match list first
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
// Final match decision
val isMatched = isOnlineMatch || isLocalMatch
// Derived button flags
@ -175,176 +168,174 @@ fun FriendProfileScreen(
calculateAge(friendProfileData.birth_date)
}
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
.verticalScroll(rememberScrollState())
.verticalScroll(scrollState)
.padding(16.dp)
) {
// Header Cover & Profile Avatar
Box(
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
)
// ====================================================
// 1. HEADER: Active Now & Action Icons
// ====================================================
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 25.dp),
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
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(
onClick = onBackClick,
modifier = Modifier.size(20.dp).clip(CircleShape)
modifier = Modifier
.size(36.dp)
.clip(CircleShape)
.background(Color(0xFFF7F7F7))
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "back button"
contentDescription = "Back",
tint = textDark,
modifier = Modifier.size(18.dp)
)
}
IconButton(
onClick = onMoreClick,
modifier = Modifier.size(20.dp).clip(CircleShape)
modifier = Modifier
.size(36.dp)
.clip(CircleShape)
.background(Color(0xFFF7F7F7))
) {
Icon(
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(
model = friendProfileData.profile_image_url,
contentDescription = "Profile Avatar",
model = friendProfileData.profile_image_url ?: "https://picsum.photos/300/300",
contentDescription = "Profile Picture",
contentScale = ContentScale.Crop,
placeholder = painterResource(id = R.drawable.locationpermissionbg),
error = painterResource(id = R.drawable.locationpermissionbg),
modifier = Modifier
.padding(start = 20.dp)
.align(Alignment.BottomStart)
.size(100.dp)
.fillMaxSize()
.clip(CircleShape)
.border(4.dp, Color.White, CircleShape),
contentScale = ContentScale.Crop
)
}
Spacer(modifier = Modifier.height(12.dp))
// Profile Main Details
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
val displayName = friendProfileData.full_name ?: "User"
val nameAndAge = if (age != null) "$displayName, $age" else displayName
val displayName = friendProfileData.full_name ?: "Alex Rivera"
Text(
text = nameAndAge,
fontSize = 24.sp,
text = displayName,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = textDark
)
Surface(
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)
)
if (age != null) {
Text(
text = "${friendProfileData.county ?: 0}% Match",
color = Color.White,
fontSize = 10.sp,
fontWeight = FontWeight.Bold
text = "$age",
fontSize = 20.sp,
fontWeight = FontWeight.Normal,
color = textDark
)
}
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.padding(vertical = 4.dp)
) {
Icon(
imageVector = Icons.Outlined.LocationOn,
contentDescription = null,
tint = Color.Gray,
modifier = Modifier.size(14.dp)
)
Text(
text = "${friendProfileData.city ?: "Unknown"}, ${friendProfileData.country ?: ""}",
fontSize = 12.sp,
color = Color.Gray
imageVector = Icons.Default.Check,
contentDescription = "Verified",
tint = BrandMagenta,
modifier = Modifier.size(16.dp)
)
}
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
shape = RoundedCornerShape(12.dp),
color = Color(0xFFF9F9F9),
shadowElevation = 1.dp
) {
val city = friendProfileData.city ?: "New York"
Text(
text = friendProfileData.bio ?: "",
fontSize = 13.sp,
color = Color(0xFF4A4A4A),
modifier = Modifier.padding(12.dp),
maxLines = 2,
lineHeight = 18.sp
text = "📍 $city, NY • 2 miles away",
fontSize = 12.sp,
color = BrandPurple,
fontWeight = FontWeight.SemiBold
)
}
}
// =========================================================================
// ACTION BUTTON ROW WITH EVALUATED MATCH STATES
// =========================================================================
Spacer(modifier = Modifier.height(16.dp))
// ====================================================
// 3. ACTION BUTTON ROW (Message, Like, Super Like, Dislike)
// ====================================================
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(10.dp),
.padding(vertical = 4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 💬 Message Button — Enabled ONLY when matched
// 💬 Message Button
Card(
onClick = onMessageClick,
enabled = isMessageClickable,
modifier = Modifier
.weight(1f)
.height(48.dp),
shape = RoundedCornerShape(10.dp),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
containerColor = if (isMessageClickable) Color.White else Color(0xFFF0F0F0),
disabledContainerColor = Color(0xFFF0F0F0)
),
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(
modifier = Modifier.fillMaxSize(),
@ -355,12 +346,12 @@ fun FriendProfileScreen(
imageVector = Icons.Default.ChatBubble,
contentDescription = "Message",
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 = "Message",
fontSize = 16.sp,
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = if (isMessageClickable) textDark else Color(0xFFBDBDBD)
)
@ -369,7 +360,6 @@ fun FriendProfileScreen(
// ❤️ Like & ⭐ Super Like Buttons — Hidden when matched
if (showLikeAndSuperLike) {
// ❤️ Like Button
Card(
onClick = {
onLikeClick()
@ -379,14 +369,15 @@ fun FriendProfileScreen(
},
enabled = !likeDisabled,
modifier = Modifier.size(48.dp),
shape = RoundedCornerShape(10.dp),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
containerColor = if (heLikedMe) Color(0xFFE53935) else Color.White,
disabledContainerColor = if (heLikedMe) Color(0xFFE53935) else Color(0xFFF0F0F0)
),
elevation = CardDefaults.cardElevation(
defaultElevation = if (likeDisabled) 0.dp else 2.dp
)
),
border = CardDefaults.outlinedCardBorder().copy(width = 0.5.dp)
) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Icon(
@ -402,7 +393,6 @@ fun FriendProfileScreen(
}
}
// ⭐ Super Like (Star) Button
Card(
onClick = {
onStarClick()
@ -412,14 +402,15 @@ fun FriendProfileScreen(
},
enabled = !likeDisabled,
modifier = Modifier.size(48.dp),
shape = RoundedCornerShape(10.dp),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
containerColor = Color.White,
disabledContainerColor = Color(0xFFF0F0F0)
),
elevation = CardDefaults.cardElevation(
defaultElevation = if (likeDisabled) 0.dp else 2.dp
)
),
border = CardDefaults.outlinedCardBorder().copy(width = 0.5.dp)
) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Icon(
@ -432,21 +423,21 @@ fun FriendProfileScreen(
}
}
// ❌ Cancel / Dislike Button — Resets everything back to initial state
// ❌ Dislike / Cancel Button
Card(
onClick = {
onCancelClick()
handleLikeAndDislikeAction(context, "pass", friendProfileData, swipeViewModel)
localLiked = false
likedViewModel.likedResponse(context, "Likes")
likedViewModel.likedResponse(context, "Liked")
matchesViewModel.loadMatches(context)
},
modifier = Modifier.size(48.dp),
shape = RoundedCornerShape(10.dp),
shape = RoundedCornerShape(12.dp),
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) {
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))
// 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 = "Posts",
fontSize = 18.sp,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = textDark,
modifier = Modifier.padding(bottom = 8.dp)
)
Spacer(modifier = Modifier.height(16.dp))
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(
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 ->
val aspectRatio = if (idx % 2 == 0) 0.8f else 1.2f
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 ->
val aspectRatio = if (idx % 2 == 0) 1.2f else 0.8f
MasonryPostItem(post = post, aspectRatio = aspectRatio)
@ -521,6 +851,7 @@ fun MasonryPostItem(post: FriendPost, aspectRatio: Float) {
Card(
shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
border = CardDefaults.outlinedCardBorder().copy(width = 0.5.dp),
modifier = Modifier
.fillMaxWidth()
.aspectRatio(aspectRatio)
@ -556,28 +887,23 @@ fun MasonryPostItem(post: FriendPost, aspectRatio: Float) {
Column(
modifier = Modifier
.align(Alignment.BottomStart)
.padding(2.dp),
.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
if (!post.feeling.isNullOrBlank()) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Text(
text = post.feeling,
color = Color.White.copy(alpha = 0.9f),
fontSize = 8.sp,
fontSize = 9.sp,
fontWeight = FontWeight.Bold
)
}
}
if (!post.content.isNullOrBlank()) {
Text(
text = post.content,
color = Color.White,
fontSize = 10.sp,
maxLines = 1,
fontSize = 11.sp,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
@ -586,55 +912,21 @@ fun MasonryPostItem(post: FriendPost, aspectRatio: Float) {
Column(
modifier = Modifier
.fillMaxSize()
.background(BrandPurple.copy(alpha = 0.8f))
.padding(10.dp),
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 = post.post_type.uppercase(),
color = Color.White.copy(alpha = 0.9f),
fontSize = 7.sp,
fontSize = 8.sp,
fontWeight = FontWeight.ExtraBold
)
}
}
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
if (!post.feeling.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
)
}
}
if (!post.content.isNullOrBlank()) {
Text(
text = post.content ?: "",
text = post.content,
color = Color.White,
fontSize = 11.sp,
fontWeight = FontWeight.SemiBold,
@ -643,6 +935,7 @@ fun MasonryPostItem(post: FriendPost, aspectRatio: Float) {
lineHeight = 14.sp
)
}
}
Row(
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.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
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.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
import com.example.dattingapp.homes.networkandapi.LikedUiState
@ -62,7 +66,8 @@ class LikeANdMatchActivity : ComponentActivity() {
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)
}
@ -106,14 +111,19 @@ class LikeANdMatchActivity : ComponentActivity() {
is LikedUiState.Idle, is LikedUiState.Loading -> {
CircularProgressIndicator()
}
is LikedUiState.Success -> {
ProfileGrid(
context = context,
likedResponse = state.LikeData,
isLoading = false,
errorMessage = null
errorMessage = null,
onSwipe = { profile, direction ->
likedViewModel.swipeUser(context, type, profile.id, direction)
}
)
}
is LikedUiState.Error -> {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
@ -144,6 +154,9 @@ fun ProfileCard(
likedProfile: LikedUserResponse,
modifier: Modifier = Modifier,
onNameClick: () -> Unit = {},
onCancel: (() -> Unit)? = null,
onSuperLike: (() -> Unit)? = null,
onLike: (() -> Unit)? = null
) {
Card(
shape = RoundedCornerShape(16.dp),
@ -165,7 +178,7 @@ fun ProfileCard(
onNameClick()
}
) {
// FIX: media can be null from the server, so use ?. here
// media can be null from the server, so use ?. here
AsyncImage(
model = likedProfile.profile_image_url?.takeIf { it.isNotBlank() }
?: likedProfile.media?.firstOrNull(),
@ -223,8 +236,6 @@ fun ProfileCard(
}
val nameAndAge = if (age != null) "$displayName, $age" else displayName
Log.d("user", "${likedProfile.id}")
Text(
text = nameAndAge,
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,
errorMessage: String?,
onRetry: () -> Unit = {},
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
onSwipe: ((LikedUserResponse, String) -> Unit)? = null
) {
if (likedResponse.isEmpty() && !isLoading) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
@ -276,8 +339,14 @@ fun ProfileGrid(
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = modifier.fillMaxSize()
) {
items(likedResponse) { profile ->
ProfileCard(context, likedProfile = profile)
items(likedResponse, key = { it.id }) { 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,
likedResponse = sampleLikedUsers,
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.res.painterResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
@ -171,6 +172,24 @@ fun PostCard(
var showPostMenu 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(
modifier = modifier
.fillMaxWidth()
@ -254,7 +273,8 @@ fun PostCard(
text = { Text("Edit") },
onClick = {
showPostMenu = false
// TODO: edit post (empty for now)
postEditText = post.content.orEmpty()
isEditingPost = true
}
)
DropdownMenuItem(
@ -272,7 +292,29 @@ fun PostCard(
// --- 2. Media / text content ---
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 = post.content,
style = MaterialTheme.typography.bodyMedium,
@ -281,6 +323,7 @@ fun PostCard(
.padding(horizontal = 12.dp, vertical = 4.dp)
)
}
Card(
shape = RoundedCornerShape(16.dp),
modifier = Modifier
@ -288,9 +331,7 @@ fun PostCard(
.padding(start = 10.dp, end = 10.dp)
.height(360.dp),
) {
// FIX: was AsyncImage(model = post.media.first().url) — showed ONLY
// the first media item, always as an image, with the raw URL.
// Now: swipeable pager over ALL media, images AND videos.
// Swipeable pager over ALL media, images AND videos
PostMediaPager(
media = post.media,
modifier = Modifier.fillMaxSize()
@ -299,31 +340,46 @@ fun PostCard(
} else {
Card(
shape = RoundedCornerShape(16.dp),
// Text-only post: no card, no background, just the text
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
.fillMaxWidth()
.padding(start = 10.dp, end = 10.dp)
.height(360.dp),
colors = CardDefaults.cardColors(containerColor = Color.White)
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
.padding(horizontal = 12.dp, vertical = 8.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 = post.content.orEmpty(),
text = post.content,
color = TextPostTextColor,
fontSize = if ((post.content?.length ?: 0) > 120) 18.sp else 24.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
maxLines = 8,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(horizontal = 32.dp)
fontSize = textSize,
fontWeight = FontWeight.Medium,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 8.dp)
)
}
}
}
// --- 3. Interaction Buttons ---
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.LikedUserResponse
import com.example.dattingapp.homes.profile.MatchListItem
import com.example.dattingapp.homes.profile.UpdateProfileRequest
import retrofit2.http.DELETE
import retrofit2.http.PATCH
import retrofit2.http.PUT
interface HomeApiService {
@ -163,4 +166,23 @@ interface HomeApiService {
suspend fun unlikePost(
@Path("post_id") postId: Int
): 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 com.example.dattingapp.homes.profile.LikedUserResponse
import com.example.dattingapp.homes.profile.MatchListItem
import com.example.dattingapp.homes.profile.UpdateProfileRequest
import kotlinx.coroutines.Job
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 {
@ -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?) {
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
fun toggleLikeLocallyAndServer(context: Context, postId: Int, currentlyLiked: Boolean) {
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
import kotlinx.serialization.Serializable
@Serializable
data class UserResponse(
val id: Int,
val email_or_phone: String,
@ -131,3 +134,14 @@ data class MatchedUser(
val distance_km: Double? = 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(
@Body request: LoginRequest
): 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_show_posting">ShowPostingActivity</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>
Loading…
Cancel
Save