Skip to content

Instantly share code, notes, and snippets.

@mireabot
Created February 19, 2025 15:42
Show Gist options
  • Save mireabot/335fedde45e52f6d81f9e420a13b39f4 to your computer and use it in GitHub Desktop.
Save mireabot/335fedde45e52f6d81f9e420a13b39f4 to your computer and use it in GitHub Desktop.
Plan Purchase Cards
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