BusinessCard/BusinessCard/Views/CardEditorView.swift

298 lines
12 KiB
Swift

import SwiftUI
import SwiftData
struct CardEditorView: View {
@Environment(AppState.self) private var appState
@Environment(\.dismiss) private var dismiss
let card: BusinessCard?
let onSave: (BusinessCard) -> Void
@State private var displayName: String = ""
@State private var role: String = ""
@State private var company: String = ""
@State private var label: String = "Work"
@State private var email: String = ""
@State private var phone: String = ""
@State private var website: String = ""
@State private var location: String = ""
@State private var avatarSystemName: String = "person.crop.circle"
@State private var selectedTheme: CardTheme = .coral
@State private var selectedLayout: CardLayoutStyle = .stacked
private var isEditing: Bool { card != nil }
private var isFormValid: Bool {
!displayName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
var body: some View {
NavigationStack {
Form {
Section {
CardPreviewSection(
displayName: displayName.isEmpty ? String.localized("Your Name") : displayName,
role: role.isEmpty ? String.localized("Your Role") : role,
company: company.isEmpty ? String.localized("Company") : company,
label: label,
avatarSystemName: avatarSystemName,
theme: selectedTheme,
layoutStyle: selectedLayout
)
}
.listRowBackground(Color.clear)
.listRowInsets(EdgeInsets())
Section(String.localized("Personal Information")) {
TextField(String.localized("Full Name"), text: $displayName)
.textContentType(.name)
.accessibilityLabel(String.localized("Full Name"))
TextField(String.localized("Role / Title"), text: $role)
.textContentType(.jobTitle)
.accessibilityLabel(String.localized("Role"))
TextField(String.localized("Company"), text: $company)
.textContentType(.organizationName)
.accessibilityLabel(String.localized("Company"))
TextField(String.localized("Card Label"), text: $label)
.accessibilityLabel(String.localized("Card Label"))
.accessibilityHint(String.localized("A short label like Work or Personal"))
}
Section(String.localized("Contact Details")) {
TextField(String.localized("Email"), text: $email)
.textContentType(.emailAddress)
.keyboardType(.emailAddress)
.textInputAutocapitalization(.never)
.accessibilityLabel(String.localized("Email"))
TextField(String.localized("Phone"), text: $phone)
.textContentType(.telephoneNumber)
.keyboardType(.phonePad)
.accessibilityLabel(String.localized("Phone"))
TextField(String.localized("Website"), text: $website)
.textContentType(.URL)
.keyboardType(.URL)
.textInputAutocapitalization(.never)
.accessibilityLabel(String.localized("Website"))
TextField(String.localized("Location"), text: $location)
.textContentType(.fullStreetAddress)
.accessibilityLabel(String.localized("Location"))
}
Section(String.localized("Appearance")) {
AvatarPickerRow(selection: $avatarSystemName)
Picker(String.localized("Theme"), selection: $selectedTheme) {
ForEach(CardTheme.all) { theme in
HStack {
Circle()
.fill(theme.primaryColor)
.frame(width: Design.Spacing.large, height: Design.Spacing.large)
Text(theme.localizedName)
}
.tag(theme)
}
}
Picker(String.localized("Layout"), selection: $selectedLayout) {
ForEach(CardLayoutStyle.allCases) { layout in
Text(layout.displayName)
.tag(layout)
}
}
}
}
.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()
}
.disabled(!isFormValid)
}
}
.onAppear {
if let card {
displayName = card.displayName
role = card.role
company = card.company
label = card.label
email = card.email
phone = card.phone
website = card.website
location = card.location
avatarSystemName = card.avatarSystemName
selectedTheme = card.theme
selectedLayout = card.layoutStyle
}
}
}
}
private func saveCard() {
if let existingCard = card {
existingCard.displayName = displayName
existingCard.role = role
existingCard.company = company
existingCard.label = label
existingCard.email = email
existingCard.phone = phone
existingCard.website = website
existingCard.location = location
existingCard.avatarSystemName = avatarSystemName
existingCard.theme = selectedTheme
existingCard.layoutStyle = selectedLayout
onSave(existingCard)
} else {
let newCard = BusinessCard(
displayName: displayName,
role: role,
company: company,
label: label,
email: email,
phone: phone,
website: website,
location: location,
isDefault: false,
themeName: selectedTheme.name,
layoutStyleRawValue: selectedLayout.rawValue,
avatarSystemName: avatarSystemName
)
onSave(newCard)
}
dismiss()
}
}
private struct CardPreviewSection: View {
let displayName: String
let role: String
let company: String
let label: String
let avatarSystemName: String
let theme: CardTheme
let layoutStyle: CardLayoutStyle
var body: some View {
VStack(spacing: Design.Spacing.medium) {
previewCard
}
.padding(.vertical, Design.Spacing.medium)
}
private var previewCard: some View {
VStack(spacing: Design.Spacing.medium) {
HStack(spacing: Design.Spacing.medium) {
Circle()
.fill(Color.Text.inverted)
.frame(width: Design.Size.avatarSize, height: Design.Size.avatarSize)
.overlay(
Image(systemName: avatarSystemName)
.foregroundStyle(theme.accentColor)
)
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(displayName)
.font(.headline)
.bold()
.foregroundStyle(Color.Text.inverted)
Text(role)
.font(.subheadline)
.foregroundStyle(Color.Text.inverted.opacity(Design.Opacity.almostFull))
Text(company)
.font(.caption)
.foregroundStyle(Color.Text.inverted.opacity(Design.Opacity.medium))
}
Spacer(minLength: Design.Spacing.small)
Text(String.localized(label))
.font(.caption)
.bold()
.foregroundStyle(Color.Text.inverted)
.padding(.horizontal, Design.Spacing.small)
.padding(.vertical, Design.Spacing.xxSmall)
.background(theme.accentColor.opacity(Design.Opacity.medium))
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
}
}
.padding(Design.Spacing.large)
.frame(maxWidth: .infinity)
.background(
LinearGradient(
colors: [theme.primaryColor, theme.secondaryColor],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.clipShape(.rect(cornerRadius: Design.CornerRadius.xLarge))
.shadow(
color: Color.Text.secondary.opacity(Design.Opacity.hint),
radius: Design.Shadow.radiusLarge,
x: Design.Shadow.offsetNone,
y: Design.Shadow.offsetMedium
)
}
}
private struct AvatarPickerRow: View {
@Binding var selection: String
private let avatarOptions = [
"person.crop.circle",
"person.crop.circle.fill",
"person.crop.square",
"person.circle",
"sparkles",
"music.mic",
"briefcase.fill",
"building.2.fill",
"star.fill",
"bolt.fill"
]
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text("Icon")
.font(.subheadline)
.foregroundStyle(Color.Text.secondary)
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 5), spacing: Design.Spacing.small) {
ForEach(avatarOptions, id: \.self) { icon in
Button {
selection = icon
} label: {
Image(systemName: icon)
.font(.title2)
.foregroundStyle(selection == icon ? Color.Accent.red : Color.Text.secondary)
.frame(width: Design.Size.avatarSize, height: Design.Size.avatarSize)
.background(selection == icon ? Color.AppBackground.accent : Color.AppBackground.base)
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
}
.buttonStyle(.plain)
.accessibilityLabel(icon)
.accessibilityAddTraits(selection == icon ? .isSelected : [])
}
}
}
}
}
#Preview("New Card") {
let container = try! ModelContainer(for: BusinessCard.self, Contact.self)
return CardEditorView(card: nil) { _ in }
.environment(AppState(modelContext: container.mainContext))
}