diff --git a/README.md b/README.md new file mode 100644 index 0000000..b98a78c --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ + + +# Screenshots +![Login_ui_login (1)](https://github.com/KaushalVasava/ComposeUI/assets/49050597/8f1930a6-67d8-4caa-bbe8-1f5f5ee80f1c) + +![Login_ui_registration (1)](https://github.com/KaushalVasava/ComposeUI/assets/49050597/5c068141-b828-4b39-9042-e084cb7c278b) + +# Video + + +https://github.com/KaushalVasava/ComposeUI/assets/49050597/069357c0-041c-49a9-9dcc-117c91605698 + diff --git a/app/src/main/java/com/kaushalvasava/app/composeui/screen/AuthenticationScreen.kt b/app/src/main/java/com/kaushalvasava/app/composeui/screen/AuthenticationScreen.kt new file mode 100644 index 0000000..d3c4402 --- /dev/null +++ b/app/src/main/java/com/kaushalvasava/app/composeui/screen/AuthenticationScreen.kt @@ -0,0 +1,400 @@ +package com.kaushalvasava.app.composeui.screen + +import android.widget.Toast +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +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.draw.drawBehind +import androidx.compose.ui.draw.rotate +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.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.kaushalvasava.app.composeui.R +import com.kaushalvasava.app.composeui.ui.navigation.NavigationItem +import com.kaushalvasava.app.composeui.util.ValidUtil.isValidEmail +import com.kaushalvasava.app.composeui.util.ValidUtil.isValidName +import com.kaushalvasava.app.composeui.util.ValidUtil.isValidPasswordFormat + +@Preview(showBackground = false) +@Composable +fun AuthenticationScreen(navController: NavController = rememberNavController()) { + + var isNewUser by rememberSaveable { + mutableStateOf(true) + } + val backgroundColor = MaterialTheme.colorScheme.background + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .fillMaxSize() + .drawBehind { + drawRect( + Brush.linearGradient( + colors = listOf( + Color.Blue, + backgroundColor, + Color.Blue, + ) + ) + ) + } + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 24.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + if (isNewUser) { + "Don't have an account?" + } else { + "Already have an account?" + }, + style = MaterialTheme.typography.bodyMedium, + color = Color.White + ) + Spacer(modifier = Modifier.width(8.dp)) + Button( + onClick = { + isNewUser = !isNewUser + }, colors = ButtonDefaults.buttonColors( + containerColor = backgroundColor.copy(0.3f), + contentColor = Color.White + ), + shape = RoundedCornerShape(8.dp) + ) { + AnimatedContent(targetState = isNewUser, label = "") { + Text( + if (it) { + "Sign up" + } else { + "Sign in" + } + ) + } + } + } + Image( + painter = painterResource(id = R.drawable.s_2), + contentScale = ContentScale.Crop, + contentDescription = "logo", + modifier = Modifier + .size(200.dp, 150.dp) + .rotate(-20f) + .clip(RoundedCornerShape(16.dp)) + .align(Alignment.CenterHorizontally) + + ) + Text( + "Unique", + style = MaterialTheme.typography.displayLarge, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + Spacer(modifier = Modifier.height(16.dp)) + HorizontalDivider( + modifier = Modifier + .padding(horizontal = 32.dp) + .clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)), + thickness = 12.dp, color = MaterialTheme.colorScheme.background.copy(0.5f) + ) + InfoCard(modifier = Modifier, isNewUser = { isNewUser }) { + navController.navigate(NavigationItem.PRODUCTS) + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun InfoCard( + modifier: Modifier = Modifier, + isNewUser: () -> Boolean, + onSubmitClick: () -> Unit, +) { + val context = LocalContext.current + var email by rememberSaveable { + mutableStateOf("") + } + var name by rememberSaveable { + mutableStateOf("") + } + var isPwdVisible by rememberSaveable { + mutableStateOf(false) + } + val maxChar = 10 + var password by rememberSaveable { + mutableStateOf("") + } + + val isValid by remember { + derivedStateOf { + isValidEmail(email) && isValidPasswordFormat(password) + && (if (isNewUser()) isValidName(name) else true) + } + } + val msg = + "Password is Wrong!\n" + + "Please enter at least 1 digit, 1 upper case and lowercase letter, 1 special character, no white spaces, at least 8 character" + + Column( + modifier + .fillMaxWidth() + .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) + .background(MaterialTheme.colorScheme.background) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + AnimatedContent(targetState = isNewUser(), label = "") { + Text( + if (it) "Let's make life stylish" else "Welcome Back", + fontWeight = FontWeight.Bold, + fontSize = 32.sp + ) + } + Text( + "Enter your details below", + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Light + ) + AnimatedContent(targetState = isNewUser(), label = "flowRow") { + FlowRow( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + if (it) { + OutlinedTextField( + value = name, + onValueChange = { + name = it + }, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(12.dp), + textStyle = TextStyle(fontSize = 16.sp), + placeholder = { + Text("Enter name", color = Color.Gray) + }, + supportingText = { + if (!isValidName(name) && name.isNotEmpty()) { + Text( + modifier = Modifier.fillMaxWidth(), + text = "Name is wrong!", + color = MaterialTheme.colorScheme.error + ) + } + }, + isError = name.isNotEmpty() && !isValidName(name), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next) + ) + } + OutlinedTextField( + value = email, + onValueChange = { + email = it + }, + modifier = Modifier.weight(1f), + textStyle = TextStyle(fontSize = 16.sp), + placeholder = { + Text("Enter email", color = Color.Gray) + }, + shape = RoundedCornerShape(12.dp), + supportingText = { + if (!isValidEmail(email) && email.isNotEmpty()) { + Text( + modifier = Modifier.fillMaxWidth(), + text = "Email is wrong!", + color = MaterialTheme.colorScheme.error + ) + } + }, + isError = email.isNotEmpty() && !isValidEmail(email), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next) + ) + OutlinedTextField( + value = password, + onValueChange = { + if (it.length <= maxChar) password = it + }, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(12.dp), + visualTransformation = + if (isPwdVisible) + VisualTransformation.None + else + PasswordVisualTransformation(), + trailingIcon = { + IconButton( + onClick = { + isPwdVisible = !isPwdVisible + } + ) { + Icon( + if (isPwdVisible) + painterResource(id = R.drawable.ic_visibility_off) + else + painterResource(id = R.drawable.ic_visibility), + contentDescription = null + ) + } + }, + supportingText = { + if (!isValidPasswordFormat(password) && password.isNotEmpty()) { + Text( + modifier = Modifier.fillMaxWidth(), + text = msg, + color = MaterialTheme.colorScheme.error + ) + } + }, + textStyle = TextStyle(fontSize = 16.sp), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done + ), + placeholder = { + Text("Enter password", color = Color.Gray) + }, + isError = password.isNotEmpty() && !isValidPasswordFormat(password), + ) + } + } + Button( + onClick = { + if (isValid) { + onSubmitClick() + } else { + Toast.makeText(context, "Please enter all valid details", Toast.LENGTH_SHORT) + .show() + } + }, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .drawBehind { + drawRect( + Brush.linearGradient( + colors = listOf( + Color.Blue, + Color.Magenta + ) + ) + ) + }, + colors = ButtonDefaults.buttonColors( + containerColor = Color.Transparent, + contentColor = Color.White + ), + shape = RoundedCornerShape(12.dp) + ) { + Text( + if (isNewUser()) "Sign up" else "Sign in", + modifier = Modifier.padding(vertical = 8.dp) + ) + } + AnimatedVisibility(!isNewUser()) { + TextButton(onClick = { + //forgot password + }) { + Text("Forgot your password?") + } + } + Row( + Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround, + verticalAlignment = Alignment.CenterVertically + ) { + HorizontalDivider(Modifier.weight(1f)) + AnimatedContent(targetState = isNewUser(), label = "dg") { + Text( + if (it) { + "Or sign up with" + } else { + "Or sign in with" + }, + modifier = Modifier.weight(1f), + textAlign = TextAlign.Center + ) + } + HorizontalDivider(Modifier.weight(1f)) + } + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + OutlinedButton(onClick = { + // google auth + }, shape = RoundedCornerShape(8.dp)) { + Icon( + painter = painterResource(id = R.drawable.ic_google), + contentDescription = "google", + modifier = Modifier.size(ButtonDefaults.IconSize), + tint = Color.Unspecified + ) + Spacer(modifier = Modifier.width(16.dp)) + Text(text = "Google") + } + OutlinedButton(onClick = { + // facebook auth + }, shape = RoundedCornerShape(8.dp)) { + Icon( + painter = painterResource(id = R.drawable.ic_facebook), + contentDescription = "Facebook", + modifier = Modifier.size(ButtonDefaults.IconSize), + tint = Color.Unspecified + ) + Spacer(modifier = Modifier.width(16.dp)) + Text(text = "Facebook") + } + } + Spacer(Modifier.weight(1f)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kaushalvasava/app/composeui/ui/navigation/AppNavHost.kt b/app/src/main/java/com/kaushalvasava/app/composeui/ui/navigation/AppNavHost.kt index 0f37c05..189cee0 100644 --- a/app/src/main/java/com/kaushalvasava/app/composeui/ui/navigation/AppNavHost.kt +++ b/app/src/main/java/com/kaushalvasava/app/composeui/ui/navigation/AppNavHost.kt @@ -9,6 +9,7 @@ import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.navArgument +import com.kaushalvasava.app.composeui.screen.AuthenticationScreen import com.kaushalvasava.app.composeui.screen.ProductDetailScreen import com.kaushalvasava.app.composeui.screen.ProductsScreen @@ -16,7 +17,7 @@ import com.kaushalvasava.app.composeui.screen.ProductsScreen fun AppNavHost( modifier: Modifier = Modifier, navController: NavHostController, - startDestination: String = NavigationItem.PRODUCTS, + startDestination: String = NavigationItem.AUTHENTICATION, ) { NavHost( modifier = modifier, @@ -47,6 +48,9 @@ fun AppNavHost( ) } ) { + composable(NavigationItem.AUTHENTICATION) { + AuthenticationScreen(navController) + } composable(NavigationItem.PRODUCTS) { ProductsScreen(navController) } diff --git a/app/src/main/java/com/kaushalvasava/app/composeui/ui/navigation/NavigationItem.kt b/app/src/main/java/com/kaushalvasava/app/composeui/ui/navigation/NavigationItem.kt index 6f75455..57d2d0b 100644 --- a/app/src/main/java/com/kaushalvasava/app/composeui/ui/navigation/NavigationItem.kt +++ b/app/src/main/java/com/kaushalvasava/app/composeui/ui/navigation/NavigationItem.kt @@ -3,4 +3,5 @@ package com.kaushalvasava.app.composeui.ui.navigation object NavigationItem { const val PRODUCTS = "products" const val PRODUCT_DETAILS = "product_details" + const val AUTHENTICATION = "authentication" } \ No newline at end of file diff --git a/app/src/main/java/com/kaushalvasava/app/composeui/util/ValidUtil.kt b/app/src/main/java/com/kaushalvasava/app/composeui/util/ValidUtil.kt new file mode 100644 index 0000000..135e703 --- /dev/null +++ b/app/src/main/java/com/kaushalvasava/app/composeui/util/ValidUtil.kt @@ -0,0 +1,34 @@ +package com.kaushalvasava.app.composeui.util + +import android.util.Patterns +import java.util.regex.Pattern + +object ValidUtil { + + fun isValidEmail(email: String): Boolean { + val pattern: Pattern = Patterns.EMAIL_ADDRESS + return pattern.matcher(email).matches() + } + + fun isValidName(name: String): Boolean { + val pattern: Pattern = + Pattern.compile("^([a-zA-Z]{2,}\\s[a-zA-Z]+'?-?[a-zA-Z]{2,}\\s?([a-zA-Z]+)?)") + return pattern.matcher(name).matches() + } + + + fun isValidPasswordFormat(password: String): Boolean { + val passwordREGEX = Pattern.compile( + "^" + + "(?=.*[0-9])" + //at least 1 digit + "(?=.*[a-z])" + //at least 1 lower case letter + "(?=.*[A-Z])" + //at least 1 upper case letter + "(?=.*[a-zA-Z])" + //any letter + "(?=.*[@#$%^&+=])" + //at least 1 special character + "(?=\\S+$)" + //no white spaces + ".{10,}" + //at least 8 characters + "$" + ) + return passwordREGEX.matcher(password).matches() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_facebook.xml b/app/src/main/res/drawable/ic_facebook.xml new file mode 100644 index 0000000..b60ee8a --- /dev/null +++ b/app/src/main/res/drawable/ic_facebook.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_google.xml b/app/src/main/res/drawable/ic_google.xml new file mode 100644 index 0000000..61448dd --- /dev/null +++ b/app/src/main/res/drawable/ic_google.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_visibility.xml b/app/src/main/res/drawable/ic_visibility.xml new file mode 100644 index 0000000..f843e29 --- /dev/null +++ b/app/src/main/res/drawable/ic_visibility.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_visibility_off.xml b/app/src/main/res/drawable/ic_visibility_off.xml new file mode 100644 index 0000000..5993ca3 --- /dev/null +++ b/app/src/main/res/drawable/ic_visibility_off.xml @@ -0,0 +1,5 @@ + + + + +