diff --git a/VoiceInk/Views/Metrics/DashboardPromotionsSection.swift b/VoiceInk/Views/Metrics/DashboardPromotionsSection.swift new file mode 100644 index 0000000..fa15e43 --- /dev/null +++ b/VoiceInk/Views/Metrics/DashboardPromotionsSection.swift @@ -0,0 +1,172 @@ +import SwiftUI +import AppKit + +struct DashboardPromotionsSection: View { + let licenseState: LicenseViewModel.LicenseState + + private var shouldShowUpgradePromotion: Bool { + guard case .trial(let daysRemaining) = licenseState else { return false } + return daysRemaining <= 9 + } + + private var shouldShowAffiliatePromotion: Bool { + if case .licensed = licenseState { + return true + } + return false + } + + private var shouldShowPromotions: Bool { + shouldShowUpgradePromotion || shouldShowAffiliatePromotion + } + + var body: some View { + if shouldShowPromotions { + HStack(alignment: .top, spacing: 18) { + if shouldShowUpgradePromotion { + DashboardPromotionCard( + badge: "30% OFF", + title: "Share VoiceInk, Save 30%", + message: "Tell your audience about VoiceInk on social and unlock a 30% discount on VoiceInk Pro when they upgrade.", + gradient: LinearGradient( + colors: [ + Color(red: 0.08, green: 0.48, blue: 0.85), + Color(red: 0.05, green: 0.18, blue: 0.42) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + accentSymbol: "megaphone.fill", + glowColor: Color(red: 0.08, green: 0.48, blue: 0.85), + actionTitle: "Share & Unlock", + actionIcon: "arrow.up.right", + action: openSocialShare + ) + .frame(maxWidth: .infinity) + } + + if shouldShowAffiliatePromotion { + DashboardPromotionCard( + badge: "AFFILIATE 30%", + title: "Earn With The VoiceInk Affiliate Program", + message: "Share VoiceInk with friends or your audience and receive 30% on every referral that upgrades.", + gradient: LinearGradient( + colors: [ + Color(red: 0.08, green: 0.48, blue: 0.85), + Color(red: 0.05, green: 0.18, blue: 0.42) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + accentSymbol: "link.badge.plus", + glowColor: Color(red: 0.08, green: 0.48, blue: 0.85), + actionTitle: "Explore Affiliate", + actionIcon: "arrow.up.right", + action: openAffiliateProgram + ) + .frame(maxWidth: .infinity) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } else { + EmptyView() + } + } + + private func openSocialShare() { + if let url = URL(string: "https://tryvoiceink.com/social-share") { + NSWorkspace.shared.open(url) + } + } + + private func openAffiliateProgram() { + if let url = URL(string: "https://tryvoiceink.com/affiliate") { + NSWorkspace.shared.open(url) + } + } +} + +private struct DashboardPromotionCard: View { + let badge: String + let title: String + let message: String + let gradient: LinearGradient + let accentSymbol: String + let glowColor: Color + let actionTitle: String + let actionIcon: String + let action: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 18) { + HStack(alignment: .top) { + Text(badge.uppercased()) + .font(.system(size: 11, weight: .heavy)) + .tracking(0.8) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(.white.opacity(0.2)) + .clipShape(Capsule()) + .foregroundColor(.white) + + Spacer() + + Image(systemName: accentSymbol) + .font(.system(size: 20, weight: .bold)) + .foregroundColor(.white.opacity(0.85)) + .padding(10) + .background(.white.opacity(0.18)) + .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) + } + + Text(title) + .font(.system(size: 21, weight: .heavy, design: .rounded)) + .foregroundColor(.white) + .fixedSize(horizontal: false, vertical: true) + + Text(message) + .font(.system(size: 13.5, weight: .medium)) + .foregroundColor(.white.opacity(0.85)) + .fixedSize(horizontal: false, vertical: true) + + Button(action: action) { + HStack(spacing: 6) { + Text(actionTitle) + Image(systemName: actionIcon) + } + .font(.system(size: 13, weight: .semibold)) + .padding(.horizontal, 16) + .padding(.vertical, 9) + .background(.white.opacity(0.22)) + .clipShape(Capsule()) + .foregroundColor(.white) + } + .buttonStyle(.plain) + } + .padding(24) + .frame(maxWidth: .infinity, minHeight: 200, alignment: .topLeading) + .background( + RoundedRectangle(cornerRadius: 28, style: .continuous) + .fill(gradient) + .overlay { + ZStack { + Circle() + .fill(.white.opacity(0.12)) + .frame(width: 140, height: 140) + .offset(x: 60, y: -60) + Circle() + .strokeBorder(.white.opacity(0.15), lineWidth: 1) + .frame(width: 170, height: 170) + .offset(x: -40, y: 70) + } + .clipped() + } + ) + .clipShape(RoundedRectangle(cornerRadius: 28, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: 28, style: .continuous) + .stroke(.white.opacity(0.08), lineWidth: 1) + ) + .shadow(color: glowColor.opacity(0.28), radius: 24, x: 0, y: 14) + } +} diff --git a/VoiceInk/Views/Metrics/MetricsContent.swift b/VoiceInk/Views/Metrics/MetricsContent.swift index 65e3497..81d776e 100644 --- a/VoiceInk/Views/Metrics/MetricsContent.swift +++ b/VoiceInk/Views/Metrics/MetricsContent.swift @@ -12,6 +12,7 @@ struct MetricsContent: View { VStack(spacing: 24) { heroSection metricsSection + DashboardPromotionsSection() } .padding(.vertical, 28) .padding(.horizontal, 32) @@ -281,4 +282,4 @@ private struct CopySystemInfoButton: View { } } } -} \ No newline at end of file +}