Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-01-08 23:07:16 -06:00
parent 5e2ff8b990
commit 6ee74c422b
6 changed files with 480 additions and 206 deletions

View File

@ -29,6 +29,8 @@ extension Design {
static let widgetPhoneHeight: CGFloat = 120 static let widgetPhoneHeight: CGFloat = 120
static let widgetWatchSize: CGFloat = 100 static let widgetWatchSize: CGFloat = 100
static let floatingButtonSize: CGFloat = 56 static let floatingButtonSize: CGFloat = 56
/// Bottom offset for floating button above tab bar.
static let floatingButtonBottomOffset: CGFloat = 72
} }
} }
@ -49,7 +51,9 @@ extension Color {
enum AppBackground { enum AppBackground {
static let base = Color(red: 0.97, green: 0.96, blue: 0.94) static let base = Color(red: 0.97, green: 0.96, blue: 0.94)
static let secondary = Color(red: 0.95, green: 0.95, blue: 0.95)
static let elevated = Color(red: 1.0, green: 1.0, blue: 1.0) static let elevated = Color(red: 1.0, green: 1.0, blue: 1.0)
static let card = Color(red: 1.0, green: 1.0, blue: 1.0)
static let accent = Color(red: 0.95, green: 0.91, blue: 0.86) static let accent = Color(red: 0.95, green: 0.91, blue: 0.86)
} }

View File

@ -75,6 +75,12 @@
} }
} }
} }
},
"Add note" : {
},
"Add tag" : {
}, },
"Address" : { "Address" : {
@ -151,6 +157,9 @@
}, },
"Company Website" : { "Company Website" : {
},
"Connection details" : {
}, },
"Contact" : { "Contact" : {
@ -304,12 +313,18 @@
}, },
"Messaging" : { "Messaging" : {
},
"More..." : {
}, },
"No card selected" : { "No card selected" : {
}, },
"No contacts yet" : { "No contacts yet" : {
},
"Notes" : {
}, },
"Open on Apple Watch" : { "Open on Apple Watch" : {
"localizations" : { "localizations" : {
@ -663,6 +678,9 @@
}, },
"Work" : { "Work" : {
},
"Write down a memorable reminder about your contact" : {
} }
}, },
"version" : "1.1" "version" : "1.1"

View File

@ -84,7 +84,8 @@ final class ContactsStore: ContactTracking {
phone: String = "", phone: String = "",
notes: String = "", notes: String = "",
tags: String = "", tags: String = "",
metAt: String = "" metAt: String = "",
followUpDate: Date? = nil
) { ) {
let contact = Contact( let contact = Contact(
name: name, name: name,
@ -93,6 +94,7 @@ final class ContactsStore: ContactTracking {
cardLabel: "Manual", cardLabel: "Manual",
notes: notes, notes: notes,
tags: tags, tags: tags,
followUpDate: followUpDate,
email: email, email: email,
phone: phone, phone: phone,
metAt: metAt metAt: metAt

View File

@ -8,127 +8,171 @@ struct ContactDetailView: View {
@Bindable var contact: Contact @Bindable var contact: Contact
@State private var isEditing = false
@State private var showingDeleteConfirmation = false @State private var showingDeleteConfirmation = false
@State private var showingMoreActions = false
@State private var showingAddTag = false
@State private var showingAddNote = false
@State private var newTag = ""
var body: some View { var body: some View {
List { ZStack(alignment: .bottom) {
// Header section ScrollView {
Section { VStack(spacing: 0) {
ContactHeaderView(contact: contact) // Header banner
} ContactBannerView(contact: contact)
.listRowBackground(Color.clear)
// Contact info section // Content
VStack(alignment: .leading, spacing: Design.Spacing.large) {
// Name
Text(contact.name.isEmpty ? String.localized("Contact") : contact.name)
.font(.largeTitle)
.bold()
.foregroundStyle(Color.Text.primary)
// Connection details
VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text("Connection details")
.font(.subheadline)
.foregroundStyle(Color.Text.tertiary)
HStack(spacing: Design.Spacing.small) {
Image(systemName: "calendar")
.foregroundStyle(Color.Text.primary)
Text(contact.lastSharedDate, format: .dateTime.day().month().year().hour().minute())
.font(.subheadline)
.foregroundStyle(Color.Text.primary)
}
}
// Tags
HStack(spacing: Design.Spacing.small) {
ForEach(contact.tagList, id: \.self) { tag in
TagPill(text: tag, onDelete: {
removeTag(tag)
})
}
Button {
showingAddTag = true
} label: {
Label(String.localized("Add tag"), systemImage: "plus")
.font(.subheadline)
.bold()
.foregroundStyle(Color.AppText.inverted)
.padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.small)
.background(Color.Text.primary)
.clipShape(.capsule)
}
}
// Contact info card
if !contact.email.isEmpty || !contact.phone.isEmpty { if !contact.email.isEmpty || !contact.phone.isEmpty {
Section(String.localized("Contact")) { VStack(spacing: 0) {
if !contact.email.isEmpty { if !contact.email.isEmpty {
ContactInfoRow( ContactInfoRow(
title: String.localized("Email"), icon: "envelope.fill",
value: contact.email, value: contact.email,
systemImage: "envelope", label: "Home",
action: { openURL("mailto:\(contact.email)") } action: { openURL("mailto:\(contact.email)") }
) )
if !contact.phone.isEmpty {
Divider()
.padding(.leading, Design.Spacing.xLarge + Design.CardSize.avatarSize)
}
} }
if !contact.phone.isEmpty { if !contact.phone.isEmpty {
ContactInfoRow( ContactInfoRow(
title: String.localized("Phone"), icon: "phone.fill",
value: contact.phone, value: contact.phone,
systemImage: "phone", label: "Cell",
action: { openURL("tel:\(contact.phone)") } action: { openURL("tel:\(contact.phone)") }
) )
} }
} }
.background(Color.AppBackground.card)
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
} }
// Notes section // Notes section
Section(String.localized("Notes")) { VStack(alignment: .leading, spacing: Design.Spacing.medium) {
TextField(String.localized("Add notes about this contact..."), text: $contact.notes, axis: .vertical) Text("Notes")
.lineLimit(3...10) .font(.headline)
} .bold()
.foregroundStyle(Color.Text.primary)
// Tags section if contact.notes.isEmpty {
Section(String.localized("Tags")) { NotesEmptyState()
TextField(String.localized("Tags (comma separated)"), text: $contact.tags)
.accessibilityHint(String.localized("e.g. client, VIP, networking"))
if !contact.tagList.isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: Design.Spacing.small) {
ForEach(contact.tagList, id: \.self) { tag in
Text(tag)
.font(.caption)
.padding(.horizontal, Design.Spacing.small)
.padding(.vertical, Design.Spacing.xSmall)
.background(Color.Accent.red.opacity(Design.Opacity.hint))
.foregroundStyle(Color.Accent.red)
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
}
}
}
}
}
// Follow-up section
Section(String.localized("Follow-up")) {
Toggle(String.localized("Set Reminder"), isOn: Binding(
get: { contact.followUpDate != nil },
set: { newValue in
if newValue {
contact.followUpDate = .now.addingTimeInterval(86400 * 7) // 1 week default
} else { } else {
contact.followUpDate = nil Text(contact.notes)
} .font(.body)
}
))
if let followUpDate = contact.followUpDate {
DatePicker(
String.localized("Reminder Date"),
selection: Binding(
get: { followUpDate },
set: { contact.followUpDate = $0 }
),
displayedComponents: .date
)
if contact.isFollowUpOverdue {
Label(String.localized("Overdue"), systemImage: "exclamationmark.circle.fill")
.foregroundStyle(Color.Accent.red)
}
}
}
// Met at section
Section(String.localized("Where You Met")) {
TextField(String.localized("Event, location, or how you connected..."), text: $contact.metAt)
}
// Activity section
Section(String.localized("Activity")) {
LabeledContent(String.localized("Last Shared")) {
Text(appState.contactsStore.relativeShareDate(for: contact))
}
LabeledContent(String.localized("Card Used")) {
Text(String.localized(contact.cardLabel))
}
if contact.isReceivedCard {
Label(String.localized("Received via QR scan"), systemImage: "qrcode")
.foregroundStyle(Color.Text.secondary) .foregroundStyle(Color.Text.secondary)
} .frame(maxWidth: .infinity, alignment: .leading)
.padding(Design.Spacing.medium)
.background(Color.AppBackground.card)
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
} }
// Delete section Button {
Section { showingAddNote = true
} label: {
Label(String.localized("Add note"), systemImage: "plus")
.font(.subheadline)
.foregroundStyle(Color.Text.primary)
.frame(maxWidth: .infinity)
.padding(.vertical, Design.Spacing.medium)
.background(Color.AppBackground.card)
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
.overlay(
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
.stroke(Color.Text.tertiary.opacity(Design.Opacity.light), lineWidth: Design.LineWidth.thin)
)
}
}
.padding(.top, Design.Spacing.medium)
// Spacer for bottom bar
Spacer()
.frame(height: Design.Spacing.xLarge * 4)
}
.padding(.horizontal, Design.Spacing.large)
.padding(.top, Design.Spacing.large)
}
}
.background(Color.AppBackground.secondary)
.ignoresSafeArea(edges: .top)
// Bottom action bar
BottomActionBar(
onMore: { showingMoreActions = true },
onAddTag: { showingAddTag = true },
onAddNote: { showingAddNote = true }
)
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
// Edit action - could navigate to edit mode
} label: {
Image(systemName: "square.and.pencil")
.foregroundStyle(Color.AppText.inverted)
}
}
}
.toolbarBackground(.hidden, for: .navigationBar)
.confirmationDialog(String.localized("More"), isPresented: $showingMoreActions) {
Button(String.localized("Download contact")) {
// Download action
}
Button(String.localized("Share contact")) {
// Share action
}
Button(String.localized("Delete Contact"), role: .destructive) { Button(String.localized("Delete Contact"), role: .destructive) {
showingDeleteConfirmation = true showingDeleteConfirmation = true
} }
Button(String.localized("Cancel"), role: .cancel) { }
} }
}
.navigationTitle(contact.name)
.navigationBarTitleDisplayMode(.inline)
.alert(String.localized("Delete Contact"), isPresented: $showingDeleteConfirmation) { .alert(String.localized("Delete Contact"), isPresented: $showingDeleteConfirmation) {
Button(String.localized("Cancel"), role: .cancel) { } Button(String.localized("Cancel"), role: .cancel) { }
Button(String.localized("Delete"), role: .destructive) { Button(String.localized("Delete"), role: .destructive) {
@ -138,98 +182,270 @@ struct ContactDetailView: View {
} message: { } message: {
Text("Are you sure you want to delete this contact?") Text("Are you sure you want to delete this contact?")
} }
.alert(String.localized("Add Tag"), isPresented: $showingAddTag) {
TextField(String.localized("Tag name"), text: $newTag)
Button(String.localized("Cancel"), role: .cancel) {
newTag = ""
}
Button(String.localized("Add")) {
addTag()
}
}
.sheet(isPresented: $showingAddNote) {
AddNoteSheet(notes: $contact.notes)
}
} }
private func openURL(_ urlString: String) { private func openURL(_ urlString: String) {
guard let url = URL(string: urlString) else { return } guard let url = URL(string: urlString) else { return }
UIApplication.shared.open(url) UIApplication.shared.open(url)
} }
private func addTag() {
let tag = newTag.trimmingCharacters(in: .whitespacesAndNewlines)
guard !tag.isEmpty else { return }
if contact.tags.isEmpty {
contact.tags = tag
} else {
contact.tags += ", \(tag)"
}
newTag = ""
}
private func removeTag(_ tag: String) {
var tags = contact.tagList
tags.removeAll { $0 == tag }
contact.tags = tags.joined(separator: ", ")
}
} }
private struct ContactHeaderView: View { // MARK: - Banner View
private struct ContactBannerView: View {
let contact: Contact let contact: Contact
private var initials: String {
let parts = contact.name.split(separator: " ")
if parts.count >= 2 {
return "\(parts[0].prefix(1))\(parts[1].prefix(1))".uppercased()
} else if let first = parts.first {
return String(first.prefix(2)).uppercased()
}
return "?"
}
var body: some View { var body: some View {
VStack(spacing: Design.Spacing.medium) { ZStack {
if let photoData = contact.photoData, let uiImage = UIImage(data: photoData) { // Gradient background
Image(uiImage: uiImage) LinearGradient(
.resizable() colors: [
.scaledToFill() Color.CardPalette.coral,
.frame(width: Design.CardSize.qrSize / 2, height: Design.CardSize.qrSize / 2) Color.CardPalette.coral.opacity(Design.Opacity.strong)
.clipShape(.circle) ],
} else { startPoint: .top,
Image(systemName: contact.avatarSystemName) endPoint: .bottom
.font(.system(size: Design.BaseFontSize.display)) )
.foregroundStyle(Color.Accent.red)
.frame(width: Design.CardSize.qrSize / 2, height: Design.CardSize.qrSize / 2)
.background(Color.AppBackground.accent)
.clipShape(.circle)
}
VStack(spacing: Design.Spacing.xSmall) { // Decorative circles
Text(contact.name) Circle()
.font(.title2) .fill(Color.white.opacity(Design.Opacity.subtle))
.bold() .frame(width: Design.CardSize.qrSize, height: Design.CardSize.qrSize)
.foregroundStyle(Color.Text.primary) .offset(y: -Design.Spacing.xLarge)
if !contact.role.isEmpty || !contact.company.isEmpty { // Initials
Text("\(contact.role)\(contact.role.isEmpty || contact.company.isEmpty ? "" : " at ")\(contact.company)") VStack(spacing: Design.Spacing.xxSmall) {
.font(.subheadline) Text(String(initials.prefix(1)))
.foregroundStyle(Color.Text.secondary) .font(.system(size: Design.BaseFontSize.display, weight: .light))
Text(String(initials.dropFirst().prefix(1)))
.font(.system(size: Design.BaseFontSize.display, weight: .light))
} }
.foregroundStyle(Color.white.opacity(Design.Opacity.accent))
} }
} .frame(height: Design.CardSize.bannerHeight * 1.5)
.frame(maxWidth: .infinity)
.padding(.vertical, Design.Spacing.medium)
} }
} }
// MARK: - Tag Pill
private struct TagPill: View {
let text: String
let onDelete: () -> Void
var body: some View {
HStack(spacing: Design.Spacing.xSmall) {
Text(text)
.font(.subheadline)
Button {
onDelete()
} label: {
Image(systemName: "xmark")
.font(.caption2)
}
}
.foregroundStyle(Color.Text.primary)
.padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.small)
.background(Color.AppBackground.card)
.clipShape(.capsule)
.overlay(
Capsule()
.stroke(Color.Text.tertiary.opacity(Design.Opacity.light), lineWidth: Design.LineWidth.thin)
)
}
}
// MARK: - Contact Info Row
private struct ContactInfoRow: View { private struct ContactInfoRow: View {
let title: String let icon: String
let value: String let value: String
let systemImage: String let label: String
let action: () -> Void let action: () -> Void
var body: some View { var body: some View {
Button(action: action) { Button(action: action) {
HStack(spacing: Design.Spacing.medium) { HStack(spacing: Design.Spacing.medium) {
Image(systemName: systemImage) // Icon circle
.foregroundStyle(Color.Accent.red) Image(systemName: icon)
.frame(width: Design.Spacing.xLarge) .font(.body)
.foregroundStyle(Color.white)
.frame(width: Design.CardSize.avatarSize, height: Design.CardSize.avatarSize)
.background(Color.CardPalette.coral)
.clipShape(.circle)
// Text
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(title)
.font(.caption)
.foregroundStyle(Color.Text.secondary)
Text(value) Text(value)
.font(.body) .font(.body)
.foregroundStyle(Color.Text.primary) .foregroundStyle(Color.Text.primary)
Text(label)
.font(.caption)
.foregroundStyle(Color.Text.tertiary)
} }
Spacer() Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.foregroundStyle(Color.Text.secondary)
} }
.padding(Design.Spacing.medium)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
} }
} }
// MARK: - Notes Empty State
private struct NotesEmptyState: View {
var body: some View {
VStack(spacing: Design.Spacing.medium) {
Image(systemName: "note.text")
.font(.system(size: Design.BaseFontSize.display))
.foregroundStyle(Color.CardPalette.coral.opacity(Design.Opacity.medium))
Text("Write down a memorable reminder about your contact")
.font(.subheadline)
.foregroundStyle(Color.Text.tertiary)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity)
.padding(.vertical, Design.Spacing.xLarge)
.background(Color.AppBackground.card)
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
}
}
// MARK: - Bottom Action Bar
private struct BottomActionBar: View {
let onMore: () -> Void
let onAddTag: () -> Void
let onAddNote: () -> Void
var body: some View {
HStack(spacing: Design.Spacing.medium) {
Button(action: onMore) {
Text("More...")
.font(.subheadline)
.bold()
.foregroundStyle(Color.Text.primary)
.frame(maxWidth: .infinity)
.padding(.vertical, Design.Spacing.medium)
.background(Color.AppBackground.card)
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
}
Button(action: onAddTag) {
Text("Add tag")
.font(.subheadline)
.bold()
.foregroundStyle(Color.AppText.inverted)
.frame(maxWidth: .infinity)
.padding(.vertical, Design.Spacing.medium)
.background(Color.Text.primary)
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
}
Button(action: onAddNote) {
Text("Add note")
.font(.subheadline)
.bold()
.foregroundStyle(Color.AppText.inverted)
.frame(maxWidth: .infinity)
.padding(.vertical, Design.Spacing.medium)
.background(Color.Text.primary)
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
}
}
.padding(.horizontal, Design.Spacing.large)
.padding(.vertical, Design.Spacing.medium)
.background(Color.AppBackground.secondary)
}
}
// MARK: - Add Note Sheet
private struct AddNoteSheet: View {
@Environment(\.dismiss) private var dismiss
@Binding var notes: String
@State private var editedNotes: String = ""
var body: some View {
NavigationStack {
TextEditor(text: $editedNotes)
.padding(Design.Spacing.medium)
.navigationTitle(String.localized("Notes"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(String.localized("Cancel")) {
dismiss()
}
}
ToolbarItem(placement: .confirmationAction) {
Button(String.localized("Save")) {
notes = editedNotes
dismiss()
}
.bold()
}
}
.onAppear {
editedNotes = notes
}
}
}
}
#Preview { #Preview {
NavigationStack { NavigationStack {
ContactDetailView( ContactDetailView(
contact: Contact( contact: Contact(
name: "Kevin Lennox", name: "Heidi Bruce",
role: "Branch Manager", role: "Designer",
company: "Global Bank", company: "Apple",
notes: "Met at the Austin fintech conference", notes: "",
tags: "finance, potential client", tags: "",
followUpDate: .now.addingTimeInterval(86400 * 3), email: "heidi.h.bruce@gmail.com",
email: "kevin@globalbank.com", phone: "2145328862"
phone: "+1 555 123 4567",
metAt: "Austin Fintech Conference 2026"
) )
) )
.environment(AppState(modelContext: try! ModelContainer(for: BusinessCard.self, Contact.self).mainContext)) .environment(AppState(modelContext: try! ModelContainer(for: BusinessCard.self, Contact.self).mainContext))

View File

@ -24,8 +24,8 @@ struct RootTabView: View {
} }
} }
// Floating share button - hidden when no cards exist // Floating share button - only shown on cards tab when cards exist
if !appState.cardStore.cards.isEmpty { if appState.selectedTab == .cards && !appState.cardStore.cards.isEmpty {
FloatingShareButton { FloatingShareButton {
showingShareSheet = true showingShareSheet = true
} }
@ -62,7 +62,7 @@ private struct FloatingShareButton: View {
} }
.accessibilityLabel(String.localized("Share")) .accessibilityLabel(String.localized("Share"))
.accessibilityHint(String.localized("Opens the share sheet to send your card")) .accessibilityHint(String.localized("Opens the share sheet to send your card"))
.padding(.bottom, Design.Spacing.xxLarge + Design.Spacing.xLarge) .padding(.bottom, Design.CardSize.floatingButtonBottomOffset)
} }
} }

View File

@ -6,48 +6,70 @@ struct AddContactSheet: View {
@Environment(AppState.self) private var appState @Environment(AppState.self) private var appState
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@State private var name = "" @State private var firstName = ""
@State private var role = "" @State private var lastName = ""
@State private var company = ""
@State private var email = ""
@State private var phone = "" @State private var phone = ""
@State private var metAt = "" @State private var email = ""
@State private var link = ""
@State private var jobTitle = ""
@State private var company = ""
private var canSave: Bool { private var canSave: Bool {
!name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty !firstName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ||
!lastName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
private var fullName: String {
[firstName, lastName]
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
.joined(separator: " ")
} }
var body: some View { var body: some View {
NavigationStack { NavigationStack {
Form { ScrollView {
Section(String.localized("Name")) { VStack(spacing: 0) {
TextField(String.localized("Full name"), text: $name) // Name section
.textContentType(.name) VStack(alignment: .leading, spacing: Design.Spacing.medium) {
.accessibilityLabel(String.localized("Contact name")) EditorTextField(placeholder: String.localized("First name"), text: $firstName)
.textContentType(.givenName)
EditorTextField(placeholder: String.localized("Last name"), text: $lastName)
.textContentType(.familyName)
} }
.padding(Design.Spacing.large)
.background(Color.AppBackground.elevated)
Section(String.localized("Role")) { // Contact section
TextField(String.localized("Job title"), text: $role) VStack(alignment: .leading, spacing: Design.Spacing.medium) {
.textContentType(.jobTitle) EditorTextField(placeholder: String.localized("Phone"), text: $phone)
TextField(String.localized("Company"), text: $company) .keyboardType(.phonePad)
.textContentType(.organizationName) .textContentType(.telephoneNumber)
} EditorTextField(placeholder: String.localized("Email"), text: $email)
Section(String.localized("Contact")) {
TextField(String.localized("Email"), text: $email)
.keyboardType(.emailAddress) .keyboardType(.emailAddress)
.textContentType(.emailAddress) .textContentType(.emailAddress)
.textInputAutocapitalization(.never) .textInputAutocapitalization(.never)
TextField(String.localized("Phone"), text: $phone) EditorTextField(placeholder: String.localized("Link"), text: $link)
.keyboardType(.phonePad) .keyboardType(.URL)
.textContentType(.telephoneNumber) .textContentType(.URL)
.textInputAutocapitalization(.never)
} }
.padding(Design.Spacing.large)
.background(Color.AppBackground.elevated)
Section(String.localized("Where You Met")) { // Professional section
TextField(String.localized("Event, location, or how you connected..."), text: $metAt) VStack(alignment: .leading, spacing: Design.Spacing.medium) {
EditorTextField(placeholder: String.localized("Job title"), text: $jobTitle)
.textContentType(.jobTitle)
EditorTextField(placeholder: String.localized("Company"), text: $company)
.textContentType(.organizationName)
}
.padding(Design.Spacing.large)
.background(Color.AppBackground.elevated)
} }
} }
.navigationTitle(String.localized("Add Contact")) .background(Color.AppBackground.base)
.navigationTitle(String.localized("New contact"))
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItem(placement: .cancellationAction) { ToolbarItem(placement: .cancellationAction) {
@ -59,7 +81,6 @@ struct AddContactSheet: View {
Button(String.localized("Save")) { Button(String.localized("Save")) {
saveContact() saveContact()
} }
.bold()
.disabled(!canSave) .disabled(!canSave)
} }
} }
@ -68,17 +89,30 @@ struct AddContactSheet: View {
private func saveContact() { private func saveContact() {
appState.contactsStore.createContact( appState.contactsStore.createContact(
name: name.trimmingCharacters(in: .whitespacesAndNewlines), name: fullName,
role: role.trimmingCharacters(in: .whitespacesAndNewlines), role: jobTitle.trimmingCharacters(in: .whitespacesAndNewlines),
company: company.trimmingCharacters(in: .whitespacesAndNewlines), company: company.trimmingCharacters(in: .whitespacesAndNewlines),
email: email.trimmingCharacters(in: .whitespacesAndNewlines), email: email.trimmingCharacters(in: .whitespacesAndNewlines),
phone: phone.trimmingCharacters(in: .whitespacesAndNewlines), phone: phone.trimmingCharacters(in: .whitespacesAndNewlines)
metAt: metAt.trimmingCharacters(in: .whitespacesAndNewlines)
) )
dismiss() dismiss()
} }
} }
// MARK: - Shared Editor TextField
private struct EditorTextField: View {
let placeholder: String
@Binding var text: String
var body: some View {
VStack(alignment: .leading, spacing: 0) {
TextField(placeholder, text: $text)
Divider()
}
}
}
#Preview { #Preview {
AddContactSheet() AddContactSheet()
.environment(AppState(modelContext: try! ModelContainer(for: BusinessCard.self, Contact.self).mainContext)) .environment(AppState(modelContext: try! ModelContainer(for: BusinessCard.self, Contact.self).mainContext))