diff --git a/app/src/main/java/org/android/bbangzip/presentation/component/balloon/TopTailBalloon.kt b/app/src/main/java/org/android/bbangzip/presentation/component/balloon/TopTailBalloon.kt index c8c2c253..780cbb16 100644 --- a/app/src/main/java/org/android/bbangzip/presentation/component/balloon/TopTailBalloon.kt +++ b/app/src/main/java/org/android/bbangzip/presentation/component/balloon/TopTailBalloon.kt @@ -7,6 +7,7 @@ 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.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -17,6 +18,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import org.android.bbangzip.R +import org.android.bbangzip.presentation.type.BbangZipShadowType +import org.android.bbangzip.presentation.util.modifier.applyShadows import org.android.bbangzip.ui.theme.BbangZipTheme import org.android.bbangzip.ui.theme.defaultBbangZipColors @@ -32,7 +35,8 @@ fun TopTailBalloon( modifier = modifier .fillMaxWidth() - .padding(horizontal = horizontalPadding), + .padding(horizontal = horizontalPadding) + .applyShadows(BbangZipShadowType.STRONG, shape = RoundedCornerShape(size = 20.dp)), ) { Icon( imageVector = ImageVector.vectorResource(R.drawable.ic_balloon_tail_up_8), diff --git a/app/src/main/java/org/android/bbangzip/presentation/model/BadgeCategory.kt b/app/src/main/java/org/android/bbangzip/presentation/model/BadgeCategory.kt new file mode 100644 index 00000000..86fd2e6f --- /dev/null +++ b/app/src/main/java/org/android/bbangzip/presentation/model/BadgeCategory.kt @@ -0,0 +1,12 @@ +package org.android.bbangzip.presentation.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class BadgeCategory( + val name: String, + val categoryName: String, + val isLocked: Boolean, + val imageUrl: String, +) : Parcelable diff --git a/app/src/main/java/org/android/bbangzip/presentation/model/BadgeDetail.kt b/app/src/main/java/org/android/bbangzip/presentation/model/BadgeDetail.kt new file mode 100644 index 00000000..44478e19 --- /dev/null +++ b/app/src/main/java/org/android/bbangzip/presentation/model/BadgeDetail.kt @@ -0,0 +1,15 @@ +package org.android.bbangzip.presentation.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class BadgeDetail( + val name: String = "안녕", + val categoryName: String = "안녕", + val imageUrl: String = "https://www.chosun.com/resizer/v2/HRGER65PGPIW36FJOBRNAP2PJM.jpg?auth=9da0c167de2cfb03a5d344ce4098faa669a22d7a2b90cb6a21fdc518b0af3558&width=530&height=757&smart=true", + val hashTags: List = listOf("안녕", "안녕"), + val achievementCondition: String = "안녕", + val reward: Int = 50, + val isLocked: Boolean = true, +) : Parcelable diff --git a/app/src/main/java/org/android/bbangzip/presentation/ui/my/mybadgecategory/MyBadgeCategoryContract.kt b/app/src/main/java/org/android/bbangzip/presentation/ui/my/mybadgecategory/MyBadgeCategoryContract.kt new file mode 100644 index 00000000..b00645ab --- /dev/null +++ b/app/src/main/java/org/android/bbangzip/presentation/ui/my/mybadgecategory/MyBadgeCategoryContract.kt @@ -0,0 +1,86 @@ +package org.android.bbangzip.presentation.ui.my.mybadgecategory + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import org.android.bbangzip.presentation.model.BadgeCategory +import org.android.bbangzip.presentation.model.BadgeDetail +import org.android.bbangzip.presentation.util.base.BaseContract + +class MyBadgeCategoryContract { + @Parcelize + data class MyBadgeCategoryState( + val badgeCategoryList1: List = + listOf( + BadgeCategory( + name = "Badge 1", + categoryName = "Category 1", + isLocked = true, + imageUrl = "https://www.chosun.com/resizer/v2/HRGER65PGPIW36FJOBRNAP2PJM.jpg?auth=9da0c167de2cfb03a5d344ce4098faa669a22d7a2b90cb6a21fdc518b0af3558&width=530&height=757&smart=true", + ), + BadgeCategory( + name = "Badge 2", + categoryName = "Category 1", + isLocked = false, + imageUrl = "https://www.chosun.com/resizer/v2/HRGER65PGPIW36FJOBRNAP2PJM.jpg?auth=9da0c167de2cfb03a5d344ce4098faa669a22d7a2b90cb6a21fdc518b0af3558&width=530&height=757&smart=true", + ), + BadgeCategory( + name = "Badge 3", + categoryName = "Category 1", + isLocked = true, + imageUrl = "https://www.chosun.com/resizer/v2/HRGER65PGPIW36FJOBRNAP2PJM.jpg?auth=9da0c167de2cfb03a5d344ce4098faa669a22d7a2b90cb6a21fdc518b0af3558&width=530&height=757&smart=true", + ), + ), + val badgeCategoryList2: List = + listOf( + BadgeCategory( + name = "Badge 4", + categoryName = "Category 2", + isLocked = true, + imageUrl = "https://www.chosun.com/resizer/v2/HRGER65PGPIW36FJOBRNAP2PJM.jpg?auth=9da0c167de2cfb03a5d344ce4098faa669a22d7a2b90cb6a21fdc518b0af3558&width=530&height=757&smart=true", + ), + BadgeCategory( + name = "Badge 5", + categoryName = "Category 2", + isLocked = false, + imageUrl = "https://www.chosun.com/resizer/v2/HRGER65PGPIW36FJOBRNAP2PJM.jpg?auth=9da0c167de2cfb03a5d344ce4098faa669a22d7a2b90cb6a21fdc518b0af3558&width=530&height=757&smart=true", + ), + BadgeCategory( + name = "Badge 6", + categoryName = "Category 2", + isLocked = true, + imageUrl = "https://www.chosun.com/resizer/v2/HRGER65PGPIW36FJOBRNAP2PJM.jpg?auth=9da0c167de2cfb03a5d344ce4098faa669a22d7a2b90cb6a21fdc518b0af3558&width=530&height=757&smart=true", + ), + ), + val badgeDetailBottomSheetState: Boolean = false, + val badgeDetail: BadgeDetail = BadgeDetail(), + ) : BaseContract.State, Parcelable { + override fun toParcelable(): Parcelable = this + } + + sealed interface MyBadgeCategoryEvent : BaseContract.Event { + data object Initialize : MyBadgeCategoryEvent + + data object OnBackIconClicked : MyBadgeCategoryEvent + + data object OnBadgeDetailBottomSheetDismissButtonClicked : MyBadgeCategoryEvent + + data object OnBadgeDetailBottomSheetDismissRequest : MyBadgeCategoryEvent + + data class OnBadgeCardClicked(val badgeName: String) : MyBadgeCategoryEvent + } + + sealed interface MyBadgeCategoryReduce : BaseContract.Reduce { + data class UpdateBadgeCategoryList( + val badgeCategoryList1: List, + val badgeCategoryList2: List, + ) : MyBadgeCategoryReduce + + data class UpdateBadgeDetailBottomSheetState(val badgeDetailBottomSheetState: Boolean) : MyBadgeCategoryReduce + + data class UpdateBadgeDetail(val badgeDetail: BadgeDetail) : MyBadgeCategoryReduce + } + + sealed interface MyBadgeCategorySideEffect : BaseContract.SideEffect { + data object NavigateToBack : MyBadgeCategorySideEffect + } +} diff --git a/app/src/main/java/org/android/bbangzip/presentation/ui/my/mybadgecategory/MyBadgeCategoryRoute.kt b/app/src/main/java/org/android/bbangzip/presentation/ui/my/mybadgecategory/MyBadgeCategoryRoute.kt new file mode 100644 index 00000000..01757719 --- /dev/null +++ b/app/src/main/java/org/android/bbangzip/presentation/ui/my/mybadgecategory/MyBadgeCategoryRoute.kt @@ -0,0 +1,61 @@ +package org.android.bbangzip.presentation.ui.my.mybadgecategory + +import android.app.Activity +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalView +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.flow.collectLatest +import org.android.bbangzip.ui.theme.BbangZipTheme + +@Composable +fun MyBadgeCategoryRoute( + navigateToBack: () -> Unit = {}, + viewModel: MyBadgeCategoryViewModel = hiltViewModel(), +) { + val badgeCategoryState by viewModel.uiState.collectAsStateWithLifecycle() + val success by viewModel.success.collectAsStateWithLifecycle(initialValue = false) + val view = LocalView.current + val activity = view.context as Activity + + activity.window.statusBarColor = BbangZipTheme.colors.backgroundAccent_FFDAA0.toArgb() + + LaunchedEffect(viewModel.uiSideEffect) { + viewModel.uiSideEffect.collectLatest { effect -> + when (effect) { + MyBadgeCategoryContract.MyBadgeCategorySideEffect.NavigateToBack -> + navigateToBack() + } + } + } + + when (success) { + true -> + MyBadgeCategoryScreen( + badgeCategoryState = badgeCategoryState, + onBackIconClicked = { + viewModel.setEvent(MyBadgeCategoryContract.MyBadgeCategoryEvent.OnBackIconClicked) + }, + onBadgeCardClicked = { badgeName -> + viewModel.setEvent( + MyBadgeCategoryContract.MyBadgeCategoryEvent.OnBadgeCardClicked( + badgeName = badgeName, + ), + ) + }, + onBadgeDetailBottomSheetDismissRequest = { + viewModel.setEvent(MyBadgeCategoryContract.MyBadgeCategoryEvent.OnBadgeDetailBottomSheetDismissRequest) + }, + onBadgeDetailBottomSheetDismissButtonClicked = { + viewModel.setEvent(MyBadgeCategoryContract.MyBadgeCategoryEvent.OnBadgeDetailBottomSheetDismissButtonClicked) + }, + ) + + false -> + Text("땡!") + } +} diff --git a/app/src/main/java/org/android/bbangzip/presentation/ui/my/mybadgecategory/MyBadgeCategoryScreen.kt b/app/src/main/java/org/android/bbangzip/presentation/ui/my/mybadgecategory/MyBadgeCategoryScreen.kt new file mode 100644 index 00000000..3cb4af11 --- /dev/null +++ b/app/src/main/java/org/android/bbangzip/presentation/ui/my/mybadgecategory/MyBadgeCategoryScreen.kt @@ -0,0 +1,260 @@ +package org.android.bbangzip.presentation.ui.my.mybadgecategory + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +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.Spacer +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.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import org.android.bbangzip.R +import org.android.bbangzip.presentation.component.topbar.BbangZipBaseTopBar +import org.android.bbangzip.presentation.model.BadgeCategory +import org.android.bbangzip.presentation.model.BadgeDetail +import org.android.bbangzip.presentation.ui.my.mybadgecategory.component.BbangZipBadgeDetailBottomSheet +import org.android.bbangzip.presentation.ui.my.mybadgecategory.component.LockedBadgeImage +import org.android.bbangzip.ui.theme.BbangZipTheme + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun MyBadgeCategoryScreen( + badgeCategoryState: MyBadgeCategoryContract.MyBadgeCategoryState, + modifier: Modifier = Modifier, + onBackIconClicked: () -> Unit = {}, + onBadgeCardClicked: (String) -> Unit = {}, + onBadgeDetailBottomSheetDismissRequest: () -> Unit = {}, + onBadgeDetailBottomSheetDismissButtonClicked: () -> Unit = {}, +) { + val scrollState = rememberLazyListState() + val isShadowed by remember { + derivedStateOf { + scrollState.firstVisibleItemScrollOffset > 0 + } + } + LazyColumn( + modifier = + modifier + .fillMaxWidth() + .background(BbangZipTheme.colors.staticWhite_FFFFFF), + state = scrollState, + ) { + stickyHeader { + BbangZipBaseTopBar( + title = stringResource(R.string.badge_category_top_bar_title), + leadingIcon = R.drawable.ic_chevronleft_thick_small_24, + onLeadingIconClick = onBackIconClicked, + backGroundColor = BbangZipTheme.colors.backgroundAccent_FFDAA0, + isShadowed = isShadowed, + ) + } + + item { + Column( + modifier = + Modifier + .fillMaxWidth() + .background( + color = BbangZipTheme.colors.backgroundAccent_FFDAA0, + shape = RoundedCornerShape(bottomEnd = 32.dp, bottomStart = 32.dp), + ), + ) { + Text( + text = stringResource(R.string.badge_category_title, "밍밍"), + style = BbangZipTheme.typography.heading2Bold, + color = BbangZipTheme.colors.labelNormal_282119, + modifier = + Modifier + .padding(start = 24.dp, top = 28.dp, bottom = 48.dp), + ) + } + + Spacer(Modifier.height(48.dp)) + } + + item { + BadgeCategoryGridList( + badgeCategoryList = badgeCategoryState.badgeCategoryList1, + categoryDescription = "이번 학기 빵점 탈출 내가 해냄!", + onBadgeCardClicked = onBadgeCardClicked, + ) + + Spacer(Modifier.height(64.dp)) + } + + item { + BadgeCategoryGridList( + badgeCategoryList = badgeCategoryState.badgeCategoryList2, + categoryDescription = "지금 바로 시작하면 미룬이 탈출 가능!", + onBadgeCardClicked = onBadgeCardClicked, + ) + } + } + + BbangZipBadgeDetailBottomSheet( + badgeDetail = badgeCategoryState.badgeDetail, + isBottomSheetVisible = badgeCategoryState.badgeDetailBottomSheetState, + cancelButtonText = "닫기", + onDismissRequest = onBadgeDetailBottomSheetDismissRequest, + onClickCancelButton = onBadgeDetailBottomSheetDismissButtonClicked, + ) +} + +@Composable +fun BadgeCategoryGridList( + badgeCategoryList: List, + categoryDescription: String, + modifier: Modifier = Modifier, + onBadgeCardClicked: (String) -> Unit = {}, +) { + val badgeCategoryTitle = badgeCategoryList.first().categoryName + val badgeHeight = LocalConfiguration.current.screenHeightDp / 10 + + Column( + modifier = + modifier + .padding(horizontal = 20.dp) + .fillMaxWidth() + .background(BbangZipTheme.colors.staticWhite_FFFFFF), + ) { + Text( + text = badgeCategoryTitle, + style = BbangZipTheme.typography.title3Bold, + color = BbangZipTheme.colors.labelNormal_282119, + modifier = Modifier.padding(start = 8.dp), + ) + + Text( + text = categoryDescription, + style = BbangZipTheme.typography.label1Bold, + color = BbangZipTheme.colors.labelAlternative_282119_61, + modifier = Modifier.padding(start = 8.dp), + ) + + Spacer(modifier = Modifier.height(24.dp)) + + LazyVerticalGrid( + modifier = + Modifier + .padding(horizontal = 16.dp) + .fillMaxSize() + .heightIn(max = (badgeHeight * (badgeCategoryList.size - 1 / 3 + 1) + 20 * (badgeCategoryList.size / 3)).dp), + columns = GridCells.Fixed(3), + horizontalArrangement = Arrangement.spacedBy(32.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + items(badgeCategoryList.size) { index -> + Box( + modifier = + Modifier + .aspectRatio(1f) + .clip(RoundedCornerShape(24.dp)) + .clickable { onBadgeCardClicked(badgeCategoryList[index].name) }, + contentAlignment = Alignment.Center, + ) { + if (!badgeCategoryList[index].isLocked) { + LockedBadgeImage( + imgUrl = badgeCategoryList[index].imageUrl, + ) + } else { + AsyncImage( + model = badgeCategoryList[index].imageUrl, + contentDescription = null, + modifier = + Modifier + .fillMaxSize() + .clip(RoundedCornerShape(24.dp)), + ) + } + } + } + } + } +} + +@Preview +@Composable +private fun MyBadgeCategoryScreenPreview() { + val sampleBadgeCategoryList1 = + listOf( + BadgeCategory( + name = "Badge 1", + categoryName = "Category 1", + isLocked = true, + imageUrl = "https://www.chosun.com/resizer/v2/HRGER65PGPIW36FJOBRNAP2PJM.jpg?auth=9da0c167de2cfb03a5d344ce4098faa669a22d7a2b90cb6a21fdc518b0af3558&width=530&height=757&smart=true", + ), + BadgeCategory( + name = "Badge 2", + categoryName = "Category 1", + isLocked = false, + imageUrl = "https://www.chosun.com/resizer/v2/HRGER65PGPIW36FJOBRNAP2PJM.jpg?auth=9da0c167de2cfb03a5d344ce4098faa669a22d7a2b90cb6a21fdc518b0af3558&width=530&height=757&smart=true", + ), + BadgeCategory( + name = "Badge 3", + categoryName = "Category 1", + isLocked = true, + imageUrl = "https://www.chosun.com/resizer/v2/HRGER65PGPIW36FJOBRNAP2PJM.jpg?auth=9da0c167de2cfb03a5d344ce4098faa669a22d7a2b90cb6a21fdc518b0af3558&width=530&height=757&smart=true", + ), + ) + + val sampleBadgeCategoryList2 = + listOf( + BadgeCategory( + name = "Badge 4", + categoryName = "Category 2", + isLocked = true, + imageUrl = "https://www.chosun.com/resizer/v2/HRGER65PGPIW36FJOBRNAP2PJM.jpg?auth=9da0c167de2cfb03a5d344ce4098faa669a22d7a2b90cb6a21fdc518b0af3558&width=530&height=757&smart=true", + ), + BadgeCategory( + name = "Badge 5", + categoryName = "Category 2", + isLocked = false, + imageUrl = "https://www.chosun.com/resizer/v2/HRGER65PGPIW36FJOBRNAP2PJM.jpg?auth=9da0c167de2cfb03a5d344ce4098faa669a22d7a2b90cb6a21fdc518b0af3558&width=530&height=757&smart=true", + ), + BadgeCategory( + name = "Badge 6", + categoryName = "Category 2", + isLocked = true, + imageUrl = "https://www.chosun.com/resizer/v2/HRGER65PGPIW36FJOBRNAP2PJM.jpg?auth=9da0c167de2cfb03a5d344ce4098faa669a22d7a2b90cb6a21fdc518b0af3558&width=530&height=757&smart=true", + ), + ) + + val sampleState = + MyBadgeCategoryContract.MyBadgeCategoryState( + badgeCategoryList1 = sampleBadgeCategoryList1, + badgeCategoryList2 = sampleBadgeCategoryList2, + badgeDetailBottomSheetState = false, + badgeDetail = BadgeDetail(), + ) + + MyBadgeCategoryScreen( + badgeCategoryState = sampleState, + onBackIconClicked = { println("Back icon clicked") }, + onBadgeCardClicked = { badgeName -> println("Badge clicked: $badgeName") }, + onBadgeDetailBottomSheetDismissRequest = { println("BottomSheet dismissed") }, + onBadgeDetailBottomSheetDismissButtonClicked = { println("Dismiss button clicked") }, + ) +} diff --git a/app/src/main/java/org/android/bbangzip/presentation/ui/my/mybadgecategory/MyBadgeCategoryViewModel.kt b/app/src/main/java/org/android/bbangzip/presentation/ui/my/mybadgecategory/MyBadgeCategoryViewModel.kt new file mode 100644 index 00000000..557d00d0 --- /dev/null +++ b/app/src/main/java/org/android/bbangzip/presentation/ui/my/mybadgecategory/MyBadgeCategoryViewModel.kt @@ -0,0 +1,81 @@ +package org.android.bbangzip.presentation.ui.my.mybadgecategory + +import android.os.Parcelable +import androidx.lifecycle.SavedStateHandle +import dagger.hilt.android.lifecycle.HiltViewModel +import org.android.bbangzip.presentation.util.base.BaseViewModel +import javax.inject.Inject + +@HiltViewModel +class MyBadgeCategoryViewModel + @Inject + constructor( + savedStateHandle: SavedStateHandle, + ) : BaseViewModel( + savedStateHandle = savedStateHandle, + ) { + override fun createInitialState(savedState: Parcelable?): MyBadgeCategoryContract.MyBadgeCategoryState { + return savedState as? MyBadgeCategoryContract.MyBadgeCategoryState + ?: MyBadgeCategoryContract.MyBadgeCategoryState() + } + + init { + setEvent(MyBadgeCategoryContract.MyBadgeCategoryEvent.Initialize) + } + + override fun handleEvent(event: MyBadgeCategoryContract.MyBadgeCategoryEvent) { + when (event) { + MyBadgeCategoryContract.MyBadgeCategoryEvent.Initialize -> launch { initData() } + MyBadgeCategoryContract.MyBadgeCategoryEvent.OnBackIconClicked -> + setSideEffect(MyBadgeCategoryContract.MyBadgeCategorySideEffect.NavigateToBack) + + is MyBadgeCategoryContract.MyBadgeCategoryEvent.OnBadgeCardClicked -> { + // 서버에서 get하거 bottomsheet 띄우기 + updateState( + MyBadgeCategoryContract.MyBadgeCategoryReduce.UpdateBadgeDetailBottomSheetState( + badgeDetailBottomSheetState = true, + ), + ) + } + + MyBadgeCategoryContract.MyBadgeCategoryEvent.OnBadgeDetailBottomSheetDismissButtonClicked -> + updateState( + MyBadgeCategoryContract.MyBadgeCategoryReduce.UpdateBadgeDetailBottomSheetState( + badgeDetailBottomSheetState = false, + ), + ) + MyBadgeCategoryContract.MyBadgeCategoryEvent.OnBadgeDetailBottomSheetDismissRequest -> + updateState( + MyBadgeCategoryContract.MyBadgeCategoryReduce.UpdateBadgeDetailBottomSheetState( + badgeDetailBottomSheetState = false, + ), + ) + } + } + + override fun reduceState( + state: MyBadgeCategoryContract.MyBadgeCategoryState, + reduce: MyBadgeCategoryContract.MyBadgeCategoryReduce, + ): MyBadgeCategoryContract.MyBadgeCategoryState { + return when (reduce) { + is MyBadgeCategoryContract.MyBadgeCategoryReduce.UpdateBadgeCategoryList -> + state.copy( + badgeCategoryList1 = reduce.badgeCategoryList1, + badgeCategoryList2 = reduce.badgeCategoryList2, + ) + + is MyBadgeCategoryContract.MyBadgeCategoryReduce.UpdateBadgeDetail -> + state.copy( + badgeDetail = reduce.badgeDetail, + ) + + is MyBadgeCategoryContract.MyBadgeCategoryReduce.UpdateBadgeDetailBottomSheetState -> + state.copy( + badgeDetailBottomSheetState = reduce.badgeDetailBottomSheetState, + ) + } + } + + private fun initData() { + } + } diff --git a/app/src/main/java/org/android/bbangzip/presentation/ui/my/mybadgecategory/component/BadgeDetailBottomSheet.kt b/app/src/main/java/org/android/bbangzip/presentation/ui/my/mybadgecategory/component/BadgeDetailBottomSheet.kt new file mode 100644 index 00000000..f123cb55 --- /dev/null +++ b/app/src/main/java/org/android/bbangzip/presentation/ui/my/mybadgecategory/component/BadgeDetailBottomSheet.kt @@ -0,0 +1,232 @@ +package org.android.bbangzip.presentation.ui.my.mybadgecategory.component + +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.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +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.blur +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import org.android.bbangzip.R +import org.android.bbangzip.presentation.component.balloon.TopTailBalloon +import org.android.bbangzip.presentation.component.bottomsheet.BbangZipBasicModalBottomSheet +import org.android.bbangzip.presentation.component.button.BbangZipButton +import org.android.bbangzip.presentation.component.chip.BbangZipChip +import org.android.bbangzip.presentation.model.BadgeDetail +import org.android.bbangzip.presentation.type.BbangZipButtonSize +import org.android.bbangzip.presentation.type.BbangZipButtonType +import org.android.bbangzip.ui.theme.BbangZipTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BbangZipBadgeDetailBottomSheet( + badgeDetail: BadgeDetail, + isBottomSheetVisible: Boolean, + cancelButtonText: String, + modifier: Modifier = Modifier, + onDismissRequest: () -> Unit = {}, + onClickCancelButton: () -> Unit = {}, +) { + BbangZipBasicModalBottomSheet( + modifier = modifier, + isBottomSheetVisible = isBottomSheetVisible, + onDismissRequest = onDismissRequest, + content = { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (!badgeDetail.isLocked) { + BadgeDetailInfo(badgeDetail = badgeDetail) + } else { + Box( + modifier = + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(48.dp)), + contentAlignment = Alignment.Center, + ) { + Column( + modifier = + Modifier + .fillMaxWidth() + .blur(48.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(44.dp)) + + AsyncImage( + model = badgeDetail.imageUrl, + contentDescription = null, + modifier = + Modifier + .height((LocalConfiguration.current.screenHeightDp / 5).dp) + .aspectRatio(1f) + .clip(RoundedCornerShape(48.dp)), + ) + + Spacer(modifier = Modifier.height(160.dp)) + } + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_lock_default_28), + contentDescription = null, + tint = BbangZipTheme.colors.lineStrong_68645E_52, + modifier = + Modifier + .height(58.dp) + .width(41.dp), + ) + } + Spacer(modifier = Modifier.height(18.dp)) + } + Column( + modifier = + Modifier + .padding(horizontal = 40.dp) + .fillMaxWidth(), + ) { + Text( + text = stringResource(R.string.badge_detail_achievement), + style = BbangZipTheme.typography.body1Bold, + color = BbangZipTheme.colors.primaryNormal_282119, + ) + + Text( + text = badgeDetail.achievementCondition, + style = BbangZipTheme.typography.label1Bold, + color = BbangZipTheme.colors.labelAlternative_282119_61, + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = + Modifier + .padding(horizontal = 40.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.badge_detail_reward), + style = BbangZipTheme.typography.body1Bold, + color = BbangZipTheme.colors.primaryNormal_282119, + ) + + Spacer(modifier = Modifier.weight(1f)) + + BbangZipChip( + backgroundColor = BbangZipTheme.colors.statusCautionary_FFB84A, + text = + stringResource( + R.string.badge_bottom_sheet_reward, + badgeDetail.reward, + ), + ) + } + } + }, + cancelButton = { + Spacer(modifier = Modifier.height(32.dp)) + + BbangZipButton( + bbangZipButtonType = BbangZipButtonType.Solid, + bbangZipButtonSize = BbangZipButtonSize.Large, + onClick = { onClickCancelButton() }, + label = cancelButtonText, + modifier = Modifier.fillMaxWidth(), + trailingIcon = R.drawable.ic_chevronright_thick_small_24, + ) + + Spacer(modifier = Modifier.height(16.dp)) + }, + ) +} + +@Composable +fun HashTagText( + hashTags: List, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "#${hashTags[0]}", + style = BbangZipTheme.typography.body2Bold, + color = BbangZipTheme.colors.labelAssistive_282119_28, + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + ) { + Text( + text = "#${hashTags[1]}", + style = BbangZipTheme.typography.body2Bold, + color = BbangZipTheme.colors.labelAssistive_282119_28, + ) + + if (hashTags.size > 2) { + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = "#${hashTags[2]}", + style = BbangZipTheme.typography.body2Bold, + color = BbangZipTheme.colors.labelAssistive_282119_28, + ) + } + } + } +} + +@Composable +fun BadgeDetailInfo( + badgeDetail: BadgeDetail, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(44.dp)) + + AsyncImage( + model = badgeDetail.imageUrl, + contentDescription = null, + modifier = + Modifier + .height((LocalConfiguration.current.screenHeightDp / 5).dp) + .aspectRatio(1f) + .clip(RoundedCornerShape(48.dp)), + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + TopTailBalloon(text = badgeDetail.name, horizontalPadding = 132.dp) + + Spacer(modifier = Modifier.height(32.dp)) + + HashTagText(badgeDetail.hashTags) + + Spacer(modifier = Modifier.height(32.dp)) +} diff --git a/app/src/main/java/org/android/bbangzip/presentation/ui/my/mybadgecategory/component/LockedBadgeImage.kt b/app/src/main/java/org/android/bbangzip/presentation/ui/my/mybadgecategory/component/LockedBadgeImage.kt new file mode 100644 index 00000000..96713df0 --- /dev/null +++ b/app/src/main/java/org/android/bbangzip/presentation/ui/my/mybadgecategory/component/LockedBadgeImage.kt @@ -0,0 +1,37 @@ +package org.android.bbangzip.presentation.ui.my.mybadgecategory.component + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.blur +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import org.android.bbangzip.R +import org.android.bbangzip.ui.theme.BbangZipTheme + +@Composable +fun LockedBadgeImage( + imgUrl: String, + modifier: Modifier = Modifier, +) { + AsyncImage( + model = imgUrl, + contentDescription = null, + modifier = + modifier + .fillMaxSize() + .clip(RoundedCornerShape(24.dp)) + .blur(24.dp), + ) + + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_lock_default_28), + contentDescription = null, + tint = BbangZipTheme.colors.staticWhite_FFFFFF, + ) +} diff --git a/app/src/main/java/org/android/bbangzip/presentation/ui/my/mybadgecategory/navigation/MyBadgeCategoryNavigation.kt b/app/src/main/java/org/android/bbangzip/presentation/ui/my/mybadgecategory/navigation/MyBadgeCategoryNavigation.kt new file mode 100644 index 00000000..8fd3e2c3 --- /dev/null +++ b/app/src/main/java/org/android/bbangzip/presentation/ui/my/mybadgecategory/navigation/MyBadgeCategoryNavigation.kt @@ -0,0 +1,26 @@ +package org.android.bbangzip.presentation.ui.my.mybadgecategory.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import kotlinx.serialization.Serializable +import org.android.bbangzip.presentation.ui.my.mybadgecategory.MyBadgeCategoryRoute + +@Serializable +object MyBadgeCategoryRoute + +fun NavController.navigateToMyBadgeCategory() { + navigate( + route = MyBadgeCategoryRoute, + ) +} + +fun NavGraphBuilder.myBadgeCategoryNavGraph( + navigateToBack: () -> Unit, +) { + composable { + MyBadgeCategoryRoute( + navigateToBack = navigateToBack, + ) + } +} diff --git a/app/src/main/java/org/android/bbangzip/presentation/ui/navigator/MainNavHost.kt b/app/src/main/java/org/android/bbangzip/presentation/ui/navigator/MainNavHost.kt index db45c7fd..a9e2455d 100644 --- a/app/src/main/java/org/android/bbangzip/presentation/ui/navigator/MainNavHost.kt +++ b/app/src/main/java/org/android/bbangzip/presentation/ui/navigator/MainNavHost.kt @@ -12,6 +12,7 @@ import androidx.navigation.compose.NavHost import org.android.bbangzip.presentation.ui.friend.navigation.friendNavGraph import org.android.bbangzip.presentation.ui.login.loginNavGraph import org.android.bbangzip.presentation.ui.my.myNavGraph +import org.android.bbangzip.presentation.ui.my.mybadgecategory.navigation.myBadgeCategoryNavGraph import org.android.bbangzip.presentation.ui.onboarding.navigation.navigateOnboardingStart import org.android.bbangzip.presentation.ui.onboarding.navigation.onboardingEndNavGraph import org.android.bbangzip.presentation.ui.onboarding.navigation.onboardingNavGraph @@ -78,6 +79,10 @@ fun MainNavHost( navigateToLogin = { navigator.navigateToLogin() }, ) + myBadgeCategoryNavGraph( + navigateToBack = { navigator.navigateToMyBadgeCategory() }, + ) + subjectNavGraph() todoNavGraph( diff --git a/app/src/main/java/org/android/bbangzip/presentation/ui/navigator/MainNavigator.kt b/app/src/main/java/org/android/bbangzip/presentation/ui/navigator/MainNavigator.kt index 66565444..ad8f663a 100644 --- a/app/src/main/java/org/android/bbangzip/presentation/ui/navigator/MainNavigator.kt +++ b/app/src/main/java/org/android/bbangzip/presentation/ui/navigator/MainNavigator.kt @@ -14,6 +14,7 @@ import org.android.bbangzip.presentation.type.BottomNavigationType import org.android.bbangzip.presentation.ui.friend.navigation.navigateFriend import org.android.bbangzip.presentation.ui.login.LoginRoute import org.android.bbangzip.presentation.ui.login.navigateLogin +import org.android.bbangzip.presentation.ui.my.mybadgecategory.navigation.navigateToMyBadgeCategory import org.android.bbangzip.presentation.ui.my.navigateMy import org.android.bbangzip.presentation.ui.onboarding.navigation.navigateOnboarding import org.android.bbangzip.presentation.ui.onboarding.navigation.navigateOnboardingEnd @@ -71,6 +72,10 @@ class MainNavigator( navHostController.navigateOnboardingStart() } + fun navigateToMyBadgeCategory() { + navHostController.navigateToMyBadgeCategory() + } + fun navigateToOnboarding() { navHostController.navigateOnboarding() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ae5b5d11..fdd8a919 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -100,6 +100,13 @@ 개 / 총 %s개 오늘 할 공부 추가하기 + + %s 사장님\n열심히 모은 배지에요 + 달성 조건 + 리워드 + 뱃지 도감 + %sP + 제 과제 빵점 사장님은\n이번 학기 백점! 5초 만에 빵집 사장님 되기