diff --git a/core/designsystem/src/main/res/drawable/image_jeju_success.png b/core/designsystem/src/main/res/drawable/image_jeju_success.png new file mode 100644 index 00000000..8a8b8651 Binary files /dev/null and b/core/designsystem/src/main/res/drawable/image_jeju_success.png differ diff --git a/core/designsystem/src/main/res/drawable/image_onboarding_jeju.png b/core/designsystem/src/main/res/drawable/image_onboarding_jeju.png new file mode 100644 index 00000000..71af63aa Binary files /dev/null and b/core/designsystem/src/main/res/drawable/image_onboarding_jeju.png differ diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml index 30298fab..4757ff2b 100644 --- a/core/designsystem/src/main/res/values/strings.xml +++ b/core/designsystem/src/main/res/values/strings.xml @@ -1,3 +1,12 @@ mission-mate + + + + + + + + + \ No newline at end of file diff --git a/core/navigation/src/main/java/com/goalpanzi/mission_mate/core/navigation/RouteModel.kt b/core/navigation/src/main/java/com/goalpanzi/mission_mate/core/navigation/RouteModel.kt index 7b36c68e..6708b8bd 100644 --- a/core/navigation/src/main/java/com/goalpanzi/mission_mate/core/navigation/RouteModel.kt +++ b/core/navigation/src/main/java/com/goalpanzi/mission_mate/core/navigation/RouteModel.kt @@ -14,6 +14,9 @@ sealed interface OnboardingRouteModel { @Serializable data object BoardSetup : OnboardingRouteModel + @Serializable + data object BoardSetupSuccess : OnboardingRouteModel + @Serializable data object InvitationCode : OnboardingRouteModel } \ No newline at end of file diff --git a/feature/main/src/main/AndroidManifest.xml b/feature/main/src/main/AndroidManifest.xml index 703653c4..5d913de9 100644 --- a/feature/main/src/main/AndroidManifest.xml +++ b/feature/main/src/main/AndroidManifest.xml @@ -4,7 +4,8 @@ + android:theme="@style/Theme.Missionmate" + android:windowSoftInputMode="adjustResize"> diff --git a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavHost.kt b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavHost.kt index fc0cb642..12156545 100644 --- a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavHost.kt +++ b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavHost.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.Modifier import androidx.navigation.compose.NavHost import com.goalpanzi.mission_mate.feature.login.loginNavGraph import com.goalpanzi.mission_mate.feature.onboarding.boardSetupNavGraph +import com.goalpanzi.mission_mate.feature.onboarding.boardSetupSuccessNavGraph import com.goalpanzi.mission_mate.feature.onboarding.invitationCodeNavGraph import com.goalpanzi.mission_mate.feature.onboarding.onboardingNavGraph @@ -32,11 +33,23 @@ internal fun MainNavHost( onBackClick = { navigator.popBackStack() } ) onboardingNavGraph( - onClickBoardSetup = { }, - onClickInvitationCode = { }, + onClickBoardSetup = { navigator.navigationToBoardSetup() }, + onClickInvitationCode = { navigator.navigationToInvitationCode() }, onClickSetting = { } ) - boardSetupNavGraph() + boardSetupNavGraph( + onSuccess = { + navigator.navigationToBoardSetupSuccess() + }, + onBackClick = { + navigator.popBackStack() + } + ) + boardSetupSuccessNavGraph( + onClickStart = { + + } + ) invitationCodeNavGraph() } } diff --git a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavigator.kt b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavigator.kt index 7e4ca198..8b24a29e 100644 --- a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavigator.kt +++ b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavigator.kt @@ -7,6 +7,7 @@ import androidx.navigation.compose.rememberNavController import com.goalpanzi.mission_mate.core.navigation.RouteModel import com.goalpanzi.mission_mate.feature.login.navigateToLogin import com.goalpanzi.mission_mate.feature.onboarding.navigateToBoardSetup +import com.goalpanzi.mission_mate.feature.onboarding.navigateToBoardSetupSuccess import com.goalpanzi.mission_mate.feature.onboarding.navigateToInvitationCode import com.goalpanzi.mission_mate.feature.onboarding.navigateToOnboarding @@ -33,6 +34,10 @@ class MainNavigator( navController.navigateToBoardSetup() } + fun navigationToBoardSetupSuccess() { + navController.navigateToBoardSetupSuccess() + } + fun navigationToInvitationCode() { navController.navigateToInvitationCode() } diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/OnboardingNavigation.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/OnboardingNavigation.kt index 6d29deb4..4d067059 100644 --- a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/OnboardingNavigation.kt +++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/OnboardingNavigation.kt @@ -5,6 +5,9 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import com.goalpanzi.mission_mate.core.navigation.OnboardingRouteModel import com.goalpanzi.mission_mate.core.navigation.RouteModel +import com.goalpanzi.mission_mate.feature.onboarding.screen.OnboardingRoute +import com.goalpanzi.mission_mate.feature.onboarding.screen.boardsetup.BoardSetupRoute +import com.goalpanzi.mission_mate.feature.onboarding.screen.boardsetup.BoardSetupSuccessScreen fun NavController.navigateToOnboarding() { this.navigate(RouteModel.Onboarding) @@ -14,14 +17,18 @@ fun NavController.navigateToBoardSetup() { this.navigate(OnboardingRouteModel.BoardSetup) } +fun NavController.navigateToBoardSetupSuccess() { + this.navigate(OnboardingRouteModel.BoardSetupSuccess) +} + fun NavController.navigateToInvitationCode() { this.navigate(OnboardingRouteModel.InvitationCode) } fun NavGraphBuilder.onboardingNavGraph( - onClickBoardSetup : () -> Unit, - onClickInvitationCode : () -> Unit, - onClickSetting : () -> Unit + onClickBoardSetup: () -> Unit, + onClickInvitationCode: () -> Unit, + onClickSetting: () -> Unit ) { composable { OnboardingRoute( @@ -32,14 +39,29 @@ fun NavGraphBuilder.onboardingNavGraph( } } -fun NavGraphBuilder.boardSetupNavGraph() { +fun NavGraphBuilder.boardSetupNavGraph( + onSuccess: () -> Unit, + onBackClick: () -> Unit +) { composable { + BoardSetupRoute( + onSuccess = onSuccess, + onBackClick = onBackClick + ) + } +} +fun NavGraphBuilder.boardSetupSuccessNavGraph( + onClickStart: () -> Unit +) { + composable { + BoardSetupSuccessScreen( + onClickStart = onClickStart + ) } } fun NavGraphBuilder.invitationCodeNavGraph() { composable { - } } \ No newline at end of file diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/BoardSetupNavigationBar.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/BoardSetupNavigationBar.kt new file mode 100644 index 00000000..046ab23d --- /dev/null +++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/BoardSetupNavigationBar.kt @@ -0,0 +1,67 @@ +package com.goalpanzi.mission_mate.feature.onboarding.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowLeft +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249 +import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography +import com.goalpanzi.mission_mate.feature.onboarding.R + +@Composable +fun BoardSetupNavigationBar( + onBackClick: () -> Unit, + currentStep: () -> Int, + modifier: Modifier = Modifier, + maxStep: Int = 3, +) { + Column( + modifier = modifier + ) { + IconButton( + modifier = Modifier.padding(start = 4.dp), + onClick = onBackClick + ) { + Icon( + modifier = Modifier.size(24.dp), + imageVector = Icons.Default.KeyboardArrowLeft, // merge 전까지 임시 사용 + contentDescription = null + ) + } + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource( + id = when (currentStep()) { + 1 -> R.string.onboarding_board_setup_mission_title + 2 -> R.string.onboarding_board_setup_schedule_title + else -> R.string.onboarding_board_setup_verification_time_title + } + ), + modifier = Modifier + .wrapContentHeight() + .weight(1f) + .padding(start = 8.dp, end = 8.dp), + style = MissionMateTypography.heading_sm_bold, + color = ColorGray1_FF404249 + ) + OutlinedTextBox( + text = "${currentStep()}/$maxStep", + textStyle = MissionMateTypography.body_lg_regular + ) + } + } +} \ No newline at end of file diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/DatePickerDialog.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/DatePickerDialog.kt new file mode 100644 index 00000000..960f71b4 --- /dev/null +++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/DatePickerDialog.kt @@ -0,0 +1,75 @@ +package com.goalpanzi.mission_mate.feature.onboarding.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.DatePicker +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SelectableDates +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.rememberDatePickerState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateButton +import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateButtonType +import com.goalpanzi.mission_mate.feature.onboarding.util.DateUtils.localDateToMillis +import java.time.LocalDate + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DatePickerDialog( + selectedDate: LocalDate?, + selectableStartDate: LocalDate?, + selectableEndDate: LocalDate?, + onSuccess: (Long) -> Unit, + onDismiss: () -> Unit +) { + val datePickerState = rememberDatePickerState( + initialSelectedDateMillis = selectedDate?.let { localDateToMillis(it) }, + selectableDates = object : SelectableDates { + override fun isSelectableDate(utcTimeMillis: Long): Boolean { + val startMillis = localDateToMillis(selectableStartDate) + val endMillis = localDateToMillis(selectableEndDate) + val currentMillis = utcTimeMillis + return (startMillis ?: Long.MIN_VALUE) <= currentMillis && (endMillis + ?: Long.MAX_VALUE) >= currentMillis + } + } + ) + Dialog( + properties = DialogProperties(usePlatformDefaultWidth = false), + onDismissRequest = onDismiss + ) { + Surface( + modifier = Modifier + .padding(horizontal = 4.dp, vertical = 12.dp) + .fillMaxWidth() + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + + DatePicker(state = datePickerState) + MissionMateButton( + buttonType = if (datePickerState.selectedDateMillis == null) MissionMateButtonType.DISABLED + else MissionMateButtonType.SECONDARY, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 12.dp), + onClick = { + datePickerState.selectedDateMillis?.let { onSuccess(it) } + } + ) { + Text(text = "Ok") + } + } + } + } +} \ No newline at end of file diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/VerificationTimeType.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/VerificationTimeType.kt new file mode 100644 index 00000000..f042ca25 --- /dev/null +++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/VerificationTimeType.kt @@ -0,0 +1,12 @@ +package com.goalpanzi.mission_mate.feature.onboarding.model + +import androidx.annotation.StringRes +import com.goalpanzi.mission_mate.feature.onboarding.R + +enum class VerificationTimeType( + @StringRes val titleId : Int +) { + Am(R.string.onboarding_board_setup_verification_time_input_content_am), + Pm(R.string.onboarding_board_setup_verification_time_input_content_pm), + All(R.string.onboarding_board_setup_verification_time_input_content_all) +} \ No newline at end of file diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/OnboardingScreen.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/OnboardingScreen.kt similarity index 95% rename from feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/OnboardingScreen.kt rename to feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/OnboardingScreen.kt index b9acaf1e..7e2a2b85 100644 --- a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/OnboardingScreen.kt +++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/OnboardingScreen.kt @@ -1,4 +1,4 @@ -package com.goalpanzi.mission_mate.feature.onboarding +package com.goalpanzi.mission_mate.feature.onboarding.screen import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -9,6 +9,7 @@ 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.statusBarsPadding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -24,6 +25,7 @@ import androidx.compose.ui.unit.dp import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249 import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography +import com.goalpanzi.mission_mate.feature.onboarding.R import com.goalpanzi.mission_mate.feature.onboarding.component.OnboardingNavigationButton import com.goalpanzi.mission_mate.feature.onboarding.component.OutlinedTextBox import com.goalpanzi.mission_mate.feature.onboarding.component.StableImage @@ -63,12 +65,12 @@ fun OnboardingScreen( contentScale = ContentScale.FillWidth ) Column( - modifier = modifier, + modifier = modifier.statusBarsPadding(), horizontalAlignment = Alignment.CenterHorizontally ) { IconButton( modifier = Modifier - .padding(end = 10.dp, top = 24.dp) + .padding(end = 10.dp) .align(Alignment.End), onClick = onClickSetting ) { @@ -85,7 +87,7 @@ fun OnboardingScreen( color = ColorGray1_FF404249 ) OutlinedTextBox( - text = stringResource(id = R.string.onboarding_level_1), + text = stringResource(id = R.string.onboarding_level_1), modifier = Modifier.padding(bottom = 23.dp) ) Box( diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupMission.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupMission.kt new file mode 100644 index 00000000..10642630 --- /dev/null +++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupMission.kt @@ -0,0 +1,43 @@ +package com.goalpanzi.mission_mate.feature.onboarding.screen.boardsetup + +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.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateTextFieldGroup +import com.goalpanzi.mission_mate.feature.onboarding.R + +@Composable +fun BoardSetupMission( + missionTitle : String, + onTitleChange : (String) -> Unit, + modifier: Modifier = Modifier +){ + Column( + modifier = modifier + .fillMaxSize() + .padding(horizontal = 24.dp) + ) { + BoardSetupDescription( + text = stringResource(id = R.string.onboarding_board_setup_mission_description), + colorTargetTexts = listOf( + stringResource(R.string.onboarding_board_setup_mission_description_color_target1), + stringResource(R.string.onboarding_board_setup_mission_description_color_target2) + ) + ) + MissionMateTextFieldGroup( + modifier = modifier.fillMaxWidth(), + text = missionTitle, + onValueChange = onTitleChange, + useMaxLength = true, + maxLength = 12, + titleId = R.string.onboarding_board_setup_mission_input_title, + hintId = R.string.onboarding_board_setup_mission_input_hint, + guidanceId = R.string.onboarding_board_setup_mission_input_guide, + ) + } +} diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupSchedule.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupSchedule.kt new file mode 100644 index 00000000..36915ced --- /dev/null +++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupSchedule.kt @@ -0,0 +1,228 @@ +package com.goalpanzi.mission_mate.feature.onboarding.screen.boardsetup + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +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.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorBlack_FF000000 +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249 +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray2_FF4F505C +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray3_FF727484 +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray4_FFE5E5E5 +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray5_80F5F6F9 +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray5_FFF5F6F9 +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF +import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography +import com.goalpanzi.mission_mate.feature.onboarding.R +import com.goalpanzi.mission_mate.feature.onboarding.util.getStringId +import java.time.DayOfWeek + +@Composable +fun BoardSetupSchedule( + startDate: String, + endDate: String, + selectedDays: List, + enabledDaysOfWeek : Set, + count : String, + onClickStartDate: () -> Unit, + onClickEndDate: () -> Unit, + onSelectDay: (DayOfWeek) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxSize() + .padding(horizontal = 24.dp) + ) { + BoardSetupDescription( + text = stringResource(id = R.string.onboarding_board_setup_schedule_description,count), + colorTargetTexts = listOf( + stringResource(R.string.onboarding_board_setup_schedule_description_color_target1), + stringResource(R.string.onboarding_board_setup_schedule_description_color_target2) + ), + count = count + stringResource(id = R.string.onboarding_board_setup_schedule_description_style_target) + ) + Period( + startDate = startDate, + endDate = endDate, + modifier = Modifier.fillMaxWidth(), + onClickStartDate = onClickStartDate, + onClickEndDate = onClickEndDate + ) + Frequency( + modifier = Modifier + .padding(top = 40.dp) + .fillMaxWidth(), + enabledDaysOfWeek = enabledDaysOfWeek, + selectedDays = selectedDays, + onClickDay = onSelectDay + ) + } +} + +@Composable +fun Period( + startDate: String, + endDate: String, + modifier: Modifier = Modifier, + onClickStartDate: () -> Unit, + onClickEndDate: () -> Unit +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = stringResource(id = R.string.onboarding_board_setup_schedule_period_input_title), + style = MissionMateTypography.body_md_bold, + color = ColorGray3_FF727484 + ) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Button( + modifier = Modifier + .height(60.dp) + .weight(1f), + border = if(startDate.isBlank()) null else BorderStroke(1.dp, ColorGray4_FFE5E5E5) , + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.buttonColors( + containerColor = if(startDate.isBlank()) ColorGray5_80F5F6F9 else ColorWhite_FFFFFFFF + ), + contentPadding = PaddingValues(horizontal = 16.dp), + onClick = onClickStartDate + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = startDate.ifBlank { stringResource(id = R.string.onboarding_board_setup_schedule_period_start_hint) }, + color = if(startDate.isBlank()) ColorGray3_FF727484 else ColorGray1_FF404249, + style = MissionMateTypography.body_lg_regular + ) + } + Text( + modifier = Modifier.padding(horizontal = 7.dp), + text = "~", + color = ColorBlack_FF000000, + style = MissionMateTypography.body_lg_regular + ) + Button( + modifier = Modifier + .height(60.dp) + .weight(1f), + border = if(endDate.isBlank()) null else BorderStroke(1.dp, ColorGray4_FFE5E5E5) , + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.buttonColors( + containerColor = if(endDate.isBlank()) ColorGray5_80F5F6F9 else ColorWhite_FFFFFFFF + ), + contentPadding = PaddingValues(horizontal = 16.dp), + onClick = onClickEndDate + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = endDate.ifBlank { stringResource(id = R.string.onboarding_board_setup_schedule_period_end_hint) }, + color = if(endDate.isBlank()) ColorGray3_FF727484 else ColorGray1_FF404249, + style = MissionMateTypography.body_lg_regular + ) + } + } + Text( + text = stringResource(id = R.string.onboarding_board_setup_schedule_period_input_guide), + style = MissionMateTypography.body_md_regular, + color = ColorGray2_FF4F505C + ) + } +} + +@Composable +fun Frequency( + selectedDays: List, + enabledDaysOfWeek : Set, + onClickDay: (DayOfWeek) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.alpha( + if(enabledDaysOfWeek.isNotEmpty()) 1f else 0.3f + ), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + modifier = Modifier.padding(bottom = 4.dp), + text = stringResource(id = R.string.onboarding_board_setup_schedule_day_input_title), + style = MissionMateTypography.body_md_bold, + color = ColorGray3_FF727484 + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.5.dp) + ) { + DayOfWeek.entries.forEach { + DayItem( + modifier = Modifier + .weight(1f) + .aspectRatio(1f), + dayOfWeek = it, + enabled = enabledDaysOfWeek.contains(it), + selected = it in selectedDays, + onClick = { + onClickDay(it) + } + ) + } + } + Text( + text = stringResource(id = R.string.onboarding_board_setup_schedule_day_input_guide), + style = MissionMateTypography.body_md_regular, + color = ColorGray2_FF4F505C + ) + } +} + + +@Composable +fun DayItem( + dayOfWeek : DayOfWeek, + enabled : Boolean, + onClick : () -> Unit, + modifier: Modifier = Modifier, + shape: Shape = CircleShape, + selected : Boolean = false +){ + TextButton( + modifier = modifier, + contentPadding = PaddingValues(horizontal = 16.dp), + shape = shape, + colors = ButtonDefaults.textButtonColors( + containerColor = if(selected) ColorGray1_FF404249 else ColorGray5_FFF5F6F9, + disabledContainerColor = ColorGray5_FFF5F6F9.copy(0.3f) + ), + enabled = enabled, + onClick = onClick, + ) { + Text( + text = stringResource(id = dayOfWeek.getStringId()), + color = if (selected) ColorWhite_FFFFFFFF + else ColorGray1_FF404249, + style = MissionMateTypography.body_lg_regular + ) + } +} \ No newline at end of file diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupScreen.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupScreen.kt new file mode 100644 index 00000000..0b9ddc4e --- /dev/null +++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupScreen.kt @@ -0,0 +1,271 @@ +package com.goalpanzi.mission_mate.feature.onboarding.screen.boardsetup + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +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.layout.statusBarsPadding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.ExperimentalMaterial3Api +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.Modifier +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateButtonType +import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateTextButton +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray2_FF4F505C +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorOrange_FFFF5732 +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF +import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography +import com.goalpanzi.mission_mate.feature.onboarding.R +import com.goalpanzi.mission_mate.feature.onboarding.component.BoardSetupNavigationBar +import com.goalpanzi.mission_mate.feature.onboarding.component.DatePickerDialog +import com.goalpanzi.mission_mate.feature.onboarding.model.VerificationTimeType +import com.goalpanzi.mission_mate.feature.onboarding.screen.boardsetup.BoardSetupViewModel.Companion.BoardSetupStep +import com.goalpanzi.mission_mate.feature.onboarding.util.DateUtils.dateToString +import com.goalpanzi.mission_mate.feature.onboarding.util.DateUtils.filterDatesByDayOfWeek +import com.goalpanzi.mission_mate.feature.onboarding.util.styledTextWithHighlights +import java.time.DayOfWeek +import java.time.LocalDate + +@Composable +fun BoardSetupRoute( + onSuccess : () -> Unit, + onBackClick: () -> Unit, + viewModel: BoardSetupViewModel = hiltViewModel() +) { + val keyboardController = LocalSoftwareKeyboardController.current + val localFocusManager = LocalFocusManager.current + val pagerState = rememberPagerState { BoardSetupStep.entries.size } + + val startDate by viewModel.startDate.collectAsStateWithLifecycle() + val endDate by viewModel.endDate.collectAsStateWithLifecycle() + val selectedDays by viewModel.selectedDays.collectAsStateWithLifecycle() + val selectedVerificationTimeType by viewModel.selectedVerificationTimeType.collectAsStateWithLifecycle() + + val enabledDaysOfWeek by viewModel.enabledDaysOfWeek.collectAsStateWithLifecycle() + val enabledButton by viewModel.enabledButton.collectAsStateWithLifecycle() + val currentStep by viewModel.currentStep.collectAsStateWithLifecycle() + + var isShownStartDateDialog by remember { mutableStateOf(false) } + var isShownEndDateDialog by remember { mutableStateOf(false) } + + if (isShownStartDateDialog) { + DatePickerDialog( + selectedDate = startDate, + onSuccess = { + viewModel.updateStartDate(it) + isShownStartDateDialog = !isShownStartDateDialog + }, + selectableStartDate = LocalDate.now().plusDays(1), + selectableEndDate = null, + onDismiss = { isShownStartDateDialog = !isShownStartDateDialog } + ) + } + + if (isShownEndDateDialog) { + DatePickerDialog( + selectedDate = endDate, + onSuccess = { + viewModel.updateEndDate(it) + isShownEndDateDialog = !isShownEndDateDialog + }, + selectableStartDate = startDate, + selectableEndDate = startDate?.plusDays(29), + onDismiss = { isShownEndDateDialog = !isShownEndDateDialog } + ) + } + + LaunchedEffect(currentStep) { + if(currentStep != BoardSetupStep.MISSION){ + localFocusManager.clearFocus() + keyboardController?.hide() + } + if(currentStep.ordinal in 0 until pagerState.pageCount) + pagerState.animateScrollToPage(currentStep.ordinal) + } + + LaunchedEffect(key1 = Unit) { + viewModel.setupSuccessEvent.collect { + onSuccess() + } + } + + BoardSetupScreen( + currentStep = currentStep, + missionTitle = viewModel.missionTitle, + startDate = startDate?.let { + dateToString(it) + } ?: "", + endDate = endDate?.let { + dateToString(it) + } ?: "", + selectedDays = selectedDays, + count = filterDatesByDayOfWeek(startDate, endDate, selectedDays), + selectedVerificationTimeType = selectedVerificationTimeType, + enabledDaysOfWeek = enabledDaysOfWeek, + enabledButton = enabledButton, + pagerState = pagerState, + onClickNextStep = viewModel::updateCurrentStepToNext, + onMissionTitleChange = viewModel::updateMissionTitle, + onClickStartDate = { + isShownStartDateDialog = true + }, + onClickEndDate = { + isShownEndDateDialog = true + }, + onClickDayOfWeek = viewModel::updateSelectedDays, + onClickVerificationTimeType = viewModel::updateSelectedVerificationTimeType, + onBackClick = { + when(currentStep){ + BoardSetupStep.MISSION -> { + onBackClick() + } + else -> { + viewModel.updateCurrentStepToBack() + } + } + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BoardSetupScreen( + currentStep: BoardSetupStep, + missionTitle: String, + startDate: String, + endDate: String, + selectedDays : List, + count : Int, + selectedVerificationTimeType: VerificationTimeType?, + enabledDaysOfWeek : Set, + enabledButton: Boolean, + pagerState : PagerState, + onClickNextStep: () -> Unit, + onMissionTitleChange: (String) -> Unit, + onClickStartDate: () -> Unit, + onClickEndDate: () -> Unit, + onClickDayOfWeek : (DayOfWeek) -> Unit, + onClickVerificationTimeType : (VerificationTimeType) -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxSize() + .background(ColorWhite_FFFFFFFF) + .statusBarsPadding() + .imePadding() + ) { + BoardSetupNavigationBar( + onBackClick = onBackClick, + currentStep = { + currentStep.ordinal + 1 + } + ) + + HorizontalPager( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + state = pagerState, + userScrollEnabled = false + ) { + when (it) { + 0 -> { + BoardSetupMission( + missionTitle = missionTitle, + onTitleChange = onMissionTitleChange, + ) + } + + 1 -> { + BoardSetupSchedule( + startDate = startDate, + endDate = endDate, + count = "$count".padStart(2,'0'), + enabledDaysOfWeek = enabledDaysOfWeek, + onClickStartDate = onClickStartDate, + onClickEndDate = onClickEndDate, + selectedDays = selectedDays, + onSelectDay = onClickDayOfWeek + ) + } + + 2 -> { + BoardSetupVerificationTime( + selectedTimeType = selectedVerificationTimeType, + onClickTime = onClickVerificationTimeType + ) + } + } + } + MissionMateTextButton( + modifier = Modifier + .padding(vertical = 36.dp, horizontal = 24.dp) + .fillMaxWidth() + .wrapContentHeight(), + buttonType = if (enabledButton) MissionMateButtonType.ACTIVE else MissionMateButtonType.DISABLED, + textId = if (currentStep.ordinal == 2) R.string.done else R.string.next, + onClick = { + onClickNextStep() + } + ) + } +} + +@Composable +fun ColumnScope.BoardSetupDescription( + text: String, + colorTargetTexts: List +) { + Text( + modifier = Modifier.padding(top = 6.dp, bottom = 60.dp), + text = styledTextWithHighlights( + text = text, + colorTargetTexts = colorTargetTexts, + textColor = ColorGray2_FF4F505C, + targetTextColor = ColorOrange_FFFF5732, + targetFontWeight = FontWeight.Bold, + ), + style = MissionMateTypography.title_xl_regular + ) +} + +@Composable +fun ColumnScope.BoardSetupDescription( + text: String, + count : String, + colorTargetTexts: List +) { + Text( + modifier = Modifier.padding(top = 6.dp, bottom = 60.dp), + text = styledTextWithHighlights( + text = text, + colorTargetTexts = colorTargetTexts, + weightTargetTexts = listOf(count), + underlineTargetTexts = listOf(count), + textColor = ColorGray2_FF4F505C, + targetTextColor = ColorOrange_FFFF5732, + targetFontWeight = FontWeight.Bold, + ), + style = MissionMateTypography.title_xl_regular + ) +} \ No newline at end of file diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupSuccessScreen.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupSuccessScreen.kt new file mode 100644 index 00000000..a7441d76 --- /dev/null +++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupSuccessScreen.kt @@ -0,0 +1,79 @@ +package com.goalpanzi.mission_mate.feature.onboarding.screen.boardsetup + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateButtonType +import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateTextButton +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249 +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray2_FF4F505C +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray3_FF727484 +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF +import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography +import com.goalpanzi.mission_mate.feature.onboarding.R +import com.goalpanzi.mission_mate.feature.onboarding.component.StableImage + +@Composable +fun BoardSetupSuccessScreen( + onClickStart: () -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxWidth() + .background(ColorWhite_FFFFFFFF) + .padding(horizontal = 14.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + modifier = Modifier.statusBarsPadding().padding(top = 60.dp), + text = stringResource(id = R.string.onboarding_board_setup_success_title), + style = MissionMateTypography.heading_sm_bold, + color = ColorGray2_FF4F505C + ) + + Text( + text = stringResource(id = R.string.onboarding_board_setup_success_description), + style = MissionMateTypography.title_xl_regular, + color = ColorGray3_FF727484, + textAlign = TextAlign.Center + ) + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + contentAlignment = Alignment.Center + ) { + StableImage( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + drawableResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.image_jeju_success, + contentScale = ContentScale.FillWidth + ) + } + MissionMateTextButton( + modifier = Modifier + .padding(vertical = 36.dp, horizontal = 10.dp) + .fillMaxWidth() + .wrapContentHeight(), + buttonType = MissionMateButtonType.ACTIVE, + textId = R.string.start, + onClick = onClickStart + ) + } +} \ No newline at end of file diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupVerificationTime.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupVerificationTime.kt new file mode 100644 index 00000000..179eb9bf --- /dev/null +++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupVerificationTime.kt @@ -0,0 +1,108 @@ +package com.goalpanzi.mission_mate.feature.onboarding.screen.boardsetup + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249 +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray3_FF727484 +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray5_FFF5F6F9 +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF +import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography +import com.goalpanzi.mission_mate.feature.onboarding.R +import com.goalpanzi.mission_mate.feature.onboarding.model.VerificationTimeType + +@Composable +fun BoardSetupVerificationTime( + selectedTimeType : VerificationTimeType?, + onClickTime : (VerificationTimeType) -> Unit, + modifier : Modifier = Modifier +){ + Column( + modifier = modifier + .fillMaxSize() + .padding(horizontal = 24.dp) + ) { + BoardSetupDescription( + text = stringResource(id = R.string.onboarding_board_setup_verification_time_description), + colorTargetTexts = listOf( + stringResource(R.string.onboarding_board_setup_verification_time_color_target), + ) + ) + VerificationTime( + modifier = Modifier.fillMaxWidth(), + selectedTime = selectedTimeType, + onClick = onClickTime + ) + } +} + + + +@Composable +fun VerificationTime( + modifier: Modifier = Modifier, + selectedTime: VerificationTimeType?, + onClick: (VerificationTimeType) -> Unit +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = stringResource(id = R.string.onboarding_board_setup_verification_time_input_title), + style = MissionMateTypography.body_md_bold, + color = ColorGray3_FF727484 + ) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + VerificationTimeType.entries.forEach { + Button( + modifier = Modifier + .height(84.dp) + .weight(1f), + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.buttonColors( + containerColor = if (selectedTime == it) ColorGray1_FF404249 + else ColorGray5_FFF5F6F9 + ), + onClick = { + onClick(it) + }, + contentPadding = PaddingValues() + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(id = it.titleId), + color = if (selectedTime != it) ColorGray3_FF727484 + else ColorWhite_FFFFFFFF, + textAlign = TextAlign.Center, + style = MissionMateTypography.body_lg_regular + ) + + } + } + } + } + + } +} \ No newline at end of file diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupViewModel.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupViewModel.kt new file mode 100644 index 00000000..da343c6b --- /dev/null +++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupViewModel.kt @@ -0,0 +1,178 @@ +package com.goalpanzi.mission_mate.feature.onboarding.screen.boardsetup + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.goalpanzi.mission_mate.feature.onboarding.model.VerificationTimeType +import com.goalpanzi.mission_mate.feature.onboarding.util.DateUtils.isDifferenceTargetDaysOrMore +import com.goalpanzi.mission_mate.feature.onboarding.util.DateUtils.longToLocalDate +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import java.time.DayOfWeek +import java.time.LocalDate +import javax.inject.Inject + +@HiltViewModel +class BoardSetupViewModel @Inject constructor( + +) : ViewModel() { + + private val _setupSuccessEvent = MutableSharedFlow() + val setupSuccessEvent: SharedFlow = _setupSuccessEvent.asSharedFlow() + + private val _currentStep = MutableStateFlow(BoardSetupStep.MISSION) + val currentStep: StateFlow = _currentStep.asStateFlow() + + var missionTitle by mutableStateOf("") + private set + + private val _startDate = MutableStateFlow(null) + val startDate: StateFlow = _startDate.asStateFlow() + + private val _endDate = MutableStateFlow(null) + val endDate: StateFlow = _endDate.asStateFlow() + + private val _selectedDays = MutableStateFlow>(emptyList()) + val selectedDays: StateFlow> = _selectedDays.asStateFlow() + + private val _selectedVerificationTimeType = MutableStateFlow(null) + val selectedVerificationTimeType: StateFlow = + _selectedVerificationTimeType.asStateFlow() + + val enabledDaysOfWeek: StateFlow> = + combine( + startDate, + endDate + ) { startDate, endDate -> + if (startDate == null || endDate == null) { + emptySet() + } else if (isDifferenceTargetDaysOrMore(startDate,endDate)) { + DayOfWeek.entries.toSet() + } else { + getUniqueDaysOfWeekInRange(startDate,endDate) + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(500), + initialValue = emptySet() + ) + + val enabledButton: StateFlow = + combine( + currentStep, + snapshotFlow { missionTitle }, + enabledDaysOfWeek, + selectedDays, + selectedVerificationTimeType + ) { step, title, enabledDaysOfWeek, selectedDays, selectedVerificationTimeType -> + when (step) { + BoardSetupStep.MISSION -> { + title.length in MISSION_TITLE_MIN_LENGTH..MISSION_TITLE_MAX_LENGTH + } + + BoardSetupStep.SCHEDULE -> { + enabledDaysOfWeek.isNotEmpty() && selectedDays.isNotEmpty() + } + + BoardSetupStep.VERIFICATION_TIME -> { + selectedVerificationTimeType != null + } + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(500), + initialValue = false + ) + + fun updateCurrentStepToNext() { + viewModelScope.launch { + if (currentStep.value.ordinal in 0 until BoardSetupStep.entries.lastIndex) { + _currentStep.emit( + BoardSetupStep.entries[currentStep.value.ordinal + 1] + ) + } else if (currentStep.value == BoardSetupStep.VERIFICATION_TIME) { + _setupSuccessEvent.emit(Unit) + } + } + } + + fun updateCurrentStepToBack() { + viewModelScope.launch { + if (currentStep.value.ordinal in 1 .. BoardSetupStep.entries.lastIndex) { + _currentStep.emit( + BoardSetupStep.entries[currentStep.value.ordinal - 1] + ) + } + } + } + + fun updateMissionTitle(title: String) { + if (title.length <= MISSION_TITLE_MAX_LENGTH) missionTitle = title + } + + fun updateStartDate(date: Long) { + viewModelScope.launch { + _startDate.emit(longToLocalDate(date)) + endDate.value?.let { endDate -> + if(longToLocalDate(date).isAfter(endDate)) _endDate.emit(null) + } + + } + } + + fun updateEndDate(date: Long) { + viewModelScope.launch { + _endDate.emit(longToLocalDate(date)) + } + } + + fun updateSelectedDays(targetDay: DayOfWeek) { + viewModelScope.launch { + _selectedDays.update { days -> + if (days.contains(targetDay)) { + days.filter { it != targetDay } + } else days + targetDay + } + } + } + + fun updateSelectedVerificationTimeType(timeType: VerificationTimeType) { + viewModelScope.launch { + _selectedVerificationTimeType.emit(timeType) + } + } + + private fun getUniqueDaysOfWeekInRange( + startDate : LocalDate, + endDate: LocalDate + ) : Set { + return generateSequence(startDate) { date -> + if (date.isBefore(endDate)) date.plusDays(1) else null + }.map { it.dayOfWeek } + .toSet() + } + + companion object { + + const val MISSION_TITLE_MIN_LENGTH = 4 + const val MISSION_TITLE_MAX_LENGTH = 12 + + + enum class BoardSetupStep { + MISSION, SCHEDULE, VERIFICATION_TIME + } + } +} \ No newline at end of file diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/util/DateUtils.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/util/DateUtils.kt new file mode 100644 index 00000000..e07b230a --- /dev/null +++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/util/DateUtils.kt @@ -0,0 +1,55 @@ +package com.goalpanzi.mission_mate.feature.onboarding.util + +import java.time.DayOfWeek +import java.time.Instant +import java.time.LocalDate +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.time.temporal.ChronoUnit +import java.util.Locale +import kotlin.math.absoluteValue + +object DateUtils { + private fun convertMillisToLocalDateWithFormatter(date: LocalDate, dateTimeFormatter: DateTimeFormatter) : LocalDate { + val dateInMillis = LocalDate.parse(date.format(dateTimeFormatter), dateTimeFormatter) + .atStartOfDay(ZoneId.systemDefault()) + .toInstant() + .toEpochMilli() + + return Instant + .ofEpochMilli(dateInMillis) + .atZone(ZoneId.systemDefault()) + .toLocalDate() + } + fun dateToString(date: LocalDate): String { + val dateFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd", Locale.getDefault()) + val dateInMillis = convertMillisToLocalDateWithFormatter(date, dateFormatter) + return dateFormatter.format(dateInMillis) + } + + fun longToLocalDate(milliseconds: Long): LocalDate { + val instant = Instant.ofEpochMilli(milliseconds) + + return instant.atZone(ZoneId.systemDefault()).toLocalDate() + } + + fun localDateToMillis(localDate: LocalDate?): Long? { + val instant = localDate?.atStartOfDay(ZoneId.of("UTC"))?.toInstant() ?: return null + + return instant.toEpochMilli() + } + + fun filterDatesByDayOfWeek(startDate: LocalDate?, endDate: LocalDate?, days: List): Int { + if(startDate == null || endDate == null) return 0 + return generateSequence(startDate) { date -> + if (date.isBefore(endDate)) date.plusDays(1) else null + }.filter { it.dayOfWeek in days }.toList().size + } + + fun isDifferenceTargetDaysOrMore( + startDate: LocalDate, + endDate: LocalDate, + targetDifferenceDays : Int = 7 + ) = ChronoUnit.DAYS.between(startDate, endDate).absoluteValue >= targetDifferenceDays + +} \ No newline at end of file diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/util/DayOfWeekExt.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/util/DayOfWeekExt.kt new file mode 100644 index 00000000..046f5211 --- /dev/null +++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/util/DayOfWeekExt.kt @@ -0,0 +1,16 @@ +package com.goalpanzi.mission_mate.feature.onboarding.util + +import com.goalpanzi.mission_mate.core.designsystem.R +import java.time.DayOfWeek + +fun DayOfWeek.getStringId() : Int { + return when(this){ + DayOfWeek.MONDAY -> R.string.monday_short + DayOfWeek.TUESDAY -> R.string.tuesday_short + DayOfWeek.WEDNESDAY -> R.string.wednesday_short + DayOfWeek.THURSDAY -> R.string.thursday_short + DayOfWeek.FRIDAY -> R.string.friday_short + DayOfWeek.SATURDAY -> R.string.saturday_short + DayOfWeek.SUNDAY -> R.string.sunday_short + } +} \ No newline at end of file diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/util/StringUtils.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/util/StringUtils.kt new file mode 100644 index 00000000..9edcb56b --- /dev/null +++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/util/StringUtils.kt @@ -0,0 +1,75 @@ +package com.goalpanzi.mission_mate.feature.onboarding.util + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration + +fun List.getStringRanges(text: String): List { + return this.mapNotNull { target -> + val index = text.indexOf(target, 0) + if (index != -1) { + (index until index + target.length) + } else { + null + } + } +} + +fun styledTextWithHighlights( + text: String, + colorTargetTexts: List, + textColor: Color, + targetTextColor: Color, + weightTargetTexts: List = emptyList(), + underlineTargetTexts: List = emptyList(), + targetFontWeight: FontWeight = FontWeight.Normal +): AnnotatedString { + return styledTextWithHighlightsWithIndices( + text = text, + colorTargetTextIndices = colorTargetTexts.getStringRanges(text), + weightTargetTextIndices = weightTargetTexts.getStringRanges(text), + underlineTargetTextIndices = underlineTargetTexts.getStringRanges(text), + textColor = textColor, + targetTextColor = targetTextColor, + targetFontWeight = targetFontWeight + ) +} + +fun styledTextWithHighlightsWithIndices( + text: String, + colorTargetTextIndices: List, + weightTargetTextIndices: List, + underlineTargetTextIndices: List, + textColor: Color, + targetTextColor: Color, + targetFontWeight: FontWeight = FontWeight.Normal, +): AnnotatedString { + return buildAnnotatedString { + addStyle(style = SpanStyle(color = textColor), start = 0, end = text.length) + append(text) + colorTargetTextIndices.forEach { range -> + addStyle( + style = SpanStyle(color = targetTextColor), + start = range.first, + end = range.last + 1 + ) + } + weightTargetTextIndices.forEach { range -> + addStyle( + style = SpanStyle(fontWeight = targetFontWeight), + start = range.first, + end = range.last + 1 + ) + } + underlineTargetTextIndices.forEach { range -> + addStyle( + style = SpanStyle(textDecoration = TextDecoration.Underline), + start = range.first, + end = range.last + 1 + ) + } + } +} \ No newline at end of file diff --git a/feature/onboarding/src/main/res/values/strings.xml b/feature/onboarding/src/main/res/values/strings.xml index 0392472e..e43246be 100644 --- a/feature/onboarding/src/main/res/values/strings.xml +++ b/feature/onboarding/src/main/res/values/strings.xml @@ -7,4 +7,40 @@ 초대받고 왔지~ LV1. 제주도 + + 미션 설정 + 경쟁인원은\n최소 2명에서 최대 10명입니다! + 최소 2명 + 최대 10명 + 미션 + ex) 주3회 러닝하기 / 매일 책3장씩 읽기 + 4~12자 이내로 입력하세요. + + 기간 및 요일 설정 + 경쟁 기간내 인증 요일로 계산한\n총 인증 횟수는 %s번 입니다! + 경쟁 기간 + 인증 요일 + + 미션 기간 + 시작일 + 마감일 + 내일부터 시작일로 지정할 수 있어요. + 인증 요일 (다중선택) + 선택한 요일에만 미션 인증할 수 있어요. (ex.월,수,금) + + 인증 시간 설정 + 해당 시간에만 인증 가능해요!\n신중히 선택해주세요. + 해당 시간에만 인증 가능 + 인증 시간 + 오전\n00~12시 + 오후\n12~00시 + 종일\n00~00시 + + 목표가 완성되었어요! + 꾸준히 미션를 완수해\n세계 곳곳을 경험해봐요! + + 다음 + 완성 + 시작하기 + \ No newline at end of file