development
swift 254 lines 9.14 KB
Raw
1 import SwiftUI
2
3 private struct WatchGuideStore {
4 let entries: [GuideEntry] = WatchContentLoader().loadEntries()
5
6 var featuredEntries: [GuideEntry] {
7 Array(entries.filter(\.isFeatured).prefix(3))
8 }
9
10 var browseEntries: [GuideEntry] {
11 let featuredIDs = Set(featuredEntries.map(\.id))
12 let remaining = entries.filter { !featuredIDs.contains($0.id) }
13 return Array((featuredEntries + remaining).prefix(6))
14 }
15 }
16
17 struct WatchHomeView: View {
18 private let store = WatchGuideStore()
19
20 var body: some View {
21 NavigationStack {
22 ScrollView {
23 LazyVStack(alignment: .leading, spacing: 14) {
24 introCard
25
26 if store.entries.isEmpty {
27 emptyStateCard
28 } else {
29 if !store.featuredEntries.isEmpty {
30 WatchKleponSectionHeader(
31 "Start here",
32 subtitle: "A warm first taste of the guide"
33 )
34
35 ForEach(store.featuredEntries) { entry in
36 NavigationLink {
37 WatchEntryDetailView(entry: entry)
38 } label: {
39 WatchEntryCard(entry: entry)
40 }
41 .buttonStyle(.plain)
42 }
43 }
44
45 WatchKleponSectionHeader(
46 "Browse",
47 subtitle: "A few essentials that feel good on the wrist"
48 )
49
50 ForEach(store.browseEntries) { entry in
51 NavigationLink {
52 WatchEntryDetailView(entry: entry)
53 } label: {
54 WatchEntryCard(entry: entry)
55 }
56 .buttonStyle(.plain)
57 }
58
59 WatchKleponFooterCard(
60 message:
61 "Browse on watch. Use the iPhone app for the fuller guide and private follow-up answers."
62 )
63 }
64 }
65 .padding(.horizontal, 12)
66 .padding(.vertical, 10)
67 }
68 .background(KleponColor.background.ignoresSafeArea())
69 .navigationTitle("Klepon")
70 }
71 }
72
73 private var introCard: some View {
74 WatchKleponCard(padding: 16) {
75 VStack(alignment: .leading, spacing: 10) {
76 Text("Taste of Indonesia")
77 .font(WatchKleponTypography.heroTitle)
78 .foregroundStyle(KleponColor.textPrimary)
79
80 Text(
81 "A calmer way to browse dishes, flavors, and food traditions from your wrist."
82 )
83 .font(WatchKleponTypography.body)
84 .foregroundStyle(KleponColor.textSecondary)
85 .lineSpacing(2)
86
87 HStack(spacing: 8) {
88 WatchKleponChip(title: "Curated")
89 WatchKleponChip(title: "Private", icon: "lock.fill")
90 }
91 }
92 }
93 }
94
95 private var emptyStateCard: some View {
96 WatchKleponCard {
97 VStack(alignment: .leading, spacing: 10) {
98 Image(systemName: "fork.knife.circle.fill")
99 .font(.system(size: 22, weight: .semibold))
100 .foregroundStyle(KleponColor.accent)
101
102 Text("Guide unavailable")
103 .font(WatchKleponTypography.cardTitle)
104 .foregroundStyle(KleponColor.textPrimary)
105
106 Text(
107 "Open Klepon on iPhone first if you want the fuller guide and private follow-up answers."
108 )
109 .font(WatchKleponTypography.bodySecondary)
110 .foregroundStyle(KleponColor.textSecondary)
111 .lineSpacing(2)
112 }
113 }
114 }
115 }
116
117 private struct WatchEntryCard: View {
118 let entry: GuideEntry
119
120 var body: some View {
121 WatchKleponCard {
122 VStack(alignment: .leading, spacing: 10) {
123 HStack(alignment: .top, spacing: 10) {
124 VStack(alignment: .leading, spacing: 6) {
125 Text(entry.title)
126 .font(WatchKleponTypography.cardTitle)
127 .foregroundStyle(KleponColor.textPrimary)
128 .lineLimit(2)
129
130 Text(entry.subtitle)
131 .font(WatchKleponTypography.bodySecondary)
132 .foregroundStyle(KleponColor.textSecondary)
133 .lineLimit(2)
134 }
135
136 Spacer(minLength: 0)
137
138 Image(systemName: entry.kind.symbolName)
139 .font(.system(size: 12, weight: .semibold))
140 .foregroundStyle(KleponColor.accentWarm)
141 .padding(8)
142 .background(Circle().fill(KleponColor.surfaceSecondary))
143 }
144
145 HStack(spacing: 8) {
146 WatchKleponChip(title: entry.kind.displayTitle, icon: entry.kind.symbolName)
147
148 if let region = entry.region {
149 Text(region)
150 .font(WatchKleponTypography.caption)
151 .foregroundStyle(KleponColor.accentWarm)
152 .lineLimit(1)
153 }
154 }
155 }
156 }
157 }
158 }
159
160 private struct WatchEntryDetailView: View {
161 let entry: GuideEntry
162
163 private var tasteItems: [String] {
164 Array(entry.tasteNotes.prefix(4))
165 }
166
167 private var highlightItems: [String] {
168 Array(entry.highlights.prefix(4))
169 }
170
171 var body: some View {
172 ScrollView {
173 LazyVStack(alignment: .leading, spacing: 12) {
174 WatchKleponCard(padding: 16) {
175 VStack(alignment: .leading, spacing: 10) {
176 Text(entry.title)
177 .font(WatchKleponTypography.heroTitle)
178 .foregroundStyle(KleponColor.textPrimary)
179
180 Text(entry.subtitle)
181 .font(WatchKleponTypography.body)
182 .foregroundStyle(KleponColor.textSecondary)
183 .lineSpacing(2)
184
185 HStack(spacing: 8) {
186 WatchKleponChip(
187 title: entry.kind.displayTitle,
188 icon: entry.kind.symbolName
189 )
190
191 if let region = entry.region {
192 Text(region)
193 .font(WatchKleponTypography.caption)
194 .foregroundStyle(KleponColor.accentWarm)
195 .lineLimit(1)
196 }
197 }
198 }
199 }
200
201 WatchKleponCard {
202 Text(entry.summary)
203 .font(WatchKleponTypography.body)
204 .foregroundStyle(KleponColor.textSecondary)
205 .lineSpacing(2)
206 }
207
208 if !tasteItems.isEmpty {
209 WatchKleponCard {
210 VStack(alignment: .leading, spacing: 10) {
211 WatchKleponSectionHeader("Taste")
212 WatchKleponChipGrid(items: tasteItems)
213 }
214 }
215 }
216
217 if !highlightItems.isEmpty {
218 WatchKleponCard {
219 VStack(alignment: .leading, spacing: 10) {
220 WatchKleponSectionHeader("Quick notes")
221
222 ForEach(highlightItems, id: \.self) { note in
223 HStack(alignment: .top, spacing: 8) {
224 Circle()
225 .fill(KleponColor.accentWarm)
226 .frame(width: 5, height: 5)
227 .padding(.top, 5)
228
229 Text(note)
230 .font(WatchKleponTypography.bodySecondary)
231 .foregroundStyle(KleponColor.textSecondary)
232 .lineSpacing(2)
233 }
234 }
235 }
236 }
237 }
238
239 WatchKleponFooterCard(
240 message:
241 "Open Klepon on iPhone if you want the fuller guide and on-device follow-up answers."
242 )
243 }
244 .padding(.horizontal, 12)
245 .padding(.vertical, 10)
246 }
247 .background(KleponColor.background.ignoresSafeArea())
248 .navigationTitle(entry.title)
249 }
250 }
251
252 #Preview {
253 WatchHomeView()
254 }