a native ios client for gitbay

client ios swift

https://gitbay.org

gitbay/Views/Discovery/ProfileView.swift

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