diff --git a/app/feature/feature-help-center/build.gradle.kts b/app/feature/feature-help-center/build.gradle.kts index 2ff85470a2..9b3ac7b2f4 100644 --- a/app/feature/feature-help-center/build.gradle.kts +++ b/app/feature/feature-help-center/build.gradle.kts @@ -12,13 +12,16 @@ hedvig { dependencies { api(libs.androidx.navigation.common) + api(libs.coil.coil) implementation(libs.androidx.activity.compose) implementation(libs.androidx.compose.foundation) implementation(libs.androidx.navigation.compose) implementation(libs.apollo.runtime) + implementation(libs.apollo.normalizedCache) implementation(libs.arrow.core) implementation(libs.arrow.fx) + implementation(libs.coil.compose) implementation(libs.compose.richtext) implementation(libs.compose.richtextCommonmark) implementation(libs.coroutines.core) diff --git a/app/feature/feature-help-center/src/main/graphql/MutationPuppyGuideEngagement.graphql b/app/feature/feature-help-center/src/main/graphql/MutationPuppyGuideEngagement.graphql new file mode 100644 index 0000000000..0964b67a59 --- /dev/null +++ b/app/feature/feature-help-center/src/main/graphql/MutationPuppyGuideEngagement.graphql @@ -0,0 +1,5 @@ +mutation PuppyGuideEngagement($name: String!, $rating: Int, $read: Boolean) { + puppyGuideEngagement(engagement: {name: $name, rating: $rating, read: $read}) { + success + } +} diff --git a/app/feature/feature-help-center/src/main/graphql/QueryPuppyGuide.graphql b/app/feature/feature-help-center/src/main/graphql/QueryPuppyGuide.graphql new file mode 100644 index 0000000000..0c4c92ea65 --- /dev/null +++ b/app/feature/feature-help-center/src/main/graphql/QueryPuppyGuide.graphql @@ -0,0 +1,14 @@ +query PuppyGuide { + currentMember { + puppyGuideStories { + categories + content + image + name + rating + read + subtitle + title + } + } +} diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/HelpCenterGraph.kt b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/HelpCenterGraph.kt index cdb3f1a57c..16e5903b41 100644 --- a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/HelpCenterGraph.kt +++ b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/HelpCenterGraph.kt @@ -15,6 +15,10 @@ import com.hedvig.android.feature.help.center.home.HelpCenterHomeDestination import com.hedvig.android.feature.help.center.navigation.HelpCenterDestination import com.hedvig.android.feature.help.center.navigation.HelpCenterDestinations import com.hedvig.android.feature.help.center.navigation.HelpCenterDestinations.Emergency +import com.hedvig.android.feature.help.center.puppyguide.PuppyArticleDestination +import com.hedvig.android.feature.help.center.puppyguide.PuppyArticleViewModel +import com.hedvig.android.feature.help.center.puppyguide.PuppyGuideDestination +import com.hedvig.android.feature.help.center.puppyguide.PuppyGuideViewModel import com.hedvig.android.feature.help.center.question.HelpCenterQuestionDestination import com.hedvig.android.feature.help.center.question.HelpCenterQuestionViewModel import com.hedvig.android.feature.help.center.topic.HelpCenterTopicDestination @@ -81,6 +85,13 @@ fun NavGraphBuilder.helpCenterGraph( onNavigateToNewConversation() }, onNavigateUp = navController::navigateUp, + onNavigateToPuppyGuide = { + with(navigator) { + backStackEntry.navigate( + HelpCenterDestinations.PuppyGuide, + ) + } + }, ) } @@ -138,6 +149,35 @@ fun NavGraphBuilder.helpCenterGraph( imageLoader = imageLoader ) } + + navdestination { backStackEntry -> + val viewModel = koinViewModel() + PuppyGuideDestination( + viewModel, + onNavigateUp = navigator::navigateUp, + onNavigateToArticle = { story -> + with(navigator) { + backStackEntry.navigate( + HelpCenterDestinations.PuppyGuideArticle( + story.name, + ), + ) + } + }, + imageLoader = imageLoader, + ) + } + + navdestination { + val viewModel = koinViewModel { + parametersOf(storyName) + } + PuppyArticleDestination( + viewModel = viewModel, + navigateUp = navigator::navigateUp, + imageLoader = imageLoader, + ) + } } } diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/HelpCenterPresenter.kt b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/HelpCenterPresenter.kt index 67b1c2d431..2b1ea37f05 100644 --- a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/HelpCenterPresenter.kt +++ b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/HelpCenterPresenter.kt @@ -24,7 +24,9 @@ import com.hedvig.android.feature.help.center.HelpCenterUiState.Search import com.hedvig.android.feature.help.center.data.FAQItem import com.hedvig.android.feature.help.center.data.FAQTopic import com.hedvig.android.feature.help.center.data.GetHelpCenterFAQUseCase +import com.hedvig.android.feature.help.center.data.GetPuppyGuideUseCase import com.hedvig.android.feature.help.center.data.GetQuickLinksUseCase +import com.hedvig.android.feature.help.center.data.PuppyGuideStory import com.hedvig.android.feature.help.center.data.QuickLinkDestination import com.hedvig.android.feature.help.center.model.QuickAction import com.hedvig.android.molecule.public.MoleculePresenter @@ -61,6 +63,7 @@ internal data class HelpCenterUiState( val search: Search?, val showNavigateToInboxButton: Boolean, val destinationToNavigate: QuickLinkDestination? = null, + val puppyGuide: List?, ) { data class QuickLink(val quickAction: QuickAction) @@ -93,6 +96,7 @@ internal class HelpCenterPresenter( private val getQuickLinksUseCase: GetQuickLinksUseCase, private val hasAnyActiveConversationUseCase: HasAnyActiveConversationUseCase, private val getHelpCenterFAQUseCase: GetHelpCenterFAQUseCase, + private val getPuppyGuideUseCase: GetPuppyGuideUseCase, ) : MoleculePresenter { @Composable override fun MoleculePresenterScope.present(lastState: HelpCenterUiState): HelpCenterUiState { @@ -161,7 +165,8 @@ internal class HelpCenterPresenter( combine( flow = flow { emit(getQuickLinksUseCase.invoke()) }, flow2 = flow { emit(getHelpCenterFAQUseCase.invoke()) }, - ) { quickLinks, faq -> + flow3 = flow { emit(getPuppyGuideUseCase.invoke()) }, + ) { quickLinks, faq, puppyGuideResult -> quickLinksUiState = quickLinks.fold( ifLeft = { HelpCenterUiState.QuickLinkUiState.NoQuickLinks @@ -179,12 +184,14 @@ internal class HelpCenterPresenter( ) val topics = faq.getOrNull()?.topics ?: listOf() val questions = faq.getOrNull()?.commonFAQ ?: listOf() + val puppyGuide = puppyGuideResult.getOrNull() currentState = currentState.copy( topics = topics, questions = questions, quickLinksUiState = quickLinksUiState, selectedQuickAction = selectedQuickAction, showNavigateToInboxButton = hasAnyActiveConversation, + puppyGuide = puppyGuide, ) }.collect() } diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/HelpCenterViewModel.kt b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/HelpCenterViewModel.kt index a7fb78c540..5c325fb3c3 100644 --- a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/HelpCenterViewModel.kt +++ b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/HelpCenterViewModel.kt @@ -2,6 +2,7 @@ package com.hedvig.android.feature.help.center import com.hedvig.android.data.conversations.HasAnyActiveConversationUseCase import com.hedvig.android.feature.help.center.data.GetHelpCenterFAQUseCase +import com.hedvig.android.feature.help.center.data.GetPuppyGuideUseCase import com.hedvig.android.feature.help.center.data.GetQuickLinksUseCase import com.hedvig.android.molecule.public.MoleculeViewModel @@ -9,6 +10,7 @@ internal class HelpCenterViewModel( getQuickLinksUseCase: GetQuickLinksUseCase, hasAnyActiveConversationUseCase: HasAnyActiveConversationUseCase, getHelpCenterFAQUseCase: GetHelpCenterFAQUseCase, + getPuppyGuideUseCase: GetPuppyGuideUseCase, ) : MoleculeViewModel( initialState = HelpCenterUiState( topics = listOf(), @@ -17,10 +19,12 @@ internal class HelpCenterViewModel( quickLinksUiState = HelpCenterUiState.QuickLinkUiState.Loading, search = null, showNavigateToInboxButton = false, + puppyGuide = null, ), presenter = HelpCenterPresenter( getQuickLinksUseCase = getQuickLinksUseCase, hasAnyActiveConversationUseCase = hasAnyActiveConversationUseCase, getHelpCenterFAQUseCase = getHelpCenterFAQUseCase, + getPuppyGuideUseCase = getPuppyGuideUseCase, ), ) diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/data/GetPuppyGuideUseCase.kt b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/data/GetPuppyGuideUseCase.kt new file mode 100644 index 0000000000..03fe5387fa --- /dev/null +++ b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/data/GetPuppyGuideUseCase.kt @@ -0,0 +1,61 @@ +package com.hedvig.android.feature.help.center.data + +import arrow.core.Either +import arrow.core.raise.either +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.cache.normalized.FetchPolicy +import com.apollographql.apollo.cache.normalized.fetchPolicy +import com.hedvig.android.apollo.safeExecute +import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.logger.logcat +import kotlinx.serialization.Serializable +import octopus.PuppyGuideQuery + +internal interface GetPuppyGuideUseCase { + suspend fun invoke(): Either?> +} + +internal class GetPuppyGuideUseCaseImpl( + private val apolloClient: ApolloClient, +) : GetPuppyGuideUseCase { + override suspend fun invoke(): Either?> { + return either { + apolloClient + .query(PuppyGuideQuery()) + .fetchPolicy(FetchPolicy.NetworkOnly) + .safeExecute() + .onLeft { logcat { "Cannot load PuppyGuideStory: $it" } } + .getOrNull() + ?.currentMember + ?.puppyGuideStories + ?.map { story -> + // todo: remove log + if (story.name == "Ögonvård") { + logcat { "Mariia: story Ögonvård read or not: ${story.read} rating: ${story.rating} " } + } + PuppyGuideStory( + categories = story.categories, + content = story.content, + image = story.image, + name = story.name, + rating = story.rating, + isRead = story.read, + subtitle = story.subtitle, + title = story.title, + ) + } + } + } +} + +@Serializable +internal data class PuppyGuideStory( + val categories: List, + val content: String, + val image: String, + val name: String, + val rating: Int?, + val isRead: Boolean, + val subtitle: String, + val title: String, +) diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/data/SetArticleRatingUseCase.kt b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/data/SetArticleRatingUseCase.kt new file mode 100644 index 0000000000..776bdfb3dd --- /dev/null +++ b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/data/SetArticleRatingUseCase.kt @@ -0,0 +1,35 @@ +package com.hedvig.android.feature.help.center.data + +import arrow.core.Either +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.api.Optional +import com.hedvig.android.apollo.safeExecute +import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.logger.logcat +import octopus.PuppyGuideEngagementMutation + +interface SetArticleRatingUseCase { + suspend fun invoke(articleName: String, rating: Int): Either +} + +internal class SetArticleRatingUseCaseImpl( + private val apolloClient: ApolloClient, +) : SetArticleRatingUseCase { + override suspend fun invoke( + articleName: String, + rating: Int, + ): Either { + return apolloClient + .mutation( + PuppyGuideEngagementMutation( + name = articleName, + rating = Optional.present(rating), + ), + ) + .safeExecute() + .mapLeft { _ -> ErrorMessage() } + .onRight { data -> + logcat { "Mariia. Rating $rating for story $articleName set successfully" } + } + } +} diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/di/HelpCenterModule.kt b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/di/HelpCenterModule.kt index 053df4eaa4..b09b7cc361 100644 --- a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/di/HelpCenterModule.kt +++ b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/di/HelpCenterModule.kt @@ -14,7 +14,13 @@ import com.hedvig.android.feature.help.center.data.GetInsuranceForEditCoInsuredU import com.hedvig.android.feature.help.center.data.GetInsuranceForEditCoInsuredUseCaseImpl import com.hedvig.android.feature.help.center.data.GetMemberActionsUseCase import com.hedvig.android.feature.help.center.data.GetMemberActionsUseCaseImpl +import com.hedvig.android.feature.help.center.data.GetPuppyGuideUseCase +import com.hedvig.android.feature.help.center.data.GetPuppyGuideUseCaseImpl import com.hedvig.android.feature.help.center.data.GetQuickLinksUseCase +import com.hedvig.android.feature.help.center.data.SetArticleRatingUseCase +import com.hedvig.android.feature.help.center.data.SetArticleRatingUseCaseImpl +import com.hedvig.android.feature.help.center.puppyguide.PuppyArticleViewModel +import com.hedvig.android.feature.help.center.puppyguide.PuppyGuideViewModel import com.hedvig.android.feature.help.center.question.HelpCenterQuestionViewModel import com.hedvig.android.feature.help.center.topic.HelpCenterTopicViewModel import com.hedvig.android.featureflags.FeatureManager @@ -31,6 +37,10 @@ val helpCenterModule = module { GetHelpCenterTopicUseCaseImpl(get()) } + single { + GetPuppyGuideUseCaseImpl(get()) + } + single { GetQuickLinksUseCase( apolloClient = get(), @@ -54,6 +64,7 @@ val helpCenterModule = module { getQuickLinksUseCase = get(), hasAnyActiveConversationUseCase = get(), getHelpCenterFAQUseCase = get(), + getPuppyGuideUseCase = get(), ) } @@ -83,4 +94,20 @@ val helpCenterModule = module { hasAnyActiveConversationUseCase = get(), ) } + + viewModel { + PuppyGuideViewModel(getPuppyGuideUseCase = get()) + } + + viewModel { params -> + PuppyArticleViewModel( + getPuppyGuideUseCase = get(), + setArticleRatingUseCase = get(), + storyName = params.get(), + ) + } + + single { + SetArticleRatingUseCaseImpl(apolloClient = get()) + } } diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/home/HelpCenterHomeDestination.kt b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/home/HelpCenterHomeDestination.kt index 5c76ddcce0..a64a4b95aa 100644 --- a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/home/HelpCenterHomeDestination.kt +++ b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/home/HelpCenterHomeDestination.kt @@ -14,6 +14,7 @@ import androidx.compose.animation.shrinkVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.Image 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 @@ -46,10 +47,13 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.painterResource @@ -69,6 +73,7 @@ import com.hedvig.android.compose.ui.withoutPlacement import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonSize.Large import com.hedvig.android.design.system.hedvig.DialogDefaults import com.hedvig.android.design.system.hedvig.HedvigButton +import com.hedvig.android.design.system.hedvig.HedvigButtonGhostWithBorder import com.hedvig.android.design.system.hedvig.HedvigCard import com.hedvig.android.design.system.hedvig.HedvigDialog import com.hedvig.android.design.system.hedvig.HedvigErrorSection @@ -76,6 +81,8 @@ import com.hedvig.android.design.system.hedvig.HedvigPreview import com.hedvig.android.design.system.hedvig.HedvigText import com.hedvig.android.design.system.hedvig.HedvigTextButton import com.hedvig.android.design.system.hedvig.HedvigTheme +import com.hedvig.android.design.system.hedvig.HighlightLabel +import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighlightColor import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighlightShade.LIGHT import com.hedvig.android.design.system.hedvig.Icon @@ -98,6 +105,7 @@ import com.hedvig.android.feature.help.center.HelpCenterUiState import com.hedvig.android.feature.help.center.HelpCenterViewModel import com.hedvig.android.feature.help.center.data.FAQItem import com.hedvig.android.feature.help.center.data.FAQTopic +import com.hedvig.android.feature.help.center.data.PuppyGuideStory import com.hedvig.android.feature.help.center.data.QuickLinkDestination import com.hedvig.android.feature.help.center.model.QuickAction import com.hedvig.android.feature.help.center.model.QuickAction.MultiSelectExpandedLink @@ -136,6 +144,7 @@ internal fun HelpCenterHomeDestination( onNavigateUp: () -> Unit, onNavigateToInbox: () -> Unit, onNavigateToNewConversation: () -> Unit, + onNavigateToPuppyGuide: () -> Unit, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() LaunchedEffect(uiState.destinationToNavigate) { @@ -175,6 +184,8 @@ internal fun HelpCenterHomeDestination( reload = { viewModel.emit(HelpCenterEvent.ReloadFAQAndQuickLinks) }, + puppyGuide = uiState.puppyGuide, + onNavigateToPuppyGuide = onNavigateToPuppyGuide, ) } @@ -183,6 +194,7 @@ private fun HelpCenterHomeScreen( search: HelpCenterUiState.Search?, topics: List, questions: List, + puppyGuide: List?, quickLinksUiState: HelpCenterUiState.QuickLinkUiState, selectedQuickAction: QuickAction?, onNavigateToTopic: (topicId: String) -> Unit, @@ -197,6 +209,7 @@ private fun HelpCenterHomeScreen( onUpdateSearchResults: (String, HelpCenterUiState.HelpSearchResults?) -> Unit, onClearSearch: () -> Unit, reload: () -> Unit, + onNavigateToPuppyGuide: () -> Unit, ) { when (selectedQuickAction) { is StandaloneQuickLink -> { @@ -345,6 +358,8 @@ private fun HelpCenterHomeScreen( showNavigateToInboxButton = showNavigateToInboxButton, onNavigateToInbox = onNavigateToInbox, onNavigateToNewConversation = onNavigateToNewConversation, + puppyGuide = puppyGuide, + onNavigateToPuppyGuide = onNavigateToPuppyGuide, ) } else { SearchResults( @@ -371,10 +386,12 @@ private fun ContentWithoutSearch( topics: List, onNavigateToTopic: (topicId: String) -> Unit, questions: List, + puppyGuide: List?, onNavigateToQuestion: (questionId: String) -> Unit, showNavigateToInboxButton: Boolean, onNavigateToInbox: () -> Unit, onNavigateToNewConversation: () -> Unit, + onNavigateToPuppyGuide: () -> Unit, ) { Column { Column( @@ -382,13 +399,29 @@ private fun ContentWithoutSearch( Modifier.padding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal).asPaddingValues()), ) { Spacer(Modifier.height(32.dp)) - Image( - painter = painterResource(hedvig.resources.R.drawable.pillow_hedvig), - contentDescription = null, - modifier = Modifier - .size(170.dp) - .align(Alignment.CenterHorizontally), - ) + AnimatedContent( + puppyGuide != null, + contentAlignment = Alignment.Center, + ) { puppyGuideAvailable -> + Column( + Modifier.fillMaxWidth(), + ) { + if (puppyGuideAvailable) { + PuppyGuideCard( + onClick = onNavigateToPuppyGuide, + modifier = Modifier.padding(horizontal = 16.dp), + ) + } else { + Image( + painter = painterResource(id = hedvig.resources.R.drawable.pillow_hedvig), + contentDescription = null, + modifier = Modifier + .size(170.dp) + .align(Alignment.CenterHorizontally), + ) + } + } + } Spacer(Modifier.height(50.dp)) Column( verticalArrangement = Arrangement.spacedBy(8.dp), @@ -464,6 +497,56 @@ private fun ContentWithoutSearch( } } +@Composable +private fun PuppyGuideCard(onClick: () -> Unit, modifier: Modifier = Modifier) { + HedvigCard( + color = HedvigTheme.colorScheme.backgroundPrimary, + modifier = modifier + .fillMaxWidth() + .shadow(1.dp, HedvigTheme.shapes.cornerXLarge) + .clickable(enabled = true) { + onClick() + }, + ) { + Column { + Box(Modifier.align(Alignment.CenterHorizontally)) { + Image( + painter = painterResource(id = com.hedvig.android.feature.help.center.R.drawable.hundar_badar_pet), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .height(182.dp) + .clip(HedvigTheme.shapes.cornerXLargeTop), + ) + HighlightLabel( + stringResource(R.string.PUPPY_GUIDE_LABEL), + size = HighlightLabelDefaults.HighLightSize.Small, + color = HighlightColor.Pink(LIGHT), + modifier = Modifier.padding(top = 16.dp, start = 16.dp), + ) + } + + Spacer(Modifier.height(16.dp)) + HedvigText( + stringResource(R.string.PUPPY_GUIDE_TITLE), + modifier = Modifier.padding(horizontal = 16.dp), + ) + HedvigText( + stringResource(R.string.PUPPY_GUIDE_SUBTITLE), + modifier = Modifier.padding(horizontal = 16.dp), + color = HedvigTheme.colorScheme.textSecondary, + ) + Spacer(Modifier.height(16.dp)) + HedvigButtonGhostWithBorder( + stringResource(R.string.PUPPY_GUIDE_GO_BUTTON), + onClick = onClick, + modifier = Modifier.padding(horizontal = 16.dp).fillMaxWidth(), + ) + Spacer(Modifier.height(16.dp)) + } + } +} + @Composable private fun SearchResults( activeSearchState: HelpCenterUiState.ActiveSearchState, @@ -727,6 +810,19 @@ private fun PreviewHelpCenterHomeScreen( onUpdateSearchResults = { _, _ -> }, search = null, reload = {}, + puppyGuide = listOf( + PuppyGuideStory( + categories = listOf("Food"), + content = "some content", + image = "", + name = "", + rating = 5, + isRead = false, + subtitle = "Subtitle", + title = "Title", + ), + ), + onNavigateToPuppyGuide = {}, ) } } @@ -772,6 +868,8 @@ private fun PreviewQuickLinkAnimations() { onUpdateSearchResults = { _, _ -> }, search = null, reload = {}, + puppyGuide = null, + onNavigateToPuppyGuide = {}, ) } } @@ -801,6 +899,8 @@ private fun PreviewQuickLinkEmptyState() { onUpdateSearchResults = { _, _ -> }, search = null, reload = {}, + puppyGuide = null, + onNavigateToPuppyGuide = {}, ) } } diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/navigation/HelpCenterDestination.kt b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/navigation/HelpCenterDestination.kt index 6845fc2eea..00b88cb40b 100644 --- a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/navigation/HelpCenterDestination.kt +++ b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/navigation/HelpCenterDestination.kt @@ -1,5 +1,6 @@ package com.hedvig.android.feature.help.center.navigation +import com.hedvig.android.feature.help.center.data.PuppyGuideStory import com.hedvig.android.navigation.common.Destination import com.hedvig.android.navigation.common.DestinationNavTypeAware import com.hedvig.android.shared.partners.deflect.DeflectData @@ -46,6 +47,12 @@ internal sealed interface HelpCenterDestinations { override val typeList: List = listOf(typeOf>()) } } + + @Serializable + data object PuppyGuide : HelpCenterDestinations, Destination + + @Serializable + data class PuppyGuideArticle(val storyName: String) : HelpCenterDestinations, Destination } val helpCenterCrossSellBottomSheetPermittingDestinations: List> = listOf( diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyArticleDestination.kt b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyArticleDestination.kt new file mode 100644 index 0000000000..2ae491e381 --- /dev/null +++ b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyArticleDestination.kt @@ -0,0 +1,303 @@ +package com.hedvig.android.feature.help.center.puppyguide + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +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.ColorPainter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.ImageLoader +import coil.compose.AsyncImage +import com.halilibo.richtext.commonmark.Markdown +import com.halilibo.richtext.ui.RichTextStyle +import com.halilibo.richtext.ui.string.RichTextStringStyle +import com.hedvig.android.compose.ui.EmptyContentDescription +import com.hedvig.android.design.system.hedvig.HedvigCard +import com.hedvig.android.design.system.hedvig.HedvigErrorSection +import com.hedvig.android.design.system.hedvig.HedvigFullScreenCenterAlignedProgress +import com.hedvig.android.design.system.hedvig.HedvigScaffold +import com.hedvig.android.design.system.hedvig.HedvigShortMultiScreenPreview +import com.hedvig.android.design.system.hedvig.HedvigText +import com.hedvig.android.design.system.hedvig.HedvigTheme +import com.hedvig.android.design.system.hedvig.HorizontalItemsWithMaximumSpaceTaken +import com.hedvig.android.design.system.hedvig.ProvideTextStyle +import com.hedvig.android.design.system.hedvig.RichText +import com.hedvig.android.design.system.hedvig.Surface +import com.hedvig.android.design.system.hedvig.TopAppBarWithBack +import com.hedvig.android.design.system.hedvig.rememberPreviewImageLoader +import com.hedvig.android.feature.help.center.data.PuppyGuideStory +import com.hedvig.android.logger.logcat +import hedvig.resources.R + +@Composable +internal fun PuppyArticleDestination( + viewModel: PuppyArticleViewModel, + navigateUp: () -> Unit, + imageLoader: ImageLoader, +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + PuppyArticleScreen( + uiState, + navigateUp = navigateUp, + onReload = { + viewModel.emit(PuppyArticleEvent.Reload) + }, + imageLoader = imageLoader, + onRatingClick = { + viewModel.emit(PuppyArticleEvent.RatingClick(it)) + }, + ) +} + +@Composable +private fun PuppyArticleScreen( + uiState: PuppyArticleUiState, + navigateUp: () -> Unit, + onReload: () -> Unit, + onRatingClick: (Int) -> Unit, + imageLoader: ImageLoader, +) { + when (uiState) { + PuppyArticleUiState.Failure -> HedvigScaffold( + navigateUp = navigateUp, + ) { + HedvigErrorSection( + onButtonClick = onReload, + modifier = Modifier.weight(1f), + ) + } + + PuppyArticleUiState.Loading -> HedvigFullScreenCenterAlignedProgress() + + is PuppyArticleUiState.Success -> PuppyArticleSuccessScreen( + uiState, + navigateUp = navigateUp, + imageLoader = imageLoader, + onRatingClick = onRatingClick, + ) + } +} + +@Composable +private fun PuppyArticleSuccessScreen( + uiState: PuppyArticleUiState.Success, + navigateUp: () -> Unit, + onRatingClick: (Int) -> Unit, + imageLoader: ImageLoader, +) { + Surface( + color = HedvigTheme.colorScheme.backgroundPrimary, + modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing), + ) { + Column( + Modifier + .fillMaxSize(), + ) { + TopAppBarWithBack( + title = "", + onClick = navigateUp, + ) + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth() + .verticalScroll(rememberScrollState()), + ) { + Spacer(modifier = Modifier.height(8.dp)) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth(), + ) { + val fallbackPainter: Painter = ColorPainter(Color.Black.copy(alpha = 0.7f)) + AsyncImage( + model = uiState.story.image, + contentDescription = EmptyContentDescription, // todo + placeholder = fallbackPainter, + error = fallbackPainter, + fallback = fallbackPainter, + imageLoader = imageLoader, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .defaultMinSize(minHeight = 200.dp) + .clip(HedvigTheme.shapes.cornerMedium), + ) + } + Spacer(Modifier.height(16.dp)) + HedvigText( + uiState.story.title, + style = HedvigTheme.typography.headlineMedium, + ) + Spacer(Modifier.height(4.dp)) + HedvigText( + uiState.story.subtitle, + style = HedvigTheme.typography.label, + color = HedvigTheme.colorScheme.textSecondaryTranslucent, + ) + Spacer(Modifier.height(24.dp)) + ProvideTextStyle( + HedvigTheme.typography.bodySmall + .copy(color = HedvigTheme.colorScheme.textSecondaryTranslucent), + ) { + val headingColor = HedvigTheme.colorScheme.textPrimary + RichText( + style = RichTextStyle( + headingStyle = { _, currentStyle -> + currentStyle.copy( + color = headingColor, + ) + }, + stringStyle = RichTextStringStyle( + boldStyle = SpanStyle( + color = headingColor, + ), + ), + ), + ) { + Markdown( + content = uiState.story.content, + ) + } + } + Spacer(Modifier.height(48.dp)) + HedvigText(stringResource(R.string.PUPPY_GUIDE_RATING_QUESTION)) + Spacer(Modifier.height(16.dp)) + logcat { "Mariia: uiState.story.rating ${uiState.story.rating}" } + RatingSection( + onRatingClick = onRatingClick, + selectedRating = uiState.story.rating, + ) + Spacer(Modifier.height(16.dp)) + } + } + } +} + +@Composable +private fun RatingSection(selectedRating: Int?, onRatingClick: (Int) -> Unit, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val ratings = listOf(1, 2, 3, 4, 5) + Row( + horizontalArrangement = Arrangement.SpaceAround, + modifier = Modifier, + ) { + ratings.forEach { rating -> + val isSelectedRating = selectedRating == rating + logcat { "Mariia: isSelectedRating $isSelectedRating" } + HedvigCard( + modifier = Modifier.weight(1f), + onClick = { + onRatingClick(rating) + }, + color = if (isSelectedRating) { + HedvigTheme.colorScheme.signalGreenFill + } else { + HedvigTheme.colorScheme.surfacePrimary + }, + ) { + HedvigText( + text = rating.toString(), + style = HedvigTheme.typography.bodyLarge, + color = if (isSelectedRating) { + HedvigTheme.colorScheme.textBlack + } else { + HedvigTheme.colorScheme.textSecondaryTranslucent + }, + textAlign = TextAlign.Center, + modifier = Modifier.padding(vertical = 16.dp), + ) + } + Spacer(Modifier.width(6.dp)) + } + } + Spacer(Modifier.height(16.dp)) + HorizontalItemsWithMaximumSpaceTaken( + startSlot = { + HedvigText( + stringResource(R.string.PUPPY_GUIDE_RATING_NOT_HELPFUL), + style = HedvigTheme.typography.label, + color = HedvigTheme.colorScheme.textSecondaryTranslucent, + ) + }, + endSlot = { + Row(horizontalArrangement = Arrangement.End) { + HedvigText( + stringResource(R.string.PUPPY_GUIDE_RATING_VERY_HELPFUL), + style = HedvigTheme.typography.label, + color = HedvigTheme.colorScheme.textSecondaryTranslucent, + ) + } + }, + spaceBetween = 8.dp, + ) + } +} + +@HedvigShortMultiScreenPreview +@Composable +private fun PuppyArticleScreenPreview( + @PreviewParameter(PuppyArticleUiStatePreviewProvider::class) uiState: PuppyArticleUiState, +) { + HedvigTheme { + Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { + PuppyArticleScreen( + uiState, + navigateUp = {}, + onReload = {}, + onRatingClick = {}, + imageLoader = rememberPreviewImageLoader(), + ) + } + } +} + +private class PuppyArticleUiStatePreviewProvider : + CollectionPreviewParameterProvider( + listOf( + PuppyArticleUiState.Success( + story = PuppyGuideStory( + categories = listOf("Food"), + content = "some long long long long long long long long long long long long" + + " long long long long long long long long long long long long content", + image = "", + name = "", + rating = 5, + isRead = false, + subtitle = "5 min read", + title = "Puppy food", + ), + ), + PuppyArticleUiState.Loading, + PuppyArticleUiState.Failure, + ), + ) diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyArticleViewModel.kt b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyArticleViewModel.kt new file mode 100644 index 0000000000..1ddbdbfee6 --- /dev/null +++ b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyArticleViewModel.kt @@ -0,0 +1,105 @@ +package com.hedvig.android.feature.help.center.puppyguide + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import com.hedvig.android.feature.help.center.data.GetPuppyGuideUseCase +import com.hedvig.android.feature.help.center.data.PuppyGuideStory +import com.hedvig.android.feature.help.center.data.SetArticleRatingUseCase +import com.hedvig.android.logger.logcat +import com.hedvig.android.molecule.android.MoleculeViewModel +import com.hedvig.android.molecule.public.MoleculePresenter +import com.hedvig.android.molecule.public.MoleculePresenterScope + +internal class PuppyArticleViewModel( + getPuppyGuideUseCase: GetPuppyGuideUseCase, + setArticleRatingUseCase: SetArticleRatingUseCase, + storyName: String, +) : MoleculeViewModel( + presenter = PuppyArticlePresenter(getPuppyGuideUseCase, storyName, setArticleRatingUseCase), + initialState = PuppyArticleUiState.Loading, + ) + +private class PuppyArticlePresenter( + private val getPuppyGuideUseCase: GetPuppyGuideUseCase, + private val storyName: String, + private val setArticleRatingUseCase: SetArticleRatingUseCase, +) : MoleculePresenter { + @Composable + override fun MoleculePresenterScope.present(lastState: PuppyArticleUiState): PuppyArticleUiState { + var currentState by remember { mutableStateOf(lastState) } + var loadIteration by remember { mutableIntStateOf(0) } + var rating by remember { mutableStateOf(null) } + + CollectEvents { event -> + when (event) { + PuppyArticleEvent.Reload -> loadIteration++ + is PuppyArticleEvent.RatingClick -> { + rating = event.rating + } + } + } + + LaunchedEffect(loadIteration) { + getPuppyGuideUseCase.invoke().fold( + ifLeft = { + currentState = PuppyArticleUiState.Failure + }, + ifRight = { stories -> + val matchingStory = stories?.firstOrNull { it.name == storyName } + currentState = if (matchingStory == null) { + PuppyArticleUiState.Failure + } else { + logcat { "Mariia. Story rating is: ${matchingStory.rating} " } + rating = matchingStory.rating + PuppyArticleUiState.Success(matchingStory) + } + }, + ) + } + + LaunchedEffect(rating) { + val state = currentState as? PuppyArticleUiState.Success ?: return@LaunchedEffect + val currentRating = rating ?: return@LaunchedEffect + val articleName = state.story.name + setArticleRatingUseCase.invoke( + articleName = articleName, + rating = currentRating, + ).fold( + ifLeft = { + // todo: snackbar? + }, + ifRight = { + logcat { "Mariia: rating set!" } + }, + ) + } + + return when (val state = currentState) { + PuppyArticleUiState.Failure -> state + PuppyArticleUiState.Loading -> state + is PuppyArticleUiState.Success -> + state.copy( + story = state.story.copy(rating = rating), + ) + } + } +} + +internal sealed interface PuppyArticleEvent { + data object Reload : PuppyArticleEvent + + data class RatingClick(val rating: Int) : PuppyArticleEvent +} + +internal sealed interface PuppyArticleUiState { + data class Success(val story: PuppyGuideStory) : PuppyArticleUiState + + data object Loading : PuppyArticleUiState + + data object Failure : PuppyArticleUiState +} diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideDestination.kt b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideDestination.kt new file mode 100644 index 0000000000..8b557540c1 --- /dev/null +++ b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideDestination.kt @@ -0,0 +1,383 @@ +package com.hedvig.android.feature.help.center.puppyguide + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +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.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +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.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.painter.ColorPainter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.ImageLoader +import coil.compose.AsyncImage +import com.hedvig.android.compose.ui.EmptyContentDescription +import com.hedvig.android.design.system.hedvig.ButtonDefaults +import com.hedvig.android.design.system.hedvig.HedvigButton +import com.hedvig.android.design.system.hedvig.HedvigErrorSection +import com.hedvig.android.design.system.hedvig.HedvigFullScreenCenterAlignedProgress +import com.hedvig.android.design.system.hedvig.HedvigScaffold +import com.hedvig.android.design.system.hedvig.HedvigShortMultiScreenPreview +import com.hedvig.android.design.system.hedvig.HedvigText +import com.hedvig.android.design.system.hedvig.HedvigTheme +import com.hedvig.android.design.system.hedvig.HighlightLabel +import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults +import com.hedvig.android.design.system.hedvig.Surface +import com.hedvig.android.design.system.hedvig.TopAppBarWithBack +import com.hedvig.android.design.system.hedvig.rememberPreviewImageLoader +import com.hedvig.android.feature.help.center.data.PuppyGuideStory +import hedvig.resources.R +import kotlinx.coroutines.launch + +@Composable +internal fun PuppyGuideDestination( + viewModel: PuppyGuideViewModel, + onNavigateUp: () -> Unit, + imageLoader: ImageLoader, + onNavigateToArticle: (PuppyGuideStory) -> Unit, +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + PuppyGuideScreen( + uiState, + onNavigateToArticle = onNavigateToArticle, + onNavigateUp = onNavigateUp, + reload = { + viewModel.emit(PuppyGuideEvent.Reload) + }, + imageLoader = imageLoader, + ) +} + +@Composable +private fun PuppyGuideScreen( + uiState: PuppyGuideUiState, + onNavigateToArticle: (PuppyGuideStory) -> Unit, + onNavigateUp: () -> Unit, + reload: () -> Unit, + imageLoader: ImageLoader, +) { + when (uiState) { + PuppyGuideUiState.Failure -> HedvigScaffold( + navigateUp = onNavigateUp, + ) { + HedvigErrorSection( + onButtonClick = reload, + modifier = Modifier.weight(1f), + ) + } + + PuppyGuideUiState.Loading -> HedvigFullScreenCenterAlignedProgress() + is PuppyGuideUiState.Success -> PuppyGuideSuccessScreen( + uiState, + onNavigateUp = onNavigateUp, + onNavigateToArticle = onNavigateToArticle, + imageLoader = imageLoader, + ) + } +} + +@Composable +private fun PuppyGuideSuccessScreen( + uiState: PuppyGuideUiState.Success, + onNavigateToArticle: (PuppyGuideStory) -> Unit, + onNavigateUp: () -> Unit, + imageLoader: ImageLoader, +) { + val categories = uiState.stories.flatMap { it.categories }.toSet().toList() + var selectedCategory by remember { mutableStateOf(null) } + val listState = rememberLazyListState() + val scope = rememberCoroutineScope() + + LaunchedEffect(selectedCategory) { + selectedCategory?.let { cat -> + val index = categories.indexOf(cat) + if (index >= 0) { + // Negative offset to scroll less and avoid sticky header covering the title + scope.launch { + listState.animateScrollToItem( + index + 2, + scrollOffset = -200, // todo: wtf + ) + } + } + } + } + + Surface( + color = HedvigTheme.colorScheme.backgroundPrimary, + modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing), + ) { + Column( + Modifier.fillMaxSize(), + ) { + TopAppBarWithBack( + title = "", + onClick = onNavigateUp, + ) + + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { + item { + Column { + Spacer(modifier = Modifier.height(8.dp)) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth(), + ) { + Image( + painter = painterResource(id = com.hedvig.android.feature.help.center.R.drawable.hundar_badar_pet), + contentDescription = null, + contentScale = ContentScale.Crop, + alignment = Alignment.Center, + modifier = Modifier + .height(300.dp) + .clip(HedvigTheme.shapes.cornerXLarge), + ) + } + Spacer(modifier = Modifier.height(16.dp)) + HedvigText(stringResource(R.string.PUPPY_GUIDE_TITLE)) + Spacer(modifier = Modifier.height(8.dp)) + HedvigText( + stringResource(R.string.PUPPY_GUIDE_INFO), + color = HedvigTheme.colorScheme.textSecondary, + ) + Spacer(modifier = Modifier.height(48.dp)) + } + } + + stickyHeader { + Surface( + color = HedvigTheme.colorScheme.backgroundPrimary, + modifier = Modifier.fillMaxWidth(), + ) { + Column { + GuideCategoriesRow( + categories, + onCategoryClick = { + selectedCategory = it + }, + ) + Spacer(modifier = Modifier.height(24.dp)) + } + } + } + + items(categories) { cat -> + CategoryWithArticlesSection( + cat, + stories = uiState.stories.filter { it.categories.contains(cat) }, + onNavigateToArticle = onNavigateToArticle, + imageLoader = imageLoader, + ) + } + } + } + } +} + +@Composable +private fun GuideCategoriesRow(categories: List, onCategoryClick: (String) -> Unit) { + Row(Modifier.horizontalScroll(rememberScrollState())) { + categories.forEach { + HedvigButton( + text = it, + enabled = true, + buttonSize = ButtonDefaults.ButtonSize.Medium, + buttonStyle = ButtonDefaults.ButtonStyle.Secondary, + onClick = { + onCategoryClick(it) + }, + ) + Spacer(Modifier.width(8.dp)) + } + } +} + +@Composable +private fun CategoryWithArticlesSection( + category: String, + stories: List, + onNavigateToArticle: (PuppyGuideStory) -> Unit, + imageLoader: ImageLoader, + modifier: Modifier = Modifier, +) { + Column(modifier) { + HedvigText( + category, + fontStyle = HedvigTheme.typography.headlineSmall.fontStyle, + fontSize = HedvigTheme.typography.headlineSmall.fontSize, + fontFamily = HedvigTheme.typography.headlineSmall.fontFamily, + ) + Spacer(Modifier.height(12.dp)) + Row( + horizontalArrangement = Arrangement.spacedBy(24.dp), + modifier = Modifier.horizontalScroll(rememberScrollState()), + ) { + val size = 148.dp + stories.forEach { story -> + ArticleItem( + story = story, + onNavigateToArticle = onNavigateToArticle, + imageLoader = imageLoader, + size = size, + ) + } + } + Spacer(Modifier.height(48.dp)) + } +} + +@Composable +private fun ArticleItem( + story: PuppyGuideStory, + onNavigateToArticle: (PuppyGuideStory) -> Unit, + imageLoader: ImageLoader, + size: Dp, + modifier: Modifier = Modifier, + shape: Shape = HedvigTheme.shapes.cornerMedium, +) { + Column( + modifier + .width(size) + .clip(shape) + .clickable( + onClick = { + onNavigateToArticle(story) + }, + ), + ) { + Box( + contentAlignment = Alignment.TopEnd, + ) { + val fallbackPainter: Painter = ColorPainter(Color.Black.copy(alpha = 0.7f)) + AsyncImage( + model = story.image, + contentDescription = EmptyContentDescription, // todo + placeholder = fallbackPainter, + error = fallbackPainter, + fallback = fallbackPainter, + imageLoader = imageLoader, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(size) + .clip(shape), + ) + if (story.isRead || story.rating != null) { + HighlightLabel( + modifier = modifier.padding( + end = 12.dp, + top = 12.dp, + ), + labelText = stringResource(R.string.PUPPY_GUIDE_LABEL_READ), + size = HighlightLabelDefaults.HighLightSize.Small, + color = HighlightLabelDefaults.HighlightColor.Grey(HighlightLabelDefaults.HighlightShade.LIGHT), + ) + } + } + + Spacer(Modifier.height(8.dp)) + HedvigText( + story.title, + style = HedvigTheme.typography.label, + maxLines = 1, + overflow = TextOverflow.Ellipsis, // todo: not by a11y req + ) + HedvigText( + story.subtitle, + style = HedvigTheme.typography.label, + color = HedvigTheme.colorScheme.textSecondaryTranslucent, + ) + } +} + +@HedvigShortMultiScreenPreview +@Composable +private fun PuppyArticleScreenAnimations( + @PreviewParameter(PuppyGuideUiStatePreviewProvider::class) uiState: PuppyGuideUiState, +) { + HedvigTheme { + Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { + PuppyGuideScreen( + uiState, + {}, + {}, + reload = {}, + imageLoader = rememberPreviewImageLoader(), + ) + } + } +} + +private class PuppyGuideUiStatePreviewProvider : + CollectionPreviewParameterProvider( + listOf( + PuppyGuideUiState.Success( + stories = listOf( + PuppyGuideStory( + categories = listOf("Food"), + content = "some long long long long long long long long long long long long" + + " long long long long long long long long long long long long content", + image = "", + name = "", + rating = 5, + isRead = true, + subtitle = "5 min read", + title = "Puppy food food food food food food food ", + ), + PuppyGuideStory( + categories = listOf("Training"), + content = "some long long long long long long long long long long long long" + + " long long long long long long long long long long long long content", + image = "", + name = "", + rating = 5, + isRead = false, + subtitle = "4 min read", + title = "Puppy training", + ), + ), + ), + PuppyGuideUiState.Loading, + PuppyGuideUiState.Failure, + ), + ) diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideViewModel.kt b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideViewModel.kt new file mode 100644 index 0000000000..abfa3c0c58 --- /dev/null +++ b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideViewModel.kt @@ -0,0 +1,68 @@ +package com.hedvig.android.feature.help.center.puppyguide + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import com.hedvig.android.feature.help.center.data.GetPuppyGuideUseCase +import com.hedvig.android.feature.help.center.data.PuppyGuideStory +import com.hedvig.android.molecule.android.MoleculeViewModel +import com.hedvig.android.molecule.public.MoleculePresenter +import com.hedvig.android.molecule.public.MoleculePresenterScope +import kotlinx.coroutines.flow.SharingStarted + +internal class PuppyGuideViewModel( + getPuppyGuideUseCase: GetPuppyGuideUseCase, +) : MoleculeViewModel( + presenter = PuppyGuidePresenter(getPuppyGuideUseCase), + initialState = PuppyGuideUiState.Loading, + sharingStarted = SharingStarted.WhileSubscribed(), + ) + +private class PuppyGuidePresenter( + private val getPuppyGuideUseCase: GetPuppyGuideUseCase, +) : MoleculePresenter { + @Composable + override fun MoleculePresenterScope.present(lastState: PuppyGuideUiState): PuppyGuideUiState { + var currentState by remember { mutableStateOf(lastState) } + var loadIteration by remember { mutableIntStateOf(0) } + + CollectEvents { event -> + when (event) { + PuppyGuideEvent.Reload -> loadIteration++ + } + } + + LaunchedEffect(loadIteration) { + getPuppyGuideUseCase.invoke().fold( + ifLeft = { + currentState = PuppyGuideUiState.Failure + }, + ifRight = { stories -> + currentState = if (stories == null) { + PuppyGuideUiState.Failure + } else { + PuppyGuideUiState.Success(stories) + } + }, + ) + } + + return currentState + } +} + +internal sealed interface PuppyGuideEvent { + data object Reload : PuppyGuideEvent +} + +internal sealed interface PuppyGuideUiState { + data class Success(val stories: List) : PuppyGuideUiState + + data object Loading : PuppyGuideUiState + + data object Failure : PuppyGuideUiState +} diff --git a/app/feature/feature-help-center/src/main/res/drawable/hundar_badar_pet.jpg b/app/feature/feature-help-center/src/main/res/drawable/hundar_badar_pet.jpg new file mode 100644 index 0000000000..c7e8f8bfc2 Binary files /dev/null and b/app/feature/feature-help-center/src/main/res/drawable/hundar_badar_pet.jpg differ