| @@ -6,26 +6,68 @@ struct TourPicker: View { |
| 6 | var onPick: (Tour) -> Void |
6 | var onPick: (Tour) -> Void |
| 7 | @Environment(\.dismiss) private var dismiss |
7 | @Environment(\.dismiss) private var dismiss |
| 8 | |
8 | |
| |
9 | private let ink = Color(red: 0.95, green: 0.95, blue: 0.93) |
| |
10 | private let accent = Color(red: 0.88, green: 0.24, blue: 0.19) |
| |
11 | private let month = Calendar.current.component(.month, from: .now) |
| |
12 | |
| 9 | var body: some View { |
13 | var body: some View { |
| 10 | NavigationStack { |
14 | NavigationStack { |
| 11 | List(Tour.ordered(month: Calendar.current.component(.month, from: .now))) { tour in |
15 | ScrollView { |
| 12 | Button { |
16 | VStack(alignment: .leading, spacing: 0) { |
| 13 | onPick(tour) |
17 | Text("Guided tours").font(.title.weight(.medium)) |
| 14 | dismiss() |
18 | Rectangle().fill(accent).frame(width: 48, height: 5).padding(.top, 8).padding(.bottom, 12) |
| 15 | } label: { |
19 | Text("A walk around the evening sky, one sight at a time. The view turns to each stop and marks what to look for. Best outside after dark, but they work anywhere.") |
| 16 | VStack(alignment: .leading, spacing: 4) { |
20 | .font(.subheadline).foregroundStyle(ink.opacity(0.75)) |
| 17 | Text(tour.title).font(.body.weight(.medium)) |
21 | .padding(.bottom, 20) |
| 18 | Text(tour.intro).font(.footnote).foregroundStyle(.secondary) |
22 | ForEach(Tour.ordered(month: month)) { tour in card(tour) } |
| 19 | Text("\(tour.steps.count) stops").font(.caption).foregroundStyle(.secondary) |
| |
| 20 | } |
| |
| 21 | .padding(.vertical, 4) |
| |
| 22 | } |
23 | } |
| 23 | .buttonStyle(.plain) |
24 | .padding(20) |
| |
25 | .readableWidth() |
| 24 | } |
26 | } |
| 25 | .navigationTitle("Guided tours") |
27 | .background(Color.black.ignoresSafeArea()) |
| 26 | .navigationBarTitleDisplayMode(.inline) |
| |
| 27 | .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } } |
28 | .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } } |
| 28 | } |
29 | } |
| |
30 | .foregroundStyle(ink) |
| |
31 | .tint(accent) |
| |
32 | } |
| |
33 | |
| |
34 | private func card(_ tour: Tour) -> some View { |
| |
35 | let now = tour.months.contains(month) |
| |
36 | return Button { |
| |
37 | onPick(tour) |
| |
38 | dismiss() |
| |
39 | } label: { |
| |
40 | VStack(alignment: .leading, spacing: 10) { |
| |
41 | HStack { |
| |
42 | if now { |
| |
43 | Text("THIS MONTH").font(.caption2.weight(.bold)).tracking(0.8) |
| |
44 | .padding(.horizontal, 6).padding(.vertical, 2) |
| |
45 | .background(accent, in: RoundedRectangle(cornerRadius: 3)) |
| |
46 | } |
| |
47 | Text(tour.title).font(.caption.weight(.medium)).foregroundStyle(ink.opacity(0.7)) |
| |
48 | Spacer() |
| |
49 | Text("\(tour.steps.count) stops").font(.caption).foregroundStyle(ink.opacity(0.6)) |
| |
50 | } |
| |
51 | Text(tour.steps.prefix(4).map(\.title).joined(separator: " · ") + (tour.steps.count > 4 ? " …" : "")) |
| |
52 | .font(.title3.weight(.medium)) |
| |
53 | .multilineTextAlignment(.leading) |
| |
54 | Text(tour.intro).font(.footnote).foregroundStyle(ink.opacity(0.75)).multilineTextAlignment(.leading) |
| |
55 | HStack(spacing: 6) { |
| |
56 | Text("Start tour").font(.subheadline.weight(.semibold)) |
| |
57 | Image(systemName: "arrow.right").font(.footnote.weight(.semibold)) |
| |
58 | } |
| |
59 | .foregroundStyle(now ? accent : ink.opacity(0.8)) |
| |
60 | .padding(.top, 2) |
| |
61 | } |
| |
62 | .padding(18) |
| |
63 | .frame(maxWidth: .infinity, alignment: .leading) |
| |
64 | .background(ink.opacity(now ? 0.09 : 0.05), in: RoundedRectangle(cornerRadius: 18)) |
| |
65 | .overlay(RoundedRectangle(cornerRadius: 18).stroke(now ? accent.opacity(0.6) : ink.opacity(0.1), lineWidth: now ? 1 : 0.5)) |
| |
66 | .contentShape(RoundedRectangle(cornerRadius: 18)) |
| |
67 | } |
| |
68 | .buttonStyle(.plain) |
| |
69 | .padding(.bottom, 14) |
| |
70 | .accessibilityHint("Starts the tour") |
| 29 | } |
71 | } |
| 30 | } |
72 | } |
| 31 | |
73 | |