krz/daybyday

iOS app for children to learn days of the week, months, and seasons with audio assist.

clone: git clone https://gitbay.org/krz/daybyday.git

main: DayByDay/MonthCard.swift · raw

  1//
  2//  MonthCard.swift
  3//  DayByDay
  4//
  5
  6import SwiftUI
  7
  8/// A single large, colorful card representing one month of the year.
  9/// Tapping the card plays a bounce animation and speaks the month name aloud.
 10struct MonthCard: View {
 11    let month: MonthOfYear
 12    var isHighlighted = false
 13    @State private var isTapped = false
 14
 15    private var cardScale: CGFloat {
 16        if isTapped { return 1.12 }
 17        if isHighlighted { return 1.04 }
 18        return 1.0
 19    }
 20
 21    var body: some View {
 22        ZStack {
 23            RoundedRectangle(cornerRadius: 32, style: .continuous)
 24                .fill(month.color.gradient)
 25                .shadow(color: month.color.opacity(0.4), radius: 8, y: 4)
 26
 27            VStack(spacing: 16) {
 28                Image(systemName: month.symbol)
 29                    .font(.system(size: 64))
 30                    .foregroundStyle(.white)
 31                    .symbolRenderingMode(.hierarchical)
 32
 33                Text(month.name)
 34                    .font(.system(size: 32, weight: .bold, design: .rounded))
 35                    .foregroundStyle(.white)
 36            }
 37        }
 38        .overlay {
 39            if isHighlighted {
 40                RoundedRectangle(cornerRadius: 32, style: .continuous)
 41                    .strokeBorder(.white, lineWidth: 5)
 42            }
 43        }
 44        .shadow(color: isHighlighted ? .white.opacity(0.8) : .clear, radius: 12)
 45        .scaleEffect(cardScale)
 46        .contentShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
 47        .rotation3DEffect(.degrees(isTapped ? 6 : 0), axis: (x: 1, y: 0, z: 0))
 48        .animation(.spring(response: 0.35, dampingFraction: 0.5), value: isTapped)
 49        .onTapGesture {
 50            isTapped = true
 51            SpeechSynthesizer.shared.speak(month.name)
 52            DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
 53                isTapped = false
 54            }
 55        }
 56        .accessibilityLabel(month.name)
 57        .accessibilityAddTraits(.isButton)
 58    }
 59}
 60
 61// MARK: - Month model
 62
 63enum MonthOfYear: Int, CaseIterable, Identifiable {
 64    case january, february, march, april, may, june,
 65         july, august, september, october, november, december
 66
 67    var id: Int { rawValue }
 68
 69    /// The month for today's date.
 70    static var current: MonthOfYear {
 71        // Calendar month: 1 = January, 12 = December
 72        let month = Calendar.current.component(.month, from: Date())
 73        return MonthOfYear(rawValue: month - 1)!
 74    }
 75
 76    var name: String {
 77        switch self {
 78        case .january:   "January"
 79        case .february:  "February"
 80        case .march:     "March"
 81        case .april:     "April"
 82        case .may:       "May"
 83        case .june:      "June"
 84        case .july:      "July"
 85        case .august:    "August"
 86        case .september: "September"
 87        case .october:   "October"
 88        case .november:  "November"
 89        case .december:  "December"
 90        }
 91    }
 92
 93    var symbol: String {
 94        switch self {
 95        case .january:   "snowflake"
 96        case .february:  "heart.fill"
 97        case .march:     "wind"
 98        case .april:     "cloud.rain.fill"
 99        case .may:       "leaf.fill"
100        case .june:      "sun.max.fill"
101        case .july:      "fireworks"
102        case .august:    "beach.umbrella.fill"
103        case .september: "book.fill"
104        case .october:   "moon.stars.fill"
105        case .november:  "fork.knife"
106        case .december:  "gift.fill"
107        }
108    }
109
110    var color: Color {
111        switch self {
112        case .january:   Color(red: 0.55, green: 0.75, blue: 0.95) // ice blue
113        case .february:  Color(red: 0.9, green: 0.35, blue: 0.5)   // pink
114        case .march:     Color(red: 0.45, green: 0.8, blue: 0.55)  // spring green
115        case .april:     Color(red: 0.5, green: 0.65, blue: 0.9)   // rain blue
116        case .may:       Color(red: 0.95, green: 0.7, blue: 0.2)   // golden
117        case .june:      Color(red: 1.0, green: 0.55, blue: 0.25)  // warm orange
118        case .july:      Color(red: 0.85, green: 0.25, blue: 0.3)  // red
119        case .august:    Color(red: 0.2, green: 0.75, blue: 0.8)   // ocean teal
120        case .september: Color(red: 0.6, green: 0.45, blue: 0.85)  // purple
121        case .october:   Color(red: 0.9, green: 0.5, blue: 0.15)   // pumpkin
122        case .november:  Color(red: 0.7, green: 0.5, blue: 0.3)    // brown
123        case .december:  Color(red: 0.3, green: 0.5, blue: 0.85)   // winter blue
124        }
125    }
126}
127
128#Preview {
129    HStack(spacing: 24) {
130        MonthCard(month: .march, isHighlighted: true)
131            .frame(width: 200, height: 260)
132        MonthCard(month: .april)
133            .frame(width: 200, height: 260)
134    }
135    .padding()
136}