301 lines
9.7 KiB
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)
|
|
}
|