summaryrefslogtreecommitdiff
path: root/Hutch/Views/Projects/ProjectFormSheet.swift
blob: b05ae6606fc7ce71c59c1f7eaedce44f52718efb (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
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]
}