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

View File

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

View File

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

View File

@ -7,15 +7,18 @@ struct CardCarouselView: View {
var body: some View { var body: some View {
@Bindable var cardStore = appState.cardStore @Bindable var cardStore = appState.cardStore
let hasCards = !cardStore.cards.isEmpty
VStack(spacing: Design.Spacing.medium) { VStack(spacing: Design.Spacing.medium) {
HStack { HStack {
Text("Create multiple business cards") Text(hasCards ? "Create multiple business cards" : "Your card will appear here")
.font(.headline) .font(.headline)
.bold() .bold()
.foregroundStyle(Color.Text.primary) .foregroundStyle(Color.Text.primary)
Spacer() Spacer()
} }
if hasCards {
TabView(selection: $cardStore.selectedCardID) { TabView(selection: $cardStore.selectedCardID) {
ForEach(cardStore.cards) { card in ForEach(cardStore.cards) { card in
BusinessCardView(card: card) BusinessCardView(card: card)
@ -31,6 +34,10 @@ struct CardCarouselView: View {
cardStore.setDefaultCard(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 { #Preview {
CardCarouselView() CardCarouselView()
.environment(AppState(modelContext: try! ModelContainer(for: BusinessCard.self, Contact.self).mainContext)) .environment(AppState(modelContext: try! ModelContainer(for: BusinessCard.self, Contact.self).mainContext))

View File

@ -18,6 +18,14 @@ struct CardsHomeView: View {
CardCarouselView() CardCarouselView()
HStack(spacing: Design.Spacing.medium) { HStack(spacing: Design.Spacing.medium) {
if appState.cardStore.cards.isEmpty {
PrimaryActionButton(
title: String.localized("Create Card"),
systemImage: "plus"
) {
showingCreateCard = true
}
} else {
PrimaryActionButton( PrimaryActionButton(
title: String.localized("Send my card"), title: String.localized("Send my card"),
systemImage: "paperplane.fill" systemImage: "paperplane.fill"
@ -33,6 +41,7 @@ struct CardsHomeView: View {
.controlSize(.large) .controlSize(.large)
.accessibilityHint(String.localized("Create a new business card")) .accessibilityHint(String.localized("Create a new business card"))
} }
}
WidgetsCalloutView() WidgetsCalloutView()
} }

View File

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