From 4145f73bc99cc598d56925794ccc78c4c2b049d6 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 9 Jan 2026 11:44:07 -0600 Subject: [PATCH] Signed-off-by: Matt Bruce --- BusinessCard/Resources/Localizable.xcstrings | 12 +- BusinessCard/Views/CardEditorView.swift | 133 ++++++++++++++++++- 2 files changed, 139 insertions(+), 6 deletions(-) diff --git a/BusinessCard/Resources/Localizable.xcstrings b/BusinessCard/Resources/Localizable.xcstrings index 2ba5682..0546f79 100644 --- a/BusinessCard/Resources/Localizable.xcstrings +++ b/BusinessCard/Resources/Localizable.xcstrings @@ -81,6 +81,9 @@ } } } + }, + "Add Contact Fields" : { + }, "Add note" : { @@ -176,9 +179,6 @@ }, "Contact" : { - }, - "Contact Fields" : { - }, "Country" : { @@ -249,6 +249,9 @@ }, "Developer" : { + }, + "Drag to reorder. Swipe to delete." : { + }, "e.g. MBA, CPA, PhD" : { @@ -733,6 +736,9 @@ }, "Write down a memorable reminder about your contact" : { + }, + "Your Contact Fields" : { + }, "ZIP Code" : { diff --git a/BusinessCard/Views/CardEditorView.swift b/BusinessCard/Views/CardEditorView.swift index cde71ac..c1ee498 100644 --- a/BusinessCard/Views/CardEditorView.swift +++ b/BusinessCard/Views/CardEditorView.swift @@ -50,6 +50,10 @@ struct CardEditorView: View { // Photo editor state - just one variable! @State private var editingImageType: ImageType? + // Contact field editor state + @State private var selectedFieldTypeForAdd: ContactFieldType? + @State private var fieldToEdit: AddedContactField? + @State private var showingPreview = false enum ImageType: String, Identifiable { @@ -196,11 +200,34 @@ struct CardEditorView: View { Text("Card Label") } - // Contact & social fields manager + // Current contact fields (reorderable list) + if !contactFields.isEmpty { + Section { + ForEach(contactFields) { field in + ContactFieldRowView(field: field) { + fieldToEdit = field + } + } + .onMove { from, to in + contactFields.move(fromOffsets: from, toOffset: to) + } + .onDelete { indexSet in + contactFields.remove(atOffsets: indexSet) + } + } header: { + Text("Your Contact Fields") + } footer: { + Text("Drag to reorder. Swipe to delete.") + } + } + + // Add new contact fields Section { - ContactFieldsManagerView(fields: $contactFields) + ContactFieldPickerView { fieldType in + selectedFieldTypeForAdd = fieldType + } } header: { - Text("Contact Fields") + Text("Add Contact Fields") } // Bio section @@ -244,6 +271,29 @@ struct CardEditorView: View { editingImageType = nil } } + .sheet(item: $selectedFieldTypeForAdd) { fieldType in + ContactFieldEditorSheet( + fieldType: fieldType, + initialValue: "", + initialTitle: fieldType.titleSuggestions.first ?? "", + onSave: { value, title in + addContactField(fieldType: fieldType, value: value, title: title) + } + ) + } + .sheet(item: $fieldToEdit) { field in + ContactFieldEditorSheet( + fieldType: field.fieldType, + initialValue: field.value, + initialTitle: field.title, + onSave: { value, title in + updateContactField(id: field.id, value: value, title: title) + }, + onDelete: { + deleteContactField(id: field.id) + } + ) + } .onAppear { loadCardData() } .sheet(isPresented: $showingPreview) { CardPreviewSheet(card: buildPreviewCard()) @@ -524,6 +574,54 @@ private struct ProfilePhotoView: View { } } +// MARK: - Contact Field Row View + +private struct ContactFieldRowView: View { + let field: AddedContactField + let onTap: () -> Void + + var body: some View { + Button(action: onTap) { + HStack(spacing: Design.Spacing.medium) { + // Drag handle + Image(systemName: "line.3.horizontal") + .font(.subheadline) + .foregroundStyle(Color.Text.tertiary) + .accessibilityHidden(true) + + // Icon + Circle() + .fill(field.fieldType.iconColor) + .frame(width: Design.CardSize.avatarSize, height: Design.CardSize.avatarSize) + .overlay( + field.fieldType.iconImage() + .font(.title3) + .foregroundStyle(.white) + ) + + // Content + VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { + Text(field.value.isEmpty ? field.fieldType.valuePlaceholder : field.shortDisplayValue) + .font(.subheadline) + .foregroundStyle(field.value.isEmpty ? Color.Text.secondary : Color.Text.primary) + .lineLimit(1) + + Text(field.title.isEmpty ? field.fieldType.displayName : field.title) + .font(.caption) + .foregroundStyle(Color.Text.secondary) + .lineLimit(1) + } + + Spacer() + } + .contentShape(.rect) + } + .buttonStyle(.plain) + .accessibilityLabel("\(field.fieldType.displayName): \(field.shortDisplayValue)") + .accessibilityHint(String.localized("Tap to edit, drag to reorder")) + } +} + // MARK: - Accreditations Row private struct AccreditationsRow: View { @@ -828,6 +926,35 @@ private extension CardEditorView { return previewCard } + // MARK: - Contact Field Operations + + func addContactField(fieldType: ContactFieldType, value: String, title: String) { + guard !value.isEmpty else { return } + withAnimation { + let newField = AddedContactField( + fieldType: fieldType, + value: value, + title: title + ) + contactFields.append(newField) + } + } + + func updateContactField(id: UUID, value: String, title: String) { + if let index = contactFields.firstIndex(where: { $0.id == id }) { + withAnimation { + contactFields[index].value = value + contactFields[index].title = title + } + } + } + + func deleteContactField(id: UUID) { + withAnimation { + contactFields.removeAll { $0.id == id } + } + } + // MARK: - Contact Fields Sync /// Saves the contactFields array to the BusinessCard model