diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt index 3ccbbf00b2..7362229f4a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt @@ -24,7 +24,7 @@ interface TangemTechMarketsApi { suspend fun getCoinMarketData( @Path("coin_id") coinId: String, @Query("currency") currency: String, - ): ApiResponse + ): ApiResponse @GET("coins/{coin_id}/history") suspend fun getCoinChart( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketDetailsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt similarity index 52% rename from core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketDetailsResponse.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt index 227496a66e..13b5e0bdee 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketDetailsResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt @@ -3,21 +3,19 @@ package com.tangem.datasource.api.markets.models.response import com.squareup.moshi.Json import java.math.BigDecimal -data class TokenMarketDetailsResponse( +data class TokenMarketInfoResponse( @Json(name = "id") val id: String, @Json(name = "name") val name: String, @Json(name = "symbol") val symbol: String, - @Json(name = "active") - val active: Boolean, @Json(name = "current_price") val currentPrice: BigDecimal, @Json(name = "price_change_percentage") - val priceChangePercentage: PriceChangePercentage, + val priceChangePercentage: PriceChangePercentage?, @Json(name = "networks") - val networks: List, + val networks: List?, @Json(name = "short_description") val shortDescription: String?, @Json(name = "full_description") @@ -25,27 +23,28 @@ data class TokenMarketDetailsResponse( @Json(name = "insights") val insights: List?, @Json(name = "metrics") - val metrics: Metrics, + val metrics: Metrics?, @Json(name = "links") - val links: Links, + val links: Links?, @Json(name = "price_performance") - val pricePerformance: PricePerformance, + val pricePerformance: PricePerformance?, ) { + data class PriceChangePercentage( @Json(name = "24h") - val h24: BigDecimal, + val day: Int?, @Json(name = "1w") - val week1: BigDecimal, + val week: Int?, @Json(name = "1m") - val month1: BigDecimal, + val month: Int?, @Json(name = "3m") - val month3: BigDecimal, + val threeMonths: Int?, @Json(name = "6m") - val month6: BigDecimal, + val sixMonths: Int?, @Json(name = "1y") - val year1: BigDecimal, + val year: Int?, @Json(name = "all_time") - val allTime: BigDecimal, + val allTime: Int?, ) data class Network( @@ -55,78 +54,78 @@ data class TokenMarketDetailsResponse( val exchangeable: Boolean, @Json(name = "contract_address") val contractAddress: String, - @Json(name = "decimalCount") + @Json(name = "decimal_count") val decimalCount: Int, ) data class Insight( @Json(name = "holders_change") - val holdersChange: Change, + val holdersChange: Change?, @Json(name = "liquidity_change") - val liquidityChange: Change, + val liquidityChange: Change?, @Json(name = "buy_pressure_change") - val buyPressureChange: Change, + val buyPressureChange: Change?, @Json(name = "experienced_buyer_change") - val experiencedBuyerChange: Change, - ) { - data class Change( - @Json(name = "1d") - val day1: Int, - @Json(name = "1w") - val week1: Int, - @Json(name = "1m") - val month1: Int, - ) - } + val experiencedBuyerChange: Change?, + ) + + data class Change( + @Json(name = "24h") + val day: Int?, + @Json(name = "1w") + val week: Int?, + @Json(name = "1m") + val month: Int?, + ) data class Metrics( @Json(name = "market_rating") - val marketRating: Int, + val marketRating: Int?, @Json(name = "circulating_supply") - val circulatingSupply: BigDecimal, + val circulatingSupply: BigDecimal?, @Json(name = "market_cap") - val marketCap: BigDecimal, + val marketCap: BigDecimal?, @Json(name = "volume_24h") - val volume24h: BigDecimal, + val volume24h: BigDecimal?, @Json(name = "total_supply") - val totalSupply: BigDecimal, + val totalSupply: BigDecimal?, @Json(name = "fully_diluted_valuation") - val fullyDilutedValuation: BigDecimal, + val fullyDilutedValuation: BigDecimal?, ) data class Links( @Json(name = "official_links") - val officialLinks: List = emptyList(), + val officialLinks: List?, @Json(name = "social") - val social: List = emptyList(), + val social: List?, @Json(name = "repository") - val repository: List = emptyList(), + val repository: List?, @Json(name = "blockchain_site") - val blockchainSite: List = emptyList(), + val blockchainSite: List?, ) data class Link( @Json(name = "title") - val title: String?, + val title: String, @Json(name = "id") - val id: String, + val id: String?, @Json(name = "link") - val url: String, + val link: String, ) data class PricePerformance( - @Json(name = "high_price") - val highPrice: Price, - @Json(name = "low_price") - val lowPrice: Price, - ) { - data class Price( - @Json(name = "24h") - val h24: BigDecimal, - @Json(name = "1m") - val month1: BigDecimal, - @Json(name = "all_time") - val allTime: BigDecimal, - ) - } + @Json(name = "24h") + val day: Range?, + @Json(name = "1m") + val month: Range?, + @Json(name = "all_time") + val allTime: Range?, + ) + + data class Range( + @Json(name = "low") + val low: Int?, + @Json(name = "high") + val high: Int?, + ) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt index 7ef56f015e..9667a00860 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt @@ -10,6 +10,7 @@ import androidx.compose.material.ButtonColors import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.components.buttons.common.* @@ -26,6 +27,7 @@ fun TextButton( onClick: () -> Unit, modifier: Modifier = Modifier, colors: ButtonColors = TangemButtonsDefaults.defaultTextButtonColors, + textStyle: TextStyle = TangemTheme.typography.button, enabled: Boolean = true, ) { TangemButton( @@ -37,6 +39,7 @@ fun TextButton( showProgress = false, colors = colors, size = TangemButtonSize.Text, + textStyle = textStyle, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt index 2e5b4ee1c2..5d09690136 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt @@ -9,14 +9,23 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.buttons.PrimarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.* import com.valentinilk.shimmer.* @@ -45,6 +54,70 @@ fun CircleShimmer(modifier: Modifier = Modifier) { ) } +/** + * Shimmer for text + * Height will be set automatically + * + * @param textSizeHeight if true, height will be set to font size height. + */ +@Composable +fun TextShimmer( + style: TextStyle, + modifier: Modifier = Modifier, + text: String = "A", + radius: Dp = TangemTheme.dimens.radius3, + textSizeHeight: Boolean = false, +) { + if (textSizeHeight) { + val lineHeight = with(LocalDensity.current) { style.lineHeight.toDp() } + + Box( + modifier = Modifier.requiredHeight(lineHeight), + contentAlignment = Alignment.CenterStart, + ) { + Text( + modifier = modifier + .clip(RoundedCornerShape(size = radius)) + .shimmer(LocalTangemShimmer.current), + text = text, + style = style.copy(lineHeight = style.fontSize), + maxLines = 1, + ) + } + } else { + Text( + modifier = modifier + .clip(RoundedCornerShape(size = radius)) + .shimmer(LocalTangemShimmer.current), + text = text, + style = style, + maxLines = 1, + ) + } +} + +/** + * Shimmer for SmallButton + * Height and min width will be set automatically + */ +@Composable +fun SmallButtonShimmer(modifier: Modifier = Modifier, withIcon: Boolean = false) { + PrimarySmallButton( + config = SmallButtonConfig( + text = stringReference("B"), + onClick = {}, + icon = if (withIcon) { + TangemButtonIconPosition.Start(iconResId = R.drawable.ic_plus_24) + } else { + TangemButtonIconPosition.None + }, + ), + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius16)) + .shimmer(LocalTangemShimmer.current), + ) +} + internal val TangemShimmer: Shimmer @Composable get() = rememberShimmer( @@ -106,6 +179,17 @@ private fun ShimmersPreview() { .height(TangemTheme.dimens.size24), ) CircleShimmer(modifier = Modifier.size(size = TangemTheme.dimens.size42)) + TextShimmer( + style = TangemTheme.typography.body1, + modifier = Modifier.fillMaxWidth(fraction = 0.4f), + ) + TextShimmer( + style = TangemTheme.typography.body1, + textSizeHeight = true, + modifier = Modifier.fillMaxWidth(fraction = 0.4f), + ) + SmallButtonShimmer(withIcon = true) + SmallButtonShimmer(withIcon = false) } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt index 027109f071..900aafe401 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt @@ -97,7 +97,6 @@ private fun Preview_Grid() { }, content = { GridItems( - itemPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing4), items = persistentListOf( stringReference("Fist item"), stringReference("Second item"), @@ -105,7 +104,6 @@ private fun Preview_Grid() { itemContent = { PreviewItem(text = it) }, - horizontalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) }, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlockContent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlockContent.kt index 1a40b55863..423100d2e9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlockContent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlockContent.kt @@ -44,7 +44,6 @@ inline fun InformationBlockContentScope.GridItems( items: ImmutableList, itemContent: @Composable BoxScope.(T) -> Unit, modifier: Modifier = Modifier, - itemPadding: PaddingValues = PaddingValues(all = TangemTheme.dimens.spacing0), verticalAlignment: Alignment.Vertical = Alignment.Top, horizontalArragement: Arrangement.Horizontal = Arrangement.Start, ) { @@ -59,7 +58,6 @@ inline fun InformationBlockContentScope.GridItems( Column( modifier = modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, ) { rowItems.fastForEach { row -> @@ -70,10 +68,7 @@ inline fun InformationBlockContentScope.GridItems( ) { row.fastForEach { item -> Box( - modifier = Modifier - .padding(itemPadding) - .weight(1f), - contentAlignment = Alignment.Center, + modifier = Modifier.weight(1f), ) { itemContent(item) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/text/TooltipText.kt b/core/ui/src/main/java/com/tangem/core/ui/components/text/TooltipText.kt index 110080210d..801d47263b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/text/TooltipText.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/text/TooltipText.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -28,7 +29,7 @@ fun TooltipText( text: TextReference, onInfoClick: () -> Unit, modifier: Modifier = Modifier, - useSmallerText: Boolean = false, + textStyle: TextStyle = TangemTheme.typography.caption2, ) { val interactionSource = remember { MutableInteractionSource() } @@ -45,18 +46,16 @@ fun TooltipText( Text( modifier = Modifier.weight(1f, fill = false), text = text.resolveReference(), - style = if (useSmallerText) { - TangemTheme.typography.caption2 - } else { - TangemTheme.typography.subtitle2 - }, + style = textStyle, color = TangemTheme.colors.text.tertiary, maxLines = 1, overflow = TextOverflow.Ellipsis, ) IconButton( - modifier = Modifier.requiredSize(TangemTheme.dimens.size24), + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing4) + .requiredSize(TangemTheme.dimens.size16), interactionSource = interactionSource, onClick = onInfoClick, ) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemAnimations.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemAnimations.kt new file mode 100644 index 0000000000..b735f815a7 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemAnimations.kt @@ -0,0 +1,33 @@ +package com.tangem.core.ui.res + +import androidx.compose.animation.* +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.runtime.* + +@Immutable +object TangemAnimations { + + val transitionSpecs = TransitionSpecs + + @Composable + @NonRestartableComposable + fun horizontalIndicatorAsState(targetFraction: Float): State { + return animateFloatAsState( + targetValue = targetFraction, + animationSpec = tween(durationMillis = 300), + label = "Indicator fraction", + ) + } + + @Immutable + object TransitionSpecs { + + @Stable + val textChange: ContentTransform = + fadeIn(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy)) togetherWith + fadeOut(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy)) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/PreviewUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/PreviewUtils.kt new file mode 100644 index 0000000000..6cb1f784b9 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/PreviewUtils.kt @@ -0,0 +1,55 @@ +package com.tangem.core.ui.utils + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned +import com.tangem.core.ui.components.SpacerH4 +import com.tangem.core.ui.res.TangemThemePreview +import kotlinx.coroutines.delay + +/** + * A container that shows a shimmer effect on top of the actual content. + * The shimmer effect is toggled every 2 seconds. + * Used for previewing components with shimmer effect and comparing their sizes with the actual content. + * + * If height is changing during the preview, it means that the actual content is not aligned with the shimmer effect. + * + * @param actualContent The actual content to be displayed. + * @param shimmerContent The shimmer effect to be displayed. + */ +@Composable +fun PreviewShimmerContainer(actualContent: @Composable () -> Unit, shimmerContent: @Composable () -> Unit) { + Column { + var height by remember { mutableIntStateOf(0) } + Row { + Text("height = $height") + } + SpacerH4() + + var shimmerVisible by remember { mutableStateOf(true) } + + TangemThemePreview { + Box( + Modifier.onGloballyPositioned { + height = it.size.height + }, + ) { + actualContent() + if (shimmerVisible) { + shimmerContent() + } + } + } + + LaunchedEffect(Unit) { + while (true) { + delay(timeMillis = 2000) + shimmerVisible = !shimmerVisible + } + } + } +} \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index b58bd811d3..6bd59980f7 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -107,4 +107,13 @@ internal class DefaultMarketsTokenRepository( return tokenChartConverter.convert(interval, response.getOrThrow()) } + + override suspend fun getTokenInfo(fiatCurrencyCode: String, tokenId: String): TokenMarketInfo { + val response = marketsApi.getCoinMarketData( + currency = fiatCurrencyCode, + coinId = tokenId, + ) + + return TokenMarketInfoConverter().convert(response.getOrThrow()) + } } \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt new file mode 100644 index 0000000000..524d27debd --- /dev/null +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt @@ -0,0 +1,117 @@ +package com.tangem.data.markets.converters + +import com.tangem.datasource.api.markets.models.response.TokenMarketInfoResponse +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.utils.converter.Converter + +internal class TokenMarketInfoConverter : Converter { + + override fun convert(value: TokenMarketInfoResponse): TokenMarketInfo { + return with(value) { + TokenMarketInfo( + id = id, + name = name, + symbol = symbol, + currentPrice = currentPrice, + priceChangePercentage = priceChangePercentage?.convert(), + networks = networks?.convert(), + shortDescription = shortDescription, + fullDescription = fullDescription, + insights = insights?.convert(), + metrics = metrics?.convert(), + links = links?.convert(), + pricePerformance = pricePerformance?.convert(), + ) + } + } + + private fun TokenMarketInfoResponse.PriceChangePercentage.convert(): TokenMarketInfo.PriceChangePercentage { + return TokenMarketInfo.PriceChangePercentage( + day = day, + week = week, + month = month, + threeMonths = threeMonths, + sixMonths = sixMonths, + year = year, + allTime = allTime, + ) + } + + @JvmName("convertNetwork") + private fun List.convert(): List { + return map { + TokenMarketInfo.Network( + networkId = it.networkId, + exchangeable = it.exchangeable, + contractAddress = it.contractAddress, + decimalCount = it.decimalCount, + ) + } + } + + @JvmName("convertInsight") + private fun List.convert(): List { + return map { + TokenMarketInfo.Insight( + holdersChange = it.holdersChange?.convert(), + liquidityChange = it.liquidityChange?.convert(), + buyPressureChange = it.buyPressureChange?.convert(), + experiencedBuyerChange = it.experiencedBuyerChange?.convert(), + ) + } + } + + private fun TokenMarketInfoResponse.Change.convert(): TokenMarketInfo.Change { + return TokenMarketInfo.Change( + day = day, + week = week, + month = month, + ) + } + + private fun TokenMarketInfoResponse.Metrics.convert(): TokenMarketInfo.Metrics { + return TokenMarketInfo.Metrics( + marketRating = marketRating, + circulatingSupply = circulatingSupply, + marketCap = marketCap, + volume24h = volume24h, + totalSupply = totalSupply, + fullyDilutedValuation = fullyDilutedValuation, + ) + } + + private fun TokenMarketInfoResponse.Links.convert(): TokenMarketInfo.Links { + return TokenMarketInfo.Links( + officialLinks = officialLinks?.convert(), + social = social?.convert(), + repository = repository?.convert(), + blockchainSite = blockchainSite?.convert(), + ) + } + + @JvmName("convertLink") + private fun List.convert(): List { + return map { + TokenMarketInfo.Link( + title = it.title, + id = it.id, + link = it.link, + ) + } + } + + private fun TokenMarketInfoResponse.PricePerformance.convert(): TokenMarketInfo.PricePerformance { + return TokenMarketInfo.PricePerformance( + day = day?.convert(), + month = month?.convert(), + allTime = allTime?.convert(), + ) + } + + private fun TokenMarketInfoResponse.Range.convert(): TokenMarketInfo.Range { + return TokenMarketInfo.Range( + low = low, + high = high, + ) + } +} \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt new file mode 100644 index 0000000000..1ad9daf777 --- /dev/null +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt @@ -0,0 +1,81 @@ +package com.tangem.domain.markets + +import java.math.BigDecimal + +data class TokenMarketInfo( + val id: String, + val name: String, + val symbol: String, + val currentPrice: BigDecimal, + val priceChangePercentage: PriceChangePercentage?, + val networks: List?, + val shortDescription: String?, + val fullDescription: String?, + val insights: List?, + val metrics: Metrics?, + val links: Links?, + val pricePerformance: PricePerformance?, +) { + data class PriceChangePercentage( + val day: Int?, + val week: Int?, + val month: Int?, + val threeMonths: Int?, + val sixMonths: Int?, + val year: Int?, + val allTime: Int?, + ) + + data class Network( + val networkId: String, + val exchangeable: Boolean, + val contractAddress: String, + val decimalCount: Int, + ) + + data class Insight( + val holdersChange: Change?, + val liquidityChange: Change?, + val buyPressureChange: Change?, + val experiencedBuyerChange: Change?, + ) + + data class Change( + val day: Int?, + val week: Int?, + val month: Int?, + ) + + data class Metrics( + val marketRating: Int?, + val circulatingSupply: BigDecimal?, + val marketCap: BigDecimal?, + val volume24h: BigDecimal?, + val totalSupply: BigDecimal?, + val fullyDilutedValuation: BigDecimal?, + ) + + data class Links( + val officialLinks: List?, + val social: List?, + val repository: List?, + val blockchainSite: List?, + ) + + data class Link( + val title: String, + val id: String?, + val link: String, + ) + + data class PricePerformance( + val day: Range?, + val month: Range?, + val allTime: Range?, + ) + + data class Range( + val low: Int?, + val high: Int?, + ) +} \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenMarketInfoUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenMarketInfoUseCase.kt new file mode 100644 index 0000000000..f6b2ecf317 --- /dev/null +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenMarketInfoUseCase.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.markets + +import arrow.core.Either +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.repositories.MarketsTokenRepository + +class GetTokenMarketInfoUseCase( + private val marketsTokenRepository: MarketsTokenRepository, +) { + + suspend operator fun invoke(appCurrency: AppCurrency, tokenId: String): Either { + return Either.catch { + marketsTokenRepository.getTokenInfo( + fiatCurrencyCode = appCurrency.code, + tokenId = tokenId, + ) + }.mapLeft {} + } +} \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt index 8b3ce43ce5..0fd36a20fa 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt @@ -11,4 +11,6 @@ interface MarketsTokenRepository { ): TokenListBatchFlow suspend fun getChart(fiatCurrencyCode: String, interval: PriceChangeInterval, tokenId: String): TokenChart + + suspend fun getTokenInfo(fiatCurrencyCode: String, tokenId: String): TokenMarketInfo } \ No newline at end of file diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index d5b82bb93d..f2becec495 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { /* Compose */ implementation(deps.compose.coil) implementation(deps.compose.foundation) + implementation(deps.compose.material) implementation(deps.compose.material3) implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoPoint.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoPoint.kt new file mode 100644 index 0000000000..5be2fb74a5 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoPoint.kt @@ -0,0 +1,145 @@ +package com.tangem.features.markets.details.impl.ui.components + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.text.TooltipText +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemAnimations +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.PreviewShimmerContainer +import com.tangem.features.markets.details.impl.ui.entity.InfoPointUM + +@Composable +internal fun InfoPoint(infoPointUM: InfoPointUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier.padding(vertical = TangemTheme.dimens.spacing8), + horizontalAlignment = Alignment.Start, + ) { + if (infoPointUM.onInfoClick != null) { + TooltipText( + text = infoPointUM.title, + onInfoClick = infoPointUM.onInfoClick, + textStyle = TangemTheme.typography.caption2, + ) + } else { + Text( + text = infoPointUM.title.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + AnimatedContent( + modifier = Modifier, + targetState = infoPointUM.value, + transitionSpec = { TangemAnimations.transitionSpecs.textChange }, + label = "insight block", + ) { + Text( + text = it, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + } + } +} + +@Composable +internal fun InfoPointShimmer(modifier: Modifier = Modifier, withTooltip: Boolean = false) { + Column( + modifier = modifier.padding(vertical = TangemTheme.dimens.spacing8), + horizontalAlignment = Alignment.Start, + ) { + if (withTooltip) { + Box( + modifier = Modifier + .requiredHeight(TangemTheme.dimens.size16) + .fillMaxWidth(), + contentAlignment = Alignment.CenterStart, + ) { + TextShimmer( + modifier = Modifier.fillMaxWidth(), + style = TangemTheme.typography.caption2, + textSizeHeight = false, + ) + } + } else { + TextShimmer( + modifier = Modifier.fillMaxWidth(), + style = TangemTheme.typography.caption2, + textSizeHeight = true, + ) + } + TextShimmer( + modifier = Modifier.fillMaxWidth(fraction = 0.5f), + style = TangemTheme.typography.body1, + textSizeHeight = true, + ) + } +} + +@Preview +@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun ContentPreview() { + TangemThemePreview { + Column( + modifier = Modifier + .width(150.dp) + .background(TangemTheme.colors.background.tertiary), + ) { + InfoPoint( + infoPointUM = InfoPointUM( + title = stringReference("Market Cap"), + value = "$1,000,000,000", + ), + ) + InfoPoint( + infoPointUM = InfoPointUM( + title = stringReference("Market Cap"), + value = "$1,000,000,000", + onInfoClick = { }, + ), + ) + } + } +} + +@Preview +@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewShimmer() { + TangemThemePreview { + PreviewShimmerContainer( + shimmerContent = { + Column( + modifier = Modifier + .width(150.dp) + .background(TangemTheme.colors.background.tertiary), + ) { + InfoPointShimmer(modifier = Modifier.fillMaxWidth()) + InfoPointShimmer( + modifier = Modifier.fillMaxWidth(), + withTooltip = true, + ) + } + }, + actualContent = { + ContentPreview() + }, + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt new file mode 100644 index 0000000000..a8cd4bf110 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt @@ -0,0 +1,198 @@ +package com.tangem.features.markets.details.impl.ui.components + +import android.content.res.Configuration +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.block.information.GridItems +import com.tangem.core.ui.components.block.information.InformationBlock +import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.PreviewShimmerContainer +import com.tangem.domain.markets.PriceChangeInterval +import com.tangem.features.markets.details.impl.ui.entity.InfoPointUM +import com.tangem.features.markets.details.impl.ui.entity.InsightsUM +import com.tangem.features.markets.details.impl.ui.getText +import com.tangem.features.markets.impl.R +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +@Composable +internal fun InsightsBlock(state: InsightsUM, modifier: Modifier = Modifier) { + var currentInterval by remember { mutableStateOf(PriceChangeInterval.H24) } + + InformationBlock( + modifier = modifier, + title = { + Text( + modifier = Modifier, + text = stringResource(R.string.markets_token_details_insights), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + action = { + SegmentedButtons( + config = persistentListOf( + PriceChangeInterval.H24, + PriceChangeInterval.WEEK, + PriceChangeInterval.MONTH, + ), + initialSelectedItem = PriceChangeInterval.H24, + onClick = { currentInterval = it }, + ) { + Box( + Modifier + .fillMaxSize() + .align(Alignment.Center) + .padding(vertical = TangemTheme.dimens.spacing4), + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = it.getText().resolveReference(), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.primary1, + ) + } + } + }, + content = { + val infoPoints = when (currentInterval) { + PriceChangeInterval.H24 -> state.h24Info + PriceChangeInterval.WEEK -> state.weekInfo + PriceChangeInterval.MONTH -> state.monthInfo + else -> state.h24Info + } + + GridItems( + items = infoPoints, + itemContent = { + InfoPoint( + modifier = Modifier.align(Alignment.CenterStart), + infoPointUM = it, + ) + }, + ) + }, + ) +} + +@Composable +internal fun InsightsBlockPlaceholder(modifier: Modifier = Modifier) { + val subtitle2dp = with(LocalDensity.current) { TangemTheme.typography.subtitle2.lineHeight.toDp() } + val caption1dp = with(LocalDensity.current) { TangemTheme.typography.caption1.lineHeight.toDp() } + val headerHeight = maxOf(subtitle2dp, caption1dp) + TangemTheme.dimens.spacing4 + + InformationBlock( + modifier = modifier, + title = { + RectangleShimmer( + modifier = Modifier + .height(headerHeight) + .fillMaxWidth(), + radius = TangemTheme.dimens.radius3, + ) + }, + content = { + GridItems( + items = List(size = 4) { it }.toImmutableList(), + horizontalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + itemContent = { + InfoPointShimmer( + modifier = Modifier.fillMaxWidth(), + ) + }, + ) + }, + ) +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun ContentPreview() { + TangemThemePreview { + InsightsBlock( + state = InsightsUM( + h24Info = persistentListOf( + InfoPointUM( + title = resourceReference(R.string.markets_token_details_experienced_buyers), + value = "1 000 000 000", + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_buy_pressure), + value = "1 000 000 000", + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_holders), + value = "1 000 000 000", + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_liquidity), + value = "1 000 000 000", + ), + ), + weekInfo = persistentListOf( + InfoPointUM( + title = resourceReference(R.string.markets_token_details_experienced_buyers), + value = "1 000 000", + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_buy_pressure), + value = "1 000 000", + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_holders), + value = "1 000 000", + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_liquidity), + value = "1 000 000", + ), + ), + monthInfo = persistentListOf( + InfoPointUM( + title = resourceReference(R.string.markets_token_details_experienced_buyers), + value = "1 000", + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_buy_pressure), + value = "1 000", + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_holders), + value = "1 000", + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_liquidity), + value = "1 000", + ), + ), + ), + ) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewPlaceholder() { + TangemThemePreview { + PreviewShimmerContainer( + actualContent = { ContentPreview() }, + shimmerContent = { InsightsBlockPlaceholder() }, + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt new file mode 100644 index 0000000000..2d968b1327 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt @@ -0,0 +1,221 @@ +package com.tangem.features.markets.details.impl.ui.components + +import android.content.res.Configuration +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.charts.downsample.fastForEach +import com.tangem.core.ui.components.SmallButtonShimmer +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.block.information.InformationBlock +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.components.inputrow.inner.DividerContainer +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.PreviewShimmerContainer +import com.tangem.features.markets.details.impl.ui.entity.LinksUM +import com.tangem.features.markets.impl.R +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun LinksBlock(state: LinksUM, modifier: Modifier = Modifier) { + InformationBlock( + modifier = modifier, + title = { + Text( + text = stringResource(id = R.string.markets_token_details_links), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + content = { + Column { + SubBlock( + title = stringResource(id = R.string.markets_token_details_official_links), + links = state.officialLinks, + onLinkClick = state.onLinkClick, + ) + SubBlock( + title = stringResource(id = R.string.markets_token_details_social), + links = state.social, + onLinkClick = state.onLinkClick, + ) + SubBlock( + title = stringResource(id = R.string.markets_token_details_repository), + links = state.repository, + onLinkClick = state.onLinkClick, + ) + SubBlock( + title = stringResource(id = R.string.markets_token_details_blockchain_site), + links = state.blockchainSite, + onLinkClick = state.onLinkClick, + ) + } + }, + ) +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun SubBlock( + links: ImmutableList, + onLinkClick: (LinksUM.LinkUM) -> Unit, + modifier: Modifier = Modifier, + lastBlock: Boolean = false, + title: String = "Official links", +) { + if (links.isEmpty()) return + + DividerContainer( + modifier = modifier, + showDivider = !lastBlock, + ) { + Column( + modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + Text( + text = title, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + links.fastForEach { + SecondarySmallButton( + config = SmallButtonConfig( + text = it.title, + onClick = { onLinkClick(it) }, + icon = TangemButtonIconPosition.Start(iconResId = it.iconRes), + ), + ) + } + } + } + } +} + +@Composable +fun LinksBlockPlaceholder(modifier: Modifier = Modifier) { + InformationBlock( + modifier = modifier, + title = { + TextShimmer( + modifier = Modifier.fillMaxWidth(), + style = TangemTheme.typography.subtitle2, + ) + }, + content = { + Column { + SubBlockPlaceholder() + SubBlockPlaceholder() + SubBlockPlaceholder(lastBlock = true) + } + }, + ) +} + +@Composable +private fun SubBlockPlaceholder(modifier: Modifier = Modifier, lastBlock: Boolean = false) { + DividerContainer( + modifier = modifier, + showDivider = !lastBlock, + ) { + Column( + modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + TextShimmer( + modifier = Modifier.width(78.dp), + style = TangemTheme.typography.caption2, + ) + Row( + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + repeat(times = 3) { + SmallButtonShimmer( + modifier = Modifier.weight(1f), + withIcon = true, + ) + } + } + } + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun ContentPreview() { + TangemThemePreview { + LinksBlock( + state = LinksUM( + officialLinks = persistentListOf( + LinksUM.LinkUM( + title = stringReference("Website"), + iconRes = R.drawable.ic_plus_24, + url = "https://tangem.com", + ), + LinksUM.LinkUM( + title = stringReference("Website"), + iconRes = R.drawable.ic_plus_24, + url = "https://tangem.com", + ), + LinksUM.LinkUM( + title = stringReference("Website"), + iconRes = R.drawable.ic_plus_24, + url = "https://tangem.com", + ), + ), + social = persistentListOf( + LinksUM.LinkUM( + title = stringReference("Twitter"), + iconRes = R.drawable.ic_plus_24, + url = "https://tangem.com", + ), + LinksUM.LinkUM( + title = stringReference("Facebook"), + iconRes = R.drawable.ic_plus_24, + url = "https://tangem.com", + ), + ), + repository = persistentListOf( + LinksUM.LinkUM( + title = stringReference("Github"), + iconRes = R.drawable.ic_plus_24, + url = "https://tangem.com", + ), + ), + blockchainSite = persistentListOf(), + onLinkClick = {}, + ), + ) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PlaceholderPreview() { + TangemThemePreview { + PreviewShimmerContainer( + shimmerContent = { LinksBlockPlaceholder() }, + actualContent = { ContentPreview() }, + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt index d138a1c23d..ee9ce1d5b3 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt @@ -18,7 +18,7 @@ import com.tangem.features.markets.details.impl.ui.entity.MarketsTokenDetailsUM import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData @Composable -fun MarketTokenDetailsChart(state: MarketsTokenDetailsUM.ChartState, modifier: Modifier = Modifier) { +internal fun MarketTokenDetailsChart(state: MarketsTokenDetailsUM.ChartState, modifier: Modifier = Modifier) { val growingColor = TangemTheme.colors.icon.accent val fallingColor = TangemTheme.colors.icon.warning diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MetricsBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MetricsBlock.kt new file mode 100644 index 0000000000..20308e8e72 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MetricsBlock.kt @@ -0,0 +1,166 @@ +package com.tangem.features.markets.details.impl.ui.components + +import android.content.res.Configuration +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.TextButton +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.block.information.GridItems +import com.tangem.core.ui.components.block.information.InformationBlock +import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.PreviewShimmerContainer +import com.tangem.features.markets.details.impl.ui.entity.InfoPointUM +import com.tangem.features.markets.details.impl.ui.entity.MetricsUM +import com.tangem.features.markets.impl.R +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +const val MAX_METRICS_COUNT = 6 + +@Composable +internal fun MetricsBlock(state: MetricsUM, modifier: Modifier = Modifier) { + var expanded by remember { mutableStateOf(false) } + + InformationBlock( + modifier = modifier, + title = { + Text( + text = stringResource(id = R.string.markets_token_details_metrics), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + action = { + if (state.metrics.size > MAX_METRICS_COUNT) { + ShowLessMoreButton(expanded = expanded, onClick = { expanded = !expanded }) + } + }, + content = { + val metrics = if (expanded) { + state.metrics + } else { + state.metrics.take(MAX_METRICS_COUNT).toImmutableList() + } + + GridItems( + items = metrics, + itemContent = { + InfoPoint(infoPointUM = it) + }, + ) + }, + ) +} + +// TODO make TextButton clickable area smaller and remove paddings for an action in InformationBlock +@Composable +private fun ShowLessMoreButton(expanded: Boolean, onClick: () -> Unit) { + // FIXME add string resources + val text = if (expanded) { + "See less" + } else { + "See more" + } + + TextButton( + text = text, + onClick = onClick, + colors = TangemButtonsDefaults.positiveButtonColors, + textStyle = TangemTheme.typography.body2, + ) +} + +@Composable +internal fun MetricsBlockPlaceholder(modifier: Modifier = Modifier) { + InformationBlock( + modifier = modifier, + title = { + TextShimmer( + modifier = Modifier.fillMaxWidth(), + radius = TangemTheme.dimens.radius3, + style = TangemTheme.typography.subtitle2, + ) + }, + action = { + Box(Modifier) + }, + content = { + GridItems( + items = List(size = 6) { it }.toImmutableList(), + horizontalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + itemContent = { + InfoPointShimmer( + modifier = Modifier.fillMaxWidth(), + withTooltip = true, + ) + }, + ) + }, + ) +} + +@Preview +@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun BlockPreview() { + TangemThemePreview { + MetricsBlock( + state = MetricsUM( + metrics = persistentListOf( + InfoPointUM( + title = resourceReference(R.string.markets_token_details_market_capitalization), + value = "1.2T", + onInfoClick = {}, + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_market_rating), + value = "A", + onInfoClick = {}, + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_trading_volume), + value = "1.2T", + onInfoClick = {}, + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_fully_diluted_valuation), + value = "1.2T", + onInfoClick = {}, + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_circulating_supply), + value = "1.2T", + onInfoClick = {}, + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_total_supply), + value = "1.2T", + onInfoClick = {}, + ), + ), + ), + ) + } +} + +@Preview +@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewPlaceholder() { + TangemThemePreview { + PreviewShimmerContainer( + actualContent = { BlockPreview() }, + shimmerContent = { MetricsBlockPlaceholder() }, + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt new file mode 100644 index 0000000000..799200dc99 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt @@ -0,0 +1,260 @@ +package com.tangem.features.markets.details.impl.ui.components + +import android.content.res.Configuration +import androidx.compose.animation.* +import androidx.compose.foundation.layout.* +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerW8 +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.block.information.InformationBlock +import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemAnimations +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.PreviewShimmerContainer +import com.tangem.domain.markets.PriceChangeInterval +import com.tangem.features.markets.details.impl.ui.entity.PricePerformanceUM +import com.tangem.features.markets.details.impl.ui.getText +import com.tangem.features.markets.impl.R +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun PricePerformanceBlock(state: PricePerformanceUM, modifier: Modifier = Modifier) { + var currentInterval by remember { mutableStateOf(PriceChangeInterval.H24) } + + InformationBlock( + modifier = modifier, + title = { + Text( + text = stringResource(id = R.string.markets_token_details_price_performance), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + action = { + SegmentedButtons( + config = persistentListOf( + PriceChangeInterval.H24, + PriceChangeInterval.MONTH, + PriceChangeInterval.ALL_TIME, + ), + initialSelectedItem = PriceChangeInterval.H24, + onClick = { currentInterval = it }, + ) { + Box( + Modifier + .fillMaxSize() + .align(Alignment.Center) + .padding(vertical = TangemTheme.dimens.spacing4), + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = it.getText().resolveReference(), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.primary1, + ) + } + } + }, + content = { + val value = when (currentInterval) { + PriceChangeInterval.H24 -> state.h24 + PriceChangeInterval.MONTH -> state.month + PriceChangeInterval.ALL_TIME -> state.all + else -> error("") + } + + Content( + modifier = Modifier.fillMaxWidth(), + state = value, + ) + }, + ) +} + +@Composable +private fun Content(state: PricePerformanceUM.Value, modifier: Modifier = Modifier) { + val animatedIndicatorFraction by TangemAnimations.horizontalIndicatorAsState( + targetFraction = state.indicatorFraction, + ) + + Column( + modifier = modifier + .padding(vertical = TangemTheme.dimens.spacing8), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(R.string.markets_token_details_low), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + SpacerW8() + Text( + text = stringResource(R.string.markets_token_details_high), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + LinearProgressIndicator( + modifier = Modifier + .height(TangemTheme.dimens.size6) + .fillMaxWidth(), + progress = { animatedIndicatorFraction }, + color = TangemTheme.colors.text.accent, + trackColor = TangemTheme.colors.background.tertiary, + strokeCap = StrokeCap.Round, + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + AnimatedContent( + targetState = state.low, + transitionSpec = { TangemAnimations.transitionSpecs.textChange }, + label = "Low price", + ) { + Text( + text = it, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + } + SpacerW8() + AnimatedContent( + targetState = state.high, + transitionSpec = { TangemAnimations.transitionSpecs.textChange }, + label = "High price", + ) { + Text( + text = it, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.End, + ) + } + } + } +} + +@Composable +internal fun PricePerformanceBlockPlaceholder(modifier: Modifier = Modifier) { + val subtitle2dp = with(LocalDensity.current) { TangemTheme.typography.subtitle2.lineHeight.toDp() } + val caption1dp = with(LocalDensity.current) { TangemTheme.typography.caption1.lineHeight.toDp() } + val headerHeight = maxOf(subtitle2dp, caption1dp) + TangemTheme.dimens.spacing4 + + InformationBlock( + modifier = modifier, + title = { + RectangleShimmer( + modifier = Modifier + .height(headerHeight) + .fillMaxWidth(), + radius = TangemTheme.dimens.radius3, + ) + }, + content = { + Column( + modifier = modifier.padding(vertical = TangemTheme.dimens.spacing8), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + TextShimmer( + modifier = Modifier.width(35.dp), + style = TangemTheme.typography.caption2, + ) + SpacerW8() + TextShimmer( + modifier = Modifier.width(35.dp), + style = TangemTheme.typography.caption2, + ) + } + RectangleShimmer( + modifier = Modifier + .height(TangemTheme.dimens.size6) + .fillMaxWidth(), + radius = 27.dp, + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + TextShimmer( + modifier = Modifier.width(TangemTheme.dimens.size56), + style = TangemTheme.typography.body1, + ) + SpacerW8() + TextShimmer( + modifier = Modifier.width(TangemTheme.dimens.size56), + style = TangemTheme.typography.body1, + ) + } + } + }, + ) +} + +@Preview +@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun ContentPreview() { + TangemThemePreview { + PricePerformanceBlock( + modifier = Modifier, + state = PricePerformanceUM( + h24 = PricePerformanceUM.Value( + low = "\$38,5K", + high = "\$58,5K", + indicatorFraction = 0.5f, + ), + month = PricePerformanceUM.Value( + low = "\$500,5K", + high = "\$5800,5K", + indicatorFraction = 0.8f, + ), + all = PricePerformanceUM.Value( + low = "\$58,52", + high = "\$580,5M", + indicatorFraction = 0.2f, + ), + ), + ) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PlaceholderPreview() { + TangemThemePreview { + PreviewShimmerContainer( + shimmerContent = { + PricePerformanceBlockPlaceholder() + }, + actualContent = { + ContentPreview() + }, + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt new file mode 100644 index 0000000000..319a08de34 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt @@ -0,0 +1,203 @@ +package com.tangem.features.markets.details.impl.ui.components + +import android.content.res.Configuration +import androidx.annotation.FloatRange +import androidx.compose.foundation.layout.* +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.drawWithCache +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.CompositingStrategy +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.block.information.InformationBlock +import com.tangem.core.ui.components.text.TooltipText +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.PreviewShimmerContainer +import com.tangem.features.markets.details.impl.ui.entity.SecurityScoreUM +import com.tangem.features.markets.impl.R +import kotlin.math.round + +private const val STARS_COUNT = 5 + +@Composable +internal fun SecurityScoreBlock(state: SecurityScoreUM, modifier: Modifier = Modifier) { + val rounded = state.score.roundTo1decimal() + val percentage = rounded / STARS_COUNT + InformationBlock( + modifier = modifier, + title = { + Column( + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing6), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + ) { + TooltipText( + text = resourceReference(R.string.markets_token_details_security_score), + onInfoClick = state.onInfoClick, + textStyle = TangemTheme.typography.subtitle2, + ) + + Text( + text = state.description, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + }, + action = { + Row( + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing6), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = rounded.toString(), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + Stars(fraction = percentage) + } + }, + ) +} + +@Suppress("MagicNumber") +@Composable +private fun Stars(@FloatRange(0.0, 1.0) fraction: Float = 0f) { + val grayColor = TangemTheme.colors.icon.inactive + + Row( + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + verticalAlignment = Alignment.CenterVertically, + ) { + repeat(times = 5) { i -> + Box( + modifier = Modifier.size(TangemTheme.dimens.size16), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier + .requiredSize(13.dp) + .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) + .drawWithCache { + onDrawWithContent { + val starFraction = ((fraction - i * 0.2) / 0.2).coerceIn(0.0, 1.0) + val starFractionFloat = starFraction + .toFloat() + .roundTo1decimal() + + drawContent() + drawRect( + color = grayColor, + topLeft = Offset(x = size.width * starFractionFloat, y = 0f), + size = Size(size.width * (1 - starFractionFloat), size.height), + blendMode = BlendMode.SrcIn, + ) + } + }, + imageVector = ImageVector.vectorResource(R.drawable.ic_star_24), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + ) + } + } + } +} + +@Suppress("MagicNumber") +private fun Float.roundTo1decimal(): Float { + return round(this * 10) / 10 +} + +@Composable +internal fun SecurityScorePlaceHolder(modifier: Modifier = Modifier) { + InformationBlock( + modifier = modifier, + title = { + Column( + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing6), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + ) { + TextShimmer( + modifier = Modifier.fillMaxWidth(), + style = TangemTheme.typography.subtitle2, + textSizeHeight = true, + ) + TextShimmer( + modifier = Modifier.fillMaxWidth(), + style = TangemTheme.typography.body2, + textSizeHeight = true, + ) + } + }, + action = { + Box( + modifier = Modifier.padding( + start = TangemTheme.dimens.spacing24, + bottom = TangemTheme.dimens.spacing6, + ), + contentAlignment = Alignment.CenterEnd, + ) { + TextShimmer( + modifier = Modifier.fillMaxWidth(), + style = TangemTheme.typography.body2, + textSizeHeight = true, + ) + RectangleShimmer( + modifier = Modifier + .height(TangemTheme.dimens.size16) + .fillMaxWidth(), + radius = TangemTheme.dimens.radius3, + ) + } + }, + ) +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun ContentPreview() { + TangemThemePreview { + SecurityScoreBlock( + state = SecurityScoreUM( + score = 3.5f, + description = "Based on 3 ratings", + onInfoClick = {}, + ), + ) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewPlaceholder() { + TangemThemePreview { + PreviewShimmerContainer( + shimmerContent = { + SecurityScorePlaceHolder( + modifier = Modifier.fillMaxWidth(), + ) + }, + actualContent = { + ContentPreview() + }, + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/InfoPointUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/InfoPointUM.kt new file mode 100644 index 0000000000..44816c5ab1 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/InfoPointUM.kt @@ -0,0 +1,11 @@ +package com.tangem.features.markets.details.impl.ui.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +internal data class InfoPointUM( + val title: TextReference, + val value: String, + val onInfoClick: (() -> Unit)? = null, +) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/InsightsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/InsightsUM.kt new file mode 100644 index 0000000000..9c240b9214 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/InsightsUM.kt @@ -0,0 +1,11 @@ +package com.tangem.features.markets.details.impl.ui.entity + +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.PersistentList + +@Immutable +internal data class InsightsUM( + val h24Info: PersistentList, + val weekInfo: PersistentList, + val monthInfo: PersistentList, +) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/LinksUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/LinksUM.kt new file mode 100644 index 0000000000..e22571424f --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/LinksUM.kt @@ -0,0 +1,22 @@ +package com.tangem.features.markets.details.impl.ui.entity + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.PersistentList + +@Immutable +internal data class LinksUM( + val officialLinks: PersistentList, + val social: PersistentList, + val repository: PersistentList, + val blockchainSite: PersistentList, + val onLinkClick: (LinkUM) -> Unit, +) { + @Immutable + data class LinkUM( + @DrawableRes val iconRes: Int, + val title: TextReference, + val url: String, + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MarketsTokenDetailsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MarketsTokenDetailsUM.kt index 48941c6461..713d890bfb 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MarketsTokenDetailsUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MarketsTokenDetailsUM.kt @@ -1,5 +1,6 @@ package com.tangem.features.markets.details.impl.ui.entity +import androidx.compose.runtime.Immutable import com.tangem.common.ui.charts.state.MarketChartDataProducer import com.tangem.common.ui.charts.state.MarketChartLook import com.tangem.core.ui.components.marketprice.PriceChangeType @@ -7,7 +8,8 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.markets.PriceChangeInterval import java.math.BigDecimal -data class MarketsTokenDetailsUM( +@Immutable +internal data class MarketsTokenDetailsUM( val tokenName: String, val priceText: String, val iconUrl: String, @@ -17,8 +19,10 @@ data class MarketsTokenDetailsUM( val selectedInterval: PriceChangeInterval, val chartState: ChartState, val onSelectedIntervalChange: (PriceChangeInterval) -> Unit, + // val info : Information TODO [REDACTED_TASK_KEY] ) { + @Immutable data class ChartState( val status: Status, val dataProducer: MarketChartDataProducer, @@ -26,8 +30,18 @@ data class MarketsTokenDetailsUM( val onLoadRetryClick: () -> Unit, val onMarkerPointSelected: (time: BigDecimal?, price: BigDecimal?) -> Unit, ) { + @Immutable enum class Status { LOADING, ERROR, DATA } } + + @Immutable + data class Information( + val insights: InsightsUM?, + val securityScore: SecurityScoreUM?, + val metrics: MetricsUM?, + val pricePerformance: PricePerformanceUM?, + val links: LinksUM?, + ) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MetricsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MetricsUM.kt new file mode 100644 index 0000000000..14f5a9fb86 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MetricsUM.kt @@ -0,0 +1,9 @@ +package com.tangem.features.markets.details.impl.ui.entity + +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.PersistentList + +@Immutable +internal data class MetricsUM( + val metrics: PersistentList, +) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/PricePerformanceUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/PricePerformanceUM.kt new file mode 100644 index 0000000000..194354a363 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/PricePerformanceUM.kt @@ -0,0 +1,18 @@ +package com.tangem.features.markets.details.impl.ui.entity + +import androidx.annotation.FloatRange +import androidx.compose.runtime.Immutable + +@Immutable +internal data class PricePerformanceUM( + val h24: Value, + val month: Value, + val all: Value, +) { + @Immutable + data class Value( + val low: String, + val high: String, + @FloatRange(from = 0.0, to = 1.0) val indicatorFraction: Float, + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/SecurityScoreUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/SecurityScoreUM.kt new file mode 100644 index 0000000000..4afa252dba --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/SecurityScoreUM.kt @@ -0,0 +1,11 @@ +package com.tangem.features.markets.details.impl.ui.entity + +import androidx.annotation.FloatRange +import androidx.compose.runtime.Immutable + +@Immutable +internal data class SecurityScoreUM( + @FloatRange(from = 0.0, to = 5.0) val score: Float, + val description: String, + val onInfoClick: () -> Unit, +) \ No newline at end of file