summaryrefslogtreecommitdiff
path: root/Hutch/Views/Projects/ProjectFormSheet.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-08-07 04:19:04 -0500
committerGitHub <[email protected]>2026-08-07 04:19:04 -0500
commit3ae35daa9495b9b1e01f1063bd05c636b97fd927 (patch)
tree25cf810fa0e9a0ab343b60b59c26d8cc096c9a98 /Hutch/Views/Projects/ProjectFormSheet.swift
parentdb40d346c28800a1eec384a5e4f3d7d090198e4d (diff)
parent1f8f651a9c2ac2d6be231ff9f71ee3bcce9936d8 (diff)
downloadhutch-3ae35daa9495b9b1e01f1063bd05c636b97fd927.tar.gz
hutch-3ae35daa9495b9b1e01f1063bd05c636b97fd927.tar.bz2
hutch-3ae35daa9495b9b1e01f1063bd05c636b97fd927.zip
Merge pull request #33 from krazywarez/projects-crud
Projects: discovery, create, edit, manage linked resources (#12, #13, #14, #15)
Diffstat (limited to 'Hutch/Views/Projects/ProjectFormSheet.swift')
-rw-r--r--Hutch/Views/Projects/ProjectFormSheet.swift144
1 files changed, 144 insertions, 0 deletions
diff --git a/Hutch/Views/Projects/ProjectFormSheet.swift b/Hutch/Views/Projects/ProjectFormSheet.swift
new file mode 100644
index 0000000..b05ae66
--- /dev/null
+++ b/Hutch/Views/Projects/ProjectFormSheet.swift
@@ -0,0 +1,144 @@
+import SwiftUI
+
+/// Shared create/edit form for hub.sr.ht projects. The parent owns the save
+/// state and performs the mutation via `onSave`, dismissing on success.
+struct ProjectFormSheet: View {
+ let title: String
+ let confirmationTitle: String
+ let isSaving: Bool
+ let error: String?
+ /// `createProject` takes no website; only the edit flow shows the field.
+ let includeWebsite: Bool
+ let onSave: (ProjectFormValues) async -> Bool
+
+ @Environment(\.dismiss) private var dismiss
+ @State private var name: String
+ @State private var description: String
+ @State private var website: String
+ @State private var tags: String
+ @State private var visibility: Visibility
+
+ init(
+ title: String,
+ confirmationTitle: String,
+ isSaving: Bool,
+ error: String?,
+ includeWebsite: Bool,
+ initialName: String = "",
+ initialDescription: String = "",
+ initialWebsite: String = "",
+ initialTags: [String] = [],
+ initialVisibility: Visibility = .publicVisibility,
+ onSave: @escaping (ProjectFormValues) async -> Bool
+ ) {
+ self.title = title
+ self.confirmationTitle = confirmationTitle
+ self.isSaving = isSaving
+ self.error = error
+ self.includeWebsite = includeWebsite
+ self.onSave = onSave
+ _name = State(initialValue: initialName)
+ _description = State(initialValue: initialDescription)
+ _website = State(initialValue: initialWebsite)
+ _tags = State(initialValue: initialTags.joined(separator: ", "))
+ _visibility = State(initialValue: initialVisibility)
+ }
+
+ private var trimmedName: String {
+ name.trimmingCharacters(in: .whitespacesAndNewlines)
+ }
+
+ var body: some View {
+ NavigationStack {
+ Form {
+ Section("Project Details") {
+ TextField("Project name", text: $name)
+ .textInputAutocapitalization(.never)
+ .autocorrectionDisabled()
+ .themedRow()
+ TextField("Description (optional)", text: $description, axis: .vertical)
+ .lineLimit(2...4)
+ .themedRow()
+ if includeWebsite {
+ TextField("Website (optional)", text: $website)
+ .textInputAutocapitalization(.never)
+ .autocorrectionDisabled()
+ .keyboardType(.URL)
+ .themedRow()
+ }
+ Picker("Visibility", selection: $visibility) {
+ Text("Public").tag(Visibility.publicVisibility)
+ Text("Unlisted").tag(Visibility.unlisted)
+ Text("Private").tag(Visibility.privateVisibility)
+ }
+ .themedRow()
+ }
+
+ Section {
+ TextField("Tags (comma separated)", text: $tags)
+ .textInputAutocapitalization(.never)
+ .autocorrectionDisabled()
+ .themedRow()
+ } footer: {
+ Text("Separate tags with commas.")
+ }
+
+ if let error, !error.isEmpty {
+ Section {
+ Text(error)
+ .foregroundStyle(.red)
+ .themedRow()
+ }
+ }
+ }
+ .themedList()
+ .navigationTitle(title)
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .cancellationAction) {
+ Button("Cancel") { dismiss() }
+ }
+ ToolbarItem(placement: .confirmationAction) {
+ Button {
+ Task {
+ let values = ProjectFormValues(
+ name: trimmedName,
+ description: description.trimmingCharacters(in: .whitespacesAndNewlines),
+ website: website.trimmingCharacters(in: .whitespacesAndNewlines),
+ visibility: visibility,
+ tags: Self.parseTags(tags)
+ )
+ if await onSave(values) {
+ dismiss()
+ }
+ }
+ } label: {
+ if isSaving {
+ ProgressView().controlSize(.small)
+ } else {
+ Text(confirmationTitle)
+ }
+ }
+ .disabled(trimmedName.isEmpty || isSaving)
+ }
+ }
+ }
+ }
+
+ static func parseTags(_ raw: String) -> [String] {
+ var seen = Set<String>()
+ return raw
+ .split(whereSeparator: { $0 == "," || $0.isNewline })
+ .map { $0.trimmingCharacters(in: .whitespaces) }
+ .filter { !$0.isEmpty }
+ .filter { seen.insert($0.lowercased()).inserted }
+ }
+}
+
+struct ProjectFormValues: Sendable {
+ let name: String
+ let description: String
+ let website: String
+ let visibility: Visibility
+ let tags: [String]
+}