30 changed files with 1719 additions and 1553 deletions
@ -0,0 +1 @@ |
|||||
|
DattingAPP |
||||
@ -0,0 +1,5 @@ |
|||||
|
import android.app.Application |
||||
|
import dagger.hilt.android.HiltAndroidApp |
||||
|
|
||||
|
@HiltAndroidApp |
||||
|
class MainApplication : Application() |
||||
@ -0,0 +1,19 @@ |
|||||
|
package com.example.dattingapp |
||||
|
|
||||
|
|
||||
|
import android.app.Application |
||||
|
import dagger.hilt.android.HiltAndroidApp |
||||
|
|
||||
|
@HiltAndroidApp |
||||
|
class DatingApp : Application() { |
||||
|
|
||||
|
override fun onCreate() { |
||||
|
super.onCreate() |
||||
|
instance = this |
||||
|
} |
||||
|
|
||||
|
companion object { |
||||
|
lateinit var instance: DatingApp |
||||
|
private set |
||||
|
} |
||||
|
} |
||||
@ -1,47 +0,0 @@ |
|||||
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.Scaffold |
|
||||
import androidx.compose.material3.Text |
|
||||
import androidx.compose.runtime.Composable |
|
||||
import androidx.compose.ui.Modifier |
|
||||
import androidx.compose.ui.tooling.preview.Preview |
|
||||
import com.example.dattingapp.ui.theme.DattingAPPTheme |
|
||||
|
|
||||
class ExploreScreen : ComponentActivity() { |
|
||||
override fun onCreate(savedInstanceState: Bundle?) { |
|
||||
super.onCreate(savedInstanceState) |
|
||||
enableEdgeToEdge() |
|
||||
setContent { |
|
||||
DattingAPPTheme { |
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> |
|
||||
Greeting2( |
|
||||
name = "Android", |
|
||||
modifier = Modifier.padding(innerPadding) |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Composable |
|
||||
fun Greeting2(name: String, modifier: Modifier = Modifier) { |
|
||||
Text( |
|
||||
text = "Hello $name!", |
|
||||
modifier = modifier |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
@Preview(showBackground = true) |
|
||||
@Composable |
|
||||
fun GreetingPreview2() { |
|
||||
DattingAPPTheme { |
|
||||
Greeting2("Android") |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,7 @@ |
|||||
|
package com.example.dattingapp |
||||
|
|
||||
|
import android.app.Application |
||||
|
import dagger.hilt.android.HiltAndroidApp |
||||
|
|
||||
|
@HiltAndroidApp |
||||
|
class MainApplication : Application() |
||||
@ -1,301 +0,0 @@ |
|||||
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.Scaffold |
|
||||
import androidx.compose.material3.Text |
|
||||
import androidx.compose.runtime.Composable |
|
||||
import androidx.compose.ui.Modifier |
|
||||
import androidx.compose.ui.tooling.preview.Preview |
|
||||
import com.example.dattingapp.ui.theme.DattingAPPTheme |
|
||||
import androidx.compose.foundation.Image |
|
||||
import androidx.compose.foundation.background |
|
||||
import androidx.compose.foundation.layout.* |
|
||||
import androidx.compose.foundation.shape.CircleShape |
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape |
|
||||
import androidx.compose.material.icons.Icons |
|
||||
import androidx.compose.material.icons.filled.* |
|
||||
import androidx.compose.material.icons.outlined.Close |
|
||||
import androidx.compose.material.icons.outlined.LocationOn |
|
||||
import androidx.compose.material.icons.outlined.Search |
|
||||
import androidx.compose.material3.* |
|
||||
import androidx.compose.runtime.* |
|
||||
import androidx.compose.ui.Alignment |
|
||||
import androidx.compose.ui.draw.clip |
|
||||
import androidx.compose.ui.draw.shadow |
|
||||
import androidx.compose.ui.graphics.Brush |
|
||||
import androidx.compose.ui.graphics.Color |
|
||||
import androidx.compose.ui.layout.ContentScale |
|
||||
import androidx.compose.ui.res.painterResource |
|
||||
import androidx.compose.ui.text.font.FontWeight |
|
||||
import androidx.compose.ui.unit.dp |
|
||||
import androidx.compose.ui.unit.sp |
|
||||
import com.example.dattingapp.R |
|
||||
import com.example.dattingapp.ui.theme.BrandPink |
|
||||
import com.example.dattingapp.ui.theme.BrandPurple |
|
||||
import com.example.dattingapp.ui.theme.DattingAPPTheme |
|
||||
|
|
||||
class MeetScreen : ComponentActivity() { |
|
||||
override fun onCreate(savedInstanceState: Bundle?) { |
|
||||
super.onCreate(savedInstanceState) |
|
||||
enableEdgeToEdge() |
|
||||
setContent { |
|
||||
DattingAPPTheme { |
|
||||
MeetDiscoveryScreen() |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@Composable |
|
||||
fun MeetDiscoveryScreen( |
|
||||
modifier: Modifier = Modifier, |
|
||||
onAddClick: () -> Unit = {}, |
|
||||
onSearchClick: () -> Unit = {}, |
|
||||
onSuperLikeClick: () -> Unit = {}, |
|
||||
onRejectClick: () -> Unit = {}, |
|
||||
onLikeClick: () -> Unit = {} |
|
||||
) { |
|
||||
Box( |
|
||||
modifier = modifier |
|
||||
.fillMaxSize() |
|
||||
.background(Color(0xFFF8F9FA)) |
|
||||
) { |
|
||||
Column( |
|
||||
modifier = Modifier |
|
||||
.fillMaxSize() |
|
||||
.padding(horizontal = 16.dp), |
|
||||
horizontalAlignment = Alignment.CenterHorizontally |
|
||||
) { |
|
||||
// TOP BAR |
|
||||
Row( |
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.padding(top = 12.dp, bottom = 12.dp), |
|
||||
horizontalArrangement = Arrangement.SpaceBetween, |
|
||||
verticalAlignment = Alignment.CenterVertically |
|
||||
) { |
|
||||
IconButton( |
|
||||
onClick = onAddClick, |
|
||||
modifier = Modifier |
|
||||
.size(38.dp) |
|
||||
.clip(CircleShape) |
|
||||
.background(Color(0xFFEBEBEB)) |
|
||||
) { |
|
||||
Icon( |
|
||||
imageVector = Icons.Default.Add, |
|
||||
contentDescription = "Add", |
|
||||
tint = BrandPink, |
|
||||
modifier = Modifier.size(20.dp) |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
Text( |
|
||||
text = "VibeCheck", |
|
||||
fontSize = 24.sp, |
|
||||
fontWeight = FontWeight.Bold, |
|
||||
color = BrandPurple |
|
||||
) |
|
||||
|
|
||||
IconButton(onClick = onSearchClick) { |
|
||||
Icon( |
|
||||
imageVector = Icons.Outlined.Search, |
|
||||
contentDescription = "Search", |
|
||||
tint = BrandPink, |
|
||||
modifier = Modifier.size(26.dp) |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// PROFILE SWIPE CARD |
|
||||
Box( |
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.weight(1f) |
|
||||
.padding(bottom = 16.dp) |
|
||||
.clip(RoundedCornerShape(20.dp)) |
|
||||
) { |
|
||||
// Background Profile Image |
|
||||
Image( |
|
||||
painter = painterResource(id = R.drawable.locationpermissionbg), |
|
||||
contentDescription = "User Profile Image", |
|
||||
modifier = Modifier.fillMaxSize(), |
|
||||
contentScale = ContentScale.Crop |
|
||||
) |
|
||||
|
|
||||
// Dark Bottom Gradient Overlay |
|
||||
Box( |
|
||||
modifier = Modifier |
|
||||
.fillMaxSize() |
|
||||
.background( |
|
||||
Brush.verticalGradient( |
|
||||
colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.85f)), |
|
||||
startY = 350f |
|
||||
) |
|
||||
) |
|
||||
) |
|
||||
|
|
||||
// RIGHT-SIDE ACTION BUTTONS (Moved lower using BottomEnd alignment) |
|
||||
Column( |
|
||||
modifier = Modifier |
|
||||
.align(Alignment.BottomEnd) |
|
||||
.padding(end = 14.dp, bottom = 20.dp), |
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp), |
|
||||
horizontalAlignment = Alignment.CenterHorizontally |
|
||||
) { |
|
||||
// Star (Super Like) Button |
|
||||
IconButton( |
|
||||
onClick = onSuperLikeClick, |
|
||||
modifier = Modifier |
|
||||
.size(50.dp) |
|
||||
.shadow(6.dp, CircleShape) |
|
||||
.clip(CircleShape) |
|
||||
.background(Color.White) |
|
||||
) { |
|
||||
Icon( |
|
||||
imageVector = Icons.Default.Star, |
|
||||
contentDescription = "Super Like", |
|
||||
tint = BrandPurple, |
|
||||
modifier = Modifier.size(24.dp) |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
// Pass (X) Button |
|
||||
IconButton( |
|
||||
onClick = onRejectClick, |
|
||||
modifier = Modifier |
|
||||
.size(54.dp) |
|
||||
.shadow(6.dp, CircleShape) |
|
||||
.clip(CircleShape) |
|
||||
.background(Color.White) |
|
||||
) { |
|
||||
Icon( |
|
||||
imageVector = Icons.Outlined.Close, |
|
||||
contentDescription = "Reject", |
|
||||
tint = Color(0xFF555555), |
|
||||
modifier = Modifier.size(26.dp) |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
// Like (Heart) Button |
|
||||
IconButton( |
|
||||
onClick = onLikeClick, |
|
||||
modifier = Modifier |
|
||||
.size(60.dp) |
|
||||
.shadow(8.dp, CircleShape) |
|
||||
.clip(CircleShape) |
|
||||
.background(BrandPink) |
|
||||
) { |
|
||||
Icon( |
|
||||
imageVector = Icons.Default.Favorite, |
|
||||
contentDescription = "Like", |
|
||||
tint = Color.White, |
|
||||
modifier = Modifier.size(30.dp) |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// BOTTOM PROFILE DETAILS |
|
||||
Column( |
|
||||
modifier = Modifier |
|
||||
.align(Alignment.BottomStart) |
|
||||
.padding(16.dp) |
|
||||
.fillMaxWidth(0.70f) |
|
||||
) { |
|
||||
Row( |
|
||||
verticalAlignment = Alignment.CenterVertically, |
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp) |
|
||||
) { |
|
||||
Text( |
|
||||
text = "Alex, 24", |
|
||||
fontSize = 26.sp, |
|
||||
fontWeight = FontWeight.Bold, |
|
||||
color = Color.White |
|
||||
) |
|
||||
|
|
||||
// 98% Match Badge |
|
||||
Surface( |
|
||||
shape = RoundedCornerShape(10.dp), |
|
||||
color = BrandPink |
|
||||
) { |
|
||||
Row( |
|
||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), |
|
||||
verticalAlignment = Alignment.CenterVertically, |
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp) |
|
||||
) { |
|
||||
Icon( |
|
||||
imageVector = Icons.Default.Verified, |
|
||||
contentDescription = null, |
|
||||
tint = Color.White, |
|
||||
modifier = Modifier.size(10.dp) |
|
||||
) |
|
||||
Text( |
|
||||
text = "98%", |
|
||||
color = Color.White, |
|
||||
fontSize = 10.sp, |
|
||||
fontWeight = FontWeight.Bold |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
Spacer(modifier = Modifier.height(4.dp)) |
|
||||
|
|
||||
Row( |
|
||||
verticalAlignment = Alignment.CenterVertically, |
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp) |
|
||||
) { |
|
||||
Icon( |
|
||||
imageVector = Icons.Outlined.LocationOn, |
|
||||
contentDescription = null, |
|
||||
tint = Color.White.copy(alpha = 0.9f), |
|
||||
modifier = Modifier.size(14.dp) |
|
||||
) |
|
||||
Text( |
|
||||
text = "2 miles away • Active recently", |
|
||||
fontSize = 13.sp, |
|
||||
color = Color.White.copy(alpha = 0.9f) |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
Spacer(modifier = Modifier.height(10.dp)) |
|
||||
|
|
||||
// INTEREST CHIPS |
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { |
|
||||
InterestChip(text = "Photography") |
|
||||
InterestChip(text = "Dogs") |
|
||||
} |
|
||||
Spacer(modifier = Modifier.height(6.dp)) |
|
||||
InterestChip(text = "Coffee Shop Lover") |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Composable |
|
||||
private fun InterestChip(text: String) { |
|
||||
Surface( |
|
||||
shape = RoundedCornerShape(16.dp), |
|
||||
color = Color.White.copy(alpha = 0.3f) |
|
||||
) { |
|
||||
Text( |
|
||||
text = text, |
|
||||
color = Color.White, |
|
||||
fontSize = 12.sp, |
|
||||
fontWeight = FontWeight.Medium, |
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 5.dp) |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Preview(showBackground = true, showSystemUi = true) |
|
||||
@Composable |
|
||||
fun MeetDiscoveryScreenPreview() { |
|
||||
DattingAPPTheme { |
|
||||
MeetDiscoveryScreen() |
|
||||
} |
|
||||
} |
|
||||
@ -1,931 +0,0 @@ |
|||||
package com.example.dattingapp |
|
||||
|
|
||||
import android.os.Bundle |
|
||||
import android.widget.Toast |
|
||||
import androidx.activity.ComponentActivity |
|
||||
import androidx.activity.compose.setContent |
|
||||
import androidx.activity.enableEdgeToEdge |
|
||||
import androidx.compose.foundation.Image |
|
||||
import androidx.compose.foundation.background |
|
||||
import androidx.compose.foundation.border |
|
||||
import androidx.compose.foundation.rememberScrollState |
|
||||
import androidx.compose.foundation.verticalScroll |
|
||||
import androidx.compose.foundation.layout.* |
|
||||
import androidx.compose.foundation.shape.CircleShape |
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape |
|
||||
import androidx.compose.material.icons.Icons |
|
||||
import androidx.compose.material.icons.filled.Check |
|
||||
import androidx.compose.material.icons.outlined.Edit |
|
||||
import androidx.compose.material.icons.outlined.GridView |
|
||||
import androidx.compose.material.icons.outlined.PersonPin |
|
||||
import androidx.compose.material.icons.outlined.Settings |
|
||||
import androidx.compose.material3.* |
|
||||
import androidx.compose.runtime.* |
|
||||
import androidx.compose.ui.Alignment |
|
||||
import androidx.compose.ui.Modifier |
|
||||
import androidx.compose.ui.draw.clip |
|
||||
import androidx.compose.ui.graphics.Brush |
|
||||
import androidx.compose.ui.graphics.Color |
|
||||
import androidx.compose.ui.layout.ContentScale |
|
||||
import androidx.compose.ui.res.painterResource |
|
||||
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 com.example.dattingapp.ui.theme.BrandMagenta |
|
||||
import com.example.dattingapp.ui.theme.BrandPurple |
|
||||
import com.example.dattingapp.ui.theme.DattingAPPTheme |
|
||||
import com.example.dattingapp.ui.theme.textDark |
|
||||
import com.example.dattingapp.ui.theme.TextMuted |
|
||||
|
|
||||
|
|
||||
// ============================================================ |
|
||||
// MY PROFILE ACTIVITY |
|
||||
// ============================================================ |
|
||||
|
|
||||
class MyProfile : ComponentActivity() { |
|
||||
|
|
||||
override fun onCreate(savedInstanceState: Bundle?) { |
|
||||
|
|
||||
super.onCreate(savedInstanceState) |
|
||||
|
|
||||
enableEdgeToEdge() |
|
||||
|
|
||||
setContent { |
|
||||
|
|
||||
DattingAPPTheme { |
|
||||
|
|
||||
MyProfileScreen( |
|
||||
|
|
||||
onEditClick = { |
|
||||
|
|
||||
Toast.makeText( |
|
||||
this, |
|
||||
"Edit Profile Clicked", |
|
||||
Toast.LENGTH_SHORT |
|
||||
).show() |
|
||||
|
|
||||
// TODO: |
|
||||
// startActivity( |
|
||||
// Intent( |
|
||||
// this, |
|
||||
// EditProfileActivity::class.java |
|
||||
// ) |
|
||||
// ) |
|
||||
}, |
|
||||
|
|
||||
onUpgradeClick = { |
|
||||
|
|
||||
Toast.makeText( |
|
||||
this, |
|
||||
"Upgrade to Premium Clicked", |
|
||||
Toast.LENGTH_SHORT |
|
||||
).show() |
|
||||
|
|
||||
}, |
|
||||
|
|
||||
onSettingsClick = { |
|
||||
|
|
||||
Toast.makeText( |
|
||||
this, |
|
||||
"Settings Clicked", |
|
||||
Toast.LENGTH_SHORT |
|
||||
).show() |
|
||||
|
|
||||
// TODO: |
|
||||
// startActivity( |
|
||||
// Intent( |
|
||||
// this, |
|
||||
// SettingsActivity::class.java |
|
||||
// ) |
|
||||
// ) |
|
||||
} |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
// ============================================================ |
|
||||
// MY PROFILE SCREEN |
|
||||
// ============================================================ |
|
||||
|
|
||||
@Composable |
|
||||
fun MyProfileScreen( |
|
||||
|
|
||||
onEditClick: () -> Unit = {}, |
|
||||
|
|
||||
onUpgradeClick: () -> Unit = {}, |
|
||||
|
|
||||
onSettingsClick: () -> Unit = {} |
|
||||
|
|
||||
) { |
|
||||
|
|
||||
var selectedTab by remember { |
|
||||
|
|
||||
mutableIntStateOf(0) |
|
||||
} |
|
||||
|
|
||||
|
|
||||
// ======================================================== |
|
||||
// FULL SCREEN SCROLL |
|
||||
// ======================================================== |
|
||||
|
|
||||
val scrollState = |
|
||||
rememberScrollState() |
|
||||
|
|
||||
|
|
||||
Column( |
|
||||
|
|
||||
modifier = Modifier |
|
||||
.fillMaxSize() |
|
||||
.background(Color.White) |
|
||||
.verticalScroll(scrollState) |
|
||||
) { |
|
||||
|
|
||||
|
|
||||
// ==================================================== |
|
||||
// HEADER |
|
||||
// ==================================================== |
|
||||
|
|
||||
Box( |
|
||||
|
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.height(260.dp) |
|
||||
) { |
|
||||
|
|
||||
|
|
||||
// ================================================= |
|
||||
// COVER IMAGE |
|
||||
// ================================================= |
|
||||
|
|
||||
Image( |
|
||||
|
|
||||
painter = |
|
||||
painterResource( |
|
||||
id = |
|
||||
R.drawable.locationpermissionbg |
|
||||
), |
|
||||
|
|
||||
contentDescription = |
|
||||
"Cover Banner", |
|
||||
|
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.height(200.dp), |
|
||||
|
|
||||
contentScale = |
|
||||
ContentScale.Crop |
|
||||
) |
|
||||
|
|
||||
|
|
||||
// ================================================= |
|
||||
// SETTINGS BUTTON |
|
||||
// ================================================= |
|
||||
|
|
||||
IconButton( |
|
||||
|
|
||||
onClick = |
|
||||
onSettingsClick, |
|
||||
|
|
||||
modifier = Modifier |
|
||||
.align( |
|
||||
Alignment.TopEnd |
|
||||
) |
|
||||
.padding( |
|
||||
top = 16.dp, |
|
||||
end = 16.dp |
|
||||
) |
|
||||
.size(36.dp) |
|
||||
.clip( |
|
||||
CircleShape |
|
||||
) |
|
||||
.background( |
|
||||
Color.Black.copy( |
|
||||
alpha = 0.3f |
|
||||
) |
|
||||
) |
|
||||
) { |
|
||||
|
|
||||
Icon( |
|
||||
|
|
||||
imageVector = |
|
||||
Icons.Outlined.Settings, |
|
||||
|
|
||||
contentDescription = |
|
||||
"Settings", |
|
||||
|
|
||||
tint = |
|
||||
Color.White, |
|
||||
|
|
||||
modifier = |
|
||||
Modifier.size(20.dp) |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
|
|
||||
// ================================================= |
|
||||
// PROFILE IMAGE |
|
||||
// ================================================= |
|
||||
|
|
||||
Box( |
|
||||
|
|
||||
modifier = Modifier |
|
||||
.align( |
|
||||
Alignment.BottomStart |
|
||||
) |
|
||||
.padding( |
|
||||
start = 24.dp |
|
||||
) |
|
||||
.size(110.dp) |
|
||||
) { |
|
||||
|
|
||||
|
|
||||
Image( |
|
||||
|
|
||||
painter = |
|
||||
painterResource( |
|
||||
id = |
|
||||
R.drawable.locationpermissionbg |
|
||||
), |
|
||||
|
|
||||
contentDescription = |
|
||||
"Profile Picture", |
|
||||
|
|
||||
contentScale = |
|
||||
ContentScale.Crop, |
|
||||
|
|
||||
modifier = Modifier |
|
||||
.fillMaxSize() |
|
||||
.clip( |
|
||||
CircleShape |
|
||||
) |
|
||||
.border( |
|
||||
width = 4.dp, |
|
||||
color = Color.White, |
|
||||
shape = CircleShape |
|
||||
) |
|
||||
) |
|
||||
|
|
||||
|
|
||||
// ================================================= |
|
||||
// VERIFIED BADGE |
|
||||
// ================================================= |
|
||||
|
|
||||
Box( |
|
||||
|
|
||||
modifier = Modifier |
|
||||
.align( |
|
||||
Alignment.BottomEnd |
|
||||
) |
|
||||
.size(28.dp) |
|
||||
.clip( |
|
||||
CircleShape |
|
||||
) |
|
||||
.background( |
|
||||
BrandMagenta |
|
||||
) |
|
||||
.border( |
|
||||
width = 2.dp, |
|
||||
color = Color.White, |
|
||||
shape = CircleShape |
|
||||
), |
|
||||
|
|
||||
contentAlignment = |
|
||||
Alignment.Center |
|
||||
) { |
|
||||
|
|
||||
Icon( |
|
||||
|
|
||||
imageVector = |
|
||||
Icons.Default.Check, |
|
||||
|
|
||||
contentDescription = |
|
||||
"Verified", |
|
||||
|
|
||||
tint = |
|
||||
Color.White, |
|
||||
|
|
||||
modifier = |
|
||||
Modifier.size(16.dp) |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
// ==================================================== |
|
||||
// USER INFORMATION |
|
||||
// ==================================================== |
|
||||
|
|
||||
Column( |
|
||||
|
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.padding( |
|
||||
horizontal = 24.dp, |
|
||||
vertical = 8.dp |
|
||||
) |
|
||||
) { |
|
||||
|
|
||||
|
|
||||
// ================================================= |
|
||||
// NAME + EDIT |
|
||||
// ================================================= |
|
||||
|
|
||||
Row( |
|
||||
|
|
||||
verticalAlignment = |
|
||||
Alignment.CenterVertically, |
|
||||
|
|
||||
horizontalArrangement = |
|
||||
Arrangement.spacedBy(8.dp) |
|
||||
) { |
|
||||
|
|
||||
Text( |
|
||||
|
|
||||
text = |
|
||||
"Alex, 28", |
|
||||
|
|
||||
fontSize = |
|
||||
22.sp, |
|
||||
|
|
||||
fontWeight = |
|
||||
FontWeight.Bold, |
|
||||
|
|
||||
color = |
|
||||
BrandMagenta |
|
||||
) |
|
||||
|
|
||||
|
|
||||
// EDIT BUTTON |
|
||||
|
|
||||
OutlinedButton( |
|
||||
|
|
||||
onClick = |
|
||||
onEditClick, |
|
||||
|
|
||||
modifier = |
|
||||
Modifier.height(30.dp), |
|
||||
|
|
||||
contentPadding = |
|
||||
PaddingValues( |
|
||||
horizontal = 10.dp, |
|
||||
vertical = 0.dp |
|
||||
), |
|
||||
|
|
||||
border = |
|
||||
ButtonDefaults |
|
||||
.outlinedButtonBorder |
|
||||
.copy( |
|
||||
width = 1.dp |
|
||||
) |
|
||||
) { |
|
||||
|
|
||||
Icon( |
|
||||
|
|
||||
imageVector = |
|
||||
Icons.Outlined.Edit, |
|
||||
|
|
||||
contentDescription = |
|
||||
null, |
|
||||
|
|
||||
tint = |
|
||||
BrandMagenta, |
|
||||
|
|
||||
modifier = |
|
||||
Modifier.size(14.dp) |
|
||||
) |
|
||||
|
|
||||
|
|
||||
Spacer( |
|
||||
modifier = |
|
||||
Modifier.width(4.dp) |
|
||||
) |
|
||||
|
|
||||
|
|
||||
Text( |
|
||||
|
|
||||
text = |
|
||||
"Edit", |
|
||||
|
|
||||
fontSize = |
|
||||
12.sp, |
|
||||
|
|
||||
color = |
|
||||
BrandMagenta, |
|
||||
|
|
||||
fontWeight = |
|
||||
FontWeight.Bold |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
Spacer( |
|
||||
modifier = |
|
||||
Modifier.height(4.dp) |
|
||||
) |
|
||||
|
|
||||
|
|
||||
// ================================================= |
|
||||
// BIO |
|
||||
// ================================================= |
|
||||
|
|
||||
Text( |
|
||||
|
|
||||
text = |
|
||||
"Adventure seeker, dog lover, and amateur chef. Let's find a vibe!", |
|
||||
|
|
||||
fontSize = |
|
||||
13.sp, |
|
||||
|
|
||||
color = |
|
||||
TextMuted, |
|
||||
|
|
||||
lineHeight = |
|
||||
18.sp |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
|
|
||||
Spacer( |
|
||||
modifier = |
|
||||
Modifier.height(12.dp) |
|
||||
) |
|
||||
|
|
||||
|
|
||||
// ==================================================== |
|
||||
// PREMIUM BANNER |
|
||||
// ==================================================== |
|
||||
|
|
||||
Box( |
|
||||
|
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.padding( |
|
||||
horizontal = 16.dp |
|
||||
) |
|
||||
.clip( |
|
||||
RoundedCornerShape(16.dp) |
|
||||
) |
|
||||
.background( |
|
||||
|
|
||||
brush = |
|
||||
Brush.horizontalGradient( |
|
||||
|
|
||||
colors = |
|
||||
listOf( |
|
||||
BrandMagenta, |
|
||||
BrandPurple |
|
||||
) |
|
||||
) |
|
||||
) |
|
||||
.padding(16.dp) |
|
||||
) { |
|
||||
|
|
||||
Row( |
|
||||
|
|
||||
modifier = |
|
||||
Modifier.fillMaxWidth(), |
|
||||
|
|
||||
horizontalArrangement = |
|
||||
Arrangement.SpaceBetween, |
|
||||
|
|
||||
verticalAlignment = |
|
||||
Alignment.CenterVertically |
|
||||
) { |
|
||||
|
|
||||
Column { |
|
||||
|
|
||||
Text( |
|
||||
|
|
||||
text = |
|
||||
"VibeCheck Premium", |
|
||||
|
|
||||
color = |
|
||||
Color.White, |
|
||||
|
|
||||
fontSize = |
|
||||
16.sp, |
|
||||
|
|
||||
fontWeight = |
|
||||
FontWeight.Bold |
|
||||
) |
|
||||
|
|
||||
|
|
||||
Spacer( |
|
||||
modifier = |
|
||||
Modifier.height(2.dp) |
|
||||
) |
|
||||
|
|
||||
|
|
||||
Text( |
|
||||
|
|
||||
text = |
|
||||
"Unlock unlimited likes & more", |
|
||||
|
|
||||
color = |
|
||||
Color.White.copy( |
|
||||
alpha = 0.8f |
|
||||
), |
|
||||
|
|
||||
fontSize = |
|
||||
12.sp |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
|
|
||||
// UPGRADE BUTTON |
|
||||
|
|
||||
Button( |
|
||||
|
|
||||
onClick = |
|
||||
onUpgradeClick, |
|
||||
|
|
||||
colors = |
|
||||
ButtonDefaults.buttonColors( |
|
||||
|
|
||||
containerColor = |
|
||||
Color.White |
|
||||
), |
|
||||
|
|
||||
shape = |
|
||||
RoundedCornerShape(20.dp), |
|
||||
|
|
||||
contentPadding = |
|
||||
PaddingValues( |
|
||||
horizontal = 14.dp, |
|
||||
vertical = 4.dp |
|
||||
) |
|
||||
) { |
|
||||
|
|
||||
Text( |
|
||||
|
|
||||
text = |
|
||||
"Upgrade", |
|
||||
|
|
||||
color = |
|
||||
BrandMagenta, |
|
||||
|
|
||||
fontSize = |
|
||||
12.sp, |
|
||||
|
|
||||
fontWeight = |
|
||||
FontWeight.Bold |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
Spacer( |
|
||||
modifier = |
|
||||
Modifier.height(16.dp) |
|
||||
) |
|
||||
|
|
||||
|
|
||||
// ==================================================== |
|
||||
// STATISTICS |
|
||||
// ==================================================== |
|
||||
|
|
||||
Row( |
|
||||
|
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.padding( |
|
||||
horizontal = 16.dp |
|
||||
), |
|
||||
|
|
||||
horizontalArrangement = |
|
||||
Arrangement.spacedBy(8.dp) |
|
||||
) { |
|
||||
|
|
||||
StatCard( |
|
||||
|
|
||||
number = |
|
||||
"24", |
|
||||
|
|
||||
label = |
|
||||
"Likes", |
|
||||
|
|
||||
modifier = |
|
||||
Modifier.weight(1f) |
|
||||
) |
|
||||
|
|
||||
|
|
||||
StatCard( |
|
||||
|
|
||||
number = |
|
||||
"12", |
|
||||
|
|
||||
label = |
|
||||
"Liked", |
|
||||
|
|
||||
modifier = |
|
||||
Modifier.weight(1f) |
|
||||
) |
|
||||
|
|
||||
|
|
||||
StatCard( |
|
||||
|
|
||||
number = |
|
||||
"Top", |
|
||||
|
|
||||
label = |
|
||||
"Match", |
|
||||
|
|
||||
numberColor = |
|
||||
BrandMagenta, |
|
||||
|
|
||||
modifier = |
|
||||
Modifier.weight(1f) |
|
||||
) |
|
||||
|
|
||||
|
|
||||
StatCard( |
|
||||
|
|
||||
number = |
|
||||
"8", |
|
||||
|
|
||||
label = |
|
||||
"Matches", |
|
||||
|
|
||||
modifier = |
|
||||
Modifier.weight(1f) |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
|
|
||||
Spacer( |
|
||||
modifier = |
|
||||
Modifier.height(16.dp) |
|
||||
) |
|
||||
|
|
||||
|
|
||||
// ==================================================== |
|
||||
// TABS |
|
||||
// ==================================================== |
|
||||
|
|
||||
TabRow( |
|
||||
|
|
||||
selectedTabIndex = |
|
||||
selectedTab, |
|
||||
|
|
||||
containerColor = |
|
||||
Color.White, |
|
||||
|
|
||||
contentColor = |
|
||||
BrandMagenta, |
|
||||
|
|
||||
divider = { |
|
||||
|
|
||||
HorizontalDivider( |
|
||||
color = |
|
||||
Color(0xFFEEEEEE) |
|
||||
) |
|
||||
} |
|
||||
) { |
|
||||
|
|
||||
|
|
||||
// ================================================= |
|
||||
// GRID TAB |
|
||||
// ================================================= |
|
||||
|
|
||||
Tab( |
|
||||
|
|
||||
selected = |
|
||||
selectedTab == 0, |
|
||||
|
|
||||
onClick = { |
|
||||
|
|
||||
selectedTab = 0 |
|
||||
}, |
|
||||
|
|
||||
icon = { |
|
||||
|
|
||||
Icon( |
|
||||
|
|
||||
imageVector = |
|
||||
Icons.Outlined.GridView, |
|
||||
|
|
||||
contentDescription = |
|
||||
"Grid View" |
|
||||
) |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
|
|
||||
// ================================================= |
|
||||
// TAGGED TAB |
|
||||
// ================================================= |
|
||||
|
|
||||
Tab( |
|
||||
|
|
||||
selected = |
|
||||
selectedTab == 1, |
|
||||
|
|
||||
onClick = { |
|
||||
|
|
||||
selectedTab = 1 |
|
||||
}, |
|
||||
|
|
||||
icon = { |
|
||||
|
|
||||
Icon( |
|
||||
|
|
||||
imageVector = |
|
||||
Icons.Outlined.PersonPin, |
|
||||
|
|
||||
contentDescription = |
|
||||
"Tagged View" |
|
||||
) |
|
||||
} |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
|
|
||||
// ==================================================== |
|
||||
// MEDIA GRID |
|
||||
// ==================================================== |
|
||||
|
|
||||
ProfileMediaGrid() |
|
||||
|
|
||||
|
|
||||
Spacer( |
|
||||
modifier = |
|
||||
Modifier.height(30.dp) |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
// ============================================================ |
|
||||
// PROFILE MEDIA GRID |
|
||||
// ============================================================ |
|
||||
|
|
||||
@Composable |
|
||||
private fun ProfileMediaGrid() { |
|
||||
|
|
||||
Column( |
|
||||
|
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.padding(2.dp) |
|
||||
) { |
|
||||
|
|
||||
// 6 rows × 3 columns = 18 images |
|
||||
|
|
||||
repeat(6) { rowIndex -> |
|
||||
|
|
||||
Row( |
|
||||
|
|
||||
modifier = |
|
||||
Modifier.fillMaxWidth() |
|
||||
) { |
|
||||
|
|
||||
repeat(3) { columnIndex -> |
|
||||
|
|
||||
val imageIndex = |
|
||||
rowIndex * 3 + |
|
||||
columnIndex |
|
||||
|
|
||||
|
|
||||
Image( |
|
||||
|
|
||||
painter = |
|
||||
painterResource( |
|
||||
id = |
|
||||
R.drawable.locationpermissionbg |
|
||||
), |
|
||||
|
|
||||
contentDescription = |
|
||||
"Profile Image $imageIndex", |
|
||||
|
|
||||
modifier = Modifier |
|
||||
.weight(1f) |
|
||||
.aspectRatio(1f) |
|
||||
.padding(1.dp), |
|
||||
|
|
||||
contentScale = |
|
||||
ContentScale.Crop |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
// ============================================================ |
|
||||
// STAT CARD |
|
||||
// ============================================================ |
|
||||
|
|
||||
@Composable |
|
||||
private fun StatCard( |
|
||||
|
|
||||
number: String, |
|
||||
|
|
||||
label: String, |
|
||||
|
|
||||
modifier: Modifier = Modifier, |
|
||||
|
|
||||
numberColor: Color = textDark |
|
||||
|
|
||||
) { |
|
||||
|
|
||||
Card( |
|
||||
|
|
||||
modifier = |
|
||||
modifier, |
|
||||
|
|
||||
shape = |
|
||||
RoundedCornerShape(12.dp), |
|
||||
|
|
||||
colors = |
|
||||
CardDefaults.cardColors( |
|
||||
|
|
||||
containerColor = |
|
||||
Color(0xFFFAFAFA) |
|
||||
), |
|
||||
|
|
||||
border = |
|
||||
CardDefaults |
|
||||
.outlinedCardBorder() |
|
||||
.copy( |
|
||||
width = 0.5.dp |
|
||||
) |
|
||||
) { |
|
||||
|
|
||||
Column( |
|
||||
|
|
||||
modifier = Modifier |
|
||||
.fillMaxWidth() |
|
||||
.padding( |
|
||||
vertical = 12.dp |
|
||||
), |
|
||||
|
|
||||
horizontalAlignment = |
|
||||
Alignment.CenterHorizontally |
|
||||
) { |
|
||||
|
|
||||
Text( |
|
||||
|
|
||||
text = |
|
||||
number, |
|
||||
|
|
||||
fontSize = |
|
||||
16.sp, |
|
||||
|
|
||||
fontWeight = |
|
||||
FontWeight.Bold, |
|
||||
|
|
||||
color = |
|
||||
numberColor |
|
||||
) |
|
||||
|
|
||||
|
|
||||
Spacer( |
|
||||
modifier = |
|
||||
Modifier.height(2.dp) |
|
||||
) |
|
||||
|
|
||||
|
|
||||
Text( |
|
||||
|
|
||||
text = |
|
||||
label, |
|
||||
|
|
||||
fontSize = |
|
||||
11.sp, |
|
||||
|
|
||||
color = |
|
||||
TextMuted |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
// ============================================================ |
|
||||
// PREVIEW |
|
||||
// ============================================================ |
|
||||
|
|
||||
@Preview( |
|
||||
|
|
||||
showBackground = true, |
|
||||
|
|
||||
showSystemUi = true, |
|
||||
|
|
||||
name = "My Profile" |
|
||||
) |
|
||||
@Composable |
|
||||
fun MyProfileScreenPreview() { |
|
||||
|
|
||||
DattingAPPTheme { |
|
||||
|
|
||||
MyProfileScreen() |
|
||||
} |
|
||||
} |
|
||||
@ -1,4 +1,4 @@ |
|||||
package com.example.dattingapp |
package com.example.dattingapp.homes |
||||
|
|
||||
import android.os.Bundle |
import android.os.Bundle |
||||
import androidx.activity.ComponentActivity |
import androidx.activity.ComponentActivity |
||||
@ -0,0 +1,385 @@ |
|||||
|
package com.example.dattingapp.homes |
||||
|
|
||||
|
import android.util.Log |
||||
|
import androidx.compose.foundation.background |
||||
|
import androidx.compose.foundation.layout.* |
||||
|
import androidx.compose.foundation.shape.CircleShape |
||||
|
import androidx.compose.foundation.shape.RoundedCornerShape |
||||
|
import androidx.compose.material.icons.Icons |
||||
|
import androidx.compose.material.icons.filled.* |
||||
|
import androidx.compose.material.icons.outlined.Close |
||||
|
import androidx.compose.material.icons.outlined.LocationOn |
||||
|
import androidx.compose.material.icons.outlined.Search |
||||
|
import androidx.compose.material3.* |
||||
|
import androidx.compose.runtime.* |
||||
|
import androidx.compose.ui.Alignment |
||||
|
import androidx.compose.ui.Modifier |
||||
|
import androidx.compose.ui.draw.clip |
||||
|
import androidx.compose.ui.draw.shadow |
||||
|
import androidx.compose.ui.graphics.Brush |
||||
|
import androidx.compose.ui.graphics.Color |
||||
|
import androidx.compose.ui.layout.ContentScale |
||||
|
import androidx.compose.ui.platform.LocalContext |
||||
|
import androidx.compose.ui.text.font.FontWeight |
||||
|
import androidx.compose.ui.unit.dp |
||||
|
import androidx.compose.ui.unit.sp |
||||
|
import coil.compose.AsyncImage |
||||
|
import coil.request.ImageRequest |
||||
|
import com.example.dattingapp.homes.networkandapi.MeetUserProfile |
||||
|
import com.example.dattingapp.homes.networkandapi.TokenManager |
||||
|
import com.example.dattingapp.ui.theme.BrandPink |
||||
|
import com.example.dattingapp.ui.theme.BrandPurple |
||||
|
import java.time.LocalDate |
||||
|
import java.time.Period |
||||
|
import java.time.format.DateTimeFormatter |
||||
|
|
||||
|
@Composable |
||||
|
fun MeetDiscoveryScreen( |
||||
|
modifier: Modifier = Modifier, |
||||
|
profiles: List<MeetUserProfile> = emptyList(), |
||||
|
isLoading: Boolean = false, |
||||
|
errorMessage: String? = null, |
||||
|
onAddClick: () -> Unit = {}, |
||||
|
onSearchClick: () -> Unit = {}, |
||||
|
onSuperLikeClick: (MeetUserProfile) -> Unit = {}, |
||||
|
onRejectClick: (MeetUserProfile) -> Unit = {}, |
||||
|
onLikeClick: (MeetUserProfile) -> Unit = {} |
||||
|
) { |
||||
|
// Index tracking current active user profile (starts at 0) |
||||
|
var currentIndex by remember { mutableIntStateOf(0) } |
||||
|
|
||||
|
Box( |
||||
|
modifier = modifier |
||||
|
.fillMaxSize() |
||||
|
.background(Color(0xFFF8F9FA)) |
||||
|
) { |
||||
|
Column( |
||||
|
modifier = Modifier |
||||
|
.fillMaxSize() |
||||
|
.padding(horizontal = 16.dp), |
||||
|
horizontalAlignment = Alignment.CenterHorizontally |
||||
|
) { |
||||
|
// TOP BAR |
||||
|
Row( |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.padding(top = 12.dp, bottom = 12.dp), |
||||
|
horizontalArrangement = Arrangement.SpaceBetween, |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
IconButton( |
||||
|
onClick = onAddClick, |
||||
|
modifier = Modifier |
||||
|
.size(38.dp) |
||||
|
.clip(CircleShape) |
||||
|
.background(Color(0xFFEBEBEB)) |
||||
|
) { |
||||
|
Icon( |
||||
|
imageVector = Icons.Default.Add, |
||||
|
contentDescription = "Add", |
||||
|
tint = BrandPink, |
||||
|
modifier = Modifier.size(20.dp) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
Text( |
||||
|
text = "VibeCheck", |
||||
|
fontSize = 24.sp, |
||||
|
fontWeight = FontWeight.Bold, |
||||
|
color = BrandPurple |
||||
|
) |
||||
|
|
||||
|
IconButton(onClick = onSearchClick) { |
||||
|
Icon( |
||||
|
imageVector = Icons.Outlined.Search, |
||||
|
contentDescription = "Search", |
||||
|
tint = BrandPink, |
||||
|
modifier = Modifier.size(26.dp) |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MAIN CONTENT CARD |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.weight(1f) |
||||
|
.padding(bottom = 16.dp), |
||||
|
contentAlignment = Alignment.Center |
||||
|
) { |
||||
|
when { |
||||
|
isLoading -> { |
||||
|
CircularProgressIndicator(color = BrandPink) |
||||
|
} |
||||
|
|
||||
|
errorMessage != null -> { |
||||
|
Text( |
||||
|
text = errorMessage, |
||||
|
color = MaterialTheme.colorScheme.error, |
||||
|
fontSize = 16.sp |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
profiles.isNotEmpty() && currentIndex < profiles.size -> { |
||||
|
// Current profile starting at index 0 |
||||
|
val currentProfile = profiles[currentIndex] |
||||
|
|
||||
|
ProfileCardItem( |
||||
|
profile = currentProfile, |
||||
|
onSuperLike = { |
||||
|
onSuperLikeClick(currentProfile) |
||||
|
currentIndex++ |
||||
|
}, |
||||
|
onReject = { |
||||
|
onRejectClick(currentProfile) |
||||
|
currentIndex++ |
||||
|
}, |
||||
|
onLike = { |
||||
|
onLikeClick(currentProfile) |
||||
|
currentIndex++ |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
else -> { |
||||
|
Text( |
||||
|
text = "No more profiles available!", |
||||
|
fontSize = 16.sp, |
||||
|
color = Color.Gray |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Composable |
||||
|
private fun ProfileCardItem( |
||||
|
profile: MeetUserProfile, |
||||
|
onSuperLike: () -> Unit, |
||||
|
onReject: () -> Unit, |
||||
|
onLike: () -> Unit |
||||
|
) { |
||||
|
// Extract primary photo URL from media list |
||||
|
val photoUrl = remember(profile) { |
||||
|
profile.media |
||||
|
?.firstOrNull { it.mediaType.equals("image", ignoreCase = true) || it.url != null } |
||||
|
?.fullMediaUrl |
||||
|
} |
||||
|
|
||||
|
Log.d("photourl", "$photoUrl") |
||||
|
// Helper to calculate age from birth_date string (formatted e.g. "YYYY-MM-DD") |
||||
|
val age = remember(profile.birthDate) { |
||||
|
calculateAge(profile.birthDate) |
||||
|
} |
||||
|
|
||||
|
// Location display formatting |
||||
|
val locationText = remember(profile) { |
||||
|
val loc = listOfNotNull(profile.city, profile.country).joinToString(", ") |
||||
|
val dist = profile.distanceKm?.let { "${it.toInt()} km away" } |
||||
|
listOfNotNull(dist, loc.takeIf { it.isNotBlank() }).joinToString(" • ") |
||||
|
} |
||||
|
|
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.fillMaxSize() |
||||
|
.clip(RoundedCornerShape(20.dp)) |
||||
|
) { |
||||
|
// BACKEND PROFILE IMAGE |
||||
|
val context = LocalContext.current |
||||
|
val token = remember(context) { |
||||
|
TokenManager.getInstance(context).getAccessToken() |
||||
|
} |
||||
|
|
||||
|
AsyncImage( |
||||
|
model = ImageRequest.Builder(context) |
||||
|
.data(photoUrl) |
||||
|
.apply { |
||||
|
if (!token.isNullOrBlank()) { |
||||
|
// Formatting matches "Bearer <token>" or raw token based on backend preference |
||||
|
val authHeader = if (token.startsWith("Bearer ", ignoreCase = true)) token else "Bearer $token" |
||||
|
addHeader("Authorization", authHeader) |
||||
|
} |
||||
|
} |
||||
|
.crossfade(true) |
||||
|
.build(), |
||||
|
contentDescription = "Profile Image", |
||||
|
modifier = Modifier.fillMaxSize(), |
||||
|
contentScale = ContentScale.Crop, |
||||
|
onError = { errorState -> |
||||
|
Log.e("CoilStatus", "Failed to load image", errorState.result.throwable) |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
// DARK GRADIENT OVERLAY |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.fillMaxSize() |
||||
|
.background( |
||||
|
Brush.verticalGradient( |
||||
|
colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.85f)), |
||||
|
startY = 350f |
||||
|
) |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
// ACTION BUTTONS (Right Side) |
||||
|
Column( |
||||
|
modifier = Modifier |
||||
|
.align(Alignment.BottomEnd) |
||||
|
.padding(end = 14.dp, bottom = 20.dp), |
||||
|
verticalArrangement = Arrangement.spacedBy(10.dp), |
||||
|
horizontalAlignment = Alignment.CenterHorizontally |
||||
|
) { |
||||
|
// Super Like (Star) |
||||
|
IconButton( |
||||
|
onClick = onSuperLike, |
||||
|
modifier = Modifier |
||||
|
.size(50.dp) |
||||
|
.shadow(6.dp, CircleShape) |
||||
|
.clip(CircleShape) |
||||
|
.background(Color.White) |
||||
|
) { |
||||
|
Icon( |
||||
|
imageVector = Icons.Default.Star, |
||||
|
contentDescription = "Super Like", |
||||
|
tint = BrandPurple, |
||||
|
modifier = Modifier.size(24.dp) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// Reject (X) |
||||
|
IconButton( |
||||
|
onClick = onReject, |
||||
|
modifier = Modifier |
||||
|
.size(54.dp) |
||||
|
.shadow(6.dp, CircleShape) |
||||
|
.clip(CircleShape) |
||||
|
.background(Color.White) |
||||
|
) { |
||||
|
Icon( |
||||
|
imageVector = Icons.Outlined.Close, |
||||
|
contentDescription = "Reject", |
||||
|
tint = Color(0xFF555555), |
||||
|
modifier = Modifier.size(26.dp) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// Like (Heart) |
||||
|
IconButton( |
||||
|
onClick = onLike, |
||||
|
modifier = Modifier |
||||
|
.size(60.dp) |
||||
|
.shadow(8.dp, CircleShape) |
||||
|
.clip(CircleShape) |
||||
|
.background(BrandPink) |
||||
|
) { |
||||
|
Icon( |
||||
|
imageVector = Icons.Default.Favorite, |
||||
|
contentDescription = "Like", |
||||
|
tint = Color.White, |
||||
|
modifier = Modifier.size(30.dp) |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// PROFILE DETAILS (Bottom Left) |
||||
|
Column( |
||||
|
modifier = Modifier |
||||
|
.align(Alignment.BottomStart) |
||||
|
.padding(16.dp) |
||||
|
.fillMaxWidth(0.70f) |
||||
|
) { |
||||
|
Row( |
||||
|
verticalAlignment = Alignment.CenterVertically, |
||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp) |
||||
|
) { |
||||
|
// Name & Age |
||||
|
val displayName = profile.fullName ?: "User" |
||||
|
val nameAndAge = if (age != null) "$displayName, $age" else displayName |
||||
|
|
||||
|
Text( |
||||
|
text = nameAndAge, |
||||
|
fontSize = 26.sp, |
||||
|
fontWeight = FontWeight.Bold, |
||||
|
color = Color.White |
||||
|
) |
||||
|
|
||||
|
// Compatibility / Match Percentage Badge |
||||
|
profile.compatibilityPercentage?.let { match -> |
||||
|
Surface( |
||||
|
shape = RoundedCornerShape(10.dp), |
||||
|
color = BrandPink |
||||
|
) { |
||||
|
Row( |
||||
|
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), |
||||
|
verticalAlignment = Alignment.CenterVertically, |
||||
|
horizontalArrangement = Arrangement.spacedBy(2.dp) |
||||
|
) { |
||||
|
Icon( |
||||
|
imageVector = Icons.Default.Verified, |
||||
|
contentDescription = null, |
||||
|
tint = Color.White, |
||||
|
modifier = Modifier.size(10.dp) |
||||
|
) |
||||
|
Text( |
||||
|
text = "$match%", |
||||
|
color = Color.White, |
||||
|
fontSize = 10.sp, |
||||
|
fontWeight = FontWeight.Bold |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Spacer(modifier = Modifier.height(4.dp)) |
||||
|
|
||||
|
// Location & Distance |
||||
|
if (locationText.isNotBlank()) { |
||||
|
Row( |
||||
|
verticalAlignment = Alignment.CenterVertically, |
||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp) |
||||
|
) { |
||||
|
Icon( |
||||
|
imageVector = Icons.Outlined.LocationOn, |
||||
|
contentDescription = null, |
||||
|
tint = Color.White.copy(alpha = 0.9f), |
||||
|
modifier = Modifier.size(14.dp) |
||||
|
) |
||||
|
Text( |
||||
|
text = locationText, |
||||
|
fontSize = 13.sp, |
||||
|
color = Color.White.copy(alpha = 0.9f) |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Bio |
||||
|
profile.bio?.takeIf { it.isNotBlank() }?.let { bio -> |
||||
|
Spacer(modifier = Modifier.height(8.dp)) |
||||
|
Text( |
||||
|
text = bio, |
||||
|
fontSize = 12.sp, |
||||
|
color = Color.White.copy(alpha = 0.85f), |
||||
|
maxLines = 2 |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Utility to calculate age from date string (e.g. "1999-05-15") |
||||
|
private fun calculateAge(birthDateStr: String?): Int? { |
||||
|
if (birthDateStr.isNull_or_blank()) 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 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private fun String?.isNull_or_blank(): Boolean = this == null || this.trim().isEmpty() |
||||
@ -0,0 +1,445 @@ |
|||||
|
package com.example.dattingapp.homes |
||||
|
|
||||
|
import android.os.Bundle |
||||
|
import android.widget.Toast |
||||
|
import androidx.activity.ComponentActivity |
||||
|
import androidx.activity.compose.setContent |
||||
|
import androidx.activity.enableEdgeToEdge |
||||
|
import androidx.compose.foundation.Image |
||||
|
import androidx.compose.foundation.background |
||||
|
import androidx.compose.foundation.border |
||||
|
import androidx.compose.foundation.rememberScrollState |
||||
|
import androidx.compose.foundation.verticalScroll |
||||
|
import androidx.compose.foundation.layout.* |
||||
|
import androidx.compose.foundation.shape.CircleShape |
||||
|
import androidx.compose.foundation.shape.RoundedCornerShape |
||||
|
import androidx.compose.material.icons.Icons |
||||
|
import androidx.compose.material.icons.filled.Check |
||||
|
import androidx.compose.material.icons.outlined.Edit |
||||
|
import androidx.compose.material.icons.outlined.GridView |
||||
|
import androidx.compose.material.icons.outlined.PersonPin |
||||
|
import androidx.compose.material.icons.outlined.Settings |
||||
|
import androidx.compose.material3.* |
||||
|
import androidx.compose.runtime.* |
||||
|
import androidx.compose.ui.Alignment |
||||
|
import androidx.compose.ui.Modifier |
||||
|
import androidx.compose.ui.draw.clip |
||||
|
import androidx.compose.ui.graphics.Brush |
||||
|
import androidx.compose.ui.graphics.Color |
||||
|
import androidx.compose.ui.layout.ContentScale |
||||
|
import androidx.compose.ui.res.painterResource |
||||
|
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 |
||||
|
import com.example.dattingapp.R |
||||
|
import com.example.dattingapp.homes.networkandapi.MyProfileResponse |
||||
|
import com.example.dattingapp.ui.theme.BrandMagenta |
||||
|
import com.example.dattingapp.ui.theme.BrandPurple |
||||
|
import com.example.dattingapp.ui.theme.DattingAPPTheme |
||||
|
import com.example.dattingapp.ui.theme.textDark |
||||
|
import com.example.dattingapp.ui.theme.TextMuted |
||||
|
|
||||
|
// ============================================================ |
||||
|
// MY PROFILE SCREEN |
||||
|
// ============================================================ |
||||
|
|
||||
|
@Composable |
||||
|
fun MyProfileScreen( |
||||
|
|
||||
|
profile: MyProfileResponse?, // Change from MyProfileData? to MyProfileResponse? |
||||
|
isLoading: Boolean, |
||||
|
errorMessage: String?, |
||||
|
onRetry: () -> Unit = {}, |
||||
|
onEditClick: () -> Unit = {}, |
||||
|
onUpgradeClick: () -> Unit = {}, |
||||
|
onSettingsClick: () -> Unit = {} |
||||
|
|
||||
|
) { |
||||
|
|
||||
|
|
||||
|
// ======================================================== |
||||
|
// FULL SCREEN SCROLL |
||||
|
// ======================================================== |
||||
|
|
||||
|
val scrollState = rememberScrollState() |
||||
|
|
||||
|
|
||||
|
Column( |
||||
|
|
||||
|
modifier = Modifier.fillMaxSize().background(Color.White).verticalScroll(scrollState)) { |
||||
|
|
||||
|
|
||||
|
// ==================================================== |
||||
|
// HEADER |
||||
|
// ==================================================== |
||||
|
|
||||
|
Box( |
||||
|
|
||||
|
modifier = Modifier.fillMaxWidth().height(260.dp) |
||||
|
) { |
||||
|
// ================================================= |
||||
|
// COVER IMAGE |
||||
|
// ================================================= |
||||
|
|
||||
|
Image( |
||||
|
painter = painterResource( |
||||
|
id = R.drawable.locationpermissionbg |
||||
|
), |
||||
|
|
||||
|
contentDescription = "Cover Banner", |
||||
|
|
||||
|
modifier = Modifier.fillMaxWidth().height(200.dp), |
||||
|
contentScale = ContentScale.Crop |
||||
|
) |
||||
|
// ================================================= |
||||
|
// SETTINGS BUTTON |
||||
|
// ================================================= |
||||
|
|
||||
|
IconButton( |
||||
|
onClick = onSettingsClick, |
||||
|
modifier = Modifier.align(Alignment.TopEnd) |
||||
|
.padding(top = 16.dp, end = 16.dp) |
||||
|
.size(36.dp) |
||||
|
.clip(CircleShape) |
||||
|
.background(Color.Black.copy(alpha = 0.3f) |
||||
|
) |
||||
|
) { |
||||
|
|
||||
|
Icon( |
||||
|
|
||||
|
imageVector = Icons.Outlined.Settings, |
||||
|
|
||||
|
contentDescription = "Settings", |
||||
|
tint = Color.White, |
||||
|
modifier = Modifier.size(20.dp) |
||||
|
) |
||||
|
} |
||||
|
// ================================================= |
||||
|
// PROFILE IMAGE |
||||
|
// ================================================= |
||||
|
Box( |
||||
|
modifier = Modifier.align(Alignment.BottomStart) |
||||
|
.padding(start = 24.dp) |
||||
|
.size(110.dp) |
||||
|
) { |
||||
|
|
||||
|
|
||||
|
AsyncImage( |
||||
|
// Access nested inner 'profile' first, then 'profileImageUrl' |
||||
|
model = profile?.profile?.profileImageUrl, |
||||
|
contentDescription = "Profile Picture", |
||||
|
contentScale = ContentScale.Crop, |
||||
|
placeholder = painterResource(id = R.drawable.locationpermissionbg), |
||||
|
error = painterResource(id = R.drawable.locationpermissionbg), |
||||
|
modifier = Modifier |
||||
|
.fillMaxSize() |
||||
|
.clip(CircleShape) |
||||
|
.border(width = 4.dp, color = Color.White, shape = CircleShape) |
||||
|
) |
||||
|
|
||||
|
// ================================================= |
||||
|
// VERIFIED BADGE |
||||
|
// ================================================= |
||||
|
Box( |
||||
|
modifier = Modifier.align(Alignment.BottomEnd).size(28.dp).clip(CircleShape) |
||||
|
.background(BrandMagenta) |
||||
|
.border(width = 2.dp, color = Color.White, shape = CircleShape |
||||
|
), |
||||
|
|
||||
|
contentAlignment = Alignment.Center |
||||
|
) { |
||||
|
Icon( |
||||
|
imageVector = Icons.Default.Check, |
||||
|
contentDescription = "Verified", |
||||
|
tint = Color.White, |
||||
|
modifier = Modifier.size(16.dp) |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
// ==================================================== |
||||
|
// USER INFORMATION |
||||
|
// ==================================================== |
||||
|
|
||||
|
Column( |
||||
|
|
||||
|
modifier = Modifier.fillMaxWidth() |
||||
|
.padding(horizontal = 24.dp, vertical = 8.dp) |
||||
|
) { |
||||
|
// ================================================= |
||||
|
// NAME + EDIT |
||||
|
// ================================================= |
||||
|
|
||||
|
Row( |
||||
|
|
||||
|
verticalAlignment = Alignment.CenterVertically, |
||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp) |
||||
|
) { |
||||
|
|
||||
|
Text( |
||||
|
text = "Alex, 28", |
||||
|
fontSize = 22.sp, |
||||
|
fontWeight = FontWeight.Bold, |
||||
|
color = BrandMagenta |
||||
|
) |
||||
|
|
||||
|
|
||||
|
// EDIT BUTTON |
||||
|
|
||||
|
OutlinedButton( |
||||
|
onClick = onEditClick, |
||||
|
modifier = Modifier.height(30.dp), |
||||
|
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp), |
||||
|
border = ButtonDefaults |
||||
|
.outlinedButtonBorder |
||||
|
.copy(width = 1.dp) |
||||
|
) { |
||||
|
|
||||
|
Icon( |
||||
|
imageVector = Icons.Outlined.Edit, |
||||
|
contentDescription = null, |
||||
|
tint = BrandMagenta, |
||||
|
modifier = Modifier.size(14.dp) |
||||
|
) |
||||
|
|
||||
|
Spacer(modifier = Modifier.width(4.dp)) |
||||
|
|
||||
|
Text( |
||||
|
text = "Edit", |
||||
|
fontSize = 12.sp, |
||||
|
color = BrandMagenta, |
||||
|
fontWeight = FontWeight.Bold |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Spacer(modifier = Modifier.height(4.dp)) |
||||
|
// ================================================= |
||||
|
// BIO |
||||
|
// ================================================= |
||||
|
|
||||
|
Text( |
||||
|
|
||||
|
text = "Adventure seeker, dog lover, and amateur chef. Let's find a vibe!", |
||||
|
fontSize = 13.sp, |
||||
|
color = TextMuted, |
||||
|
lineHeight = 18.sp |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
Spacer( |
||||
|
modifier = Modifier.height(12.dp) |
||||
|
) |
||||
|
// ==================================================== |
||||
|
// PREMIUM BANNER |
||||
|
// ==================================================== |
||||
|
Box( |
||||
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp) |
||||
|
.clip(RoundedCornerShape(16.dp)) |
||||
|
.background( |
||||
|
brush = Brush.horizontalGradient( |
||||
|
colors = listOf(BrandMagenta, BrandPurple) |
||||
|
) |
||||
|
) |
||||
|
.padding(16.dp) |
||||
|
) { |
||||
|
Row( |
||||
|
modifier = Modifier.fillMaxWidth(), |
||||
|
horizontalArrangement = Arrangement.SpaceBetween, |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
Column { |
||||
|
|
||||
|
Text( |
||||
|
text = "VibeCheck Premium", |
||||
|
color = Color.White, |
||||
|
fontSize = 16.sp, |
||||
|
fontWeight = FontWeight.Bold |
||||
|
) |
||||
|
|
||||
|
Spacer( |
||||
|
modifier = Modifier.height(2.dp) |
||||
|
) |
||||
|
|
||||
|
Text( |
||||
|
|
||||
|
text = "Unlock unlimited likes & more", |
||||
|
color = Color.White.copy(alpha = 0.8f), |
||||
|
fontSize = 12.sp |
||||
|
) |
||||
|
} |
||||
|
// UPGRADE BUTTON |
||||
|
|
||||
|
Button( |
||||
|
|
||||
|
onClick = onUpgradeClick, |
||||
|
colors = ButtonDefaults.buttonColors( |
||||
|
containerColor = Color.White |
||||
|
), |
||||
|
shape = RoundedCornerShape(20.dp), |
||||
|
contentPadding = PaddingValues( |
||||
|
horizontal = 14.dp, |
||||
|
vertical = 4.dp |
||||
|
) |
||||
|
) { |
||||
|
|
||||
|
Text( |
||||
|
|
||||
|
text = "Upgrade", |
||||
|
color = BrandMagenta, |
||||
|
fontSize = 12.sp, |
||||
|
fontWeight = FontWeight.Bold |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
Spacer( |
||||
|
modifier = Modifier.height(16.dp) |
||||
|
) |
||||
|
|
||||
|
// ==================================================== |
||||
|
// STATISTICS |
||||
|
// ==================================================== |
||||
|
|
||||
|
Row( |
||||
|
|
||||
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), |
||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp) |
||||
|
) { |
||||
|
|
||||
|
StatCard( |
||||
|
number = "24", |
||||
|
label = "Likes", |
||||
|
modifier = Modifier.weight(1f) |
||||
|
) |
||||
|
|
||||
|
StatCard( |
||||
|
|
||||
|
number = "12", |
||||
|
label = "Liked", |
||||
|
modifier = Modifier.weight(1f) |
||||
|
) |
||||
|
|
||||
|
|
||||
|
StatCard( |
||||
|
number = "Top", |
||||
|
label = "Match", |
||||
|
numberColor = BrandMagenta, |
||||
|
modifier = Modifier.weight(1f) |
||||
|
) |
||||
|
StatCard( |
||||
|
number = "8", |
||||
|
label = "Matches", |
||||
|
modifier = Modifier.weight(1f) |
||||
|
) |
||||
|
} |
||||
|
Spacer( |
||||
|
modifier = Modifier.height(16.dp) |
||||
|
) |
||||
|
|
||||
|
ProfileMediaGrid() |
||||
|
|
||||
|
Spacer( |
||||
|
modifier = Modifier.height(30.dp) |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
// ============================================================ |
||||
|
// PROFILE MEDIA GRID |
||||
|
// ============================================================ |
||||
|
|
||||
|
@Composable |
||||
|
private fun ProfileMediaGrid() { |
||||
|
|
||||
|
Column( |
||||
|
|
||||
|
modifier = Modifier.fillMaxWidth().padding(2.dp) |
||||
|
) { |
||||
|
// 6 rows × 3 columns = 18 images |
||||
|
|
||||
|
repeat(6) { rowIndex -> |
||||
|
|
||||
|
Row( |
||||
|
modifier = Modifier.fillMaxWidth() |
||||
|
) { |
||||
|
|
||||
|
repeat(3) { columnIndex -> |
||||
|
|
||||
|
val imageIndex = rowIndex * 3 + columnIndex |
||||
|
|
||||
|
|
||||
|
Image( |
||||
|
painter = painterResource( |
||||
|
id = R.drawable.locationpermissionbg |
||||
|
), |
||||
|
|
||||
|
contentDescription = "Profile Image $imageIndex", |
||||
|
modifier = Modifier.weight(1f).aspectRatio(1f).padding(1.dp), |
||||
|
contentScale = ContentScale.Crop |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
// ============================================================ |
||||
|
// STAT CARD |
||||
|
// ============================================================ |
||||
|
|
||||
|
@Composable |
||||
|
private fun StatCard( |
||||
|
number: String, |
||||
|
label: String, |
||||
|
modifier: Modifier = Modifier, |
||||
|
numberColor: Color = textDark |
||||
|
|
||||
|
) { |
||||
|
|
||||
|
Card( |
||||
|
modifier = modifier, |
||||
|
shape = RoundedCornerShape(12.dp), |
||||
|
|
||||
|
colors = CardDefaults.cardColors( |
||||
|
containerColor = Color(0xFFFAFAFA) |
||||
|
), |
||||
|
|
||||
|
border = |
||||
|
CardDefaults.outlinedCardBorder() |
||||
|
.copy(width = 0.5.dp) |
||||
|
) { |
||||
|
|
||||
|
Column( |
||||
|
modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp), |
||||
|
horizontalAlignment = Alignment.CenterHorizontally |
||||
|
) { |
||||
|
Text( |
||||
|
text = number, |
||||
|
fontSize = 16.sp, |
||||
|
fontWeight = FontWeight.Bold, |
||||
|
color = numberColor |
||||
|
) |
||||
|
Spacer( |
||||
|
modifier = Modifier.height(2.dp) |
||||
|
) |
||||
|
|
||||
|
Text( |
||||
|
text = label, |
||||
|
fontSize = 11.sp, |
||||
|
color = TextMuted |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// ============================================================ |
||||
|
// PREVIEW |
||||
|
// ============================================================ |
||||
@ -0,0 +1,17 @@ |
|||||
|
package com.example.dattingapp.homes.networkandapi |
||||
|
|
||||
|
import retrofit2.Response |
||||
|
import retrofit2.http.GET |
||||
|
import retrofit2.http.Query |
||||
|
|
||||
|
interface HomeApiService { |
||||
|
|
||||
|
@GET("api/v1/meet") |
||||
|
suspend fun getMeetProfiles( |
||||
|
@Query("limit") limit: Int = 20 |
||||
|
): Response<List<MeetUserProfile>> |
||||
|
|
||||
|
@GET("api/v1/users/me") |
||||
|
suspend fun getCurrentUserProfile(): Response<MyProfileResponse> |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,93 @@ |
|||||
|
package com.example.dattingapp.homes.networkandapi |
||||
|
|
||||
|
import com.google.gson.annotations.SerializedName |
||||
|
|
||||
|
private const val MEDIA_BASE_URL = "https://melove.youthome.com.cn" |
||||
|
|
||||
|
private fun resolveMediaUrl(value: String?): String? { |
||||
|
val path = value?.trim()?.takeIf { it.isNotBlank() } ?: return null |
||||
|
|
||||
|
return when { |
||||
|
path.startsWith("https://", ignoreCase = true) -> path |
||||
|
path.startsWith("http://", ignoreCase = true) -> path // (Note: original code duplicated https:// twice) |
||||
|
path.startsWith("//") -> "https:$path" |
||||
|
else -> "$MEDIA_BASE_URL/${path.trimStart('/')}" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
data class MeetUserProfile( |
||||
|
@SerializedName("id") |
||||
|
val id: Int, |
||||
|
|
||||
|
@SerializedName("full_name") |
||||
|
val fullName: String?, |
||||
|
|
||||
|
@SerializedName("gender") |
||||
|
val gender: String?, |
||||
|
|
||||
|
@SerializedName("birth_date") |
||||
|
val birthDate: String?, |
||||
|
|
||||
|
@SerializedName("city") |
||||
|
val city: String?, |
||||
|
|
||||
|
@SerializedName("country") |
||||
|
val country: String?, |
||||
|
|
||||
|
@SerializedName("bio") |
||||
|
val bio: String?, |
||||
|
|
||||
|
@SerializedName("video_url") |
||||
|
val videoUrl: String?, |
||||
|
|
||||
|
@SerializedName("distance_km") |
||||
|
val distanceKm: Double?, |
||||
|
|
||||
|
@SerializedName("compatibility_percentage") |
||||
|
val compatibilityPercentage: Int?, |
||||
|
|
||||
|
@SerializedName("media") |
||||
|
val media: List<MediaItem>? |
||||
|
) { |
||||
|
val fullVideoUrl: String? |
||||
|
get() = resolveMediaUrl(videoUrl) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* Standard API wrapper format for MyProfile |
||||
|
*/ |
||||
|
data class MyProfileResponse( |
||||
|
@SerializedName("id") val id: Int?, |
||||
|
@SerializedName("email_or_phone") val emailOrPhone: String?, |
||||
|
@SerializedName("status") val status: String?, |
||||
|
@SerializedName("profile") val profile: UserProfileDetails?, // Nested profile details |
||||
|
@SerializedName("created_at") val createdAt: String? |
||||
|
) |
||||
|
|
||||
|
data class UserProfileDetails( |
||||
|
@SerializedName("full_name") val fullName: String?, |
||||
|
@SerializedName("gender") val gender: String?, |
||||
|
@SerializedName("city") val city: String?, |
||||
|
@SerializedName("country") val country: String?, |
||||
|
@SerializedName("bio") val bio: String?, |
||||
|
@SerializedName("profile_image_url") val profileImageUrl: String?, // Resolved field |
||||
|
@SerializedName("video_url") val videoUrl: String? |
||||
|
) |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
data class MediaItem( |
||||
|
@SerializedName("id") |
||||
|
val id: Int, |
||||
|
|
||||
|
@SerializedName("media_type") |
||||
|
val mediaType: String?, |
||||
|
|
||||
|
@SerializedName("url") |
||||
|
val url: String? |
||||
|
) { |
||||
|
val fullMediaUrl: String? |
||||
|
get() = resolveMediaUrl(url) |
||||
|
} |
||||
@ -0,0 +1,68 @@ |
|||||
|
package com.example.dattingapp.homes.networkandapi |
||||
|
|
||||
|
import android.content.Context |
||||
|
import android.util.Log |
||||
|
import okhttp3.OkHttpClient |
||||
|
import okhttp3.logging.HttpLoggingInterceptor |
||||
|
import retrofit2.Retrofit |
||||
|
import retrofit2.converter.gson.GsonConverterFactory |
||||
|
import java.util.concurrent.TimeUnit |
||||
|
|
||||
|
object HomeNetworkModule { |
||||
|
|
||||
|
private const val BASE_URL = "https://melove.youthome.com.cn/" |
||||
|
|
||||
|
@Volatile |
||||
|
private var apiService: HomeApiService? = null |
||||
|
|
||||
|
fun getApiService(context: Context): HomeApiService { |
||||
|
return apiService ?: synchronized(this) { |
||||
|
apiService ?: createApi(context.applicationContext) |
||||
|
.also { apiService = it } |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private fun createApi(context: Context): HomeApiService { |
||||
|
val tokenManager = TokenManager.getInstance(context) |
||||
|
|
||||
|
val loggingInterceptor = HttpLoggingInterceptor { message -> |
||||
|
Log.d("MeetHttp", message) |
||||
|
}.apply { |
||||
|
// Logs request/status lines without profile bodies. |
||||
|
level = HttpLoggingInterceptor.Level.BASIC |
||||
|
redactHeader("Authorization") |
||||
|
redactHeader("Cookie") |
||||
|
} |
||||
|
|
||||
|
val client = OkHttpClient.Builder() |
||||
|
.connectTimeout(30, TimeUnit.SECONDS) |
||||
|
.readTimeout(30, TimeUnit.SECONDS) |
||||
|
.callTimeout(60, TimeUnit.SECONDS) |
||||
|
.addInterceptor { chain -> |
||||
|
val token = tokenManager.getAccessToken() |
||||
|
?.replace(Regex("^Bearer\\s+", RegexOption.IGNORE_CASE), "")?.trim()?.takeIf { it.isNotBlank() } |
||||
|
|
||||
|
Log.d("MeetApi", "Access token available: ${token != null}") |
||||
|
|
||||
|
val request = chain.request().newBuilder() |
||||
|
.header("Accept", "application/json") |
||||
|
.apply { |
||||
|
if (token != null) { |
||||
|
header("Authorization", "Bearer $token") |
||||
|
} |
||||
|
} |
||||
|
.build() |
||||
|
|
||||
|
chain.proceed(request) |
||||
|
} |
||||
|
.addInterceptor(loggingInterceptor) |
||||
|
.build() |
||||
|
|
||||
|
return Retrofit.Builder() |
||||
|
.baseUrl(BASE_URL) |
||||
|
.client(client) |
||||
|
.addConverterFactory(GsonConverterFactory.create()) |
||||
|
.build() |
||||
|
.create(HomeApiService::class.java) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,155 @@ |
|||||
|
package com.example.dattingapp.homes.networkandapi |
||||
|
|
||||
|
import android.content.Context |
||||
|
import android.util.Log |
||||
|
import androidx.lifecycle.ViewModel |
||||
|
import androidx.lifecycle.viewModelScope |
||||
|
import kotlinx.coroutines.flow.MutableStateFlow |
||||
|
import kotlinx.coroutines.flow.StateFlow |
||||
|
import kotlinx.coroutines.flow.asStateFlow |
||||
|
import kotlinx.coroutines.launch |
||||
|
|
||||
|
sealed class MeetUiState { |
||||
|
object Idle : MeetUiState() |
||||
|
object Loading : MeetUiState() |
||||
|
data class Success(val response: List<MeetUserProfile>) : MeetUiState() |
||||
|
data class Error(val message: String) : MeetUiState() |
||||
|
} |
||||
|
|
||||
|
class MeetViewModel : ViewModel() { |
||||
|
|
||||
|
private val _meetUiState = MutableStateFlow<MeetUiState>(MeetUiState.Idle) |
||||
|
val meetUiState: StateFlow<MeetUiState> = _meetUiState.asStateFlow() |
||||
|
|
||||
|
fun meetUserResponse(context: Context) { |
||||
|
if (_meetUiState.value is MeetUiState.Loading) return |
||||
|
|
||||
|
val appContext = context.applicationContext |
||||
|
|
||||
|
viewModelScope.launch { |
||||
|
_meetUiState.value = MeetUiState.Loading |
||||
|
|
||||
|
if (TokenManager.getInstance(appContext).getAccessToken().isNullOrBlank()) { |
||||
|
Log.e("MeetApi", "No access token in user_tokens preferences") |
||||
|
_meetUiState.value = MeetUiState.Error("No access token found. Please log in again.") |
||||
|
return@launch |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
val response = HomeNetworkModule |
||||
|
.getApiService(appContext) |
||||
|
.getMeetProfiles() |
||||
|
|
||||
|
Log.d("MeetApi", "HTTP Response Code: ${response.code()}") |
||||
|
|
||||
|
if (!response.isSuccessful) { |
||||
|
response.errorBody()?.close() |
||||
|
val message = when (response.code()) { |
||||
|
401 -> "Authentication failed. Please log in again." |
||||
|
403 -> "The server refused access to these profiles." |
||||
|
404 -> "Meet endpoint was not found." |
||||
|
else -> "Request failed: HTTP ${response.code()}." |
||||
|
} |
||||
|
_meetUiState.value = MeetUiState.Error(message) |
||||
|
return@launch |
||||
|
} |
||||
|
|
||||
|
val profiles = response.body() |
||||
|
|
||||
|
if (profiles.isNullOrEmpty()) { |
||||
|
_meetUiState.value = MeetUiState.Error("No profiles available.") |
||||
|
} else { |
||||
|
// ------------------------------------------------------------- |
||||
|
// LOGGING IMAGE DATA FROM BACKEND RESPONSE |
||||
|
// ------------------------------------------------------------- |
||||
|
profiles.forEachIndexed { index, profile -> |
||||
|
val name = profile.fullName ?: "No Name (ID: ${profile.id})" |
||||
|
val mediaList = profile.media |
||||
|
|
||||
|
if (mediaList.isNullOrEmpty()) { |
||||
|
Log.w("MeetMediaCheck", "[$index] Profile: $name -> No media items returned from backend (media is empty/null)") |
||||
|
} else { |
||||
|
Log.d("MeetMediaCheck", "[$index] Profile: $name -> Found ${mediaList.size} media item(s)") |
||||
|
mediaList.forEachIndexed { mediaIdx, media -> |
||||
|
Log.d( |
||||
|
"MeetMediaCheck", |
||||
|
" └── Item #$mediaIdx: type='${media.mediaType}', raw_url='${media.url}', resolved_full_url='${media.fullMediaUrl}'" |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
// ------------------------------------------------------------- |
||||
|
|
||||
|
_meetUiState.value = MeetUiState.Success(profiles) |
||||
|
} |
||||
|
|
||||
|
} catch (e: Exception) { |
||||
|
Log.e("MeetApi", "Unexpected request failure", e) |
||||
|
_meetUiState.value = MeetUiState.Error("Request failed: ${e.javaClass.simpleName}") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
sealed interface MyProfileUiState { |
||||
|
object Idle : MyProfileUiState |
||||
|
object Loading : MyProfileUiState |
||||
|
data class Success(val myProfile: MyProfileResponse) : MyProfileUiState // Ensure this is MyProfileData, not MyProfileResponse |
||||
|
data class Error(val message: String) : MyProfileUiState |
||||
|
} |
||||
|
class MyProfileViewModel : ViewModel() { |
||||
|
|
||||
|
private val _myProfileUiState = MutableStateFlow<MyProfileUiState>(MyProfileUiState.Idle) |
||||
|
val myProfileUiState: StateFlow<MyProfileUiState> = _myProfileUiState.asStateFlow() |
||||
|
|
||||
|
fun meetUserResponse(context: Context) { |
||||
|
if (_myProfileUiState.value is MyProfileUiState.Loading) return |
||||
|
|
||||
|
val appContext = context.applicationContext |
||||
|
|
||||
|
viewModelScope.launch { |
||||
|
_myProfileUiState.value = MyProfileUiState.Loading |
||||
|
|
||||
|
if (TokenManager.getInstance(appContext).getAccessToken().isNullOrBlank()) { |
||||
|
Log.e("MeetApi", "No access token in user_tokens preferences") |
||||
|
_myProfileUiState.value = MyProfileUiState.Error("No access token found. Please log in again.") |
||||
|
return@launch |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
val response = HomeNetworkModule |
||||
|
.getApiService(appContext) |
||||
|
.getCurrentUserProfile() |
||||
|
|
||||
|
if (!response.isSuccessful) { |
||||
|
response.errorBody()?.close() |
||||
|
val message = when (response.code()) { |
||||
|
401 -> "Authentication failed. Please log in again." |
||||
|
403 -> "The server refused access to these profiles." |
||||
|
404 -> "Meet endpoint was not found." |
||||
|
else -> "Request failed: HTTP ${response.code()}." |
||||
|
} |
||||
|
_myProfileUiState.value = MyProfileUiState.Error(message) |
||||
|
return@launch |
||||
|
} |
||||
|
|
||||
|
// FIX: Use response.body() directly as profileData |
||||
|
val profileData = response.body() |
||||
|
|
||||
|
Log.d("my profiler", "my data $profileData") |
||||
|
|
||||
|
if (profileData != null) { |
||||
|
_myProfileUiState.value = MyProfileUiState.Success(profileData) |
||||
|
} else { |
||||
|
_myProfileUiState.value = MyProfileUiState.Error("Profile data is empty.") |
||||
|
} |
||||
|
|
||||
|
} catch (e: Exception) { |
||||
|
Log.e("MeetApi", "Unexpected request failure", e) |
||||
|
_myProfileUiState.value = MyProfileUiState.Error("Request failed: ${e.javaClass.simpleName}") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,66 @@ |
|||||
|
package com.example.dattingapp.homes.networkandapi |
||||
|
|
||||
|
import android.content.Context |
||||
|
import android.util.Log |
||||
|
|
||||
|
class TokenManager private constructor(context: Context) { |
||||
|
|
||||
|
private val preferences = |
||||
|
context.applicationContext.getSharedPreferences( |
||||
|
PREF_NAME, |
||||
|
Context.MODE_PRIVATE |
||||
|
) |
||||
|
|
||||
|
fun saveTokens(accessToken: String, refreshToken: String?) { |
||||
|
val cleanToken = accessToken.trim() |
||||
|
.replace( |
||||
|
Regex("^Bearer\\s+", RegexOption.IGNORE_CASE), |
||||
|
"" |
||||
|
) |
||||
|
.trim() |
||||
|
|
||||
|
val saved = preferences.edit() |
||||
|
.putString(KEY_ACCESS_TOKEN, cleanToken) |
||||
|
.putString(KEY_REFRESH_TOKEN, refreshToken) |
||||
|
.commit() |
||||
|
|
||||
|
// Do not log the token itself. |
||||
|
Log.d("TokenManager", "Tokens saved: $saved") |
||||
|
} |
||||
|
|
||||
|
fun getAccessToken(): String? { |
||||
|
return preferences.getString(KEY_ACCESS_TOKEN, null) |
||||
|
?.trim() |
||||
|
?.takeIf { it.isNotBlank() } |
||||
|
} |
||||
|
|
||||
|
fun getRefreshToken(): String? { |
||||
|
return preferences.getString(KEY_REFRESH_TOKEN, null) |
||||
|
?.takeIf { it.isNotBlank() } |
||||
|
} |
||||
|
|
||||
|
// Checks local token presence, not server validity or expiration. |
||||
|
fun isLoggedIn(): Boolean { |
||||
|
return !getAccessToken().isNullOrBlank() |
||||
|
} |
||||
|
|
||||
|
fun clearTokens() { |
||||
|
preferences.edit().clear().commit() |
||||
|
} |
||||
|
|
||||
|
companion object { |
||||
|
private const val PREF_NAME = "user_tokens" |
||||
|
private const val KEY_ACCESS_TOKEN = "access_token" |
||||
|
private const val KEY_REFRESH_TOKEN = "refresh_token" |
||||
|
|
||||
|
@Volatile |
||||
|
private var INSTANCE: TokenManager? = null |
||||
|
|
||||
|
fun getInstance(context: Context): TokenManager { |
||||
|
return INSTANCE ?: synchronized(this) { |
||||
|
INSTANCE ?: TokenManager(context.applicationContext) |
||||
|
.also { INSTANCE = it } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,51 +0,0 @@ |
|||||
package com.example.dattingapp.registration |
|
||||
|
|
||||
import android.content.Context |
|
||||
import android.content.SharedPreferences |
|
||||
|
|
||||
class TokenManager private constructor(context: Context) { |
|
||||
|
|
||||
private val sharedPreferences: SharedPreferences = context.applicationContext.getSharedPreferences( |
|
||||
PREF_NAME, |
|
||||
Context.MODE_PRIVATE |
|
||||
) |
|
||||
|
|
||||
fun saveTokens(accessToken: String, refreshToken: String?) { |
|
||||
sharedPreferences.edit().apply { |
|
||||
putString(KEY_ACCESS_TOKEN, accessToken) |
|
||||
putString(KEY_REFRESH_TOKEN, refreshToken) |
|
||||
apply() |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
fun getAccessToken(): String? { |
|
||||
return sharedPreferences.getString(KEY_ACCESS_TOKEN, null) |
|
||||
} |
|
||||
|
|
||||
fun getRefreshToken(): String? { |
|
||||
return sharedPreferences.getString(KEY_REFRESH_TOKEN, null) |
|
||||
} |
|
||||
|
|
||||
fun clearTokens() { |
|
||||
sharedPreferences.edit().apply { |
|
||||
remove(KEY_ACCESS_TOKEN) |
|
||||
remove(KEY_REFRESH_TOKEN) |
|
||||
apply() |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
companion object { |
|
||||
private const val PREF_NAME = "user_tokens" |
|
||||
private const val KEY_ACCESS_TOKEN = "access_token" |
|
||||
private const val KEY_REFRESH_TOKEN = "refresh_token" |
|
||||
|
|
||||
@Volatile |
|
||||
private var INSTANCE: TokenManager? = null |
|
||||
|
|
||||
fun getInstance(context: Context): TokenManager { |
|
||||
return INSTANCE ?: synchronized(this) { |
|
||||
INSTANCE ?: TokenManager(context.applicationContext).also { INSTANCE = it } |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,11 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<paths xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
|
<!-- Grants access to the app's internal cache directory --> |
||||
|
<cache-path name="cache_files" path="." /> |
||||
|
|
||||
|
<!-- Grants access to internal storage app-specific files directory --> |
||||
|
<files-path name="internal_files" path="." /> |
||||
|
|
||||
|
<!-- Grants access to external app storage directory --> |
||||
|
<external-files-path name="external_files" path="." /> |
||||
|
</paths> |
||||
Loading…
Reference in new issue