Added Promotional Cards
This commit is contained in:
parent
367da05feb
commit
5cd64a1247
172
VoiceInk/Views/Metrics/DashboardPromotionsSection.swift
Normal file
172
VoiceInk/Views/Metrics/DashboardPromotionsSection.swift
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,6 +12,7 @@ struct MetricsContent: View {
|
|||||||
VStack(spacing: 24) {
|
VStack(spacing: 24) {
|
||||||
heroSection
|
heroSection
|
||||||
metricsSection
|
metricsSection
|
||||||
|
DashboardPromotionsSection()
|
||||||
}
|
}
|
||||||
.padding(.vertical, 28)
|
.padding(.vertical, 28)
|
||||||
.padding(.horizontal, 32)
|
.padding(.horizontal, 32)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user