Created
February 19, 2025 15:42
-
-
Save mireabot/335fedde45e52f6d81f9e420a13b39f4 to your computer and use it in GitHub Desktop.
Plan Purchase Cards
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
struct EventPurchasePlan: Identifiable, Hashable { | |
let id = UUID() | |
let price: Double | |
let type: EventPlanType | |
let discount: Double | |
let description: String | |
} | |
enum EventPlanType: CaseIterable { | |
case regular | |
case earlyBird | |
case vip | |
var name: String { | |
switch self { | |
case .regular: | |
return "Regular" | |
case .earlyBird: | |
return "Early Bird" | |
case .vip: | |
return "VIP" | |
} | |
} | |
var icon: Image { | |
switch self { | |
case .regular: | |
return Image(systemName: "tag.fill") | |
case .earlyBird: | |
return Image(systemName: "sunrise.fill") | |
case .vip: | |
return Image(systemName: "star.fill") | |
} | |
} | |
var color: Color { | |
switch self { | |
case .regular: | |
return Color.blue | |
case .earlyBird: | |
return Color.orange | |
case .vip: | |
return Color.yellow | |
} | |
} | |
} | |
struct EventPurchasePage: View { | |
@Environment(\.dismiss) var dismiss | |
let purchasePlans: [EventPurchasePlan] = [ | |
EventPurchasePlan(price: 25.00, type: .regular, discount: 20.00, description: "Standard admission after 11 PM"), | |
EventPurchasePlan(price: 50.00, type: .earlyBird, discount: 0.0, description: "Entry before 11 PM"), | |
EventPurchasePlan(price: 85.50, type: .vip, discount: 0.0, description: "Table service, priority entry, 4 person minimum") | |
] | |
var body: some View { | |
ScrollView(.vertical, showsIndicators: false) { | |
VStack(spacing: 12) { | |
ForEach(purchasePlans) { package in | |
EventPlanCard(package: package).padding(.horizontal, 10) | |
} | |
} | |
.padding(.vertical, 16) | |
} | |
.navigationBarBackButtonHidden() | |
.navigationBarTitleDisplayMode(.inline) | |
.toolbar(content: { | |
ToolbarItem(placement: .topBarLeading) { | |
Button(action: { | |
dismiss() | |
}, label: { | |
Image(systemName: "chevron.left") | |
.font(.system(.headline, weight: .semibold)) | |
.foregroundColor(.gray) | |
}) | |
} | |
ToolbarItem(placement: .topBarLeading) { | |
Text("Select plan and amount") | |
.font(.system(.title2, weight: .medium)) | |
.foregroundStyle(.primary) | |
} | |
}) | |
.preferredColorScheme(.dark) | |
} | |
} | |
// MARK: - Plan Card | |
struct EventPlanCard: View { | |
let package: EventPurchasePlan | |
@State private var isAdded = false | |
var body: some View { | |
VStack(alignment: .leading, spacing: 0) { | |
// Main card content | |
VStack(alignment: .leading, spacing: 16) { | |
HStack(spacing: 8) { | |
package.type.icon | |
.foregroundColor(package.type.color) | |
Text(package.type.name) | |
.textCase(.uppercase) | |
.foregroundColor(.gray) | |
} | |
.font(.system(.footnote, weight: .medium)) | |
VStack(alignment: .leading, spacing: 8) { | |
HStack(alignment: .top) { | |
Text("\(Text("$").foregroundStyle(.gray))\(package.price - package.price * (package.discount / 100), specifier: "%.0f")") | |
.font(.system(.title, weight: .medium)) | |
.foregroundColor(.white) | |
if package.discount > 0 { | |
Text("$\(package.price, specifier: "%.0f")") | |
.font(.system(.headline, weight: .medium)) | |
.foregroundColor(.gray) | |
.strikethrough() | |
} | |
} | |
Text(package.description) | |
.font(.system(.subheadline, weight: .regular)) | |
.foregroundColor(.gray) | |
Button(action: { | |
DispatchQueue.main.async { | |
withAnimation(.smooth(duration: 0.42)) { | |
self.isAdded.toggle() | |
} | |
} | |
}) { | |
HStack { | |
Image(systemName: "plus") | |
.foregroundStyle(isAdded ? .green : .white) | |
.transition(.scale) | |
Text(isAdded ? "In cart" : "Add to cart") | |
.foregroundStyle(.white) | |
} | |
.font(.system(.subheadline, weight: .regular)) | |
.padding(.horizontal, 16) | |
.padding(.vertical, 10) | |
.background(Color.black) | |
.clipShape(Capsule()) | |
} | |
.padding(.top, 10) | |
} | |
} | |
.padding(16) | |
// Bottom banner | |
if package.discount > 0 { | |
HStack { | |
Image(systemName: "percent") | |
Text("\(package.discount, specifier: "%.0f")% OFF • ENDS IN 2 HOURS") | |
} | |
.font(.system(.footnote, weight: .bold)) | |
.foregroundColor(.green) | |
.frame(maxWidth: .infinity, alignment: .leading) | |
.padding() | |
.background( | |
Color.green.opacity(0.15) | |
.overlay( | |
StripedPattern() | |
.clipShape(RoundedRectangle(cornerRadius: 12)) | |
) | |
) | |
} | |
} | |
.frame(maxWidth: .infinity, alignment: .leading) | |
.background(Color(uiColor: .systemGray6)) | |
.cornerRadius(12) | |
.preferredColorScheme(.dark) | |
} | |
} | |
// MARK: - Custom pattern for discount banner | |
struct StripedPattern: View { | |
var body: some View { | |
GeometryReader { geo in | |
Path { path in | |
let stripeWidth: CGFloat = 8 | |
let spacing: CGFloat = 16 | |
let height = geo.size.height | |
for x in stride(from: 0, through: geo.size.width, by: spacing) { | |
path.move(to: CGPoint(x: x, y: 0)) | |
path.addLine(to: CGPoint(x: x + stripeWidth, y: height)) | |
} | |
} | |
.stroke(Color.green.opacity(0.3), lineWidth: 1) | |
} | |
} | |
} | |
#Preview("Event plan card", body: { | |
VStack { | |
EventPlanCard(package: EventPurchasePlan(price: 49.99, type: .regular, discount: 50.00, description: "Standard admission after 11 PM")) | |
EventPlanCard(package: EventPurchasePlan(price: 49.99, type: .earlyBird, discount: 0, description: "Standard admission after 11 PM")) | |
} | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment