BusinessCard/BusinessCard/Views/BusinessCardView.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)
}