From f1d000965981c6aae6d375cd1f61c89d9114c803 Mon Sep 17 00:00:00 2001 From: isfaaghyth Date: Tue, 27 Aug 2024 04:14:13 +0700 Subject: [PATCH] feat: create simple event card for previous event --- .../kotlin/id/gdg/app/AppViewModel.kt | 2 +- .../kotlin/id/gdg/app/ui/AppContent.kt | 6 +- .../id/gdg/app/ui/screen/EventDetailScreen.kt | 7 +- .../kotlin/id/gdg/app/ui/screen/MainScreen.kt | 19 ++- .../ui/screen/content/PreviousEventContent.kt | 29 ++++- .../ui/screen/uimodel/EventContent.mapper.kt | 14 +++ .../id/gdg/app/ui/state/ChapterUiModel.kt | 6 +- gdg-events/build.gradle.kts | 1 + .../id/gdg/event/data/entity/EventDetail.kt | 56 +-------- .../id/gdg/event/domain/mapper/mapper.kt | 22 +++- .../kotlin/id/gdg/event/model/EventModel.kt | 1 + gdg-ui/build.gradle.kts | 2 + .../id/gdg/ui/component/EventSimpleCard.kt | 115 ++++++++++++++++++ gradle/libs.versions.toml | 2 + 14 files changed, 206 insertions(+), 76 deletions(-) create mode 100644 app/src/commonMain/kotlin/id/gdg/app/ui/screen/uimodel/EventContent.mapper.kt create mode 100644 gdg-ui/src/commonMain/kotlin/id/gdg/ui/component/EventSimpleCard.kt diff --git a/app/src/commonMain/kotlin/id/gdg/app/AppViewModel.kt b/app/src/commonMain/kotlin/id/gdg/app/AppViewModel.kt index 1d2ee7b..30b0dd8 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/AppViewModel.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/AppViewModel.kt @@ -52,7 +52,7 @@ class AppViewModel( _upcomingEvent, _previousEvents ) { upcoming, previous -> - ChapterUiModel(upcoming, previous) + ChapterUiModel(upcoming, previous, true) }.stateIn( viewModelScope, SharingStarted.WhileSubscribed(5_000), diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/AppContent.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/AppContent.kt index 9d6f659..82d1e07 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/AppContent.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/AppContent.kt @@ -82,9 +82,9 @@ fun AppContent( private fun NavHostController.navigateTo(from: AppRouter? = null, to: AppRouter) { navigate(to.route) { if (from != null) { - popUpTo(from.route) { - inclusive = true - } +// popUpTo(from.route) { +// inclusive = true +// } } } } diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/EventDetailScreen.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/screen/EventDetailScreen.kt index 9c65bf6..2e8177b 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/EventDetailScreen.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/screen/EventDetailScreen.kt @@ -17,10 +17,9 @@ import id.gdg.app.ui.AppEvent fun EventDetailScreen(viewModel: AppViewModel, eventId: String) { val eventDetailUiState by viewModel.eventDetailUiState.collectAsState() - LaunchedEffect(Unit) { - if (eventId.isNotEmpty()) { - viewModel.sendEvent(AppEvent.EventDetail(eventId.toInt())) - } + LaunchedEffect(eventId) { + if (eventId.isEmpty()) return@LaunchedEffect + viewModel.sendEvent(AppEvent.EventDetail(eventId.toInt())) } Box { diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/MainScreen.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/screen/MainScreen.kt index ec5bc8f..ad5d049 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/MainScreen.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/screen/MainScreen.kt @@ -39,7 +39,7 @@ fun MainScreen( } } - LaunchedEffect(Unit) { + LaunchedEffect(!chapterUiState.isInitiated) { viewModel.sendEvent(AppEvent.InitialContent) } @@ -57,14 +57,14 @@ fun MainScreen( finishedListener = { fraction -> if (fraction == 1f) hasDetailOpened = null } ), body = { - MainScreenView( + MainScreenContent( chapterUiState = chapterUiState, onEventDetailClicked = { // If the screen size is compact (or mobile device screen size), then // navigate to detail page with router. Otherwise, render the [panel]. if (windowSizeClazz.widthSizeClass == CommonWindowWidthSizeClass.Compact) { navigateToDetailScreen(it) - return@MainScreenView + return@MainScreenContent } selectedEventId = it @@ -90,7 +90,7 @@ fun MainScreen( } @Composable -fun MainScreenView( +fun MainScreenContent( chapterUiState: ChapterUiModel, onEventDetailClicked: (String) -> Unit, onRefreshPreviousContentClicked: () -> Unit @@ -98,18 +98,13 @@ fun MainScreenView( Column { UpcomingEventContent(chapterUiState.upcomingEvent) - Button( - onClick = { - onEventDetailClicked("60014") - } - ) { - Text("Show Detail") - } - PreviousEventContent( data = chapterUiState.previousEvents, onRefreshContent = { onRefreshPreviousContentClicked() + }, + onEventClicked = { + onEventDetailClicked(it) } ) } diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/content/PreviousEventContent.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/screen/content/PreviousEventContent.kt index 62f00d7..d0cf320 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/content/PreviousEventContent.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/screen/content/PreviousEventContent.kt @@ -3,16 +3,28 @@ package id.gdg.app.ui.screen.content import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import id.gdg.app.ui.screen.uimodel.toEventContent import id.gdg.app.ui.state.partial.PreviousEventsUiModel +import id.gdg.ui.component.EventContent +import id.gdg.ui.component.EventSimpleCard @Composable fun PreviousEventContent( data: PreviousEventsUiModel, onRefreshContent: () -> Unit, + onEventClicked: (String) -> Unit, ) { Box { AnimatedVisibility(data.state.isLoading) { @@ -21,7 +33,22 @@ fun PreviousEventContent( when { data.state.isSuccess -> { - Text(text = "${data.previousEvents}") + LazyColumn( + modifier = Modifier + .fillMaxWidth(), + ) { + item { + Text( + text = "Previous Events", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + } + + items(data.previousEvents.map { it.toEventContent() }) { + EventSimpleCard(it) { onEventClicked(it) } + } + } } data.state.isFail -> { Row { diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/screen/uimodel/EventContent.mapper.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/screen/uimodel/EventContent.mapper.kt new file mode 100644 index 0000000..8fab9de --- /dev/null +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/screen/uimodel/EventContent.mapper.kt @@ -0,0 +1,14 @@ +package id.gdg.app.ui.screen.uimodel + +import id.gdg.event.model.EventModel +import id.gdg.ui.component.EventContent + +fun EventModel.toEventContent(): EventContent { + return EventContent( + id = "$id", + bannerUrl = eventImageUrl, + eventName = title, + date = startDate, + type = audienceType + ) +} \ No newline at end of file diff --git a/app/src/commonMain/kotlin/id/gdg/app/ui/state/ChapterUiModel.kt b/app/src/commonMain/kotlin/id/gdg/app/ui/state/ChapterUiModel.kt index 2eef368..3b1971e 100644 --- a/app/src/commonMain/kotlin/id/gdg/app/ui/state/ChapterUiModel.kt +++ b/app/src/commonMain/kotlin/id/gdg/app/ui/state/ChapterUiModel.kt @@ -5,13 +5,15 @@ import id.gdg.app.ui.state.partial.UpcomingEventUiModel data class ChapterUiModel( val upcomingEvent: UpcomingEventUiModel, - val previousEvents: PreviousEventsUiModel + val previousEvents: PreviousEventsUiModel, + val isInitiated: Boolean ) { companion object { val Default get() = ChapterUiModel( upcomingEvent = UpcomingEventUiModel.Empty, - previousEvents = PreviousEventsUiModel.Empty + previousEvents = PreviousEventsUiModel.Empty, + isInitiated = false ) } } diff --git a/gdg-events/build.gradle.kts b/gdg-events/build.gradle.kts index e7e6035..e1e62e8 100644 --- a/gdg-events/build.gradle.kts +++ b/gdg-events/build.gradle.kts @@ -30,6 +30,7 @@ kotlin { implementation(project(":gdg-network")) implementation(libs.kotlin.serialization.json) + implementation(libs.kotlin.datetime) implementation(libs.common.koin) } diff --git a/gdg-events/src/commonMain/kotlin/id/gdg/event/data/entity/EventDetail.kt b/gdg-events/src/commonMain/kotlin/id/gdg/event/data/entity/EventDetail.kt index 79130ac..f127040 100644 --- a/gdg-events/src/commonMain/kotlin/id/gdg/event/data/entity/EventDetail.kt +++ b/gdg-events/src/commonMain/kotlin/id/gdg/event/data/entity/EventDetail.kt @@ -15,18 +15,12 @@ data class EventDetail( @SerialName("allows_cohosting") val allowsCohosting: Boolean, - @SerialName("attendee_virtual_venue_link") - val attendeeVirtualVenueLink: String, - @SerialName("audience_type") val audienceType: String, @SerialName("banner") val banner: Banner, - @SerialName("banner_crop_vertical") - val bannerCropVertical: Int, - @SerialName("chapter") val chapter: Chapter, @@ -205,23 +199,20 @@ data class EventDetail( val useFeaturedAttendees: Boolean, @SerialName("venue_address") - val venueAddress: String, + val venueAddress: String? = "", @SerialName("venue_city") - val venueCity: String, + val venueCity: String? = "", @SerialName("venue_name") - val venueName: String, + val venueName: String? = "", @SerialName("venue_zip_code") - val venueZipCode: String, + val venueZipCode: String? = "", @SerialName("video_url") val videoUrl: String?, - @SerialName("virtual_event_type") - val virtualEventType: String, - @SerialName("visible_on_parent_chapter_only") val visibleOnParentChapterOnly: Boolean ) { @@ -457,45 +448,6 @@ data class EventDetail( @SerialName("id") val id: Int, - @SerialName("is_for_sale") - val isForSale: Boolean, - - @SerialName("max_per_order") - val maxPerOrder: Int, - - @SerialName("min_per_order") - val minPerOrder: Int, - - @SerialName("price") - val price: Int, - - @SerialName("reported_fees") - val reportedFees: Int, - - @SerialName("reported_original_price") - val reportedOriginalPrice: Int, - - @SerialName("reported_price") - val reportedPrice: Int, - - @SerialName("sale_end_date") - val saleEndDate: String, - - @SerialName("sale_end_date_derived_from_event_end") - val saleEndDateDerivedFromEventEnd: Boolean, - - @SerialName("sale_end_date_naive") - val saleEndDateNaive: String, - - @SerialName("sale_start_date") - val saleStartDate: String, - - @SerialName("sale_start_date_derived_from_event_publish") - val saleStartDateDerivedFromEventPublish: Boolean, - - @SerialName("sale_start_date_naive") - val saleStartDateNaive: String, - @SerialName("title") val title: String, diff --git a/gdg-events/src/commonMain/kotlin/id/gdg/event/domain/mapper/mapper.kt b/gdg-events/src/commonMain/kotlin/id/gdg/event/domain/mapper/mapper.kt index 1ebe6d8..c31c4db 100644 --- a/gdg-events/src/commonMain/kotlin/id/gdg/event/domain/mapper/mapper.kt +++ b/gdg-events/src/commonMain/kotlin/id/gdg/event/domain/mapper/mapper.kt @@ -4,6 +4,8 @@ import id.gdg.event.data.entity.EventDetail import id.gdg.event.data.entity.Events.Event import id.gdg.event.model.EventDetailModel import id.gdg.event.model.EventModel +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant fun List.toEventModels(): List { return map { it.toEventModel() } @@ -16,11 +18,29 @@ fun Event.toEventModel(): EventModel { chapterTitle = chapterTitle, descriptionShort = descriptionShort, eventImageUrl = picture, - startDate = startDate, + startDate = formatDate(startDate), timezoneAbbreviation = timezoneAbbreviation, + audienceType = audienceType.lowercase() ) } fun EventDetail.toEventDetailModel(): EventDetailModel { return EventDetailModel(title) +} + +internal fun formatDate(dateString: String): String { + val date = Instant.parse(dateString) + val now = Clock.System.now() + + val duration = now - date + val days = duration.inWholeDays + val months = (days / 30).toInt() // Approximate months + val years = (days / 365).toInt() // Approximate years + + return when { + years > 0 -> "$years year${if (years > 1) "s" else ""} ago" + months > 0 -> "$months month${if (months > 1) "s" else ""} ago" + days > 0 -> "$days day${if (days > 1) "s" else ""} ago" + else -> "today" + } } \ No newline at end of file diff --git a/gdg-events/src/commonMain/kotlin/id/gdg/event/model/EventModel.kt b/gdg-events/src/commonMain/kotlin/id/gdg/event/model/EventModel.kt index 8af7478..722b1e7 100644 --- a/gdg-events/src/commonMain/kotlin/id/gdg/event/model/EventModel.kt +++ b/gdg-events/src/commonMain/kotlin/id/gdg/event/model/EventModel.kt @@ -8,4 +8,5 @@ data class EventModel( val eventImageUrl: String, val startDate: String, val timezoneAbbreviation: String, + val audienceType: String ) \ No newline at end of file diff --git a/gdg-ui/build.gradle.kts b/gdg-ui/build.gradle.kts index b580d12..d214b68 100644 --- a/gdg-ui/build.gradle.kts +++ b/gdg-ui/build.gradle.kts @@ -28,6 +28,8 @@ kotlin { implementation(libs.androidx.compose.windowsizeclass) } commonMain.dependencies { + api(libs.util.qdsfdhvh.image.loader) + implementation(compose.runtime) implementation(compose.foundation) implementation(compose.ui) diff --git a/gdg-ui/src/commonMain/kotlin/id/gdg/ui/component/EventSimpleCard.kt b/gdg-ui/src/commonMain/kotlin/id/gdg/ui/component/EventSimpleCard.kt new file mode 100644 index 0000000..972b223 --- /dev/null +++ b/gdg-ui/src/commonMain/kotlin/id/gdg/ui/component/EventSimpleCard.kt @@ -0,0 +1,115 @@ +package id.gdg.ui.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.seiko.imageloader.model.ImageAction +import com.seiko.imageloader.rememberImageSuccessPainter +import com.seiko.imageloader.ui.AutoSizeBox + +data class EventContent( + val id: String, + val bannerUrl: String, + val eventName: String, + val date: String, + val type: String +) + +@Composable +fun EventSimpleCard( + content: EventContent, + navigateToEvent: (String) -> Unit +) { + Row( + modifier = Modifier + .clickable(onClick = { navigateToEvent(content.id) }) + .padding(4.dp) + ) { + EventBannerImage( + url = content.bannerUrl, + modifier = Modifier + .padding(16.dp) + ) + Column( + modifier = Modifier + .weight(1f) + .padding(vertical = 10.dp) + ) { + EventTitle(content.eventName) + DateAndAudienceType(content.date, content.type) + } + } +} + +@Composable +fun EventTitle(title: String) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) +} + +@Composable +fun DateAndAudienceType( + date: String, + type: String, + modifier: Modifier = Modifier +) { + Row(modifier) { + Text( + text = "$date - $type", + style = MaterialTheme.typography.bodyMedium + ) + } +} + +@Composable +private fun EventBannerImage(url: String?, modifier: Modifier = Modifier) { + if (url.isNullOrEmpty()) return + + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { + AutoSizeBox(url = url) { action -> + when (action) { + is ImageAction.Success -> { + Image( + modifier = Modifier + .size(48.dp, 48.dp) + .clip(RoundedCornerShape(8.dp)), + contentScale = ContentScale.Crop, + painter = rememberImageSuccessPainter(action), + contentDescription = null, + ) + } + is ImageAction.Loading -> { + CircularProgressIndicator( + modifier = Modifier + .align(Alignment.Center) + .size(48.dp), + color = MaterialTheme.colorScheme.tertiary, + ) + } + is ImageAction.Failure -> {} // TODO: Placeholder + } + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index aa6f67b..5a5f484 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,6 +32,7 @@ ktorfit = "2.0.0-beta1" test-coroutines = "1.6.4" test-mockk = "1.13.12" test-turbine = "0.12.1" +qdsfdhvh-image-loader = "1.7.1" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } @@ -76,6 +77,7 @@ test-mockk = { module = "io.mockk:mockk", version.ref = "test-mockk" } test-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "test-coroutines" } util-constraintlayout = { module = "tech.annexflow.compose:constraintlayout-compose-multiplatform", version.ref = "constraint-layout" } +util-qdsfdhvh-image-loader = { module = "io.github.qdsfdhvh:image-loader", version.ref = "qdsfdhvh-image-loader" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" }