312 lines
10 KiB
Swift
312 lines
10 KiB
Swift
import SwiftUI
|
|
import Bedrock
|
|
import SwiftData
|
|
|
|
struct BusinessCardView: View {
|
|
let card: BusinessCard
|
|
var isCompact: Bool = false
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
// Banner with logo
|
|
CardBannerView(card: card)
|
|
|
|
// Content area with avatar overlapping
|
|
CardContentView(card: card, isCompact: isCompact)
|
|
.offset(y: -Design.CardSize.avatarOverlap)
|
|
.padding(.bottom, -Design.CardSize.avatarOverlap)
|
|
}
|
|
.background(Color.AppBackground.elevated)
|
|
.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
|
|
)
|
|
.accessibilityElement(children: .ignore)
|
|
.accessibilityLabel(String.localized("Business card"))
|
|
.accessibilityValue("\(card.effectiveDisplayName), \(card.role), \(card.company)")
|
|
}
|
|
}
|
|
|
|
// MARK: - Banner View
|
|
|
|
private struct CardBannerView: View {
|
|
let card: BusinessCard
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Gradient background
|
|
LinearGradient(
|
|
colors: [card.theme.primaryColor, card.theme.secondaryColor],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
|
|
// Company logo
|
|
if let logoData = card.logoData, let uiImage = UIImage(data: logoData) {
|
|
Image(uiImage: uiImage)
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(height: Design.CardSize.logoSize)
|
|
} else if !card.company.isEmpty {
|
|
Text(card.company.prefix(1).uppercased())
|
|
.font(.system(size: Design.BaseFontSize.display, weight: .bold, design: .rounded))
|
|
.foregroundStyle(card.theme.textColor.opacity(Design.Opacity.medium))
|
|
}
|
|
}
|
|
.frame(height: Design.CardSize.bannerHeight)
|
|
}
|
|
}
|
|
|
|
// MARK: - Content View
|
|
|
|
private struct CardContentView: View {
|
|
let card: BusinessCard
|
|
let isCompact: Bool
|
|
|
|
private var textColor: Color { Color.Text.primary }
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
|
|
// Avatar and label row
|
|
HStack(alignment: .bottom) {
|
|
ProfileAvatarView(card: card)
|
|
Spacer()
|
|
LabelBadgeView(label: card.label, accentColor: card.theme.accentColor, textColor: card.theme.textColor)
|
|
.padding(.bottom, Design.CardSize.avatarOverlap)
|
|
}
|
|
|
|
// Name and title
|
|
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
|
HStack(spacing: Design.Spacing.xSmall) {
|
|
Text(card.effectiveDisplayName)
|
|
.font(.title2)
|
|
.bold()
|
|
.foregroundStyle(textColor)
|
|
|
|
if !card.pronouns.isEmpty {
|
|
Text("(\(card.pronouns))")
|
|
.font(.subheadline)
|
|
.foregroundStyle(Color.Text.secondary)
|
|
}
|
|
}
|
|
|
|
Text(card.role)
|
|
.font(.headline)
|
|
.foregroundStyle(textColor)
|
|
|
|
Text(card.company)
|
|
.font(.subheadline)
|
|
.foregroundStyle(Color.Text.secondary)
|
|
|
|
if !card.headline.isEmpty {
|
|
Text(card.headline)
|
|
.font(.caption)
|
|
.foregroundStyle(Color.Text.secondary)
|
|
.padding(.top, Design.Spacing.xxSmall)
|
|
}
|
|
}
|
|
|
|
if !isCompact {
|
|
Divider()
|
|
.padding(.vertical, Design.Spacing.xSmall)
|
|
|
|
// Contact details
|
|
ContactDetailsView(card: card)
|
|
|
|
// Social links
|
|
if card.hasSocialLinks {
|
|
SocialLinksRow(card: card)
|
|
.padding(.top, Design.Spacing.xSmall)
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, Design.Spacing.large)
|
|
.padding(.bottom, Design.Spacing.large)
|
|
}
|
|
}
|
|
|
|
// MARK: - Profile Avatar
|
|
|
|
private struct ProfileAvatarView: View {
|
|
let card: BusinessCard
|
|
|
|
var body: some View {
|
|
Group {
|
|
if let photoData = card.photoData, let uiImage = UIImage(data: photoData) {
|
|
Image(uiImage: uiImage)
|
|
.resizable()
|
|
.scaledToFill()
|
|
} else {
|
|
Image(systemName: card.avatarSystemName)
|
|
.font(.system(size: Design.BaseFontSize.title))
|
|
.foregroundStyle(card.theme.textColor)
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
.background(card.theme.accentColor)
|
|
}
|
|
}
|
|
.frame(width: Design.CardSize.avatarLarge, height: Design.CardSize.avatarLarge)
|
|
.clipShape(.circle)
|
|
.overlay(
|
|
Circle()
|
|
.stroke(Color.AppBackground.elevated, lineWidth: Design.LineWidth.thick)
|
|
)
|
|
.shadow(
|
|
color: Color.Text.secondary.opacity(Design.Opacity.hint),
|
|
radius: Design.Shadow.radiusSmall,
|
|
x: Design.Shadow.offsetNone,
|
|
y: Design.Shadow.offsetSmall
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Contact Details
|
|
|
|
private struct ContactDetailsView: View {
|
|
let card: BusinessCard
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
|
if !card.email.isEmpty {
|
|
ContactRowView(
|
|
systemImage: "envelope.fill",
|
|
text: card.email,
|
|
label: card.emailLabel
|
|
)
|
|
}
|
|
if !card.phone.isEmpty {
|
|
ContactRowView(
|
|
systemImage: "phone.fill",
|
|
text: card.phoneExtension.isEmpty ? card.phone : "\(card.phone) ext. \(card.phoneExtension)",
|
|
label: card.phoneLabel
|
|
)
|
|
}
|
|
if !card.website.isEmpty {
|
|
ContactRowView(
|
|
systemImage: "link",
|
|
text: card.website,
|
|
label: nil
|
|
)
|
|
}
|
|
if !card.location.isEmpty {
|
|
ContactRowView(
|
|
systemImage: "location.fill",
|
|
text: card.location,
|
|
label: nil
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct ContactRowView: View {
|
|
let systemImage: String
|
|
let text: String
|
|
let label: String?
|
|
|
|
var body: some View {
|
|
HStack(spacing: Design.Spacing.medium) {
|
|
Image(systemName: systemImage)
|
|
.font(.body)
|
|
.foregroundStyle(Color.Accent.red)
|
|
.frame(width: Design.CardSize.socialIconSize, height: Design.CardSize.socialIconSize)
|
|
.background(Color.AppBackground.accent)
|
|
.clipShape(.circle)
|
|
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
Text(text)
|
|
.font(.subheadline)
|
|
.foregroundStyle(Color.Text.primary)
|
|
|
|
if let label, !label.isEmpty {
|
|
Text(label)
|
|
.font(.caption)
|
|
.foregroundStyle(Color.Text.secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Social Links
|
|
|
|
private struct SocialLinksRow: View {
|
|
let card: BusinessCard
|
|
|
|
var body: some View {
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(spacing: Design.Spacing.small) {
|
|
if !card.linkedIn.isEmpty {
|
|
SocialIconButton(name: "LinkedIn", color: Color.Social.linkedIn)
|
|
}
|
|
if !card.twitter.isEmpty {
|
|
SocialIconButton(name: "X", color: Color.Social.twitter)
|
|
}
|
|
if !card.instagram.isEmpty {
|
|
SocialIconButton(name: "IG", color: Color.Social.instagram)
|
|
}
|
|
if !card.facebook.isEmpty {
|
|
SocialIconButton(name: "FB", color: Color.Social.facebook)
|
|
}
|
|
if !card.tiktok.isEmpty {
|
|
SocialIconButton(name: "TT", color: Color.Social.tiktok)
|
|
}
|
|
if !card.github.isEmpty {
|
|
SocialIconButton(name: "GH", color: Color.Social.github)
|
|
}
|
|
if !card.threads.isEmpty {
|
|
SocialIconButton(name: "TH", color: Color.Social.threads)
|
|
}
|
|
if !card.telegram.isEmpty {
|
|
SocialIconButton(name: "TG", color: Color.Social.telegram)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct SocialIconButton: View {
|
|
let name: String
|
|
let color: Color
|
|
|
|
var body: some View {
|
|
Text(name)
|
|
.font(.caption2)
|
|
.bold()
|
|
.foregroundStyle(.white)
|
|
.frame(width: Design.CardSize.socialIconSize, height: Design.CardSize.socialIconSize)
|
|
.background(color)
|
|
.clipShape(.circle)
|
|
}
|
|
}
|
|
|
|
// MARK: - Preview
|
|
|
|
#Preview {
|
|
let container = try! ModelContainer(for: BusinessCard.self, Contact.self)
|
|
let context = container.mainContext
|
|
let card = BusinessCard(
|
|
displayName: "Matt Bruce",
|
|
role: "Lead iOS Developer",
|
|
company: "Toyota",
|
|
email: "matt.bruce@toyota.com",
|
|
emailLabel: "Work",
|
|
phone: "+1 (214) 755-1043",
|
|
phoneLabel: "Cell",
|
|
website: "toyota.com",
|
|
location: "Dallas, TX",
|
|
themeName: "Coral",
|
|
layoutStyleRawValue: "stacked",
|
|
headline: "Building the future of mobility",
|
|
linkedIn: "linkedin.com/in/mattbruce",
|
|
twitter: "twitter.com/mattbruce"
|
|
)
|
|
context.insert(card)
|
|
|
|
return BusinessCardView(card: card)
|
|
.padding()
|
|
.background(Color.AppBackground.base)
|
|
}
|