summaryrefslogtreecommitdiff
path: root/Hutch/Views/Projects/ProjectsListView.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-08-07 04:12:50 -0500
committerChristian Cleberg <[email protected]>2026-08-07 04:12:50 -0500
commit1f8f651a9c2ac2d6be231ff9f71ee3bcce9936d8 (patch)
tree25cf810fa0e9a0ab343b60b59c26d8cc096c9a98 /Hutch/Views/Projects/ProjectsListView.swift
parentdb40d346c28800a1eec384a5e4f3d7d090198e4d (diff)
downloadhutch-1f8f651a9c2ac2d6be231ff9f71ee3bcce9936d8.tar.gz
hutch-1f8f651a9c2ac2d6be231ff9f71ee3bcce9936d8.tar.bz2
hutch-1f8f651a9c2ac2d6be231ff9f71ee3bcce9936d8.zip
Projects: discovery, create, edit, manage linked resources (#12, #13, #14, #15)
hub.sr.ht's GraphQL now exposes the project write API (create/update/ link/unlink) and a public `projects` discovery query, so these previously-blocked issues can be built. ProjectService gains: fetchPublicProjects (#12); createProject (#13); updateProject with ProjectInput (#14); link/unlink for sources, trackers, and mailing lists plus linkable-resource candidate fetches (#15); and hub cache invalidation after every mutation. UI: a Discover browser and a create form reached from the projects list; an Edit form and a Manage Resources sheet on project detail, gated to projects the user owns. Mutations degrade to a visible error if a field isn't live yet. Built against the master schema; live deployment on sr.ht/query is not yet confirmed (introspection there requires a token).
Diffstat (limited to 'Hutch/Views/Projects/ProjectsListView.swift')
-rw-r--r--Hutch/Views/Projects/ProjectsListView.swift58
1 files changed, 56 insertions, 2 deletions
diff --git a/Hutch/Views/Projects/ProjectsListView.swift b/Hutch/Views/Projects/ProjectsListView.swift
index 783417c..1a34346 100644
--- a/Hutch/Views/Projects/ProjectsListView.swift
+++ b/Hutch/Views/Projects/ProjectsListView.swift
@@ -5,15 +5,38 @@ import SwiftUI
final class ProjectsListViewModel {
private(set) var projects: [Project] = []
private(set) var isLoading = false
+ private(set) var isSaving = false
var error: String?
+ var saveError: String?
var searchText = ""
- private let service: ProjectService
+ let service: ProjectService
init(service: ProjectService) {
self.service = service
}
+ func createProject(_ values: ProjectFormValues) async -> Bool {
+ guard !isSaving else { return false }
+ isSaving = true
+ saveError = nil
+ defer { isSaving = false }
+
+ do {
+ _ = try await service.createProject(
+ name: values.name,
+ visibility: values.visibility,
+ description: values.description.isEmpty ? nil : values.description,
+ tags: values.tags
+ )
+ await loadProjects(forceRefresh: true)
+ return true
+ } catch {
+ saveError = error.userFacingMessage
+ return false
+ }
+ }
+
var filteredProjects: [Project] {
let query = searchText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard !query.isEmpty else { return projects }
@@ -46,11 +69,42 @@ final class ProjectsListViewModel {
struct ProjectsListView: View {
@Environment(AppState.self) private var appState
@State private var viewModel: ProjectsListViewModel?
+ @State private var isPresentingCreate = false
var body: some View {
Group {
if let viewModel {
content(viewModel)
+ .toolbar {
+ ToolbarItem(placement: .topBarTrailing) {
+ NavigationLink {
+ DiscoverProjectsView()
+ } label: {
+ Image(systemName: "sparkle.magnifyingglass")
+ }
+ .accessibilityLabel("Discover public projects")
+ }
+ if appState.currentUser != nil {
+ ToolbarItem(placement: .topBarTrailing) {
+ Button {
+ isPresentingCreate = true
+ } label: {
+ Image(systemName: "plus")
+ }
+ .accessibilityLabel("Create project")
+ }
+ }
+ }
+ .sheet(isPresented: $isPresentingCreate) {
+ ProjectFormSheet(
+ title: "New Project",
+ confirmationTitle: "Create",
+ isSaving: viewModel.isSaving,
+ error: viewModel.saveError,
+ includeWebsite: false,
+ onSave: { await viewModel.createProject($0) }
+ )
+ }
} else {
SRHTLoadingStateView(message: "Loading projects…")
}
@@ -70,7 +124,7 @@ struct ProjectsListView: View {
List {
ForEach(viewModel.filteredProjects) { project in
NavigationLink {
- ProjectDetailView(project: project)
+ ProjectDetailView(project: project, canManage: true)
} label: {
ProjectListRow(project: project)
}