gitbay/Views/Discovery/ProfileView.swift

1c5622210d0f72efd86582ce195a6741401e7939
gitbay-ios/gitbay/Views/Discovery/ProfileView.swift history · blame · raw

138 lines · 5499 bytes

  1import SwiftUI
  2
  3/// A user or organization: who they are, who they work with, what they
  4/// have been doing, and what they own that you can see.
  5struct ProfileView: View {
  6
  7    @State private var model: ProfileViewModel
  8
  9    init(client: GitbayClient, name: String) {
 10        _model = State(initialValue: ProfileViewModel(client: client, name: name))
 11    }
 12
 13    var body: some View {
 14        List {
 15            if let profile = model.state.value {
 16                header(profile)
 17                peopleSection(profile)
 18                activitySection(profile)
 19                reposSection(profile)
 20                if profile.isOrg {
 21                    Section {
 22                        NavigationLink(value: OrgRoute.org(profile.name)) {
 23                            Label("Members & Teams", systemImage: "person.3")
 24                        }
 25                    }
 26                }
 27            }
 28        }
 29        .overlay { LoadStateOverlay(state: model.state) }
 30        .navigationTitle(model.name)
 31        .navigationBarTitleDisplayMode(.inline)
 32        .task { await model.load() }
 33        .refreshable { await model.load() }
 34    }
 35
 36    // MARK: - Sections
 37
 38    private func header(_ profile: ProfileViewModel.Profile) -> some View {
 39        Section {
 40            VStack(alignment: .leading, spacing: 8) {
 41                HStack(spacing: 8) {
 42                    Image(systemName: profile.isOrg ? "building.2" : "person.crop.circle")
 43                        .font(.gbSans(.title2))
 44                        .foregroundStyle(Color.gbAccent)
 45                    Text(profile.name)
 46                        .font(.gbSans(.title3).weight(.semibold))
 47                    GBChip(profile.kind, .secondary)
 48                }
 49                if let description = profile.description, !description.isEmpty {
 50                    Text(description)
 51                        .font(.gbSans(.subheadline))
 52                        .foregroundStyle(.secondary)
 53                }
 54                if let website = profile.website, !website.isEmpty,
 55                   let url = URL(string: website) {
 56                    Link(destination: url) {
 57                        Label(website, systemImage: "link")
 58                            .font(.gbSans(.caption))
 59                            .lineLimit(1)
 60                    }
 61                }
 62            }
 63            .padding(.vertical, 4)
 64        }
 65    }
 66
 67    /// A user's organizations, or an organization's members — either way,
 68    /// who they work with, each one openable.
 69    @ViewBuilder
 70    private func peopleSection(_ profile: ProfileViewModel.Profile) -> some View {
 71        let people = profile.isOrg ? (profile.members ?? []) : (profile.orgs ?? [])
 72        if !people.isEmpty {
 73            Section(profile.isOrg ? "Members" : "Organizations") {
 74                ForEach(people) { person in
 75                    NavigationLink(value: RepoRoute.profile(person.name)) {
 76                        HStack {
 77                            Label(person.name, systemImage: profile.isOrg
 78                                  ? "person.crop.circle" : "building.2")
 79                                .font(.gbSans(.subheadline))
 80                            Spacer()
 81                            if let role = person.role, !role.isEmpty {
 82                                GBChip(role, .secondary)
 83                            }
 84                        }
 85                    }
 86                }
 87            }
 88        }
 89    }
 90
 91    @ViewBuilder
 92    private func activitySection(_ profile: ProfileViewModel.Profile) -> some View {
 93        if let activity = profile.activity, !activity.isEmpty {
 94            Section {
 95                ActivityGraph(days: activity)
 96            } header: {
 97                Text("Activity")
 98            } footer: {
 99                Text("\(profile.activityTotal) contributions in the last year.")
100            }
101        }
102    }
103
104    private func reposSection(_ profile: ProfileViewModel.Profile) -> some View {
105        Section("Repositories \(profile.repos.count)") {
106            if profile.repos.isEmpty {
107                Text("No visible repositories.")
108                    .font(.gbSans(.subheadline))
109                    .foregroundStyle(.secondary)
110            } else {
111                ForEach(profile.repos) { repo in
112                    NavigationLink(value: RepoRoute.repo(repo.path)) {
113                        VStack(alignment: .leading, spacing: 3) {
114                            HStack(spacing: 6) {
115                                Text(repo.name)
116                                    .font(.gbSans(.subheadline).weight(.medium))
117                                if repo.isPrivate {
118                                    Image(systemName: "lock.fill")
119                                        .font(.gbSans(.caption2))
120                                        .foregroundStyle(.secondary)
121                                }
122                                if repo.isArchived {
123                                    GBChip("archived", .secondary)
124                                }
125                            }
126                            if let description = repo.description, !description.isEmpty {
127                                Text(description)
128                                    .font(.gbSans(.caption))
129                                    .foregroundStyle(.secondary)
130                                    .lineLimit(2)
131                            }
132                        }
133                    }
134                }
135            }
136        }
137    }
138}