From 5e2ff8b9906ca34bbcdf453d760344adc6c142c1 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 8 Jan 2026 22:52:40 -0600 Subject: [PATCH] Signed-off-by: Matt Bruce --- BusinessCard/Resources/Localizable.xcstrings | 6 +- BusinessCard/State/ContactsStore.swift | 27 ++++++ BusinessCard/Views/ContactsView.swift | 19 ++++- .../Views/Sheets/AddContactSheet.swift | 85 +++++++++++++++++++ README.md | 1 + ai_implmentation.md | 1 + 6 files changed, 132 insertions(+), 7 deletions(-) create mode 100644 BusinessCard/Views/Sheets/AddContactSheet.swift diff --git a/BusinessCard/Resources/Localizable.xcstrings b/BusinessCard/Resources/Localizable.xcstrings index ab28fde..ecd8a2a 100644 --- a/BusinessCard/Resources/Localizable.xcstrings +++ b/BusinessCard/Resources/Localizable.xcstrings @@ -500,9 +500,6 @@ } } } - }, - "Share your card and track recipients, or scan someone else's QR code to save their card." : { - }, "Shared With" : { @@ -512,6 +509,9 @@ }, "Support & Funding" : { + }, + "Tap + to add a contact, scan a QR code, or track who you share your card with." : { + }, "Tap a field below to add it" : { diff --git a/BusinessCard/State/ContactsStore.swift b/BusinessCard/State/ContactsStore.swift index 6e15bf5..46acc29 100644 --- a/BusinessCard/State/ContactsStore.swift +++ b/BusinessCard/State/ContactsStore.swift @@ -75,6 +75,33 @@ final class ContactsStore: ContactTracking { fetchContacts() } + /// Creates a new contact manually + func createContact( + name: String, + role: String = "", + company: String = "", + email: String = "", + phone: String = "", + notes: String = "", + tags: String = "", + metAt: String = "" + ) { + let contact = Contact( + name: name, + role: role, + company: company, + cardLabel: "Manual", + notes: notes, + tags: tags, + email: email, + phone: phone, + metAt: metAt + ) + modelContext.insert(contact) + saveContext() + fetchContacts() + } + /// Updates a contact's notes func updateNotes(for contact: Contact, notes: String) { contact.notes = notes diff --git a/BusinessCard/Views/ContactsView.swift b/BusinessCard/Views/ContactsView.swift index d870bb3..628fdf0 100644 --- a/BusinessCard/Views/ContactsView.swift +++ b/BusinessCard/Views/ContactsView.swift @@ -5,6 +5,7 @@ import SwiftData struct ContactsView: View { @Environment(AppState.self) private var appState @State private var showingScanner = false + @State private var showingAddContact = false var body: some View { @Bindable var contactsStore = appState.contactsStore @@ -20,10 +21,17 @@ struct ContactsView: View { .navigationTitle(String.localized("Contacts")) .toolbar { ToolbarItem(placement: .primaryAction) { - Button(String.localized("Scan Card"), systemImage: "qrcode.viewfinder") { - showingScanner = true + HStack(spacing: Design.Spacing.small) { + Button(String.localized("Add Contact"), systemImage: "plus") { + showingAddContact = true + } + .accessibilityHint(String.localized("Manually add a new contact")) + + Button(String.localized("Scan Card"), systemImage: "qrcode.viewfinder") { + showingScanner = true + } + .accessibilityHint(String.localized("Scan someone else's QR code to save their card")) } - .accessibilityHint(String.localized("Scan someone else's QR code to save their card")) } } .sheet(isPresented: $showingScanner) { @@ -34,6 +42,9 @@ struct ContactsView: View { showingScanner = false } } + .sheet(isPresented: $showingAddContact) { + AddContactSheet() + } } } } @@ -49,7 +60,7 @@ private struct EmptyContactsView: View { .font(.headline) .foregroundStyle(Color.Text.primary) - Text("Share your card and track recipients, or scan someone else's QR code to save their card.") + Text("Tap + to add a contact, scan a QR code, or track who you share your card with.") .font(.subheadline) .foregroundStyle(Color.Text.secondary) .multilineTextAlignment(.center) diff --git a/BusinessCard/Views/Sheets/AddContactSheet.swift b/BusinessCard/Views/Sheets/AddContactSheet.swift new file mode 100644 index 0000000..879badd --- /dev/null +++ b/BusinessCard/Views/Sheets/AddContactSheet.swift @@ -0,0 +1,85 @@ +import SwiftUI +import SwiftData +import Bedrock + +struct AddContactSheet: View { + @Environment(AppState.self) private var appState + @Environment(\.dismiss) private var dismiss + + @State private var name = "" + @State private var role = "" + @State private var company = "" + @State private var email = "" + @State private var phone = "" + @State private var metAt = "" + + private var canSave: Bool { + !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + } + + var body: some View { + NavigationStack { + Form { + Section(String.localized("Name")) { + TextField(String.localized("Full name"), text: $name) + .textContentType(.name) + .accessibilityLabel(String.localized("Contact name")) + } + + Section(String.localized("Role")) { + TextField(String.localized("Job title"), text: $role) + .textContentType(.jobTitle) + TextField(String.localized("Company"), text: $company) + .textContentType(.organizationName) + } + + Section(String.localized("Contact")) { + TextField(String.localized("Email"), text: $email) + .keyboardType(.emailAddress) + .textContentType(.emailAddress) + .textInputAutocapitalization(.never) + TextField(String.localized("Phone"), text: $phone) + .keyboardType(.phonePad) + .textContentType(.telephoneNumber) + } + + Section(String.localized("Where You Met")) { + TextField(String.localized("Event, location, or how you connected..."), text: $metAt) + } + } + .navigationTitle(String.localized("Add Contact")) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button(String.localized("Cancel")) { + dismiss() + } + } + ToolbarItem(placement: .confirmationAction) { + Button(String.localized("Save")) { + saveContact() + } + .bold() + .disabled(!canSave) + } + } + } + } + + private func saveContact() { + appState.contactsStore.createContact( + name: name.trimmingCharacters(in: .whitespacesAndNewlines), + role: role.trimmingCharacters(in: .whitespacesAndNewlines), + company: company.trimmingCharacters(in: .whitespacesAndNewlines), + email: email.trimmingCharacters(in: .whitespacesAndNewlines), + phone: phone.trimmingCharacters(in: .whitespacesAndNewlines), + metAt: metAt.trimmingCharacters(in: .whitespacesAndNewlines) + ) + dismiss() + } +} + +#Preview { + AddContactSheet() + .environment(AppState(modelContext: try! ModelContainer(for: BusinessCard.self, Contact.self).mainContext)) +} diff --git a/README.md b/README.md index 478eb39..fe5000e 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ Each field has: ### Contacts +- **Add contacts manually**: Tap + to create contacts with name, role, company, email, phone - Track who you've shared your card with - **Scan QR codes** to save someone else's business card - **Notes & annotations**: Add notes about each contact diff --git a/ai_implmentation.md b/ai_implmentation.md index c8fb6f0..6cc3881 100644 --- a/ai_implmentation.md +++ b/ai_implmentation.md @@ -129,6 +129,7 @@ Reusable components (in `Views/Components/`): Sheets (in `Views/Sheets/`): - `RecordContactSheet.swift` — track share recipient - `ContactFieldEditorSheet.swift` — add/edit contact field with type-specific UI +- `AddContactSheet.swift` — manually add a new contact Small utilities: - `Views/EmptyStateView.swift` — empty state placeholder