Skip to content

Commit

Permalink
feat/past conference selection (#1447)
Browse files Browse the repository at this point in the history
* Store past conference attendance

* Add group header for years

* Improve layout accessibility

* Specify constants

---------

Co-authored-by: Ashley Davies <[email protected]>
  • Loading branch information
ashdavies and ashdavies authored Jan 19, 2025
1 parent 0467651 commit b89b41d
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 43 deletions.
2 changes: 2 additions & 0 deletions conferences-app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ kotlin {
implementation(libs.ktor.io)
implementation(libs.slack.circuit.foundation)
implementation(libs.slack.circuit.overlay)
implementation(libs.sqldelight.coroutines.extensions)
implementation(libs.sqldelight.paging3.extensions)
implementation(libs.sqldelight.runtime)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import io.ashdavies.playground.PlaygroundDatabase
import io.ashdavies.sql.LocalTransacter
import io.ktor.client.HttpClient
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import io.ashdavies.party.events.Event as DatabaseEvent

@Composable
Expand Down Expand Up @@ -81,8 +82,11 @@ public fun rememberCircuit(
.addCircuit<PastEventsScreen, PastEventsScreen.State>(
presenterFactory = { _, _, _ ->
presenterOf {
val callable = PastConferencesCallable(LocalHttpClient.current)
PastEventsPresenter(callable)
PastEventsPresenter(
pastConferencesCallable = PastConferencesCallable(LocalHttpClient.current),
attendanceQueries = playgroundDatabase.attendanceQueries,
ioDispatcher = Dispatchers.IO,
)
}
},
uiFactory = { state, modifier ->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,58 @@
package io.ashdavies.party.past

import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import io.ashdavies.aggregator.AsgConference
import app.cash.sqldelight.coroutines.asFlow
import app.cash.sqldelight.coroutines.mapToList
import io.ashdavies.aggregator.callable.PastConferencesCallable
import io.ashdavies.party.events.Event
import io.ashdavies.party.events.AttendanceQueries
import kotlinx.collections.immutable.toImmutableList
import kotlinx.serialization.encodeToString
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDate
import kotlinx.serialization.json.Json
import okio.ByteString.Companion.encode

@Composable
internal fun PastEventsPresenter(
pastConferencesCallable: PastConferencesCallable,
attendanceQueries: AttendanceQueries,
ioDispatcher: CoroutineDispatcher,
): PastEventsScreen.State {
val itemList by produceState(emptyList()) {
value = pastConferencesCallable(Unit).map { it.toEvent() }
val attendanceList by attendanceQueries
.selectAll { id, _ -> id }
.asFlow()
.mapToList(ioDispatcher)
.collectAsState(emptyList())

val itemList by produceState(emptyList(), attendanceList) {
value = pastConferencesCallable(Unit).map {
val startDate = LocalDate.parse(it.dateStart)
val uuid = Json.encodeToString(it)
.encode()
.md5()
.hex()

PastEventsScreen.State.Item(
uuid = uuid,
title = "${it.name} ${startDate.year}",
subtitle = it.location,
group = "${startDate.year}",
attended = uuid in attendanceList,
)
}
}

return PastEventsScreen.State(
itemList = itemList.toImmutableList(),
)
) { event ->
when (event) {
is PastEventsScreen.Event.MarkAttendance -> when (event.value) {
true -> attendanceQueries.insert(event.id, "${Clock.System.now()}")
false -> attendanceQueries.delete(event.id)
}
}
}
}

internal fun AsgConference.toEvent(): Event = Event(
id = hash(), name = name, website = website, location = location, dateStart = dateStart,
dateEnd = dateEnd, imageUrl = imageUrl, status = status, online = online,
cfpStart = cfp?.start, cfpEnd = cfp?.end, cfpSite = cfp?.site,
)

private inline fun <reified T : Any> T.hash() = Json
.encodeToString(this)
.encode().md5().hex()
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package io.ashdavies.party.past

import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.exclude
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.BottomAppBarDefaults
Expand All @@ -28,7 +29,6 @@ import com.slack.circuit.runtime.CircuitUiState
import com.slack.circuit.runtime.screen.Screen
import io.ashdavies.parcelable.Parcelable
import io.ashdavies.parcelable.Parcelize
import io.ashdavies.party.events.Event
import io.ashdavies.party.events.EventsTopBar
import io.ashdavies.party.material.LocalWindowSizeClass
import io.ashdavies.party.material.padding
Expand All @@ -40,19 +40,46 @@ import playground.conferences_app.generated.resources.Res
import playground.conferences_app.generated.resources.past_events

internal object PastEventsDefaults {
const val ASPECT_RATIO = 3 / 1f
const val MIN_COLUMN_COUNT = 2
const val MAX_COLUMN_COUNT = 5
}

@Parcelize
internal object PastEventsScreen : Parcelable, Screen {
data class State(val itemList: ImmutableList<Event>) : CircuitUiState
sealed interface Event {
data class MarkAttendance(
val id: String,
val value: Boolean,
) : Event
}

data class State(
val itemList: ImmutableList<Item>,
val eventSink: (Event) -> Unit,
) : CircuitUiState {

data class Item(
val uuid: String,
val title: String,
val group: String,
val subtitle: String,
val attended: Boolean,
)
}
}

@Composable
internal fun PastEventsScreen(
state: PastEventsScreen.State,
modifier: Modifier = Modifier,
) {
val columnCount = when (LocalWindowSizeClass.current.widthSizeClass) {
WindowWidthSizeClass.Compact -> PastEventsDefaults.MIN_COLUMN_COUNT
else -> PastEventsDefaults.MAX_COLUMN_COUNT
}

val eventSink = state.eventSink

Scaffold(
modifier = modifier,
topBar = { EventsTopBar(stringResource(Res.string.past_events)) },
Expand All @@ -61,52 +88,68 @@ internal fun PastEventsScreen(
),
) { contentPadding ->
LazyVerticalGrid(
columns = GridCells.Fixed(
count = when (LocalWindowSizeClass.current.widthSizeClass) {
WindowWidthSizeClass.Compact -> 3
else -> 5
},
),
columns = GridCells.Fixed(columnCount),
modifier = Modifier.padding(contentPadding),
contentPadding = MaterialTheme.spacing.large.values,
verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.small.vertical),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.small.horizontal),
) {
items(state.itemList) { event ->
EventItemContent(
name = event.name,
modifier = Modifier
.aspectRatio(PastEventsDefaults.ASPECT_RATIO)
.animateItem(),
)
state.itemList.groupBy { it.group }.forEach { (group, items) ->
item(span = { GridItemSpan(columnCount) }) {
Text(
text = group,
modifier = Modifier.padding(MaterialTheme.spacing.medium),
style = MaterialTheme.typography.labelLarge,
)
}

items(items) { item ->
PastEventItem(
item = item,
modifier = Modifier
.clickable { eventSink(PastEventsScreen.Event.MarkAttendance(item.uuid, !item.attended)) }
.animateItem(),
)
}
}
}
}
}

@Composable
private fun EventItemContent(
name: String,
private fun PastEventItem(
item: PastEventsScreen.State.Item,
modifier: Modifier = Modifier,
) {
Surface(
modifier = modifier,
shape = MaterialTheme.shapes.small,
color = Color.Transparent,
color = when (item.attended) {
true -> MaterialTheme.colorScheme.surfaceContainerHighest
false -> Color.Transparent
},
border = BorderStroke(
width = 1.0.dp,
color = MaterialTheme.colorScheme.outline,
),
) {
Column(
modifier = Modifier.fillMaxHeight(),
modifier = Modifier
.padding(MaterialTheme.spacing.medium)
.fillMaxHeight(),
verticalArrangement = Arrangement.Center,
) {
Text(
text = name,
modifier = Modifier
.padding(MaterialTheme.spacing.small)
.fillMaxWidth(),
text = item.title,
modifier = Modifier.fillMaxWidth(),
color = LocalContentColor.current,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.labelMedium,
)

Text(
text = item.subtitle,
modifier = Modifier.fillMaxWidth(),
color = LocalContentColor.current,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.labelSmall,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
CREATE TABLE attendance(
id TEXT NOT NULL UNIQUE PRIMARY KEY,
registeredOn TEXT
);

selectAll:
SELECT *
FROM attendance
ORDER BY registeredOn;

insert:
INSERT INTO attendance VALUES (?, ?);

delete:
DELETE FROM attendance WHERE id = :id;

deleteAll:
DELETE FROM attendance;

0 comments on commit b89b41d

Please sign in to comment.