diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 855bafe3..7ee8309a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,7 +16,8 @@ android:name=".MainActivity" android:exported="true" android:label="@string/app_name" - android:theme="@style/Theme.WeSpot"> + android:theme="@style/Theme.WeSpot" + android:windowSoftInputMode="adjustResize"> diff --git a/app/src/main/kotlin/com/bff/wespot/MainActivity.kt b/app/src/main/kotlin/com/bff/wespot/MainActivity.kt index fff46e95..c6770b32 100644 --- a/app/src/main/kotlin/com/bff/wespot/MainActivity.kt +++ b/app/src/main/kotlin/com/bff/wespot/MainActivity.kt @@ -5,8 +5,8 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.ui.Modifier +import com.bff.wespot.auth.screen.SchoolScreen import com.bff.wespot.designsystem.theme.WeSpotTheme class MainActivity : ComponentActivity() { @@ -18,7 +18,7 @@ class MainActivity : ComponentActivity() { Surface( modifier = Modifier.fillMaxSize(), ) { - Text(text = "Hello, World!") + SchoolScreen() } } } diff --git a/core/model/src/main/kotlin/com/bff/wespot/model/SchoolItem.kt b/core/model/src/main/kotlin/com/bff/wespot/model/SchoolItem.kt new file mode 100644 index 00000000..4c031fbd --- /dev/null +++ b/core/model/src/main/kotlin/com/bff/wespot/model/SchoolItem.kt @@ -0,0 +1,7 @@ +package com.bff.wespot.model + +data class SchoolItem( + val id: String, + val name: String, + val address: String, +) diff --git a/core/model/src/main/kotlin/com/bff/wespot/model/empty b/core/model/src/main/kotlin/com/bff/wespot/model/empty deleted file mode 100644 index e5f106f7..00000000 --- a/core/model/src/main/kotlin/com/bff/wespot/model/empty +++ /dev/null @@ -1 +0,0 @@ -model module \ No newline at end of file diff --git a/core/ui/src/main/kotlin/com/bff/wespot/ui/SchoolListItem.kt b/core/ui/src/main/kotlin/com/bff/wespot/ui/SchoolListItem.kt new file mode 100644 index 00000000..af611c7f --- /dev/null +++ b/core/ui/src/main/kotlin/com/bff/wespot/ui/SchoolListItem.kt @@ -0,0 +1,123 @@ +package com.bff.wespot.ui + +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 +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.School +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.bff.wespot.designsystem.theme.StaticTypeScale +import com.bff.wespot.designsystem.theme.WeSpotTheme +import com.bff.wespot.designsystem.theme.WeSpotThemeManager +import com.bff.wespot.designsystem.util.OrientationPreviews + +@Composable +fun SchoolListItem( + schoolName: String, + address: String, + selected: Boolean, + onClick: () -> Unit = { } +) { + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(vertical = 8.dp) + .clip(WeSpotThemeManager.shapes.medium) + .border( + width = 1.dp, + color = if(selected) { + WeSpotThemeManager.colors.primaryColor + } else { + WeSpotThemeManager.colors.cardBackgroundColor + }, + shape = WeSpotThemeManager.shapes.medium + ) + .background(WeSpotThemeManager.colors.cardBackgroundColor) + .clickable { onClick.invoke() } + ) { + Row( + modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp), + ) { + Box( + modifier = Modifier + .size(56.dp) + .clip(CircleShape) + .background(Color.White), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.School, + contentDescription = stringResource(id = R.string.school_icon) + ) + } + + Column( + modifier = Modifier.padding(top = 4.dp, bottom = 4.dp, start = 16.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text(text = schoolName, style = StaticTypeScale.Default.body2, maxLines = 1) + Text( + text = address, + style = StaticTypeScale.Default.body6, + color = WeSpotThemeManager.colors.txtSubColor, + maxLines = 1 + ) + } + } + Box( + modifier = Modifier + .fillMaxWidth() + .padding(end = 14.dp, top = 14.dp), + contentAlignment = Alignment.TopEnd + ) { + Icon( + painter = painterResource(id = R.drawable.exclude), + contentDescription = "", + tint = if (selected) { + WeSpotThemeManager.colors.primaryColor + } else { + WeSpotThemeManager.colors.disableIcnColor + } + ) + } + } +} + +@OrientationPreviews +@Composable +private fun SchoolListItemPreview() { + WeSpotTheme { + Surface(modifier = Modifier.fillMaxSize()) { + Column { + repeat(10) { + SchoolListItem( + schoolName = "역삼 중학교", + address = "서울특별시 강남구 도곡로 43길 10", + selected = false + ) + } + } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/kotlin/com/bff/wespot/ui/WSBottomSheet.kt b/core/ui/src/main/kotlin/com/bff/wespot/ui/WSBottomSheet.kt new file mode 100644 index 00000000..f020bf04 --- /dev/null +++ b/core/ui/src/main/kotlin/com/bff/wespot/ui/WSBottomSheet.kt @@ -0,0 +1,31 @@ +package com.bff.wespot.ui + +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState +import androidx.compose.material3.rememberStandardBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.bff.wespot.designsystem.theme.WeSpotThemeManager + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WSBottomSheet( + closeSheet: () -> Unit, + sheetState: SheetState = rememberStandardBottomSheetState(), + content: @Composable () -> Unit +) { + ModalBottomSheet( + onDismissRequest = closeSheet, + sheetState = sheetState, + shape = RoundedCornerShape(topStart = 25.dp, topEnd = 25.dp), + containerColor = WeSpotThemeManager.colors.bottomSheetColor, + dragHandle = null, + modifier = Modifier.navigationBarsPadding() + ) { + content.invoke() + } +} \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/exclude.xml b/core/ui/src/main/res/drawable/exclude.xml new file mode 100644 index 00000000..4d82e563 --- /dev/null +++ b/core/ui/src/main/res/drawable/exclude.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/female_student.png b/core/ui/src/main/res/drawable/female_student.png new file mode 100644 index 00000000..7580db85 Binary files /dev/null and b/core/ui/src/main/res/drawable/female_student.png differ diff --git a/core/ui/src/main/res/drawable/male_student.png b/core/ui/src/main/res/drawable/male_student.png new file mode 100644 index 00000000..faaee59d Binary files /dev/null and b/core/ui/src/main/res/drawable/male_student.png differ diff --git a/core/ui/src/main/res/values/string.xml b/core/ui/src/main/res/values/string.xml new file mode 100644 index 00000000..22ec7b9b --- /dev/null +++ b/core/ui/src/main/res/values/string.xml @@ -0,0 +1,4 @@ + + + school icon + \ No newline at end of file diff --git a/feature/auth/src/main/kotlin/com/bff/wespot/auth/screen/ClassScreen.kt b/feature/auth/src/main/kotlin/com/bff/wespot/auth/screen/ClassScreen.kt new file mode 100644 index 00000000..99d85b62 --- /dev/null +++ b/feature/auth/src/main/kotlin/com/bff/wespot/auth/screen/ClassScreen.kt @@ -0,0 +1,120 @@ +package com.bff.wespot.auth.screen + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.bff.wespot.auth.R +import com.bff.wespot.auth.state.AuthAction +import com.bff.wespot.auth.viewmodel.AuthViewModel +import com.bff.wespot.designsystem.component.button.WSButton +import com.bff.wespot.designsystem.component.header.WSTopBar +import com.bff.wespot.designsystem.component.input.WsTextField +import com.bff.wespot.designsystem.theme.StaticTypeScale +import com.bff.wespot.designsystem.theme.WeSpotThemeManager +import kotlinx.coroutines.delay +import org.orbitmvi.orbit.compose.collectAsState + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ClassScreen(viewModel: AuthViewModel = viewModel()) { + val keyboard = LocalSoftwareKeyboardController.current + + val state by viewModel.collectAsState() + val action = viewModel::onAction + val focusRequester = remember { FocusRequester() } + + Scaffold( + topBar = { + WSTopBar(title = stringResource(id = R.string.register), canNavigateBack = true) + }, + modifier = Modifier.padding(horizontal = 20.dp), + ) { + Column( + modifier = + Modifier + .fillMaxSize() + .padding(it), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + text = stringResource(id = R.string.get_class), + style = StaticTypeScale.Default.header1, + ) + + Text( + text = stringResource(id = R.string.cannot_change_class_after_register), + style = StaticTypeScale.Default.body6, + color = Color(0xFF7A7A7A), + ) + + WsTextField( + value = + if (state.classNumber != -1) { + state.classNumber.toString() + } else { + "" + }, + onValueChange = { classNumber -> + if (classNumber.isEmpty()) { + action(AuthAction.OnClassNumberChanged(-1)) + return@WsTextField + } + action(AuthAction.OnClassNumberChanged(classNumber.toInt())) + }, + placeholder = stringResource(id = R.string.enter_number), + focusRequester = focusRequester, + keyBoardOption = KeyboardOptions(keyboardType = KeyboardType.Number), + ) + + if (state.classNumber != -1 && state.classNumber !in 1..20) { + Text( + text = stringResource(id = R.string.class_number_error), + color = WeSpotThemeManager.colors.dangerColor, + style = StaticTypeScale.Default.body8, + ) + } + } + } + + Box( + modifier = + Modifier + .fillMaxSize() + .imePadding(), + contentAlignment = Alignment.BottomCenter, + ) { + WSButton( + onClick = { }, + text = stringResource(id = R.string.next), + enabled = state.classNumber in 1..20, + ) { + it.invoke() + } + } + + LaunchedEffect(focusRequester) { + focusRequester.requestFocus() + delay(10) + keyboard?.show() + } +} diff --git a/feature/auth/src/main/kotlin/com/bff/wespot/auth/screen/GenderScreen.kt b/feature/auth/src/main/kotlin/com/bff/wespot/auth/screen/GenderScreen.kt new file mode 100644 index 00000000..fcb03b6c --- /dev/null +++ b/feature/auth/src/main/kotlin/com/bff/wespot/auth/screen/GenderScreen.kt @@ -0,0 +1,128 @@ +package com.bff.wespot.auth.screen + +import androidx.compose.foundation.Image +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 +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +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.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.bff.wespot.auth.R +import com.bff.wespot.auth.state.AuthAction +import com.bff.wespot.auth.viewmodel.AuthViewModel +import com.bff.wespot.designsystem.component.header.WSTopBar +import com.bff.wespot.designsystem.theme.StaticTypeScale +import com.bff.wespot.designsystem.theme.WeSpotThemeManager +import org.orbitmvi.orbit.compose.collectAsState + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun GenderScreen(viewModel: AuthViewModel = viewModel()) { + val state by viewModel.collectAsState() + val action = viewModel::onAction + + Scaffold( + topBar = { + WSTopBar(title = stringResource(id = R.string.register), canNavigateBack = true) + }, + modifier = Modifier.padding(horizontal = 20.dp), + ) { + Column( + modifier = Modifier.padding(it), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + text = stringResource(id = R.string.gender), + style = StaticTypeScale.Default.header1, + ) + Text( + text = stringResource(id = R.string.cannot_change_gender_after_register), + style = StaticTypeScale.Default.body6, + color = Color(0xFF7A7A7A), + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + GenderBox( + title = stringResource(id = R.string.male_student), + icon = + painterResource( + id = com.bff.wespot.ui.R.drawable.male_student, + ), + selected = "male" == state.gender, + onClicked = { + action(AuthAction.OnGenderChanged("male")) + }, + ) + GenderBox( + title = stringResource(id = R.string.female_student), + icon = + painterResource( + id = com.bff.wespot.ui.R.drawable.female_student, + ), + selected = "female" == state.gender, + onClicked = { + action(AuthAction.OnGenderChanged("female")) + }, + ) + } + } + } +} + +@Composable +private fun RowScope.GenderBox( + title: String, + icon: Painter, + selected: Boolean = false, + onClicked: () -> Unit, +) { + Box( + modifier = + Modifier + .weight(1f) + .clip(WeSpotThemeManager.shapes.medium) + .border( + width = 1.dp, + color = if (selected) WeSpotThemeManager.colors.primaryColor else Color.Transparent, + shape = WeSpotThemeManager.shapes.medium, + ) + .clickable { onClicked() } + .background(WeSpotThemeManager.colors.cardBackgroundColor), + contentAlignment = Alignment.Center, + ) { + Column( + modifier = Modifier.padding(start = 30.dp, end = 30.dp, top = 30.dp, bottom = 17.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = icon, + contentDescription = stringResource(id = R.string.gender_icon), + modifier = Modifier.size(90.dp), + ) + Spacer(modifier = Modifier.height(16.dp)) + Text(text = title, style = StaticTypeScale.Default.header2) + } + } +} diff --git a/feature/auth/src/main/kotlin/com/bff/wespot/auth/screen/GradeScreen.kt b/feature/auth/src/main/kotlin/com/bff/wespot/auth/screen/GradeScreen.kt new file mode 100644 index 00000000..bb2df36c --- /dev/null +++ b/feature/auth/src/main/kotlin/com/bff/wespot/auth/screen/GradeScreen.kt @@ -0,0 +1,195 @@ +package com.bff.wespot.auth.screen + +import androidx.compose.foundation.clickable +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.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.bff.wespot.auth.R +import com.bff.wespot.auth.state.AuthAction +import com.bff.wespot.auth.viewmodel.AuthViewModel +import com.bff.wespot.designsystem.component.button.WSButton +import com.bff.wespot.designsystem.component.button.WSOutlineButton +import com.bff.wespot.designsystem.component.header.WSTopBar +import com.bff.wespot.designsystem.theme.StaticTypeScale +import com.bff.wespot.designsystem.theme.WeSpotTheme +import com.bff.wespot.designsystem.theme.WeSpotThemeManager +import com.bff.wespot.designsystem.util.OrientationPreviews +import com.bff.wespot.ui.WSBottomSheet +import org.orbitmvi.orbit.compose.collectAsState + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun GradeScreen(viewModel: AuthViewModel = viewModel()) { + val state by viewModel.collectAsState() + val action = viewModel::onAction + + Scaffold( + topBar = { + WSTopBar(title = stringResource(id = R.string.register), canNavigateBack = true) + }, + ) { + Column( + modifier = Modifier.padding(it), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + text = stringResource(id = R.string.grade), + style = StaticTypeScale.Default.header1, + modifier = Modifier.padding(horizontal = 20.dp), + ) + + Text( + text = stringResource(id = R.string.cannot_change_grade_after_register), + style = StaticTypeScale.Default.body6, + color = Color(0xFF7A7A7A), + modifier = Modifier.padding(horizontal = 20.dp), + ) + + Box( + modifier = + Modifier + .fillMaxWidth() + .clickable { + action(AuthAction.OnGradeBottomSheetChanged(true)) + }, + ) { + WSOutlineButton( + text = "", + onClick = { + action(AuthAction.OnGradeBottomSheetChanged(true)) + }, + ) { + Box( + modifier = + Modifier + .fillMaxWidth() + .padding(20.dp), + ) { + Text( + text = + if (state.grade == -1) { + stringResource(id = R.string.select_grade) + } else { + "${state.grade}${stringResource(id = R.string.grade)}" + }, + style = StaticTypeScale.Default.body4, + color = + if (state.grade == -1) { + WeSpotThemeManager.colors.disableBtnColor + } else { + WeSpotThemeManager.colors.txtTitleColor + }, + ) + } + } + } + + if (state.gradeBottomSheet) { + WSBottomSheet( + closeSheet = { action(AuthAction.OnGradeBottomSheetChanged(false)) }, + sheetState = + rememberModalBottomSheetState( + skipPartiallyExpanded = true, + ), + ) { + BottomSheetContent(currentGrade = state.grade, onGradeSelected = { grade -> + action(AuthAction.OnGradeChanged(grade)) + }) + } + } + } + + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) { + WSButton(onClick = { }, text = stringResource(id = R.string.next)) { + it.invoke() + } + } + } +} + +@Composable +private fun BottomSheetContent( + currentGrade: Int, + onGradeSelected: (Int) -> Unit, +) { + Column( + modifier = Modifier.padding(vertical = 28.dp, horizontal = 32.dp), + ) { + Text( + text = stringResource(id = R.string.select_grade), + style = StaticTypeScale.Default.body1, + ) + Spacer(modifier = Modifier.padding(vertical = 4.dp)) + + Text( + text = stringResource(id = R.string.more_than_14_to_register), + style = StaticTypeScale.Default.body6, + color = Color(0xFF7A7A7A), + ) + + Spacer(modifier = Modifier.padding(vertical = 8.dp)) + + Column( + modifier = Modifier.fillMaxWidth(), + ) { + repeat(3) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = + Modifier + .clickable { onGradeSelected(it + 1) } + .padding(vertical = 12.dp) + .fillMaxWidth(), + ) { + Text( + text = "${it + 1}${stringResource(id = R.string.grade)}", + style = StaticTypeScale.Default.body3, + ) + + Icon( + painter = painterResource(id = com.bff.wespot.ui.R.drawable.exclude), + contentDescription = stringResource(id = R.string.check_icon), + tint = + if (it == currentGrade - 1) { + WeSpotThemeManager.colors.primaryColor + } else { + WeSpotThemeManager.colors.disableBtnColor + }, + ) + } + } + } + } +} + +@OrientationPreviews +@Composable +private fun GradeScreenPreview() { + WeSpotTheme { + Surface( + modifier = Modifier.fillMaxSize(), + ) { + GradeScreen() + } + } +} diff --git a/feature/auth/src/main/kotlin/com/bff/wespot/auth/screen/NameScreen.kt b/feature/auth/src/main/kotlin/com/bff/wespot/auth/screen/NameScreen.kt new file mode 100644 index 00000000..3fc614d4 --- /dev/null +++ b/feature/auth/src/main/kotlin/com/bff/wespot/auth/screen/NameScreen.kt @@ -0,0 +1,122 @@ +package com.bff.wespot.auth.screen + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.focus.FocusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.bff.wespot.auth.R +import com.bff.wespot.auth.state.AuthAction +import com.bff.wespot.auth.viewmodel.AuthViewModel +import com.bff.wespot.designsystem.component.button.WSButton +import com.bff.wespot.designsystem.component.header.WSTopBar +import com.bff.wespot.designsystem.component.input.WsTextField +import com.bff.wespot.designsystem.theme.StaticTypeScale +import com.bff.wespot.designsystem.theme.WeSpotThemeManager +import kotlinx.coroutines.delay +import org.orbitmvi.orbit.compose.collectAsState + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun NameScreen(viewModel: AuthViewModel = viewModel()) { + val keyboard = LocalSoftwareKeyboardController.current + + val state by viewModel.collectAsState() + val action = viewModel::onAction + + val focusRequester = + remember { + FocusRequester() + } + + var error by remember { + mutableStateOf(false) + } + + Scaffold( + topBar = { + WSTopBar(title = stringResource(id = R.string.register), canNavigateBack = true) + }, + modifier = Modifier.padding(horizontal = 20.dp), + ) { + Column( + modifier = Modifier.padding(it), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + text = stringResource(id = R.string.name), + style = StaticTypeScale.Default.header1, + ) + + Text( + text = stringResource(id = R.string.cannot_change_name_after_register), + style = StaticTypeScale.Default.body6, + color = Color(0xFF7A7A7A), + ) + + WsTextField( + value = state.name, + onValueChange = { name -> + if (name.length > 5) { + error = true + return@WsTextField + } + error = false + action(AuthAction.OnNameChanged(name)) + }, + placeholder = stringResource(id = R.string.enter_name), + focusRequester = focusRequester, + ) + + if (error) { + Text( + text = stringResource(id = R.string.name_error), + color = WeSpotThemeManager.colors.dangerColor, + style = StaticTypeScale.Default.body6, + ) + } + + Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.TopEnd) { + Text( + text = "${state.name.length} / 5", + color = Color(0xFF7A7A7A), + style = StaticTypeScale.Default.body7, + ) + } + } + } + + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) { + WSButton( + onClick = {}, + text = stringResource(id = R.string.next), + enabled = state.name.length in 2..5, + ) { + it.invoke() + } + } + + LaunchedEffect(key1 = focusRequester) { + focusRequester.requestFocus() + delay(10) + keyboard?.show() + } +} diff --git a/feature/auth/src/main/kotlin/com/bff/wespot/auth/screen/SchoolScreen.kt b/feature/auth/src/main/kotlin/com/bff/wespot/auth/screen/SchoolScreen.kt new file mode 100644 index 00000000..54afc217 --- /dev/null +++ b/feature/auth/src/main/kotlin/com/bff/wespot/auth/screen/SchoolScreen.kt @@ -0,0 +1,151 @@ +package com.bff.wespot.auth.screen + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.bff.wespot.auth.R +import com.bff.wespot.auth.state.AuthAction +import com.bff.wespot.auth.viewmodel.AuthViewModel +import com.bff.wespot.designsystem.component.button.WSButton +import com.bff.wespot.designsystem.component.button.WSTextButton +import com.bff.wespot.designsystem.component.button.WSTextButtonType +import com.bff.wespot.designsystem.component.header.WSTopBar +import com.bff.wespot.designsystem.component.input.WsTextField +import com.bff.wespot.designsystem.component.input.WsTextFieldType +import com.bff.wespot.designsystem.theme.StaticTypeScale +import com.bff.wespot.designsystem.theme.WeSpotTheme +import com.bff.wespot.designsystem.theme.WeSpotThemeManager +import com.bff.wespot.designsystem.util.OrientationPreviews +import com.bff.wespot.ui.SchoolListItem +import kotlinx.coroutines.delay +import org.orbitmvi.orbit.compose.collectAsState + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SchoolScreen(viewModel: AuthViewModel = viewModel()) { + val keyboard = LocalSoftwareKeyboardController.current + val focusRequester = remember { FocusRequester() } + + val state by viewModel.collectAsState() + val action = viewModel::onAction + + Scaffold( + topBar = { + WSTopBar(title = stringResource(id = R.string.register)) + }, + ) { + Column( + modifier = + Modifier + .fillMaxSize() + .padding(it) + .padding(horizontal = 24.dp), + ) { + Text( + stringResource(id = R.string.search_school), + style = StaticTypeScale.Default.header1, + ) + Spacer(modifier = Modifier.padding(8.dp)) + Text( + stringResource(id = R.string.search_base_on_your_school), + style = StaticTypeScale.Default.body8, + color = Color(0xFF7A7A7A), + ) + Spacer(modifier = Modifier.padding(12.dp)) + + WsTextField( + value = state.schoolName, + onValueChange = { + action(AuthAction.OnSchoolSearchChanged(it)) + }, + placeholder = stringResource(id = R.string.search_with_school_name), + textFieldType = WsTextFieldType.Search, + focusRequester = focusRequester, + singleLine = true, + ) + + Spacer(modifier = Modifier.padding(vertical = 8.dp)) + if (state.schoolName.length >= 20) { + Text( + stringResource(id = R.string.within_20_characters), + style = StaticTypeScale.Default.body8, + color = WeSpotThemeManager.colors.dangerColor, + ) + } + + if (state.schoolSearchList.isEmpty()) { + Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + WSTextButton( + text = stringResource(id = R.string.no_school_found), + onClick = { }, + buttonType = WSTextButtonType.Underline, + ) + } + } + + LazyColumn { + items(state.schoolSearchList, key = { school -> + school.id + }) { school -> + SchoolListItem( + schoolName = school.name, + address = school.address, + selected = state.selectedSchool?.name == school.name, + ) { + action(AuthAction.OnSchoolSelected(school)) + } + } + } + } + } + + Box( + modifier = + Modifier + .fillMaxSize() + .imePadding(), + contentAlignment = Alignment.BottomCenter, + ) { + WSButton(onClick = { }, enabled = false, text = stringResource(id = R.string.next)) { + it() + } + } + + LaunchedEffect(focusRequester) { + focusRequester.requestFocus() + delay(10) + keyboard?.show() + } +} + +@OrientationPreviews +@Composable +private fun SchoolScreenPreview() { + WeSpotTheme { + Surface(modifier = Modifier.fillMaxSize()) { + SchoolScreen() + } + } +} diff --git a/feature/auth/src/main/kotlin/com/bff/wespot/auth/state/AuthAction.kt b/feature/auth/src/main/kotlin/com/bff/wespot/auth/state/AuthAction.kt new file mode 100644 index 00000000..6528adca --- /dev/null +++ b/feature/auth/src/main/kotlin/com/bff/wespot/auth/state/AuthAction.kt @@ -0,0 +1,19 @@ +package com.bff.wespot.auth.state + +import com.bff.wespot.model.SchoolItem + +sealed class AuthAction { + data class OnSchoolSearchChanged(val text: String) : AuthAction() + + data class OnSchoolSelected(val school: SchoolItem) : AuthAction() + + data class OnGradeChanged(val grade: Int) : AuthAction() + + data class OnGradeBottomSheetChanged(val isOpen: Boolean) : AuthAction() + + data class OnClassNumberChanged(val number: Int) : AuthAction() + + data class OnGenderChanged(val gender: String) : AuthAction() + + data class OnNameChanged(val name: String) : AuthAction() +} diff --git a/feature/auth/src/main/kotlin/com/bff/wespot/auth/state/AuthSideEffect.kt b/feature/auth/src/main/kotlin/com/bff/wespot/auth/state/AuthSideEffect.kt new file mode 100644 index 00000000..996b90d8 --- /dev/null +++ b/feature/auth/src/main/kotlin/com/bff/wespot/auth/state/AuthSideEffect.kt @@ -0,0 +1,3 @@ +package com.bff.wespot.auth.state + +sealed class AuthSideEffect diff --git a/feature/auth/src/main/kotlin/com/bff/wespot/auth/state/AuthUiState.kt b/feature/auth/src/main/kotlin/com/bff/wespot/auth/state/AuthUiState.kt new file mode 100644 index 00000000..b7c652f8 --- /dev/null +++ b/feature/auth/src/main/kotlin/com/bff/wespot/auth/state/AuthUiState.kt @@ -0,0 +1,15 @@ +package com.bff.wespot.auth.state + +import com.bff.wespot.model.SchoolItem + +data class AuthUiState( + val schoolName: String = "", + val schoolList: List = emptyList(), + val schoolSearchList: List = emptyList(), + val selectedSchool: SchoolItem? = null, + val grade: Int = -1, + val gradeBottomSheet: Boolean = true, + val classNumber: Int = -1, + val gender: String = "", + val name: String = "", +) diff --git a/feature/auth/src/main/kotlin/com/bff/wespot/auth/viewmodel/AuthViewModel.kt b/feature/auth/src/main/kotlin/com/bff/wespot/auth/viewmodel/AuthViewModel.kt new file mode 100644 index 00000000..f106c77e --- /dev/null +++ b/feature/auth/src/main/kotlin/com/bff/wespot/auth/viewmodel/AuthViewModel.kt @@ -0,0 +1,98 @@ +package com.bff.wespot.auth.viewmodel + +import androidx.lifecycle.ViewModel +import com.bff.wespot.auth.state.AuthAction +import com.bff.wespot.auth.state.AuthSideEffect +import com.bff.wespot.auth.state.AuthUiState +import com.bff.wespot.model.SchoolItem +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.reduce +import org.orbitmvi.orbit.viewmodel.container + +class AuthViewModel : ViewModel(), ContainerHost { + override val container = container(AuthUiState()) + + fun onAction(action: AuthAction) { + when (action) { + is AuthAction.OnSchoolSearchChanged -> handleSchoolSearchChanged(action.text) + is AuthAction.OnSchoolSelected -> handleSchoolSelected(action.school) + is AuthAction.OnGradeBottomSheetChanged -> handleGradeBottomSheetChanged(action.isOpen) + is AuthAction.OnGradeChanged -> handleGradeChanged(action.grade) + is AuthAction.OnClassNumberChanged -> handleClassNumberChanged(action.number) + is AuthAction.OnGenderChanged -> handleGenderChanged(action.gender) + is AuthAction.OnNameChanged -> handleNameChanged(action.name) + else -> {} + } + } + + private fun handleSchoolSearchChanged(text: String) = + intent { + reduce { + state.copy( + schoolName = text, + schoolSearchList = + state.schoolList.filter { + it.name.contains( + text, + ignoreCase = true, + ) + }, + ) + } + } + + private fun handleSchoolSelected(school: SchoolItem) = + intent { + reduce { + state.copy( + selectedSchool = school, + ) + } + } + + private fun handleGradeBottomSheetChanged(isOpen: Boolean) = + intent { + reduce { + state.copy( + gradeBottomSheet = isOpen, + ) + } + } + + private fun handleGradeChanged(grade: Int) = + intent { + reduce { + state.copy( + grade = grade, + ) + } + } + + private fun handleClassNumberChanged(number: Int) = + intent { + reduce { + state.copy( + classNumber = number, + ) + } + } + + private fun handleGenderChanged(gender: String) = + intent { + reduce { + state.copy( + gender = gender, + ) + } + } + + private fun handleNameChanged(name: String) = + intent { + reduce { + state.copy( + name = name, + ) + } + } +} diff --git a/feature/auth/src/main/res/values/string.xml b/feature/auth/src/main/res/values/string.xml new file mode 100644 index 00000000..aa3c32f0 --- /dev/null +++ b/feature/auth/src/main/res/values/string.xml @@ -0,0 +1,30 @@ + + + 회원가입 + 학교 검색 + 현재 재학 중인 학교 기준으로 검색해 주세요 + 20자 이내로 검색해 주세요 + 학교 이름으로 검색해 보세요 + 찾는 학교가 없다면? + 학교 + 다음 + 학년 + 회원가입 이후에는 학년을 변경할 수 없어요 + 햔재 학년을 선택해 주세요 + 만 14세 미만 학생은 가입이 어려워요 + 체크 아이콘 + + 회원가입 이후에는 반을 변경할 수 없어요 + 숫자로 입력해 주세요 + 정확한 반을 입력해주세요 + 성별 + 회원가입 이후에는 성별을 변경할 수 없어요 + 성별 아이콘 + 남학생 + 여학생 + 이름 + 회원가입 이후에는 이름을 변경할 수 없어요 + 실명을 입력해 주세요 + 2~5자의 한글만 입력 가능해요 + 확인 + \ No newline at end of file