ios widget showing what is available at chucks
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: use tabs for scrolling

+149 -97
+149 -97
ios/wasup-chucks/ContentView.swift
··· 40 40 allMenus.keys.sorted() 41 41 } 42 42 43 - var isViewingToday: Bool { 44 - selectedDateIndex == 0 45 - } 46 - 47 - var selectedDateMenu: [VenueMenu] { 48 - guard selectedDateIndex < availableDates.count else { return [] } 49 - return allMenus[availableDates[selectedDateIndex]] ?? [] 50 - } 51 - 52 - var selectedDateSchedule: [MealSchedule] { 53 - guard selectedDateIndex < availableDates.count else { return [] } 54 - let dateKey = availableDates[selectedDateIndex] 55 - let formatter = DateFormatter() 56 - formatter.dateFormat = "yyyy-MM-dd" 57 - formatter.timeZone = TimeZone(identifier: "America/New_York") 58 - guard let date = formatter.date(from: dateKey) else { return [] } 59 - let weekday = CedarvilleTime.calendar.component(.weekday, from: date) 60 - return MealSchedule.schedule(for: weekday) 61 - } 62 - 63 - var futureMealVenues: [VenueMenu] { 64 - selectedDateMenu.filter { $0.slot == selectedFutureMeal.apiSlot } 65 - .sorted { $0.venue < $1.venue } 66 - } 67 - 68 - var futureAlwaysAvailable: [VenueMenu] { 69 - selectedDateMenu.filter { $0.slot == "anytime" } 70 - .sorted { $0.venue < $1.venue } 71 - } 72 - 73 43 var body: some View { 74 44 NavigationStack { 75 - ScrollView { 76 - VStack(spacing: 16) { 77 - if availableDates.count > 1 { 45 + Group { 46 + if availableDates.count > 1 { 47 + TabView(selection: $selectedDateIndex) { 48 + ForEach(availableDates.indices, id: \.self) { index in 49 + dayPage(for: index) 50 + .tag(index) 51 + } 52 + } 53 + .tabViewStyle(.page(indexDisplayMode: .never)) 54 + .onChange(of: selectedDateIndex) { _ in 55 + selectedFutureMeal = .breakfast 56 + } 57 + .safeAreaInset(edge: .top, spacing: 0) { 78 58 DateNavigationHeader( 79 59 selectedDateIndex: $selectedDateIndex, 80 60 selectedFutureMeal: $selectedFutureMeal, 81 61 availableDates: availableDates 82 62 ) 63 + .background(.bar) 83 64 } 84 - 85 - if isViewingToday { 86 - // Today's view 87 - if isRegularWidth { 88 - HStack(spacing: 16) { 89 - StatusCard(status: status) 90 - .frame(maxHeight: .infinity, alignment: .top) 91 - ScheduleCard(status: status, todayMenu: todayMenu, selectedMeal: $selectedMeal) 92 - .frame(maxHeight: .infinity, alignment: .top) 93 - } 94 - .fixedSize(horizontal: false, vertical: true) 95 - } else { 96 - StatusCard(status: status) 97 - ScheduleCard(status: status, todayMenu: todayMenu, selectedMeal: $selectedMeal) 98 - } 99 - 100 - if isLoading { 101 - ProgressView() 102 - .frame(maxWidth: .infinity, minHeight: 200) 103 - } else if let error = loadError { 104 - ErrorCard(error: error) { 105 - Task { await loadMenu() } 106 - } 107 - } else { 108 - CurrentMealView(menu: todayMenu, slot: currentSlot, isOpen: status.isOpen, isRegularWidth: isRegularWidth) 109 - } 110 - } else { 111 - // Future day view 112 - ScheduleCard( 113 - schedule: selectedDateSchedule, 114 - selectedMealPhase: $selectedFutureMeal 115 - ) 116 - 117 - if isLoading { 118 - ProgressView() 119 - .frame(maxWidth: .infinity, minHeight: 200) 120 - } else if let error = loadError { 121 - ErrorCard(error: error) { 122 - Task { await loadMenu() } 123 - } 124 - } else { 125 - CurrentMealView(menu: selectedDateMenu, slot: selectedFutureMeal.apiSlot, isOpen: true, isRegularWidth: isRegularWidth) 126 - } 127 - } 128 - 129 - // Footer 130 - VStack(spacing: 4) { 131 - Text("Made with \u{2665} by Kieran Klukas") 132 - .font(.caption2) 133 - .foregroundStyle(.tertiary) 134 - Link("Privacy Policy", destination: URL(string: "https://dunkirk.sh/wasup-chucks/")!) 135 - .font(.caption2) 136 - .foregroundStyle(.tertiary) 137 - } 138 - .padding(.top, 16) 65 + } else { 66 + dayPage(for: 0) 139 67 } 140 - .padding(.horizontal, 16) 141 - .padding(.bottom, 16) 142 - .frame(maxWidth: isRegularWidth ? 900 : .infinity) 143 - .frame(maxWidth: .infinity) 144 68 } 145 69 .navigationTitle("Wasup Chuck's") 146 70 .onReceive(timer) { _ in ··· 149 73 .task { 150 74 await loadMenu() 151 75 } 152 - .refreshable { 153 - selectedDateIndex = 0 154 - await ChucksService.shared.invalidateCache() 155 - await loadMenu() 156 - } 157 76 .sheet(item: $selectedMeal) { meal in 158 77 MealDetailSheet(meal: meal, menu: todayMenu) 159 78 } 160 79 } 161 80 } 162 81 82 + @ViewBuilder 83 + func dayPage(for index: Int) -> some View { 84 + ScrollView { 85 + VStack(spacing: 16) { 86 + if index == 0 { 87 + TodayContent( 88 + status: status, 89 + todayMenu: todayMenu, 90 + selectedMeal: $selectedMeal, 91 + currentSlot: currentSlot, 92 + isLoading: isLoading, 93 + loadError: loadError, 94 + isRegularWidth: isRegularWidth, 95 + onRetry: { Task { await loadMenu() } } 96 + ) 97 + } else { 98 + FutureDayContent( 99 + schedule: scheduleForDate(at: index), 100 + selectedFutureMeal: $selectedFutureMeal, 101 + menu: menuForDate(at: index), 102 + isLoading: isLoading, 103 + loadError: loadError, 104 + isRegularWidth: isRegularWidth, 105 + onRetry: { Task { await loadMenu() } } 106 + ) 107 + } 108 + 109 + // Footer 110 + VStack(spacing: 4) { 111 + Text("Made with \u{2665} by Kieran Klukas") 112 + .font(.caption2) 113 + .foregroundStyle(.tertiary) 114 + Link("Privacy Policy", destination: URL(string: "https://dunkirk.sh/wasup-chucks/")!) 115 + .font(.caption2) 116 + .foregroundStyle(.tertiary) 117 + } 118 + .padding(.top, 16) 119 + } 120 + .padding(.horizontal, 16) 121 + .padding(.bottom, 16) 122 + .frame(maxWidth: isRegularWidth ? 900 : .infinity) 123 + .frame(maxWidth: .infinity) 124 + } 125 + .refreshable { 126 + selectedDateIndex = 0 127 + await ChucksService.shared.invalidateCache() 128 + await loadMenu() 129 + } 130 + } 131 + 132 + func menuForDate(at index: Int) -> [VenueMenu] { 133 + guard index < availableDates.count else { return [] } 134 + return allMenus[availableDates[index]] ?? [] 135 + } 136 + 137 + func scheduleForDate(at index: Int) -> [MealSchedule] { 138 + guard index < availableDates.count else { return [] } 139 + let dateKey = availableDates[index] 140 + let formatter = DateFormatter() 141 + formatter.dateFormat = "yyyy-MM-dd" 142 + formatter.timeZone = TimeZone(identifier: "America/New_York") 143 + guard let date = formatter.date(from: dateKey) else { return [] } 144 + let weekday = CedarvilleTime.calendar.component(.weekday, from: date) 145 + return MealSchedule.schedule(for: weekday) 146 + } 147 + 163 148 func loadMenu() async { 164 149 isLoading = true 165 150 loadError = nil ··· 176 161 print("Failed to load menu: \(error)") 177 162 } 178 163 isLoading = false 164 + } 165 + } 166 + 167 + // MARK: - Page Content Views 168 + 169 + private struct TodayContent: View { 170 + let status: ChucksStatus 171 + let todayMenu: [VenueMenu] 172 + @Binding var selectedMeal: MealSchedule? 173 + let currentSlot: String 174 + let isLoading: Bool 175 + let loadError: Error? 176 + let isRegularWidth: Bool 177 + let onRetry: () -> Void 178 + 179 + var body: some View { 180 + Group { 181 + if isRegularWidth { 182 + HStack(spacing: 16) { 183 + StatusCard(status: status) 184 + .frame(maxHeight: .infinity, alignment: .top) 185 + ScheduleCard(status: status, todayMenu: todayMenu, selectedMeal: $selectedMeal) 186 + .frame(maxHeight: .infinity, alignment: .top) 187 + } 188 + .fixedSize(horizontal: false, vertical: true) 189 + } else { 190 + StatusCard(status: status) 191 + ScheduleCard(status: status, todayMenu: todayMenu, selectedMeal: $selectedMeal) 192 + } 193 + 194 + if isLoading { 195 + ProgressView() 196 + .frame(maxWidth: .infinity, minHeight: 200) 197 + } else if let error = loadError { 198 + ErrorCard(error: error, retry: onRetry) 199 + } else { 200 + CurrentMealView(menu: todayMenu, slot: currentSlot, isOpen: status.isOpen, isRegularWidth: isRegularWidth) 201 + } 202 + } 203 + } 204 + } 205 + 206 + private struct FutureDayContent: View { 207 + let schedule: [MealSchedule] 208 + @Binding var selectedFutureMeal: MealPhase 209 + let menu: [VenueMenu] 210 + let isLoading: Bool 211 + let loadError: Error? 212 + let isRegularWidth: Bool 213 + let onRetry: () -> Void 214 + 215 + var body: some View { 216 + Group { 217 + ScheduleCard( 218 + schedule: schedule, 219 + selectedMealPhase: $selectedFutureMeal 220 + ) 221 + 222 + if isLoading { 223 + ProgressView() 224 + .frame(maxWidth: .infinity, minHeight: 200) 225 + } else if let error = loadError { 226 + ErrorCard(error: error, retry: onRetry) 227 + } else { 228 + CurrentMealView(menu: menu, slot: selectedFutureMeal.apiSlot, isOpen: true, isRegularWidth: isRegularWidth) 229 + } 230 + } 179 231 } 180 232 } 181 233