From 77f70386d2364f2b359da1e7f9956c92682fc5cf Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 8 Jan 2026 22:27:47 -0600 Subject: [PATCH] Signed-off-by: Matt Bruce --- BusinessCard/Models/AppTab.swift | 1 - BusinessCard/Resources/Localizable.xcstrings | 36 ++-- BusinessCard/Views/CardCarouselView.swift | 151 --------------- BusinessCard/Views/CardsHomeView.swift | 185 +++++++++++++------ BusinessCard/Views/CustomizeCardView.swift | 172 ----------------- BusinessCard/Views/HeroBannerView.swift | 96 ---------- BusinessCard/Views/RootTabView.swift | 4 - BusinessCard/Views/WidgetsCalloutView.swift | 39 ---- README.md | 53 ++++-- ROADMAP.md | 40 +++- ai_implmentation.md | 63 +++++-- 11 files changed, 271 insertions(+), 569 deletions(-) delete mode 100644 BusinessCard/Views/CardCarouselView.swift delete mode 100644 BusinessCard/Views/CustomizeCardView.swift delete mode 100644 BusinessCard/Views/HeroBannerView.swift delete mode 100644 BusinessCard/Views/WidgetsCalloutView.swift diff --git a/BusinessCard/Models/AppTab.swift b/BusinessCard/Models/AppTab.swift index c4fcf66..21e7060 100644 --- a/BusinessCard/Models/AppTab.swift +++ b/BusinessCard/Models/AppTab.swift @@ -3,7 +3,6 @@ import Foundation enum AppTab: String, CaseIterable, Hashable, Identifiable { case cards case share - case customize case contacts case widgets diff --git a/BusinessCard/Resources/Localizable.xcstrings b/BusinessCard/Resources/Localizable.xcstrings index cd8b840..ab28fde 100644 --- a/BusinessCard/Resources/Localizable.xcstrings +++ b/BusinessCard/Resources/Localizable.xcstrings @@ -43,9 +43,6 @@ } } } - }, - "+1 555 123 4567" : { - }, "About" : { @@ -57,6 +54,7 @@ }, "Add a QR widget so your card is always one tap away." : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -126,6 +124,7 @@ }, "Change image layout" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -160,6 +159,7 @@ }, "Create multiple business cards" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -180,11 +180,15 @@ } } } + }, + "Create your first card" : { + }, "Custom Links" : { }, "Customize your card" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -211,6 +215,9 @@ }, "Delete Field" : { + }, + "Design and share polished digital business cards for every context." : { + }, "Developer" : { @@ -229,9 +236,6 @@ }, "Email or Username" : { - }, - "Example" : { - }, "Ext." : { @@ -508,9 +512,6 @@ }, "Support & Funding" : { - }, - "Tap \"New Card\" to create your first card" : { - }, "Tap a field below to add it" : { @@ -544,6 +545,7 @@ }, "The #1 Digital Business Card App" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -581,6 +583,7 @@ }, "Used by Industry Leaders" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -660,21 +663,6 @@ }, "Work" : { - }, - "Your card will appear here" : { - - }, - "Your Company" : { - - }, - "Your Name" : { - - }, - "Your Role" : { - - }, - "your@email.com" : { - } }, "version" : "1.1" diff --git a/BusinessCard/Views/CardCarouselView.swift b/BusinessCard/Views/CardCarouselView.swift deleted file mode 100644 index a86da26..0000000 --- a/BusinessCard/Views/CardCarouselView.swift +++ /dev/null @@ -1,151 +0,0 @@ -import SwiftUI -import Bedrock -import SwiftData - -struct CardCarouselView: View { - @Environment(AppState.self) private var appState - - var body: some View { - @Bindable var cardStore = appState.cardStore - let hasCards = !cardStore.cards.isEmpty - - VStack(spacing: Design.Spacing.medium) { - HStack { - Text(hasCards ? "Create multiple business cards" : "Your card will appear here") - .font(.headline) - .bold() - .foregroundStyle(Color.Text.primary) - Spacer() - } - - if hasCards { - TabView(selection: $cardStore.selectedCardID) { - ForEach(cardStore.cards) { card in - BusinessCardView(card: card, isCompact: true) - .tag(Optional(card.id)) - .padding(.vertical, Design.Spacing.medium) - } - } - .tabViewStyle(.page) - .frame(height: Design.CardSize.cardHeightCompact + Design.Spacing.xxLarge) - - if let selected = cardStore.selectedCard { - CardDefaultToggleView(card: selected) { - cardStore.setDefaultCard(selected) - } - } - } else { - DemoCardView() - .padding(.vertical, Design.Spacing.medium) - } - } - } -} - -private struct CardDefaultToggleView: View { - let card: BusinessCard - let action: () -> Void - - var body: some View { - Button( - card.isDefault ? String.localized("Default card") : String.localized("Set as default"), - systemImage: card.isDefault ? "checkmark.seal.fill" : "checkmark.seal", - action: action - ) - .buttonStyle(.bordered) - .tint(Color.Accent.red) - .accessibilityHint(String.localized("Sets this card as your default sharing card")) - } -} - -/// A demo card shown when the user has no cards yet. -/// Displays placeholder content with an "Example" badge to prompt card creation. -private struct DemoCardView: View { - var body: some View { - VStack(spacing: Design.Spacing.medium) { - VStack(alignment: .leading, spacing: Design.Spacing.small) { - HStack(spacing: Design.Spacing.medium) { - Circle() - .fill(Color.AppText.inverted) - .frame(width: Design.CardSize.avatarSize, height: Design.CardSize.avatarSize) - .overlay( - Image(systemName: "person.crop.circle") - .foregroundStyle(Color.CardPalette.coral) - ) - - VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { - Text("Your Name") - .font(.headline) - .bold() - .foregroundStyle(Color.AppText.inverted) - Text("Your Role") - .font(.subheadline) - .foregroundStyle(Color.AppText.inverted.opacity(Design.Opacity.almostFull)) - Text("Your Company") - .font(.caption) - .foregroundStyle(Color.AppText.inverted.opacity(Design.Opacity.medium)) - } - - Spacer(minLength: Design.Spacing.small) - - Text("Example") - .font(.caption) - .bold() - .foregroundStyle(Color.AppText.inverted) - .padding(.horizontal, Design.Spacing.small) - .padding(.vertical, Design.Spacing.xxSmall) - .background(Color.AppText.inverted.opacity(Design.Opacity.hint)) - .clipShape(.rect(cornerRadius: Design.CornerRadius.medium)) - } - - Divider() - .overlay(Color.AppText.inverted.opacity(Design.Opacity.medium)) - - HStack(spacing: Design.Spacing.xSmall) { - Image(systemName: "envelope") - .font(.caption) - Text("your@email.com") - .font(.caption) - } - .foregroundStyle(Color.AppText.inverted.opacity(Design.Opacity.heavy)) - - HStack(spacing: Design.Spacing.xSmall) { - Image(systemName: "phone") - .font(.caption) - Text("+1 555 123 4567") - .font(.caption) - } - .foregroundStyle(Color.AppText.inverted.opacity(Design.Opacity.heavy)) - } - .padding(Design.Spacing.large) - .frame(maxWidth: .infinity) - .background( - LinearGradient( - colors: [Color.CardPalette.coral, Color.CardPalette.sand], - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - ) - .clipShape(.rect(cornerRadius: Design.CornerRadius.xLarge)) - .shadow( - color: Color.AppText.secondary.opacity(Design.Opacity.hint), - radius: Design.Shadow.radiusLarge, - x: Design.Shadow.offsetNone, - y: Design.Shadow.offsetMedium - ) - .opacity(Design.Opacity.strong) - - Text("Tap \"New Card\" to create your first card") - .font(.caption) - .foregroundStyle(Color.AppText.secondary) - } - .accessibilityElement(children: .ignore) - .accessibilityLabel(String.localized("Example business card")) - .accessibilityHint(String.localized("Create a new card to replace this example")) - } -} - -#Preview { - CardCarouselView() - .environment(AppState(modelContext: try! ModelContainer(for: BusinessCard.self, Contact.self).mainContext)) -} diff --git a/BusinessCard/Views/CardsHomeView.swift b/BusinessCard/Views/CardsHomeView.swift index e064c62..a9b7090 100644 --- a/BusinessCard/Views/CardsHomeView.swift +++ b/BusinessCard/Views/CardsHomeView.swift @@ -5,81 +5,156 @@ import SwiftData struct CardsHomeView: View { @Environment(AppState.self) private var appState @State private var showingCreateCard = false + @State private var showingEditCard = false + @State private var showingDeleteConfirmation = false var body: some View { + @Bindable var cardStore = appState.cardStore + NavigationStack { - ScrollView { - VStack(spacing: Design.Spacing.xLarge) { - HeroBannerView() - SectionTitleView( - title: String.localized("Create your digital business card"), - subtitle: String.localized("Design and share polished cards for every context.") - ) - CardCarouselView() - - HStack(spacing: Design.Spacing.medium) { - if appState.cardStore.cards.isEmpty { - PrimaryActionButton( - title: String.localized("Create Card"), - systemImage: "plus" - ) { - showingCreateCard = true - } - } else { - PrimaryActionButton( - title: String.localized("Send my card"), - systemImage: "paperplane.fill" - ) { - appState.selectedTab = .share - } - - Button(String.localized("New Card"), systemImage: "plus") { - showingCreateCard = true - } - .buttonStyle(.bordered) - .tint(Color.Accent.ink) - .controlSize(.large) - .accessibilityHint(String.localized("Create a new business card")) - } - } - - WidgetsCalloutView() - } - .padding(.horizontal, Design.Spacing.large) - .padding(.vertical, Design.Spacing.xLarge) - } - .background( + ZStack { + // Background gradient LinearGradient( colors: [Color.AppBackground.base, Color.AppBackground.accent], startPoint: .top, endPoint: .bottom ) - ) - .navigationTitle(String.localized("My Cards")) + .ignoresSafeArea() + + if cardStore.cards.isEmpty { + EmptyCardsView(onCreateCard: { showingCreateCard = true }) + } else { + // Full-screen swipeable cards + TabView(selection: $cardStore.selectedCardID) { + ForEach(cardStore.cards) { card in + CardPageView( + card: card, + isDefault: card.isDefault, + onSetDefault: { cardStore.setDefaultCard(card) } + ) + .tag(Optional(card.id)) + } + } + .tabViewStyle(.page(indexDisplayMode: .automatic)) + .indexViewStyle(.page(backgroundDisplayMode: .automatic)) + } + } + .navigationTitle(cardStore.selectedCard?.label ?? String.localized("My Cards")) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button(String.localized("Add Card"), systemImage: "plus") { + showingCreateCard = true + } + .accessibilityHint(String.localized("Create a new business card")) + } + + ToolbarItemGroup(placement: .topBarTrailing) { + if cardStore.selectedCard != nil { + if cardStore.cards.count > 1 { + Button(String.localized("Delete"), systemImage: "trash") { + showingDeleteConfirmation = true + } + .accessibilityHint(String.localized("Delete this card")) + } + + Button(String.localized("Edit"), systemImage: "pencil") { + showingEditCard = true + } + .accessibilityHint(String.localized("Edit this card")) + } + } + } .sheet(isPresented: $showingCreateCard) { CardEditorView(card: nil) { newCard in appState.cardStore.addCard(newCard) } } + .sheet(isPresented: $showingEditCard) { + if let card = cardStore.selectedCard { + CardEditorView(card: card) { updatedCard in + appState.cardStore.updateCard(updatedCard) + } + } + } + .alert(String.localized("Delete Card"), isPresented: $showingDeleteConfirmation) { + Button(String.localized("Cancel"), role: .cancel) { } + Button(String.localized("Delete"), role: .destructive) { + if let card = cardStore.selectedCard { + appState.cardStore.deleteCard(card) + } + } + } message: { + Text("Are you sure you want to delete this card? This action cannot be undone.") + } } } } -private struct SectionTitleView: View { - let title: String - let subtitle: String +// MARK: - Card Page View +private struct CardPageView: View { + let card: BusinessCard + let isDefault: Bool + let onSetDefault: () -> Void + var body: some View { - VStack(alignment: .leading, spacing: Design.Spacing.small) { - Text(title) - .font(.title3) - .bold() - .foregroundStyle(Color.Text.primary) - Text(subtitle) - .font(.subheadline) - .foregroundStyle(Color.Text.secondary) + ScrollView { + VStack(spacing: Design.Spacing.large) { + BusinessCardView(card: card) + + // Default card toggle + Button( + isDefault ? String.localized("Default card") : String.localized("Set as default"), + systemImage: isDefault ? "checkmark.seal.fill" : "checkmark.seal", + action: onSetDefault + ) + .buttonStyle(.bordered) + .tint(Color.Accent.red) + .accessibilityHint(String.localized("Sets this card as your default sharing card")) + } + .padding(.horizontal, Design.Spacing.large) + .padding(.vertical, Design.Spacing.xLarge) + } + .scrollIndicators(.hidden) + } +} + +// MARK: - Empty State + +private struct EmptyCardsView: View { + let onCreateCard: () -> Void + + var body: some View { + VStack(spacing: Design.Spacing.xLarge) { + Spacer() + + Image(systemName: "rectangle.stack.badge.plus") + .font(.system(size: Design.BaseFontSize.display)) + .foregroundStyle(Color.Text.secondary) + + VStack(spacing: Design.Spacing.small) { + Text("Create your first card") + .font(.title2) + .bold() + .foregroundStyle(Color.Text.primary) + + Text("Design and share polished digital business cards for every context.") + .font(.subheadline) + .foregroundStyle(Color.Text.secondary) + .multilineTextAlignment(.center) + } + .padding(.horizontal, Design.Spacing.xLarge) + + PrimaryActionButton( + title: String.localized("Create Card"), + systemImage: "plus" + ) { + onCreateCard() + } + + Spacer() } - .frame(maxWidth: .infinity, alignment: .leading) } } diff --git a/BusinessCard/Views/CustomizeCardView.swift b/BusinessCard/Views/CustomizeCardView.swift deleted file mode 100644 index 80a9132..0000000 --- a/BusinessCard/Views/CustomizeCardView.swift +++ /dev/null @@ -1,172 +0,0 @@ -import SwiftUI -import Bedrock -import SwiftData - -struct CustomizeCardView: View { - @Environment(AppState.self) private var appState - @State private var showingEditCard = false - @State private var showingDeleteConfirmation = false - - var body: some View { - NavigationStack { - ScrollView { - VStack(spacing: Design.Spacing.large) { - Text("Customize your card") - .font(.title2) - .bold() - .foregroundStyle(Color.Text.primary) - - if let card = appState.cardStore.selectedCard { - BusinessCardView(card: card) - - CardActionsView( - onEdit: { showingEditCard = true }, - onDelete: { showingDeleteConfirmation = true }, - canDelete: appState.cardStore.cards.count > 1 - ) - - CardStylePickerView(selectedTheme: card.theme) { theme in - appState.cardStore.setSelectedTheme(theme) - } - CardLayoutPickerView(selectedLayout: card.layoutStyle) { layout in - appState.cardStore.setSelectedLayout(layout) - } - } else { - EmptyStateView( - title: String.localized("No card selected"), - message: String.localized("Select a card to start customizing.") - ) - } - } - .padding(.horizontal, Design.Spacing.large) - .padding(.vertical, Design.Spacing.xLarge) - } - .background(Color.AppBackground.base) - .navigationTitle(String.localized("Edit your card")) - .sheet(isPresented: $showingEditCard) { - if let card = appState.cardStore.selectedCard { - CardEditorView(card: card) { updatedCard in - appState.cardStore.updateCard(updatedCard) - } - } - } - .alert(String.localized("Delete Card"), isPresented: $showingDeleteConfirmation) { - Button(String.localized("Cancel"), role: .cancel) { } - Button(String.localized("Delete"), role: .destructive) { - if let card = appState.cardStore.selectedCard { - appState.cardStore.deleteCard(card) - } - } - } message: { - Text("Are you sure you want to delete this card? This action cannot be undone.") - } - } - } -} - -private struct CardActionsView: View { - let onEdit: () -> Void - let onDelete: () -> Void - let canDelete: Bool - - var body: some View { - HStack(spacing: Design.Spacing.medium) { - Button(String.localized("Edit Details"), systemImage: "pencil", action: onEdit) - .buttonStyle(.bordered) - .tint(Color.Accent.ink) - .accessibilityHint(String.localized("Edit card name, email, and other details")) - - if canDelete { - Button(String.localized("Delete"), systemImage: "trash", role: .destructive, action: onDelete) - .buttonStyle(.bordered) - .accessibilityHint(String.localized("Permanently delete this card")) - } - } - .padding(.vertical, Design.Spacing.small) - } -} - -private struct CardStylePickerView: View { - let selectedTheme: CardTheme - let onSelect: (CardTheme) -> Void - - var body: some View { - VStack(alignment: .leading, spacing: Design.Spacing.medium) { - Text("Card style") - .font(.headline) - .bold() - .foregroundStyle(Color.Text.primary) - - LazyVGrid(columns: gridColumns, spacing: Design.Spacing.small) { - ForEach(CardTheme.all) { theme in - Button(action: { onSelect(theme) }) { - VStack(spacing: Design.Spacing.xSmall) { - RoundedRectangle(cornerRadius: Design.CornerRadius.medium) - .fill(theme.primaryColor) - .frame(height: Design.CardSize.avatarSize) - .overlay( - RoundedRectangle(cornerRadius: Design.CornerRadius.medium) - .stroke( - selectedTheme.id == theme.id ? Color.Accent.red : Color.Text.inverted.opacity(Design.Opacity.medium), - lineWidth: Design.LineWidth.medium - ) - ) - - Text(theme.localizedName) - .font(.caption) - .foregroundStyle(Color.Text.secondary) - } - .frame(maxWidth: .infinity) - } - .buttonStyle(.plain) - .accessibilityLabel(String.localized("Card style")) - .accessibilityValue(theme.localizedName) - } - } - } - .padding(Design.Spacing.large) - .background(Color.AppBackground.elevated) - .clipShape(.rect(cornerRadius: Design.CornerRadius.large)) - } - - private var gridColumns: [GridItem] { - Array(repeating: GridItem(.flexible(), spacing: Design.Spacing.small), count: 3) - } -} - -private struct CardLayoutPickerView: View { - let selectedLayout: CardLayoutStyle - let onSelect: (CardLayoutStyle) -> Void - - var body: some View { - VStack(alignment: .leading, spacing: Design.Spacing.medium) { - Text("Images & layout") - .font(.headline) - .bold() - .foregroundStyle(Color.Text.primary) - - Picker(String.localized("Layout"), selection: Binding( - get: { selectedLayout }, - set: { onSelect($0) } - )) { - ForEach(CardLayoutStyle.allCases) { layout in - Text(layout.displayName) - .tag(layout) - } - } - .pickerStyle(.segmented) - - Text("Change image layout") - .font(.subheadline) - .foregroundStyle(Color.Text.secondary) - } - .padding(Design.Spacing.large) - .background(Color.AppBackground.elevated) - .clipShape(.rect(cornerRadius: Design.CornerRadius.large)) - } -} - -#Preview { - CustomizeCardView() - .environment(AppState(modelContext: try! ModelContainer(for: BusinessCard.self, Contact.self).mainContext)) -} diff --git a/BusinessCard/Views/HeroBannerView.swift b/BusinessCard/Views/HeroBannerView.swift deleted file mode 100644 index 0bf3b64..0000000 --- a/BusinessCard/Views/HeroBannerView.swift +++ /dev/null @@ -1,96 +0,0 @@ -import SwiftUI -import Bedrock - -struct HeroBannerView: View { - var body: some View { - VStack(spacing: Design.Spacing.medium) { - Text("The #1 Digital Business Card App") - .font(.title2) - .bold() - .foregroundStyle(Color.Text.primary) - .multilineTextAlignment(.center) - - HStack(spacing: Design.Spacing.large) { - StatBadgeView( - title: String.localized("4.9"), - subtitle: String.localized("App Rating"), - systemImage: "star.fill", - badgeColor: Color.Badge.star - ) - - StatBadgeView( - title: String.localized("100k+"), - subtitle: String.localized("Reviews"), - systemImage: "person.3.fill", - badgeColor: Color.Badge.neutral - ) - } - - Text("Used by Industry Leaders") - .font(.subheadline) - .foregroundStyle(Color.Text.secondary) - - HStack(spacing: Design.Spacing.large) { - BrandChipView(label: "Google") - BrandChipView(label: "Tesla") - BrandChipView(label: "Citi") - } - } - .padding(Design.Spacing.large) - .background(Color.AppBackground.elevated) - .clipShape(.rect(cornerRadius: Design.CornerRadius.large)) - .shadow( - color: Color.Text.secondary.opacity(Design.Opacity.hint), - radius: Design.Shadow.radiusMedium, - x: Design.Shadow.offsetNone, - y: Design.Shadow.offsetSmall - ) - } -} - -private struct StatBadgeView: View { - let title: String - let subtitle: String - let systemImage: String - let badgeColor: Color - - var body: some View { - VStack(spacing: Design.Spacing.xxSmall) { - HStack(spacing: Design.Spacing.xxSmall) { - Image(systemName: systemImage) - .font(.caption) - .foregroundStyle(Color.Text.primary) - Text(title) - .font(.headline) - .bold() - .foregroundStyle(Color.Text.primary) - } - Text(subtitle) - .font(.caption) - .foregroundStyle(Color.Text.secondary) - } - .padding(.horizontal, Design.Spacing.medium) - .padding(.vertical, Design.Spacing.small) - .background(badgeColor) - .clipShape(.rect(cornerRadius: Design.CornerRadius.medium)) - .accessibilityElement(children: .combine) - } -} - -private struct BrandChipView: View { - let label: String - - var body: some View { - Text(label) - .font(.caption) - .foregroundStyle(Color.Text.secondary) - .padding(.horizontal, Design.Spacing.medium) - .padding(.vertical, Design.Spacing.xSmall) - .background(Color.AppBackground.accent) - .clipShape(.rect(cornerRadius: Design.CornerRadius.medium)) - } -} - -#Preview { - HeroBannerView() -} diff --git a/BusinessCard/Views/RootTabView.swift b/BusinessCard/Views/RootTabView.swift index 37348f2..8d50b81 100644 --- a/BusinessCard/Views/RootTabView.swift +++ b/BusinessCard/Views/RootTabView.swift @@ -16,10 +16,6 @@ struct RootTabView: View { ShareCardView() } - Tab(String.localized("Customize"), systemImage: "slider.horizontal.3", value: AppTab.customize) { - CustomizeCardView() - } - Tab(String.localized("Contacts"), systemImage: "person.2", value: AppTab.contacts) { ContactsView() } diff --git a/BusinessCard/Views/WidgetsCalloutView.swift b/BusinessCard/Views/WidgetsCalloutView.swift deleted file mode 100644 index 0573b20..0000000 --- a/BusinessCard/Views/WidgetsCalloutView.swift +++ /dev/null @@ -1,39 +0,0 @@ -import SwiftUI -import Bedrock - -struct WidgetsCalloutView: View { - var body: some View { - HStack(spacing: Design.Spacing.large) { - VStack(alignment: .leading, spacing: Design.Spacing.small) { - Text("Share using widgets on your phone or watch") - .font(.headline) - .bold() - .foregroundStyle(Color.Text.primary) - Text("Add a QR widget so your card is always one tap away.") - .font(.subheadline) - .foregroundStyle(Color.Text.secondary) - } - - Spacer() - - Image(systemName: "applewatch") - .font(.title) - .foregroundStyle(Color.Accent.red) - } - .padding(Design.Spacing.large) - .background(Color.AppBackground.elevated) - .clipShape(.rect(cornerRadius: Design.CornerRadius.large)) - .shadow( - color: Color.Text.secondary.opacity(Design.Opacity.hint), - radius: Design.Shadow.radiusMedium, - x: Design.Shadow.offsetNone, - y: Design.Shadow.offsetSmall - ) - } -} - -#Preview { - WidgetsCalloutView() - .padding() - .background(Color.AppBackground.base) -} diff --git a/README.md b/README.md index 23156a3..164e394 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,15 @@ A SwiftUI iOS + watchOS app that creates and shares digital business cards with ### My Cards -- Create and browse multiple cards in a compact carousel view +- **Full-screen card display**: Swipe between your business cards in a full-screen view +- Tap the **edit icon** (pencil) in the top right to edit the current card +- Tap the **plus icon** to create a new card - Set a default card for sharing - **Modern card design**: Banner with company logo, overlapping profile photo, clean contact rows - **Profile photos**: Add a photo from your library or use an icon - **Company logos**: Upload a logo to display on your card's banner -- **Rich profiles**: First/middle/last name, pronouns, headline, bio, accreditations +- **Rich profiles**: First/middle/last name, prefix, maiden name, preferred name, pronouns, headline, bio, accreditations +- **Clickable contact fields**: Tap any field to call, email, open link, or launch app ### Share @@ -27,16 +30,36 @@ A SwiftUI iOS + watchOS app that creates and shares digital business cards with - **Track shares**: Record who received your card and when - Placeholder actions for Apple Wallet and NFC (alerts included) -### Customize +### Card Editor + +Access via the edit (pencil) icon in the My Cards tab: - **Horizontal color picker**: Scrollable theme swatches for quick selection - Theme picker with multiple color palettes (Coral, Midnight, Ocean, Lime, Violet) -- **Expandable name section**: First, middle, last, suffix, preferred name -- **Edit all card details**: Name, role, department, company, headline, email (with label), phone (with label & extension), website, location -- **Social media links**: LinkedIn, Twitter/X, Instagram, Facebook, TikTok, GitHub, Threads, Telegram -- **Payment links**: Venmo, Cash App -- **Custom links**: Add up to 2 custom URLs with titles -- **Suggestion chips**: Quick-fill suggestions for social link titles +- **Expandable name section**: Prefix, first, middle, last, maiden name, suffix, preferred name, pronouns +- **Edit all card details**: Name, role, department, company, headline, bio +- **Accreditations**: Tag-based input with inline editing + +#### Dynamic Contact Fields + +Add unlimited contact fields of any type, reorder by drag-and-drop: + +- **Contact**: Email, phone, website, address +- **Social Media**: LinkedIn, X/Twitter, Instagram, Facebook, TikTok, Threads, Bluesky, Mastodon, Reddit, Twitch, YouTube, Snapchat, Pinterest +- **Developer**: GitHub, GitLab, Stack Overflow, CodePen +- **Messaging**: Telegram, WhatsApp, Discord, Slack, Matrix, Signal +- **Support & Funding**: Patreon, Ko-fi, Buy Me a Coffee, GitHub Sponsors +- **Payment**: Venmo, Cash App, PayPal, Zelle +- **Scheduling**: Calendly +- **Other**: Custom links with any URL + +Each field has: +- **Custom icons**: Brand-colored icons from asset catalog (not generic SF Symbols) +- **Title/Label**: Add context like "Work", "Personal", or custom call-to-action +- **Deep linking**: Opens native apps when available (X, Instagram, etc.) +- **Drag-to-reorder**: Long press and drag to change order + +- **Suggestion chips**: Quick-fill suggestions for field titles - **Live preview**: "Preview card" button to see changes before saving - **Delete cards** you no longer need @@ -103,16 +126,22 @@ App-specific extensions are in `Design/DesignConstants.swift`. ``` BusinessCard/ +├── Assets.xcassets/ +│ └── SocialSymbols/ # Custom brand icons (LinkedIn, X, Instagram, etc.) ├── Design/ # Design constants (extends Bedrock) ├── Localization/ # String helpers -├── Models/ # SwiftData models (BusinessCard, Contact) +├── Models/ +│ ├── BusinessCard.swift # Main card model +│ ├── Contact.swift # Received contacts +│ ├── ContactField.swift # Dynamic contact fields (SwiftData) +│ └── ContactFieldType.swift # Field type definitions with icons & URLs ├── Protocols/ # Protocol definitions ├── Resources/ # String Catalogs (.xcstrings) ├── Services/ # Share link service, watch sync ├── State/ # Observable stores (CardStore, ContactsStore) └── Views/ - ├── Components/ # Reusable UI components (AvatarBadgeView, etc.) - ├── Sheets/ # Modal sheets (RecordContactSheet, etc.) + ├── Components/ # Reusable UI (ContactFieldPickerView, AddedContactFieldsView, etc.) + ├── Sheets/ # Modal sheets (ContactFieldEditorSheet, RecordContactSheet, etc.) └── [Feature].swift # Feature screens BusinessCardWatch/ # watchOS app target diff --git a/ROADMAP.md b/ROADMAP.md index 72c00e8..5e2c91b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -6,6 +6,39 @@ This document tracks planned features and their implementation status. ### High Priority (Core User Value) +- [x] **Dynamic contact fields system** - Unlimited fields with custom ordering + - New `ContactField` SwiftData model for flexible field storage + - `ContactFieldType` struct with 30+ field types across 8 categories + - Drag-to-reorder support with haptic feedback + - Sheet-based field editor with type-specific keyboards and placeholders + - Multiple fields of the same type allowed (e.g., multiple emails) + +- [x] **Custom social icons** - Brand-colored icons from asset catalog + - Custom `.symbolset` assets for all major platforms + - LinkedIn, X/Twitter, Instagram, Facebook, TikTok, Threads + - Bluesky, Mastodon, Reddit, Twitch, YouTube, Discord + - GitHub, Telegram, Slack, Matrix, Patreon, Ko-fi + - Proper `Image()` vs `Image(systemName:)` handling via `iconImage()` helper + +- [x] **Extended name fields** - Prefix, maiden name, pronouns + - Expandable name section in editor + - Prefix (Dr., Mr., Mrs., etc.) + - Maiden name with parentheses formatting in display name + - Preferred name with quotes formatting + - Pronouns displayed next to name on card + +- [x] **Accreditations as tags** - Tag bubble UI with inline editing + - Horizontal ScrollView of tag chips + - Tap to edit, check to save, x to delete + - Comma-separated storage for vCard compatibility + +- [x] **Clickable contact fields** - Deep linking to apps and actions + - Email → mailto: + - Phone → tel: + - Social links → app deep links or web fallback + - Address → Maps + - Website → Safari + - [x] **More card fields** - Social media links, custom URLs, pronouns, bio - Added: pronouns, bio, LinkedIn, Twitter/X, Instagram, Facebook, TikTok, GitHub - Added: 2 custom link slots (title + URL) @@ -78,6 +111,9 @@ This document tracks planned features and their implementation status. - [x] **Bedrock integration** - Design system, QR code generator - [x] **iOS-Watch sync** via App Groups - [x] **Unit tests** for models, stores, and new features +- [x] **Fixed data persistence** - Removed aggressive store deletion on startup +- [x] **Custom symbol assets** - Brand icons in asset catalog with proper rendering +- [x] **Drag-and-drop reordering** - Using `draggable` and `dropDestination` modifiers ### Planned @@ -85,6 +121,7 @@ This document tracks planned features and their implementation status. - [ ] **Spotlight indexing** - Search cards from iOS search - [ ] **Siri shortcuts** - "Share my work card" - [ ] **App Intents** - iOS 17+ action button support +- [ ] **Migrate legacy properties** - Move all cards to new ContactField array --- @@ -93,7 +130,8 @@ This document tracks planned features and their implementation status. - Features marked with 🔲 are planned but not yet implemented - Features requiring backend are deferred until infrastructure is available - Priority may shift based on user feedback +- Legacy contact properties (email, phone, linkedIn, etc.) maintained for backward compatibility --- -*Last updated: January 2026* +*Last updated: January 8, 2026* diff --git a/ai_implmentation.md b/ai_implmentation.md index 7215953..94dc1b3 100644 --- a/ai_implmentation.md +++ b/ai_implmentation.md @@ -48,11 +48,29 @@ App-specific extensions are in `Design/DesignConstants.swift`: ### Models - `Models/BusinessCard.swift` — SwiftData model with: - - Basic fields: name, role, company, email, phone, website, location - - Rich fields: pronouns, bio, social links (LinkedIn, Twitter, Instagram, etc.) - - Custom links: 2 slots for custom URLs - - Photo: `photoData` stored with `@Attribute(.externalStorage)` - - Computed: `theme`, `layoutStyle`, `vCardPayload`, `hasSocialLinks` + - Name fields: prefix, firstName, middleName, lastName, maidenName, suffix, preferredName + - Basic fields: role, company, email, phone, website, location + - Rich fields: pronouns, bio, headline, accreditations (comma-separated tags) + - **Dynamic contact fields**: `@Relationship` to array of `ContactField` objects + - Legacy fields: social links (LinkedIn, Twitter, etc.) for backward compatibility + - Photo: `photoData` and `logoData` stored with `@Attribute(.externalStorage)` + - Computed: `theme`, `layoutStyle`, `vCardPayload`, `hasSocialLinks`, `orderedContactFields` + - Helper methods: `addContactField`, `removeContactField`, `reorderContactFields` + +- `Models/ContactField.swift` — SwiftData model for dynamic contact fields: + - Properties: `typeId`, `value`, `title`, `orderIndex` + - Relationship: `card` (inverse to BusinessCard) + - Computed: `fieldType`, `displayName`, `iconImage()`, `iconColor`, `buildURL()` + - Supports multiple fields of same type, drag-to-reorder + +- `Models/ContactFieldType.swift` — Struct defining 30+ field types: + - Categories: contact, social, developer, messaging, payment, creator, scheduling, other + - Properties: `id`, `displayName`, `systemImage`, `isCustomSymbol`, `iconColor` + - Properties: `valueLabel`, `valuePlaceholder`, `titleSuggestions`, `keyboardType` + - Method: `iconImage()` — returns correct Image (asset vs SF Symbol) + - Method: `urlBuilder` — closure to build deep link URLs + - Static instances: `.email`, `.phone`, `.linkedIn`, `.twitter`, `.instagram`, etc. + - Uses custom assets from `Assets.xcassets/SocialSymbols/` - `Models/Contact.swift` — SwiftData model with: - Basic fields: name, role, company @@ -86,10 +104,9 @@ App-specific extensions are in `Design/DesignConstants.swift`: ### Views Main screens: -- `Views/RootTabView.swift` — tabbed shell -- `Views/CardsHomeView.swift` — hero + card carousel +- `Views/RootTabView.swift` — tabbed shell (4 tabs: My Cards, Share, Contacts, Widgets) +- `Views/CardsHomeView.swift` — full-screen swipeable card view with edit button - `Views/ShareCardView.swift` — QR + share actions + track share -- `Views/CustomizeCardView.swift` — theme/layout controls - `Views/ContactsView.swift` — contact list with sections - `Views/WidgetsView.swift` — widget preview mockups @@ -105,16 +122,31 @@ Reusable components (in `Views/Components/`): - `IconRowView.swift` — icon + text row for details - `LabelBadgeView.swift` — small badge labels - `ActionRowView.swift` — generic action row with chevron +- `ContactFieldPickerView.swift` — grid picker for selecting contact field types +- `ContactFieldsManagerView.swift` — orchestrates picker + added fields list +- `AddedContactFieldsView.swift` — displays added fields with drag-to-reorder Sheets (in `Views/Sheets/`): - `RecordContactSheet.swift` — track share recipient +- `ContactFieldEditorSheet.swift` — add/edit contact field with type-specific UI Small utilities: - `Views/EmptyStateView.swift` — empty state placeholder - `Views/PrimaryActionButton.swift` — styled action button -- `Views/HeroBannerView.swift` — home page banner -- `Views/CardCarouselView.swift` — card scroll carousel -- `Views/WidgetsCalloutView.swift` — widget promotion callout + +### Assets + +- `Assets.xcassets/SocialSymbols/` — custom brand icons as `.symbolset` files: + - Social: linkedin, x-twitter, instagram, facebook, tiktok, threads, bluesky, mastodon, reddit, twitch + - Developer: github.fill + - Messaging: telegram, discord.fill, slack, matrix + - Creator: patreon.fill, ko-fi + +**Icon Rendering:** +- Custom symbols use `Image("symbolName")` (asset catalog) +- SF Symbols use `Image(systemName: "symbolName")` +- `ContactFieldType.isCustomSymbol` flag determines which to use +- `ContactFieldType.iconImage()` returns the correct Image type ### Design + Localization @@ -141,13 +173,16 @@ Small utilities: ### Current File Sizes | File | Lines | Status | |------|-------|--------| -| CardEditorView | ~420 | Complex form, acceptable | +| ContactFieldType | ~650 | 30+ field definitions, acceptable | +| CardEditorView | ~520 | Complex form + field manager, acceptable | +| BusinessCardView | ~320 | Clickable fields + legacy fallback, acceptable | | QRScannerView | ~310 | Camera + parsing, acceptable | -| BusinessCardView | ~245 | Multiple layouts, acceptable | | ShareCardView | ~235 | Good | | ContactDetailView | ~235 | Good | | ContactsView | ~220 | Good | -| CustomizeCardView | ~170 | Good | +| CardsHomeView | ~150 | Full-screen swipeable cards, good | +| AddedContactFieldsView | ~135 | Drag-to-reorder, good | +| ContactFieldPickerView | ~100 | Grid picker, good | | All others | <110 | Good | ## Localization