import SwiftUI /// A user or organization: who they are, who they work with, what they /// have been doing, and what they own that you can see. struct ProfileView: View { @State private var model: ProfileViewModel /// Someone else's profile is titled with their name. Your own is /// reached from a tab and titled to match it. private let title: String? init(client: GitbayClient, name: String, title: String? = nil) { _model = State(initialValue: ProfileViewModel(client: client, name: name)) self.title = title } var body: some View { List { if let profile = model.state.value { header(profile) peopleSection(profile) activitySection(profile) reposSection(profile) if profile.isOrg { Section { NavigationLink(value: OrgRoute.org(profile.name)) { Label("Members & Teams", systemImage: "person.3") } } } } } .overlay { LoadStateOverlay(state: model.state) } .navigationTitle(title ?? model.name) .navigationBarTitleDisplayMode(.inline) .task { await model.load() } .refreshable { await model.load() } } // MARK: - Sections private func header(_ profile: ProfileViewModel.Profile) -> some View { Section { VStack(alignment: .leading, spacing: 8) { HStack(spacing: 8) { Image(systemName: profile.isOrg ? "building.2" : "person.crop.circle") .font(.gbSans(.title2)) .foregroundStyle(Color.gbAccent) Text(profile.name) .font(.gbSans(.title3).weight(.semibold)) GBChip(profile.kind, .secondary) } if let description = profile.description, !description.isEmpty { Text(description) .font(.gbSans(.subheadline)) .foregroundStyle(.secondary) } if let website = profile.website, !website.isEmpty, let url = URL(string: website) { Link(destination: url) { Label(website, systemImage: "link") .font(.gbSans(.caption)) .lineLimit(1) } } } .padding(.vertical, 4) } } /// A user's organizations, or an organization's members — either way, /// who they work with, each one openable. @ViewBuilder private func peopleSection(_ profile: ProfileViewModel.Profile) -> some View { let people = profile.isOrg ? (profile.members ?? []) : (profile.orgs ?? []) if !people.isEmpty { Section(profile.isOrg ? "Members" : "Organizations") { ForEach(people) { person in NavigationLink(value: RepoRoute.profile(person.name)) { HStack { Label(person.name, systemImage: profile.isOrg ? "person.crop.circle" : "building.2") .font(.gbSans(.subheadline)) Spacer() if let role = person.role, !role.isEmpty { GBChip(role, .secondary) } } } } } } } @ViewBuilder private func activitySection(_ profile: ProfileViewModel.Profile) -> some View { if let activity = profile.activity, !activity.isEmpty { Section { ActivityGraph(days: activity) } header: { Text("Activity") } footer: { Text("\(profile.activityTotal) contributions in the last year.") } } } private func reposSection(_ profile: ProfileViewModel.Profile) -> some View { Section("Repositories \(profile.repos.count)") { if profile.repos.isEmpty { Text("No visible repositories.") .font(.gbSans(.subheadline)) .foregroundStyle(.secondary) } else { ForEach(profile.repos) { repo in NavigationLink(value: RepoRoute.repo(repo.path)) { VStack(alignment: .leading, spacing: 3) { HStack(spacing: 6) { Text(repo.name) .font(.gbSans(.subheadline).weight(.medium)) if repo.isPrivate { Image(systemName: "lock.fill") .font(.gbSans(.caption2)) .foregroundStyle(.secondary) } if repo.isArchived { GBChip("archived", .secondary) } } if let description = repo.description, !description.isEmpty { Text(description) .font(.gbSans(.caption)) .foregroundStyle(.secondary) .lineLimit(2) } } } } } } } }