693 lines
24 KiB
Swift
693 lines
24 KiB
Swift
import SwiftUI
|
|
|
|
/// Defines a contact field type with all its configuration
|
|
struct ContactFieldType: Identifiable, Hashable, Sendable {
|
|
let id: String
|
|
let displayName: String
|
|
let systemImage: String
|
|
let isCustomSymbol: Bool // true = asset catalog symbol, false = SF Symbol
|
|
let iconColor: Color
|
|
let category: ContactFieldCategory
|
|
let valueLabel: String
|
|
let valuePlaceholder: String
|
|
let titleSuggestions: [String]
|
|
let keyboardType: UIKeyboardType
|
|
let autocapitalization: TextInputAutocapitalization
|
|
let urlBuilder: @Sendable (String) -> URL?
|
|
let displayValueFormatter: @Sendable (String) -> String
|
|
|
|
init(
|
|
id: String,
|
|
displayName: String,
|
|
systemImage: String,
|
|
isCustomSymbol: Bool = false,
|
|
iconColor: Color,
|
|
category: ContactFieldCategory,
|
|
valueLabel: String,
|
|
valuePlaceholder: String,
|
|
titleSuggestions: [String],
|
|
keyboardType: UIKeyboardType,
|
|
autocapitalization: TextInputAutocapitalization = .never,
|
|
urlBuilder: @escaping @Sendable (String) -> URL?,
|
|
displayValueFormatter: @escaping @Sendable (String) -> String = { $0 }
|
|
) {
|
|
self.id = id
|
|
self.displayName = displayName
|
|
self.systemImage = systemImage
|
|
self.isCustomSymbol = isCustomSymbol
|
|
self.iconColor = iconColor
|
|
self.category = category
|
|
self.valueLabel = valueLabel
|
|
self.valuePlaceholder = valuePlaceholder
|
|
self.titleSuggestions = titleSuggestions
|
|
self.keyboardType = keyboardType
|
|
self.autocapitalization = autocapitalization
|
|
self.urlBuilder = urlBuilder
|
|
self.displayValueFormatter = displayValueFormatter
|
|
}
|
|
|
|
/// Returns an Image view for this field type's icon
|
|
@MainActor
|
|
func iconImage() -> Image {
|
|
if isCustomSymbol {
|
|
return Image(systemImage)
|
|
} else {
|
|
return Image(systemName: systemImage)
|
|
}
|
|
}
|
|
|
|
// MARK: - Hashable & Equatable
|
|
|
|
static func == (lhs: ContactFieldType, rhs: ContactFieldType) -> Bool {
|
|
lhs.id == rhs.id
|
|
}
|
|
|
|
func hash(into hasher: inout Hasher) {
|
|
hasher.combine(id)
|
|
}
|
|
|
|
// MARK: - URL Building
|
|
|
|
func buildURL(value: String) -> URL? {
|
|
urlBuilder(value)
|
|
}
|
|
|
|
// MARK: - Display Value Formatting
|
|
|
|
/// Returns a formatted display value for the given raw value
|
|
func formattedDisplayValue(_ value: String) -> String {
|
|
displayValueFormatter(value)
|
|
}
|
|
}
|
|
|
|
// MARK: - All Field Types
|
|
|
|
extension ContactFieldType {
|
|
|
|
/// All available field types
|
|
static let allCases: [ContactFieldType] = [
|
|
// Contact
|
|
.phone, .email, .website, .address,
|
|
// Social
|
|
.linkedIn, .twitter, .instagram, .facebook, .tiktok, .threads,
|
|
.youtube, .snapchat, .pinterest, .twitch, .bluesky, .mastodon, .reddit,
|
|
// Developer
|
|
.github, .gitlab, .stackoverflow,
|
|
// Messaging
|
|
.telegram, .whatsapp, .signal, .discord, .slack, .matrix,
|
|
// Payment
|
|
.venmo, .cashApp, .paypal, .zelle,
|
|
// Creator
|
|
.patreon, .kofi,
|
|
// Scheduling
|
|
.calendly,
|
|
// Other
|
|
.customLink
|
|
]
|
|
|
|
/// Field types grouped by category
|
|
static var byCategory: [ContactFieldCategory: [ContactFieldType]] {
|
|
Dictionary(grouping: allCases, by: { $0.category })
|
|
}
|
|
|
|
|
|
// MARK: - Contact
|
|
|
|
static let phone = ContactFieldType(
|
|
id: "phone",
|
|
displayName: String(localized: "Phone Number"),
|
|
systemImage: "phone.fill",
|
|
iconColor: Color(red: 0.2, green: 0.2, blue: 0.2),
|
|
category: .contact,
|
|
valueLabel: String(localized: "Phone Number"),
|
|
valuePlaceholder: "+1 (555) 123-4567",
|
|
titleSuggestions: [String(localized: "Cell"), String(localized: "Work"), String(localized: "Home")],
|
|
keyboardType: .phonePad,
|
|
urlBuilder: { value in
|
|
let digits = value.filter { $0.isNumber || $0 == "+" }
|
|
return URL(string: "tel:\(digits)")
|
|
},
|
|
displayValueFormatter: formatPhoneForDisplay
|
|
)
|
|
|
|
static let email = ContactFieldType(
|
|
id: "email",
|
|
displayName: String(localized: "Email"),
|
|
systemImage: "envelope.fill",
|
|
iconColor: Color(red: 0.2, green: 0.2, blue: 0.2),
|
|
category: .contact,
|
|
valueLabel: String(localized: "Email"),
|
|
valuePlaceholder: "you@example.com",
|
|
titleSuggestions: [String(localized: "Work"), String(localized: "Personal")],
|
|
keyboardType: .emailAddress,
|
|
urlBuilder: { URL(string: "mailto:\($0)") }
|
|
)
|
|
|
|
static let website = ContactFieldType(
|
|
id: "website",
|
|
displayName: String(localized: "Company Website"),
|
|
systemImage: "globe",
|
|
iconColor: Color(red: 0.2, green: 0.2, blue: 0.2),
|
|
category: .contact,
|
|
valueLabel: String(localized: "Website URL"),
|
|
valuePlaceholder: "https://company.com",
|
|
titleSuggestions: [String(localized: "Company Website"), String(localized: "Portfolio")],
|
|
keyboardType: .URL,
|
|
urlBuilder: { buildWebURL($0) }
|
|
)
|
|
|
|
static let address = ContactFieldType(
|
|
id: "address",
|
|
displayName: String(localized: "Address"),
|
|
systemImage: "location.fill",
|
|
iconColor: Color(red: 0.2, green: 0.2, blue: 0.2),
|
|
category: .contact,
|
|
valueLabel: String(localized: "Address"),
|
|
valuePlaceholder: "123 Main St, City, State",
|
|
titleSuggestions: [String(localized: "Work"), String(localized: "Home")],
|
|
keyboardType: .default,
|
|
urlBuilder: { value in
|
|
// Try to parse as PostalAddress JSON first for proper formatting
|
|
let searchQuery: String
|
|
if let address = PostalAddress.decode(from: value) {
|
|
searchQuery = address.singleLineString
|
|
} else {
|
|
searchQuery = value
|
|
}
|
|
let encoded = searchQuery.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? searchQuery
|
|
return URL(string: "maps://?q=\(encoded)")
|
|
},
|
|
displayValueFormatter: formatAddressForDisplay
|
|
)
|
|
|
|
// MARK: - Social Media
|
|
|
|
static let linkedIn = ContactFieldType(
|
|
id: "linkedIn",
|
|
displayName: "LinkedIn",
|
|
systemImage: "linkedin",
|
|
isCustomSymbol: true,
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .social,
|
|
valueLabel: String(localized: "Username/Link"),
|
|
valuePlaceholder: "linkedin.com/in/username",
|
|
titleSuggestions: ["Connect with me on LinkedIn"],
|
|
keyboardType: .URL,
|
|
urlBuilder: { buildSocialURL($0, webBase: "linkedin.com") }
|
|
)
|
|
|
|
static let twitter = ContactFieldType(
|
|
id: "twitter",
|
|
displayName: "X",
|
|
systemImage: "x-twitter",
|
|
isCustomSymbol: true,
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .social,
|
|
valueLabel: String(localized: "Username/Link"),
|
|
valuePlaceholder: "x.com/username",
|
|
titleSuggestions: ["Follow me on X"],
|
|
keyboardType: .URL,
|
|
urlBuilder: { buildSocialURL($0, webBase: "x.com") }
|
|
)
|
|
|
|
static let instagram = ContactFieldType(
|
|
id: "instagram",
|
|
displayName: "Instagram",
|
|
systemImage: "instagram",
|
|
isCustomSymbol: true,
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .social,
|
|
valueLabel: String(localized: "Username/Link"),
|
|
valuePlaceholder: "instagram.com/username",
|
|
titleSuggestions: ["Follow me on Instagram"],
|
|
keyboardType: .URL,
|
|
urlBuilder: { buildSocialURL($0, webBase: "instagram.com") }
|
|
)
|
|
|
|
static let facebook = ContactFieldType(
|
|
id: "facebook",
|
|
displayName: "Facebook",
|
|
systemImage: "facebook",
|
|
isCustomSymbol: true,
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .social,
|
|
valueLabel: String(localized: "Username/Link"),
|
|
valuePlaceholder: "facebook.com/username",
|
|
titleSuggestions: ["Connect on Facebook"],
|
|
keyboardType: .URL,
|
|
urlBuilder: { buildSocialURL($0, webBase: "facebook.com") }
|
|
)
|
|
|
|
static let tiktok = ContactFieldType(
|
|
id: "tiktok",
|
|
displayName: "TikTok",
|
|
systemImage: "tiktok",
|
|
isCustomSymbol: true,
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .social,
|
|
valueLabel: String(localized: "Username/Link"),
|
|
valuePlaceholder: "tiktok.com/@username",
|
|
titleSuggestions: ["Follow me on TikTok"],
|
|
keyboardType: .URL,
|
|
urlBuilder: { buildSocialURL($0, webBase: "tiktok.com") }
|
|
)
|
|
|
|
static let threads = ContactFieldType(
|
|
id: "threads",
|
|
displayName: "Threads",
|
|
systemImage: "threads",
|
|
isCustomSymbol: true,
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .social,
|
|
valueLabel: String(localized: "Username/Link"),
|
|
valuePlaceholder: "threads.net/@username",
|
|
titleSuggestions: ["Follow me on Threads"],
|
|
keyboardType: .URL,
|
|
urlBuilder: { buildSocialURL($0, webBase: "threads.net") }
|
|
)
|
|
|
|
static let youtube = ContactFieldType(
|
|
id: "youtube",
|
|
displayName: "YouTube",
|
|
systemImage: "youtube",
|
|
isCustomSymbol: true,
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .social,
|
|
valueLabel: String(localized: "Username/Link"),
|
|
valuePlaceholder: "youtube.com/@channel",
|
|
titleSuggestions: ["Subscribe to my channel"],
|
|
keyboardType: .URL,
|
|
urlBuilder: { buildSocialURL($0, webBase: "youtube.com") }
|
|
)
|
|
|
|
static let snapchat = ContactFieldType(
|
|
id: "snapchat",
|
|
displayName: "Snapchat",
|
|
systemImage: "camera.fill",
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .social,
|
|
valueLabel: String(localized: "Username/Link"),
|
|
valuePlaceholder: "snapchat.com/add/username",
|
|
titleSuggestions: ["Add me on Snapchat"],
|
|
keyboardType: .URL,
|
|
urlBuilder: { buildSocialURL($0, webBase: "snapchat.com") }
|
|
)
|
|
|
|
static let pinterest = ContactFieldType(
|
|
id: "pinterest",
|
|
displayName: "Pinterest",
|
|
systemImage: "pin.fill",
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .social,
|
|
valueLabel: String(localized: "Username/Link"),
|
|
valuePlaceholder: "pinterest.com/username",
|
|
titleSuggestions: ["Follow me on Pinterest"],
|
|
keyboardType: .URL,
|
|
urlBuilder: { buildSocialURL($0, webBase: "pinterest.com") }
|
|
)
|
|
|
|
static let twitch = ContactFieldType(
|
|
id: "twitch",
|
|
displayName: "Twitch",
|
|
systemImage: "twitch",
|
|
isCustomSymbol: true,
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .social,
|
|
valueLabel: String(localized: "Username/Link"),
|
|
valuePlaceholder: "twitch.tv/username",
|
|
titleSuggestions: ["Watch me on Twitch"],
|
|
keyboardType: .URL,
|
|
urlBuilder: { buildSocialURL($0, webBase: "twitch.tv") }
|
|
)
|
|
|
|
static let bluesky = ContactFieldType(
|
|
id: "bluesky",
|
|
displayName: "Bluesky",
|
|
systemImage: "bluesky",
|
|
isCustomSymbol: true,
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .social,
|
|
valueLabel: String(localized: "Username/Link"),
|
|
valuePlaceholder: "bsky.app/profile/username",
|
|
titleSuggestions: ["Follow me on Bluesky"],
|
|
keyboardType: .URL,
|
|
urlBuilder: { buildSocialURL($0, webBase: "bsky.app") }
|
|
)
|
|
|
|
static let mastodon = ContactFieldType(
|
|
id: "mastodon",
|
|
displayName: "Mastodon",
|
|
systemImage: "mastodon",
|
|
isCustomSymbol: true,
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .social,
|
|
valueLabel: String(localized: "Username/Link"),
|
|
valuePlaceholder: "mastodon.social/@username",
|
|
titleSuggestions: ["Follow me on Mastodon"],
|
|
keyboardType: .URL,
|
|
urlBuilder: { value in
|
|
if value.hasPrefix("http://") || value.hasPrefix("https://") {
|
|
return URL(string: value)
|
|
}
|
|
if value.contains(".") {
|
|
return URL(string: "https://\(value)")
|
|
}
|
|
let username = value.hasPrefix("@") ? String(value.dropFirst()) : value
|
|
return URL(string: "https://mastodon.social/@\(username)")
|
|
}
|
|
)
|
|
|
|
static let reddit = ContactFieldType(
|
|
id: "reddit",
|
|
displayName: "Reddit",
|
|
systemImage: "reddit",
|
|
isCustomSymbol: true,
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .social,
|
|
valueLabel: String(localized: "Username/Link"),
|
|
valuePlaceholder: "reddit.com/user/username",
|
|
titleSuggestions: ["Follow me on Reddit"],
|
|
keyboardType: .URL,
|
|
urlBuilder: { buildSocialURL($0, webBase: "reddit.com") }
|
|
)
|
|
|
|
// MARK: - Developer
|
|
|
|
static let github = ContactFieldType(
|
|
id: "github",
|
|
displayName: "GitHub",
|
|
systemImage: "github.fill",
|
|
isCustomSymbol: true,
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .developer,
|
|
valueLabel: String(localized: "Username/Link"),
|
|
valuePlaceholder: "github.com/username",
|
|
titleSuggestions: ["View our work on GitHub", "View our GitHub Repo"],
|
|
keyboardType: .URL,
|
|
urlBuilder: { buildSocialURL($0, webBase: "github.com") }
|
|
)
|
|
|
|
static let gitlab = ContactFieldType(
|
|
id: "gitlab",
|
|
displayName: "GitLab",
|
|
systemImage: "chevron.left.forwardslash.chevron.right",
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .developer,
|
|
valueLabel: String(localized: "Username/Link"),
|
|
valuePlaceholder: "gitlab.com/username",
|
|
titleSuggestions: ["View our work on GitLab"],
|
|
keyboardType: .URL,
|
|
urlBuilder: { buildSocialURL($0, webBase: "gitlab.com") }
|
|
)
|
|
|
|
static let stackoverflow = ContactFieldType(
|
|
id: "stackoverflow",
|
|
displayName: "Stack Overflow",
|
|
systemImage: "text.bubble.fill",
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .developer,
|
|
valueLabel: String(localized: "Username/Link"),
|
|
valuePlaceholder: "stackoverflow.com/users/id",
|
|
titleSuggestions: ["Ask me on Stack Overflow"],
|
|
keyboardType: .URL,
|
|
urlBuilder: { buildSocialURL($0, webBase: "stackoverflow.com") }
|
|
)
|
|
|
|
// MARK: - Messaging
|
|
|
|
static let telegram = ContactFieldType(
|
|
id: "telegram",
|
|
displayName: "Telegram",
|
|
systemImage: "telegram",
|
|
isCustomSymbol: true,
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .messaging,
|
|
valueLabel: String(localized: "Username/Link"),
|
|
valuePlaceholder: "t.me/username",
|
|
titleSuggestions: ["Connect with me on Telegram"],
|
|
keyboardType: .URL,
|
|
urlBuilder: { value in
|
|
if value.hasPrefix("t.me/") || value.hasPrefix("https://t.me/") {
|
|
return URL(string: value.hasPrefix("https://") ? value : "https://\(value)")
|
|
}
|
|
return URL(string: "tg://resolve?domain=\(value)")
|
|
}
|
|
)
|
|
|
|
static let whatsapp = ContactFieldType(
|
|
id: "whatsapp",
|
|
displayName: "WhatsApp",
|
|
systemImage: "message.fill",
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .messaging,
|
|
valueLabel: String(localized: "Username/Link"),
|
|
valuePlaceholder: "+1 555 123 4567",
|
|
titleSuggestions: ["Message me on WhatsApp"],
|
|
keyboardType: .phonePad,
|
|
urlBuilder: { value in
|
|
let digits = value.filter { $0.isNumber }
|
|
return URL(string: "https://wa.me/\(digits)")
|
|
}
|
|
)
|
|
|
|
static let signal = ContactFieldType(
|
|
id: "signal",
|
|
displayName: "Signal",
|
|
systemImage: "bubble.left.fill",
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .messaging,
|
|
valueLabel: String(localized: "Username/Link"),
|
|
valuePlaceholder: "+1 555 123 4567",
|
|
titleSuggestions: ["Message me on Signal"],
|
|
keyboardType: .phonePad,
|
|
urlBuilder: { value in
|
|
let digits = value.filter { $0.isNumber || $0 == "+" }
|
|
return URL(string: "sgnl://signal.me/#p/\(digits)")
|
|
}
|
|
)
|
|
|
|
static let discord = ContactFieldType(
|
|
id: "discord",
|
|
displayName: "Discord",
|
|
systemImage: "discord",
|
|
isCustomSymbol: true,
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .messaging,
|
|
valueLabel: String(localized: "Username/Link"),
|
|
valuePlaceholder: "discord.gg/invite",
|
|
titleSuggestions: ["Join my Discord"],
|
|
keyboardType: .URL,
|
|
urlBuilder: { buildSocialURL($0, webBase: "discord.gg") }
|
|
)
|
|
|
|
static let slack = ContactFieldType(
|
|
id: "slack",
|
|
displayName: "Slack",
|
|
systemImage: "slack",
|
|
isCustomSymbol: true,
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .messaging,
|
|
valueLabel: String(localized: "Username/Link"),
|
|
valuePlaceholder: "yourworkspace.slack.com",
|
|
titleSuggestions: ["Join our Slack"],
|
|
keyboardType: .URL,
|
|
urlBuilder: { buildSocialURL($0, webBase: "slack.com") }
|
|
)
|
|
|
|
static let matrix = ContactFieldType(
|
|
id: "matrix",
|
|
displayName: "Matrix",
|
|
systemImage: "matrix",
|
|
isCustomSymbol: true,
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .messaging,
|
|
valueLabel: String(localized: "Username/Link"),
|
|
valuePlaceholder: "@username:matrix.org",
|
|
titleSuggestions: ["Chat with me on Matrix"],
|
|
keyboardType: .URL,
|
|
urlBuilder: { value in
|
|
if value.contains("matrix.to") || value.contains("element.io") {
|
|
return URL(string: value.hasPrefix("https://") ? value : "https://\(value)")
|
|
}
|
|
let encoded = value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? value
|
|
return URL(string: "https://matrix.to/#/\(encoded)")
|
|
}
|
|
)
|
|
|
|
// MARK: - Payment
|
|
|
|
static let venmo = ContactFieldType(
|
|
id: "venmo",
|
|
displayName: "Venmo",
|
|
systemImage: "dollarsign.circle.fill",
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .payment,
|
|
valueLabel: String(localized: "Username"),
|
|
valuePlaceholder: "@username",
|
|
titleSuggestions: ["Pay via Venmo"],
|
|
keyboardType: .default,
|
|
urlBuilder: { value in
|
|
let username = value.hasPrefix("@") ? String(value.dropFirst()) : value
|
|
return URL(string: "venmo://users/\(username)")
|
|
}
|
|
)
|
|
|
|
static let cashApp = ContactFieldType(
|
|
id: "cashApp",
|
|
displayName: "Cash App",
|
|
systemImage: "dollarsign.square.fill",
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .payment,
|
|
valueLabel: String(localized: "Username"),
|
|
valuePlaceholder: "$cashtag",
|
|
titleSuggestions: ["Pay via Cash App"],
|
|
keyboardType: .default,
|
|
urlBuilder: { value in
|
|
let cashtag = value.hasPrefix("$") ? String(value.dropFirst()) : value
|
|
return URL(string: "cashapp://cash.app/$\(cashtag)")
|
|
}
|
|
)
|
|
|
|
static let paypal = ContactFieldType(
|
|
id: "paypal",
|
|
displayName: "PayPal",
|
|
systemImage: "creditcard.fill",
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .payment,
|
|
valueLabel: String(localized: "Email or Username"),
|
|
valuePlaceholder: "paypal.me/username",
|
|
titleSuggestions: ["Pay via PayPal"],
|
|
keyboardType: .emailAddress,
|
|
urlBuilder: { URL(string: "https://paypal.me/\($0)") }
|
|
)
|
|
|
|
static let zelle = ContactFieldType(
|
|
id: "zelle",
|
|
displayName: "Zelle",
|
|
systemImage: "dollarsign.arrow.circlepath",
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .payment,
|
|
valueLabel: String(localized: "Phone or Email"),
|
|
valuePlaceholder: "email@example.com",
|
|
titleSuggestions: ["Pay via Zelle"],
|
|
keyboardType: .phonePad,
|
|
urlBuilder: { _ in nil } // Zelle has no universal deep link
|
|
)
|
|
|
|
// MARK: - Creator/Funding
|
|
|
|
static let patreon = ContactFieldType(
|
|
id: "patreon",
|
|
displayName: "Patreon",
|
|
systemImage: "patreon.fill",
|
|
isCustomSymbol: true,
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .creator,
|
|
valueLabel: String(localized: "Profile Link"),
|
|
valuePlaceholder: "patreon.com/username",
|
|
titleSuggestions: ["Support me on Patreon"],
|
|
keyboardType: .URL,
|
|
urlBuilder: { buildSocialURL($0, webBase: "patreon.com") }
|
|
)
|
|
|
|
static let kofi = ContactFieldType(
|
|
id: "kofi",
|
|
displayName: "Ko-fi",
|
|
systemImage: "ko-fi",
|
|
isCustomSymbol: true,
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .creator,
|
|
valueLabel: String(localized: "Profile Link"),
|
|
valuePlaceholder: "ko-fi.com/username",
|
|
titleSuggestions: ["Buy me a coffee"],
|
|
keyboardType: .URL,
|
|
urlBuilder: { buildSocialURL($0, webBase: "ko-fi.com") }
|
|
)
|
|
|
|
// MARK: - Scheduling
|
|
|
|
static let calendly = ContactFieldType(
|
|
id: "calendly",
|
|
displayName: "Calendly",
|
|
systemImage: "calendar",
|
|
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0),
|
|
category: .scheduling,
|
|
valueLabel: String(localized: "Calendly Link"),
|
|
valuePlaceholder: "calendly.com/username",
|
|
titleSuggestions: ["Schedule a meeting"],
|
|
keyboardType: .URL,
|
|
urlBuilder: { buildWebURL($0) }
|
|
)
|
|
|
|
// MARK: - Other
|
|
|
|
static let customLink = ContactFieldType(
|
|
id: "customLink",
|
|
displayName: String(localized: "Link"),
|
|
systemImage: "link",
|
|
iconColor: Color(red: 0.2, green: 0.2, blue: 0.2),
|
|
category: .other,
|
|
valueLabel: String(localized: "URL"),
|
|
valuePlaceholder: "https://example.com",
|
|
titleSuggestions: [],
|
|
keyboardType: .URL,
|
|
urlBuilder: { buildWebURL($0) }
|
|
)
|
|
}
|
|
|
|
// MARK: - Display Value Formatters
|
|
|
|
/// Formats an address for multi-line display
|
|
/// Tries to parse as structured PostalAddress JSON first, falls back to legacy format
|
|
nonisolated private func formatAddressForDisplay(_ value: String) -> String {
|
|
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !trimmed.isEmpty else { return value }
|
|
|
|
// Parse as structured PostalAddress
|
|
if let address = PostalAddress.decode(from: trimmed), address.hasValue {
|
|
return address.formattedString
|
|
}
|
|
|
|
// Not a valid address format
|
|
return trimmed
|
|
}
|
|
|
|
/// Formats a phone number for display with lightweight US-first grouping.
|
|
/// Falls back to a spaced international style for non-US lengths.
|
|
nonisolated private func formatPhoneForDisplay(_ value: String) -> String {
|
|
PhoneNumberText.formatted(value)
|
|
}
|
|
|
|
// MARK: - URL Helper Functions
|
|
|
|
nonisolated private func buildWebURL(_ value: String) -> URL? {
|
|
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !trimmed.isEmpty else { return nil }
|
|
|
|
if trimmed.hasPrefix("http://") || trimmed.hasPrefix("https://") {
|
|
return URL(string: trimmed)
|
|
}
|
|
return URL(string: "https://\(trimmed)")
|
|
}
|
|
|
|
nonisolated private func buildSocialURL(_ value: String, webBase: String) -> URL? {
|
|
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !trimmed.isEmpty else { return nil }
|
|
|
|
// If already a full URL, use it
|
|
if trimmed.hasPrefix("http://") || trimmed.hasPrefix("https://") {
|
|
return URL(string: trimmed)
|
|
}
|
|
|
|
// If contains the base domain, add https
|
|
if trimmed.contains(webBase) {
|
|
return URL(string: "https://\(trimmed)")
|
|
}
|
|
|
|
// Otherwise, treat as username and build URL
|
|
let username = trimmed.hasPrefix("@") ? String(trimmed.dropFirst()) : trimmed
|
|
return URL(string: "https://\(webBase)/\(username)")
|
|
}
|
|
|
|
// MARK: - Phone Text Utilities
|