842 lines
30 KiB
Swift
842 lines
30 KiB
Swift
import SwiftUI
|
|
import Bedrock
|
|
import SwiftData
|
|
import PhotosUI
|
|
|
|
struct CardEditorView: View {
|
|
@Environment(AppState.self) private var appState
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
let card: BusinessCard?
|
|
let onSave: (BusinessCard) -> Void
|
|
|
|
// Name fields
|
|
@State private var displayName = ""
|
|
@State private var prefix = ""
|
|
@State private var firstName = ""
|
|
@State private var middleName = ""
|
|
@State private var lastName = ""
|
|
@State private var suffix = ""
|
|
@State private var maidenName = ""
|
|
@State private var preferredName = ""
|
|
@State private var pronouns = ""
|
|
@State private var showNameDetails = false
|
|
|
|
// Professional info
|
|
@State private var role = ""
|
|
@State private var department = ""
|
|
@State private var company = ""
|
|
@State private var headline = ""
|
|
@State private var label = "Work"
|
|
@State private var bio = ""
|
|
@State private var accreditations = ""
|
|
|
|
// Contact fields (unified list for picker-based UI)
|
|
@State private var contactFields: [AddedContactField] = []
|
|
|
|
// Appearance
|
|
@State private var avatarSystemName = "person.crop.circle"
|
|
@State private var selectedTheme: CardTheme = .coral
|
|
@State private var selectedLayout: CardLayoutStyle = .stacked
|
|
|
|
// Photos
|
|
@State private var photoData: Data?
|
|
@State private var coverPhotoData: Data?
|
|
@State private var logoData: Data?
|
|
|
|
// Photo editor state - just one variable!
|
|
@State private var editingImageType: ImageType?
|
|
|
|
@State private var showingPreview = false
|
|
|
|
enum ImageType: String, Identifiable {
|
|
case profile
|
|
case cover
|
|
case logo
|
|
|
|
var id: String { rawValue }
|
|
|
|
var title: String {
|
|
switch self {
|
|
case .profile: return String.localized("Add profile picture")
|
|
case .cover: return String.localized("Add cover photo")
|
|
case .logo: return String.localized("Add company logo")
|
|
}
|
|
}
|
|
|
|
var cropAspectRatio: CropAspectRatio {
|
|
switch self {
|
|
case .profile: return .square
|
|
case .cover: return .banner
|
|
case .logo: return .square
|
|
}
|
|
}
|
|
}
|
|
|
|
private var isEditing: Bool { card != nil }
|
|
private var isFormValid: Bool {
|
|
!effectiveDisplayName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
}
|
|
|
|
/// Simple name for validation and storage (without quotes/parentheses formatting)
|
|
private var effectiveDisplayName: String {
|
|
if !displayName.isEmpty { return displayName }
|
|
let parts = [prefix, firstName, middleName, lastName, suffix].filter { !$0.isEmpty }
|
|
return parts.joined(separator: " ")
|
|
}
|
|
|
|
/// Formatted name for display with special formatting
|
|
private var formattedDisplayName: String {
|
|
if !displayName.isEmpty { return displayName }
|
|
|
|
var parts: [String] = []
|
|
|
|
if !prefix.isEmpty { parts.append(prefix) }
|
|
if !firstName.isEmpty { parts.append(firstName) }
|
|
if !preferredName.isEmpty { parts.append("\"\(preferredName)\"") }
|
|
if !middleName.isEmpty { parts.append(middleName) }
|
|
if !lastName.isEmpty { parts.append(lastName) }
|
|
if !suffix.isEmpty { parts.append(suffix) }
|
|
if !maidenName.isEmpty { parts.append("(\(maidenName))") }
|
|
if !pronouns.isEmpty { parts.append("(\(pronouns))") }
|
|
|
|
return parts.joined(separator: " ")
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
// Card Style section
|
|
Section {
|
|
CardStylePicker(selectedTheme: $selectedTheme)
|
|
}
|
|
|
|
// Images & Layout section
|
|
Section {
|
|
ImageLayoutRow(
|
|
photoData: $photoData,
|
|
coverPhotoData: $coverPhotoData,
|
|
logoData: $logoData,
|
|
avatarSystemName: avatarSystemName,
|
|
selectedTheme: selectedTheme,
|
|
onSelectImage: { imageType in
|
|
editingImageType = imageType
|
|
}
|
|
)
|
|
} header: {
|
|
Text("Images & layout")
|
|
}
|
|
|
|
// Personal details section
|
|
Section {
|
|
// Name row with expand button
|
|
Button {
|
|
withAnimation { showNameDetails.toggle() }
|
|
} label: {
|
|
HStack {
|
|
Text(formattedDisplayName.isEmpty ? "Full Name" : formattedDisplayName)
|
|
.foregroundStyle(formattedDisplayName.isEmpty ? Color.secondary : Color.primary)
|
|
Spacer()
|
|
Image(systemName: showNameDetails ? "chevron.up" : "chevron.down")
|
|
.foregroundStyle(Color.accentColor)
|
|
}
|
|
}
|
|
.tint(.primary)
|
|
|
|
if showNameDetails {
|
|
TextField("Prefix (e.g. Dr., Mr., Ms.)", text: $prefix)
|
|
TextField("First Name", text: $firstName)
|
|
TextField("Middle Name", text: $middleName)
|
|
TextField("Last Name", text: $lastName)
|
|
TextField("Suffix (e.g. Jr., III)", text: $suffix)
|
|
TextField("Maiden Name", text: $maidenName)
|
|
TextField("Preferred Name", text: $preferredName)
|
|
TextField("Pronouns (e.g. she/her)", text: $pronouns)
|
|
}
|
|
} header: {
|
|
Text("Personal details")
|
|
}
|
|
|
|
// Professional section
|
|
Section {
|
|
TextField("Job Title", text: $role)
|
|
TextField("Department", text: $department)
|
|
TextField("Company", text: $company)
|
|
TextField("Headline", text: $headline)
|
|
}
|
|
|
|
// Accreditations
|
|
Section {
|
|
AccreditationsRow(accreditations: $accreditations)
|
|
} header: {
|
|
Text("Accreditations")
|
|
}
|
|
|
|
// Card Label
|
|
Section {
|
|
Picker("Card Label", selection: $label) {
|
|
ForEach(["Work", "Personal", "Creative", "Other"], id: \.self) { option in
|
|
Text(option).tag(option)
|
|
}
|
|
}
|
|
.pickerStyle(.segmented)
|
|
} header: {
|
|
Text("Card Label")
|
|
}
|
|
|
|
// Contact & social fields manager
|
|
Section {
|
|
ContactFieldsManagerView(fields: $contactFields)
|
|
} header: {
|
|
Text("Contact Fields")
|
|
}
|
|
|
|
// Bio section
|
|
Section {
|
|
TextField("Tell people about yourself...", text: $bio, axis: .vertical)
|
|
.lineLimit(3...8)
|
|
} header: {
|
|
Text("About")
|
|
}
|
|
}
|
|
.safeAreaInset(edge: .bottom) {
|
|
PreviewCardButton { showingPreview = true }
|
|
}
|
|
.navigationTitle(isEditing ? String.localized("Edit Card") : String.localized("New Card"))
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button(String.localized("Cancel")) { dismiss() }
|
|
}
|
|
ToolbarItem(placement: .confirmationAction) {
|
|
Button(String.localized("Save")) { saveCard() }
|
|
.bold()
|
|
.disabled(!isFormValid)
|
|
}
|
|
}
|
|
.sheet(item: $editingImageType) { imageType in
|
|
ImageEditorFlow(
|
|
imageType: imageType,
|
|
hasExistingImage: hasExistingPhoto(for: imageType)
|
|
) { imageData in
|
|
if let imageData {
|
|
if imageData.isEmpty {
|
|
// Empty data = remove photo
|
|
removePhoto(for: imageType)
|
|
} else {
|
|
// Save the cropped image
|
|
savePhoto(imageData, for: imageType)
|
|
}
|
|
}
|
|
// nil = cancelled, do nothing
|
|
editingImageType = nil
|
|
}
|
|
}
|
|
.onAppear { loadCardData() }
|
|
.sheet(isPresented: $showingPreview) {
|
|
CardPreviewSheet(card: buildPreviewCard())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Card Style Picker
|
|
|
|
private struct CardStylePicker: View {
|
|
@Binding var selectedTheme: CardTheme
|
|
|
|
var body: some View {
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(spacing: Design.Spacing.medium) {
|
|
ForEach(CardTheme.all) { theme in
|
|
Button {
|
|
selectedTheme = theme
|
|
} label: {
|
|
Circle()
|
|
.fill(theme.primaryColor)
|
|
.frame(width: Design.CardSize.colorSwatchSize, height: Design.CardSize.colorSwatchSize)
|
|
.overlay(
|
|
Circle()
|
|
.stroke(selectedTheme == theme ? Color.primary : .clear, lineWidth: Design.LineWidth.medium)
|
|
.padding(Design.Spacing.xxSmall)
|
|
)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.accessibilityLabel(theme.name)
|
|
.accessibilityAddTraits(selectedTheme == theme ? .isSelected : [])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Image Layout Row
|
|
|
|
private struct ImageLayoutRow: View {
|
|
@Binding var photoData: Data?
|
|
@Binding var coverPhotoData: Data?
|
|
@Binding var logoData: Data?
|
|
let avatarSystemName: String
|
|
let selectedTheme: CardTheme
|
|
let onSelectImage: (CardEditorView.ImageType) -> Void
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
|
|
// Card preview with edit buttons
|
|
ZStack(alignment: .bottomLeading) {
|
|
// Banner with cover photo or gradient
|
|
BannerPreviewView(
|
|
coverPhotoData: coverPhotoData,
|
|
logoData: logoData,
|
|
selectedTheme: selectedTheme,
|
|
onEditCover: { onSelectImage(.cover) },
|
|
onEditLogo: { onSelectImage(.logo) }
|
|
)
|
|
|
|
// Profile photo with edit button
|
|
ZStack(alignment: .bottomTrailing) {
|
|
ProfilePhotoView(photoData: photoData, avatarSystemName: avatarSystemName, theme: selectedTheme)
|
|
|
|
Button {
|
|
onSelectImage(.profile)
|
|
} label: {
|
|
Image(systemName: "pencil")
|
|
.font(.caption2)
|
|
.padding(Design.Spacing.xSmall)
|
|
.background(.ultraThinMaterial)
|
|
.clipShape(.circle)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
.offset(x: Design.Spacing.large, y: Design.CardSize.avatarOverlap)
|
|
}
|
|
.padding(.bottom, Design.CardSize.avatarOverlap)
|
|
|
|
// Photo action buttons
|
|
ImageActionButtonsRow(
|
|
photoData: $photoData,
|
|
coverPhotoData: $coverPhotoData,
|
|
logoData: $logoData,
|
|
onSelectImage: onSelectImage
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Banner Preview View
|
|
|
|
private struct BannerPreviewView: View {
|
|
let coverPhotoData: Data?
|
|
let logoData: Data?
|
|
let selectedTheme: CardTheme
|
|
let onEditCover: () -> Void
|
|
let onEditLogo: () -> Void
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Background: cover photo or gradient
|
|
if let coverPhotoData, let uiImage = UIImage(data: coverPhotoData) {
|
|
Image(uiImage: uiImage)
|
|
.resizable()
|
|
.scaledToFill()
|
|
.frame(height: Design.CardSize.bannerHeight)
|
|
.clipped()
|
|
} else {
|
|
LinearGradient(
|
|
colors: [selectedTheme.primaryColor, selectedTheme.secondaryColor],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
}
|
|
|
|
// Company logo overlay
|
|
if let logoData, let uiImage = UIImage(data: logoData) {
|
|
Image(uiImage: uiImage)
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(height: Design.CardSize.logoSize)
|
|
}
|
|
|
|
// Edit buttons overlay
|
|
VStack {
|
|
HStack {
|
|
// Edit cover photo button (top-left)
|
|
Button(action: onEditCover) {
|
|
Image(systemName: "photo")
|
|
.font(.caption)
|
|
.padding(Design.Spacing.small)
|
|
.background(.ultraThinMaterial)
|
|
.clipShape(.circle)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.accessibilityLabel(String.localized("Edit cover photo"))
|
|
|
|
Spacer()
|
|
|
|
// Edit logo button (top-right)
|
|
Button(action: onEditLogo) {
|
|
Image(systemName: "building.2")
|
|
.font(.caption)
|
|
.padding(Design.Spacing.small)
|
|
.background(.ultraThinMaterial)
|
|
.clipShape(.circle)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.accessibilityLabel(String.localized("Edit company logo"))
|
|
}
|
|
Spacer()
|
|
}
|
|
.padding(Design.Spacing.small)
|
|
}
|
|
.frame(height: Design.CardSize.bannerHeight)
|
|
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
|
|
}
|
|
}
|
|
|
|
// MARK: - Image Action Buttons Row
|
|
|
|
private struct ImageActionButtonsRow: View {
|
|
@Binding var photoData: Data?
|
|
@Binding var coverPhotoData: Data?
|
|
@Binding var logoData: Data?
|
|
let onSelectImage: (CardEditorView.ImageType) -> Void
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
|
// Profile photo action
|
|
ImageActionRow(
|
|
title: String.localized("Profile Photo"),
|
|
subtitle: photoData == nil ? String.localized("Add your headshot") : String.localized("Change or remove"),
|
|
systemImage: "person.crop.circle",
|
|
hasImage: photoData != nil,
|
|
onTap: { onSelectImage(.profile) },
|
|
onRemove: { photoData = nil }
|
|
)
|
|
|
|
// Cover photo action
|
|
ImageActionRow(
|
|
title: String.localized("Cover Photo"),
|
|
subtitle: coverPhotoData == nil ? String.localized("Add banner background") : String.localized("Change or remove"),
|
|
systemImage: "photo.fill",
|
|
hasImage: coverPhotoData != nil,
|
|
onTap: { onSelectImage(.cover) },
|
|
onRemove: { coverPhotoData = nil }
|
|
)
|
|
|
|
// Company logo action
|
|
ImageActionRow(
|
|
title: String.localized("Company Logo"),
|
|
subtitle: logoData == nil ? String.localized("Add your logo") : String.localized("Change or remove"),
|
|
systemImage: "building.2",
|
|
hasImage: logoData != nil,
|
|
onTap: { onSelectImage(.logo) },
|
|
onRemove: { logoData = nil }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Image Action Row
|
|
|
|
private struct ImageActionRow: View {
|
|
let title: String
|
|
let subtitle: String
|
|
let systemImage: String
|
|
let hasImage: Bool
|
|
let onTap: () -> Void
|
|
let onRemove: () -> Void
|
|
|
|
var body: some View {
|
|
Button(action: onTap) {
|
|
HStack(spacing: Design.Spacing.medium) {
|
|
Image(systemName: systemImage)
|
|
.font(.title3)
|
|
.foregroundStyle(hasImage ? Color.accentColor : Color.Text.secondary)
|
|
.frame(width: Design.CardSize.socialIconSize)
|
|
|
|
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
|
Text(title)
|
|
.font(.subheadline)
|
|
.foregroundStyle(Color.Text.primary)
|
|
|
|
Text(subtitle)
|
|
.font(.caption)
|
|
.foregroundStyle(Color.Text.secondary)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Image(systemName: "chevron.right")
|
|
.font(.caption)
|
|
.foregroundStyle(Color.Text.tertiary)
|
|
}
|
|
.padding(.vertical, Design.Spacing.xSmall)
|
|
.contentShape(.rect)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.accessibilityLabel("\(title): \(subtitle)")
|
|
.contextMenu {
|
|
if hasImage {
|
|
Button(role: .destructive) {
|
|
onRemove()
|
|
} label: {
|
|
Label(String.localized("Remove"), systemImage: "trash")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct ProfilePhotoView: View {
|
|
let photoData: Data?
|
|
let avatarSystemName: String
|
|
let theme: CardTheme
|
|
|
|
var body: some View {
|
|
Group {
|
|
if let photoData, let uiImage = UIImage(data: photoData) {
|
|
Image(uiImage: uiImage)
|
|
.resizable()
|
|
.scaledToFill()
|
|
} else {
|
|
Image(systemName: avatarSystemName)
|
|
.font(.title)
|
|
.foregroundStyle(theme.textColor)
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
.background(theme.accentColor)
|
|
}
|
|
}
|
|
.frame(width: Design.CardSize.avatarLarge, height: Design.CardSize.avatarLarge)
|
|
.clipShape(.circle)
|
|
.overlay(Circle().stroke(Color.AppBackground.elevated, lineWidth: Design.LineWidth.thick))
|
|
}
|
|
}
|
|
|
|
// MARK: - Accreditations Row
|
|
|
|
private struct AccreditationsRow: View {
|
|
@Binding var accreditations: String
|
|
@State private var accreditationInput = ""
|
|
|
|
private var accreditationsList: [String] {
|
|
accreditations.split(separator: ",")
|
|
.map { $0.trimmingCharacters(in: .whitespaces) }
|
|
.filter { !$0.isEmpty }
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
|
// Input row
|
|
HStack {
|
|
TextField("e.g. MBA, CPA, PhD", text: $accreditationInput)
|
|
|
|
Button {
|
|
addAccreditation()
|
|
} label: {
|
|
Image(systemName: "plus.circle.fill")
|
|
.font(.title2)
|
|
.foregroundStyle(Color.accentColor)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.disabled(accreditationInput.trimmingCharacters(in: .whitespaces).isEmpty)
|
|
}
|
|
|
|
// Tag bubbles
|
|
if !accreditationsList.isEmpty {
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(spacing: Design.Spacing.small) {
|
|
ForEach(accreditationsList, id: \.self) { tag in
|
|
HStack(spacing: Design.Spacing.xSmall) {
|
|
Text(tag)
|
|
.font(.subheadline)
|
|
Button {
|
|
removeAccreditation(tag)
|
|
} label: {
|
|
Image(systemName: "xmark.circle.fill")
|
|
.font(.caption)
|
|
.foregroundStyle(Color.secondary)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
.padding(.horizontal, Design.Spacing.small)
|
|
.padding(.vertical, Design.Spacing.xSmall)
|
|
.background(Color.secondary.opacity(Design.Opacity.subtle))
|
|
.clipShape(.capsule)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func addAccreditation() {
|
|
let trimmed = accreditationInput.trimmingCharacters(in: .whitespaces)
|
|
guard !trimmed.isEmpty else { return }
|
|
|
|
var list = accreditationsList
|
|
list.append(trimmed)
|
|
accreditations = list.joined(separator: ", ")
|
|
accreditationInput = ""
|
|
}
|
|
|
|
private func removeAccreditation(_ tag: String) {
|
|
var list = accreditationsList
|
|
list.removeAll { $0 == tag }
|
|
accreditations = list.joined(separator: ", ")
|
|
}
|
|
}
|
|
|
|
// MARK: - Supporting Views
|
|
|
|
private struct PreviewCardButton: View {
|
|
let action: () -> Void
|
|
|
|
var body: some View {
|
|
Button(action: action) {
|
|
Text("Preview card")
|
|
.font(.headline)
|
|
.foregroundStyle(.white)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(Design.Spacing.medium)
|
|
.background(Color.Text.primary)
|
|
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
|
|
}
|
|
.padding(.horizontal, Design.Spacing.large)
|
|
.padding(.vertical, Design.Spacing.medium)
|
|
.background(.ultraThinMaterial)
|
|
}
|
|
}
|
|
|
|
private struct CardPreviewSheet: View {
|
|
let card: BusinessCard
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
ScrollView {
|
|
BusinessCardView(card: card)
|
|
.padding(Design.Spacing.large)
|
|
}
|
|
.background(Color.AppBackground.base)
|
|
.navigationTitle(String.localized("Preview"))
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .confirmationAction) {
|
|
Button(String.localized("Done")) { dismiss() }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Data Operations
|
|
|
|
private extension CardEditorView {
|
|
func loadCardData() {
|
|
guard let card else { return }
|
|
displayName = card.displayName
|
|
prefix = card.prefix
|
|
firstName = card.firstName
|
|
middleName = card.middleName
|
|
lastName = card.lastName
|
|
suffix = card.suffix
|
|
maidenName = card.maidenName
|
|
preferredName = card.preferredName
|
|
pronouns = card.pronouns
|
|
role = card.role
|
|
department = card.department
|
|
company = card.company
|
|
headline = card.headline
|
|
label = card.label
|
|
bio = card.bio
|
|
accreditations = card.accreditations
|
|
avatarSystemName = card.avatarSystemName
|
|
|
|
// Load contact fields from the array
|
|
contactFields = card.orderedContactFields.compactMap { $0.toAddedContactField() }
|
|
selectedTheme = card.theme
|
|
selectedLayout = card.layoutStyle
|
|
photoData = card.photoData
|
|
coverPhotoData = card.coverPhotoData
|
|
logoData = card.logoData
|
|
}
|
|
|
|
// MARK: - Photo Helpers
|
|
|
|
func hasExistingPhoto(for imageType: ImageType?) -> Bool {
|
|
guard let imageType else { return false }
|
|
switch imageType {
|
|
case .profile: return photoData != nil
|
|
case .cover: return coverPhotoData != nil
|
|
case .logo: return logoData != nil
|
|
}
|
|
}
|
|
|
|
func removePhoto(for imageType: ImageType?) {
|
|
guard let imageType else { return }
|
|
switch imageType {
|
|
case .profile: photoData = nil
|
|
case .cover: coverPhotoData = nil
|
|
case .logo: logoData = nil
|
|
}
|
|
}
|
|
|
|
func savePhoto(_ data: Data, for imageType: ImageType) {
|
|
switch imageType {
|
|
case .profile: photoData = data
|
|
case .cover: coverPhotoData = data
|
|
case .logo: logoData = data
|
|
}
|
|
}
|
|
|
|
func saveCard() {
|
|
if let existingCard = card {
|
|
updateCard(existingCard)
|
|
onSave(existingCard)
|
|
} else {
|
|
let newCard = createCard()
|
|
onSave(newCard)
|
|
}
|
|
dismiss()
|
|
}
|
|
|
|
func updateCard(_ card: BusinessCard) {
|
|
card.displayName = displayName.isEmpty ? effectiveDisplayName : displayName
|
|
card.prefix = prefix
|
|
card.firstName = firstName
|
|
card.middleName = middleName
|
|
card.lastName = lastName
|
|
card.suffix = suffix
|
|
card.maidenName = maidenName
|
|
card.preferredName = preferredName
|
|
card.pronouns = pronouns
|
|
card.role = role
|
|
card.department = department
|
|
card.company = company
|
|
card.headline = headline
|
|
card.label = label
|
|
card.bio = bio
|
|
card.accreditations = accreditations
|
|
card.avatarSystemName = avatarSystemName
|
|
card.theme = selectedTheme
|
|
card.layoutStyle = selectedLayout
|
|
card.photoData = photoData
|
|
card.coverPhotoData = coverPhotoData
|
|
card.logoData = logoData
|
|
|
|
// Save contact fields to the model's array
|
|
saveContactFieldsToCard(card)
|
|
}
|
|
|
|
func createCard() -> BusinessCard {
|
|
let newCard = BusinessCard(
|
|
displayName: displayName.isEmpty ? effectiveDisplayName : displayName,
|
|
role: role,
|
|
company: company,
|
|
label: label,
|
|
isDefault: false,
|
|
themeName: selectedTheme.name,
|
|
layoutStyleRawValue: selectedLayout.rawValue,
|
|
avatarSystemName: avatarSystemName,
|
|
prefix: prefix,
|
|
firstName: firstName,
|
|
middleName: middleName,
|
|
lastName: lastName,
|
|
suffix: suffix,
|
|
maidenName: maidenName,
|
|
preferredName: preferredName,
|
|
pronouns: pronouns,
|
|
department: department,
|
|
headline: headline,
|
|
bio: bio,
|
|
accreditations: accreditations,
|
|
photoData: photoData,
|
|
coverPhotoData: coverPhotoData,
|
|
logoData: logoData
|
|
)
|
|
|
|
// Save contact fields to the model's array
|
|
saveContactFieldsToCard(newCard)
|
|
|
|
return newCard
|
|
}
|
|
|
|
func buildPreviewCard() -> BusinessCard {
|
|
let previewCard = BusinessCard(
|
|
displayName: displayName.isEmpty ? effectiveDisplayName : displayName,
|
|
role: role,
|
|
company: company,
|
|
label: label,
|
|
isDefault: false,
|
|
themeName: selectedTheme.name,
|
|
layoutStyleRawValue: selectedLayout.rawValue,
|
|
avatarSystemName: avatarSystemName,
|
|
prefix: prefix,
|
|
firstName: firstName,
|
|
middleName: middleName,
|
|
lastName: lastName,
|
|
suffix: suffix,
|
|
maidenName: maidenName,
|
|
preferredName: preferredName,
|
|
pronouns: pronouns,
|
|
department: department,
|
|
headline: headline,
|
|
bio: bio,
|
|
accreditations: accreditations,
|
|
photoData: photoData,
|
|
coverPhotoData: coverPhotoData,
|
|
logoData: logoData
|
|
)
|
|
|
|
// Add contact fields to preview card
|
|
for (index, field) in contactFields.enumerated() {
|
|
let contactField = ContactField(
|
|
typeId: field.fieldType.id,
|
|
value: field.value,
|
|
title: field.title,
|
|
orderIndex: index
|
|
)
|
|
if previewCard.contactFields == nil {
|
|
previewCard.contactFields = []
|
|
}
|
|
previewCard.contactFields?.append(contactField)
|
|
}
|
|
|
|
return previewCard
|
|
}
|
|
|
|
// MARK: - Contact Fields Sync
|
|
|
|
/// Saves the contactFields array to the BusinessCard model
|
|
func saveContactFieldsToCard(_ card: BusinessCard) {
|
|
// Clear existing contact fields
|
|
card.contactFields?.removeAll()
|
|
|
|
// Add new fields from the UI array
|
|
for (index, addedField) in contactFields.enumerated() {
|
|
let value = addedField.value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !value.isEmpty else { continue }
|
|
|
|
let field = ContactField(
|
|
typeId: addedField.fieldType.id,
|
|
value: value,
|
|
title: addedField.title,
|
|
orderIndex: index
|
|
)
|
|
field.card = card
|
|
if card.contactFields == nil {
|
|
card.contactFields = []
|
|
}
|
|
card.contactFields?.append(field)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Preview
|
|
|
|
#Preview("New Card") {
|
|
let container = try! ModelContainer(for: BusinessCard.self, Contact.self)
|
|
return CardEditorView(card: nil) { _ in }
|
|
.environment(AppState(modelContext: container.mainContext))
|
|
}
|