summaryrefslogtreecommitdiff
path: root/NumberCard.swift
blob: 9ae1c391c48efe2a1872f0731447cd3983d2f07f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
//
//  NumberCard.swift
//  DayByDay
//

import SwiftUI

/// A single large, colorful card representing one number (1–10).
/// Tapping the card plays a bounce animation and speaks the number name aloud.
struct NumberCard: View {
    let number: LearnNumber
    var isHighlighted = false
    @State private var isTapped = false

    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 32, style: .continuous)
                .fill(number.color.gradient)
                .shadow(color: number.color.opacity(0.4), radius: 8, y: 4)

            VStack(spacing: 10) {
                Text("\(number.numeral)")
                    .font(.system(size: 60, weight: .heavy, design: .rounded))
                    .foregroundStyle(.white)

                Text(number.name)
                    .font(.system(size: 28, weight: .bold, design: .rounded))
                    .foregroundStyle(.white)

                // Dot pattern showing the quantity
                DotPattern(count: number.numeral)
                    .padding(.top, 4)
            }
        }
        .overlay {
            if isHighlighted {
                RoundedRectangle(cornerRadius: 32, style: .continuous)
                    .strokeBorder(.white, lineWidth: 5)
            }
        }
        .shadow(color: isHighlighted ? .white.opacity(0.8) : .clear, radius: 12)
        .scaleEffect(isTapped ? 1.12 : (isHighlighted ? 1.04 : 1.0))
        .contentShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
        .rotation3DEffect(.degrees(isTapped ? 6 : 0), axis: (x: 1, y: 0, z: 0))
        .animation(.spring(response: 0.35, dampingFraction: 0.5), value: isTapped)
        .onTapGesture {
            isTapped = true
            SpeechSynthesizer.shared.speak(number.name)
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
                isTapped = false
            }
        }
        .accessibilityLabel(number.name)
        .accessibilityAddTraits(.isButton)
    }
}

// MARK: - Number model

enum LearnNumber: Int, CaseIterable, Identifiable {
    case one = 1, two, three, four, five, six, seven, eight, nine, ten

    var id: Int { rawValue }

    /// Highlight the number matching today's day-of-month if it falls in 1–10.
    static var current: LearnNumber? {
        let day = Calendar.current.component(.day, from: Date())
        return LearnNumber(rawValue: day)
    }

    var numeral: Int { rawValue }

    var name: String {
        switch self {
        case .one:   "One"
        case .two:   "Two"
        case .three: "Three"
        case .four:  "Four"
        case .five:  "Five"
        case .six:   "Six"
        case .seven: "Seven"
        case .eight: "Eight"
        case .nine:  "Nine"
        case .ten:   "Ten"
        }
    }

    var color: Color {
        switch self {
        case .one:   Color(red: 0.9, green: 0.3, blue: 0.3)   // red
        case .two:   Color(red: 1.0, green: 0.55, blue: 0.2)  // orange
        case .three: Color(red: 1.0, green: 0.8, blue: 0.2)   // yellow
        case .four:  Color(red: 0.35, green: 0.75, blue: 0.4)  // green
        case .five:  Color(red: 0.2, green: 0.7, blue: 0.85)   // sky blue
        case .six:   Color(red: 0.4, green: 0.45, blue: 0.9)   // blue
        case .seven: Color(red: 0.6, green: 0.4, blue: 0.85)   // purple
        case .eight: Color(red: 0.85, green: 0.35, blue: 0.6)  // pink
        case .nine:  Color(red: 0.5, green: 0.75, blue: 0.45)  // lime
        case .ten:   Color(red: 0.25, green: 0.8, blue: 0.75)  // teal
        }
    }
}

// MARK: - Dot pattern

/// Displays a grid of filled circles representing a quantity.
/// 1–5 dots use centered rows; 6–10 use a 5-column grid that wraps naturally.
struct DotPattern: View {
    let count: Int

    private var columns: [GridItem] {
        let maxCols = count <= 5 ? min(count, 5) : 5
        return Array(repeating: GridItem(.fixed(14), spacing: 6), count: maxCols)
    }

    var body: some View {
        LazyVGrid(columns: columns, spacing: 6) {
            ForEach(0..<count, id: \.self) { _ in
                Circle()
                    .fill(.white.opacity(0.8))
                    .frame(width: 14, height: 14)
            }
        }
        .frame(maxWidth: .infinity)
    }
}

#Preview {
    HStack(spacing: 24) {
        NumberCard(number: .three, isHighlighted: true)
            .frame(width: 280, height: 300)
        NumberCard(number: .seven)
            .frame(width: 280, height: 300)
    }
    .padding()
}