import SwiftUI /// What needs me, in one read. struct DashboardView: View { @State private var model: DashboardViewModel init(client: GitbayClient) { _model = State(initialValue: DashboardViewModel(client: client)) } var body: some View { List { if let data = model.state.value { if !data.pinned.isEmpty { pinnedSection(data.pinned) } itemSection( "Needs review", items: data.openMRs, empty: "No open merge requests.", marker: "!" ) { MRRoute.mr(repo: $0.repo, number: $0.number) } itemSection( "Assigned to me", items: data.assignedIssues, empty: "No assigned issues.", marker: "#" ) { IssueRoute.issue(repo: $0.repo, number: $0.number) } buildsSection(data.builds) } } .overlay { LoadStateOverlay(state: model.state) } .navigationTitle("Dashboard") .toolbar { ToolbarItem(placement: .topBarTrailing) { NavigationLink(value: RepoRoute.account) { Image(systemName: "key") } .accessibilityIdentifier("account-screen-link") } AccountMenu() } .task { await model.load() } .refreshable { await model.load() } } private func pinnedSection(_ pinned: [RepoSummary]) -> some View { Section("Pinned") { ForEach(pinned) { repo in NavigationLink(value: RepoRoute.repo(repo.path)) { VStack(alignment: .leading, spacing: 2) { HStack(spacing: 6) { Text(repo.path) .font(.subheadline.weight(.medium)) .lineLimit(1) if repo.isPrivate { Image(systemName: "lock.fill") .font(.caption2) .foregroundStyle(.secondary) } } if let description = repo.description, !description.isEmpty { Text(description) .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) } } } } } } private func itemSection( _ title: String, items: [DashboardItem], empty: String, marker: String, route: @escaping (DashboardItem) -> some Hashable ) -> some View { Section(title) { if items.isEmpty { Text(empty) .font(.subheadline) .foregroundStyle(.secondary) } else { ForEach(items) { item in NavigationLink(value: route(item)) { VStack(alignment: .leading, spacing: 2) { Text(item.repo) .font(.caption) .foregroundStyle(.secondary) HStack(alignment: .firstTextBaseline, spacing: 6) { Text(marker + String(item.number)) .font(.caption.monospaced()) .foregroundStyle(.secondary) Text(item.title) .font(.subheadline.weight(.medium)) .lineLimit(2) } HStack(spacing: 6) { Text("by \(item.author)") Text(item.updatedAt, format: .relative(presentation: .named)) .foregroundStyle(.tertiary) } .font(.caption) .foregroundStyle(.secondary) } } } } } } private func buildsSection(_ builds: [DashboardBuild]) -> some View { Section("Recent builds") { if builds.isEmpty { Text("No recent builds.") .font(.subheadline) .foregroundStyle(.secondary) } else { ForEach(builds) { build in NavigationLink(value: BuildRoute.log(repo: build.repo, number: build.number)) { HStack(spacing: 8) { Image(systemName: buildIcon(build.status)) .foregroundStyle(buildColor(build.status)) VStack(alignment: .leading, spacing: 2) { Text(build.repo) .font(.caption) .foregroundStyle(.secondary) Text("#\(build.number) \(build.job)") .font(.subheadline) } Spacer() Text(build.createdAt, format: .relative(presentation: .named)) .font(.caption) .foregroundStyle(.tertiary) } } } } } } private func buildIcon(_ status: String) -> String { switch status { case "success": "checkmark.circle.fill" case "failure", "error": "xmark.circle.fill" case "running": "circle.dotted" case "queued", "pending": "clock" default: "questionmark.circle" } } private func buildColor(_ status: String) -> Color { switch status { case "success": .green case "failure", "error": .red case "running", "queued", "pending": .orange default: .secondary } } }