From 22e27e238f731846b39586736285ba77a23a390c Mon Sep 17 00:00:00 2001 From: Harrishan Date: Wed, 30 Oct 2024 22:38:36 +0100 Subject: [PATCH 01/12] feat: center GlideImage and update EditProfileScreen layout - Center GlideImage horizontally in EditProfileScreen - Adjust padding and layout for better UI alignment - Update text styles and field arrangements - Add rounded corners and borders to input fields - Ensure Save Changes button is centered and styled --- .../com/android/periodpals/MainActivity.kt | 1 + .../periodpals/ui/profile/EditProfile.kt | 179 +++++++++++++++++- 2 files changed, 178 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/android/periodpals/MainActivity.kt b/app/src/main/java/com/android/periodpals/MainActivity.kt index 592561408..bcf1ea538 100644 --- a/app/src/main/java/com/android/periodpals/MainActivity.kt +++ b/app/src/main/java/com/android/periodpals/MainActivity.kt @@ -75,6 +75,7 @@ class MainActivity : ComponentActivity() { PeriodPalsAppTheme { // A surface container using the 'background' color from the theme Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { + // EditProfileScreen(NavigationActions(rememberNavController())) PeriodPalsApp(locationPermissionGranted, authViewModel) } } diff --git a/app/src/main/java/com/android/periodpals/ui/profile/EditProfile.kt b/app/src/main/java/com/android/periodpals/ui/profile/EditProfile.kt index 558f22028..149ff2709 100644 --- a/app/src/main/java/com/android/periodpals/ui/profile/EditProfile.kt +++ b/app/src/main/java/com/android/periodpals/ui/profile/EditProfile.kt @@ -1,19 +1,59 @@ package com.android.periodpals.ui.profile +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AddCircleOutline +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp import com.android.periodpals.ui.navigation.BottomNavigationMenu import com.android.periodpals.ui.navigation.LIST_TOP_LEVEL_DESTINATION import com.android.periodpals.ui.navigation.NavigationActions import com.android.periodpals.ui.navigation.TopAppBar +import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi +import com.bumptech.glide.integration.compose.GlideImage /* Placeholder Screen, waiting for implementation */ +@OptIn(ExperimentalGlideComposeApi::class) @Composable fun EditProfileScreen(navigationActions: NavigationActions) { + var name by remember { mutableStateOf("") } + var dob by remember { mutableStateOf("") } + var description by remember { mutableStateOf("") } + Scaffold( bottomBar = ({ BottomNavigationMenu( @@ -23,11 +63,146 @@ fun EditProfileScreen(navigationActions: NavigationActions) { }), topBar = { TopAppBar( - title = "Edit Profile", + title = "Edit your Profile", true, onBackButtonClick = { navigationActions.navigateTo("profile") }) }, content = { pd -> - Text("Edit Profile Screen", modifier = Modifier.fillMaxSize().padding(pd)) + Column( + modifier = Modifier.padding(pd).padding(24.dp).fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + Box { + GlideImage( + model = "", + modifier = + Modifier.padding(1.dp) + .width(124.dp) + .height(124.dp) + .background(color = Color(0xFFD9D9D9), shape = CircleShape), + contentDescription = "image profile", + contentScale = ContentScale.None) + Icon( + Icons.Filled.AddCircleOutline, + contentDescription = "add circle", + modifier = + Modifier.align(Alignment.BottomEnd) + .size(40.dp) + .background(color = Color(0xFF79747E), shape = CircleShape) + .testTag("add_circle_icon")) + } + } + + Text( + text = "Mandatory Fields", + style = + TextStyle( + fontWeight = FontWeight(500), + )) + + HorizontalDivider(thickness = 2.dp) + + Row(horizontalArrangement = Arrangement.Start) { + Text( + text = "Email: ", + style = + TextStyle( + fontWeight = FontWeight(500), + )) + + Text( + text = "emilia.jones@email.com", + style = + TextStyle( + fontWeight = FontWeight(400), + textDecoration = TextDecoration.Underline, + )) + } + + Text( + text = "Name:", + style = + TextStyle( + fontWeight = FontWeight(500), + )) + + OutlinedTextField( + value = name, + onValueChange = { name = it }, + modifier = + Modifier.testTag("name_field") + .clip(RoundedCornerShape(10.dp)) // Clip the box to have rounded corners. + .border( + 1.dp, + MaterialTheme.colorScheme.onSurface, // Color of the border. + RoundedCornerShape(10.dp), // Rounded corners for the border. + ), + ) + + Text( + text = "Date of Birth:", + style = + TextStyle( + fontWeight = FontWeight(500), + )) + + OutlinedTextField( + value = dob, + onValueChange = { dob = it }, + modifier = + Modifier.testTag("dob_field") + .clip(RoundedCornerShape(10.dp)) // Clip the box to have rounded corners. + .border( + 1.dp, + MaterialTheme.colorScheme.onSurface, // Color of the border. + RoundedCornerShape(10.dp), // Rounded corners for the border. + ), + ) + + Text( + text = "Your Profile", + style = + TextStyle( + fontWeight = FontWeight(500), + )) + + HorizontalDivider(thickness = 2.dp) + + Text( + text = "Description:", + style = + TextStyle( + fontWeight = FontWeight(500), + )) + + OutlinedTextField( + value = description, + onValueChange = { description = it }, + modifier = + Modifier.testTag("description_field") + .clip(RoundedCornerShape(10.dp)) // Clip the box to have rounded corners. + .border( + 1.dp, + MaterialTheme.colorScheme.onSurface, // Color of the border. + RoundedCornerShape(10.dp), // Rounded corners for the border. + ) + .height(84.dp), + ) + + Button( + onClick = {}, + enabled = true, + modifier = + Modifier.padding(1.dp) + .testTag("save_button") + .align(Alignment.CenterHorizontally) + .background( + color = Color(0xFFD9D9D9), shape = RoundedCornerShape(size = 100.dp)), + colors = ButtonDefaults.buttonColors(Color(0xFFD9D9D9)), + ) { + Text("Save Changes") + } + } }) } From aa7ae4fb1e75cca4dfbf37340d8217894da9228c Mon Sep 17 00:00:00 2001 From: Harrishan Date: Thu, 31 Oct 2024 00:52:51 +0100 Subject: [PATCH 02/12] fix: modularize EditProfileScreen without changing UI - Extract ProfileImage composable - Extract ProfileTextField composable - Extract ProfileSection composable - Add comments for better code understanding - Maintain existing UI layout and design --- .../com/android/periodpals/MainActivity.kt | 4 +- .../periodpals/ui/profile/EditProfile.kt | 148 ++++++++---------- 2 files changed, 65 insertions(+), 87 deletions(-) diff --git a/app/src/main/java/com/android/periodpals/MainActivity.kt b/app/src/main/java/com/android/periodpals/MainActivity.kt index c43d4cf11..77b429921 100644 --- a/app/src/main/java/com/android/periodpals/MainActivity.kt +++ b/app/src/main/java/com/android/periodpals/MainActivity.kt @@ -70,8 +70,8 @@ class MainActivity : ComponentActivity() { PeriodPalsAppTheme { // A surface container using the 'background' color from the theme Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { - // EditProfileScreen(NavigationActions(rememberNavController())) - PeriodPalsApp(locationPermissionGranted, authViewModel) + EditProfileScreen(NavigationActions(rememberNavController())) + // PeriodPalsApp(locationPermissionGranted, authViewModel) } } } diff --git a/app/src/main/java/com/android/periodpals/ui/profile/EditProfile.kt b/app/src/main/java/com/android/periodpals/ui/profile/EditProfile.kt index 149ff2709..33516123f 100644 --- a/app/src/main/java/com/android/periodpals/ui/profile/EditProfile.kt +++ b/app/src/main/java/com/android/periodpals/ui/profile/EditProfile.kt @@ -46,21 +46,21 @@ import com.android.periodpals.ui.navigation.TopAppBar import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage -/* Placeholder Screen, waiting for implementation */ @OptIn(ExperimentalGlideComposeApi::class) @Composable fun EditProfileScreen(navigationActions: NavigationActions) { + // State variables to hold the input values var name by remember { mutableStateOf("") } var dob by remember { mutableStateOf("") } var description by remember { mutableStateOf("") } Scaffold( - bottomBar = ({ - BottomNavigationMenu( - onTabSelect = { route -> navigationActions.navigateTo(route) }, - tabList = LIST_TOP_LEVEL_DESTINATION, - selectedItem = navigationActions.currentRoute()) - }), + bottomBar = { + BottomNavigationMenu( + onTabSelect = { route -> navigationActions.navigateTo(route) }, + tabList = LIST_TOP_LEVEL_DESTINATION, + selectedItem = navigationActions.currentRoute()) + }, topBar = { TopAppBar( title = "Edit your Profile", @@ -72,6 +72,7 @@ fun EditProfileScreen(navigationActions: NavigationActions) { modifier = Modifier.padding(pd).padding(24.dp).fillMaxSize(), verticalArrangement = Arrangement.spacedBy(8.dp), ) { + // Profile image section Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { Box { GlideImage( @@ -94,102 +95,48 @@ fun EditProfileScreen(navigationActions: NavigationActions) { } } - Text( - text = "Mandatory Fields", - style = - TextStyle( - fontWeight = FontWeight(500), - )) + // Section title + ProfileText("Mandatory Fields") + // Divider HorizontalDivider(thickness = 2.dp) + // Email row Row(horizontalArrangement = Arrangement.Start) { - Text( - text = "Email: ", - style = - TextStyle( - fontWeight = FontWeight(500), - )) - - Text( - text = "emilia.jones@email.com", - style = - TextStyle( - fontWeight = FontWeight(400), - textDecoration = TextDecoration.Underline, - )) + ProfileText("Email: ") + ProfileText( + "emilia.jones@email.com", + TextStyle(fontWeight = FontWeight(400), textDecoration = TextDecoration.Underline)) } - Text( - text = "Name:", - style = - TextStyle( - fontWeight = FontWeight(500), - )) - - OutlinedTextField( + // Name input field + ProfileField( + title = "Name: ", value = name, onValueChange = { name = it }, - modifier = - Modifier.testTag("name_field") - .clip(RoundedCornerShape(10.dp)) // Clip the box to have rounded corners. - .border( - 1.dp, - MaterialTheme.colorScheme.onSurface, // Color of the border. - RoundedCornerShape(10.dp), // Rounded corners for the border. - ), - ) - - Text( - text = "Date of Birth:", - style = - TextStyle( - fontWeight = FontWeight(500), - )) + modifier = Modifier.testTag("name_field")) - OutlinedTextField( + // Date of Birth input field + ProfileField( + title = "Date of Birth: ", value = dob, onValueChange = { dob = it }, - modifier = - Modifier.testTag("dob_field") - .clip(RoundedCornerShape(10.dp)) // Clip the box to have rounded corners. - .border( - 1.dp, - MaterialTheme.colorScheme.onSurface, // Color of the border. - RoundedCornerShape(10.dp), // Rounded corners for the border. - ), - ) + modifier = Modifier.testTag("dob_field")) - Text( - text = "Your Profile", - style = - TextStyle( - fontWeight = FontWeight(500), - )) + // Section title + ProfileText("Your Profile: ") + // Divider HorizontalDivider(thickness = 2.dp) - Text( - text = "Description:", - style = - TextStyle( - fontWeight = FontWeight(500), - )) - - OutlinedTextField( + // Description input field + ProfileField( + title = "Description: ", value = description, onValueChange = { description = it }, - modifier = - Modifier.testTag("description_field") - .clip(RoundedCornerShape(10.dp)) // Clip the box to have rounded corners. - .border( - 1.dp, - MaterialTheme.colorScheme.onSurface, // Color of the border. - RoundedCornerShape(10.dp), // Rounded corners for the border. - ) - .height(84.dp), - ) + modifier = Modifier.testTag("description_field").height(84.dp)) + // Save Changes button Button( onClick = {}, enabled = true, @@ -206,3 +153,34 @@ fun EditProfileScreen(navigationActions: NavigationActions) { } }) } + +@Composable +fun ProfileTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier +) { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + modifier = + modifier + .clip(RoundedCornerShape(10.dp)) + .border(1.dp, MaterialTheme.colorScheme.onSurface, RoundedCornerShape(10.dp))) +} + +@Composable +fun ProfileText(title: String, textStyle: TextStyle = TextStyle(fontWeight = FontWeight(500))) { + Text(text = title, style = textStyle) +} + +@Composable +fun ProfileField( + title: String, + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier +) { + ProfileText(title) + ProfileTextField(value = value, onValueChange = onValueChange, modifier = modifier) +} From d61be6e7fc4712997d603587266c08dd09f7b76b Mon Sep 17 00:00:00 2001 From: Harrishan Date: Thu, 31 Oct 2024 14:37:04 +0100 Subject: [PATCH 03/12] feat: add UI tests for `EditProfileScreen` and validate fields - Added UI tests for `EditProfileScreen` to verify UI elements and interactions. - Implemented field validation to check if name, date of birth, and description fields are filled. --- .../periodpals/ui/profile/EditProfileTest.kt | 66 +++++++++++++++++++ .../periodpals/ui/profile/EditProfile.kt | 59 ++++++++++++++--- app/src/main/res/values-night/themes.xml | 2 +- app/src/main/res/values/themes.xml | 2 +- 4 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 app/src/androidTest/java/com/android/periodpals/ui/profile/EditProfileTest.kt diff --git a/app/src/androidTest/java/com/android/periodpals/ui/profile/EditProfileTest.kt b/app/src/androidTest/java/com/android/periodpals/ui/profile/EditProfileTest.kt new file mode 100644 index 000000000..99f49d19b --- /dev/null +++ b/app/src/androidTest/java/com/android/periodpals/ui/profile/EditProfileTest.kt @@ -0,0 +1,66 @@ +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import androidx.navigation.compose.rememberNavController +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.periodpals.ui.navigation.NavigationActions +import com.android.periodpals.ui.profile.EditProfileScreen +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class EditProfileTest { + + @get:Rule val composeTestRule = createComposeRule() + + @Before + // Set up the EditProfileScreen + fun setUp() { + composeTestRule.setContent { + MaterialTheme { EditProfileScreen(NavigationActions(rememberNavController())) } + } + } + + @Test + fun testEmailRowDisplayed() { + + // Check if the email row is displayed + composeTestRule.onNodeWithTag("email_row").assertIsDisplayed() + } + + @Test + fun testProfileImageDisplayed() { + + // Check if the profile image is displayed + composeTestRule.onNodeWithTag("profile_image").assertIsDisplayed() + + // Check if the add circle icon is displayed + composeTestRule.onNodeWithTag("add_circle_icon").assertIsDisplayed() + } + + @Test + fun testPerformTextInput() { + + // Perform text input + composeTestRule.onNodeWithTag("name_field").performTextInput("New Name") + composeTestRule.onNodeWithTag("dob_field").performTextInput("02/02/2022") + composeTestRule.onNodeWithTag("description_field").performTextInput("New Description") + + // Verify text input + composeTestRule.onNodeWithTag("name_field").assertTextEquals("New Name") + composeTestRule.onNodeWithTag("dob_field").assertTextEquals("02/02/2022") + composeTestRule.onNodeWithTag("description_field").assertTextEquals("New Description") + } + + @Test + fun testPerformSaveButtonClick() { + // Perform save button click + composeTestRule.onNodeWithTag("save_button").performClick() + } +} diff --git a/app/src/main/java/com/android/periodpals/ui/profile/EditProfile.kt b/app/src/main/java/com/android/periodpals/ui/profile/EditProfile.kt index 33516123f..b3d3347dc 100644 --- a/app/src/main/java/com/android/periodpals/ui/profile/EditProfile.kt +++ b/app/src/main/java/com/android/periodpals/ui/profile/EditProfile.kt @@ -1,7 +1,14 @@ package com.android.periodpals.ui.profile +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -11,7 +18,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -34,6 +40,7 @@ import androidx.compose.ui.Modifier 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.platform.testTag import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight @@ -54,6 +61,18 @@ fun EditProfileScreen(navigationActions: NavigationActions) { var dob by remember { mutableStateOf("") } var description by remember { mutableStateOf("") } + var profileImageUri by remember { mutableStateOf(Uri.parse("")) } + + val launcher = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + profileImageUri = result.data?.data + } + } + + val context = LocalContext.current + Scaffold( bottomBar = { BottomNavigationMenu( @@ -76,14 +95,15 @@ fun EditProfileScreen(navigationActions: NavigationActions) { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { Box { GlideImage( - model = "", + model = profileImageUri, modifier = Modifier.padding(1.dp) - .width(124.dp) - .height(124.dp) - .background(color = Color(0xFFD9D9D9), shape = CircleShape), + .size(124.dp) + .background(color = Color(0xFFD9D9D9), shape = CircleShape) + .testTag("profile_image"), contentDescription = "image profile", contentScale = ContentScale.None) + Icon( Icons.Filled.AddCircleOutline, contentDescription = "add circle", @@ -91,7 +111,12 @@ fun EditProfileScreen(navigationActions: NavigationActions) { Modifier.align(Alignment.BottomEnd) .size(40.dp) .background(color = Color(0xFF79747E), shape = CircleShape) - .testTag("add_circle_icon")) + .testTag("add_circle_icon") + .clickable { + val pickImageIntent = + Intent(Intent.ACTION_PICK).apply { type = "image/*" } + launcher.launch(pickImageIntent) + }) } } @@ -102,7 +127,7 @@ fun EditProfileScreen(navigationActions: NavigationActions) { HorizontalDivider(thickness = 2.dp) // Email row - Row(horizontalArrangement = Arrangement.Start) { + Row(horizontalArrangement = Arrangement.Start, modifier = Modifier.testTag("email_row")) { ProfileText("Email: ") ProfileText( "emilia.jones@email.com", @@ -138,7 +163,15 @@ fun EditProfileScreen(navigationActions: NavigationActions) { // Save Changes button Button( - onClick = {}, + onClick = { + val errorMessage = validateFields(name, dob, description) + if (errorMessage != null) { + Toast.makeText(context, errorMessage, Toast.LENGTH_SHORT).show() + } else { + // Save the profile (future implementation) + Toast.makeText(context, "Profile saved", Toast.LENGTH_SHORT).show() + } + }, enabled = true, modifier = Modifier.padding(1.dp) @@ -184,3 +217,13 @@ fun ProfileField( ProfileText(title) ProfileTextField(value = value, onValueChange = onValueChange, modifier = modifier) } + +/** Validates the fields of the profile screen. */ +private fun validateFields(name: String, date: String, description: String): String? { + return when { + name.isEmpty() -> "Please enter a name" + !validateDate(date) -> "Invalid date" + description.isEmpty() -> "Please enter a description" + else -> null + } +} diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 3e10e95a7..486a07347 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,4 +1,4 @@ - +