diff --git a/app-android/build.gradle.kts b/app-android/build.gradle.kts index 5a1414a78..6aa1e80bb 100644 --- a/app-android/build.gradle.kts +++ b/app-android/build.gradle.kts @@ -102,6 +102,7 @@ dependencies { implementation(projects.feature.eventmap) implementation(projects.feature.profilecard) implementation(projects.feature.about) + implementation(projects.feature.staff) implementation(projects.core.model) implementation(projects.core.data) implementation(projects.core.designsystem) diff --git a/app-android/src/main/java/io/github/droidkaigi/confsched/KaigiApp.kt b/app-android/src/main/java/io/github/droidkaigi/confsched/KaigiApp.kt index 92b790dae..dbd401143 100644 --- a/app-android/src/main/java/io/github/droidkaigi/confsched/KaigiApp.kt +++ b/app-android/src/main/java/io/github/droidkaigi/confsched/KaigiApp.kt @@ -53,6 +53,8 @@ import io.github.droidkaigi.confsched.sessions.nestedSessionScreens import io.github.droidkaigi.confsched.sessions.sessionScreens import io.github.droidkaigi.confsched.sessions.timetableScreenRoute import io.github.droidkaigi.confsched.share.ShareNavigator +import io.github.droidkaigi.confsched.staff.staffScreenRoute +import io.github.droidkaigi.confsched.staff.staffScreens import io.github.droidkaigi.confsched.ui.NavHostWithSharedAxisX import io.github.droidkaigi.confshed.profilecard.navigateProfileCardScreen import io.github.droidkaigi.confshed.profilecard.profileCardScreen @@ -103,6 +105,11 @@ private fun KaigiNavHost( onNavigationIconClick = navController::popBackStack, onContributorItemClick = externalNavController::navigate, ) + + staffScreens( + onNavigationIconClick = navController::popBackStack, + onStaffItemClick = externalNavController::navigate, + ) } } @@ -153,7 +160,7 @@ private fun NavGraphBuilder.mainScreen( ) } - AboutItem.Staff -> TODO() + AboutItem.Staff -> navController.navigate(staffScreenRoute) AboutItem.X -> externalNavController.navigate( url = "https://twitter.com/DroidKaigi", ) diff --git a/app-ios-shared/build.gradle.kts b/app-ios-shared/build.gradle.kts index 48e1aac78..3a52b5d48 100644 --- a/app-ios-shared/build.gradle.kts +++ b/app-ios-shared/build.gradle.kts @@ -57,6 +57,7 @@ kotlin { api(projects.feature.contributors) api(projects.feature.profilecard) api(projects.feature.about) + api(projects.feature.staff) implementation(libs.kotlinxCoroutinesCore) implementation(libs.skieAnnotation) } diff --git a/core/data/src/androidMain/kotlin/io/github/droidkaigi/confsched/data/staff/StaffRepositoryModule.kt b/core/data/src/androidMain/kotlin/io/github/droidkaigi/confsched/data/staff/StaffRepositoryModule.kt index 75eec4e57..3b4fd2f83 100644 --- a/core/data/src/androidMain/kotlin/io/github/droidkaigi/confsched/data/staff/StaffRepositoryModule.kt +++ b/core/data/src/androidMain/kotlin/io/github/droidkaigi/confsched/data/staff/StaffRepositoryModule.kt @@ -1,23 +1,34 @@ package io.github.droidkaigi.confsched.data.staff +import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import io.github.droidkaigi.confsched.data.di.RepositoryQualifier import io.github.droidkaigi.confsched.model.StaffRepository import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -public class StaffRepositoryModule { +public abstract class StaffRepositoryModule { + @Binds + @RepositoryQualifier + @IntoMap + @ClassKey(StaffRepository::class) + public abstract fun bind(repository: StaffRepository): Any - @Provides - @Singleton - public fun provideStaffRepository( - staffApi: StaffApiClient, - ): StaffRepository { - return DefaultStaffRepository( - staffApi = staffApi, - ) + public companion object { + @Provides + @Singleton + public fun provideStaffRepository( + staffApi: StaffApiClient, + ): StaffRepository { + return DefaultStaffRepository( + staffApi = staffApi, + ) + } } } diff --git a/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched/data/staff/DefaultStaffRepository.kt b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched/data/staff/DefaultStaffRepository.kt index d140b4418..004920ca4 100644 --- a/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched/data/staff/DefaultStaffRepository.kt +++ b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched/data/staff/DefaultStaffRepository.kt @@ -1,5 +1,9 @@ package io.github.droidkaigi.confsched.data.staff +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import io.github.droidkaigi.confsched.compose.SafeLaunchedEffect +import io.github.droidkaigi.confsched.compose.safeCollectAsRetainedState import io.github.droidkaigi.confsched.model.Staff import io.github.droidkaigi.confsched.model.StaffRepository import kotlinx.collections.immutable.PersistentList @@ -28,4 +32,15 @@ public class DefaultStaffRepository( .getStaff() .toPersistentList() } + + @Composable + override fun staff(): PersistentList { + val staff by staffsStateFlow.safeCollectAsRetainedState() + SafeLaunchedEffect(Unit) { + if (staff.isEmpty()) { + refresh() + } + } + return staff + } } diff --git a/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched/model/StaffRepository.kt b/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched/model/StaffRepository.kt index 0619c1ed8..b088b477c 100644 --- a/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched/model/StaffRepository.kt +++ b/core/model/src/commonMain/kotlin/io/github/droidkaigi/confsched/model/StaffRepository.kt @@ -1,5 +1,7 @@ package io.github.droidkaigi.confsched.model +import androidx.compose.runtime.Composable +import io.github.droidkaigi.confsched.model.compositionlocal.LocalRepositories import kotlinx.collections.immutable.PersistentList import kotlinx.coroutines.flow.Flow import kotlin.coroutines.cancellation.CancellationException @@ -10,4 +12,12 @@ interface StaffRepository { @Throws(CancellationException::class) public suspend fun refresh() + + @Composable + fun staff(): PersistentList +} + +@Composable +fun localStaffRepository(): StaffRepository { + return LocalRepositories.current[StaffRepository::class] as StaffRepository } diff --git a/core/testing/build.gradle.kts b/core/testing/build.gradle.kts index 43a404fc8..03ba886d0 100644 --- a/core/testing/build.gradle.kts +++ b/core/testing/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { implementation(projects.feature.sessions) implementation(projects.feature.profilecard) implementation(projects.feature.about) + implementation(projects.feature.staff) implementation(libs.daggerHiltAndroidTesting) implementation(libs.roborazzi) implementation(libs.kermit) diff --git a/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/MiniRobots.kt b/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/MiniRobots.kt index 079da563b..8e4121922 100644 --- a/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/MiniRobots.kt +++ b/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/MiniRobots.kt @@ -9,6 +9,8 @@ import io.github.droidkaigi.confsched.data.eventmap.EventMapApiClient import io.github.droidkaigi.confsched.data.eventmap.FakeEventMapApiClient import io.github.droidkaigi.confsched.data.sessions.FakeSessionsApiClient import io.github.droidkaigi.confsched.data.sessions.SessionsApiClient +import io.github.droidkaigi.confsched.data.staff.FakeStaffApiClient +import io.github.droidkaigi.confsched.data.staff.StaffApiClient import io.github.droidkaigi.confsched.testing.coroutines.runTestWithLogging import kotlinx.coroutines.test.TestDispatcher import org.robolectric.RuntimeEnvironment @@ -195,3 +197,25 @@ class DefaultEventMapServerRobot @Inject constructor(contributorsApiClient: Even ) } } + +interface StaffServerRobot { + enum class ServerStatus { + Operational, + Error, + } + + fun setupStaffServer(serverStatus: ServerStatus) +} + +class DefaultStaffServerRobot @Inject constructor(staffApiClient: StaffApiClient) : + StaffServerRobot { + private val fakeStaffApiClient = staffApiClient as FakeStaffApiClient + override fun setupStaffServer(serverStatus: StaffServerRobot.ServerStatus) { + fakeStaffApiClient.setup( + when (serverStatus) { + StaffServerRobot.ServerStatus.Operational -> FakeStaffApiClient.Status.Operational + StaffServerRobot.ServerStatus.Error -> FakeStaffApiClient.Status.Error + }, + ) + } +} diff --git a/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/robot/StaffScreenRobot.kt b/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/robot/StaffScreenRobot.kt new file mode 100644 index 000000000..d4aab9173 --- /dev/null +++ b/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/robot/StaffScreenRobot.kt @@ -0,0 +1,27 @@ +package io.github.droidkaigi.confsched.testing.robot + +import io.github.droidkaigi.confsched.designsystem.theme.KaigiTheme +import io.github.droidkaigi.confsched.staff.StaffScreen +import io.github.droidkaigi.confsched.testing.DefaultScreenRobot +import io.github.droidkaigi.confsched.testing.DefaultStaffServerRobot +import io.github.droidkaigi.confsched.testing.ScreenRobot +import io.github.droidkaigi.confsched.testing.StaffServerRobot +import javax.inject.Inject + +class StaffScreenRobot @Inject constructor( + private val screenRobot: DefaultScreenRobot, + private val staffServerRobot: DefaultStaffServerRobot, +) : ScreenRobot by screenRobot, + StaffServerRobot by staffServerRobot { + suspend fun setupScreenContent() { + robotTestRule.setContent { + KaigiTheme { + StaffScreen( + onNavigationIconClick = { }, + onStaffItemClick = { }, + ) + } + } + waitUntilIdle() + } +} diff --git a/feature/staff/.gitignore b/feature/staff/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/staff/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/staff/build.gradle.kts b/feature/staff/build.gradle.kts new file mode 100644 index 000000000..6d20b6a03 --- /dev/null +++ b/feature/staff/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + id("droidkaigi.convention.kmpfeature") +} + +android.namespace = "io.github.droidkaigi.confsched.feature.staff" +roborazzi.generateComposePreviewRobolectricTests.packages = listOf("io.github.droidkaigi.confsched.staff") +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(projects.core.model) + implementation(projects.core.ui) + implementation(libs.kotlinxCoroutinesCore) + implementation(projects.core.designsystem) + implementation(libs.moleculeRuntime) + } + } + androidUnitTest { + dependencies { + implementation(projects.core.testing) + } + } + } +} diff --git a/feature/staff/proguard-rules.pro b/feature/staff/proguard-rules.pro new file mode 100644 index 000000000..ff59496d8 --- /dev/null +++ b/feature/staff/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/staff/src/androidUnitTest/kotlin/io/github/droidkaigi/confsched/staff/StaffScreenTest.kt b/feature/staff/src/androidUnitTest/kotlin/io/github/droidkaigi/confsched/staff/StaffScreenTest.kt new file mode 100644 index 000000000..77b91b7b4 --- /dev/null +++ b/feature/staff/src/androidUnitTest/kotlin/io/github/droidkaigi/confsched/staff/StaffScreenTest.kt @@ -0,0 +1,77 @@ +package io.github.droidkaigi.confsched.staff + +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidTest +import io.github.droidkaigi.confsched.testing.DescribedBehavior +import io.github.droidkaigi.confsched.testing.RobotTestRule +import io.github.droidkaigi.confsched.testing.StaffServerRobot.ServerStatus.Error +import io.github.droidkaigi.confsched.testing.StaffServerRobot.ServerStatus.Operational +import io.github.droidkaigi.confsched.testing.describeBehaviors +import io.github.droidkaigi.confsched.testing.execute +import io.github.droidkaigi.confsched.testing.robot.StaffScreenRobot +import io.github.droidkaigi.confsched.testing.runRobot +import io.github.droidkaigi.confsched.testing.todoChecks +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.ParameterizedRobolectricTestRunner +import javax.inject.Inject + +@RunWith(ParameterizedRobolectricTestRunner::class) +@HiltAndroidTest +class StaffScreenTest( + private val testCase: DescribedBehavior, +) { + @get:Rule + @BindValue val robotTestRule: RobotTestRule = RobotTestRule(testInstance = this) + + @Inject + lateinit var staffScreenRobot: StaffScreenRobot + + @Test + fun runTest() { + runRobot(staffScreenRobot) { + testCase.execute(staffScreenRobot) + } + } + + companion object { + @JvmStatic + @ParameterizedRobolectricTestRunner.Parameters(name = "{0}") + fun behaviors(): List> { + return describeBehaviors(name = "StaffScreen") { + describe("when server is operational") { + run { + setupStaffServer(Operational) + } + describe("when launch") { + run { + setupScreenContent() + } + itShould("show staff screen") { + captureScreenWithChecks( + checks = todoChecks("This screen is still empty now. Please add some checks."), + ) + } + } + } + + describe("when server is down") { + run { + setupStaffServer(Error) + } + describe("when launch") { + run { + setupScreenContent() + } + itShould("show snackbar") { + captureScreenWithChecks( + checks = todoChecks("This screen is still empty now. Please add some checks."), + ) + } + } + } + } + } + } +} diff --git a/feature/staff/src/androidUnitTest/resources/robolectric.properties b/feature/staff/src/androidUnitTest/resources/robolectric.properties new file mode 100644 index 000000000..273245ecb --- /dev/null +++ b/feature/staff/src/androidUnitTest/resources/robolectric.properties @@ -0,0 +1,7 @@ +sdk=34 +# RobolectricDeviceQualifiers.NexusOne +qualifiers=w320dp-h533dp-normal-long-notround-any-hdpi-keyshidden-trackball + +application=dagger.hilt.android.testing.HiltTestApplication +# https://github.com/robolectric/robolectric/issues/6593 +instrumentedPackages=androidx.loader.content diff --git a/feature/staff/src/commonMain/kotlin/io/github/droidkaigi/confsched/staff/StaffScreen.kt b/feature/staff/src/commonMain/kotlin/io/github/droidkaigi/confsched/staff/StaffScreen.kt new file mode 100644 index 000000000..5896ab716 --- /dev/null +++ b/feature/staff/src/commonMain/kotlin/io/github/droidkaigi/confsched/staff/StaffScreen.kt @@ -0,0 +1,173 @@ +package io.github.droidkaigi.confsched.staff + +import androidx.compose.foundation.layout.fillMaxSize +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.material.icons.Icons.AutoMirrored.Filled +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.platform.testTag +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import io.github.droidkaigi.confsched.compose.rememberEventEmitter +import io.github.droidkaigi.confsched.designsystem.theme.KaigiTheme +import io.github.droidkaigi.confsched.model.Staff +import io.github.droidkaigi.confsched.model.fakes +import io.github.droidkaigi.confsched.staff.component.StaffItem +import io.github.droidkaigi.confsched.ui.SnackbarMessageEffect +import io.github.droidkaigi.confsched.ui.UserMessageStateHolder +import io.github.droidkaigi.confsched.ui.UserMessageStateHolderImpl +import io.github.droidkaigi.confsched.ui.handleOnClickIfNotNavigating +import kotlinx.collections.immutable.PersistentList +import org.jetbrains.compose.ui.tooling.preview.Preview + +const val staffScreenRoute = "staff" +const val StaffScreenTestTag = "StaffScreenTestTag" + +fun NavGraphBuilder.staffScreens( + onNavigationIconClick: () -> Unit, + onStaffItemClick: (url: String) -> Unit, +) { + composable(staffScreenRoute) { + val lifecycleOwner = LocalLifecycleOwner.current + + StaffScreen( + onNavigationIconClick = { + handleOnClickIfNotNavigating( + lifecycleOwner, + onNavigationIconClick, + ) + }, + onStaffItemClick = onStaffItemClick, + ) + } +} + +data class StaffUiState( + val staff: PersistentList, + val userMessageStateHolder: UserMessageStateHolder, +) + +@Composable +fun StaffScreen( + onNavigationIconClick: () -> Unit, + onStaffItemClick: (url: String) -> Unit, + modifier: Modifier = Modifier, + isTopAppBarHidden: Boolean = false, +) { + val eventEmitter = rememberEventEmitter() + val uiState = staffScreenPresenter(events = eventEmitter) + + val snackbarHostState = remember { SnackbarHostState() } + + SnackbarMessageEffect( + snackbarHostState = snackbarHostState, + userMessageStateHolder = uiState.userMessageStateHolder, + ) + StaffScreen( + uiState = uiState, + snackbarHostState = snackbarHostState, + onBackClick = onNavigationIconClick, + onStaffItemClick = onStaffItemClick, + modifier = modifier, + isTopAppBarHidden = isTopAppBarHidden, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun StaffScreen( + uiState: StaffUiState, + snackbarHostState: SnackbarHostState, + onBackClick: () -> Unit, + isTopAppBarHidden: Boolean, + modifier: Modifier = Modifier, + onStaffItemClick: (url: String) -> Unit, +) { + val scrollBehavior = + if (!isTopAppBarHidden) { + TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + } else { + null + } + Scaffold( + modifier = modifier.testTag(StaffScreenTestTag), + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + topBar = { + if (scrollBehavior != null) { + LargeTopAppBar( + title = { + Text(text = "Staff") + }, + navigationIcon = { + IconButton( + onClick = onBackClick, + ) { + Icon( + imageVector = Filled.ArrowBack, + contentDescription = "Back", + ) + } + }, + scrollBehavior = scrollBehavior, + ) + } + }, + ) { padding -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .let { + if (scrollBehavior != null) { + it.nestedScroll(scrollBehavior.nestedScrollConnection) + } else { + it + } + }, + ) { + items(uiState.staff) { staff -> + StaffItem( + staff = staff, + onStaffItemClick = onStaffItemClick, + modifier = Modifier.fillMaxWidth(), + ) + } + } + } +} + +@Composable +@Preview +fun StaffScreenPreview() { + KaigiTheme { + Surface { + StaffScreen( + uiState = StaffUiState( + staff = Staff.fakes(), + userMessageStateHolder = UserMessageStateHolderImpl(), + ), + snackbarHostState = SnackbarHostState(), + onStaffItemClick = {}, + onBackClick = {}, + isTopAppBarHidden = false, + ) + } + } +} diff --git a/feature/staff/src/commonMain/kotlin/io/github/droidkaigi/confsched/staff/StaffScreenPresenter.kt b/feature/staff/src/commonMain/kotlin/io/github/droidkaigi/confsched/staff/StaffScreenPresenter.kt new file mode 100644 index 000000000..723356c13 --- /dev/null +++ b/feature/staff/src/commonMain/kotlin/io/github/droidkaigi/confsched/staff/StaffScreenPresenter.kt @@ -0,0 +1,27 @@ +package io.github.droidkaigi.confsched.staff + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import io.github.droidkaigi.confsched.compose.SafeLaunchedEffect +import io.github.droidkaigi.confsched.model.StaffRepository +import io.github.droidkaigi.confsched.model.localStaffRepository +import io.github.droidkaigi.confsched.ui.providePresenterDefaults +import kotlinx.coroutines.flow.Flow + +sealed interface StaffScreenEvent + +@Composable +fun staffScreenPresenter( + events: Flow, + staffRepository: StaffRepository = localStaffRepository(), +): StaffUiState = providePresenterDefaults { userMessageStateHolder -> + val staff by rememberUpdatedState(staffRepository.staff()) + SafeLaunchedEffect(Unit) { + events.collect {} + } + StaffUiState( + staff = staff, + userMessageStateHolder = userMessageStateHolder, + ) +} diff --git a/feature/staff/src/commonMain/kotlin/io/github/droidkaigi/confsched/staff/component/StaffItem.kt b/feature/staff/src/commonMain/kotlin/io/github/droidkaigi/confsched/staff/component/StaffItem.kt new file mode 100644 index 000000000..02e9a444d --- /dev/null +++ b/feature/staff/src/commonMain/kotlin/io/github/droidkaigi/confsched/staff/component/StaffItem.kt @@ -0,0 +1,81 @@ +package io.github.droidkaigi.confsched.staff.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Person +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +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.graphics.vector.rememberVectorPainter +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import io.github.droidkaigi.confsched.designsystem.theme.KaigiTheme +import io.github.droidkaigi.confsched.model.Staff +import io.github.droidkaigi.confsched.model.fakes +import io.github.droidkaigi.confsched.ui.previewOverride +import io.github.droidkaigi.confsched.ui.rememberAsyncImagePainter +import org.jetbrains.compose.ui.tooling.preview.Preview + +private val staffIconShape = CircleShape + +@Composable +fun StaffItem( + staff: Staff, + onStaffItemClick: (url: String) -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .clickable(enabled = staff.profileUrl.isNotEmpty()) { + staff.profileUrl.let(onStaffItemClick) + } + .padding(horizontal = 16.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(23.dp), + ) { + Image( + painter = previewOverride(previewPainter = { rememberVectorPainter(image = Icons.Default.Person) }) { + rememberAsyncImagePainter(staff.iconUrl) + }, + contentDescription = null, + modifier = Modifier + .size(52.dp) + .clip(staffIconShape) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outline, + shape = staffIconShape, + ), + ) + Text( + text = staff.username, + style = MaterialTheme.typography.bodyLarge, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@Composable +@Preview +fun StaffItemPreview() { + KaigiTheme { + Surface { + StaffItem( + staff = Staff.fakes().first(), + onStaffItemClick = {}, + ) + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index f30bebb28..08724d56f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -28,6 +28,7 @@ include( ":feature:eventmap", ":feature:profilecard", ":feature:about", + ":feature:staff", ":core:designsystem", ":core:data", ":core:model",