From 10400285fe2ffefaf27bf27b31aca03da1444b90 Mon Sep 17 00:00:00 2001 From: TheRealAshik <177647015+TheRealAshik@users.noreply.github.com> Date: Sat, 16 May 2026 08:10:15 +0000 Subject: [PATCH] feat: Add OrganizationsScreen to profile - Create `OrganizationsScreen` with Material 3 styling - Create `OrganizationsViewModel` and `OrganizationsUiState` to manage screen state - Update `ProfileScreen` to navigate to `OrganizationsScreen` - Update `App.kt` and `NavRoutes.kt` with the new route - Update `ProfileUiState` and `ProfileViewModel` to include organization description - Add necessary string resources --- .../composeResources/values/strings.xml | 1 + .../kotlin/dev/therealashik/github/App.kt | 11 +- .../dev/therealashik/github/NavRoutes.kt | 1 + .../github/profile/OrganizationsScreen.kt | 155 ++++++++++++++++++ .../github/profile/OrganizationsUiState.kt | 7 + .../github/profile/OrganizationsViewModel.kt | 47 ++++++ .../github/profile/ProfileScreen.kt | 11 +- .../github/profile/ProfileUiState.kt | 3 +- .../github/profile/ProfileViewModel.kt | 2 +- 9 files changed, 231 insertions(+), 7 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/OrganizationsScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/OrganizationsUiState.kt create mode 100644 composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/OrganizationsViewModel.kt diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index d9b77f4..d308eac 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -166,6 +166,7 @@ Repositories Organizations + No organizations found Starred Projects diff --git a/composeApp/src/commonMain/kotlin/dev/therealashik/github/App.kt b/composeApp/src/commonMain/kotlin/dev/therealashik/github/App.kt index feab061..6fdeae7 100755 --- a/composeApp/src/commonMain/kotlin/dev/therealashik/github/App.kt +++ b/composeApp/src/commonMain/kotlin/dev/therealashik/github/App.kt @@ -10,6 +10,8 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import dev.therealashik.github.profile.ProfileScreen import dev.therealashik.github.profile.ProfileViewModel +import dev.therealashik.github.profile.OrganizationsScreen +import dev.therealashik.github.profile.OrganizationsViewModel import dev.therealashik.github.repository.RepositoryListScreen import dev.therealashik.github.repository.RepositoryListViewModel import dev.therealashik.github.settings.AddPatScreen @@ -45,7 +47,14 @@ fun App() { viewModel = ProfileViewModel(), onBack = { navController.popBackStack() }, onNavigateToRepositories = { navController.navigate(Route.Repositories) }, - onNavigateToSettings = { navController.navigate(Route.Settings) } + onNavigateToSettings = { navController.navigate(Route.Settings) }, + onNavigateToOrganizations = { navController.navigate(Route.Organizations) } + ) + } + composable { + OrganizationsScreen( + viewModel = OrganizationsViewModel(), + onBack = { navController.popBackStack() } ) } composable { diff --git a/composeApp/src/commonMain/kotlin/dev/therealashik/github/NavRoutes.kt b/composeApp/src/commonMain/kotlin/dev/therealashik/github/NavRoutes.kt index caa8988..7d6b2f0 100644 --- a/composeApp/src/commonMain/kotlin/dev/therealashik/github/NavRoutes.kt +++ b/composeApp/src/commonMain/kotlin/dev/therealashik/github/NavRoutes.kt @@ -11,4 +11,5 @@ sealed interface Route { @Serializable data object NotificationOptions : Route @Serializable data object CodeOptions : Route @Serializable data object AddPat : Route + @Serializable data object Organizations : Route } diff --git a/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/OrganizationsScreen.kt b/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/OrganizationsScreen.kt new file mode 100644 index 0000000..7cdbee4 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/OrganizationsScreen.kt @@ -0,0 +1,155 @@ +package dev.therealashik.github.profile + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import coil3.compose.AsyncImage +import dev.therealashik.github.Dimens +import github.composeapp.generated.resources.Res +import github.composeapp.generated.resources.content_description_back +import github.composeapp.generated.resources.nav_organizations +import github.composeapp.generated.resources.orgs_empty_state +import github.composeapp.generated.resources.retry +import org.jetbrains.compose.resources.stringResource + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun OrganizationsScreen( + viewModel: OrganizationsViewModel, + onBack: () -> Unit +) { + val uiState by viewModel.uiState.collectAsState() + + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(Res.string.nav_organizations)) }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(Res.string.content_description_back) + ) + } + } + ) + } + ) { innerPadding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + ) { + when (val state = uiState) { + is OrganizationsUiState.Loading -> { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } + is OrganizationsUiState.Error -> { + Column( + modifier = Modifier.align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = state.message, + color = MaterialTheme.colorScheme.error + ) + Spacer(modifier = Modifier.height(Dimens.SpacingMedium)) + Button(onClick = { viewModel.loadData() }) { + Text(stringResource(Res.string.retry)) + } + } + } + is OrganizationsUiState.Success -> { + if (state.orgs.isEmpty()) { + Text( + text = stringResource(Res.string.orgs_empty_state), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.align(Alignment.Center) + ) + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = androidx.compose.foundation.layout.PaddingValues(Dimens.SpacingMedium), + verticalArrangement = Arrangement.spacedBy(Dimens.SpacingSmall) + ) { + items(state.orgs) { org -> + OrgItem(org = org) + } + } + } + } + } + } + } +} + +@Composable +private fun OrgItem(org: OrgSummary) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(Dimens.SpacingMedium), + verticalAlignment = Alignment.CenterVertically + ) { + AsyncImage( + model = org.avatarUrl, + contentDescription = null, + modifier = Modifier + .size(Dimens.IconSizeHuge) + .clip(CircleShape) + ) + Spacer(modifier = Modifier.width(Dimens.SpacingMedium)) + Column { + Text( + text = org.login, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + if (!org.description.isNullOrEmpty()) { + Spacer(modifier = Modifier.height(Dimens.SpacingExtraSmall)) + Text( + text = org.description, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/OrganizationsUiState.kt b/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/OrganizationsUiState.kt new file mode 100644 index 0000000..354162d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/OrganizationsUiState.kt @@ -0,0 +1,7 @@ +package dev.therealashik.github.profile + +sealed class OrganizationsUiState { + data object Loading : OrganizationsUiState() + data class Error(val message: String) : OrganizationsUiState() + data class Success(val orgs: List) : OrganizationsUiState() +} diff --git a/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/OrganizationsViewModel.kt b/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/OrganizationsViewModel.kt new file mode 100644 index 0000000..ffe1779 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/OrganizationsViewModel.kt @@ -0,0 +1,47 @@ +package dev.therealashik.github.profile + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dev.therealashik.github.data.GitHubApiClient +import dev.therealashik.github.data.createTokenStorage +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +class OrganizationsViewModel : ViewModel() { + private val apiClient = GitHubApiClient(createTokenStorage()) + private val _uiState = MutableStateFlow(OrganizationsUiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + loadData() + } + + fun loadData() { + viewModelScope.launch { + _uiState.value = OrganizationsUiState.Loading + apiClient.getUserOrgs() + .onSuccess { orgs -> + _uiState.value = OrganizationsUiState.Success( + orgs.map { o -> + OrgSummary( + id = o.id, + login = o.login, + avatarUrl = o.avatarUrl, + description = o.description + ) + } + ) + } + .onFailure { error -> + _uiState.value = OrganizationsUiState.Error(error.message ?: "Unknown error") + } + } + } + + override fun onCleared() { + super.onCleared() + apiClient.close() + } +} diff --git a/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/ProfileScreen.kt b/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/ProfileScreen.kt index 9d053c2..2488b23 100644 --- a/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/ProfileScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/ProfileScreen.kt @@ -34,7 +34,8 @@ fun ProfileScreen( viewModel: ProfileViewModel, onBack: () -> Unit, onNavigateToRepositories: () -> Unit, - onNavigateToSettings: () -> Unit = {} + onNavigateToSettings: () -> Unit, + onNavigateToOrganizations: () -> Unit ) { val uiState by viewModel.uiState.collectAsState() @@ -105,7 +106,8 @@ fun ProfileScreen( HorizontalDivider(color = MaterialTheme.colorScheme.surfaceVariant) NavigationListSection( state = state, - onNavigateToRepositories = onNavigateToRepositories + onNavigateToRepositories = onNavigateToRepositories, + onNavigateToOrganizations = onNavigateToOrganizations ) } } @@ -349,7 +351,8 @@ private fun PopularReposSection(popularRepos: List) { @Composable private fun NavigationListSection( state: ProfileUiState.Success, - onNavigateToRepositories: () -> Unit + onNavigateToRepositories: () -> Unit, + onNavigateToOrganizations: () -> Unit ) { Column { NavigationItem( @@ -390,7 +393,7 @@ private fun NavigationListSection( }, label = stringResource(Res.string.nav_organizations), count = state.orgs.size, - onClick = { /* TODO */ } + onClick = onNavigateToOrganizations ) NavigationItem( icon = { diff --git a/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/ProfileUiState.kt b/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/ProfileUiState.kt index 22d1fed..fdf3088 100644 --- a/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/ProfileUiState.kt +++ b/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/ProfileUiState.kt @@ -34,5 +34,6 @@ data class PopularRepo( data class OrgSummary( val id: Long, val login: String, - val avatarUrl: String + val avatarUrl: String, + val description: String? = null ) diff --git a/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/ProfileViewModel.kt b/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/ProfileViewModel.kt index b58b3fc..4ecbef4 100644 --- a/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/ProfileViewModel.kt +++ b/composeApp/src/commonMain/kotlin/dev/therealashik/github/profile/ProfileViewModel.kt @@ -63,7 +63,7 @@ class ProfileViewModel : ViewModel() { language = repo.language ) }, - orgs = orgs.map { OrgSummary(it.id, it.login, it.avatarUrl) }, + orgs = orgs.map { OrgSummary(it.id, it.login, it.avatarUrl, it.description) }, starredCount = starred.size, statusEmoji = status?.emoji, statusMessage = status?.message