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: responsive design

+107 -48
+2 -2
wasup-chucks.xcodeproj/project.pbxproj
··· 379 379 DEVELOPMENT_TEAM = M67B42LX8D; 380 380 ENABLE_PREVIEWS = YES; 381 381 GENERATE_INFOPLIST_FILE = YES; 382 - INFOPLIST_KEY_CFBundleDisplayName = "Wasup Chucks"; 382 + INFOPLIST_KEY_CFBundleDisplayName = "Wasup Chuck's"; 383 383 INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.food-and-drink"; 384 384 INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 385 385 INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; ··· 414 414 DEVELOPMENT_TEAM = M67B42LX8D; 415 415 ENABLE_PREVIEWS = YES; 416 416 GENERATE_INFOPLIST_FILE = YES; 417 - INFOPLIST_KEY_CFBundleDisplayName = "Wasup Chucks"; 417 + INFOPLIST_KEY_CFBundleDisplayName = "Wasup Chuck's"; 418 418 INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.food-and-drink"; 419 419 INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 420 420 INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+105 -46
wasup-chucks/ContentView.swift
··· 15 15 @State private var todayMenu: [VenueMenu] = [] 16 16 @State private var isLoading = true 17 17 @State private var selectedMeal: MealSchedule? = nil 18 - 18 + @Environment(\.horizontalSizeClass) private var horizontalSizeClass 19 + 19 20 let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() 20 21 21 22 var currentSlot: String { ··· 27 28 return "lunch" 28 29 } 29 30 31 + private var isRegularWidth: Bool { 32 + horizontalSizeClass == .regular 33 + } 34 + 30 35 var body: some View { 31 36 NavigationStack { 32 37 ScrollView { 33 38 VStack(spacing: 16) { 34 - StatusCard(status: status) 35 - 36 - ScheduleCard(status: status, todayMenu: todayMenu, selectedMeal: $selectedMeal) 37 - 39 + if isRegularWidth { 40 + // iPad: Two-column layout for top cards 41 + HStack(spacing: 16) { 42 + StatusCard(status: status) 43 + .frame(maxHeight: .infinity, alignment: .top) 44 + ScheduleCard(status: status, todayMenu: todayMenu, selectedMeal: $selectedMeal) 45 + .frame(maxHeight: .infinity, alignment: .top) 46 + } 47 + .fixedSize(horizontal: false, vertical: true) 48 + } else { 49 + // iPhone: Stacked layout 50 + StatusCard(status: status) 51 + ScheduleCard(status: status, todayMenu: todayMenu, selectedMeal: $selectedMeal) 52 + } 53 + 38 54 if isLoading { 39 55 ProgressView() 40 56 .frame(maxWidth: .infinity, minHeight: 200) 41 57 } else { 42 - CurrentMealView(menu: todayMenu, slot: currentSlot, isOpen: status.isOpen) 58 + CurrentMealView(menu: todayMenu, slot: currentSlot, isOpen: status.isOpen, isRegularWidth: isRegularWidth) 43 59 } 44 60 } 45 61 .padding(.horizontal, 16) 46 62 .padding(.bottom, 16) 63 + .frame(maxWidth: isRegularWidth ? 900 : .infinity) 64 + .frame(maxWidth: .infinity) 47 65 } 48 66 .navigationTitle("Wasup Chucks") 49 67 .onReceive(timer) { _ in ··· 120 138 } 121 139 } 122 140 .padding(16) 123 - .frame(maxWidth: .infinity) 141 + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) 124 142 .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16)) 125 143 .shadow(color: .black.opacity(0.08), radius: 8, x: 0, y: 2) 126 144 } ··· 154 172 } 155 173 } 156 174 .padding(16) 157 - .frame(maxWidth: .infinity, alignment: .leading) 175 + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) 158 176 .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16)) 159 177 .shadow(color: .black.opacity(0.08), radius: 8, x: 0, y: 2) 160 178 } ··· 269 287 let menu: [VenueMenu] 270 288 let slot: String 271 289 let isOpen: Bool 272 - 290 + let isRegularWidth: Bool 291 + 273 292 var mealSpecificVenues: [VenueMenu] { 274 293 menu.filter { $0.slot == slot } 275 294 .sorted { $0.venue < $1.venue } 276 295 } 277 - 296 + 278 297 var alwaysAvailableVenues: [VenueMenu] { 279 298 menu.filter { $0.slot == "anytime" } 280 299 .sorted { $0.venue < $1.venue } 281 300 } 282 - 301 + 283 302 var mealLabel: String { 284 303 switch slot { 285 304 case "breakfast": return "Breakfast" ··· 288 307 default: return "This Meal" 289 308 } 290 309 } 291 - 310 + 292 311 var body: some View { 293 312 VStack(alignment: .leading, spacing: 16) { 313 + // Meal Specials Section 294 314 if !mealSpecificVenues.isEmpty { 295 315 VStack(alignment: .leading, spacing: 12) { 296 316 Label("\(mealLabel) Specials", systemImage: "clock.fill") 297 317 .font(.headline) 298 318 .foregroundStyle(.primary) 299 - 300 - ForEach(mealSpecificVenues) { venue in 301 - VenueSection(venue: venue, isHighlighted: venue.venue == "Home Cooking") 319 + .padding(.horizontal, 4) 320 + 321 + MasonryLayout(mealSpecificVenues, columns: isRegularWidth ? 2 : 1, spacing: 12) { venue in 322 + VenueCard(venue: venue) 302 323 } 303 324 } 304 - .padding(16) 305 - .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16)) 306 - .shadow(color: .black.opacity(0.08), radius: 8, x: 0, y: 2) 325 + } 326 + 327 + // Always Available Section 328 + if !alwaysAvailableVenues.isEmpty { 329 + VStack(alignment: .leading, spacing: 12) { 330 + if !mealSpecificVenues.isEmpty { 331 + HStack(spacing: 12) { 332 + Rectangle() 333 + .fill(.secondary.opacity(0.3)) 334 + .frame(height: 1) 335 + Text("Always Available") 336 + .font(.caption.weight(.medium)) 337 + .foregroundStyle(.secondary) 338 + Rectangle() 339 + .fill(.secondary.opacity(0.3)) 340 + .frame(height: 1) 341 + } 342 + .padding(.top, 8) 343 + } 344 + 345 + MasonryLayout(alwaysAvailableVenues, columns: isRegularWidth ? 2 : 1, spacing: 12) { venue in 346 + VenueCard(venue: venue) 347 + } 348 + } 307 349 } 308 - 309 - if !mealSpecificVenues.isEmpty && !alwaysAvailableVenues.isEmpty { 310 - HStack(spacing: 12) { 311 - Rectangle() 312 - .fill(.secondary.opacity(0.3)) 313 - .frame(height: 1) 314 - Text("Always Available") 315 - .font(.caption.weight(.medium)) 316 - .foregroundStyle(.secondary) 317 - Rectangle() 318 - .fill(.secondary.opacity(0.3)) 319 - .frame(height: 1) 350 + } 351 + } 352 + } 353 + 354 + // MARK: - Masonry Layout 355 + 356 + struct MasonryLayout<Data: RandomAccessCollection, Content: View>: View where Data.Element: Identifiable { 357 + let data: Data 358 + let columns: Int 359 + let spacing: CGFloat 360 + let content: (Data.Element) -> Content 361 + 362 + init(_ data: Data, columns: Int = 2, spacing: CGFloat = 12, @ViewBuilder content: @escaping (Data.Element) -> Content) { 363 + self.data = data 364 + self.columns = columns 365 + self.spacing = spacing 366 + self.content = content 367 + } 368 + 369 + private func itemsForColumn(_ column: Int) -> [Data.Element] { 370 + data.enumerated().compactMap { index, item in 371 + index % columns == column ? item : nil 372 + } 373 + } 374 + 375 + var body: some View { 376 + if columns == 1 { 377 + VStack(spacing: spacing) { 378 + ForEach(data) { item in 379 + content(item) 320 380 } 321 - .padding(.vertical, 8) 322 381 } 323 - 324 - if !alwaysAvailableVenues.isEmpty { 325 - VStack(alignment: .leading, spacing: 12) { 326 - ForEach(alwaysAvailableVenues) { venue in 327 - VenueSection(venue: venue, isHighlighted: false) 382 + } else { 383 + HStack(alignment: .top, spacing: spacing) { 384 + ForEach(0..<columns, id: \.self) { column in 385 + VStack(spacing: spacing) { 386 + ForEach(itemsForColumn(column)) { item in 387 + content(item) 388 + } 328 389 } 329 390 } 330 - .padding(16) 331 - .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16)) 332 - .shadow(color: .black.opacity(0.08), radius: 8, x: 0, y: 2) 333 391 } 334 392 } 335 393 } 336 394 } 337 395 338 - struct VenueSection: View { 396 + struct VenueCard: View { 339 397 let venue: VenueMenu 340 - let isHighlighted: Bool 341 398 @State private var isExpanded = true 342 - 399 + 343 400 var body: some View { 344 401 DisclosureGroup(isExpanded: $isExpanded) { 345 402 VStack(alignment: .leading, spacing: 8) { ··· 359 416 } 360 417 .padding(.top, 8) 361 418 } label: { 362 - HStack(spacing: 6) { 363 - Text(venue.venue) 364 - .font(.subheadline.weight(.semibold)) 365 - } 419 + Text(venue.venue) 420 + .font(.subheadline.weight(.semibold)) 366 421 } 367 422 .sensoryFeedback(.selection, trigger: isExpanded) 423 + .padding(16) 424 + .frame(maxWidth: .infinity, alignment: .leading) 425 + .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12)) 426 + .shadow(color: .black.opacity(0.06), radius: 4, x: 0, y: 2) 368 427 } 369 428 } 370 429