270 lines
10 KiB
Swift
270 lines
10 KiB
Swift
import SwiftUI
|
|
import Bedrock
|
|
|
|
/// Displays a preview of the shared card with option to save to Contacts.
|
|
struct ClipCardPreview: View {
|
|
private struct ContactSection {
|
|
let title: String
|
|
let rows: [SharedCardSnapshot.ContactInfoRow]
|
|
}
|
|
|
|
let snapshot: SharedCardSnapshot
|
|
let onSave: () -> Void
|
|
|
|
var body: some View {
|
|
VStack(spacing: ClipDesign.Spacing.xLarge) {
|
|
Spacer(minLength: ClipDesign.Spacing.medium)
|
|
|
|
previewCard
|
|
.frame(maxWidth: ClipDesign.Size.previewMaxWidth)
|
|
.padding(.horizontal, ClipDesign.Spacing.large)
|
|
|
|
VStack(spacing: ClipDesign.Spacing.medium) {
|
|
// Save button
|
|
ClipPrimaryButton(
|
|
title: String(localized: "Save to Contacts"),
|
|
systemImage: "person.crop.circle.badge.plus",
|
|
action: onSave
|
|
)
|
|
.accessibilityLabel(Text("Save \(snapshot.displayName) to contacts"))
|
|
|
|
// Get full app prompt
|
|
Button {
|
|
openAppStore()
|
|
} label: {
|
|
Text(String(localized: "Get the full app"))
|
|
.styled(.subheading)
|
|
.foregroundStyle(Color.Clip.accent)
|
|
}
|
|
}
|
|
.padding(.horizontal, ClipDesign.Spacing.xLarge)
|
|
|
|
Spacer(minLength: ClipDesign.Spacing.large)
|
|
}
|
|
}
|
|
|
|
private var previewCard: some View {
|
|
VStack(spacing: 0) {
|
|
previewHeader
|
|
|
|
VStack(alignment: .leading, spacing: ClipDesign.Spacing.medium) {
|
|
Text(snapshot.displayName)
|
|
.styled(.title2)
|
|
.foregroundStyle(Color.Clip.text)
|
|
.multilineTextAlignment(.center)
|
|
.lineLimit(2)
|
|
.minimumScaleFactor(0.8)
|
|
.frame(maxWidth: .infinity, alignment: .center)
|
|
|
|
if !snapshot.role.isEmpty || !snapshot.company.isEmpty {
|
|
VStack(spacing: ClipDesign.Spacing.xSmall) {
|
|
if !snapshot.role.isEmpty {
|
|
Text(snapshot.role)
|
|
.styled(.headingEmphasis)
|
|
.foregroundStyle(Color.Clip.text)
|
|
.multilineTextAlignment(.center)
|
|
.lineLimit(1)
|
|
.frame(maxWidth: .infinity, alignment: .center)
|
|
}
|
|
|
|
if !snapshot.company.isEmpty {
|
|
Text(snapshot.company)
|
|
.styled(.subheading)
|
|
.foregroundStyle(Color.Clip.secondaryText)
|
|
.multilineTextAlignment(.center)
|
|
.lineLimit(1)
|
|
.frame(maxWidth: .infinity, alignment: .center)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .center)
|
|
}
|
|
|
|
Divider()
|
|
.overlay(Color.Clip.secondaryText.opacity(ClipDesign.Opacity.faint))
|
|
|
|
if !contactSections.isEmpty {
|
|
contactSectionsView
|
|
|
|
Divider()
|
|
.overlay(Color.Clip.secondaryText.opacity(ClipDesign.Opacity.faint))
|
|
}
|
|
|
|
Text(String(localized: "Shared business card"))
|
|
.styled(.caption)
|
|
.foregroundStyle(Color.Clip.secondaryText)
|
|
.frame(maxWidth: .infinity, alignment: .center)
|
|
}
|
|
.padding(.horizontal, ClipDesign.Spacing.xLarge)
|
|
.padding(.bottom, ClipDesign.Spacing.xLarge)
|
|
}
|
|
.frame(minHeight: ClipDesign.Size.previewCardMinHeight)
|
|
.background(Color.Clip.cardBackground)
|
|
.clipShape(.rect(cornerRadius: ClipDesign.CornerRadius.xLarge))
|
|
.shadow(
|
|
color: Color.Clip.text.opacity(ClipDesign.Opacity.faint),
|
|
radius: ClipDesign.Shadow.radius,
|
|
x: 0,
|
|
y: ClipDesign.Shadow.y
|
|
)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: ClipDesign.CornerRadius.xLarge)
|
|
.stroke(Color.Clip.secondaryText.opacity(ClipDesign.Opacity.subtle), lineWidth: ClipDesign.Size.cardStrokeWidth)
|
|
)
|
|
}
|
|
|
|
private var contactSectionsView: some View {
|
|
VStack(alignment: .leading, spacing: ClipDesign.Spacing.medium) {
|
|
ForEach(contactSections, id: \.title) { section in
|
|
VStack(alignment: .leading, spacing: ClipDesign.Spacing.xSmall) {
|
|
Text(section.title.uppercased())
|
|
.styled(.caption)
|
|
.foregroundStyle(Color.Clip.secondaryText)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
|
|
VStack(spacing: 0) {
|
|
ForEach(Array(section.rows.enumerated()), id: \.element.id) { index, row in
|
|
contactRow(row)
|
|
.padding(.horizontal, ClipDesign.Spacing.medium)
|
|
.padding(.vertical, ClipDesign.Spacing.small)
|
|
|
|
if index < section.rows.count - 1 {
|
|
Divider()
|
|
.overlay(Color.Clip.secondaryText.opacity(ClipDesign.Opacity.faint))
|
|
.padding(.leading, ClipDesign.Spacing.medium)
|
|
}
|
|
}
|
|
}
|
|
.background(Color.Clip.background.opacity(ClipDesign.Opacity.faint))
|
|
.clipShape(.rect(cornerRadius: ClipDesign.CornerRadius.medium))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: ClipDesign.CornerRadius.medium)
|
|
.stroke(Color.Clip.secondaryText.opacity(ClipDesign.Opacity.faint), lineWidth: ClipDesign.Size.cardStrokeWidth)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
|
|
private func contactRow(_ row: SharedCardSnapshot.ContactInfoRow) -> some View {
|
|
HStack(alignment: .top, spacing: ClipDesign.Spacing.small) {
|
|
Image(systemName: row.systemImage)
|
|
.font(.system(size: ClipDesign.Size.contactRowIconSize, weight: .semibold))
|
|
.foregroundStyle(Color.Clip.secondaryText)
|
|
.frame(width: ClipDesign.Size.contactRowIconSize + ClipDesign.Spacing.small)
|
|
|
|
VStack(alignment: .leading, spacing: ClipDesign.Spacing.xSmall) {
|
|
Text(row.value)
|
|
.styled(.subheading)
|
|
.foregroundStyle(Color.Clip.text)
|
|
.lineLimit(row.kind == .note ? nil : 2)
|
|
.truncationMode(.tail)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
|
|
Text(row.label)
|
|
.styled(.caption)
|
|
.foregroundStyle(Color.Clip.secondaryText)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
|
|
private var contactSections: [ContactSection] {
|
|
let groups = Dictionary(grouping: snapshot.contactInfoRows, by: \.kind)
|
|
let order: [(SharedCardSnapshot.ContactInfoRow.Kind, String)] = [
|
|
(.phone, String(localized: "Phone")),
|
|
(.email, String(localized: "Email")),
|
|
(.address, String(localized: "Address")),
|
|
(.website, String(localized: "Links")),
|
|
(.social, String(localized: "Social Profiles")),
|
|
(.note, String(localized: "Notes"))
|
|
]
|
|
|
|
return order.compactMap { kind, title in
|
|
guard let rows = groups[kind], !rows.isEmpty else { return nil }
|
|
return ContactSection(title: title, rows: rows)
|
|
}
|
|
}
|
|
|
|
private var previewHeader: some View {
|
|
ZStack(alignment: .bottom) {
|
|
LinearGradient(
|
|
colors: [
|
|
Color.Clip.accent.opacity(ClipDesign.Opacity.strong),
|
|
Color.Clip.accent.opacity(ClipDesign.Opacity.subtle)
|
|
],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
.frame(height: ClipDesign.Size.previewBannerHeight)
|
|
|
|
avatarView
|
|
.offset(y: ClipDesign.Size.previewAvatarOverlap)
|
|
}
|
|
.padding(.bottom, ClipDesign.Size.previewAvatarOverlap)
|
|
}
|
|
|
|
private var avatarView: some View {
|
|
ZStack {
|
|
Circle()
|
|
.fill(Color.Clip.background.opacity(ClipDesign.Opacity.medium))
|
|
|
|
if let photoData = snapshot.photoData, let uiImage = UIImage(data: photoData) {
|
|
Image(uiImage: uiImage)
|
|
.resizable()
|
|
.scaledToFill()
|
|
} else {
|
|
Image(systemName: "person.fill")
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: ClipDesign.Size.avatarFallbackSymbolSize, height: ClipDesign.Size.avatarFallbackSymbolSize)
|
|
.foregroundStyle(Color.Clip.secondaryText)
|
|
}
|
|
}
|
|
.frame(width: ClipDesign.Size.previewAvatarSize, height: ClipDesign.Size.previewAvatarSize)
|
|
.clipShape(.circle)
|
|
.overlay(
|
|
Circle()
|
|
.stroke(Color.Clip.cardBackground, lineWidth: ClipDesign.Size.avatarStrokeWidth)
|
|
)
|
|
.shadow(
|
|
color: Color.Clip.text.opacity(ClipDesign.Opacity.faint),
|
|
radius: ClipDesign.Shadow.radius,
|
|
x: 0,
|
|
y: ClipDesign.Shadow.y
|
|
)
|
|
}
|
|
|
|
private func openAppStore() {
|
|
if let url = URL(string: ClipDesign.URL.appStore) {
|
|
UIApplication.shared.open(url)
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
ZStack {
|
|
Color.Clip.background
|
|
.ignoresSafeArea()
|
|
|
|
ClipCardPreview(
|
|
snapshot: SharedCardSnapshot(
|
|
recordName: "test",
|
|
vCardData: """
|
|
BEGIN:VCARD
|
|
VERSION:3.0
|
|
N:Sullivan;Daniel;;;
|
|
FN:Daniel Sullivan
|
|
ORG:WR Construction
|
|
TITLE:Property Developer
|
|
END:VCARD
|
|
"""
|
|
)
|
|
) {
|
|
print("Save tapped")
|
|
}
|
|
}
|
|
}
|