BusinessCard/BusinessCard/Views/BusinessCardView.swift

301 lines
9.7 KiB
Swift

import SwiftUI
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)")
}
}
private struct StackedCardLayout: View {
let card: BusinessCard
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
CardHeaderView(card: card)
Divider()
.overlay(Color.Text.inverted.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)
CardAccentBlockView(color: card.theme.accentColor)
}
}
}
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)
CardAvatarBadgeView(
systemName: card.avatarSystemName,
accentColor: card.theme.accentColor,
photoData: card.photoData
)
}
}
}
private struct CardHeaderView: View {
let card: BusinessCard
var body: some View {
HStack(spacing: Design.Spacing.medium) {
CardAvatarBadgeView(
systemName: card.avatarSystemName,
accentColor: card.theme.accentColor,
photoData: card.photoData
)
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
HStack(spacing: Design.Spacing.xSmall) {
Text(card.displayName)
.font(.headline)
.bold()
.foregroundStyle(Color.Text.inverted)
if !card.pronouns.isEmpty {
Text("(\(card.pronouns))")
.font(.caption)
.foregroundStyle(Color.Text.inverted.opacity(Design.Opacity.strong))
}
}
Text(card.role)
.font(.subheadline)
.foregroundStyle(Color.Text.inverted.opacity(Design.Opacity.almostFull))
Text(card.company)
.font(.caption)
.foregroundStyle(Color.Text.inverted.opacity(Design.Opacity.medium))
}
Spacer(minLength: Design.Spacing.small)
CardLabelBadgeView(label: card.label, accentColor: card.theme.accentColor)
}
}
}
private struct CardDetailsView: View {
let card: BusinessCard
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
if !card.email.isEmpty {
InfoRowView(systemImage: "envelope", text: card.email)
}
if !card.phone.isEmpty {
InfoRowView(systemImage: "phone", text: card.phone)
}
if !card.website.isEmpty {
InfoRowView(systemImage: "link", text: card.website)
}
if !card.bio.isEmpty {
Text(card.bio)
.font(.caption)
.foregroundStyle(Color.Text.inverted.opacity(Design.Opacity.strong))
.lineLimit(2)
.padding(.top, Design.Spacing.xxSmall)
}
}
}
}
private struct SocialLinksRow: View {
let card: BusinessCard
var body: some View {
HStack(spacing: Design.Spacing.small) {
if !card.linkedIn.isEmpty {
SocialIconView(systemImage: "link")
}
if !card.twitter.isEmpty {
SocialIconView(systemImage: "at")
}
if !card.instagram.isEmpty {
SocialIconView(systemImage: "camera")
}
if !card.facebook.isEmpty {
SocialIconView(systemImage: "person.2")
}
if !card.tiktok.isEmpty {
SocialIconView(systemImage: "play.rectangle")
}
if !card.github.isEmpty {
SocialIconView(systemImage: "chevron.left.forwardslash.chevron.right")
}
}
.padding(.top, Design.Spacing.xxSmall)
}
}
private struct SocialIconView: View {
let systemImage: String
var body: some View {
Image(systemName: systemImage)
.font(.caption2)
.foregroundStyle(Color.Text.inverted.opacity(Design.Opacity.strong))
.frame(width: Design.Spacing.xLarge, height: Design.Spacing.xLarge)
.background(Color.Text.inverted.opacity(Design.Opacity.hint))
.clipShape(.circle)
}
}
private struct InfoRowView: View {
let systemImage: String
let text: String
var body: some View {
HStack(spacing: Design.Spacing.xSmall) {
Image(systemName: systemImage)
.font(.caption)
.foregroundStyle(Color.Text.inverted.opacity(Design.Opacity.heavy))
Text(text)
.font(.caption)
.foregroundStyle(Color.Text.inverted)
.lineLimit(1)
}
}
}
private struct CardAccentBlockView: View {
let color: Color
var body: some View {
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
.fill(color)
.frame(width: Design.Size.avatarSize, height: Design.Size.avatarSize)
.overlay(
Image(systemName: "bolt.fill")
.foregroundStyle(Color.Text.inverted)
)
}
}
private struct CardAvatarBadgeView: View {
let systemName: String
let accentColor: Color
let photoData: Data?
var body: some View {
if let photoData, let uiImage = UIImage(data: photoData) {
Image(uiImage: uiImage)
.resizable()
.scaledToFill()
.frame(width: Design.Size.avatarSize, height: Design.Size.avatarSize)
.clipShape(.circle)
.overlay(
Circle()
.stroke(Color.Text.inverted.opacity(Design.Opacity.medium), lineWidth: Design.LineWidth.thin)
)
} else {
Circle()
.fill(Color.Text.inverted)
.frame(width: Design.Size.avatarSize, height: Design.Size.avatarSize)
.overlay(
Image(systemName: systemName)
.foregroundStyle(accentColor)
)
.overlay(
Circle()
.stroke(Color.Text.inverted.opacity(Design.Opacity.medium), lineWidth: Design.LineWidth.thin)
)
}
}
}
private struct CardLabelBadgeView: View {
let label: String
let accentColor: Color
var body: some View {
Text(String.localized(label))
.font(.caption)
.bold()
.foregroundStyle(Color.Text.inverted)
.padding(.horizontal, Design.Spacing.small)
.padding(.vertical, Design.Spacing.xxSmall)
.background(accentColor.opacity(Design.Opacity.medium))
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
}
}
#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)
}