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

This commit is contained in:
Matt Bruce 2026-01-08 19:41:24 -06:00
parent 7fc0249846
commit d91190d66f
87 changed files with 153 additions and 40 deletions

View File

@ -33,6 +33,9 @@
}
}
}
},
"+1 555 123 4567" : {
},
"Add a QR widget so your card is always one tap away." : {
"localizations" : {
@ -153,6 +156,9 @@
}
}
}
},
"Example" : {
},
"Hold your phone near another device to share instantly. NFC setup is on the way." : {
"localizations" : {
@ -350,6 +356,9 @@
},
"Shared With" : {
},
"Tap \"New Card\" to create your first card" : {
},
"Tap to share" : {
"localizations" : {
@ -466,6 +475,21 @@
}
}
}
},
"Your card will appear here" : {
},
"Your Company" : {
},
"Your Name" : {
},
"Your Role" : {
},
"your@email.com" : {
}
},
"version" : "1.1"

View File

@ -12,13 +12,6 @@ final class CardStore: BusinessCardProviding {
init(modelContext: ModelContext) {
self.modelContext = modelContext
fetchCards()
if cards.isEmpty {
BusinessCard.createSamples(in: modelContext)
saveContext()
fetchCards()
}
self.selectedCardID = cards.first(where: { $0.isDefault })?.id ?? cards.first?.id
syncToWatch()
}

View File

@ -12,12 +12,6 @@ final class ContactsStore: ContactTracking {
init(modelContext: ModelContext) {
self.modelContext = modelContext
fetchContacts()
if contacts.isEmpty {
Contact.createSamples(in: modelContext)
saveContext()
fetchContacts()
}
}
func fetchContacts() {

View File

@ -7,29 +7,36 @@ struct CardCarouselView: View {
var body: some View {
@Bindable var cardStore = appState.cardStore
let hasCards = !cardStore.cards.isEmpty
VStack(spacing: Design.Spacing.medium) {
HStack {
Text("Create multiple business cards")
Text(hasCards ? "Create multiple business cards" : "Your card will appear here")
.font(.headline)
.bold()
.foregroundStyle(Color.Text.primary)
Spacer()
}
TabView(selection: $cardStore.selectedCardID) {
ForEach(cardStore.cards) { card in
BusinessCardView(card: card)
.tag(Optional(card.id))
.padding(.vertical, Design.Spacing.medium)
if hasCards {
TabView(selection: $cardStore.selectedCardID) {
ForEach(cardStore.cards) { card in
BusinessCardView(card: card)
.tag(Optional(card.id))
.padding(.vertical, Design.Spacing.medium)
}
}
}
.tabViewStyle(.page)
.frame(height: Design.CardSize.cardHeight + Design.Spacing.xxLarge)
.tabViewStyle(.page)
.frame(height: Design.CardSize.cardHeight + Design.Spacing.xxLarge)
if let selected = cardStore.selectedCard {
CardDefaultToggleView(card: selected) {
cardStore.setDefaultCard(selected)
if let selected = cardStore.selectedCard {
CardDefaultToggleView(card: selected) {
cardStore.setDefaultCard(selected)
}
}
} else {
DemoCardView()
.padding(.vertical, Design.Spacing.medium)
}
}
}
@ -51,6 +58,93 @@ private struct CardDefaultToggleView: View {
}
}
/// 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))

View File

@ -18,20 +18,29 @@ struct CardsHomeView: View {
CardCarouselView()
HStack(spacing: Design.Spacing.medium) {
PrimaryActionButton(
title: String.localized("Send my card"),
systemImage: "paperplane.fill"
) {
appState.selectedTab = .share
}
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
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"))
}
.buttonStyle(.bordered)
.tint(Color.Accent.ink)
.controlSize(.large)
.accessibilityHint(String.localized("Create a new business card"))
}
WidgetsCalloutView()

View File

@ -137,7 +137,6 @@ BusinessCardTests/ # Unit tests
- Share URLs are sample placeholders
- Wallet/NFC flows are stubs with alerts only
- Widget UI is a visual preview (not a WidgetKit extension)
- First launch creates sample cards for demonstration
## Running

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB