BusinessCard/BusinessCard/Views/BusinessCardView.swift

255 lines
8.2 KiB
Swift

import SwiftUI
import Bedrock
import SwiftData
struct BusinessCardView: View {
let card: BusinessCard
var body: some View {
VStack(spacing: Design.Spacing.medium) {
switch card.layoutStyle {
case .stacked:
StackedCardLayout(card: card)
case .split:
SplitCardLayout(card: card)
case .photo:
PhotoCardLayout(card: card)
}
}
.padding(Design.Spacing.large)
.frame(maxWidth: .infinity)
.background(
LinearGradient(
colors: [card.theme.primaryColor, card.theme.secondaryColor],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.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.displayName), \(card.role), \(card.company)")
}
}
// MARK: - Layout Variants
private struct StackedCardLayout: View {
let card: BusinessCard
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
CardHeaderView(card: card)
Divider()
.overlay(card.theme.textColor.opacity(Design.Opacity.medium))
CardDetailsView(card: card)
if card.hasSocialLinks {
SocialLinksRow(card: card)
}
}
}
}
private struct SplitCardLayout: View {
let card: BusinessCard
var body: some View {
HStack(spacing: Design.Spacing.large) {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
CardHeaderView(card: card)
CardDetailsView(card: card)
if card.hasSocialLinks {
SocialLinksRow(card: card)
}
}
Spacer(minLength: Design.Spacing.medium)
AccentBlockView(color: card.theme.accentColor, textColor: card.theme.textColor)
}
}
}
private struct PhotoCardLayout: View {
let card: BusinessCard
var body: some View {
HStack(spacing: Design.Spacing.large) {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
CardHeaderView(card: card)
CardDetailsView(card: card)
if card.hasSocialLinks {
SocialLinksRow(card: card)
}
}
Spacer(minLength: Design.Spacing.medium)
AvatarBadgeView(
systemName: card.avatarSystemName,
accentColor: card.theme.accentColor,
photoData: card.photoData,
borderColor: card.theme.textColor
)
}
}
}
// MARK: - Card Sections
private struct CardHeaderView: View {
let card: BusinessCard
private var textColor: Color { card.theme.textColor }
var body: some View {
HStack(spacing: Design.Spacing.medium) {
AvatarBadgeView(
systemName: card.avatarSystemName,
accentColor: card.theme.accentColor,
photoData: card.photoData,
borderColor: textColor
)
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
HStack(spacing: Design.Spacing.xSmall) {
Text(card.displayName)
.font(.headline)
.bold()
.foregroundStyle(textColor)
if !card.pronouns.isEmpty {
Text("(\(card.pronouns))")
.font(.caption)
.foregroundStyle(textColor.opacity(Design.Opacity.strong))
}
}
Text(card.role)
.font(.subheadline)
.foregroundStyle(textColor.opacity(Design.Opacity.almostFull))
Text(card.company)
.font(.caption)
.foregroundStyle(textColor.opacity(Design.Opacity.medium))
}
Spacer(minLength: Design.Spacing.small)
LabelBadgeView(label: card.label, accentColor: card.theme.accentColor, textColor: textColor)
}
}
}
private struct CardDetailsView: View {
let card: BusinessCard
private var textColor: Color { card.theme.textColor }
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
if !card.email.isEmpty {
IconRowView(systemImage: "envelope", text: card.email, textColor: textColor)
}
if !card.phone.isEmpty {
IconRowView(systemImage: "phone", text: card.phone, textColor: textColor)
}
if !card.website.isEmpty {
IconRowView(systemImage: "link", text: card.website, textColor: textColor)
}
if !card.bio.isEmpty {
Text(card.bio)
.font(.caption)
.foregroundStyle(textColor.opacity(Design.Opacity.strong))
.lineLimit(2)
.padding(.top, Design.Spacing.xxSmall)
}
}
}
}
private struct SocialLinksRow: View {
let card: BusinessCard
private var textColor: Color { card.theme.textColor }
var body: some View {
HStack(spacing: Design.Spacing.small) {
if !card.linkedIn.isEmpty {
SocialIconView(systemImage: "link", textColor: textColor)
}
if !card.twitter.isEmpty {
SocialIconView(systemImage: "at", textColor: textColor)
}
if !card.instagram.isEmpty {
SocialIconView(systemImage: "camera", textColor: textColor)
}
if !card.facebook.isEmpty {
SocialIconView(systemImage: "person.2", textColor: textColor)
}
if !card.tiktok.isEmpty {
SocialIconView(systemImage: "play.rectangle", textColor: textColor)
}
if !card.github.isEmpty {
SocialIconView(systemImage: "chevron.left.forwardslash.chevron.right", textColor: textColor)
}
}
.padding(.top, Design.Spacing.xxSmall)
}
}
// MARK: - Small Components
private struct SocialIconView: View {
let systemImage: String
var textColor: Color = Color.Text.inverted
var body: some View {
Image(systemName: systemImage)
.font(.caption2)
.foregroundStyle(textColor.opacity(Design.Opacity.strong))
.frame(width: Design.Spacing.xLarge, height: Design.Spacing.xLarge)
.background(textColor.opacity(Design.Opacity.hint))
.clipShape(.circle)
}
}
private struct AccentBlockView: View {
let color: Color
var textColor: Color = Color.Text.inverted
var body: some View {
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
.fill(color)
.frame(width: Design.CardSize.avatarSize, height: Design.CardSize.avatarSize)
.overlay(
Image(systemName: "bolt.fill")
.foregroundStyle(textColor)
)
}
}
// MARK: - Preview
#Preview {
let container = try! ModelContainer(for: BusinessCard.self, Contact.self)
let context = container.mainContext
let card = BusinessCard(
displayName: "Daniel Sullivan",
role: "Property Developer",
company: "WR Construction",
email: "daniel@example.com",
phone: "+1 555 123 4567",
website: "example.com",
location: "Dallas, TX",
themeName: "Coral",
layoutStyleRawValue: "split",
pronouns: "he/him",
bio: "Building the future of Dallas real estate",
linkedIn: "linkedin.com/in/daniel",
twitter: "twitter.com/daniel"
)
context.insert(card)
return BusinessCardView(card: card)
.padding()
.background(Color.AppBackground.base)
}