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