17 changed files with 2564 additions and 669 deletions
@ -0,0 +1,301 @@ |
|||
package com.example.dattingapp |
|||
|
|||
import android.content.Context |
|||
import android.content.Intent |
|||
import android.os.Bundle |
|||
import androidx.activity.ComponentActivity |
|||
import androidx.activity.compose.setContent |
|||
import androidx.activity.enableEdgeToEdge |
|||
import androidx.compose.foundation.background |
|||
import androidx.compose.foundation.clickable |
|||
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.RoundedCornerShape |
|||
import androidx.compose.material.icons.Icons |
|||
import androidx.compose.material.icons.filled.Favorite |
|||
import androidx.compose.material.icons.filled.LocationOn |
|||
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 |
|||
import androidx.compose.ui.Alignment |
|||
import androidx.compose.ui.Modifier |
|||
import androidx.compose.ui.draw.clip |
|||
import androidx.compose.ui.graphics.Brush |
|||
import androidx.compose.ui.graphics.Color |
|||
import androidx.compose.ui.layout.ContentScale |
|||
import androidx.compose.ui.platform.LocalContext |
|||
import androidx.compose.ui.text.font.FontWeight |
|||
import androidx.compose.ui.tooling.preview.Preview |
|||
import androidx.compose.ui.unit.dp |
|||
import androidx.compose.ui.unit.sp |
|||
import androidx.lifecycle.viewmodel.compose.viewModel |
|||
import coil.compose.AsyncImage |
|||
import com.example.dattingapp.homes.networkandapi.LikedUiState |
|||
import com.example.dattingapp.homes.networkandapi.LikedViewModel |
|||
import com.example.dattingapp.homes.profile.LikedUserResponse |
|||
import com.example.dattingapp.ui.theme.DattingAPPTheme |
|||
import java.time.LocalDate |
|||
import java.time.Period |
|||
import java.time.format.DateTimeFormatter |
|||
import kotlin.jvm.java |
|||
|
|||
class LikeANdMatchActivity : ComponentActivity() { |
|||
override fun onCreate(savedInstanceState: Bundle?) { |
|||
super.onCreate(savedInstanceState) |
|||
enableEdgeToEdge() |
|||
|
|||
setContent { |
|||
DattingAPPTheme { |
|||
val likedViewModel: LikedViewModel = viewModel() |
|||
val context = LocalContext.current |
|||
|
|||
// Collect state from ViewModel |
|||
val uiState by likedViewModel.likeUiState.collectAsState() |
|||
|
|||
// Trigger network call once when screen loads |
|||
LaunchedEffect(Unit) { |
|||
likedViewModel.likedResponse(context) |
|||
} |
|||
|
|||
Scaffold( |
|||
modifier = Modifier.fillMaxSize(), |
|||
) { innerPadding -> |
|||
Box( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.padding(innerPadding), |
|||
contentAlignment = Alignment.Center |
|||
) { |
|||
when (val state = uiState) { |
|||
is LikedUiState.Idle, is LikedUiState.Loading -> { |
|||
CircularProgressIndicator() |
|||
} |
|||
is LikedUiState.Success -> { |
|||
ProfileGrid( |
|||
context =context, |
|||
likedResponse = state.LikeData, |
|||
isLoading = false, |
|||
errorMessage = null |
|||
) |
|||
} |
|||
is LikedUiState.Error -> { |
|||
Column( |
|||
horizontalAlignment = Alignment.CenterHorizontally, |
|||
verticalArrangement = Arrangement.spacedBy(8.dp), |
|||
modifier = Modifier.padding(16.dp) |
|||
) { |
|||
Text( |
|||
text = state.message, |
|||
color = MaterialTheme.colorScheme.error, |
|||
fontWeight = FontWeight.Medium |
|||
) |
|||
Button(onClick = { likedViewModel.likedResponse(context) }) { |
|||
Text("Retry") |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 1. Define ProfileCard FIRST so ProfileGrid can reference it without issue |
|||
@Composable |
|||
fun ProfileCard(context: Context, likedProfile: LikedUserResponse, modifier: Modifier = Modifier, |
|||
onNameClick: () -> Unit={}, |
|||
) { |
|||
Card( |
|||
shape = RoundedCornerShape(16.dp), |
|||
colors = CardDefaults.cardColors(containerColor = Color.White), |
|||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), |
|||
modifier = modifier.fillMaxWidth() |
|||
) { |
|||
Column( |
|||
modifier = Modifier.padding(8.dp), |
|||
verticalArrangement = Arrangement.spacedBy(8.dp) |
|||
) { |
|||
Box( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.height(180.dp) |
|||
.clip(RoundedCornerShape(12.dp)) |
|||
) { |
|||
// Use profile_image_url or fallback to the first media item |
|||
AsyncImage( |
|||
model = likedProfile.profile_image_url?.takeIf { it.isNotBlank() } |
|||
?: likedProfile.media.firstOrNull(), |
|||
contentDescription = likedProfile.full_name, |
|||
contentScale = ContentScale.Crop, |
|||
modifier = Modifier.fillMaxSize() |
|||
) |
|||
|
|||
// Gradient Overlay |
|||
Box( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.background( |
|||
Brush.verticalGradient( |
|||
colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.7f)), |
|||
startY = 100f |
|||
) |
|||
) |
|||
) |
|||
|
|||
// Match Badge |
|||
Surface( |
|||
shape = RoundedCornerShape(20.dp), |
|||
color = Color.White.copy(alpha = 0.9f), |
|||
modifier = Modifier |
|||
.padding(8.dp) |
|||
.align(Alignment.TopStart) |
|||
) { |
|||
Row( |
|||
verticalAlignment = Alignment.CenterVertically, |
|||
horizontalArrangement = Arrangement.spacedBy(4.dp), |
|||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) |
|||
) { |
|||
Icon( |
|||
imageVector = Icons.Default.Favorite, |
|||
contentDescription = null, |
|||
tint = Color(0xFF6C5CE7), |
|||
modifier = Modifier.size(12.dp) |
|||
) |
|||
Text( |
|||
text = "${likedProfile.compatibility_percentage}%", |
|||
fontSize = 11.sp, |
|||
fontWeight = FontWeight.Bold, |
|||
color = Color(0xFF2D3436) |
|||
) |
|||
} |
|||
} |
|||
|
|||
// Name, Age & Distance |
|||
Column( |
|||
modifier = Modifier |
|||
.align(Alignment.BottomStart) |
|||
.clickable { |
|||
// Create an intent to launch your target activity (e.g., FriendProfileActivity) |
|||
val intent = Intent(context, FriendProfile::class.java).apply { |
|||
putExtra("USER_ID", likedProfile.id) // Pass the user ID here |
|||
} |
|||
context.startActivity(intent) |
|||
|
|||
onNameClick() } // Makes just this text section clickable |
|||
.padding(8.dp) |
|||
){ |
|||
val displayName = likedProfile.full_name.ifBlank { "User" } |
|||
val age = remember(likedProfile.birth_date) { |
|||
calculateAge(likedProfile.birth_date) |
|||
} |
|||
val nameAndAge = if (age != null) "$displayName, $age" else displayName |
|||
|
|||
Text( |
|||
text = nameAndAge, |
|||
color = Color.White, |
|||
fontWeight = FontWeight.Bold, |
|||
fontSize = 14.sp |
|||
) |
|||
Row( |
|||
verticalAlignment = Alignment.CenterVertically, |
|||
horizontalArrangement = Arrangement.spacedBy(2.dp) |
|||
) { |
|||
Icon( |
|||
imageVector = Icons.Default.LocationOn, |
|||
contentDescription = null, |
|||
tint = Color.White.copy(alpha = 0.8f), |
|||
modifier = Modifier.size(10.dp) |
|||
) |
|||
Text( |
|||
text = "${likedProfile.distance_km ?: 0.0} km", |
|||
color = Color.White.copy(alpha = 0.8f), |
|||
fontSize = 10.sp |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 2. Define ProfileGrid AFTER ProfileCard |
|||
@Composable |
|||
fun ProfileGrid( |
|||
context: Context, |
|||
likedResponse: List<LikedUserResponse>, |
|||
isLoading: Boolean, |
|||
errorMessage: String?, |
|||
onRetry: () -> Unit = {}, |
|||
modifier: Modifier = Modifier |
|||
) { |
|||
if (likedResponse.isEmpty() && !isLoading) { |
|||
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { |
|||
Text("No liked profiles found.", color = Color.Gray) |
|||
} |
|||
return |
|||
} |
|||
|
|||
LazyVerticalGrid( |
|||
columns = GridCells.Fixed(2), |
|||
contentPadding = PaddingValues(12.dp), |
|||
horizontalArrangement = Arrangement.spacedBy(12.dp), |
|||
verticalArrangement = Arrangement.spacedBy(12.dp), |
|||
modifier = modifier.fillMaxSize() |
|||
) { |
|||
items(likedResponse) { profile -> |
|||
ProfileCard(context,likedProfile = profile) |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Helper to calculate age from birth date string (e.g., "1999-05-15") |
|||
private fun calculateAge(birthDateStr: String?): Int? { |
|||
if (birthDateStr.isNullOrBlank()) return null |
|||
return try { |
|||
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") |
|||
val birthDate = LocalDate.parse(birthDateStr.take(10), formatter) |
|||
Period.between(birthDate, LocalDate.now()).years |
|||
} catch (e: Exception) { |
|||
null |
|||
} |
|||
} |
|||
|
|||
@Preview(showBackground = true) |
|||
@Composable |
|||
fun ProfileGridPreview() { |
|||
val sampleLikedUsers = listOf( |
|||
LikedUserResponse( |
|||
id = 55, |
|||
full_name = "Sarah Jenkins", |
|||
gender = "female", |
|||
birth_date = "1995-08-20", |
|||
city = "Shanghai", |
|||
county = "District A", |
|||
country = "China", |
|||
relationship_goal = "dating", |
|||
bio = "Coffee lover and traveler.", |
|||
profile_image_url = "", |
|||
video_url = null, |
|||
distance_km = 4.1, |
|||
compatibility_percentage = 92, |
|||
media = emptyList() |
|||
) |
|||
) |
|||
val context = LocalContext.current |
|||
|
|||
DattingAPPTheme { |
|||
ProfileGrid( |
|||
context=context, |
|||
likedResponse = sampleLikedUsers, |
|||
isLoading = false, |
|||
errorMessage = null |
|||
) |
|||
} |
|||
} |
|||
@ -1,252 +0,0 @@ |
|||
package com.example.dattingapp |
|||
import androidx.compose.foundation.background |
|||
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.Close |
|||
import androidx.compose.material.icons.filled.Favorite |
|||
import androidx.compose.material.icons.filled.LocationOn |
|||
import androidx.compose.material.icons.outlined.ChatBubbleOutline |
|||
import androidx.compose.material3.* |
|||
import androidx.compose.runtime.Composable |
|||
import androidx.compose.ui.Alignment |
|||
import androidx.compose.ui.Modifier |
|||
import androidx.compose.ui.draw.clip |
|||
import androidx.compose.ui.graphics.Brush |
|||
import androidx.compose.ui.graphics.Color |
|||
import androidx.compose.ui.layout.ContentScale |
|||
import androidx.compose.ui.text.font.FontWeight |
|||
import androidx.compose.ui.tooling.preview.Preview |
|||
import androidx.compose.ui.unit.dp |
|||
import androidx.compose.ui.unit.sp |
|||
import coil.compose.AsyncImage |
|||
|
|||
|
|||
|
|||
class LikeAndMatchScreen { |
|||
} |
|||
|
|||
data class Profile( |
|||
val name: String, |
|||
val age: Int, |
|||
val distance: String, |
|||
val matchPercentage: Int, |
|||
val tags: List<String>, |
|||
val imageUrl: String, |
|||
val hasUnread: Boolean = false |
|||
) |
|||
|
|||
@Composable |
|||
fun ProfileGrid(profiles: List<Profile>, modifier: Modifier = Modifier) { |
|||
LazyVerticalGrid( |
|||
columns = GridCells.Fixed(2), |
|||
contentPadding = PaddingValues(12.dp), |
|||
horizontalArrangement = Arrangement.spacedBy(12.dp), |
|||
verticalArrangement = Arrangement.spacedBy(12.dp), |
|||
modifier = modifier.fillMaxSize() |
|||
) { |
|||
items(profiles) { profile -> |
|||
ProfileCard(profile = profile) |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Composable |
|||
fun ProfileCard(profile: Profile, modifier: Modifier = Modifier) { |
|||
Card( |
|||
shape = RoundedCornerShape(16.dp), |
|||
colors = CardDefaults.cardColors(containerColor = Color.White), |
|||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), |
|||
modifier = modifier.fillMaxWidth() |
|||
) { |
|||
Column( |
|||
modifier = Modifier.padding(8.dp), |
|||
verticalArrangement = Arrangement.spacedBy(8.dp) |
|||
) { |
|||
// Image with Overlays |
|||
Box( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.height(180.dp) |
|||
.clip(RoundedCornerShape(12.dp)) |
|||
) { |
|||
AsyncImage( |
|||
model = profile.imageUrl, |
|||
contentDescription = profile.name, |
|||
contentScale = ContentScale.Crop, |
|||
modifier = Modifier.fillMaxSize() |
|||
) |
|||
|
|||
// Gradient Overlay for Text Readability |
|||
Box( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.background( |
|||
Brush.verticalGradient( |
|||
colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.7f)), |
|||
startY = 100f |
|||
) |
|||
) |
|||
) |
|||
|
|||
// Match Badge |
|||
Surface( |
|||
shape = RoundedCornerShape(20.dp), |
|||
color = Color.White.copy(alpha = 0.9f), |
|||
modifier = Modifier |
|||
.padding(8.dp) |
|||
.align(Alignment.TopStart) |
|||
) { |
|||
Row( |
|||
verticalAlignment = Alignment.CenterVertically, |
|||
horizontalArrangement = Arrangement.spacedBy(4.dp), |
|||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) |
|||
) { |
|||
Icon( |
|||
imageVector = Icons.Default.Favorite, |
|||
contentDescription = null, |
|||
tint = Color(0xFF6C5CE7), |
|||
modifier = Modifier.size(12.dp) |
|||
) |
|||
Text( |
|||
text = "${profile.matchPercentage}%", |
|||
fontSize = 11.sp, |
|||
fontWeight = FontWeight.Bold, |
|||
color = Color(0xFF2D3436) |
|||
) |
|||
} |
|||
} |
|||
|
|||
// Notification Dot |
|||
if (profile.hasUnread) { |
|||
Box( |
|||
modifier = Modifier |
|||
.padding(8.dp) |
|||
.size(8.dp) |
|||
.clip(CircleShape) |
|||
.background(Color(0xFF6C5CE7)) |
|||
.align(Alignment.TopEnd) |
|||
) |
|||
} |
|||
|
|||
// Name, Age & Distance |
|||
Column( |
|||
modifier = Modifier |
|||
.align(Alignment.BottomStart) |
|||
.padding(8.dp) |
|||
) { |
|||
Text( |
|||
text = "${profile.name}, ${profile.age}", |
|||
color = Color.White, |
|||
fontWeight = FontWeight.Bold, |
|||
fontSize = 14.sp |
|||
) |
|||
Row( |
|||
verticalAlignment = Alignment.CenterVertically, |
|||
horizontalArrangement = Arrangement.spacedBy(2.dp) |
|||
) { |
|||
Icon( |
|||
imageVector = Icons.Default.LocationOn, |
|||
contentDescription = null, |
|||
tint = Color.White.copy(alpha = 0.8f), |
|||
modifier = Modifier.size(10.dp) |
|||
) |
|||
Text( |
|||
text = profile.distance, |
|||
color = Color.White.copy(alpha = 0.8f), |
|||
fontSize = 10.sp |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Tags Row |
|||
Row( |
|||
horizontalArrangement = Arrangement.spacedBy(4.dp), |
|||
modifier = Modifier.fillMaxWidth() |
|||
) { |
|||
profile.tags.take(2).forEach { tag -> |
|||
Surface( |
|||
shape = RoundedCornerShape(6.dp), |
|||
color = Color(0xFFF1F2F6), |
|||
modifier = Modifier.weight(1f) |
|||
) { |
|||
Text( |
|||
text = tag, |
|||
fontSize = 10.sp, |
|||
color = Color(0xFF636E72), |
|||
modifier = Modifier.padding(vertical = 4.dp, horizontal = 4.dp), |
|||
maxLines = 1 |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Action Buttons |
|||
Row( |
|||
horizontalArrangement = Arrangement.SpaceBetween, |
|||
verticalAlignment = Alignment.CenterVertically, |
|||
modifier = Modifier.fillMaxWidth().padding(10.dp) |
|||
) { |
|||
IconButton( |
|||
onClick = { }, |
|||
modifier = Modifier |
|||
.size(22.dp) |
|||
.background(Color(0xFFF8F9FA), CircleShape) |
|||
) { |
|||
Icon( |
|||
imageVector = Icons.Default.Close, |
|||
contentDescription = "Pass", |
|||
tint = Color(0xFF636E72), |
|||
modifier = Modifier.size(16.dp) |
|||
) |
|||
} |
|||
|
|||
IconButton( |
|||
onClick = { }, |
|||
modifier = Modifier |
|||
.size(22.dp) |
|||
.background(Color(0xFFF1EFFE), CircleShape) |
|||
) { |
|||
Icon( |
|||
imageVector = Icons.Outlined.ChatBubbleOutline, |
|||
contentDescription = "Message", |
|||
tint = Color(0xFF6C5CE7), |
|||
modifier = Modifier.size(16.dp) |
|||
) |
|||
} |
|||
|
|||
IconButton( |
|||
onClick = { }, |
|||
modifier = Modifier |
|||
.size(22.dp) |
|||
.background(Color(0xFFD63031), CircleShape) |
|||
) { |
|||
Icon( |
|||
imageVector = Icons.Default.Favorite, |
|||
contentDescription = "Like", |
|||
tint = Color.White, |
|||
modifier = Modifier.size(16.dp) |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Preview(showBackground = true) |
|||
@Composable |
|||
fun ProfileGridPreview() { |
|||
val sampleProfiles = listOf( |
|||
Profile("Elena", 24, "2 miles away", 98, listOf("Photography", "Coffee"), "", true), |
|||
Profile("Sarah", 26, "4 miles away", 94, listOf("Travel", "Music"), ""), |
|||
Profile("Julian", 27, "3 miles away", 91, listOf("Music Prod", "Vinyl"), ""), |
|||
Profile("Maya", 25, "5 miles away", 89, listOf("Surfing", "Yoga"), "") |
|||
) |
|||
|
|||
ProfileGrid(profiles = sampleProfiles) |
|||
} |
|||
@ -0,0 +1,447 @@ |
|||
package com.example.dattingapp |
|||
|
|||
import android.os.Bundle |
|||
import androidx.activity.ComponentActivity |
|||
import androidx.activity.compose.setContent |
|||
import androidx.activity.enableEdgeToEdge |
|||
import androidx.compose.foundation.layout.fillMaxSize |
|||
import androidx.compose.foundation.layout.padding |
|||
import androidx.compose.material3.Text |
|||
import androidx.compose.runtime.Composable |
|||
import androidx.compose.runtime.remember |
|||
import androidx.compose.ui.Modifier |
|||
import com.example.dattingapp.ui.theme.DattingAPPTheme |
|||
import androidx.compose.foundation.background |
|||
import androidx.compose.foundation.layout.* |
|||
import androidx.compose.foundation.shape.CircleShape |
|||
import androidx.compose.material.icons.Icons |
|||
import androidx.compose.material.icons.filled.Favorite |
|||
import androidx.compose.material3.* |
|||
import androidx.compose.runtime.* |
|||
import androidx.compose.ui.Alignment |
|||
import androidx.compose.ui.draw.clip |
|||
import androidx.compose.ui.graphics.Color |
|||
import androidx.compose.ui.layout.ContentScale |
|||
import androidx.compose.ui.platform.LocalContext |
|||
import androidx.compose.ui.unit.dp |
|||
import coil.compose.AsyncImage |
|||
import android.util.Log |
|||
import androidx.compose.foundation.rememberScrollState |
|||
import androidx.compose.foundation.shape.RoundedCornerShape |
|||
import androidx.compose.foundation.text.BasicTextField |
|||
import androidx.compose.foundation.verticalScroll |
|||
import androidx.compose.material.icons.filled.FavoriteBorder |
|||
import androidx.compose.material.icons.outlined.ChatBubbleOutline |
|||
import androidx.compose.ui.text.SpanStyle |
|||
import androidx.compose.ui.text.buildAnnotatedString |
|||
import androidx.compose.ui.text.font.FontWeight |
|||
import androidx.compose.ui.text.withStyle |
|||
import androidx.compose.ui.unit.sp |
|||
import androidx.lifecycle.viewmodel.compose.viewModel |
|||
import com.example.dattingapp.dataclass.Author |
|||
import com.example.dattingapp.dataclass.ShowPostResponse |
|||
import com.example.dattingapp.homes.networkandapi.ShowPostUiState |
|||
import com.example.dattingapp.homes.networkandapi.ShowPostViewModel |
|||
import androidx.compose.material.icons.filled.Send |
|||
import androidx.compose.ui.tooling.preview.Preview |
|||
import com.example.dattingapp.dataclass.CommentResponse |
|||
import com.example.dattingapp.dataclass.Location |
|||
|
|||
// Color definitions matching the design palette |
|||
val PrimaryDarkText = Color(0xFF1E1E1E) |
|||
val SecondaryGrayText = Color(0xFF757575) |
|||
val AccentPink = Color(0xFFD81B60) |
|||
val CommentBubbleBg = Color(0xFFF6F6F6) |
|||
|
|||
class ShowPostingActivity : ComponentActivity() { |
|||
override fun onCreate(savedInstanceState: Bundle?) { |
|||
super.onCreate(savedInstanceState) |
|||
enableEdgeToEdge() |
|||
|
|||
val postId = intent.getIntExtra("Post_ID", 0) |
|||
Log.d("Post_ID", "$postId") |
|||
|
|||
setContent { |
|||
DattingAPPTheme { |
|||
val context = LocalContext.current |
|||
val showPostViewModel: ShowPostViewModel = viewModel() |
|||
val uiState by showPostViewModel.showPostUiState.collectAsState() |
|||
val comments by showPostViewModel.commentsUiState.collectAsState() |
|||
|
|||
// Trigger API calls when the screen launches |
|||
LaunchedEffect(postId) { |
|||
showPostViewModel.fetchPostById(context, postId) |
|||
showPostViewModel.fetchComments(context, postId) |
|||
} |
|||
|
|||
Scaffold( |
|||
modifier = Modifier.fillMaxSize() |
|||
) { innerPadding -> |
|||
Box( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.padding(innerPadding), |
|||
) { |
|||
when (val state = uiState) { |
|||
is ShowPostUiState.Idle, is ShowPostUiState.Loading -> { |
|||
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) |
|||
} |
|||
is ShowPostUiState.Success -> { |
|||
// Full screen scrollable container |
|||
Column( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.verticalScroll(rememberScrollState()) |
|||
.padding(bottom = 24.dp) |
|||
) { |
|||
PostCard( |
|||
post = state.post, |
|||
comments = comments, |
|||
onAddComment = { commentText -> |
|||
val currentUserName = state.post.author?.full_name |
|||
showPostViewModel.addCommentLocallyAndServer( |
|||
context = context, |
|||
postId = postId, |
|||
content = commentText, |
|||
currentUserName = currentUserName |
|||
) |
|||
} |
|||
) |
|||
} |
|||
} |
|||
is ShowPostUiState.Error -> { |
|||
Text( |
|||
text = state.message, |
|||
color = Color.Red, |
|||
modifier = Modifier.align(Alignment.Center) |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Composable |
|||
fun PostCard( |
|||
post: ShowPostResponse, |
|||
comments: List<CommentResponse>, |
|||
onAddComment: (String) -> Unit = {}, |
|||
modifier: Modifier = Modifier |
|||
) { |
|||
Column( |
|||
modifier = modifier |
|||
.fillMaxWidth() |
|||
.padding(vertical = 8.dp) |
|||
) { |
|||
// --- 1. Author Header --- |
|||
Row( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.padding(horizontal = 12.dp, vertical = 8.dp), |
|||
verticalAlignment = Alignment.CenterVertically |
|||
) { |
|||
Box( |
|||
modifier = Modifier |
|||
.size(40.dp) |
|||
.clip(CircleShape) |
|||
.background(Color.LightGray), |
|||
contentAlignment = Alignment.Center |
|||
) { |
|||
Text(text = post.author?.full_name?.take(1) ?: "U") |
|||
} |
|||
Spacer(modifier = Modifier.width(8.dp)) |
|||
Column { |
|||
Row(verticalAlignment = Alignment.CenterVertically) { |
|||
val authorName = post.author?.full_name ?: "Anonymous" |
|||
val feelingText = post.feeling ?: "" |
|||
|
|||
Text( |
|||
text = buildAnnotatedString { |
|||
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { |
|||
append("$authorName ") |
|||
} |
|||
if (feelingText.isNotEmpty()) { |
|||
withStyle(style = SpanStyle(fontWeight = FontWeight.Normal)) { |
|||
append(" — feeling ") |
|||
} |
|||
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { |
|||
append(feelingText) |
|||
} |
|||
} |
|||
}, |
|||
fontSize = 16.sp |
|||
) |
|||
} |
|||
|
|||
if (!post.location?.name.isNullOrEmpty()) { |
|||
Text( |
|||
text = post.location?.name ?: "", |
|||
style = MaterialTheme.typography.bodySmall, |
|||
color = Color.Gray |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
// --- 2. Post Text Content --- |
|||
if (!post.content.isNullOrEmpty()) { |
|||
Text( |
|||
text = post.content, |
|||
style = MaterialTheme.typography.bodyMedium, |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.padding(horizontal = 12.dp, vertical = 4.dp) |
|||
) |
|||
} |
|||
|
|||
// --- 3. Media Carousel (Images) --- |
|||
if (post.media.isNotEmpty()) { |
|||
Box( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.height(350.dp) |
|||
.padding(vertical = 4.dp) |
|||
) { |
|||
AsyncImage( |
|||
model = post.media.first().url, |
|||
contentDescription = "Post Media Image", |
|||
contentScale = ContentScale.Crop, |
|||
modifier = Modifier.fillMaxSize() |
|||
) |
|||
} |
|||
} |
|||
|
|||
// --- 4. Interaction Buttons --- |
|||
Row( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.padding(horizontal = 8.dp, vertical = 4.dp), |
|||
verticalAlignment = Alignment.CenterVertically |
|||
) { |
|||
IconButton(onClick = { /* Handle like action */ }) { |
|||
Icon( |
|||
imageVector = if (post.likedByMe) Icons.Filled.Favorite else Icons.Default.FavoriteBorder, |
|||
contentDescription = "Like", |
|||
tint = if (post.likedByMe) Color.Red else Color.Black |
|||
) |
|||
} |
|||
Text(text = "${post.likeCount}") |
|||
|
|||
Spacer(modifier = Modifier.width(16.dp)) |
|||
|
|||
IconButton(onClick = { /* Handle comments */ }) { |
|||
Icon( |
|||
imageVector = Icons.Outlined.ChatBubbleOutline, |
|||
contentDescription = "Comments" |
|||
) |
|||
} |
|||
Text(text = "${post.commentCount}") |
|||
} |
|||
|
|||
// --- 5. Comment Section UI --- |
|||
CommentSection( |
|||
comments = comments, |
|||
onAddCommentClick = onAddComment |
|||
) |
|||
} |
|||
} |
|||
|
|||
@Composable |
|||
fun CommentSection( |
|||
comments: List<CommentResponse>, |
|||
onAddCommentClick: (String) -> Unit |
|||
) { |
|||
var newCommentText by remember { mutableStateOf("") } |
|||
|
|||
Column( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.padding(horizontal = 12.dp, vertical = 8.dp) |
|||
) { |
|||
Text( |
|||
text = "Comments", |
|||
style = MaterialTheme.typography.titleSmall, |
|||
fontWeight = FontWeight.Bold, |
|||
modifier = Modifier.padding(bottom = 8.dp) |
|||
) |
|||
|
|||
// List of fetched comments |
|||
comments.reversed().forEach { comment -> |
|||
Row( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.padding(vertical = 6.dp), |
|||
verticalAlignment = Alignment.CenterVertically |
|||
) { |
|||
Box( |
|||
modifier = Modifier |
|||
.size(32.dp) |
|||
.clip(CircleShape) |
|||
.background(Color.LightGray), |
|||
contentAlignment = Alignment.Center |
|||
) { |
|||
Text( |
|||
text = comment.author?.full_name?.take(1) ?: "U", |
|||
style = MaterialTheme.typography.bodySmall, |
|||
fontWeight = FontWeight.Bold |
|||
) |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.width(8.dp)) |
|||
|
|||
Column( |
|||
modifier = Modifier |
|||
.weight(1f) |
|||
.background(CommentBubbleBg, shape = RoundedCornerShape(12.dp)) |
|||
.padding(horizontal = 12.dp, vertical = 8.dp) |
|||
) { |
|||
Text( |
|||
text = comment.author?.full_name ?: "Anonymous", |
|||
style = MaterialTheme.typography.bodySmall, |
|||
fontWeight = FontWeight.Bold |
|||
) |
|||
Spacer(modifier = Modifier.height(2.dp)) |
|||
Text( |
|||
text = comment.content, |
|||
style = MaterialTheme.typography.bodyMedium |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(8.dp)) |
|||
|
|||
// Bottom Comment Input Field Bar |
|||
Row( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.background(CommentBubbleBg, shape = RoundedCornerShape(24.dp)) |
|||
.padding(horizontal = 12.dp, vertical = 4.dp), |
|||
verticalAlignment = Alignment.CenterVertically |
|||
) { |
|||
BasicTextField( |
|||
value = newCommentText, |
|||
onValueChange = { newCommentText = it }, |
|||
modifier = Modifier |
|||
.weight(1f) |
|||
.padding(vertical = 8.dp), |
|||
decorationBox = { innerTextField -> |
|||
if (newCommentText.isEmpty()) { |
|||
Text( |
|||
text = "Add a comment...", |
|||
color = Color.Gray, |
|||
style = MaterialTheme.typography.bodyMedium |
|||
) |
|||
} |
|||
innerTextField() |
|||
} |
|||
) |
|||
|
|||
IconButton( |
|||
onClick = { |
|||
if (newCommentText.isNotBlank()) { |
|||
onAddCommentClick(newCommentText) |
|||
newCommentText = "" |
|||
} |
|||
}, |
|||
modifier = Modifier.size(32.dp) |
|||
) { |
|||
Icon( |
|||
imageVector = Icons.Default.Send, |
|||
contentDescription = "Send Comment", |
|||
tint = AccentPink, |
|||
modifier = Modifier.size(16.dp) |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) |
|||
@Composable |
|||
fun ShowPostScreenPreview() { |
|||
DattingAPPTheme { |
|||
// Mock sample data mirroring your API schema |
|||
val mockPost = ShowPostResponse( |
|||
id = 21, |
|||
content = "Exploring the city and enjoying some coffee! ☕🏙️ #Android #JetpackCompose", |
|||
postType = "image", |
|||
contentFormat = "markdown", |
|||
feeling = "happy 😊", |
|||
location = Location( |
|||
name = "Chiattagong", |
|||
latitude = 27.9671, |
|||
longitude = -175.0599 |
|||
), |
|||
author = Author( |
|||
id = 49, |
|||
full_name = "Alex Rivera", |
|||
city = "Shanghai", |
|||
country = "China", |
|||
compatibilityPercentage = 95 |
|||
), |
|||
likeCount = 12, |
|||
commentCount = 2, |
|||
likedByMe = true, |
|||
media = listOf( |
|||
com.example.dattingapp.dataclass.MediaItem( |
|||
id = 5, |
|||
mediaType = "image", |
|||
mimeType = "image/png", |
|||
sizeBytes = 199619, |
|||
position = 0, |
|||
url = "https://images.unsplash.com/photo-1541167760496-1628856ab772" |
|||
) |
|||
), |
|||
createdAt = "2026-10-13T06:06:15" |
|||
) |
|||
|
|||
// Mock comments list |
|||
val mockComments = listOf( |
|||
CommentResponse( |
|||
id = 1, |
|||
postId = 21, |
|||
content = "This spot looks amazing! Enjoy your day! ☕", |
|||
author = Author( |
|||
id = 54, |
|||
full_name = "Sarah", |
|||
city = "Shanghai", |
|||
country = "China", |
|||
compatibilityPercentage = 100 |
|||
), |
|||
createdAt = "2026-10-13T07:45:15" |
|||
), |
|||
CommentResponse( |
|||
id = 2, |
|||
postId = 21, |
|||
content = "What a wonderful view, I love it!", |
|||
author = Author( |
|||
id = 55, |
|||
full_name = "David", |
|||
city = "Beijing", |
|||
country = "China", |
|||
compatibilityPercentage = 90 |
|||
), |
|||
createdAt = "2026-10-13T08:04:48" |
|||
) |
|||
) |
|||
|
|||
// Full screen container matching activity layout |
|||
Column( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.verticalScroll(rememberScrollState()) |
|||
.padding(vertical = 16.dp) |
|||
) { |
|||
PostCard( |
|||
post = mockPost, |
|||
comments = mockComments, |
|||
onAddComment = { newComment -> |
|||
// Preview action handler |
|||
} |
|||
) |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
package com.example.dattingapp.dataclass |
|||
|
|||
import kotlinx.serialization.SerialName |
|||
import kotlinx.serialization.Serializable |
|||
|
|||
@Serializable |
|||
data class ShowPostResponse( |
|||
val id: Int, |
|||
val content: String?, |
|||
@SerialName("post_type") val postType: String?, |
|||
@SerialName("content_format") val contentFormat: String?, |
|||
val feeling: String?, |
|||
@SerialName("location") val location: Location?, |
|||
@SerialName("author") val author: Author? = null, |
|||
@SerialName("like_count") val likeCount: Int = 0, |
|||
@SerialName("comment_count") val commentCount: Int = 0, |
|||
@SerialName("liked_by_me") val likedByMe: Boolean = false, |
|||
val media: List<MediaItem> = emptyList(), |
|||
@SerialName("created_at") val createdAt: String? |
|||
) |
|||
|
|||
@Serializable |
|||
data class Location( |
|||
val name: String?, |
|||
val latitude: Double?, |
|||
val longitude: Double? |
|||
) |
|||
|
|||
@Serializable |
|||
data class Author( |
|||
val id: Int, |
|||
val full_name: String?, |
|||
val city: String?, |
|||
val country: String?, |
|||
@SerialName("compatibility_percentage") val compatibilityPercentage: Int? = 0 |
|||
) |
|||
|
|||
@Serializable |
|||
data class MediaItem( |
|||
val id: Long, |
|||
@SerialName("media_type") val mediaType: String, // "image" or "video" |
|||
@SerialName("mime_type") val mimeType: String?, |
|||
@SerialName("size_bytes") val sizeBytes: Long?, |
|||
val position: Int?, |
|||
val url: String |
|||
) |
|||
|
|||
|
|||
@Serializable |
|||
data class CreateCommentRequest( |
|||
val content: String |
|||
) |
|||
|
|||
@Serializable |
|||
data class CommentResponse( |
|||
val id: Int, |
|||
@SerialName("post_id") val postId: Int, |
|||
val content: String, |
|||
val author: Author?, |
|||
@SerialName("created_at") val createdAt: String? |
|||
) |
|||
@ -0,0 +1,72 @@ |
|||
package com.example.dattingapp.homes.profile |
|||
|
|||
import kotlinx.serialization.Serializable |
|||
|
|||
@Serializable |
|||
data class FriendProfileResponse( |
|||
val id: Int, |
|||
val full_name: String, |
|||
val gender: String?, |
|||
val birth_date: String?, |
|||
val city: String?, |
|||
val county: String?, |
|||
val country: String?, |
|||
val relationship_goal: String?, |
|||
val bio: String?, |
|||
val profile_image_url: String?, |
|||
val video_url: String?, |
|||
val distance_km: Double?, |
|||
val compatibility_percentage: Int, |
|||
val media: List<FriendUserMedia>, |
|||
val interests: List<String>, |
|||
val posts: List<FriendPost> |
|||
) |
|||
|
|||
@Serializable |
|||
data class FriendUserMedia( |
|||
val id: Int, |
|||
val media_type: String, |
|||
val url: String |
|||
) |
|||
|
|||
@Serializable |
|||
data class FriendPost( |
|||
val id: Int, |
|||
val content: String?, |
|||
val post_type: String, |
|||
val content_format: String, |
|||
val feeling: String?, |
|||
val location: FrienLocation?, |
|||
val author: FriendAuthor, |
|||
val like_count: Int, |
|||
val comment_count: Int, |
|||
val liked_by_me: Boolean, |
|||
val media: List<FrienPostMedia>, |
|||
val created_at: String |
|||
) |
|||
|
|||
@Serializable |
|||
data class FrienLocation( |
|||
val name: String, |
|||
val latitude: Double, |
|||
val longitude: Double |
|||
) |
|||
|
|||
@Serializable |
|||
data class FriendAuthor( |
|||
val id: Int, |
|||
val full_name: String?, |
|||
val city: String?, |
|||
val country: String?, |
|||
val compatibility_percentage: Int |
|||
) |
|||
|
|||
@Serializable |
|||
data class FrienPostMedia( |
|||
val id: Int, |
|||
val media_type: String, |
|||
val mime_type: String?, |
|||
val size_bytes: Long?, |
|||
val position: Int?, |
|||
val url: String |
|||
) |
|||
@ -0,0 +1,107 @@ |
|||
package com.example.dattingapp.homes.profile |
|||
|
|||
data class UserResponse( |
|||
val id: Int, |
|||
val email_or_phone: String, |
|||
val status: String, |
|||
val profile: ProfileData, |
|||
val created_at: String, |
|||
val posts: List<Post>, |
|||
val liked_users: List<LikedUser>, |
|||
val liked_by_users: List<Any>, |
|||
val matches: List<Any>, |
|||
val liked_users_count: Int, |
|||
val liked_by_users_count: Int, |
|||
val matches_count: Int |
|||
) |
|||
|
|||
data class ProfileData( |
|||
val full_name: String, |
|||
val gender: String?, |
|||
val birth_date: String?, |
|||
val city: String?, |
|||
val county: String?, |
|||
val country: String?, |
|||
val relationship_goal: String?, |
|||
val bio: String?, |
|||
val profile_image_url: String?, |
|||
val video_url: String?, |
|||
val interests: List<String> |
|||
) |
|||
|
|||
data class Post( |
|||
val id: Int, |
|||
val content: String?, |
|||
val post_type: String, |
|||
val content_format: String, |
|||
val feeling: String?, |
|||
val location: Location?, |
|||
val author: Author, |
|||
val like_count: Int, |
|||
val comment_count: Int, |
|||
val liked_by_me: Boolean, |
|||
val media: List<Media>, |
|||
val created_at: String |
|||
) |
|||
|
|||
data class Location( |
|||
val name: String, |
|||
val latitude: Double, |
|||
val longitude: Double |
|||
) |
|||
|
|||
data class Author( |
|||
val id: Int, |
|||
val full_name: String?, |
|||
val city: String?, |
|||
val country: String?, |
|||
val compatibility_percentage: Int |
|||
) |
|||
|
|||
data class Media( |
|||
val id: Int, |
|||
val media_type: String, |
|||
val mime_type: String?, |
|||
val size_bytes: Long?, |
|||
val position: Int?, |
|||
val url: String |
|||
) |
|||
|
|||
data class LikedUser( |
|||
val id: Int, |
|||
val full_name: String?, |
|||
val gender: String?, |
|||
val birth_date: String?, |
|||
val city: String?, |
|||
val county: String?, |
|||
val country: String?, |
|||
val relationship_goal: String?, |
|||
val bio: String?, |
|||
val profile_image_url: String?, |
|||
val video_url: String?, |
|||
val distance_km: Double?, |
|||
val compatibility_percentage: Int, |
|||
val media: List<UserMedia> |
|||
) |
|||
|
|||
data class UserMedia( |
|||
val id: Int, |
|||
val media_type: String, |
|||
val url: String |
|||
) |
|||
data class LikedUserResponse( |
|||
val id: Int, |
|||
val full_name: String, |
|||
val gender: String, |
|||
val birth_date: String, |
|||
val city: String, |
|||
val county: String?, |
|||
val country: String, |
|||
val relationship_goal: String, |
|||
val bio: String, |
|||
val profile_image_url: String?, |
|||
val video_url: String?, |
|||
val distance_km: Double?, |
|||
val compatibility_percentage: Int, |
|||
val media: List<Media> // Adjust if media contains custom objects |
|||
) |
|||
Loading…
Reference in new issue